2.3 配置系统与日志观察窗 配置决定管线"按什么参数跑",日志告诉你管线"实际怎么跑了"。两者都由主机在启动时装载:配置是多来源分层的键值仓库,日志是带级别与分类的观察窗。本节把这两条观察通道接进观察哨。 前两节管线的骨架和血液都有了,本节补上神经与仪表盘。配置和日志放一节讲,是因为它们共享同一个装载时机(主机启动)和同一个设计哲学(分层、可组合、按环境切换)。 配置:多来源分层合并 ASP.NET Core 的配置不是"读一个文件",而是把多个来源按顺序叠成一张键值表,后加载的覆盖先加载的。默认链条大致是:appsettings 文件、环境专属文件(如 Development 版)、用户机密(开发期)、环境变量、命令行参数。
配置决定管线"按什么参数跑",日志告诉你管线"实际怎么跑了"。两者都由主机在启动时装载:配置是多来源分层的键值仓库,日志是带级别与分类的观察窗。本节把这两条观察通道接进观察哨。
前两节管线的骨架和血液都有了,本节补上神经与仪表盘。配置和日志放一节讲,是因为它们共享同一个装载时机(主机启动)和同一个设计哲学(分层、可组合、按环境切换)。
ASP.NET Core 的配置不是"读一个文件",而是把多个来源按顺序叠成一张键值表,后加载的覆盖先加载的。默认链条大致是:appsettings 文件、环境专属文件(如 Development 版)、用户机密(开发期)、环境变量、命令行参数。环境变量和命令行排最后,意味着生产环境不改文件就能覆盖任意配置项——这是容器化部署的基石。

动手实验覆盖效果。准备主配置文件里的一项:
// 配置文件节选:观察哨参数 { "Obs": { "MaxSize": 10, "Tag": "from-file" } }
用强类型选项模式读取,并用环境变量覆盖其中一项:
// 1. 定义选项类,属性名与配置键对应 public class ObsOptions { public int MaxSize { get; set; } public string Tag { get; set; } = ""; } var builder = WebApplication.CreateBuilder(args); // 2. 绑定配置节到选项类 builder.Services.Configure<ObsOptions>(builder.Configuration.GetSection("Obs")); var app = builder.Build(); app.MapGet("/opt", (Microsoft.Extensions.Options.IOptions<ObsOptions> opt) => $"MaxSize={opt.Value.MaxSize}, Tag={opt.Value.Tag}"); app.Run();
正常运行输出 MaxSize=10, Tag=from-file。设置环境变量后重启:
# 双下划线表示层级:Obs 节下的 Tag 项 set Obs__Tag=from-env dotnet run # GET /opt 输出:MaxSize=10, Tag=from-env # 文件里的值被环境变量覆盖,其余键不受影响
IOptions 是一次性快照。需要每请求读到最新值用 IOptionsSnapshot,需要在单例里用 IOptionsMonitor——三个名字对应三种新鲜度,按需选。
⚠️ 常见坑:开发期把数据库连接串明文写进配置文件并提交到代码库。正确做法是开发期用用户机密机制存储,生产期用环境变量或密钥服务,配置文件里只留占位。
日志的三个坐标:分类(默认是记录者的类名,用于过滤来源)、级别(Trace/Debug/Information/Warning/Error/Critical 六级)、作用域(把一组日志圈进同一上下文)。通过依赖注入拿到 ILogger 即可记录:
app.MapGet("/logdemo", (ILogger<Program> logger) => { logger.LogInformation("普通观察记录:处理 /logdemo"); logger.LogWarning("值得注意:演示警告级别"); // 结构化日志:用命名占位而非字符串拼接,便于日志系统检索 logger.LogInformation("请求 {Path} 完成,耗时 {Ms}ms", "/logdemo", 3); return "看控制台输出"; });
默认配置下,控制台只显示 Information 及以上。想在开发期看到更细的调试信息,改 appsettings 的日志节即可,不用改代码:
// 配置文件节选:日志过滤规则 { "Logging": { "LogLevel": { "Default": "Information", // 全局默认级别 "Microsoft.AspNetCore": "Warning" // 框架内部日志太吵,压到警告级 } } }
这条配置有个实用细节:把 Microsoft 前缀压到 Warning 后,控制台清爽很多,只剩你自己的日志——第 1 章实验时你也许已经被框架刷屏困扰过。
💡 关键直觉:日志不是"打出来给人看的字符串",而是结构化事件。命名占位符让每条日志变成可检索的字段组合,接上集中式日志系统(第 8 章的生产监控)后差距会非常明显。
三件套的协作场景:观察哨中间件读取 MaxSize 配置决定是否记录大响应体,再用 ILogger 分级输出。类封装中间件里注入 IOptions 与 ILogger,正是 2.2 节类封装形式的直接应用。这个组合在后面每一章都会反复出现——控制器的构造注入、EF Core 的连接串读取、API 的版本开关,全部是本节机制的换装上演。
再用一个案例把日志的第三个坐标"作用域"补完整。
背景:并发一高,控制台里多个请求的日志交错混排,分不清哪条属于哪个请求。操作:给观察哨中间件加日志作用域,把每条下游日志圈进同一个上下文:
// 中间件进站时开启作用域:此后本请求产生的所有日志都携带这两个字段 app.Use(async (ctx, next) => { // 内联中间件不便构造注入,从请求级服务提供器取日志器(每请求可用) var logger = ctx.RequestServices .GetRequiredService<ILoggerFactory>().CreateLogger("哨站"); // BeginScope 返回 IDisposable,请求结束即释放 using var _ = logger.BeginScope(new Dictionary<string, object> { ["TraceId"] = Guid.NewGuid().ToString("N")[..8], // 本请求唯一标识 ["Path"] = ctx.Request.Path.Value ?? "/" }); await next(); // 下游控制器、EF Core 的日志都会自动带上该标识 });
结果:开启作用域后,日志输出变成带前缀的形式(控制台提供程序会展开作用域字段):
=> TraceId=3f9a2c1d Path=/orders => 处理订单查询 => TraceId=3f9a2c1d Path=/orders => 数据库查询完成 耗时 12ms => TraceId=8b07e4aa Path=/report => 处理报表请求
解读:并发日志从"混线"变"可按请求归组",排障时拿一个 TraceId 就能拉出该请求的全部行为轨迹。ASP.NET Core 在开发环境默认会为每个请求生成请求标识(框架级),自定义作用域是它的精细控制版——生产环境接集中式日志后,这些字段直接变成检索维度。变式:作用域嵌套合法——中间件开一层(请求级),控制器里再开一层(业务级),日志会同时携带两层字段,粒度自定。