Skip to content

Instantly share code, notes, and snippets.

@CarnaViire
Last active June 30, 2023 12:33
Show Gist options
  • Save CarnaViire/08346da1634d357f6bcb8adefa01da67 to your computer and use it in GitHub Desktop.
Save CarnaViire/08346da1634d357f6bcb8adefa01da67 to your computer and use it in GitHub Desktop.
HttpLoggingHandler implemented via IHttpClientLogger
internal class HttpClientLogger : IHttpClientLogger
{
internal TimeProvider TimeProvider = TimeProvider.System;
private readonly IHttpClientLogEnricher[] _enrichers;
private readonly IHttpRequestReader _httpRequestReader;
private readonly ObjectPool<List<KeyValuePair<string, string>>> _headersPool =
PoolFactory.CreateListPool<KeyValuePair<string, string>>();
private readonly ObjectPool<LogRecord> _logRecordPool =
PoolFactory.CreatePool(new LogRecordPooledObjectPolicy());
private readonly bool _logRequestStart;
private readonly bool _logRequestHeaders;
private readonly bool _logResponseHeaders;
public ILogger Logger { get; private set; }
public HttpClientLogger(
ILogger<HttpLoggingHandler> logger,
IHttpRequestReader httpRequestReader,
IEnumerable<IHttpClientLogEnricher> enrichers,
IOptions<LoggingOptions> options)
{
_httpRequestReader = httpRequestReader;
_enrichers = enrichers.ToArray();
_ = Throw.IfMemberNull(options, options.Value);
Logger = logger;
_logRequestStart = options.Value.LogRequestStart;
_logResponseHeaders = options.Value.ResponseHeadersDataClasses.Count > 0;
_logRequestHeaders = options.Value.RequestHeadersDataClasses.Count > 0;
}
public async ValueTask<object?> LogRequestStartAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
{
var timestamp = TimeProvider.GetTimestamp();
var logRecord = _logRecordPool.Get();
List<KeyValuePair<string, string>>? requestHeadersBuffer = null;
if (_logRequestHeaders)
{
requestHeadersBuffer = _headersPool.Get();
}
await _httpRequestReader.ReadRequestAsync(logRecord, request, requestHeadersBuffer, cancellationToken).ConfigureAwait(false);
if (_logRequestStart)
{
Log.OutgoingRequest(Logger, LogLevel.Information, logRecord);
}
return logRecord;
}
public async ValueTask LogRequestStopAsync(
object? context,
HttpRequestMessage request,
HttpResponseMessage response,
TimeSpan elapsed,
CancellationToken cancellationToken = default)
{
var logRecord = (LogRecord?)context;
_ = Throw.IfNull(logRecord);
List<KeyValuePair<string, string>>? responseHeadersBuffer = null;
if (_logResponseHeaders)
{
responseHeadersBuffer = _headersPool.Get();
}
await _httpRequestReader.ReadResponseAsync(logRecord, response, responseHeadersBuffer, cancellationToken).ConfigureAwait(false);
var propertyBag = LogMethodHelper.GetHelper();
FillLogRecord(logRecord, propertyBag, elapsed, request, response);
Log.OutgoingRequest(Logger, GetLogLevel(logRecord), logRecord);
ReturnToPool(logRecord, propertyBag);
}
public ValueTask LogRequestFailedAsync(object? context, HttpRequestMessage request, HttpResponseMessage? response, Exception exception, TimeSpan elapsed, CancellationToken cancellationToken = default)
{
var logRecord = (LogRecord?)context;
_ = Throw.IfNull(logRecord);
var propertyBag = LogMethodHelper.GetHelper();
FillLogRecord(logRecord, propertyBag, elapsed, request, response);
Log.OutgoingRequestError(Logger, logRecord, exception);
ReturnToPool(logRecord, propertyBag);
return ValueTask.CompletedTask;
}
private void ReturnToPool(LogRecord logRecord, LogMethodHelper propertyBag)
{
LogMethodHelper.ReturnHelper(propertyBag);
if (logRecord.RequestHeaders is not null)
{
_headersPool.Return(logRecord.RequestHeaders);
}
if (logRecord.ResponseHeaders is not null)
{
_headersPool.Return(logRecord.ResponseHeaders);
}
_logRecordPool.Return(logRecord);
}
private void FillLogRecord(
LogRecord logRecord, LogMethodHelper propertyBag, TimeSpan elapsed,
HttpRequestMessage request, HttpResponseMessage? response)
{
foreach (var enricher in _enrichers)
{
try
{
enricher.Enrich(propertyBag, request, response);
}
catch (Exception e)
{
Log.EnrichmentError(Logger, e);
}
}
logRecord.EnrichmentProperties = propertyBag;
logRecord.Duration = (long)elapsed.TotalMilliseconds;
}
private static LogLevel GetLogLevel(LogRecord logRecord)
{
const int HttpErrorsRangeStart = 400;
const int HttpErrorsRangeEnd = 599;
int statusCode = logRecord.StatusCode!.Value;
if (statusCode >= HttpErrorsRangeStart && statusCode <= HttpErrorsRangeEnd)
{
return LogLevel.Error;
}
return LogLevel.Information;
}
}
internal sealed class HttpLoggingHandler : DelegatingHandler
{
internal TimeProvider TimeProvider = TimeProvider.System;
private IHttpClientLogger _httpClientLogger;
public HttpLoggingHandler(
IHttpClientLogger httpClientLogger)
{
_httpClientLogger = httpClientLogger;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_ = Throw.IfNull(request);
var timestamp = TimeProvider.GetTimestamp();
HttpResponseMessage? response = null;
object? state = await _httpClientLogger.LogRequestStartAsync(request, cancellationToken).ConfigureAwait(false);
try
{
response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
TimeSpan elapsed = TimeProvider.GetElapsedTime(timestamp, TimeProvider.GetTimestamp());
await _httpClientLogger.LogRequestStopAsync(state, request, response, elapsed, cancellationToken).ConfigureAwait(false);
return response;
}
catch (Exception exception)
{
TimeSpan elapsed = TimeProvider.GetElapsedTime(timestamp, TimeProvider.GetTimestamp());
await _httpClientLogger.LogRequestFailedAsync(state, request, response, exception, elapsed);
throw;
}
}
}
public interface IHttpClientLogger
{
ValueTask<object?> LogRequestStartAsync(
HttpRequestMessage request,
CancellationToken cancellationToken = default);
ValueTask LogRequestStopAsync(
object? context,
HttpRequestMessage request,
HttpResponseMessage response,
TimeSpan elapsed,
CancellationToken cancellationToken = default);
ValueTask LogRequestFailedAsync(
object? context,
HttpRequestMessage request,
HttpResponseMessage? response,
Exception exception,
TimeSpan elapsed,
CancellationToken cancellationToken = default);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment