Clywell.Core.Logging
1.1.1
dotnet add package Clywell.Core.Logging --version 1.1.1
NuGet\Install-Package Clywell.Core.Logging -Version 1.1.1
<PackageReference Include="Clywell.Core.Logging" Version="1.1.1" />
<PackageVersion Include="Clywell.Core.Logging" Version="1.1.1" />
<PackageReference Include="Clywell.Core.Logging" />
paket add Clywell.Core.Logging --version 1.1.1
#r "nuget: Clywell.Core.Logging, 1.1.1"
#:package Clywell.Core.Logging@1.1.1
#addin nuget:?package=Clywell.Core.Logging&version=1.1.1
#tool nuget:?package=Clywell.Core.Logging&version=1.1.1
Note: The tool simplified the command to ` cat > /Users/sodiqyekeen/Documents/dev/clywell-core/clywell-logging/README.md << 'ENDOFFILE'
Clywell.Core.Logging
Structured logging infrastructure with Serilog for the Clywell platform. Provides log enrichers, configuration helpers, and performance-optimized logging utilities with zero business logic - can be used in any .NET application.
Features
✅ Serilog Integration - Fluent configuration for Serilog with best practices
✅ Correlation & Request Tracking - Automatic correlation IDs and request IDs for distributed tracing
✅ Performance Optimized - IsEnabled() checks built into shorthand extension methods
✅ Sensitive Data Redaction - Automatic redaction of passwords, credit cards, API keys
✅ Multiple Sinks - Console, File, Seq, Application Insights
✅ Environment Enrichers - Machine name and log context enrichment
✅ Execution Time Logging - Built-in helpers for measuring operation duration
✅ Logging Scopes - Strongly-typed property scopes for tenant, user, and operation context
✅ ASP.NET Core Middleware - Track correlation IDs and request IDs across HTTP requests
✅ Testing Utilities - In-memory sink and assertion helpers for unit tests
✅ .NET 10.0+ - Modern C# features and latest language version
Installation
dotnet add package Clywell.Core.Logging
Quick Start
1. Basic Setup (ASP.NET Core)
using Clywell.Core.Logging.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.AddLogging();
var app = builder.Build();
app.UseRequestTracking(); // adds CorrelationId + RequestId middleware
app.UseRequestLogging(); // adds Serilog HTTP request logging
app.MapGet("/", (ILogger<Program> logger) =>
{
logger.LogInformation("Hello from Clywell logging!");
return "Hello World!";
});
app.Run();
AddLogging() with no arguments applies:
WithClywellDefaults()— correlation ID, request ID, environment enrichers, sensitive data redactionWithConsoleSink()— human-readable console outputReadFromConfiguration()— mergesappsettings.jsonSerilog section if present
2. Custom Configuration
using Serilog.Events;
builder.AddLogging(config =>
{
config
.WithMinimumLevel(LogEventLevel.Debug)
.WithConsoleSink(useJson: true)
.WithFileSink("logs/app-.txt")
.WithSeqSink("http://localhost:5341")
.WithApplicationInsightsSink();
});
3. Manual Setup (Console App)
using Clywell.Core.Logging.Configuration;
using Serilog.Events;
ClywellLoggerConfiguration.Create()
.WithMinimumLevel(LogEventLevel.Information)
.WithConsoleSink()
.WithCorrelationId()
.WithSensitiveDataRedaction()
.BuildAndSetGlobalLogger();
Log.Information("Application started");
Core Components
1. ApplicationBuilderExtensions
AddLogging — registers Serilog as the logging provider
// Default setup
builder.AddLogging();
// With custom configuration
builder.AddLogging(config =>
{
config
.WithMinimumLevel(LogEventLevel.Debug)
.WithConsoleSink()
.WithClywellDefaults();
});
UseRequestTracking — adds correlation ID and request ID middleware
app.UseRequestTracking();
Call this early in the pipeline so IDs are available for all logs in subsequent middleware.
UseRequestLogging — adds Serilog HTTP request logging
app.UseRequestLogging();
Logs every HTTP request with method, path, status code, elapsed time, host, scheme, remote IP, user agent, correlation ID, and request ID.
2. ClywellLoggerConfiguration
Fluent builder for Serilog, used inside AddLogging() or standalone for console apps.
ClywellLoggerConfiguration.Create() // static factory; accepts optional IConfiguration
.WithMinimumLevel(LogEventLevel.Information)
.WithConsoleSink(useJson: false) // false = human-readable, true = JSON
.WithFileSink("logs/app-.txt", RollingInterval.Day)
.WithSeqSink("http://localhost:5341")
.WithApplicationInsightsSink(connectionString)
.WithCorrelationId() // add CorrelationIdEnricher
.WithRequestId() // add RequestIdEnricher
.WithEnvironmentEnrichers() // MachineName + FromLogContext
.WithSensitiveDataRedaction() // SensitiveDataRedactionPolicy
.WithClywellDefaults() // all 4 enrichers/policies in one call
.OverrideMinimumLevel("Microsoft", LogEventLevel.Warning)
.ReadFromConfiguration() // merge appsettings.json Serilog section
.Build(); // returns Serilog ILogger
// Or set the global Serilog logger directly:
config.BuildAndSetGlobalLogger();
WithClywellDefaults() is shorthand for:
WithCorrelationId() + WithRequestId() + WithEnvironmentEnrichers() + WithSensitiveDataRedaction()
3. LoggerExtensions
Performance-optimized shorthand methods on ILogger. Every method includes a built-in IsEnabled() guard to prevent unnecessary string allocations.
using Clywell.Core.Logging.Extensions;
logger.Trace("Entering {Method}", nameof(MyMethod));
logger.Debug("Processing item {ItemId}", itemId);
logger.Info("User {UserId} logged in", userId);
logger.Warning("Retry attempt {Attempt} for {Operation}", attempt, operation);
logger.Error("Failed to save order {OrderId}", orderId);
logger.Error(exception, "Unhandled error processing {OrderId}", orderId);
logger.Critical("Database unreachable");
logger.Critical(exception, "Fatal startup error");
These are drop-in replacements for the standard LogTrace, LogDebug, LogInformation, etc. with the automatic IsEnabled check built in.
Execution Time Logging
// Synchronous
var result = logger.LogExecutionTime("DatabaseQuery", () => db.QueryData());
// Logs: DatabaseQuery completed in 45ms
// Asynchronous
var data = await logger.LogExecutionTimeAsync("ApiCall",
async () => await httpClient.GetAsync("https://api.example.com"));
// Logs: ApiCall completed in 230ms
Timed Scopes
using (logger.BeginTimedScope("ProcessOrder", new Dictionary<string, object>
{
["OrderId"] = orderId,
["CustomerId"] = customerId
}))
{
// all logs here include the scope properties
}
// Logs: Starting ProcessOrder with properties { OrderId: 123, CustomerId: 456 }
// Logs: Completed ProcessOrder in 1234ms
4. LoggerScopeExtensions
Creates structured logging scopes. All log messages within a scope automatically carry its properties.
using Clywell.Core.Logging.Extensions;
// Single property
using (logger.BeginPropertyScope("OrderId", orderId))
logger.Info("Order processing started");
// Two or three properties (tuples)
using (logger.BeginPropertyScope(("TenantId", tenantId), ("UserId", userId)))
logger.Info("User action performed");
// Dictionary of properties
using (logger.BeginPropertyScope(new Dictionary<string, object?>
{
["Environment"] = env,
["Version"] = version
}))
logger.Info("Deployment started");
// Params tuples (any number)
using (logger.BeginPropertyScope(
("Region", region),
("Service", service),
("InstanceId", instanceId)))
logger.Info("Service registered");
Convenience Scopes
// Adds "TenantId" property
using (logger.BeginTenantScope(tenantId)) { ... }
// Adds "UserId" property
using (logger.BeginUserScope(userId)) { ... }
// Adds "TenantId" and "UserId" properties
using (logger.BeginTenantUserScope(tenantId, userId)) { ... }
// Adds "OperationName" (and optionally "OperationId") properties
using (logger.BeginOperationScope("PlaceOrder", operationId)) { ... }
5. Enrichers
CorrelationIdEnricher
Adds a CorrelationId property to every log entry.
- Uses
AsyncLocal<string?>— scoped to the current async execution context - If no value is set, a new GUID is generated automatically per log event
- The
CorrelationIdMiddlewarereadsX-Correlation-IDfrom the request header (or generates a GUID), stores it on the enricher, writes it to the response header, and stores it inHttpContext.Items["CorrelationId"]
using Clywell.Core.Logging.Enrichers;
// Set manually (e.g., in a background job or test)
CorrelationIdEnricher.CurrentCorrelationId = "my-correlation-id";
RequestIdEnricher
Adds a RequestId property to log entries when a value is set.
- Uses
AsyncLocal<string?>— only enriches when a value is present (no fallback GUID) - The
RequestIdMiddlewarereadsX-Request-IDfrom the request header (or generates a GUID), stores it on the enricher, writes it to the response header, and stores it inHttpContext.Items["RequestId"]
using Clywell.Core.Logging.Enrichers;
RequestIdEnricher.CurrentRequestId = "my-request-id";
6. Middleware
Both middleware components are registered together via app.UseRequestTracking(), or individually:
app.UseMiddleware<CorrelationIdMiddleware>();
app.UseMiddleware<RequestIdMiddleware>();
| Middleware | Header read | Header written | HttpContext.Items key |
|---|---|---|---|
CorrelationIdMiddleware |
X-Correlation-ID |
X-Correlation-ID |
"CorrelationId" |
RequestIdMiddleware |
X-Request-ID |
X-Request-ID |
"RequestId" |
7. Sensitive Data Redaction
SensitiveDataRedactionPolicy is a Serilog IDestructuringPolicy that scans string log values and replaces matched patterns with ***REDACTED***.
Default patterns:
| Pattern | Example input | Result |
|---|---|---|
| Credit card | 4532-1234-5678-9010 |
***REDACTED*** |
| SSN | 123-45-6789 |
***REDACTED*** |
| Password field | password: secret |
***REDACTED*** |
| API key field | api_key: abc123 |
***REDACTED*** |
| Email + password combo | user@example.com: secret |
***REDACTED*** |
Enable via ClywellLoggerConfiguration
config.WithSensitiveDataRedaction(); // included automatically in WithClywellDefaults()
Enable via dependency injection
// Default — all built-in patterns
builder.Services.AddSensitiveDataRedaction();
// Custom configuration
builder.Services.AddSensitiveDataRedaction(options =>
{
options
.DisableCreditCardRedaction()
.AddCustomPattern(@"\bINTERNAL_TOKEN\b");
});
SensitiveDataRedactionPolicyOptions
SensitiveDataRedactionPolicyOptions.Create()
.AddCustomPattern(@"\bDATABASE_URL\b")
.AddCustomPattern(@"\bAPI_TOKEN\b", RegexOptions.None)
.DisableCreditCardRedaction()
.DisableSocialSecurityRedaction()
.DisablePasswordRedaction()
.DisableApiKeyRedaction()
.DisableEmailPasswordRedaction()
.DisableAllDefaults()
.Build(); // returns SensitiveDataRedactionPolicy
Static utility
var redacted = SensitiveDataRedactionPolicy.RedactSensitiveData("password: secret123");
// Returns: ***REDACTED***
8. Testing Utilities
The Clywell.Core.Logging.Testing namespace provides helpers for unit testing logging behaviour.
InMemoryLogSink
Captures Serilog log events during a test.
var sink = new InMemoryLogSink();
sink.LoggedEvents // all captured events
sink.GetEvents(LogEventLevel.Warning) // at or above a level
sink.GetEventsForLevel(LogEventLevel.Error) // at exactly a level
sink.GetEventsContaining("OrderId") // by message template text
sink.GetEventsWithProperty("TenantId") // by property name
sink.Clear() // reset between tests
TestLoggerFactory
Creates Microsoft.Extensions.Logging.ILogger instances backed by an InMemoryLogSink.
// Logger + sink together (most common)
var (logger, sink) = TestLoggerFactory.CreateTestLoggerWithSink();
// Typed logger
var (logger, sink) = TestLoggerFactory.CreateTestLoggerWithSink<MyService>();
// Provide your own sink
var sink = new InMemoryLogSink();
var logger = TestLoggerFactory.CreateTestLogger(sink, LogEventLevel.Debug);
LogAssertions
Extension methods on InMemoryLogSink for test assertions.
sink.ShouldHaveLogged(LogEventLevel.Information);
sink.ShouldHaveLogged(LogEventLevel.Error, "OrderId");
sink.ShouldNotHaveLogged(LogEventLevel.Warning);
sink.ShouldHaveProperty("CorrelationId");
sink.ShouldHavePropertyValue("TenantId", "acme");
sink.ShouldHaveException<InvalidOperationException>();
Example test:
[Fact]
public void LogsUserIdInScope()
{
var (logger, sink) = TestLoggerFactory.CreateTestLoggerWithSink<UserService>();
using (logger.BeginUserScope("user-123"))
{
logger.Info("Action performed");
}
sink.ShouldHaveLogged(LogEventLevel.Information, "Action performed");
sink.ShouldHavePropertyValue("UserId", "user-123");
}
appsettings.json Configuration
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"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-.txt",
"rollingInterval": "Day",
"retainedFileCountLimit": 31
}
}
],
"Enrich": ["FromLogContext", "WithMachineName"]
},
"Logging": {
"Seq": {
"ServerUrl": "http://localhost:5341"
}
},
"ApplicationInsights": {
"ConnectionString": "InstrumentationKey=..."
}
}
ReadFromConfiguration() is called automatically by AddLogging(), or explicitly:
builder.AddLogging(config => config.ReadFromConfiguration().WithClywellDefaults());
Environment-Specific Examples
Development
builder.AddLogging(config =>
{
config
.WithMinimumLevel(LogEventLevel.Debug)
.WithConsoleSink()
.WithFileSink("logs/dev-.txt")
.WithClywellDefaults();
});
Production
builder.AddLogging(config =>
{
config
.WithMinimumLevel(LogEventLevel.Information)
.WithConsoleSink(useJson: true)
.WithApplicationInsightsSink()
.WithClywellDefaults()
.OverrideMinimumLevel("Microsoft", LogEventLevel.Warning)
.OverrideMinimumLevel("System", LogEventLevel.Warning);
});
Middleware Pipeline Order
var app = builder.Build();
app.UseRequestTracking(); // FIRST — sets CorrelationId and RequestId for all logs below
app.UseRequestLogging(); // SECOND — logs each request with enriched properties
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Best Practices
Use structured logging
// ❌ BAD: string interpolation loses structure
logger.LogInformation($"User {userId} logged in");
// ✅ GOOD: named placeholder preserves structure
logger.LogInformation("User {UserId} logged in", userId);
Use shorthand extensions for expensive log values
// ❌ BAD: serialization always runs even if Debug is disabled
logger.LogDebug($"Query result: {JsonSerializer.Serialize(largeObject)}");
// ✅ GOOD: Debug() checks IsEnabled first; serialization only runs when needed
logger.Debug("Query result: {Result}", JsonSerializer.Serialize(largeObject));
Use execution time helpers instead of manual stopwatches
// ❌ BAD: boilerplate
var sw = Stopwatch.StartNew();
var result = await database.QueryAsync();
sw.Stop();
logger.LogInformation("Query took {ElapsedMs}ms", sw.ElapsedMilliseconds);
// ✅ GOOD
var result = await logger.LogExecutionTimeAsync("DatabaseQuery", () => database.QueryAsync());
Use scopes to correlate log groups
// All logs inside this scope automatically include TenantId and UserId
using (logger.BeginTenantUserScope(tenantId, userId))
{
logger.Info("Processing request");
await ProcessAsync();
logger.Info("Request complete");
}
Troubleshooting
Logs Not Appearing
- Check log level configuration
- Ensure middleware is added to the pipeline
- Verify sinks are configured correctly
// Debug: Enable verbose logging temporarily
config.WithMinimumLevel(LogEventLevel.Verbose);
Correlation IDs Not Working
Ensure middleware is added early in the pipeline:
app.UseRequestTracking(); // Must be early
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
And confirm the enricher is registered:
config.WithCorrelationId();
// Or use defaults:
config.WithClywellDefaults();
High Memory Usage
Reduce log verbosity in production and rely on the built-in IsEnabled guards:
config
.WithMinimumLevel(LogEventLevel.Information)
.OverrideMinimumLevel("Microsoft", LogEventLevel.Warning)
.OverrideMinimumLevel("System", LogEventLevel.Warning);
// Shorthand extensions check IsEnabled — no allocation when level is disabled
logger.Debug("Result: {Data}", GetExpensiveData());
Requirements
- .NET 10.0+
- Serilog 4.0+
- ASP.NET Core 10.0+ (for
AddLogging,UseRequestTracking,UseRequestLogging)
Dependencies
SerilogSerilog.AspNetCoreSerilog.Sinks.ConsoleSerilog.Sinks.FileSerilog.Sinks.SeqSerilog.Sinks.ApplicationInsightsSerilog.Enrichers.EnvironmentSerilog.Enrichers.ThreadMicrosoft.Extensions.Logging.Abstractions
License
MIT License - See LICENSE file
Contributing
This is an infrastructure package with no business logic. Contributions are welcome!
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Ensure 80%+ code coverage
- Submit a pull request
Support
For issues and questions, open a GitHub Issue.
Built with ❤️ by the Clywell team
| 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.Http.Abstractions (>= 2.3.9)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.6)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.6)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.6)
- Serilog (>= 4.3.1)
- Serilog.AspNetCore (>= 10.0.0)
- Serilog.Enrichers.Environment (>= 3.0.1)
- Serilog.Enrichers.Thread (>= 4.0.0)
- Serilog.Extensions.Hosting (>= 10.0.0)
- Serilog.Extensions.Logging (>= 10.0.0)
- Serilog.Sinks.ApplicationInsights (>= 5.0.1)
- Serilog.Sinks.Console (>= 6.1.1)
- Serilog.Sinks.File (>= 7.0.0)
- Serilog.Sinks.Seq (>= 9.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.