Skip to content

Instantly share code, notes, and snippets.

@boarnoah
Created October 28, 2025 17:56
Show Gist options
  • Select an option

  • Save boarnoah/7413168d37e8d8558ccf6a4979e7463e to your computer and use it in GitHub Desktop.

Select an option

Save boarnoah/7413168d37e8d8558ccf6a4979e7463e to your computer and use it in GitHub Desktop.
Experimenting with using Log
#:sdk Microsoft.NET.Sdk.Worker
#:package Microsoft.Extensions.Hosting@9.0.10
#:package Microsoft.Extensions.Telemetry@9.10.0
// https://github.com/dotnet/runtime/issues/35995
// https://learn.microsoft.com/en-us/dotnet/core/enrichment/custom-enricher
using System.Text.Json;
using Microsoft.Extensions.Diagnostics.Enrichment;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddScoped<MyScopedService>();
// Adds a service, very similar to HttpContextAccessor, in this case to hold tenant context info
builder.Services.AddSingleton<TenantContextAccessor>();
builder.Services.AddLogEnricher<TenantLogEnricher>();
builder.Logging.AddJsonConsole(
options => options.JsonWriterOptions = new JsonWriterOptions()
{
Indented = true
}
);
builder.Logging.EnableEnrichment();
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();
public class Worker(
ILogger<Worker> logger,
IHostApplicationLifetime appLifeTime,
TenantContextAccessor tenantContextAccessor,
IServiceScopeFactory scopeFactory
) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var tenants = new List<string>{ "TenantA", "TenantB"};
var i = 0;
await Parallel.ForEachAsync(tenants, stoppingToken, async (tenant, token) =>
{
var tenantLetter = tenant.Last().ToString();
logger.LogInformation("Starting processing for a tenant {TenantLetter} in separate thread", tenantLetter);
// ILogEnrichers have to be registered as singletons to work -> which in turn restricts us to only inject
// other singletons to them, hence the need for this HttpContextAccessor-like pattern
tenantContextAccessor.TenantContext = new TenantContextObject()
{
Tenant = tenant
};
using var scope = scopeFactory.CreateScope();
var myService = scope.ServiceProvider.GetRequiredService<MyScopedService>();
myService.DoTheThing(tenantLetter);
});
logger.LogInformation("All done");
appLifeTime.StopApplication();
}
}
public class MyScopedService(
ILogger<MyScopedService> logger
) {
public void DoTheThing(string i){
logger.LogInformation("Done the thing: {thing}", i);
}
}
internal class TenantLogEnricher(TenantContextAccessor tenantContext) : ILogEnricher
{
public void Enrich(IEnrichmentTagCollector collector)
{
if (!string.IsNullOrEmpty(tenantContext.TenantContext?.Tenant))
{
collector.Add("Tenant", tenantContext.TenantContext.Tenant);
}
}
}
// Ripped from https://github.com/dotnet/aspnetcore/blob/5dde6e4691f73763cc31ce3934e6a37fd9707188/src/Http/Http/src/HttpContextAccessor.cs
public class TenantContextAccessor
{
private static readonly AsyncLocal<TenantContextHolder> _tenantContextCurrent = new();
public TenantContextObject? TenantContext
{
get
{
return _tenantContextCurrent.Value?.Context;
}
set
{
if (_tenantContextCurrent.Value is not null)
{
_tenantContextCurrent.Value.Context = null;
}
if (value != null)
{
// Use an object indirection to hold the HttpContext in the AsyncLocal,
// so it can be cleared in all ExecutionContexts when its cleared.
_tenantContextCurrent.Value = new TenantContextHolder { Context = value };
}
}
}
private sealed class TenantContextHolder
{
public TenantContextObject? Context;
}
}
public class TenantContextObject
{
public string Tenant { get; set; }
}
@boarnoah
Copy link
Author

You get nice structured logs with your properties like:

{
  "EventId": 0,
  "LogLevel": "Information",
  "Category": "ILoggerEnrichmentTest.MyScopedService",
  "Message": "Done the thing: C",
  "State": {
    "Tenant": "TenantC",
    "thing": "C",
    "{OriginalFormat}": "Done the thing: {thing}"
  }
}
{
  "EventId": 0,
  "LogLevel": "Information",
  "Category": "ILoggerEnrichmentTest.Worker",
  "Message": "All done",
  "State": {
    "{OriginalFormat}": "All done"
  }
}

A much better state than how something similar with scopes look:

{
  "EventId": 0,
  "LogLevel": "Information",
  "Category": "ILoggerScopeTest.MyScopedService",
  "Message": "Done the thing: C",
  "State": {
    "Message": "Done the thing: C",
    "thing": "C",
    "{OriginalFormat}": "Done the thing: {thing}"
  },
  "Scopes": [
    {
      "Message": "System.Collections.Generic.Dictionary\u00602[System.String,System.Object]",
      "TenantId": "TenantC"
    }
  ]
}
{
  "EventId": 0,
  "LogLevel": "Information",
  "Category": "ILoggerScopeTest.Worker",
  "Message": "All done",
  "State": {
    "Message": "All done",
    "{OriginalFormat}": "All done"
  },
  "Scopes": []
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment