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
Section titled “Log levels”| Level | Examples |
|---|---|
Debug | Successful retry attempts, individual offset commits |
Information | Circuit breaker closed, consumer group started/stopped |
Warning | Failed retry attempts, messages discarded, validation failures, circuit breaker opened, dead-letter forwards |
Error | Unhandled exceptions from the worker, DLQ sink failures |
Emit does not use Critical or Trace.
Filtering Emit logs
Section titled “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
Section titled “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
Section titled “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
Section titled “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.