logging-observability

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
You are a senior .NET architect specializing in observability. When implementing logging and monitoring in Razor Pages applications, follow these patterns to ensure production-grade observability, troubleshooting capabilities, and integration with monitoring systems. Target .NET 8+ with nullable reference types enabled.
你是一名专精可观测性领域的资深.NET架构师。在Razor Pages应用中实现日志与监控能力时,请遵循以下模式,以确保生产级可观测性、问题排查能力,以及与监控系统的集成。目标版本为.NET 8及以上,且启用了可空引用类型。

Rationale

设计思路

Effective observability is critical for production applications. Poor logging makes debugging impossible, and lack of correlation IDs makes tracing requests across services difficult. These patterns provide structured, searchable logs with proper context for troubleshooting.
生产级应用的有效可观测性至关重要。糟糕的日志会导致无法调试,缺少关联ID则会让跨服务的请求追踪变得困难。这些模式提供了带完整上下文的结构化、可检索日志,便于问题排查。

Core Principles

核心原则

  1. Structured Logging: Use structured formats (JSON) for machine parsing
  2. Correlation IDs: Every request gets a unique ID for end-to-end tracing
  3. Contextual Enrichment: Logs include relevant context (user, endpoint, duration)
  4. Log Levels: Use appropriate levels (Debug, Info, Warning, Error, Fatal)
  5. External Sinks: Send logs to centralized systems (Seq, Datadog, CloudWatch)
  1. 结构化日志:使用结构化格式(JSON)以便机器解析
  2. 关联ID:每个请求分配唯一ID用于端到端追踪
  3. 上下文增强:日志包含相关上下文(用户、端点、耗时)
  4. 日志级别:使用合适的级别(Debug、Info、Warning、Error、Fatal)
  5. 外部Sink:将日志发送到中心化系统(Seq、Datadog、CloudWatch)

Pattern 1: Serilog Configuration

模式1:Serilog配置

NuGet Packages

NuGet包

xml
<PackageReference Include="Serilog.AspNetCore" Version="8.0.*" />
<PackageReference Include="Serilog.Expressions" Version="4.0.*" />
<PackageReference Include="Serilog.Sinks.Seq" Version="7.0.*" /> <!-- Optional -->
xml
<PackageReference Include="Serilog.AspNetCore" Version="8.0.*" />
<PackageReference Include="Serilog.Expressions" Version="4.0.*" />
<PackageReference Include="Serilog.Sinks.Seq" Version="7.0.*" /> <!-- Optional -->

Bootstrap Logger (Program.cs Start)

启动引导Logger(Program.cs开头)

csharp
using Serilog;
using Serilog.Debugging;

// Enable Serilog self-logging for diagnostics
SelfLog.Enable(msg => Console.Error.WriteLine($"[SERILOG] {msg}"));

// Create bootstrap logger for startup errors
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
    .Enrich.FromLogContext()
    .WriteTo.Console(formatProvider: CultureInfo.CurrentCulture)
    .CreateBootstrapLogger();

try
{
    Log.Information("Starting web application...");
    var builder = WebApplication.CreateBuilder(args);
    
    // ... configure services ...
    
    var app = builder.Build();
    await app.RunAsync();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
    await Log.CloseAndFlushAsync();
}
csharp
using Serilog;
using Serilog.Debugging;

// 启用Serilog自日志用于诊断
SelfLog.Enable(msg => Console.Error.WriteLine($"[SERILOG] {msg}"));

// 创建引导Logger用于记录启动错误
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
    .Enrich.FromLogContext()
    .WriteTo.Console(formatProvider: CultureInfo.CurrentCulture)
    .CreateBootstrapLogger();

try
{
    Log.Information("Starting web application...");
    var builder = WebApplication.CreateBuilder(args);
    
    // ... 配置服务 ...
    
    var app = builder.Build();
    await app.RunAsync();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
    await Log.CloseAndFlushAsync();
}

Service Registration

服务注册

