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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Riya.OpenTelemetry.HttpLogs" Version="1.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Riya.OpenTelemetry.HttpLogs" Version="1.0.3" />
                    
Directory.Packages.props
<PackageReference Include="Riya.OpenTelemetry.HttpLogs" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Riya.OpenTelemetry.HttpLogs --version 1.0.3
                    
#r "nuget: Riya.OpenTelemetry.HttpLogs, 1.0.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Riya.OpenTelemetry.HttpLogs@1.0.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Riya.OpenTelemetry.HttpLogs&version=1.0.3
                    
Install as a Cake Addin
#tool nuget:?package=Riya.OpenTelemetry.HttpLogs&version=1.0.3
                    
Install as a Cake Tool

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
  • DisableLogging flag 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.apiKey
  • request.header.authorization
  • request.body.password
  • request.body.payment.card.number
  • response.body.token
  • response.body.data.user.ssn
  • response.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"]

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 OptionsValidationException at 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 PaymentService as Scoped or Transient in 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.apiKey
  • request.header.authorization

Constructor adds:

  • request.body.cvv
  • response.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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.3 136 5/13/2026
1.0.2 153 5/8/2026
1.0.1 114 5/7/2026