- Skill ID
- codewithmukesh/dotnet-claude-kit/serilog
- Publisher
- codewithmukesh
- Repository
- dotnet-claude-kit
- Installs
- 0
- Files
- 1
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
codewithmukesh/dotnet-claude-kit/serilogInstalls these files- SKILL.md
What this skill tells the agent
Serilog
Core Principles
- Two-stage initialization — Create a bootstrap logger for startup, then replace it with the full logger after DI is ready. This captures startup errors that would otherwise be lost.
- `AddSerilog()` over `UseSerilog()` — Use
builder.Services.AddSerilog()(the modern API) instead ofbuilder.Host.UseSerilog(). It integrates with DI services viaReadFrom.Services(services). - Message templates, not interpolation —
{PropertyName}syntax creates structured data that can be queried. String interpolation ($"...") breaks structure and allocates even when the log level is disabled. - Configure via appsettings.json — Keep log levels, sinks, and overrides in configuration so they can change per environment without redeployment.
Patterns
Two-Stage Bootstrap Setup
using Serilog;
// Stage 1: Bootstrap logger — captures startup errors before DI
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Console()
.CreateBootstrapLogger();
try
{
Log.Information("Starting application");
var builder = WebApplication.CreateBuilder(args);
// Stage 2: Full logger with DI and configuration
builder.Services.AddSerilog((services, lc) => lc
.ReadFrom.Configuration(builder.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
.Enrich.WithProperty("Application", "MyApp.Api"));
var app = builder.Build();
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
{
diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
diagnosticContext.Set("UserAgent",
httpContext.Request.Headers.UserAgent.ToString());
};
});
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
await Log.CloseAndFlushAsync();
}appsettings.json Configuration
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "logs/app-.log",
"rollingInterval": "Day",
"retainedFileCountLimit": 30,
"fileSizeLimitBytes": 104857600
}
},
{
"Name": "Seq",
"Args": { "serverUrl": "http://localhost:5341" }
}
],
"Enrich": ["FromLogContext", "WithMachineName", "WithEnvironmentName"],
"Destructure": [
{ "Name": "ToMaximumDepth", "Args": { "maximumDestructuringDepth": 4 } },
{ "Name": "ToMaximumStringLength", "Args": { "maximumStringLength": 1024 } },
{ "Name": "ToMaximumCollectionCount", "Args": { "maximumCollectionCount": 10 } }
]
}
}Override section uses namespace prefixes matched against SourceContext. More specific prefixes take precedence.
Request Logging Middleware
Replaces the multiple per-request log events from ASP.NET Core with a single summary event.
app.UseSerilogRequestLogging(options =>
{
options.MessageTemplate =
"HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms";
options.GetLevel = (httpContext, elapsed, ex) => ex is not null
? LogEventLevel.Error
: httpContext.Response.StatusCode >= 500
? LogEventLevel.Error
: LogEventLevel.Information;
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
{
diagnosticContext.Set("UserId",
httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous");
};
});Structured Logging and Destructuring
// Named properties — creates queryable structured data
logger.LogInformation("Order {OrderId} placed by {CustomerId} for {Total:C}",
orderId, customerId, total);
// @ operator preserves object structure as properties
logger.LogInformation("Processing {@SensorInput}", sensorInput);
// Output: Processing {"Latitude": 25, "Longitude": 134}
// $ operator forces ToString()
logger.LogInformation("Received {$Data}", new[] { 1, 2, 3 });
// Output: Received "System.Int32[]"Scoped Properties with LogContext
using (LogContext.PushProperty("CorrelationId", correlationId))
using (LogContext.PushProperty("TenantId", tenantId))
{
logger.LogInformation("Processing order {OrderId}", orderId);
// CorrelationId and TenantId attached to ALL log events in this scope
}Requires .Enrich.FromLogContext() on the logger configuration.
OpenTelemetry Sink (OTLP Export)
Export Serilog events directly to any OTLP backend without the OpenTelemetry SDK:
.WriteTo.OpenTelemetry(options =>
{
options.Endpoint = "http://localhost:4317";
options.Protocol = OtlpProtocol.Grpc;
options.ResourceAttributes = new Dictionary<string, object>
{
["service.name"] = "MyApp.Api",
["deployment.environment"] = "production"
};
})Serilog.Expressions for Filtering
Requires the Serilog.Expressions package.
// Exclude health check noise
.Filter.ByExcluding("RequestPath like '/health%'")
// Route errors to a separate file
.WriteTo.Conditional("@l = 'Error'",
wt => wt.File("logs/errors-.log", rollingInterval: RollingInterval.Day))[LoggerMessage] Source Generator for Hot Paths
Built into Microsoft.Extensions.Logging.Abstractions — compile-time generated, zero allocations when the level is disabled.
public static partial class OrderLogs
{
[LoggerMessage(Level = LogLevel.Information,
Message = "Order {OrderId} created for {CustomerId}")]
public static partial void OrderCreated(this ILogger logger, Guid orderId, Guid customerId);
}
// Usage
logger.OrderCreated(order.Id, order.CustomerId);Anti-patterns
Don't Use String Interpolation
// BAD — breaks structured logging, allocates even when level is disabled
logger.LogInformation($"Order {orderId} created for {customerId}");
// GOOD — message template with named parameters
logger.LogInformation("Order {OrderId} created for {CustomerId}", orderId, customerId);Don't Skip CloseAndFlush
// BAD — async sinks (Seq, OTLP, Elasticsearch) lose buffered events
app.Run();
// GOOD — wrap in try/finally
try { app.Run(); }
catch (Exception ex) { Log.Fatal(ex, "Unhandled exception"); }
finally { await Log.CloseAndFlushAsync(); }Don't Log Sensitive Data
// BAD — passwords and tokens in logs
logger.LogInformation("Login: {Email} with password {Password}", email, password);
// GOOD — never log secrets, passwords, tokens, or PII
logger.LogInformation("Login: {Email}", email);Don't Destructure Without Limits
// BAD — large object graphs cause memory issues and massive log entries
logger.LogInformation("Request: {@Request}", httpContext.Request);
// GOOD — configure destructuring limits
.Destructure.ToMaximumDepth(4)
.Destructure.ToMaximumStringLength(1024)
.Destructure.ToMaximumCollectionCount(10)
// BETTER — destructure to specific properties
.Destructure.ByTransforming<HttpRequest>(r => new { r.Method, r.Path })Don't Use the Deprecated Elasticsearch Sink
// BAD — the Serilog.Sinks.Elasticsearch PACKAGE is deprecated
// <PackageReference Include="Serilog.Sinks.Elasticsearch" />More from codewithmukesh
Use this skill in a project
Open any RiverX project, search the Skills panel for codewithmukesh/dotnet-claude-kit/serilog, and install it in one click.