csharp
public static class LoggingServiceRegistration
{
    public static IServiceCollection ConfigureSerilog(
        this IServiceCollection services, 
        IConfiguration configuration)
    {
        services.AddSerilog((services, lc) => lc
            .ReadFrom.Configuration(configuration)
            .Enrich.FromLogContext()
            .Enrich.WithMachineName()
            .Enrich.WithEnvironmentName()
            .WriteTo.Console(formatter: new ExpressionTemplate(
                "[{@t:hh:mm:ss.fff tt} {@l:u3}] {SourceContext} - {CorrelationId} - {@m}\n{@x}",
                theme: TemplateTheme.Code))
            .WriteTo.Seq(
                configuration["Seq:ServerUrl"] ?? "http://localhost:5341",
                apiKey: configuration["Seq:ApiKey"],
                formatProvider: CultureInfo.CurrentCulture)
        );

        return services;
    }
}
csharp
public static class LoggingServiceRegistration
{
    public static IServiceCollection ConfigureSerilog(
        this IServiceCollection services, 
        IConfiguration configuration)
    {
        services.AddSerilog((services, lc) => lc
            .ReadFrom.Configuration(configuration)
            .Enrich.FromLogContext()
            .Enrich.WithMachineName()
            .Enrich.WithEnvironmentName()
            .WriteTo.Console(formatter: new ExpressionTemplate(
                "[{@t:hh:mm:ss.fff tt} {@l:u3}] {SourceContext} - {CorrelationId} - {@m}\n{@x}",
                theme: TemplateTheme.Code))
            .WriteTo.Seq(
                configuration["Seq:ServerUrl"] ?? "http://localhost:5341",
                apiKey: configuration["Seq:ApiKey"],
                formatProvider: CultureInfo.CurrentCulture)
        );

        return services;
    }
}

appsettings.json Configuration

appsettings.json配置

json
{
  "Serilog": {
    "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.Seq"],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "Microsoft.AspNetCore": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
        }
      }
    ],
    "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"],
    "Properties": {
      "Application": "MyApp"
    }
  }
}
json
{
  "Serilog": {
    "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.Seq"],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "Microsoft.AspNetCore": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
        }
      }
    ],
    "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"],
    "Properties": {
      "Application": "MyApp"
    }
  }
}

Pattern 2: Correlation ID Middleware

模式2:关联ID中间件

csharp
public class RequestContextLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private const string CorrelationHeaderName = "X-Correlation-Id";

    public RequestContextLoggingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        var correlationId = GetCorrelationId(httpContext);
        
        // Add to response headers
        httpContext.Response.OnStarting(() =>
        {
            httpContext.Response.Headers[CorrelationHeaderName] = correlationId;
            return Task.CompletedTask;
        });

        using (LogContext.PushProperty("CorrelationId", correlationId))
        using (LogContext.PushProperty("RequestPath", httpContext.Request.Path))
        using (LogContext.PushProperty("RequestMethod", httpContext.Request.Method))
        {
            await _next.Invoke(httpContext);
        }
    }

    private static string GetCorrelationId(HttpContext httpContext)
    {
        httpContext.Request.Headers.TryGetValue(
            CorrelationHeaderName, out var correlationId);

        return correlationId.FirstOrDefault() ?? httpContext.TraceIdentifier;
    }
}

// Extension method for easy registration
public static class RequestContextLoggingExtensions
{
    public static IApplicationBuilder UseRequestContextLogging(
        this IApplicationBuilder app)
    {
        return app.UseMiddleware<RequestContextLoggingMiddleware>();
    }
}
csharp
public class RequestContextLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private const string CorrelationHeaderName = "X-Correlation-Id";

    public RequestContextLoggingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        var correlationId = GetCorrelationId(httpContext);
        
        // 添加到响应头
        httpContext.Response.OnStarting(() =>
        {
            httpContext.Response.Headers[CorrelationHeaderName] = correlationId;
            return Task.CompletedTask;
        });

        using (LogContext.PushProperty("CorrelationId", correlationId))
        using (LogContext.PushProperty("RequestPath", httpContext.Request.Path))
        using (LogContext.PushProperty("RequestMethod", httpContext.Request.Method))
        {
            await _next.Invoke(httpContext);
        }
    }

    private static string GetCorrelationId(HttpContext httpContext)
    {
        httpContext.Request.Headers.TryGetValue(
            CorrelationHeaderName, out var correlationId);

        return correlationId.FirstOrDefault() ?? httpContext.TraceIdentifier;
    }
}

