mcpField
D
dotnet-logging-configuration

dotnet-logging-configuration

github.com/ronnythedev开发7762 次安装15 周前更新
代码仓库

关于 dotnet-logging-configuration

面向 .NET 开发者的日志配置技能,提供结构化日志、Microsoft.Extensions.Logging 与 Serilog 的实现模板,并涵盖性能、上下文和敏感数据处理。

主要功能

  1. 01ILogger 分类日志配置
  2. 02结构化日志与上下文
  3. 03Source Generators 高性能日志
  4. 04Serilog 配置与扩展
  5. 05日志级别及依赖注入指导

适用场景

  1. 01为服务设置分类日志
  2. 02为高频路径生成高性能日志
  3. 03添加关联 ID 和用户上下文
  4. 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标准服务日志记录
ILoggerFactorySingleton动态创建日志记录器
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需要立即处理的系统故障

关键规则

  1. 始终使用 ILogger - 基于类别的日志记录,便于追踪
  2. 使用结构化日志 - 使用 {Property} 占位符,而非字符串插值
  3. 包含上下文 - 在日志中添加相关 ID 和数据
  4. 选择适当的级别记录日志 - 正常流程不要使用 Error
  5. 使用源生成器 - 用于高性能热点路径
  6. 不要记录敏感数据 - 不记录密码、令牌或个人身份信息(PII)
  7. 包含关联 ID - 用于跨服务追踪请求
  8. 按命名空间配置级别 - 减少框架产生的噪声
  9. 使用范围记录操作 - 将相关日志归为一组
  10. 始终将异常作为首个参数传递 - 使用 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