-
Notifications
You must be signed in to change notification settings - Fork 4.5k
/
LoggingScopeHttpMessageHandler.cs
157 lines (132 loc) · 7.3 KB
/
LoggingScopeHttpMessageHandler.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
namespace Microsoft.Extensions.Http.Logging
{
/// <summary>
/// Handles logging of the lifecycle for an HTTP request within a log scope.
/// </summary>
public class LoggingScopeHttpMessageHandler : DelegatingHandler
{
private readonly ILogger _logger;
private readonly HttpClientFactoryOptions? _options;
private static readonly Func<string, bool> _shouldNotRedactHeaderValue = (header) => false;
/// <summary>
/// Initializes a new instance of the <see cref="LoggingScopeHttpMessageHandler"/> class with a specified logger.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/> to log to.</param>
/// <exception cref="ArgumentNullException"><paramref name="logger"/> is <see langword="null"/>.</exception>
public LoggingScopeHttpMessageHandler(ILogger logger)
{
ThrowHelper.ThrowIfNull(logger);
_logger = logger;
}
/// <summary>
/// Initializes a new instance of the <see cref="LoggingScopeHttpMessageHandler"/> class with a specified logger and options.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/> to log to.</param>
/// <param name="options">The <see cref="HttpClientFactoryOptions"/> used to configure the <see cref="LoggingScopeHttpMessageHandler"/> instance.</param>
/// <exception cref="ArgumentNullException"><paramref name="logger"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
public LoggingScopeHttpMessageHandler(ILogger logger, HttpClientFactoryOptions options)
{
ThrowHelper.ThrowIfNull(logger);
ThrowHelper.ThrowIfNull(options);
_logger = logger;
_options = options;
}
private Task<HttpResponseMessage> SendCoreAsync(HttpRequestMessage request, bool useAsync, CancellationToken cancellationToken)
{
ThrowHelper.ThrowIfNull(request);
return Core(request, useAsync, cancellationToken);
async Task<HttpResponseMessage> Core(HttpRequestMessage request, bool useAsync, CancellationToken cancellationToken)
{
var stopwatch = ValueStopwatch.StartNew();
Func<string, bool> shouldRedactHeaderValue = _options?.ShouldRedactHeaderValue ?? _shouldNotRedactHeaderValue;
using (Log.BeginRequestPipelineScope(_logger, request))
{
Log.RequestPipelineStart(_logger, request, shouldRedactHeaderValue);
HttpResponseMessage response = useAsync
? await base.SendAsync(request, cancellationToken).ConfigureAwait(false)
#if NET5_0_OR_GREATER
: base.Send(request, cancellationToken);
#else
: throw new NotImplementedException("Unreachable code");
#endif
Log.RequestPipelineEnd(_logger, response, stopwatch.GetElapsedTime(), shouldRedactHeaderValue);
return response;
}
}
}
/// <inheritdoc />
/// <remarks>Logs the request to and response from the sent <see cref="HttpRequestMessage"/>.</remarks>
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
=> SendCoreAsync(request, useAsync: true, cancellationToken);
#if NET5_0_OR_GREATER
/// <inheritdoc />
/// <remarks>Logs the request to and response from the sent <see cref="HttpRequestMessage"/>.</remarks>
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
=> SendCoreAsync(request, useAsync: false, cancellationToken).GetAwaiter().GetResult();
#endif
// Used in tests
internal static class Log
{
public static class EventIds
{
public static readonly EventId PipelineStart = new EventId(100, "RequestPipelineStart");
public static readonly EventId PipelineEnd = new EventId(101, "RequestPipelineEnd");
public static readonly EventId RequestHeader = new EventId(102, "RequestPipelineRequestHeader");
public static readonly EventId ResponseHeader = new EventId(103, "RequestPipelineResponseHeader");
}
private static readonly Func<ILogger, HttpMethod, string?, IDisposable?> _beginRequestPipelineScope = LoggerMessage.DefineScope<HttpMethod, string?>("HTTP {HttpMethod} {Uri}");
private static readonly Action<ILogger, HttpMethod, string?, Exception?> _requestPipelineStart = LoggerMessage.Define<HttpMethod, string?>(
LogLevel.Information,
EventIds.PipelineStart,
"Start processing HTTP request {HttpMethod} {Uri}");
private static readonly Action<ILogger, double, int, Exception?> _requestPipelineEnd = LoggerMessage.Define<double, int>(
LogLevel.Information,
EventIds.PipelineEnd,
"End processing HTTP request after {ElapsedMilliseconds}ms - {StatusCode}");
public static IDisposable? BeginRequestPipelineScope(ILogger logger, HttpRequestMessage request)
{
return _beginRequestPipelineScope(logger, request.Method, GetUriString(request.RequestUri));
}
public static void RequestPipelineStart(ILogger logger, HttpRequestMessage request, Func<string, bool> shouldRedactHeaderValue)
{
_requestPipelineStart(logger, request.Method, GetUriString(request.RequestUri), null);
if (logger.IsEnabled(LogLevel.Trace))
{
logger.Log(
LogLevel.Trace,
EventIds.RequestHeader,
new HttpHeadersLogValue(HttpHeadersLogValue.Kind.Request, request.Headers, request.Content?.Headers, shouldRedactHeaderValue),
null,
(state, ex) => state.ToString());
}
}
public static void RequestPipelineEnd(ILogger logger, HttpResponseMessage response, TimeSpan duration, Func<string, bool> shouldRedactHeaderValue)
{
_requestPipelineEnd(logger, duration.TotalMilliseconds, (int)response.StatusCode, null);
if (logger.IsEnabled(LogLevel.Trace))
{
logger.Log(
LogLevel.Trace,
EventIds.ResponseHeader,
new HttpHeadersLogValue(HttpHeadersLogValue.Kind.Response, response.Headers, response.Content?.Headers, shouldRedactHeaderValue),
null,
(state, ex) => state.ToString());
}
}
private static string? GetUriString(Uri? requestUri)
{
return requestUri?.IsAbsoluteUri == true
? requestUri.AbsoluteUri
: requestUri?.ToString();
}
}
}
}