// 用于便捷注册的扩展方法
public static class RequestContextLoggingExtensions
{
    public static IApplicationBuilder UseRequestContextLogging(
        this IApplicationBuilder app)
    {
        return app.UseMiddleware<RequestContextLoggingMiddleware>();
    }
}

Registration

注册

csharp
// Program.cs
var app = builder.Build();

// Place early in pipeline
app.UseRequestContextLogging();
app.UseSerilogRequestLogging(); // Logs each request
csharp
// Program.cs
var app = builder.Build();

// 放在管道靠前的位置
app.UseRequestContextLogging();
app.UseSerilogRequestLogging(); // 记录每个请求的日志

Pattern 3: Request Logging with Serilog

模式3:基于Serilog的请求日志

csharp
// Program.cs
app.UseSerilogRequestLogging(options =>
{
    options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
    {
        diagnosticContext.Set("UserId", httpContext.User.Identity?.Name ?? "anonymous");
        diagnosticContext.Set("ClientIp", httpContext.Connection.RemoteIpAddress?.ToString());
        diagnosticContext.Set("UserAgent", httpContext.Request.Headers["User-Agent"].ToString());
    };
    
    options.GetLevel = (httpContext, elapsed, ex) =>
    {
        // Log 5xx as Error, slow requests as Warning
        if (ex != null || httpContext.Response.StatusCode > 499)
            return LogEventLevel.Error;
        
        if (elapsed > 1000)
            return LogEventLevel.Warning;
        
        return LogEventLevel.Information;
    };
});
csharp
// Program.cs
app.UseSerilogRequestLogging(options =>
{
    options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
    {
        diagnosticContext.Set("UserId", httpContext.User.Identity?.Name ?? "anonymous");
        diagnosticContext.Set("ClientIp", httpContext.Connection.RemoteIpAddress?.ToString());
        diagnosticContext.Set("UserAgent", httpContext.Request.Headers["User-Agent"].ToString());
    };
    
    options.GetLevel = (httpContext, elapsed, ex) =>
    {
        // 5xx响应记为Error,慢请求记为Warning
        if (ex != null || httpContext.Response.StatusCode > 499)
            return LogEventLevel.Error;
        
        if (elapsed > 1000)
            return LogEventLevel.Warning;
        
        return LogEventLevel.Information;
    };
});

Custom Request Logging (PageModel)

自定义请求日志(PageModel)

csharp
public class OrderDetailsModel(ILogger<OrderDetailsModel> logger) : PageModel
{
    public async Task OnGetAsync(Guid orderId)
    {
        // Push contextual properties
        using (logger.BeginScope(new Dictionary<string, object>
        {
            ["OrderId"] = orderId,
            ["UserId"] = User.Identity?.Name ?? "anonymous"
        }))
        {
            logger.LogInformation("Loading order details");
            
            try
            {
                var order = await _orderService.GetAsync(orderId);
                
                if (order == null)
                {
                    logger.LogWarning("Order not found");
                    return NotFound();
                }
                
                logger.LogInformation("Order loaded successfully");
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Failed to load order");
                throw;
            }
        }
    }
}
csharp
public class OrderDetailsModel(ILogger<OrderDetailsModel> logger) : PageModel
{
    public async Task OnGetAsync(Guid orderId)
    {
        // 添加上下文属性
        using (logger.BeginScope(new Dictionary<string, object>
        {
            ["OrderId"] = orderId,
            ["UserId"] = User.Identity?.Name ?? "anonymous"
        }))
        {
            logger.LogInformation("Loading order details");
            
            try
            {
                var order = await _orderService.GetAsync(orderId);
                
                if (order == null)
                {
                    logger.LogWarning("Order not found");
                    return NotFound();
                }
                
                logger.LogInformation("Order loaded successfully");
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Failed to load order");
                throw;
            }
        }
    }
}

Pattern 4: MediatR Pipeline Logging

模式4:MediatR管道日志

csharp
public class RequestLoggingBehavior<TRequest, TResponse>(ILogger<RequestLoggingBehavior<TRequest, TResponse>> logger)
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private static readonly string RequestName = typeof(TRequest).Name;
    private static readonly TimeSpan SlowRequestThreshold = TimeSpan.FromMilliseconds(500);

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        logger.LogInformation("Handling {RequestName}", RequestName);

        var stopwatch = Stopwatch.StartNew();

        try
        {
            var response = await next(cancellationToken);

            stopwatch.Stop();
            var elapsed = stopwatch.Elapsed;

            if (elapsed > SlowRequestThreshold)
            {
                logger.LogWarning(
                    "Handled {RequestName} successfully in {ElapsedMs}ms (exceeds {ThresholdMs}ms threshold)",
                    RequestName,
                    elapsed.TotalMilliseconds,
                    SlowRequestThreshold.TotalMilliseconds);
            }
            else
            {
                logger.LogInformation(
                    "Handled {RequestName} successfully in {ElapsedMs}ms",
                    RequestName,
                    elapsed.TotalMilliseconds);
            }

            return response;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();

            logger.LogError(
                ex,
                "Error handling {RequestName} after {ElapsedMs}ms",
                RequestName,
                stopwatch.Elapsed.TotalMilliseconds);

            throw;
        }
    }
}
csharp
public class RequestLoggingBehavior<TRequest, TResponse>(ILogger<RequestLoggingBehavior<TRequest, TResponse>> logger)
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private static readonly string RequestName = typeof(TRequest).Name;
    private static readonly TimeSpan SlowRequestThreshold = TimeSpan.FromMilliseconds(500);

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        logger.LogInformation("Handling {RequestName}", RequestName);

        var stopwatch = Stopwatch.StartNew();

        try
        {
            var response = await next(cancellationToken);

            stopwatch.Stop();
            var elapsed = stopwatch.Elapsed;

            if (elapsed > SlowRequestThreshold)
            {
                logger.LogWarning(
                    "Handled {RequestName} successfully in {ElapsedMs}ms (exceeds {ThresholdMs}ms threshold)",
                    RequestName,
                    elapsed.TotalMilliseconds,
                    SlowRequestThreshold.TotalMilliseconds);
            }
            else
            {
                logger.LogInformation(
                    "Handled {RequestName} successfully in {ElapsedMs}ms",
                    RequestName,
                    elapsed.TotalMilliseconds);
            }

            return response;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();

            logger.LogError(
                ex,
                "Error handling {RequestName} after {ElapsedMs}ms",
                RequestName,
                stopwatch.Elapsed.TotalMilliseconds);

            throw;
        }
    }
}

Registration

注册

csharp
builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssemblyContaining<Program>();
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(RequestLoggingBehavior<,>));
});
csharp
builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssemblyContaining<Program>();
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(RequestLoggingBehavior<,>));
});

Pattern 5: Health Checks

模式5:健康检查

Basic Configuration

基础配置

csharp
// Program.cs
builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>(
        name: "database",
        tags: new[] { "db", "critical" })
    .AddCheck<EmailServiceHealthCheck>(
        name: "email-service",
        tags: new[] { "external", "email" })
    .AddRedis(
        name: "redis",
        tags: new[] { "cache", "distributed" });

var app = builder.Build();

// Simple health endpoint
app.MapHealthChecks("/up");

// Detailed health with authentication
app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = HealthCheckResponseWriter.WriteResponse,
    AllowCachingResponses = false
}).RequireAuthorization("HealthCheckPolicy");
csharp
// Program.cs
builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>(
        name: "database",
        tags: new[] { "db", "critical" })
    .AddCheck<EmailServiceHealthCheck>(
        name: "email-service",
        tags: new[] { "external", "email" })
    .AddRedis(
        name: "redis",
        tags: new[] { "cache", "distributed" });

var app = builder.Build();

// 简单健康检查端点
app.MapHealthChecks("/up");

// 带认证的详细健康检查端点
app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = HealthCheckResponseWriter.WriteResponse,
    AllowCachingResponses = false
}).RequireAuthorization("HealthCheckPolicy");

Custom Health Check

自定义健康检查

