Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 36 additions & 8 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ builder
var app = builder.Build();

app
.UseRequestResponseLogging()
.UseRequestLogging()
.UseResponseCrafter()
.UseCors()
.MapMinimalApis()
Expand Down Expand Up @@ -253,7 +253,8 @@ Add the following configuration to your `appsettings.json` file:
Based on the above configuration, the UI will be accessible at the following URLs:

- **Swagger (all documents):** [http://localhost/swagger](http://localhost/swagger)
- **Swagger (external document only):** [http://localhost/swagger/integration-v1](http://localhost/swagger/integration-v1)
- **Swagger (external document only):
** [http://localhost/swagger/integration-v1](http://localhost/swagger/integration-v1)
- **Scalar (admin document):** [http://localhost/scalar/admin-v1](http://localhost/scalar/admin-v1)
- **Scalar (integration document):** [http://localhost/scalar/integration-v1](http://localhost/scalar/integration-v1)

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

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

```csharp
var app = builder.Build();
app.UseRequestResponseLogging();
app.UseRequestLogging();
```

In your `appsettings.{Environment}.json` configure `Serilog`.
Expand Down Expand Up @@ -337,12 +337,39 @@ builder.LogStartAttempt();
// Configure services
var app = builder.Build();
// Configure middleware
app.UseRequestResponseLogging();
app.UseRequestLogging();
// Other middleware
app.LogStartSuccess();
app.Run();
```

### Outbound Logging with HttpClient

In addition to the `RequestLoggingMiddleware` for inbound requests, you can now log **outbound** HTTP calls via an
`OutboundLoggingHandler`. This handler captures request and response data (including headers and bodies), automatically
redacting sensitive information (e.g., passwords, tokens).

#### Usage

1. **Register the handler** in your `WebApplicationBuilder`:
```csharp
builder.AddOutboundLoggingHandler();
```
2. **Attach** the handler to any HttpClient registration:
```csharp
builder.Services
.AddHttpClient("RandomApiClient", client =>
{
client.BaseAddress = new Uri("http://localhost");
})
.AddOutboundLoggingHandler();
```
3. **Check logs:** Outbound requests and responses are now logged with redacted headers and bodies, just like inbound
traffic.

> Note: The same redaction rules apply to inbound and outbound calls. Update RedactionHelper if you need to modify the
> behavior (e.g., adding new sensitive keywords).

## MediatR and FluentValidation Integration

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

3. OTLP Configuration:
To configure the OTLP exporter, ensure the following entries are present in your appsettings{Environment}.json or as environment variables:
To configure the OTLP exporter, ensure the following entries are present in your appsettings{Environment}.json or as
environment variables:
```json
{
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317"
Expand Down
23 changes: 22 additions & 1 deletion Shared.Kernel.Demo/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,23 @@
//.AddDistributedSignalR("DistributedSignalR") // or .AddSignalR()
.MapDefaultTimeZone()
.AddCors()
.AddOutboundLoggingHandler()
.AddHealthChecks();


builder.Services
.AddHttpClient("RandomApiClient",
client =>
{
client.BaseAddress = new Uri("http://localhost");
})
.AddOutboundLoggingHandler();


var app = builder.Build();

app
.UseRequestResponseLogging()
.UseRequestLogging()
.UseResponseCrafter()
.UseCors()
.MapMinimalApis()
Expand All @@ -49,6 +59,17 @@

app.MapPost("/params", ([AsParameters] TestTypes testTypes) => TypedResults.Ok(testTypes));
app.MapPost("/body", ([FromBody] TestTypes testTypes) => TypedResults.Ok(testTypes));
app.MapGet("/hello", () => TypedResults.Ok("Hello World!"));

app.MapGet("/get-data",
async (IHttpClientFactory httpClientFactory) =>
{
var httpClient = httpClientFactory.CreateClient("RandomApiClient");
httpClient.DefaultRequestHeaders.Add("auth", "hardcoded-auth-value");
var response = await httpClient.GetFromJsonAsync<object>("hello");

return response;
});


app.LogStartSuccess();
Expand Down
2 changes: 1 addition & 1 deletion Shared.Kernel.Demo/Shared.Kernel.Demo.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0"/>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.1" />
</ItemGroup>

<ItemGroup>
Expand Down
18 changes: 16 additions & 2 deletions src/SharedKernel/Logging/LoggingExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace SharedKernel.Logging;

public static class LoggingExtensions
{
public static WebApplication UseRequestResponseLogging(this WebApplication app)
public static WebApplication UseRequestLogging(this WebApplication app)
{
if (app.Logger.IsEnabled(LogLevel.Information))
{
app.UseMiddleware<RequestResponseLoggingMiddleware>();
app.UseMiddleware<RequestLoggingMiddleware>();
}

return app;
}

public static WebApplicationBuilder AddOutboundLoggingHandler(this WebApplicationBuilder builder)
{
builder.Services.AddTransient<OutboundLoggingHandler>();

return builder;
}

public static IHttpClientBuilder AddOutboundLoggingHandler(this IHttpClientBuilder builder)
{
builder.AddHttpMessageHandler<OutboundLoggingHandler>();
return builder;
}
}
90 changes: 90 additions & 0 deletions src/SharedKernel/Logging/OutboundLoggingHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.Diagnostics;
using System.Text.Json;
using Microsoft.Extensions.Logging;

namespace SharedKernel.Logging;

internal sealed class OutboundLoggingHandler(ILogger<OutboundLoggingHandler> logger) : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.GetTimestamp();

// Capture request
var requestHeadersDict = CreateHeadersDictionary(request);

var requestHeaders = RedactionHelper.RedactHeaders(requestHeadersDict);

var requestBodyRaw = request.Content == null
? string.Empty
: await request.Content.ReadAsStringAsync(cancellationToken);
var requestBody = JsonSerializer.Serialize(RedactionHelper.ParseAndRedactJson(requestBodyRaw));

var response = await base.SendAsync(request, cancellationToken);

// Capture response
var elapsedMs = Stopwatch.GetElapsedTime(stopwatch)
.TotalMilliseconds;

var responseHeadersDict = CreateHeadersDictionary(response);
var responseHeaders = RedactionHelper.RedactHeaders(responseHeadersDict);

var responseBodyRaw = await response.Content.ReadAsStringAsync(cancellationToken);
var responseBody = JsonSerializer.Serialize(RedactionHelper.ParseAndRedactJson(responseBodyRaw));

// Log everything
logger.LogInformation(
"[Outbound Call] HTTP {Method} to {Uri} responded with {StatusCode} in {ElapsedMs}ms. " +
"Request Headers: {RequestHeaders}, Request Body: {RequestBody}, " +
"Response Headers: {ResponseHeaders}, Response Body: {ResponseBody}",
request.Method,
request.RequestUri,
(int)response.StatusCode,
elapsedMs,
JsonSerializer.Serialize(requestHeaders),
requestBody,
JsonSerializer.Serialize(responseHeaders),
responseBody);

return response;
}

private static Dictionary<string, IEnumerable<string>> CreateHeadersDictionary(HttpRequestMessage request)
{
var dict = new Dictionary<string, IEnumerable<string>>(StringComparer.OrdinalIgnoreCase);

// Request-wide headers
foreach (var h in request.Headers)
dict[h.Key] = h.Value;

// Content headers
if (request.Content?.Headers == null)
{
return dict;
}


foreach (var h in request.Content.Headers)
dict[h.Key] = h.Value;


return dict;
}

private static Dictionary<string, IEnumerable<string>> CreateHeadersDictionary(HttpResponseMessage response)
{
var dict = new Dictionary<string, IEnumerable<string>>(StringComparer.OrdinalIgnoreCase);

// Response-wide headers
foreach (var h in response.Headers)
dict[h.Key] = h.Value;


foreach (var h in response.Content.Headers)
dict[h.Key] = h.Value;


return dict;
}
}
Loading