在ASP.NET Core中间件异常解除期间,如何充实Serilog日志条目?

时间:2018-10-26 01:19:48

标签: asp.net-core serilog

我想使用Serilog记录来自HTTP请求的详细信息以及任何未处理的异常(例如完整的请求路径,所有HTTP标头,任何表单字段等)。因此,我遵循了本教程,将信息从当前HttpContext.Request添加到已记录的Serilog日志中:https://blog.getseq.net/smart-logging-middleware-for-asp-net-core/

这是我的SerilogMiddleware版本;

/// <summary>This class logs Request Headers of any failed request.</summary>
public class SerilogMiddleware
{
    private static readonly ILogger _log = global::Serilog.Log.ForContext<SerilogMiddleware>();

    private readonly RequestDelegate next;

    public SerilogMiddleware( RequestDelegate next )
    {
        this.next = next ?? throw new ArgumentNullException( nameof( next ) );
    }

    public async Task Invoke( HttpContext httpContext )
    {
        if( httpContext == null ) throw new ArgumentNullException( nameof( httpContext ) );

        try
        {
            await this.next( httpContext );

            // TODO: Log certian HTTP 4xx responses?

            if( httpContext.Response?.StatusCode >= 500 )
            {
                GetLogForErrorContext( httpContext ).Warning( _MessageTemplateForHttp500 );
            }
        }
        catch( Exception ex ) when( LogException( httpContext, ex ) )
        {
            // LogException returns false, so this catch block will never be entered.
        }
    }

    const String _MessageTemplateForException = "Unhandled exception in {RequestResource}";
    const String _MessageTemplateForHttp500   = "Handled HTTP 500 in {RequestResource}";

    private static Boolean LogException( HttpContext httpContext, Exception ex )
    {
        GetLogForErrorContext( httpContext ).Error( ex, _MessageTemplateForException );

        return false; // return false so the exception is not caught and continues to propagate upwards. (I understand this is cheaper than `throw;` inside catch).
    }

    private static ILogger GetLogForErrorContext( HttpContext httpContext )
    {
        HttpRequest req = httpContext.Request;

        String resource = "{0} {1}{2} {3}".FormatInvariant( req.Method, req.Path, req.QueryString.ToString(), req.Protocol );

        // re: `ForContext`: https://nblumhardt.com/2016/08/context-and-correlation-structured-logging-concepts-in-net-5/

        ILogger result = _log
            .ForContext( "RequestHeaders" , req.Headers.ToDictionary( h => h.Key, h => h.Value.ToString() /* Returns all values, comma-separated */ ), destructureObjects: true )
            .ForContext( "RequestResource", resource )
            .ForContext( "ResponseStatus", httpContext.Response?.StatusCode )
        ;

        if( req.HasFormContentType )
            result = result.ForContext( "RequestForm", req.Form.ToDictionary( v => v.Key, v => v.Value.ToString() ) );

        return result;
    }
}

但是,我的IWebHostBuilder代码中也有Seri​​log:

IWebHostBuilder webHostBuilder = WebHost
    .CreateDefaultBuilder( args )
    .ConfigureLogging( (ctx, cfg ) =>
    {
        cfg.ClearProviders();
        cfg.AddSerilog(); // it's unclear if this is required or not
    } )
    .UseStartup<Startup>()
    .UseSerilog();

webHostBuilder.Build().Run();

简而言之:

  • 这是一个ASP.NET Core中间件类,它将await next( context )包裹在try/catch中,该ILogger使用Log.ForContext( ... )向记录器添加新属性(例如,请求路径,响应代码等)。
  • 因为此代码实际上调用了ILogger.Error,所以该事件立即被记录。
  • 但是try/catch让异常继续在调用堆栈中传播(通过使用catch( Exception ex ) when ( LogExceptionThenReturnFalse( httpContext, ex ) )
  • ...这意味着Serilog使用默认扩充功能再次记录异常和HTTP请求。

我希望Serilog仅记录一次异常,并添加更多内容。快速解决方案是完全捕获我的SerilogMiddleware中的异常以防止进一步传播,但这意味着它不会达到在我的ILogger中配置的Serilog IWebHostBuilder的影响。而且,如果我让异常传播并且不将其记录在中间件中,那么我将无法记录来自HttpContext的数据。

如何将信息“附加”到当前Serilog的“上下文”中,以便最终由IWebHostBuilder Serilog记录器捕获并记录该异常时,它包括其他HttpContext数据?

2 个答案:

答案 0 :(得分:0)

到目前为止,我已经找到了最佳解决方案,这显然是黑客, 想法从这里被盗-https://blog.datalust.co/smart-logging-middleware-for-asp-net-core/

添加一个中间件类以捕获异常并手动添加。

// Idea from https://blog.datalust.co/smart-logging-middleware-for-asp-net-core/
public class LogDetailsMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IUserManager _userManager;

    private readonly ILogger _logger = Serilog.Log.ForContext<LogDetailsMiddleware>();

    public LogDetailsMiddleware(RequestDelegate next, IHttpContextAccessor httpContextAccessor, IUserManager userManager)
    {
        if (next == null)
        {
            throw new ArgumentNullException(nameof(next));
        }

        _next = next;
        _httpContextAccessor = httpContextAccessor;
        _userManager = userManager;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        if (httpContext == null)
        {
            throw new ArgumentNullException(nameof(httpContext));
        }

        LogContext.PushProperty("Email", _userManager.CurrentUser.Email);
        LogContext.PushProperty("Url", _httpContextAccessor.HttpContext.Request.GetDisplayUrl());

        Stopwatch sw = Stopwatch.StartNew();
        try
        {
        await _next(httpContext);
            sw.Stop();
        }
        // Never caught, because `LogException()` returns false.
        catch (Exception ex) when (LogException( sw, ex)) { }
    }

    bool LogException(Stopwatch sw, Exception ex)
    {
        sw.Stop();
        
        _logger.Error(ex, "An unhandled exception has occurred while executing the request.");

        return false;
    }
}

答案 1 :(得分:-2)

我们正在用HttpClientFactory

记录请求
services.AddHttpClient("clientWithLogger")
    .AddHttpMessageHandler<HttpClientLoggingHandler>();

还有我们的HttpClientLoggingHandler

public class HttpClientLoggingHandler : DelegatingHandler
{
    private readonly ILogger<HttpClientLoggingHandler> _logger;

    public HttpClientLoggingHandler(ILogger<HttpClientLoggingHandler> logger)
    {
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var sw = Stopwatch.StartNew();
        _logger.LogInformation("Starting request to '{requestUri}'", request.RequestUri);
        var response = await base.SendAsync(request, cancellationToken);
        sw.Stop();
        _logger.LogInformation("Finished request to '{requestUri}' in {elapsedMilliseconds}ms, response: {response}",
            request.RequestUri, sw.ElapsedMilliseconds, await response.Content.ReadAsStringAsync());
        return response;
    }
}

然后我们可以简单地使用HttpClientFactory

public class DeviceDetector
{
    private readonly IHttpClientFactory _httpClientFactory;
    private const string LicenceKey = "XXX";
    private const string Domain = "https://xxx/api/v1/";

    public DeviceDetector(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task<Device> DetectDevice(string userAgent)
    {
        var url = $"{Domain}{LicenceKey}";
        var result = await _httpClientFactory.CreateClient("clientWithLogger").GetStringAsync(url);
        return Newtonsoft.Json.JsonConvert.DeserializeObject<Device>(result);
    }
}

通过这种方式,我们使用普通的ILogger,它是后台的Serilog,并且可以完全控制什么日志以及何时登录。

修改

如果您只想记录错误,则可以轻松添加逻辑

public class HttpClientLoggingHandler : DelegatingHandler
{
    private readonly ILogger<HttpClientLoggingHandler> _logger;

    public HttpClientLoggingHandler(ILogger<HttpClientLoggingHandler> logger)
    {
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        // Log error requests
        try
        {
            var response = await base.SendAsync(request, cancellationToken);

            if(response.IsSuccessStatusCode)
            {
                // Success status code
            }
            else if( (int)response.StatusCode >= 500 )
            {
                // error 500
            }
        }
        catch( Exception ex ) when( LogException( httpContext, ex ) )
        {
            // LogException returns false, so this catch block will never be entered.
        }
    }
}