csharp
public class EmailServiceHealthCheck(IEmailServiceAgent emailService) : IHealthCheck
{
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            var canConnect = await emailService.PingAsync(cancellationToken);
            
            if (canConnect)
            {
                return HealthCheckResult.Healthy("Email service is accessible");
            }
            
            return HealthCheckResult.Unhealthy("Cannot connect to email service");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy(
                "Email service health check failed", ex);
        }
    }
}
csharp
public class EmailServiceHealthCheck(IEmailServiceAgent emailService) : IHealthCheck
{
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            var canConnect = await emailService.PingAsync(cancellationToken);
            
            if (canConnect)
            {
                return HealthCheckResult.Healthy("Email service is accessible");
            }
            
            return HealthCheckResult.Unhealthy("Cannot connect to email service");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy(
                "Email service health check failed", ex);
        }
    }
}

Custom Response Writer

自定义响应写入器

csharp
public static class HealthCheckResponseWriter
{
    public static Task WriteResponse(
        HttpContext context, 
        HealthReport report)
    {
        context.Response.ContentType = "application/json";

        var response = new
        {
            Status = report.Status.ToString(),
            TotalDuration = report.TotalDuration.TotalMilliseconds,
            Checks = report.Entries.Select(e => new
            {
                Name = e.Key,
                Status = e.Value.Status.ToString(),
                Duration = e.Value.Duration.TotalMilliseconds,
                Exception = e.Value.Exception?.Message,
                Data = e.Value.Data
            })
        };

        return context.Response.WriteAsJsonAsync(response);
    }
}
csharp
public static class HealthCheckResponseWriter
{
    public static Task WriteResponse(
        HttpContext context, 
        HealthReport report)
    {
        context.Response.ContentType = "application/json";

        var response = new
        {
            Status = report.Status.ToString(),
            TotalDuration = report.TotalDuration.TotalMilliseconds,
            Checks = report.Entries.Select(e => new
            {
                Name = e.Key,
                Status = e.Value.Status.ToString(),
                Duration = e.Value.Duration.TotalMilliseconds,
                Exception = e.Value.Exception?.Message,
                Data = e.Value.Data
            })
        };

        return context.Response.WriteAsJsonAsync(response);
    }
}

Pattern 6: LoggerMessage Pattern (High Performance)

模式6:LoggerMessage模式(高性能)

For hot paths, use compile-time logging to avoid string interpolation overhead.
csharp
public static partial class LoggerMessages
{
    // Information
    [LoggerMessage(
        EventId = 1001,
        Level = LogLevel.Information,
        Message = "User {UserId} signed up successfully")]
    public static partial void UserSignedUp(ILogger logger, string userId);

    // Warning
    [LoggerMessage(
        EventId = 2001,
        Level = LogLevel.Warning,
        Message = "Slow database query detected: {QueryName} took {ElapsedMs}ms")]
    public static partial void SlowQueryDetected(
        ILogger logger, string queryName, long elapsedMs);

    // Error
    [LoggerMessage(
        EventId = 3001,
        Level = LogLevel.Error,
        Message = "Failed to send email to {EmailAddress}")]
    public static partial void EmailSendFailed(
        ILogger logger, Exception ex, string emailAddress);

    // With scopes
    [LoggerMessage(
        EventId = 1002,
        Level = LogLevel.Information,
        Message = "Order {OrderId} processed")]
    public static partial void OrderProcessed(ILogger logger, Guid orderId);
}

// Usage
public class SignUpHandler(ILogger<SignUpHandler> logger)
{
    public async Task<SignUpResponse> Handle(SignUpRequest request)
    {
        // ... process signup ...
        
        LoggerMessages.UserSignedUp(logger, user.Id);
        
        return new SignUpResponse { Success = true };
    }
}
对于热点路径,使用编译时日志避免字符串插值的开销。
csharp
public static partial class LoggerMessages
{
    // Information
    [LoggerMessage(
        EventId = 1001,
        Level = LogLevel.Information,
        Message = "User {UserId} signed up successfully")]
    public static partial void UserSignedUp(ILogger logger, string userId);

    // Warning
    [LoggerMessage(
        EventId = 2001,
        Level = LogLevel.Warning,
        Message = "Slow database query detected: {QueryName} took {ElapsedMs}ms")]
    public static partial void SlowQueryDetected(
        ILogger logger, string queryName, long elapsedMs);

    // Error
    [LoggerMessage(
        EventId = 3001,
        Level = LogLevel.Error,
        Message = "Failed to send email to {EmailAddress}")]
    public static partial void EmailSendFailed(
        ILogger logger, Exception ex, string emailAddress);

    // 带Scope
    [LoggerMessage(
        EventId = 1002,
        Level = LogLevel.Information,
        Message = "Order {OrderId} processed")]
    public static partial void OrderProcessed(ILogger logger, Guid orderId);
}

// 使用示例
public class SignUpHandler(ILogger<SignUpHandler> logger)
{
    public async Task<SignUpResponse> Handle(SignUpRequest request)
    {
        // ... 处理注册流程 ...
        
        LoggerMessages.UserSignedUp(logger, user.Id);
        
        return new SignUpResponse { Success = true };
    }
}

Pattern 7: OpenTelemetry Integration

模式7:OpenTelemetry集成

Configuration

配置

csharp
// NuGet: OpenTelemetry.Extensions.Hosting, OpenTelemetry.Instrumentation.AspNetCore

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddEntityFrameworkCoreInstrumentation()
            .AddSource("MyApp") // Custom activity source
            .AddOtlpExporter(options =>
            {
                options.Endpoint = new Uri("http://localhost:4317");
            });
    })
    .WithMetrics(metrics =>
    {
        metrics
            .AddAspNetCoreInstrumentation()
            .AddRuntimeInstrumentation()
            .AddPrometheusExporter();
    });

var app = builder.Build();
app.UseOpenTelemetryPrometheusScrapingEndpoint();
csharp
// NuGet: OpenTelemetry.Extensions.Hosting, OpenTelemetry.Instrumentation.AspNetCore

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddEntityFrameworkCoreInstrumentation()
            .AddSource("MyApp") // 自定义Activity源
            .AddOtlpExporter(options =>
            {
                options.Endpoint = new Uri("http://localhost:4317");
            });
    })
    .WithMetrics(metrics =>
    {
        metrics
            .AddAspNetCoreInstrumentation()
            .AddRuntimeInstrumentation()
            .AddPrometheusExporter();
    });

var app = builder.Build();
app.UseOpenTelemetryPrometheusScrapingEndpoint();

Custom Activities

自定义Activity

csharp
public class OrderProcessingService
{
    private static readonly ActivitySource ActivitySource = new("MyApp.Orders");

    public async Task ProcessOrderAsync(Order order)
    {
        using var activity = ActivitySource.StartActivity("ProcessOrder");
        activity?.SetTag("order.id", order.Id);
        activity?.SetTag("order.total", order.Total);

        try
        {
            activity?.AddEvent(new ActivityEvent("ValidatingOrder"));
            await ValidateOrderAsync(order);

            activity?.AddEvent(new ActivityEvent("ChargingPayment"));
            await ChargePaymentAsync(order);

            activity?.AddEvent(new ActivityEvent("SendingConfirmation"));
            await SendConfirmationAsync(order);

            activity?.SetStatus(ActivityStatusCode.Ok);
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            throw;
        }
    }
}
csharp
public class OrderProcessingService
{
    private static readonly ActivitySource ActivitySource = new("MyApp.Orders");

    public async Task ProcessOrderAsync(Order order)
    {
        using var activity = ActivitySource.StartActivity("ProcessOrder");
        activity?.SetTag("order.id", order.Id);
        activity?.SetTag("order.total", order.Total);

        try
        {
            activity?.AddEvent(new ActivityEvent("ValidatingOrder"));
            await ValidateOrderAsync(order);

            activity?.AddEvent(new ActivityEvent("ChargingPayment"));
            await ChargePaymentAsync(order);

            activity?.AddEvent(new ActivityEvent("SendingConfirmation"));
            await SendConfirmationAsync(order);

            activity?.SetStatus(ActivityStatusCode.Ok);
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            throw;
        }
    }
}

