Skip to content

Logging

Emit uses Microsoft.Extensions.Logging throughout and follows standard .NET structured logging conventions. It does not configure any logging providers; log output goes wherever your application routes it.

Log levels

LevelExamples
DebugSuccessful retry attempts, individual offset commits
InformationCircuit breaker closed, consumer group started/stopped
WarningFailed retry attempts, messages discarded, validation failures, circuit breaker opened, dead-letter forwards
ErrorUnhandled exceptions from the worker, DLQ sink failures

Emit does not use Critical or Trace.

Filtering Emit logs

All Emit loggers use categories that start with Emit.. To suppress verbose logs in development:

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Emit": "Warning"
}
}
}

To see only specific subsystems:

{
"Logging": {
"LogLevel": {
"Emit.Outbox": "Debug",
"Emit": "Warning"
}
}
}

Structured log properties

Emit uses named placeholders in log messages, so structured logging backends (Serilog, Seq, Application Insights) capture them as queryable properties:

Retry {Attempt}/{MaxAttempts} failed for message from {Source}
Dead-lettered message from {Source} to {DlqDestination}
Validation failed for message from {Source}: {Errors}. Discarding.
Circuit breaker opened, pausing consumer group. Pause duration: {PauseDuration}

{Source} is a compact string derived from the TransportContext that includes topic, partition, and offset (e.g., orders[2]@1547). In a structured log query you can filter on Source to find all events for a specific partition.

Adding log context in consumers

Standard ILogger scopes work in consumers because each message runs in its own scope. Wrap your consumer logic in a scope for richer log context:

public class OrderPlacedConsumer(ILogger<OrderPlacedConsumer> logger) : IConsumer<OrderPlaced>
{
public async Task ConsumeAsync(ConsumeContext<OrderPlaced> context, CancellationToken ct)
{
using var scope = logger.BeginScope(new Dictionary<string, object>
{
["OrderId"] = context.Message.OrderId,
["CustomerId"] = context.Message.CustomerId,
});
logger.LogInformation("Processing order");
// ...
}
}

Logging middleware

For cross-cutting log context (adding a tenant ID to every message log, for example), use middleware to open a scope once and let all downstream code benefit:

public class TenantLoggingMiddleware<T>(ILogger<TenantLoggingMiddleware<T>> logger, ITenantResolver tenant)
: IMiddleware<ConsumeContext<T>>
{
public async Task InvokeAsync(ConsumeContext<T> context, IMiddlewarePipeline<ConsumeContext<T>> next)
{
var tenantId = await tenant.ResolveAsync(context, context.CancellationToken);
using var scope = logger.BeginScope(new { TenantId = tenantId });
await next.InvokeAsync(context);
}
}

Register it at the global or provider level to apply it to all consumers.