Riya.OpenTelemetry.HttpLogs
1.0.3
dotnet add package Riya.OpenTelemetry.HttpLogs --version 1.0.3
NuGet\Install-Package Riya.OpenTelemetry.HttpLogs -Version 1.0.3
<PackageReference Include="Riya.OpenTelemetry.HttpLogs" Version="1.0.3" />
<PackageVersion Include="Riya.OpenTelemetry.HttpLogs" Version="1.0.3" />
<PackageReference Include="Riya.OpenTelemetry.HttpLogs" />
paket add Riya.OpenTelemetry.HttpLogs --version 1.0.3
#r "nuget: Riya.OpenTelemetry.HttpLogs, 1.0.3"
#:package Riya.OpenTelemetry.HttpLogs@1.0.3
#addin nuget:?package=Riya.OpenTelemetry.HttpLogs&version=1.0.3
#tool nuget:?package=Riya.OpenTelemetry.HttpLogs&version=1.0.3
Riya.OpenTelemetry.HttpLogs
1. Project Overview
Riya.OpenTelemetry.HttpLogs is a .NET NuGet package that provides automatic HTTP request/response logging for outgoing HTTP calls, with a configurable masking system to protect sensitive data in logs.
Built on top of DelegatingHandler, it intercepts every outgoing HTTP call and logs structured information including method, URI, status code, request/response headers, and request/response bodies — with sensitive fields masked before logging.
Key Features:
- Automatic logging of all outgoing HTTP calls
- Configurable masking at 3 levels — Startup, Service Constructor, and Function scope
- Nested JSON path masking — e.g.
response.body.data.user.token - Query string parameter masking
- Request and Response header masking
- Payload truncation for large bodies (30KB limit)
- Startup validation of masking paths — invalid config fails fast at boot
DisableLoggingflag for latency-sensitive calls- Zero impact on actual request/response data — masking only applies to logs
2. Folder Structure
Riya.OpenTelemetry.HttpLogs/
├── Masking/
│ ├── MaskingOptions.cs # Masking rules model + MergeWith logic
│ ├── MaskingPathParser.cs # Path string parser → ParsedMaskingPath
│ ├── BodyMasker.cs # JSON body nested field masker
│ ├── QueryMasker.cs # Query string parameter masker
│ ├── HeaderMasker.cs # Request/Response header masker
│ ├── MaskingContext.cs # AsyncLocal 3-level context holder
│ ├── MaskingContextScope.cs # Service-level scope (IDisposable)
│ └── MaskingOptionsValidator.cs # Startup validation via IValidateOptions
├── HttpLoggingDelegatingHandler.cs # Core delegating handler
├── MaskingOptionsStartupFilter.cs # Eager startup validation trigger
└── ServiceCollectionExtensions.cs # DI registration extension method
3. Architecture & Flow
3-Level Masking Priority
| Level | Scope | Details |
|---|---|---|
| Level 1 | Startup.cs (Global) | Rules apply to every outgoing HTTP call in the application |
| Level 2 | Service Constructor (Service Scope) | Rules apply to all HTTP calls made within that service. Set via MaskingContextScope |
| Level 3 | Function Scope (Call Level) | Rules apply to one specific HTTP call only. Set via MaskingContext.SetCallOverride() |
Priority: Function > Constructor > Startup
All three levels are merged — lower priority rules are not replaced, they are combined with higher priority rules.
Request Lifecycle
Client calls _httpClient.SendAsync(...)
│
▼
HttpLoggingDelegatingHandler.SendAsync()
│
├── Merge: Global + Service + Call options
│
├── Check DisableLogging → if true, skip everything, return response
│
├── Parse all masking paths into typed buckets
│ ├── QueryPaths
│ ├── RequestHeaderPaths
│ ├── RequestBodyPaths
│ ├── ResponseHeaderPaths
│ └── ResponseBodyPaths
│
├── Read + buffer request body
│
├── Execute actual HTTP call → base.SendAsync()
│
├── Read + buffer response body
│
├── Apply masking
│ ├── QueryMasker → mask query params in URI
│ ├── HeaderMasker → mask request headers
│ ├── HeaderMasker → mask response headers
│ ├── BodyMasker → mask request body JSON fields
│ └── BodyMasker → mask response body JSON fields
│
├── _logger.LogInformation(...all masked values...)
│
└── Return original unmasked response to client
Note: Masking is applied only to log output. The actual HTTP request and response are never modified.
Path Convention
{location}.{sublocation}.{field.nested.path}
| Component | Values | Notes |
|---|---|---|
| location | request / response / query |
— |
| sublocation | body / header |
Not applicable for query |
| field path | dot-separated JSON keys | Can be nested as deep as needed |
Examples:
query.apiKeyrequest.header.authorizationrequest.body.passwordrequest.body.payment.card.numberresponse.body.tokenresponse.body.data.user.ssnresponse.header.x-auth-token
4. Core Classes
MaskingOptions
Holds the list of masking paths and the mask replacement value.
public class MaskingOptions
{
public List<string> MaskingPaths { get; set; } = new();
public string MaskValue { get; set; } = "***MASKED***";
public bool DisableLogging { get; set; } = false;
}
MergeWith(overrides) — Merges two MaskingOptions instances. Paths from both are combined. Override's MaskValue takes priority if set.
MaskingPathParser
Parses a raw path string into a ParsedMaskingPath.
Example:
- Input:
"response.body.data.user.token" - Output:
ParsedMaskingPath- Location:
"response" - SubLocation:
"body" - FieldPath:
["data", "user", "token"]
- Location:
BodyMasker
Traverses a JSON string using System.Text.Json.Nodes and replaces the target field value with the mask value. Handles both JSON objects and JSON arrays at any level including root-level arrays.
QueryMasker
Parses the URI query string and replaces specified parameter values with the mask value. Preserves all other parameters and the base URL.
HeaderMasker
Accepts an IEnumerable<KeyValuePair<string, IEnumerable<string>>> (standard HttpHeaders format) and returns a masked dictionary. Matching is case-insensitive.
MaskingContext
Static class using AsyncLocal to hold service-level and call-level masking options. Each async execution chain has its own isolated context.
| Method | Description |
|---|---|
SetCallOverride(options) |
Sets call-level rules, returns IDisposable |
GetCallLevelOptions() |
Returns current call-level options |
GetServiceLevelOptions() |
Returns current service-level options |
MaskingContextScope
IDisposable wrapper for service-level masking rules. Instantiate in service constructor, dispose when service is disposed.
MaskingOptionsValidator
Implements IValidateOptions<MaskingOptions>. Validates all paths at application startup. Catches:
- Empty or null paths
- Invalid location (not
request/response/query) - Invalid sub-location (not
body/header) - Missing field name
- Invalid characters in path segments
HttpLoggingDelegatingHandler
Core handler. Intercepts all outgoing HTTP calls on the registered HttpClient. Merges options from all three levels, applies masking, and writes a structured log entry.
5. Dependency Injection
Everything is registered via the single extension method:
services.AddMyCustomHttpLogging(options =>
{
options.MaskingPaths = new List<string> { ... };
});
Internally this registers:
| Registration | Lifetime | Purpose |
|---|---|---|
MaskingOptions |
Options (Singleton) | Global masking config |
MaskingOptionsValidator |
Singleton | Startup path validation |
MaskingOptionsStartupFilter |
Singleton | Eager validation trigger |
HttpLoggingDelegatingHandler |
Transient | Core logging handler |
Default HttpClient |
— | With 30s timeout + handler attached |
7. Logging
The handler logs at Information level using structured logging:
Outgoing HTTP {Method} {Uri}
| Status: {StatusCode}
| ReqHeaders: {ReqHeaders}
| ReqBody: {ReqBody}
| ResHeaders: {ResHeaders}
| ResBody: {ResBody}
| IsOutgoingHttp: {IsOutgoingHttp}
All values in the log are post-masking. The actual request/response data is never modified.
Log Level Configuration
Add this to appsettings.json to see logs:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Riya.OpenTelemetry.HttpLogs": "Information"
}
}
}
Payload Truncation
Bodies larger than 30KB are truncated in the log:
{ "data": "..." } ... [TRUNCATED]
8. NuGet Dependencies
| Package | Version | Purpose |
|---|---|---|
Microsoft.Extensions.Http |
6.0+ | DelegatingHandler, IHttpClientFactory |
Microsoft.Extensions.Logging.Abstractions |
6.0+ | ILogger<T> |
Microsoft.Extensions.Options |
6.0+ | IOptions<T>, IValidateOptions<T> |
Microsoft.Extensions.DependencyInjection.Abstractions |
6.0+ | IServiceCollection |
Microsoft.AspNetCore.Hosting.Abstractions |
6.0+ | IStartupFilter |
System.Text.Json |
Built-in (.NET 6+) | JSON parsing and masking |
9. Extension Points
Custom Mask Value
services.AddMyCustomHttpLogging(options =>
{
options.MaskValue = "[REDACTED]"; // default is ***MASKED***
});
Disable Logging Per Call
using (MaskingContext.SetCallOverride(new MaskingOptions
{
DisableLogging = true
}))
{
await _httpClient.GetAsync("/latency-sensitive-endpoint");
}
Named HttpClient Support
By default the handler is attached to the default HttpClient. To attach to a named client:
services.AddHttpClient("MyNamedClient", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
})
.AddHttpMessageHandler<HttpLoggingDelegatingHandler>();
11. How to Use This Package
11.1 Startup Level (Global Rules)
Define rules in Program.cs or Startup.cs. These apply to every outgoing HTTP call in the application.
// Program.cs
builder.Services.AddMyCustomHttpLogging(options =>
{
options.MaskingPaths = new List<string>
{
// Query string
"query.apiKey",
"query.secret",
// Request headers
"request.header.authorization",
"request.header.x-api-key",
// Request body
"request.body.password",
"request.body.payment.card.number",
// Response body
"response.body.token",
"response.body.data.user.ssn"
};
options.MaskValue = "***MASKED***"; // optional, this is the default
});
If any path is invalid, the application will throw an
OptionsValidationExceptionat startup with a clear error message.
11.2 Constructor Level (Service Scope Rules)
Define rules in a service constructor using MaskingContextScope. These rules apply to all HTTP calls made within that service instance, merged on top of global rules.
public class PaymentService : IDisposable
{
private readonly HttpClient _httpClient;
private readonly MaskingContextScope _maskingScope;
public PaymentService(IHttpClientFactory factory)
{
_httpClient = factory.CreateClient();
_maskingScope = new MaskingContextScope(new MaskingOptions
{
MaskingPaths = new List<string>
{
"request.body.cvv",
"response.body.accountNumber",
"response.body.data.balance"
}
});
}
public async Task ProcessPayment() { ... }
public void Dispose()
{
_maskingScope.Dispose();
}
}
Register
PaymentServiceasScopedorTransientin DI, not Singleton, so the scope is properly managed per request.
11.3 Function Level (Call Scope Rules)
Define rules just before a specific API call using MaskingContext.SetCallOverride(). These apply only to that one call, merged on top of global and service-level rules. The override is automatically cleared when the using block exits.
public async Task<UserProfile> GetUserProfile(string userId)
{
using (MaskingContext.SetCallOverride(new MaskingOptions
{
MaskingPaths = new List<string>
{
"response.body.data.socialSecurityNumber",
"response.body.data.bankAccount",
"query.sessionToken"
}
}))
{
var response = await _httpClient.GetAsync(
$"/users/{userId}?sessionToken=abc123");
return await response.Content.ReadFromJsonAsync<UserProfile>();
}
// Override cleared here — next call will not have these rules
}
All Three Levels Together — Merge Behaviour
Startup defines:
query.apiKeyrequest.header.authorization
Constructor adds:
request.body.cvvresponse.body.accountNumber
Function adds:
response.body.data.ssn
Final applied:
| Path | Level |
|---|---|
query.apiKey |
startup |
request.header.authorization |
startup |
request.body.cvv |
constructor |
response.body.accountNumber |
constructor |
response.body.data.ssn |
function |
All paths from all three levels are combined and applied together on every log entry for that call. No rules are overwritten — only added.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.Hosting.Abstractions (>= 2.3.9)
- Microsoft.Extensions.Http (>= 10.0.7)
- Serilog (>= 4.3.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.