Skip to content

Commit 7248926

Browse files
committed
request logging optimization and new httpclienthandler addition
1 parent 2a5db16 commit 7248926

File tree

10 files changed

+407
-203
lines changed

10 files changed

+407
-203
lines changed

Readme.md

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ builder
147147
var app = builder.Build();
148148

149149
app
150-
.UseRequestResponseLogging()
150+
.UseRequestLogging()
151151
.UseResponseCrafter()
152152
.UseCors()
153153
.MapMinimalApis()
@@ -253,7 +253,8 @@ Add the following configuration to your `appsettings.json` file:
253253
Based on the above configuration, the UI will be accessible at the following URLs:
254254

255255
- **Swagger (all documents):** [http://localhost/swagger](http://localhost/swagger)
256-
- **Swagger (external document only):** [http://localhost/swagger/integration-v1](http://localhost/swagger/integration-v1)
256+
- **Swagger (external document only):
257+
** [http://localhost/swagger/integration-v1](http://localhost/swagger/integration-v1)
257258
- **Scalar (admin document):** [http://localhost/scalar/admin-v1](http://localhost/scalar/admin-v1)
258259
- **Scalar (integration document):** [http://localhost/scalar/integration-v1](http://localhost/scalar/integration-v1)
259260

@@ -266,9 +267,8 @@ Based on the above configuration, the UI will be accessible at the following URL
266267
Development, Production).
267268
- **Elastic Common Schema Formatting:** Logs are formatted using the Elastic Common Schema (ECS) for compatibility with
268269
Elasticsearch.
269-
- **Request and Response Logging Middleware:** Middleware that logs incoming requests and outgoing responses while
270-
redacting
271-
sensitive information.
270+
- **Request Logging Middleware:** Middleware that logs incoming requests and outgoing responses while redacting
271+
sensitive information and 5kb exceeding properties.
272272
- **Log Filtering:** Excludes unwanted logs from Hangfire Dashboard, Swagger, and outbox database commands.
273273
- **Distributed:** Designed to work with distributed systems and microservices.
274274

@@ -285,7 +285,7 @@ In your middleware pipeline, add the request and response logging middleware:
285285

286286
```csharp
287287
var app = builder.Build();
288-
app.UseRequestResponseLogging();
288+
app.UseRequestLogging();
289289
```
290290

291291
In your `appsettings.{Environment}.json` configure `Serilog`.
@@ -337,12 +337,39 @@ builder.LogStartAttempt();
337337
// Configure services
338338
var app = builder.Build();
339339
// Configure middleware
340-
app.UseRequestResponseLogging();
340+
app.UseRequestLogging();
341341
// Other middleware
342342
app.LogStartSuccess();
343343
app.Run();
344344
```
345345

346+
### Outbound Logging with HttpClient
347+
348+
In addition to the `RequestLoggingMiddleware` for inbound requests, you can now log **outbound** HTTP calls via an
349+
`OutboundLoggingHandler`. This handler captures request and response data (including headers and bodies), automatically
350+
redacting sensitive information (e.g., passwords, tokens).
351+
352+
#### Usage
353+
354+
1. **Register the handler** in your `WebApplicationBuilder`:
355+
```csharp
356+
builder.AddOutboundLoggingHandler();
357+
```
358+
2. **Attach** the handler to any HttpClient registration:
359+
```csharp
360+
builder.Services
361+
.AddHttpClient("RandomApiClient", client =>
362+
{
363+
client.BaseAddress = new Uri("http://localhost");
364+
})
365+
.AddOutboundLoggingHandler();
366+
```
367+
3. **Check logs:** Outbound requests and responses are now logged with redacted headers and bodies, just like inbound
368+
traffic.
369+
370+
> Note: The same redaction rules apply to inbound and outbound calls. Update RedactionHelper if you need to modify the
371+
> behavior (e.g., adding new sensitive keywords).
372+
346373
## MediatR and FluentValidation Integration
347374

348375
### Key Features
@@ -543,7 +570,8 @@ Integrate OpenTelemetry for observability, including metrics, traces, and loggin
543570
- Health Metrics: `url/above-board/prometheus/health`
544571

545572
3. OTLP Configuration:
546-
To configure the OTLP exporter, ensure the following entries are present in your appsettings{Environment}.json or as environment variables:
573+
To configure the OTLP exporter, ensure the following entries are present in your appsettings{Environment}.json or as
574+
environment variables:
547575
```json
548576
{
549577
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317"

Shared.Kernel.Demo/Program.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,23 @@
2929
//.AddDistributedSignalR("DistributedSignalR") // or .AddSignalR()
3030
.MapDefaultTimeZone()
3131
.AddCors()
32+
.AddOutboundLoggingHandler()
3233
.AddHealthChecks();
3334

3435

36+
builder.Services
37+
.AddHttpClient("RandomApiClient",
38+
client =>
39+
{
40+
client.BaseAddress = new Uri("http://localhost");
41+
})
42+
.AddOutboundLoggingHandler();
43+
44+
3545
var app = builder.Build();
3646

3747
app
38-
.UseRequestResponseLogging()
48+
.UseRequestLogging()
3949
.UseResponseCrafter()
4050
.UseCors()
4151
.MapMinimalApis()
@@ -49,6 +59,17 @@
4959

5060
app.MapPost("/params", ([AsParameters] TestTypes testTypes) => TypedResults.Ok(testTypes));
5161
app.MapPost("/body", ([FromBody] TestTypes testTypes) => TypedResults.Ok(testTypes));
62+
app.MapGet("/hello", () => TypedResults.Ok("Hello World!"));
63+
64+
app.MapGet("/get-data",
65+
async (IHttpClientFactory httpClientFactory) =>
66+
{
67+
var httpClient = httpClientFactory.CreateClient("RandomApiClient");
68+
httpClient.DefaultRequestHeaders.Add("auth", "hardcoded-auth-value");
69+
var response = await httpClient.GetFromJsonAsync<object>("hello");
70+
71+
return response;
72+
});
5273

5374

5475
app.LogStartSuccess();

Shared.Kernel.Demo/Shared.Kernel.Demo.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0"/>
10+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.1" />
1111
</ItemGroup>
1212

1313
<ItemGroup>
Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
using Microsoft.AspNetCore.Builder;
2+
using Microsoft.Extensions.DependencyInjection;
23
using Microsoft.Extensions.Logging;
34

45
namespace SharedKernel.Logging;
56

67
public static class LoggingExtensions
78
{
8-
public static WebApplication UseRequestResponseLogging(this WebApplication app)
9+
public static WebApplication UseRequestLogging(this WebApplication app)
910
{
1011
if (app.Logger.IsEnabled(LogLevel.Information))
1112
{
12-
app.UseMiddleware<RequestResponseLoggingMiddleware>();
13+
app.UseMiddleware<RequestLoggingMiddleware>();
1314
}
1415

1516
return app;
1617
}
18+
19+
public static WebApplicationBuilder AddOutboundLoggingHandler(this WebApplicationBuilder builder)
20+
{
21+
builder.Services.AddTransient<OutboundLoggingHandler>();
22+
23+
return builder;
24+
}
25+
26+
public static IHttpClientBuilder AddOutboundLoggingHandler(this IHttpClientBuilder builder)
27+
{
28+
builder.AddHttpMessageHandler<OutboundLoggingHandler>();
29+
return builder;
30+
}
1731
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System.Diagnostics;
2+
using System.Text.Json;
3+
using Microsoft.Extensions.Logging;
4+
5+
namespace SharedKernel.Logging;
6+
7+
internal sealed class OutboundLoggingHandler(ILogger<OutboundLoggingHandler> logger) : DelegatingHandler
8+
{
9+
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
10+
CancellationToken cancellationToken)
11+
{
12+
var stopwatch = Stopwatch.GetTimestamp();
13+
14+
// Capture request
15+
var requestHeadersDict = CreateHeadersDictionary(request);
16+
17+
var requestHeaders = RedactionHelper.RedactHeaders(requestHeadersDict);
18+
19+
var requestBodyRaw = request.Content == null
20+
? string.Empty
21+
: await request.Content.ReadAsStringAsync(cancellationToken);
22+
var requestBody = JsonSerializer.Serialize(RedactionHelper.ParseAndRedactJson(requestBodyRaw));
23+
24+
var response = await base.SendAsync(request, cancellationToken);
25+
26+
// Capture response
27+
var elapsedMs = Stopwatch.GetElapsedTime(stopwatch)
28+
.TotalMilliseconds;
29+
30+
var responseHeadersDict = CreateHeadersDictionary(response);
31+
var responseHeaders = RedactionHelper.RedactHeaders(responseHeadersDict);
32+
33+
var responseBodyRaw = await response.Content.ReadAsStringAsync(cancellationToken);
34+
var responseBody = JsonSerializer.Serialize(RedactionHelper.ParseAndRedactJson(responseBodyRaw));
35+
36+
// Log everything
37+
logger.LogInformation(
38+
"[Outbound Call] HTTP {Method} to {Uri} responded with {StatusCode} in {ElapsedMs}ms. " +
39+
"Request Headers: {RequestHeaders}, Request Body: {RequestBody}, " +
40+
"Response Headers: {ResponseHeaders}, Response Body: {ResponseBody}",
41+
request.Method,
42+
request.RequestUri,
43+
(int)response.StatusCode,
44+
elapsedMs,
45+
JsonSerializer.Serialize(requestHeaders),
46+
requestBody,
47+
JsonSerializer.Serialize(responseHeaders),
48+
responseBody);
49+
50+
return response;
51+
}
52+
53+
private static Dictionary<string, IEnumerable<string>> CreateHeadersDictionary(HttpRequestMessage request)
54+
{
55+
var dict = new Dictionary<string, IEnumerable<string>>(StringComparer.OrdinalIgnoreCase);
56+
57+
// Request-wide headers
58+
foreach (var h in request.Headers)
59+
dict[h.Key] = h.Value;
60+
61+
// Content headers
62+
if (request.Content?.Headers == null)
63+
{
64+
return dict;
65+
}
66+
67+
68+
foreach (var h in request.Content.Headers)
69+
dict[h.Key] = h.Value;
70+
71+
72+
return dict;
73+
}
74+
75+
private static Dictionary<string, IEnumerable<string>> CreateHeadersDictionary(HttpResponseMessage response)
76+
{
77+
var dict = new Dictionary<string, IEnumerable<string>>(StringComparer.OrdinalIgnoreCase);
78+
79+
// Response-wide headers
80+
foreach (var h in response.Headers)
81+
dict[h.Key] = h.Value;
82+
83+
84+
foreach (var h in response.Content.Headers)
85+
dict[h.Key] = h.Value;
86+
87+
88+
return dict;
89+
}
90+
}

0 commit comments

Comments
 (0)