我想使用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
代码中也有Serilog:
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();
简而言之:
await next( context )
包裹在try/catch
中,该ILogger
使用Log.ForContext( ... )
向记录器添加新属性(例如,请求路径,响应代码等)。ILogger.Error
,所以该事件立即被记录。try/catch
让异常继续在调用堆栈中传播(通过使用catch( Exception ex ) when ( LogExceptionThenReturnFalse( httpContext, ex ) )
。我希望Serilog仅记录一次异常,并添加更多内容。快速解决方案是完全捕获我的SerilogMiddleware
中的异常以防止进一步传播,但这意味着它不会达到在我的ILogger
中配置的Serilog IWebHostBuilder
的影响。而且,如果我让异常传播并且不将其记录在中间件中,那么我将无法记录来自HttpContext
的数据。
如何将信息“附加”到当前Serilog的“上下文”中,以便最终由IWebHostBuilder
Serilog记录器捕获并记录该异常时,它包括其他HttpContext
数据?
答案 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.
}
}
}