Pattern 8: Razor Pages Error Logging

模式8:Razor Pages错误日志

csharp
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel(ILogger<ErrorModel> logger) : PageModel
{
    public string? RequestId { get; set; }
    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
    public string? ErrorMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
        
        // Log the error that brought us here
        var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
        
        if (exceptionHandlerPathFeature?.Error != null)
        {
            var ex = exceptionHandlerPathFeature.Error;
            var path = exceptionHandlerPathFeature.Path;
            
            logger.LogError(ex, 
                "Unhandled exception at {Path}. RequestId: {RequestId}", 
                path, RequestId);
            
            // Only show details in development
            if (HttpContext.RequestServices.GetRequiredService<IWebHostEnvironment>().IsDevelopment())
            {
                ErrorMessage = ex.ToString();
            }
        }
    }
}
csharp
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel(ILogger<ErrorModel> logger) : PageModel
{
    public string? RequestId { get; set; }
    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
    public string? ErrorMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
        
        // 记录导致跳转至此的错误
        var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
        
        if (exceptionHandlerPathFeature?.Error != null)
        {
            var ex = exceptionHandlerPathFeature.Error;
            var path = exceptionHandlerPathFeature.Path;
            
            logger.LogError(ex, 
                "Unhandled exception at {Path}. RequestId: {RequestId}", 
                path, RequestId);
            
            // 仅在开发环境展示详细错误
            if (HttpContext.RequestServices.GetRequiredService<IWebHostEnvironment>().IsDevelopment())
            {
                ErrorMessage = ex.ToString();
            }
        }
    }
}

Anti-Patterns

反模式

Static Logger Access

静态Logger访问

csharp
// ❌ BAD: Static logger access
public class OrderService
{
    private static readonly Logger Logger = Log.ForContext<OrderService>();
}

// ✅ GOOD: Inject ILogger<T>
public class OrderService(ILogger<OrderService> logger)
{
}
csharp
// ❌ 错误用法:静态Logger访问
public class OrderService
{
    private static readonly Logger Logger = Log.ForContext<OrderService>();
}

// ✅ 正确用法:注入ILogger<T>
public class OrderService(ILogger<OrderService> logger)
{
}

String Interpolation in Logs

日志中使用字符串插值

csharp
// ❌ BAD: String interpolation evaluated even if log level is disabled
_logger.LogInformation($"Processing order {orderId} for user {userId}");

// ✅ GOOD: Structured logging with parameters
_logger.LogInformation("Processing order {OrderId} for user {UserId}", orderId, userId);

// ✅ BETTER: Use LoggerMessage for hot paths
[LoggerMessage(EventId = 1001, Level = LogLevel.Information, Message = "Processing order {OrderId} for user {UserId}")]
public static partial void ProcessingOrder(ILogger logger, Guid orderId, string userId);
csharp
// ❌ 错误用法:即使日志级别未启用,字符串插值也会执行
_logger.LogInformation($"Processing order {orderId} for user {userId}");

// ✅ 正确用法:带参数的结构化日志
_logger.LogInformation("Processing order {OrderId} for user {UserId}", orderId, userId);

// ✅ 更优用法:热点路径使用LoggerMessage
[LoggerMessage(EventId = 1001, Level = LogLevel.Information, Message = "Processing order {OrderId} for user {UserId}")]
public static partial void ProcessingOrder(ILogger logger, Guid orderId, string userId);

Catching and Swallowing Exceptions

捕获并吞掉异常

csharp
// ❌ BAD: Silent failures
try
{
    await _service.DoWorkAsync();
}
catch (Exception ex)
{
    // Nothing logged!
}

// ✅ GOOD: Always log exceptions
catch (Exception ex)
{
    _logger.LogError(ex, "Failed to complete work");
    throw; // Re-throw if you can't handle it
}
csharp
// ❌ 错误用法:静默失败
try
{
    await _service.DoWorkAsync();
}
catch (Exception ex)
{
    // 没有任何日志记录!
}

// ✅ 正确用法:始终记录异常
catch (Exception ex)
{
    _logger.LogError(ex, "Failed to complete work");
    throw; // 无法处理则重新抛出
}

References

参考资料