Logging
Overview
ASOL.Core.Logging adds application-defined classifications to standard .NET logging. Use classifications to identify entries such as order audits or business events while keeping the existing logger category, log level, event ID, and exception.
The package targets .NET 8 and .NET 10 and depends on Microsoft.Extensions.Logging.Abstractions. It works directly with ILogger and ILogger<T> and uses the application's existing logging providers and configuration.
Install and setup
Add a reference to the ASOL.Core.Logging NuGet package and import the ASOL.Core.Logging namespace. Continue injecting ILogger<T> as usual. No additional dependency-injection registration, logger adapter, or configuration section is required.
Provider and exporter setup remains the responsibility of the host. For the Core telemetry and logging configuration, see Telemetry.
Define classifications
Applications define their own classification names without changing the package. Reuse named values to keep classification names consistent across call sites:
using ASOL.Core.Logging;
public static class OrderLogClassifications
{
public static readonly LogClassification Audit = new("Orders.Audit");
public static readonly LogClassification Business = new("Orders.Business");
}
Names use ordinal, case-sensitive equality: Audit and audit are different classifications. Null, empty, and whitespace-only names are rejected. The exact name Default is reserved for fallback behavior.
Write classified entries
Pass the classification before the message template:
using ASOL.Core.Logging;
using Microsoft.Extensions.Logging;
public sealed class OrderService(ILogger<OrderService> logger)
{
public void RecordCreated(int orderId)
{
logger.LogInformation(OrderLogClassifications.Audit,
"Order {OrderId} created", orderId);
}
public void RecordReviewRequired(int orderId)
{
logger.LogWarning(OrderLogClassifications.Business,
new EventId(1001, "OrderReviewRequired"),
"Order {OrderId} requires review", orderId);
}
public void RecordFailure(int orderId, Exception exception)
{
logger.LogError(OrderLogClassifications.Audit,
new EventId(1002, "OrderFailed"), exception,
"Order {OrderId} failed", orderId);
}
}
LogInformation, LogWarning, and LogError each provide message-template overloads with an optional event ID, exception, or both. For other levels, use the general Log overload and supply the event ID, exception, and argument array explicitly:
logger.Log(LogLevel.Debug, OrderLogClassifications.Business,
default, null, "Inspecting order {OrderId}", [orderId]);
Use constant message templates and pass values as arguments to preserve structured properties. Arguments match placeholders in their order of appearance. See the .NET logging documentation for the underlying message-template conventions.
Structured attributes and export
For a custom classification, the package adds a string property named LogClassification to the structured state passed to the logging provider. For example, the created-order entry above contains:
| Property | Value |
|---|---|
OrderId | The supplied order ID |
LogClassification | Orders.Audit |
{OriginalFormat} | Order {OrderId} created |
The classification does not change the formatted message. Reserve the property name LogClassification for this metadata rather than using it as a message-template placeholder.
Providers determine how structured properties are displayed and stored. With an OpenTelemetry logging provider configured with AddOpenTelemetry and AddOtlpExporter, the classification is exported as a log-record attribute. Scope capture (IncludeScopes) is not required because the property is carried in the entry state.
ASOL.Core.Logging does not add an exporter or an OpenTelemetry runtime dependency. Classification names also do not configure provider filters or routing; the existing logging configuration still controls enabled levels and destinations.
Default behavior
LogClassification.Default, default(LogClassification), and new LogClassification("Default") are equivalent. These calls forward to Microsoft's standard logging extensions without adding classification metadata:
logger.LogInformation(LogClassification.Default,
"Order {OrderId} created", orderId);
Any structured properties already supplied by the caller remain unchanged. Ordinary calls such as logger.LogInformation("Order {OrderId} created", orderId) continue to use the standard logging API.
Performance
Disabled custom entries are skipped before message-template parsing or formatting. Parsed templates are shared across classifications in a thread-safe cache limited to 1,024 templates; additional templates continue to work without growing the cache. Formatting is deferred until requested by a provider.
The package uses value-type message state, but calls can still allocate argument arrays, boxed values, provider state, or formatted strings. Reuse constant templates rather than building a different template for each entry.