关于 dotnet-logging-configuration
面向 .NET 开发者的日志配置技能,提供结构化日志、Microsoft.Extensions.Logging 与 Serilog 的实现模板,并涵盖性能、上下文和敏感数据处理。
主要功能
- 01ILogger 分类日志配置
- 02结构化日志与上下文
- 03Source Generators 高性能日志
- 04Serilog 配置与扩展
- 05日志级别及依赖注入指导
适用场景
- 01为服务设置分类日志
- 02为高频路径生成高性能日志
- 03添加关联 ID 和用户上下文
- 04配置日志级别并避免记录敏感数据
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ ronnythedev/ dotnet-clean-architecture-skills --skill dotnet-logging-configuration安装后在 Claude Code 中输入/dotnet-logging-configuration确认已加载。
详细介绍
.NET 日志配置
概览
此技能根据 Microsoft 最佳实践配置结构化日志:
- ILogger - 注入服务中,用于基于类别的日志记录
- ILoggerFactory - 在需要时动态创建日志记录器
- 结构化日志 - 保留日志属性以供分析
- 源生成器 - 高性能的编译时日志记录
- Serilog 集成 - 提供增强型接收器和丰富器
快速参考
| 接口 | 生命周期 | 用途 |
|---|---|---|
ILogger<T> | Singleton | 标准服务日志记录 |
ILoggerFactory | Singleton | 动态创建日志记录器 |
ILogger | - | 非泛型(在 DI 中避免使用) |
日志结构
/Infrastructure/Logging/
├── LoggerConfiguration.cs
├── LogEvents.cs # LoggerMessage source generators
├── LogEnrichers/
│ ├── UserContextEnricher.cs
│ └── CorrelationIdEnricher.cs
└── Sinks/
└── CustomSink.cs
模板:使用 ILogger 的服务
// src/{name}.application/Services/OrderService.cs
using Microsoft.Extensions.Logging;
namespace {name}.application.services;
/// <summary>
/// Services should inject ILogger<T> for category-based logging.
/// The category is automatically set to the full type name.
/// </summary>
public sealed class OrderService : IOrderService
{
private readonly ILogger<OrderService> _logger;
private readonly IOrderRepository _orderRepository;
public OrderService(
ILogger<OrderService> logger,
IOrderRepository orderRepository)
{
_logger = logger;
_orderRepository = orderRepository;
}
public async Task<Result<Order>> ProcessOrderAsync(
Guid orderId,
CancellationToken cancellationToken)
{
// ═══════════════════════════════════════════════════════════════
// STRUCTURED LOGGING - Use placeholders, not string interpolation
// ═══════════════════════════════════════════════════════════════
_logger.LogInformation(
"Processing order {OrderId}",
orderId);
try
{
var order = await _orderRepository.GetByIdAsync(orderId, cancellationToken);
if (order is null)
{
_logger.LogWarning(
"Order {OrderId} not found",
orderId);
return Result.Failure<Order>(OrderErrors.NotFound(orderId));
}
// Log with multiple properties
_logger.LogInformation(
"Order {OrderId} retrieved. Status: {Status}, Total: {Total}",
orderId,
order.Status,
order.Total);
return Result.Success(order);
}
catch (Exception ex)
{
// Always log exceptions with the exception parameter first
_logger.LogError(
ex,
"Error processing order {OrderId}",
orderId);
throw;
}
}
}
模板:ILoggerFactory 用法
// src/{name}.infrastructure/Services/DynamicLoggerService.cs
using Microsoft.Extensions.Logging;
namespace {name}.infrastructure.services;
/// <summary>
/// Use ILoggerFactory when you need to create loggers dynamically.
/// Common use cases:
/// - Factory classes that create multiple types
/// - Plugin systems where type isn't known at compile time
/// - Base classes that want child-specific categories
/// </summary>
public sealed class PluginManager
{
private readonly ILoggerFactory _loggerFactory;
public PluginManager(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
}
public IPlugin LoadPlugin(string pluginName, Type pluginType)
{
// Create a logger with a dynamic category
var logger = _loggerFactory.CreateLogger(pluginType);
logger.LogInformation(
"Loading plugin {PluginName} of type {PluginType}",
pluginName,
pluginType.Name);
// Or with a string category
var customLogger = _loggerFactory.CreateLogger($"Plugins.{pluginName}");
customLogger.LogDebug(
"Plugin {PluginName} initialized",
pluginName);
return CreatePlugin(pluginType, customLogger);
}
}
模板:使用源生成器实现高性能日志记录
// src/{name}.application/Logging/LogEvents.cs
using Microsoft.Extensions.Logging;
namespace {name}.application.logging;
/// <summary>
/// LoggerMessage source generators provide the best logging performance.
/// Benefits:
/// - Zero allocation for disabled log levels
/// - Compile-time validation of message templates
/// - Strongly typed parameters
/// </summary>
public static partial class LogEvents
{
// ═══════════════════════════════════════════════════════════════
// INFORMATION LEVEL
// ═══════════════════════════════════════════════════════════════
[LoggerMessage(
EventId = 1000,
Level = LogLevel.Information,
Message = "Processing request {RequestName} with ID {RequestId}")]
public static partial void LogRequestProcessing(
this ILogger logger,
string requestName,
Guid requestId);
[LoggerMessage(
EventId = 1001,
Level = LogLevel.Information,
Message = "Request {RequestName} completed in {ElapsedMs}ms")]
public static partial void LogRequestCompleted(
this ILogger logger,
string requestName,
long elapsedMs);
[LoggerMessage(
EventId = 1002,
Level = LogLevel.Information,
Message = "User {UserId} authenticated successfully")]
public static partial void LogUserAuthenticated(
this ILogger logger,
Guid userId);
// ═══════════════════════════════════════════════════════════════
// WARNING LEVEL
// ═══════════════════════════════════════════════════════════════
[LoggerMessage(
EventId = 2000,
Level = LogLevel.Warning,
Message = "Slow request detected: {RequestName} took {ElapsedMs}ms")]
public static partial void LogSlowRequest(
this ILogger logger,
string requestName,
long elapsedMs);
[LoggerMessage(
EventId = 2001,
Level = LogLevel.Warning,
Message = "Cache miss for key {CacheKey}")]
public static partial void LogCacheMiss(
this ILogger logger,
string cacheKey);
[LoggerMessage(
EventId = 2002,
Level = LogLevel.Warning,
Message = "Rate limit exceeded for client {ClientId}")]
public static partial void LogRateLimitExceeded(
this ILogger logger,
string clientId);
// ═══════════════════════════════════════════════════════════════
// ERROR LEVEL
// ═══════════════════════════════════════════════════════════════
[LoggerMessage(
EventId = 3000,
Level = LogLevel.Error,
Message = "Error processing request {RequestName}")]
public static partial void LogRequestError(
this ILogger logger,
Exception exception,
string requestName);
[LoggerMessage(
EventId = 3001,
Level = LogLevel.Error,
Message = "Database operation failed for entity {EntityType} with ID {EntityId}")]
public static partial void LogDatabaseError(
this ILogger logger,
Exception exception,
string entityType,
Guid entityId);
[LoggerMessage(
EventId = 3002,
Level = LogLevel.Error,
Message = "External service {ServiceName} returned error: {ErrorCode}")]
public static partial void LogExternalServiceError(
this ILogger logger,
string serviceName,
string errorCode);
// ═══════════════════════════════════════════════════════════════
// DEBUG LEVEL
// ═══════════════════════════════════════════════════════════════
[LoggerMessage(
EventId = 4000,
Level = LogLevel.Debug,
Message = "Executing query: {QueryName}")]
public static partial void LogQueryExecution(
this ILogger logger,
string queryName);
[LoggerMessage(
EventId = 4001,
Level = LogLevel.Debug,
Message = "Cache hit for key {CacheKey}")]
public static partial void LogCacheHit(
this ILogger logger,
string cacheKey);
}
使用源生成的日志
// src/{name}.application/Features/Orders/ProcessOrder/ProcessOrderHandler.cs
public sealed class ProcessOrderHandler : ICommandHandler<ProcessOrderCommand, Guid>
{
private readonly ILogger<ProcessOrderHandler> _logger;
public async Task<Result<Guid>> Handle(
ProcessOrderCommand command,
CancellationToken cancellationToken)
{
// Use extension method from LogEvents
_logger.LogRequestProcessing("ProcessOrder", command.OrderId);
var stopwatch = Stopwatch.StartNew();
try
{
// ... process order ...
stopwatch.Stop();
_logger.LogRequestCompleted("ProcessOrder", stopwatch.ElapsedMilliseconds);
return Result.Success(command.OrderId);
}
catch (Exception ex)
{
_logger.LogRequestError(ex, "ProcessOrder");
throw;
}
}
}
模板:Serilog 配置
// src/{name}.api/Program.cs
using Serilog;
using Serilog.Events;
var builder = WebApplication.CreateBuilder(args);
// ═══════════════════════════════════════════════════════════════
// SERILOG CONFIGURATION
// ═══════════════════════════════════════════════════════════════
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
.Enrich.WithProperty("Application", "MyApp")
.WriteTo.Console(outputTemplate:
"[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
.WriteTo.File(
path: "logs/log-.txt",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
.CreateLogger();
builder.Host.UseSerilog();
// ... rest of configuration ...
var app = builder.Build();
// Request logging middleware
app.UseSerilogRequestLogging(options =>
{
options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000}ms";
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
{
diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
diagnosticContext.Set("UserAgent", httpContext.Request.Headers["User-Agent"].ToString());
};
});
模板:appsettings.json 日志配置
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
}
},
"Serilog": {
"Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.Seq"],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console"
}
},
{
"Name": "File",
"Args": {
"path": "logs/log-.txt",
"rollingInterval": "Day",
"retainedFileCountLimit": 30
}
},
{
"Name": "Seq",
"Args": {
"serverUrl": "http://localhost:5341"
}
}
],
"Enrich": ["FromLogContext", "WithMachineName", "WithEnvironmentName"],
"Properties": {
"Application": "MyApp"
}
}
}
模板:关联 ID 丰富器
// src/{name}.infrastructure/Logging/CorrelationIdEnricher.cs
using Serilog.Core;
using Serilog.Events;
namespace {name}.infrastructure.logging;
/// <summary>
/// Enriches all logs with a correlation ID for request tracing.
/// </summary>
public sealed class CorrelationIdEnricher : ILogEventEnricher
{
private readonly IHttpContextAccessor _httpContextAccessor;
private const string CorrelationIdHeader = "X-Correlation-ID";
private const string CorrelationIdProperty = "CorrelationId";
public CorrelationIdEnricher(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var correlationId = GetCorrelationId();
var property = propertyFactory.CreateProperty(CorrelationIdProperty, correlationId);
logEvent.AddPropertyIfAbsent(property);
}
private string GetCorrelationId()
{
var context = _httpContextAccessor.HttpContext;
if (context is null)
{
return Guid.NewGuid().ToString();
}
// Try to get from header first
if (context.Request.Headers.TryGetValue(CorrelationIdHeader, out var headerValue))
{
return headerValue.ToString();
}
// Generate and store in context
if (!context.Items.ContainsKey(CorrelationIdProperty))
{
context.Items[CorrelationIdProperty] = Guid.NewGuid().ToString();
}
return context.Items[CorrelationIdProperty]!.ToString()!;
}
}
模板:用户上下文丰富器
// src/{name}.infrastructure/Logging/UserContextEnricher.cs
using Serilog.Core;
using Serilog.Events;
namespace {name}.infrastructure.logging;
/// <summary>
/// Enriches logs with current user information.
/// </summary>
public sealed class UserContextEnricher : ILogEventEnricher
{
private readonly IHttpContextAccessor _httpContextAccessor;
public UserContextEnricher(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var context = _httpContextAccessor.HttpContext;
if (context?.User.Identity?.IsAuthenticated != true)
{
return;
}
var userId = context.User.FindFirst("sub")?.Value
?? context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!string.IsNullOrEmpty(userId))
{
var property = propertyFactory.CreateProperty("UserId", userId);
logEvent.AddPropertyIfAbsent(property);
}
var userName = context.User.Identity.Name;
if (!string.IsNullOrEmpty(userName))
{
var property = propertyFactory.CreateProperty("UserName", userName);
logEvent.AddPropertyIfAbsent(property);
}
}
}
模板:依赖注入注册
// src/{name}.infrastructure/DependencyInjection.cs
using Microsoft.Extensions.DependencyInjection;
using Serilog;
namespace {name}.infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
// ═══════════════════════════════════════════════════════════════
// LOGGING CONFIGURATION
// ═══════════════════════════════════════════════════════════════
services.AddLogging(loggingBuilder =>
{
loggingBuilder.ClearProviders();
loggingBuilder.AddSerilog(dispose: true);
});
// Register enrichers
services.AddSingleton<CorrelationIdEnricher>();
services.AddSingleton<UserContextEnricher>();
return services;
}
}
模板:使用 BeginScope 的范围日志记录
// src/{name}.application/Features/Orders/ProcessOrder/ProcessOrderHandler.cs
public sealed class ProcessOrderHandler : ICommandHandler<ProcessOrderCommand, Guid>
{
private readonly ILogger<ProcessOrderHandler> _logger;
public async Task<Result<Guid>> Handle(
ProcessOrderCommand command,
CancellationToken cancellationToken)
{
// ═══════════════════════════════════════════════════════════════
// SCOPED LOGGING - All logs within scope include these properties
// ═══════════════════════════════════════════════════════════════
using (_logger.BeginScope(new Dictionary<string, object>
{
["OrderId"] = command.OrderId,
["UserId"] = command.UserId,
["Operation"] = "ProcessOrder"
}))
{
_logger.LogInformation("Starting order processing");
// All logs within this scope automatically include OrderId, UserId, Operation
await ValidateOrder(command);
await CalculateTotals(command);
await ProcessPayment(command);
_logger.LogInformation("Order processing completed");
}
return Result.Success(command.OrderId);
}
}
日志级别指南
| 级别 | 用途 |
|---|---|
Trace | 详细调试(不用于生产环境) |
Debug | 开发调试 |
Information | 一般流程、重要事件 |
Warning | 意外但已处理的事件 |
Error | 需要关注的故障 |
Critical | 需要立即处理的系统故障 |
关键规则
- 始终使用 ILogger - 基于类别的日志记录,便于追踪
- 使用结构化日志 - 使用
{Property}占位符,而非字符串插值 - 包含上下文 - 在日志中添加相关 ID 和数据
- 选择适当的级别记录日志 - 正常流程不要使用 Error
- 使用源生成器 - 用于高性能热点路径
- 不要记录敏感数据 - 不记录密码、令牌或个人身份信息(PII)
- 包含关联 ID - 用于跨服务追踪请求
- 按命名空间配置级别 - 减少框架产生的噪声
- 使用范围记录操作 - 将相关日志归为一组
- 始终将异常作为首个参数传递 - 使用
LogError(ex, "Message"),而不是将异常写入消息中
应避免的反模式
// ❌ WRONG: String interpolation
_logger.LogInformation($"Processing order {orderId}");
// ✅ CORRECT: Structured logging with placeholders
_logger.LogInformation("Processing order {OrderId}", orderId);
// ❌ WRONG: Logging sensitive data
_logger.LogInformation("User {Email} logged in with password {Password}", email, password);
// ✅ CORRECT: Redact sensitive data
_logger.LogInformation("User {Email} logged in", email);
// ❌ WRONG: Exception in message
_logger.LogError($"Error: {exception.Message}");
// ✅ CORRECT: Exception as first parameter
_logger.LogError(exception, "Error processing request");
// ❌ WRONG: Injecting ILoggerFactory when ILogger<T> suffices
public class OrderService
{
private readonly ILogger _logger;
public OrderService(ILoggerFactory factory)
{
_logger = factory.CreateLogger<OrderService>(); // Unnecessary!
}
}
// ✅ CORRECT: Inject ILogger<T> directly
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
}
// ❌ WRONG: Checking log level unnecessarily (with source generators)
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Value: {Value}", expensiveOperation());
}
// ✅ CORRECT: Source generators handle this automatically
_logger.LogQueryExecution("GetOrderById"); // No-op if Debug disabled
所需的软件包
# Core logging
dotnet add package Microsoft.Extensions.Logging
# Serilog (recommended)
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Process
# Optional: Centralized logging
dotnet add package Serilog.Sinks.Seq # Seq server
dotnet add package Serilog.Sinks.Elasticsearch # Elasticsearch
dotnet add package Serilog.Sinks.ApplicationInsights # Azure
相关技能
10-dotnet-pipeline-behaviors- 为所有请求提供日志记录行为17-dotnet-health-checks- 应用程序监控01-dotnet-clean-architecture- 整体架构
评论
支持 Markdown