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
| 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
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.