![]() |
VOOZH | about |
dotnet add package EonaCat.LogStack --version 0.1.1
NuGet\Install-Package EonaCat.LogStack -Version 0.1.1
<PackageReference Include="EonaCat.LogStack" Version="0.1.1" />
<PackageVersion Include="EonaCat.LogStack" Version="0.1.1" />Directory.Packages.props
<PackageReference Include="EonaCat.LogStack" />Project file
paket add EonaCat.LogStack --version 0.1.1
#r "nuget: EonaCat.LogStack, 0.1.1"
#:package EonaCat.LogStack@0.1.1
#addin nuget:?package=EonaCat.LogStack&version=0.1.1Install as a Cake Addin
#tool nuget:?package=EonaCat.LogStack&version=0.1.1Install as a Cake Tool
EonaCat.LogStack is a flow-based, high-performance logging library for .NET, designed for zero-allocation logging paths and superior memory efficiency. It features a rich fluent API for routing log events to dozens of destinations - from console and file to Slack, Discord, Redis, Elasticsearch, and beyond.
AggressiveInlining throughout, StringBuilder pooling, and ref-based builder pattern for minimal GC pressure during logging.IAsyncDisposable and FlushAsync() for proper async resource cleanup and batching.RetryFlow with exponential backoff, FailoverFlow for primary/secondary failover, and ThrottledFlow for rate limiting with deduplication.DecryptFile() utility to decrypt files on demand.dotnet add package EonaCat.LogStack
await using var logger = LogBuilder.CreateDefault("MyApp");
logger.Information("Application started");
logger.Warning("Low memory warning");
logger.Error(ex, "Unexpected error occurred");
// Automatic cleanup on disposal
CreateDefault creates a logger writing to both the console and a ./logs directory, enriched with machine name and process ID.
Build a fully customized logger using LogBuilder:
await using var logger = new LogBuilder("MyApp")
.WithMinimumLevel(LogLevel.Debug)
.WithTimestampMode(TimestampMode.Utc)
.WriteToConsole(useColors: true)
.WriteToFile("./logs", filePrefix: "app", maxFileSize: 50 * 1024 * 1024)
.WriteToSlack("https://hooks.slack.com/services/...")
.BoostWithMachineName()
.BoostWithProcessId()
.BoostWithCorrelationId()
.Build();
try
{
logger.Information("Application started");
// Your application code...
}
finally
{
await logger.FlushAsync();
await logger.DisposeAsync();
}
logger.Trace("Verbose trace message");
logger.Debug("Debug detail");
logger.Information("Something happened");
logger.Warning("Potential problem");
logger.Error("Something failed");
logger.Critical("System is going down");
try
{
// risky operation
}
catch (Exception ex)
{
logger.Warning(ex, "Operation failed, attempting retry");
logger.Error(ex, "Operation failed after retries");
logger.Critical(ex, "Critical failure, shutting down");
}
// With tuples (preferred for performance)
logger.Log(LogLevel.Information, "User logged in",
("UserId", 42),
("IP", "192.168.1.1"),
("Session", "abc-123"));
// Properties appear in most flows (database, JSON output, etc.)
// In file output: `UserId=42, IP=192.168.1.1, Session=abc-123`
logger.AddModifier((ref LogEventBuilder builder) =>
{
builder.WithProperty("RequestId", HttpContext.TraceIdentifier);
builder.WithProperty("UserId", User.Id);
});
logger.Information("Processing request"); // RequestId and UserId added automatically
Flows are the destinations where log events are written. Each flow is independent and can have its own level filter and configuration.
| Flow | Method | Description |
|---|---|---|
| Console | WriteToConsole() |
Colored console output with customizable templates |
| File | WriteToFile() |
Batched, rotated, compressed file output with retention policies |
| Encrypted File | WriteToEncryptedFile() |
AES-encrypted log files with password protection |
| Memory | WriteToMemory() |
In-memory ring buffer for quick access and diagnostics |
| Audit | WriteToAudit() |
Tamper-evident hash-chained audit trail with verification |
| Database | WriteToDatabase() |
ADO.NET database sink with custom table support |
| HTTP | WriteToHttp() |
Generic HTTP endpoint with custom headers and batching |
| Webhook | WriteToWebhook() |
Generic webhook POST endpoint |
WriteToEmail() |
HTML digest emails via SMTP with configurable batching | |
| Slack | WriteToSlack() |
Slack incoming webhooks with message formatting |
| Discord | WriteToDiscord() |
Discord webhooks with embed formatting |
| Microsoft Teams | WriteToMicrosoftTeams() |
Teams incoming webhooks with adaptive cards |
| Telegram | WriteToTelegram() |
Telegram bot messages |
| SignalR | WriteToSignalR() |
Real-time SignalR hub push for live dashboards |
| Redis | RedisFlow() |
Redis Pub/Sub + optional List persistence with reconnect |
| Elasticsearch | WriteToElasticSearch() |
Elasticsearch index with custom index names |
| Splunk | WriteToSplunkFlow() |
Splunk HEC (HTTP Event Collector) |
| Graylog | WriteToGraylogFlow() |
GELF over UDP or TCP |
| Syslog UDP | WriteToSyslogUdp() |
RFC-5424 Syslog over UDP |
| Syslog TCP | WriteToSyslogTcp() |
RFC-5424 Syslog over TCP with optional TLS |
| TCP | WriteToTcp() |
Raw TCP with optional TLS support |
| UDP | WriteToUdp() |
Raw UDP datagrams |
| SNMP Trap | WriteToSnmpTrap() |
SNMP v2c traps for network monitoring |
| Zabbix | WriteToZabbixFlow() |
Zabbix trapper protocol |
| EventLog | WriteToEventLogFlow() |
Remote Windows event log forwarding |
| Rolling Buffer | WriteToRollingBuffer() |
Circular buffer with trigger-based context flushing |
| Throttled | WriteToThrottled() |
Token-bucket rate limiting with optional deduplication |
| Retry | WriteToRetry() |
Automatic retry with exponential back-off |
| Failover | WriteToFailover() |
Primary/secondary failover with recovery detection |
| Diagnostics | WriteDiagnostics() |
Periodic diagnostic snapshots and metrics |
| Status | WriteToStatusFlow() |
Service health monitoring |
| Conditional | WriteToConditional() |
Route logs based on custom predicates |
| Circuit Breaker | WriteToCircuitBreaker() |
Protect against cascading failures |
// Basic colored output
.WriteToConsole(useColors: true)
// Minimal console (no colors)
.WriteToConsole(useColors: false)
// Only warnings and above to console
.WriteToConsole(minimumLevel: LogLevel.Warning, useColors: true)
// Basic file logging
.WriteToFile("./logs")
// Custom configuration
.WriteToFile(
directory: "./logs",
filePrefix: "myapp",
maxFileSize: 100 * 1024 * 1024, // 100 MB
maxDirectorySize: 10L * 1024 * 1024 * 1024, // 10 GB total
flushIntervalInMilliSeconds: 2000,
batchSize: 50,
compression: CompressionFormat.GZip,
outputFormat: FileOutputFormat.Text)
// Category-based routing (separate files per category)
.WriteToFile(
directory: "./logs",
useCategoryRouting: true)
// Level-based routing (separate files per log level)
.WriteToFile(
directory: "./logs",
logLevelsForSeparateFiles: new[] { LogLevel.Error, LogLevel.Critical })
// Encrypt logs with password
.WriteToEncryptedFile(
directory: "./secure-logs",
password: "MySecurePassword123!")
// Decrypt later when needed
LogBuilder.DecryptFile(
encryptedPath: "./secure-logs/log.enc",
outputPath: "./secure-logs/log.txt",
password: "MySecurePassword123!");
// Store last 1000 events in memory
.WriteToMemory(capacity: 1000, minimumLevel: LogLevel.Information)
// Access stored events
var memoryFlow = logger.GetFlowOfType<MemoryFlow>();
var events = memoryFlow.GetEvents();
.WriteToSlack("https://hooks.slack.com/services/YOUR/WEBHOOK/URL")
// Only errors to Slack
.WriteToSlack(
webhookUrl: "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
minimumLevel: LogLevel.Error)
.WriteToDiscord(
webHookUrl: "https://discordapp.com/api/webhooks/YOUR/WEBHOOK",
botName: "ErrorBot")
.WriteToEmail(
smtpHost: "smtp.gmail.com",
smtpPort: 587,
useSsl: true,
username: "your-email@gmail.com",
password: "app-password",
from: "logs@company.com",
to: "ops@company.com",
subjectPrefix: "[Production Alerts]",
digestMinutes: 5, // Send every 5 minutes
flushOnCritical: true, // Send immediately on Critical
minimumLevel: LogLevel.Error)
.WriteToDatabase(
connectionFactory: () => new SqlConnection("connection-string"),
tableName: "ApplicationLogs",
batchSize: 10)
.WriteToElasticSearch(
elasticSearchUrl: "https://elasticsearch.company.com:9200",
indexName: "myapp-logs",
batchSize: 100)
// Pub/Sub only (real-time subscribers)
.RedisFlow(
host: "redis.company.com",
port: 6379,
channel: "eonacat:logs")
// Pub/Sub + List persistence (subscribers + history)
.RedisFlow(
host: "redis.company.com",
port: 6379,
password: "redis-password",
database: 0,
channel: "eonacat:logs",
listKey: "eonacat:logs:history",
maxListLength: 10000)
// RFC-5424 Syslog over UDP
.WriteToSyslogUdp(
host: "syslog.company.com",
port: 514)
// RFC-5424 Syslog over TCP with TLS
.WriteToSyslogTcp(
host: "syslog.company.com",
port: 514,
useTls: true)
.WriteToHttp(
endpoint: "https://logs.company.com/ingest",
batchSize: 50,
batchInterval: TimeSpan.FromSeconds(2),
headers: new Dictionary<string, string>
{
["Authorization"] = "Bearer token123",
["X-API-Key"] = "secret"
})
// Record warnings and above in hash-chained audit trail
.WriteToAudit(
directory: "./audit",
auditLevel: AuditLevel.WarningAndAbove,
includeProperties: true)
// Verify audit file integrity
bool isIntact = AuditFlow.Verify("./audit/audit.audit");
if (!isIntact)
Console.WriteLine("Audit trail has been tampered with!");
// Buffer 500 events; on Error, flush preceding 100 events to file
.WriteToRollingBuffer(
capacity: 500,
minimumLevel: LogLevel.Trace,
triggerLevel: LogLevel.Error,
triggerTarget: new FileFlow("./error-context"),
preContextLines: 100)
Boosters automatically enrich every log event with additional properties before it reaches any flow. They run once per log event and can add system information, application context, and custom data.
.BoostWithMachineName() // Add computer/host name
.BoostWithProcessId() // Add current process ID
.BoostWithThreadId() // Add managed thread ID
.BoostWithThreadName() // Add thread name (if set)
.BoostWithUser() // Add current OS user name
.BoostWithOS() // Add OS description and version
.BoostWithFramework() // Add .NET runtime description
.BoostWithMemory() // Add process working set in MB
.BoostWithUptime() // Add process uptime in seconds
.BoostWithProcStart() // Add process start time (DateTime)
.BoostWithDate() // Add current date (yyyy-MM-dd)
.BoostWithTime() // Add current time (HH:mm:ss.fff)
.BoostWithTicks() // Add current timestamp ticks (for precise timing)
.BoostWithApp() // Add app name and base directory (auto-detected)
.BoostWithApplication("MyApp", "2.0.0") // Add explicit app name + version
.BoostWithEnvironment("Production") // Add environment name (Prod/Dev/Test)
.BoostWithCorrelationId() // Add Activity.Current correlation ID for distributed tracing
// Single key/value pair
.BoostWithCustomText("Environment", "Production")
.BoostWithCustomText("ServiceVersion", "2.0.1")
// Callback-based booster for dynamic data
.Boost("RequestInfo", () => new Dictionary<string, object?>
{
["UserId"] = GetCurrentUserId(),
["TenantId"] = GetCurrentTenantId(),
["ApiVersion"] = GetApiVersion()
})
// Custom booster implementation
.Boost(new MyCustomBooster())
var logger = new LogBuilder("MyApplication")
// System info
.BoostWithMachineName()
.BoostWithProcessId()
.BoostWithThreadId()
.BoostWithOS()
.BoostWithFramework()
.BoostWithMemory()
// Application context
.BoostWithApplication("MyApp", "1.0.0")
.BoostWithEnvironment("Production")
.BoostWithCorrelationId()
// Startup info
.BoostWithProcStart()
.BoostWithUptime()
// Custom context
.BoostWithCustomText("DataCenter", "US-East-1")
.BoostWithCustomText("InstanceId", Environment.MachineName)
.Boost("Request", () => new Dictionary<string, object?>
{
["TraceId"] = Activity.Current?.Id ?? HttpContext?.TraceIdentifier,
["UserId"] = CurrentUser?.Id,
})
.WriteToConsole()
.WriteToFile("./logs")
.Build();
When you enable boosters, they add structured properties to each log event. In file output, these appear as:
[2026-03-27 09:15:00.123] [INFO] [Application=MyApp, Version=1.0.0, Environment=Production, Machine=srv-01, PID=1234, ThreadId=5]
User logged in successfully
In JSON outputs (database, Elasticsearch, etc.), boosters add properties like:
{
"timestamp": "2026-03-27T09:15:00.123Z",
"level": "Information",
"message": "User logged in successfully",
"machine": "srv-01",
"processId": 1234,
"threadId": 5,
"userId": 42,
"application": "MyApp",
"version": "1.0.0",
"environment": "Production"
}
Modifiers run after boosters and can mutate or cancel a log event before it is dispatched to flows. Use modifiers to add request-scoped data, redact sensitive info, or filter events.
logger.AddModifier((ref LogEventBuilder builder) =>
{
// Add request context
builder.WithProperty("RequestId", HttpContext.TraceIdentifier);
});
// Now every log will include RequestId automatically
logger.Information("Processing request");
// Add request context
logger.AddModifier((ref LogEventBuilder builder) =>
{
builder.WithProperty("RequestId", HttpContext?.TraceIdentifier);
builder.WithProperty("UserId", User?.Id);
});
// Add custom timing info
logger.AddModifier((ref LogEventBuilder builder) =>
{
builder.WithProperty("Timestamp", DateTime.UtcNow);
});
// Redact sensitive data (example)
logger.AddModifier((ref LogEventBuilder builder) =>
{
if (builder.Message?.Contains("password") ?? false)
builder.WithMessage("[REDACTED - contains sensitive data]");
});
logger.AddModifier((ref LogEventBuilder builder) =>
{
// Example: Skip verbose trace logs in production
if (builder.Level == LogLevel.Trace && !IsDebugMode)
builder.Cancel(); // Event won't be sent to any flow
});
// In Program.cs, register request context booster
builder.Services.AddEonaCatLogging("WebApp", logBuilder =>
{
logBuilder
.WriteToConsole()
.WriteToFile("./logs")
.BoostWithCorrelationId(); // Distributed tracing support
});
// In your page or controller
public class IndexModel : PageModel
{
private readonly ILogger _logger;
public IndexModel(ILogger logger)
{
_logger = logger;
}
public void OnGet()
{
// Correlation ID is automatically added by booster
_logger.Information("Page loaded");
}
}
Automatically retry failed writes with exponential delays. Useful for flaky remote endpoints.
.WriteToRetry(
primaryFlow: new HttpFlow("https://logs.example.com"),
maxRetries: 5,
initialDelay: TimeSpan.FromMilliseconds(200),
exponentialBackoff: true)
// Retry delays: 200ms, 400ms, 800ms, 1.6s, 3.2s
Automatically fall back to a secondary destination if the primary fails.
.WriteToFailover(
primaryFlow: new ElasticSearchFlow("https://es-prod:9200"),
secondaryFlow: new FileFlow("./fallback-logs"),
recoveryCheckInterval: TimeSpan.FromSeconds(30),
failureThreshold: 5) // Switch after 5 consecutive failures
Protect high-latency sinks (email, Slack, HTTP) from log storms.
.WriteToThrottled(
inner: new SlackFlow(webhookUrl),
burstCapacity: 10, // Allow 10 events in a burst
refillPerSecond: 1.0, // Refill 1 token per second
deduplicate: true, // Collapse identical messages
dedupWindow: TimeSpan.FromSeconds(60), // Within 60-second window
dedupMaxKeys: 1000) // Track up to 1000 unique messages
Example behavior:
10 log errors occur simultaneously β first 10 are sent (burst)Buffer recent logs and flush context when an error occurs. Useful for debugging transient issues.
.WriteToRollingBuffer(
capacity: 500, // Keep last 500 events
minimumLevel: LogLevel.Trace, // Buffer everything
triggerLevel: LogLevel.Error, // Trigger on errors
triggerTarget: new FileFlow("./error-context"), // Write context to file
preContextLines: 100) // Include 100 lines of context before the error
Real-world scenario:
# In-memory buffer contains: [Trace, Debug, Info, Warning, ...100+ events...]
# Error occurs
# Rolling buffer flushes: previous 100 logs + the error itself
# Result: ./error-context/2026-03-27.log contains the full context
Stop sending to a failing destination and automatically resume when it recovers.
.WriteToCircuitBreaker(
inner: new ElasticSearchFlow("https://es-prod:9200"),
failureThreshold: 10, // Open after 10 failures
successThreshold: 3, // Close after 3 successes
timeout: TimeSpan.FromSeconds(30)) // Check recovery every 30s
await using var logger = new LogBuilder("Production")
.WriteToConsole()
// Local file as primary
.WriteToFile("./logs")
// Elasticsearch with resilience
.WriteToRetry(
primaryFlow: .WriteToFailover(
primaryFlow: new ElasticSearchFlow("https://es-prod:9200"),
secondaryFlow: new FileFlow("./fallback")),
maxRetries: 3)
// Rate-limited Slack for errors only
.WriteToThrottled(
inner: new SlackFlow(webhookUrl),
burstCapacity: 5,
refillPerSecond: 0.5,
minimumLevel: LogLevel.Error)
// Rolling buffer for debugging
.WriteToRollingBuffer(
capacity: 1000,
triggerLevel: LogLevel.Error,
triggerTarget: new FileFlow("./error-context"))
.BoostWithMachineName()
.BoostWithCorrelationId()
.Build();
Encrypt sensitive logs with AES encryption and password protection.
.WriteToEncryptedFile(
directory: "./secure-logs",
filePrefix: "encrypted",
password: "YourStrongPassword123!",
maxFileSize: 50 * 1024 * 1024)
// Decrypt a specific file when needed
LogBuilder.DecryptFile(
encryptedPath: "./secure-logs/encrypted.enc",
outputPath: "./secure-logs/decrypted.txt",
password: "YourStrongPassword123!");
// Now you can read the decrypted logs
string logs = File.ReadAllText("./secure-logs/decrypted.txt");
The audit flow produces a tamper-evident file where every entry is SHA-256 hash-chained. Deleting or modifying any past entry invalidates all subsequent hashes.
.WriteToAudit(
directory: "./audit",
filePrefix: "audit",
auditLevel: AuditLevel.WarningAndAbove, // Warning, Error, Critical
includeProperties: true)
// Verify file integrity at any time
bool intact = AuditFlow.Verify("./audit/audit.audit");
if (!intact)
{
Console.WriteLine("ERROR: Audit trail has been tampered with!");
// Take appropriate action (alert, disable service, etc.)
}
AuditLevel.All // Every log event
AuditLevel.WarningAndAbove // Warning, Error, Critical
AuditLevel.ErrorAndAbove // Error, Critical
AuditLevel.CriticalOnly // Critical only
2026-03-27T09:15:00.123Z|WARNING|Low disk space|[hash: sha256(prev_hash + data)]
2026-03-27T09:15:05.456Z|ERROR|Database connection failed|[hash: sha256(prev_hash + data)]
2026-03-27T09:15:10.789Z|CRITICAL|Service shutting down|[hash: sha256(prev_hash + data)]
If someone modifies the second entry, the third entry's hash validation fails, indicating tampering.
Both ConsoleFlow and FileFlow accept a customizable template string for formatting output.
[{ts}] [Host: {host}] [Category: {category}] [Thread: {thread}] [{logtype}] {message}{props}
| Token | Description | Example |
|---|---|---|
{ts} |
Timestamp (yyyy-MM-dd HH:mm:ss.fff) | 2026-03-27 09:15:00.123 |
{tz} |
Timezone (UTC or local name) | UTC or EST |
{host} |
Machine name | srv-prod-01 |
{category} |
Logger category | MyApp.Services |
{thread} |
Managed thread ID | 5 |
{pid} |
Process ID | 1234 |
{logtype} |
Log level label | INFO, WARN, ERROR |
{message} |
Log message text | User login successful |
{props} |
Structured properties | UserId=42, IP=192.168.1.1 |
{newline} |
Line break | (actual newline) |
// Minimal template
.WriteToFile(
directory: "./logs",
template: "[{ts}] [{logtype}] {message}")
// Output: [2026-03-27 09:15:00.123] [INFO] User login successful
// Verbose template with all info
.WriteToFile(
directory: "./logs",
template: "[{ts}] [{tz}] [{logtype}] [Thread={thread}] [PID={pid}] [Host={host}] {category}: {message}{props}")
// Output: [2026-03-27 09:15:00.123] [UTC] [INFO] [Thread=5] [PID=1234] [Host=srv-01] MyApp: User login successful UserId=42, IP=192.168.1.1
// JSON-like format
.WriteToFile(
directory: "./logs",
template: "timestamp={ts}|level={logtype}|category={category}|pid={pid}|message={message}{props}")
EonaCat.LogStack supports advanced templating beyond basic property placeholders. These features work with message templates used in logging calls:
logger.Information("User {User} logged in from {IP}", user, ipAddress);
Access properties of objects using dot notation:
var user = new { Name = "John", Address = new { City = "NYC" } };
logger.Information("User {User.Name} lives in {User.Address.City}", user, user.Address);
// Output: User John lives in NYC
Access specific items in arrays or lists:
var items = new[] { "apple", "banana", "cherry" };
logger.Information("First item: {Items[0]}, Second: {Items[1]}", items);
// Output: First item: apple, Second: banana
Pad values to a specific width for aligned output:
logger.Information("{Name,20} {Email,-30}", "John", "john@example.com");
// Output: " John john@example.com "
// (right-aligned 20 chars) (left-aligned 30 chars)
Apply transformations to property values:
// Uppercase
logger.Information("Status: {Status|uppercase}", "pending");
// Output: Status: PENDING
// Lowercase
logger.Information("Event: {Event|lowercase}", "UserCreated");
// Output: Event: usercreated
// Trim whitespace
logger.Information("Value: '{Value|trim}'", " spaces ");
// Output: Value: 'spaces'
// Truncate with ellipsis
logger.Information("Description: {Description|truncate:50}", veryLongText);
// Output: Description: This is a very long description that β¦
// Reverse string
logger.Information("Reversed: {Text|reverse}", "hello");
// Output: Reversed: olleh
// Multiple filters (chained)
logger.Information("Result: {Input|trim|uppercase}", " hello world ");
// Output: Result: HELLO WORLD
Provide default values when properties are null or missing:
// Fallback with ?? operator and quotes
logger.Information("User: {User??'Anonymous'}", user);
// Output: User: Anonymous (if user is null)
logger.Information("Email: {Email??'no-email@example.com'}", email);
// Output: Email: no-email@example.com (if email is null)
Display different text based on boolean properties:
logger.Information("Status: {?IsActive:Active|Inactive}", isActive);
// Output: Status: Active (if isActive is true)
// Output: Status: Inactive (if isActive is false)
logger.Information("Result: {?Success:β Success|β Failed}", success);
// Output: Result: β Success (if success is true)
Use comparison operators in conditional templates:
// Advanced conditional token with if syntax
// Syntax: {@if:condition:trueOutput|falseOutput}
logger.Information("User role: {@if:RoleId>5:Admin|User}", user);
// Output: User role: Admin (if RoleId > 5)
// Output: User role: User (if RoleId <= 5)
logger.Information("Account: {@if:Status==Premium:Premium Member|Standard}", account);
// Output: Account: Premium Member (if Status equals 'Premium')
// Comparison operators: ==, !=, <, >, <=, >=
logger.Information("{@if:Count>=100:Large|Small}", data);
logger.Information("{@if:Price<50:Budget|Premium}", item);
logger.Information("{@if:IsDeleted!=false:Deleted|Active}", record);
Iterate over arrays and collections in templates:
// Syntax: {@loop:CollectionName:itemTemplate:separator}
var items = new[] { "apple", "banana", "cherry" };
logger.Information("Items: {@loop:Items:{Item}|, }", items);
// Output: Items: apple, banana, cherry
// Custom separator
var tags = new[] { "urgent", "high-priority", "production" };
logger.Information("Tags: {@loop:Tags:{Item}| | }", tags);
// Output: Tags: urgent | high-priority | production
// Complex items
var users = new[]
{
new { Id = 1, Name = "John" },
new { Id = 2, Name = "Jane" }
};
logger.Information("Users: {@loop:Users:({Id}:{Name})|, }", users);
// Output: Users: (1:John), (2:Jane)
Perform arithmetic operations on numeric properties:
logger.Information("Total: ${Amount|add:10}", 50);
// Output: Total: $60
logger.Information("Discount: ${Price|multiply:0.9}", 100);
// Output: Discount: $90
logger.Information("Half: {Value|divide:2}", 100);
// Output: Half: 50
logger.Information("Remainder: {Number|modulo:3}", 10);
// Output: Remainder: 1
logger.Information("Absolute: {Change|abs}", -15);
// Output: Absolute: 15
logger.Information("Rounded: {Value|round:2}", 19.9999);
// Output: Rounded: 20
logger.Information("Max: {Value|max:100}", 150);
// Output: Max: 100
logger.Information("Floor: {Decimal|floor}", 19.9);
// Output: Floor: 19
logger.Information("Ceil: {Decimal|ceil}", 19.1);
// Output: Ceil: 20
Transform string values with various filters:
// Case conversion
logger.Information("Lower: {Text|lowercase}", "HELLO");
// Output: Lower: hello
logger.Information("Upper: {Text|uppercase}", "world");
// Output: Upper: WORLD
// Padding
logger.Information("Padded: |{Name|pad:15}|", "John");
// Output: Padded: |John |
// Repetition
logger.Information("Repeated: {Char|repeat:5}", "x");
// Output: Repeated: xxxxx
// String replacement
logger.Information("Fixed: {Path|replace:old:new}", "/old/path/old");
// Output: Fixed: /new/path/new
// Substring operations
logger.Information("Skip first 3: {Text|substring:3}", "12345");
// Output: Skip first 3: 45
// Trim variations
logger.Information("Trimmed: '{Text|trim}'", " spaces ");
// Output: Trimmed: 'spaces'
logger.Information("Trim start: '{Text|trimstart}'", " spaces ");
// Output: Trim start: 'spaces '
logger.Information("Trim end: '{Text|trimend}'", " spaces ");
// Output: Trim end: ' spaces'
// String testing
logger.Information("Starts with 'user': {Email|startswith:user}", "user@example.com");
// Output: Starts with 'user': true
logger.Information("Ends with '.org': {Url|endswith:.org}", "website.org");
// Output: Ends with '.org': true
logger.Information("Contains 'app': {Path|contains:app}", "/app/data");
// Output: Contains 'app': true
// String reversal
logger.Information("Reversed: {Text|reverse}", "hello");
// Output: Reversed: olleh
// Split and join
logger.Information("Split CSV: {Data|split:,}", "a,b,c");
// Output: Split CSV: a, b, c
Compare values and return boolean results:
logger.Information("Equals: {Status|equals:Active}", status);
// Output: Equals: true (if status == 'Active')
logger.Information("Less than 100: {Value|lessthan:100}", 50);
// Output: Less than 100: true
logger.Information("Greater than 50: {Value|greaterthan:50}", 100);
// Output: Greater than 50: true
logger.Information("Greater or equal: {Count|gte:10}", 15);
// Output: Greater or equal: true
logger.Information("Less or equal: {Count|lte:20}", 15);
// Output: Less or equal: true
logger.Information("Not equal: {Type|ne:User}", "Admin");
// Output: Not equal: true
Format and manipulate date/time values:
// Date formatting
logger.Information("Date: {CreatedAt|date:yyyy-MM-dd}", DateTime.Now);
// Output: Date: 2026-03-27
logger.Information("Full timestamp: {CreatedAt|date:O}", DateTime.Now);
// Output: Full timestamp: 2026-03-27T09:15:00.1234567Z
logger.Information("Custom format: {CreatedAt|date:dd/MM/yyyy HH:mm:ss}", DateTime.Now);
// Output: Custom format: 27/03/2026 09:15:00
// TimeSpan operations
var duration = TimeSpan.FromSeconds(3661);
logger.Information("Total seconds: {Duration|timespan:totalseconds}", duration);
// Output: Total seconds: 3661
logger.Information("Total minutes: {Duration|timespan:totalminutes}", duration);
// Output: Total minutes: 61.0166...
logger.Information("Total hours: {Duration|timespan:totalhours}", duration);
// Output: Total hours: 1.01388...
logger.Information("Days: {Duration|timespan:days}", duration);
// Output: Days: 0
logger.Information("Hours: {Duration|timespan:hours}", duration);
// Output: Hours: 1
logger.Information("Minutes: {Duration|timespan:minutes}", duration);
// Output: Minutes: 1
logger.Information("Seconds: {Duration|timespan:seconds}", duration);
// Output: Seconds: 1
Combine filters for complex transformations:
// Trim, then uppercase, then truncate
logger.Information("Processed: {Input|trim|uppercase|truncate:10}", " hello world ");
// Output: Processed: HELLO WORβ¦
// Substring, then lowercase
logger.Information("Modified: {Path|substring:5|lowercase}", "/DATA/MyFile.TXT");
// Output: Modified: myfile.txt
// Apply multiple math operations
logger.Information("Calculated: {Value|multiply:2|add:10|divide:3}", 5);
// Output: Calculated: 6.66... (5 * 2 = 10, 10 + 10 = 20, 20 / 3 = 6.66)
// API request logging with advanced features
var request = new
{
Method = "POST",
Path = "/api/users",
UserId = 42,
ResponseTime = 150,
Success = true,
Tags = new[] { "api", "users", "production" }
};
logger.Information(
"[{Timestamp|date:HH:mm:ss}] {Method|uppercase} {Path} - User {UserId} - {ResponseTime|pad:5}ms - {?Success:β|β} - Tags: {@loop:Tags:{Item}|, }",
DateTime.Now, request.Method, request.Path, request.UserId,
request.ResponseTime, request.Success, request.Tags
);
// Output: [09:15:00] POST /api/users - User 42 - 150ms - β - Tags: api, users, production
// Conditional status with comparison
var operation = new
{
Name = "DataSync",
Status = "Completed",
Duration = 45000,
RecordsProcessed = 1500,
ErrorCount = 0
};
logger.Information(
"{Name}: {@if:ErrorCount==0:β Success|β With Errors} | Duration: {Duration|timespan:totalseconds}s | Records: {RecordsProcessed|add:0} processed",
operation.Name, operation.ErrorCount, operation.Duration, operation.RecordsProcessed
);
// Output: DataSync: β Success | Duration: 45s | Records: 1500 processed
// Complex nested template
var batch = new
{
Id = "batch-001",
Items = new[] { "item1", "item2", "item3" },
Size = 3,
Price = 99.99m,
Discount = 0.15m
};
logger.Information(
"Batch {Id}: {?Size>5:Large|Small} batch | Items: {@loop:Items:{Item}|, } | Price: ${Price|multiply:Discount|add:0|round:2}",
batch.Id, batch.Size, batch.Items, batch.Price, batch.Discount
);
// Output: Batch batch-001: Small batch | Items: item1, item2, item3 | Price: $15.00
The templating engine supports:
| Feature | Syntax | Example |
|---|---|---|
| Basic Property | {PropertyName} |
{UserId} |
| Nested Properties | {Object.Property.Sub} |
{User.Address.City} |
| Array Indexing | {Array[Index]} |
{Items[0]} |
| Alignment | {Value,Width} |
{Name,20} |
| Format Specifiers | {Value:Format} |
{Date:yyyy-MM-dd} |
| String Filters | {Value\|Filter} |
{Text\|uppercase} |
| Math Filters | {Value\|add:10} |
{Price\|multiply:0.9} |
| Comparison | {Value\|equals:text} |
{Status\|equals:Active} |
| Simple Conditionals | {?Property:True\|False} |
{?IsActive:Active\|Inactive} |
| Advanced Conditionals | {@if:Condition:T\|F} |
{@if:Count>10:High\|Low} |
| Loops | {@loop:Collection:Template} |
{@loop:Items:{Item}\|, } |
| Fallback Values | {Value??'Default'} |
{Name??'Unknown'} |
| Chained Filters | {Value\|filter1\|filter2} |
{Text\|trim\|uppercase} |
| Destructuring | {@Object} |
{@User} |
Apply .NET format strings to values:
// Date formatting
logger.Information("Date: {CreatedAt:yyyy-MM-dd}", DateTime.Now);
// Output: Date: 2026-03-27
// Decimal formatting
logger.Information("Price: {Price:C}", 19.99m);
// Output: Price: $19.99
// Numeric formatting
logger.Information("Count: {Count:D5}", 42);
// Output: Count: 00042
Deep-inspect complex objects to reveal their structure:
var user = new { Id = 1, Name = "John", Email = "john@example.com" };
// Default destructuring with @ prefix
logger.Information("User: {@User}", user);
// Output: User: {Id: 1, Name: John, Email: john@example.com}
// Force string conversion with $ prefix
logger.Information("User: {$User}", user);
// Output: User: YourNamespace.User
// Nested destructuring
var order = new
{
Id = 1,
Customer = new { Name = "John", City = "NYC" },
Items = new[] { "Item1", "Item2" }
};
logger.Information("Order: {@Order}", order);
// Output: Order: {Id: 1, Customer: {Name: John, City: NYC}, Items: [...]}
// API request logging
var request = new
{
Method = "POST",
Path = "/api/users",
UserId = 42,
ResponseTime = 150,
Success = true
};
logger.Information(
"[{Time|uppercase}] {Method} {Path} - User {UserId} - {ResponseTime,5}ms - {?Success:β|β}",
DateTime.Now.ToString("HH:mm:ss"), request.Method, request.Path,
request.UserId, request.ResponseTime, request.Success
);
// Output: [09:15:00] POST /api/users - User 42 - 150ms - β
// Database operation with fallback
logger.Information(
"Database query {QueryName??'Unknown'} by user {UserId??'System'} took {Duration|truncate:10}ms",
storedProcName, currentUserId, duration
);
// Output: Database query sp_GetUsers by user System took 125ms
// File processing with conditions
logger.Information(
"File {FileName} processed: {?HasErrors:β ERRORS|β OK} - {LineCount,6} lines",
file.Name, file.HasErrors, file.LineCount
);
// Output: File log.txt processed: β OK - 1024 lines
// Nested property access
var company = new
{
Name = "Acme Corp",
HeadOffice = new { City = "New York", Country = "USA" },
Employees = new[] { "John", "Jane", "Jack" }
};
logger.Information(
"Company {Company.Name} from {Company.HeadOffice.City}, {Company.HeadOffice.Country} - First employee: {Company.Employees[0]}",
company
);
// Output: Company Acme Corp from New York, USA - First employee: John
Console output with verbose logging, local files, and no remote sends.
await using var logger = new LogBuilder("MyApp")
.WithMinimumLevel(LogLevel.Debug)
.WithTimestampMode(TimestampMode.Local)
.WriteToConsole(useColors: true)
.WriteToFile("./logs", filePrefix: "dev")
.BoostWithThreadId()
.BoostWithCorrelationId()
.Build();
Files locally, Elasticsearch for search, Slack for alerts, encrypted audit trail.
await using var logger = new LogBuilder("ProductionApp")
.WithMinimumLevel(LogLevel.Information)
.WithTimestampMode(TimestampMode.Utc)
// Local backup
.WriteToFile(
directory: "./logs",
maxFileSize: 100 * 1024 * 1024,
compression: CompressionFormat.GZip)
// Primary analytics with fallback
.WriteToFailover(
primaryFlow: new ElasticSearchFlow("https://elastic.company.com"),
secondaryFlow: new FileFlow("./fallback-elastic"))
// Rate-limited alerts
.WriteToThrottled(
inner: new SlackFlow(slackWebhookUrl),
burstCapacity: 5,
refillPerSecond: 1.0,
minimumLevel: LogLevel.Error)
// Compliance audit
.WriteToAudit(
directory: "./audit",
auditLevel: AuditLevel.WarningAndAbove)
// Encrypted sensitive logs
.WriteToEncryptedFile(
directory: "./secure-logs",
password: Environment.GetEnvironmentVariable("LOG_ENCRYPTION_KEY"))
// Diagnostics snapshot
.WriteDiagnostics(
snapshotInterval: TimeSpan.FromMinutes(5))
// Boosters
.BoostWithMachineName()
.BoostWithProcessId()
.BoostWithApplication("ProductionApp", "1.0.0")
.BoostWithEnvironment("Production")
.BoostWithCorrelationId()
.Build();
Elasticsearch for centralized logs, Redis for real-time events, correlation IDs.
await using var logger = new LogBuilder("OrderService")
.WithMinimumLevel(LogLevel.Information)
// Centralized log storage
.WriteToElasticSearch(
elasticSearchUrl: "https://elastic-cluster.company.com",
indexName: "orderservice-logs")
// Real-time event stream
.RedisFlow(
host: "redis.company.com",
channel: "orderservice:logs",
listKey: "orderservice:logs:history",
maxListLength: 10000)
// Local file backup
.WriteToFile("./logs")
// Correlation tracking for distributed tracing
.BoostWithCorrelationId() // Automatically includes Activity.Current?.Id
.BoostWithApplication("OrderService", ServiceVersion)
.BoostWithProcessId()
.BoostWithMachineName()
.Build();
SignalR flow for live log streaming to web dashboard.
// Backend: Log streaming to SignalR hub
await using var logger = new LogBuilder("DashboardApp")
.WriteToFile("./logs")
.WriteToSignalR(
hubUrl: "https://dashboard.company.com/loghub",
hubMethod: "ReceiveLog",
batchSize: 20,
batchIntervalMs: 500,
minimumLevel: LogLevel.Warning) // Only send warnings+ to dashboard
.Build();
// Frontend: Receive logs in real-time (JavaScript example)
const connection = new signalR.HubConnectionBuilder()
.withUrl("https://dashboard.company.com/loghub")
.withAutomaticReconnect()
.build();
connection.on("ReceiveLog", (log) => {
console.log(`[${log.level}] ${log.message}`);
addToLiveLogUI(log);
});
await connection.start();
Separate audit trail, encrypted logs, tamper detection.
await using var logger = new LogBuilder("ComplianceApp")
// Tamper-evident audit trail
.WriteToAudit(
directory: "./compliance/audit",
auditLevel: AuditLevel.WarningAndAbove,
includeProperties: true)
// Encrypted sensitive data
.WriteToEncryptedFile(
directory: "./compliance/encrypted",
password: GetAuditPassword(),
maxFileSize: 50 * 1024 * 1024)
// Database for detailed analysis
.WriteToDatabase(
connectionFactory: () => new SqlConnection(connectionString),
tableName: "AuditLogs",
batchSize: 10)
// Regular file backup
.WriteToFile("./logs")
// Boost with full context
.BoostWithUser()
.BoostWithMachineName()
.BoostWithProcessId()
.BoostWithApplication("ComplianceApp", "1.0.0")
.Build();
// Periodic verification
_ = Task.Run(async () =>
{
while (true)
{
await Task.Delay(TimeSpan.FromHours(1));
bool isIntact = AuditFlow.Verify("./compliance/audit/audit.audit");
if (!isIntact)
{
// Alert security team
logger.Critical("SECURITY ALERT: Audit trail tampering detected!");
}
}
});
For services handling high log volume, use batching and throttling.
await using var logger = new LogBuilder("HighVolumeApp")
.WriteToThrottled(
inner: new HttpFlow("https://logs.company.com/ingest"),
burstCapacity: 100,
refillPerSecond: 50.0, // 50 logs/sec steady state
deduplicate: true,
dedupWindow: TimeSpan.FromSeconds(30))
.WriteToFile(
directory: "./logs",
batchSize: 100, // Batch 100 logs before writing
flushIntervalInMilliSeconds: 1000) // Flush every 1 second
.RedisFlow(
host: "redis.company.com",
channel: "app:logs",
listKey: "app:logs:history")
.Build();
// Get current statistics
var stats = logger.GetDiagnostics();
Console.WriteLine($"Total Logged: {stats.TotalLogged}");
Console.WriteLine($"Total Dropped: {stats.TotalDropped}");
Console.WriteLine($"Drop Rate: {(double)stats.TotalDropped / stats.TotalLogged:P2}");
// Per-flow statistics
foreach (var flowStat in stats.FlowStats)
{
Console.WriteLine($"Flow: {flowStat.Name}");
Console.WriteLine($" Processed: {flowStat.Processed}");
Console.WriteLine($" Dropped: {flowStat.Dropped}");
Console.WriteLine($" Errors: {flowStat.ErrorCount}");
}
Enable periodic diagnostic snapshots to track performance over time.
.WriteDiagnostics(
snapshotInterval: TimeSpan.FromMinutes(5),
injectIntoEvents: true, // Include metrics in log events
writeSnapshotEvents: true, // Write snapshot to logs
snapshotCategory: "Diagnostics",
forwardTo: new FileFlow("./diagnostics"), // Also forward to file
minimumLevel: LogLevel.Information,
customMetrics: () => new Dictionary<string, object>
{
["ActiveRequests"] = GetActiveRequestCount(),
["CacheHitRate"] = GetCacheHitRate(),
["DatabasePoolSize"] = GetDbPoolSize()
})
// Monitor in a background task
_ = Task.Run(async () =>
{
while (true)
{
await Task.Delay(TimeSpan.FromMinutes(1));
var stats = logger.GetDiagnostics();
// Alert if drop rate is high
if (stats.TotalLogged > 0)
{
double dropRate = (double)stats.TotalDropped / stats.TotalLogged;
if (dropRate > 0.01) // > 1% drop rate
{
logger.Warning("High log drop rate detected", ("DropRate", dropRate));
}
}
// Alert on errors
var hasErrors = stats.FlowStats.Any(f => f.ErrorCount > 0);
if (hasErrors)
{
logger.Warning("Some flows are experiencing errors");
}
}
});
All flows support batching, so events may not be written immediately. Use FlushAsync() to ensure all pending events are written before shutdown.
// Flush all pending events synchronously (up to 5 seconds)
await logger.FlushAsync();
// After flush, you can safely dispose
await logger.DisposeAsync();
Using await using automatically flushes and disposes:
await using var logger = new LogBuilder("MyApp")
.WriteToFile("./logs")
.Build();
logger.Information("Application running");
// At scope exit: FlushAsync() called automatically, then DisposeAsync()
try
{
await using var logger = new LogBuilder("MyApp").Build();
// Log events here
logger.Information("App started");
// Your application logic
await RunApplication();
}
catch (Exception ex)
{
// Ensure errors are logged even if something goes wrong
logger?.Critical(ex, "Unexpected error during shutdown");
}
finally
{
// FlushAsync() is called automatically when leaving the using block
}
var logger = new LogBuilder("MyApp").Build();
try
{
logger.Information("Processing...");
}
finally
{
// Manual control
await logger.FlushAsync();
// Perform custom cleanup
CleanupResources();
await logger.DisposeAsync();
}
Subscribe to log events for real-time processing or custom handling.
logger.OnLog += (sender, message) =>
{
// Fired for every log event that passes filters
Console.WriteLine($"[Event] {message.Level}: {message.Message}");
};
logger.Information("This event will be raised");
logger.OnLog += (sender, message) =>
{
// Log level filtering
if (message.Level >= LogLevel.Error)
{
// Send critical errors to external alert system
SendAlert($"{message.Level}: {message.Message}");
}
// Category filtering
if (message.Category?.Contains("Payment") ?? false)
{
// Audit sensitive operations
LogToAuditSystem(message);
}
// Property-based filtering
if (message.Properties != null && message.Properties.ContainsKey("UserId"))
{
TrackUserActivity(message.Properties["UserId"], message);
}
};
// Handler 1: Alert on errors
logger.OnLog += (sender, msg) =>
{
if (msg.Level == LogLevel.Error)
SendSlackAlert(msg);
};
// Handler 2: Track metrics
logger.OnLog += (sender, msg) =>
{
Metrics.Increment($"logs.{msg.Level.ToString().ToLower()}");
};
// Handler 3: Update dashboard
logger.OnLog += (sender, msg) =>
{
Dashboard.AddLog(msg);
};
Create custom flows by implementing IFlow or extending FlowBase for complex scenarios.
public class MyCustomFlow : FlowBase
{
public MyCustomFlow() : base("MyCustomFlow", LogLevel.Trace) { }
public override Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken ct = default)
{
try
{
// Your custom logic here
string formatted = $"[{logEvent.Timestamp:O}] {logEvent.Level}: {logEvent.Message}";
MyBackendService.SendLog(formatted);
return Task.FromResult(WriteResult.Success);
}
catch (Exception ex)
{
return Task.FromResult(WriteResult.Failure(ex));
}
}
public override Task FlushAsync(CancellationToken ct = default)
{
// Optional: implement batching flush logic
return Task.CompletedTask;
}
}
// Register with:
new LogBuilder("App")
.WriteTo(new MyCustomFlow())
.Build();
public class BatchedCustomFlow : FlowBase
{
private readonly List<LogEvent> _batch = new(100);
private readonly object _lock = new();
public BatchedCustomFlow() : base("BatchedCustom", LogLevel.Trace) { }
public override Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken ct = default)
{
lock (_lock)
{
_batch.Add(logEvent);
if (_batch.Count >= 100)
{
return FlushBatchAsync(ct);
}
}
return Task.FromResult(WriteResult.Success);
}
private Task<WriteResult> FlushBatchAsync(CancellationToken ct)
{
try
{
var toSend = _batch.ToList();
_batch.Clear();
// Send batch to external service
MyBackendService.SendLogBatch(toSend);
return Task.FromResult(WriteResult.Success);
}
catch (Exception ex)
{
return Task.FromResult(WriteResult.Failure(ex));
}
}
public override async Task FlushAsync(CancellationToken ct = default)
{
lock (_lock)
{
if (_batch.Count > 0)
{
await FlushBatchAsync(ct);
}
}
}
public override async ValueTask DisposeAsync()
{
await FlushAsync();
await base.DisposeAsync();
}
}
public class RetryableCustomFlow : FlowBase
{
private const int MaxRetries = 3;
public RetryableCustomFlow() : base("RetryableCustom", LogLevel.Trace) { }
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken ct = default)
{
int attempts = 0;
Exception lastEx = null;
while (attempts < MaxRetries)
{
try
{
await SendToServiceAsync(logEvent, ct);
return WriteResult.Success;
}
catch (Exception ex)
{
lastEx = ex;
attempts++;
if (attempts < MaxRetries)
await Task.Delay(TimeSpan.FromMilliseconds(Math.Pow(2, attempts) * 100), ct);
}
}
return WriteResult.Failure(lastEx);
}
private Task SendToServiceAsync(LogEvent logEvent, CancellationToken ct)
{
// Your implementation
return MyBackendService.SendLogAsync(logEvent, ct);
}
public override Task FlushAsync(CancellationToken ct = default) => Task.CompletedTask;
}
logger.Trace("Very detailed diagnostic info (most verbose)");
logger.Debug("Debug-level diagnostic information");
logger.Information("General informational message (default level)");
logger.Warning("Warning message (potential issue)");
logger.Error("Error occurred, operation failed");
logger.Critical("Critical failure, system may be unstable");
// Good: Structured properties
logger.Information("User logged in",
("UserId", user.Id),
("Email", user.Email),
("IpAddress", request.RemoteIpAddress),
("Timestamp", DateTime.UtcNow));
// Avoid: String interpolation in message
logger.Information($"User {user.Id} logged in from {request.RemoteIpAddress}");
// Create category-specific loggers
ILogger serviceLogger = loggerFactory.CreateLogger("MyApp.Services.UserService");
ILogger dataLogger = loggerFactory.CreateLogger("MyApp.Data.Repository");
// Configure file splitting by category
.WriteToFile("./logs", useCategoryRouting: true)
// Results in: logs/MyApp.Services.UserService.log, logs/MyApp.Data.Repository.log
try
{
// Risky operation
await database.ExecuteAsync(query);
}
catch (TimeoutException ex)
{
logger.Warning(ex, "Database timeout, retrying");
}
catch (Exception ex)
{
logger.Error(ex, "Database error, operation failed");
throw; // Re-throw to propagate to caller
}
builder.Services.AddEonaCatLogging("MyApp", logBuilder =>
{
logBuilder.BoostWithCorrelationId(); // Automatic tracing
});
// All logs in the same request/operation automatically get the same correlation ID
.WithMinimumLevel(LogLevel.Information) // Global minimum
.WriteToConsole(minimumLevel: LogLevel.Debug) // More verbose
.WriteToFile(minimumLevel: LogLevel.Warning) // Less verbose
.WriteToSlack(minimumLevel: LogLevel.Error) // Errors only
var host = builder.Build();
// Graceful shutdown: flush logs before exit
var lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();
lifetime.ApplicationStopping.Register(async () =>
{
var logger = host.Services.GetRequiredService<ILogger>();
await logger.FlushAsync();
});
await host.RunAsync();
_ = Task.Run(async () =>
{
while (true)
{
await Task.Delay(TimeSpan.FromMinutes(1));
var stats = logger.GetDiagnostics();
if (stats.TotalLogged + stats.TotalDropped > 0)
{
double dropRate = (double)stats.TotalDropped / (stats.TotalLogged + stats.TotalDropped);
if (dropRate > 0.01) // > 1%
logger.Warning("High drop rate detected", ("DropRate", dropRate));
}
}
});
// Before: error happens, context is lost
// After: rolling buffer captures previous 100 logs
.WriteToRollingBuffer(
capacity: 500,
triggerLevel: LogLevel.Error,
triggerTarget: new FileFlow("./error-context"))
.WriteToEncryptedFile(
directory: "./secure-logs",
password: GetEncryptionPasswordFromVault())
Symptoms: Excessive CPU while logging
Solutions:
WriteToThrottled() to rate limit.WriteToFile("./logs", batchSize: 100, flushIntervalInMilliSeconds: 5000)
.WriteToThrottled(inner: new HttpFlow(url), burstCapacity: 50)
Symptoms: Memory grows linearly with time
Solutions:
.WriteToFile(compression: CompressionFormat.GZip)
.WriteToRollingBuffer(capacity: 250) // Reduce from 500
Symptoms: TotalDropped > 0 in diagnostics
Causes & Solutions:
// Investigate drop reasons
var stats = logger.GetDiagnostics();
foreach (var flow in stats.FlowStats.Where(f => f.Dropped > 0))
{
logger.Warning($"Flow {flow.Name} dropped {flow.Dropped} events");
}
Symptoms: Local logs exist, but remote endpoint has nothing
Troubleshooting Steps:
telnet host port.WriteToRetry(
primaryFlow: new HttpFlow(endpoint),
maxRetries: 5)
// Or use failover:
.WriteToFailover(
primaryFlow: new HttpFlow(endpoint),
secondaryFlow: new FileFlow("./fallback"))
Symptoms: Log files grow without bound
Solutions:
maxFileSize and maxDirectorySize.WriteToFile(
directory: "./logs",
maxFileSize: 100 * 1024 * 1024, // 100 MB
maxDirectorySize: 10L * 1024 * 1024 * 1024, // 10 GB
compression: CompressionFormat.GZip,
fileRetentionPolicy: new FileRetentionPolicy { RetentionDays = 30 })
A lightweight, multi-transport log server for the EonaCat LogStack ecosystem.
// Minimal - UDP on port 5555
var server = new Server();
await server.Start();
// Full control via ServerOptions
var server = new Server(new ServerOptions
{
UseTcp = true,
UseUdp = true,
UseHttp = true, // enables POST /ingest + GET /metrics
Port = 5555,
HttpPort = 5556,
MinimumLevel = ServerLogLevel.Information, // drop Debug/Trace
RateLimitPerSecond = 100, // per remote endpoint
LogRetentionDays = 30,
MaxLogDirectorySize = 10L * 1024 * 1024 * 1024, // 10 GB
LogsRootDirectory = "logs",
});
server.LogWritten += line => Console.WriteLine("[written] " + line);
server.LogDropped += line => Console.WriteLine("[dropped] " + line);
await server.Start();
| Transport | Default | Notes |
|---|---|---|
| TCP | enabled | Streams until connection closes |
| UDP | enabled | Max 65 507 bytes per packet |
| HTTP | disabled | Enable via UseHttp = true |
TCP and UDP can run simultaneously on the same port.
UseHttp = true)| Method | Path | Description |
|---|---|---|
POST |
/ingest |
Accept a JSON log entry or array |
GET |
/metrics |
Return live server metrics as JSON |
{ "level": "info", "message": "Hello world", "source": "MyApp", "host": "srv-01" }
[
{ "level": "warn", "message": "Disk at 80%", "source": "monitor" },
{ "level": "error", "message": "DB timeout", "source": "api", "exception": "TimeoutExceptionβ¦" }
]
{
"totalReceived": 12345,
"totalWritten": 12300,
"totalDropped": 45,
"totalBytes": 4096000,
"activeTcpConnections": 3,
"uptimeSeconds": 3600,
"startedAt": "2026-03-27T08:00:00Z"
}
If the incoming payload is valid JSON the server parses it and formats each entry before writing:
[2026-03-27T09:15:00Z] [ERROR] [MyApp] host=srv-01 trace=abc123 Something went wrong
EXCEPTION: System.TimeoutException: The operation timed out.
Recognised JSON fields:
| Field | Aliases | Description |
|---|---|---|
timestamp |
- | ISO-8601 timestamp |
level |
Level, severity, Severity |
Log level string |
message |
Message |
Log message |
source |
application |
App / service name |
host |
- | Hostname |
traceId |
- | Distributed trace ID |
exception |
- | Exception string |
Plain-text payloads are written as-is and always bypass the level filter.
MinimumLevel = ServerLogLevel.Warning // only Warning / Error / Critical are stored
Levels in order: Trace β Debug β Information β Warning β Error β Critical
RateLimitPerSecond = 100 // per remote IP:port, 0 = disabled
Dropped messages are counted in Metrics.TotalDropped and raise the LogDropped event.
var m = server.GetMetrics();
Console.WriteLine($"Written={m.TotalWritten} Dropped={m.TotalDropped} Uptime={m.Uptime}");
logs/
20260327/
EonaCatLogs.log β active file (β€ 200 MB)
EonaCatLogs_1.log β rolled over
20260326/
EonaCatLogs.log
Daily directories older than LogRetentionDays are deleted automatically.
The total directory is also capped at MaxLogDirectorySize.
server.LogWritten += line => NotifyDashboard(line);
server.LogDropped += line => Metrics.Increment("dropped");
Console.CancelKeyPress += (_, e) => { e.Cancel = true; server.Stop(); };
Stop() prints a throughput summary and disposes all listeners cleanly.
Register with default settings:
services.AddEonaCatLogging();
This registers:
ILoggerFactory - For creating category-specific loggersMicrosoft.Extensions.Logging.ILoggerFactory - For Microsoft.Extensions.Logging compatibilityILogger - For injecting the default loggerservices.AddEonaCatLogging(
minimumLevel: LogLevel.Information,
timestampMode: TimestampMode.Local);
If you've already created an EonaCatLogStack instance:
var logStack = new EonaCatLogStack("MyApp");
logStack.AddFlow(new ConsoleFlow());
services.AddEonaCatLogging(logStack);
Configure the logger directly with an Action<EonaCatLogStack>:
services.AddEonaCatLogging(logStack =>
{
logStack.AddFlow(new ConsoleFlow());
logStack.AddFlow(new FileFlow("./logs"));
logStack.AddBooster(new MachineNameBooster());
});
Use the fluent LogBuilder API for the most intuitive configuration:
services.AddEonaCatLogging("MyApplication", builder =>
{
builder
.WithMinimumLevel(LogLevel.Information)
.WithTimestampMode(TimestampMode.Local)
.WriteToConsole()
.WriteToFile("./logs")
.BoostWithMachineName()
.BoostWithProcessId()
.BoostWithCorrelationId();
});
Or with the default "Application" category:
services.AddEonaCatLogging(builder =>
{
builder
.WriteToConsole()
.WriteToFile("./logs");
});
For advanced scenarios where you need access to the service provider:
services.AddEonaCatLoggingFactory("MyApplication", builder =>
{
builder
.WriteToConsole()
.WriteToFile("./logs");
});
public class MyService
{
private readonly ILoggerFactory _loggerFactory;
public MyService(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
}
public void DoSomething()
{
var logger = _loggerFactory.CreateLogger("MyService");
logger.Log(LogLevel.Information, "Doing something");
}
}
public class MyService
{
private readonly ILogger _logger;
public MyService(ILogger logger)
{
_logger = logger;
}
public void DoSomething()
{
_logger.Log(LogLevel.Information, "Doing something");
}
}
public class MyService
{
private readonly Microsoft.Extensions.Logging.ILogger _logger;
public MyService(Microsoft.Extensions.Logging.ILogger logger)
{
_logger = logger;
}
public void DoSomething()
{
_logger.LogInformation("Doing something");
}
}
In your Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Add EonaCat LogStack to the service collection
builder.Services.AddEonaCatLogging("WebApplication", logBuilder =>
{
logBuilder
.WithMinimumLevel(LogLevel.Information)
.WriteToConsole(useColors: true)
.WriteToFile("./logs")
.BoostWithCorrelationId()
.BoostWithThreadId();
});
// Rest of your configuration...
var app = builder.Build();
// Configure HTTP request pipeline...
app.Run();
public class IndexModel : PageModel
{
private readonly ILogger _logger;
public IndexModel(ILogger logger)
{
_logger = logger;
}
public void OnGet()
{
_logger.Log(LogLevel.Information, "Index page loaded");
}
}
services.AddEonaCatLogging(builder =>
{
builder
.WriteToConsole() // Console output
.WriteToFile("./logs") // File output
.WriteToSlack("https://hooks.slack.com/...") // Slack
.WriteToDiscord("https://discordapp.com/...") // Discord
.WriteToElasticSearch("http://localhost:9200") // Elasticsearch
.WriteToEmail("smtp.gmail.com", 587, ...) // Email
.WriteToMicrosoftTeams("https://..."); // Teams
});
services.AddEonaCatLogging(builder =>
{
builder
.BoostWithMachineName() // Adds machine name
.BoostWithProcessId() // Adds process ID
.BoostWithThreadId() // Adds thread ID
.BoostWithCorrelationId() // Adds correlation ID (for distributed tracing)
.BoostWithMemory() // Adds memory usage
.BoostWithOS() // Adds OS info
.BoostWithUser() // Adds username
.BoostWithCustomText("Environment", "Production"); // Custom properties
});
services.AddEonaCatLogging(builder =>
{
builder
.WithMinimumLevel(LogLevel.Warning) // Only log warnings and above
.WriteToConsole(minimumLevel: LogLevel.Information) // More verbose for console
.WriteToFile("./logs", minimumLevel: LogLevel.Error); // Only errors to file
});
Use LogBuilder for Configuration: The fluent LogBuilder API is the most readable and maintainable approach.
Register Early: Register logging in Program.cs before other services that depend on logging.
Use Appropriate Log Levels:
Trace - Very detailed diagnostic infoDebug - Debug-level diagnostic infoInformation - General informational messagesWarning - Warning messagesError - Error messagesCritical - Critical failuresInject Specific Types: Prefer injecting ILoggerFactory to create category-specific loggers rather than injecting a single shared logger.
Use Categories: Create loggers with meaningful category names:
var logger = loggerFactory.CreateLogger("MyApp.Services.UserService");
Enable Correlation IDs: For distributed tracing scenarios:
builder.BoostWithCorrelationId()
services.AddEonaCatLogging(b => b.WriteToConsole());
services.AddEonaCatLogging(builder =>
{
builder
.WithMinimumLevel(LogLevel.Debug)
.WriteToConsole(useColors: true)
.WriteToFile("./logs")
.BoostWithMachineName()
.BoostWithThreadId();
});
services.AddEonaCatLogging("ProductionApp", builder =>
{
builder
.WithMinimumLevel(LogLevel.Information)
.WriteToFile("./logs", minimumLevel: LogLevel.Information)
.WriteToElasticSearch("https://elastic.company.com")
.WriteToSlack("https://hooks.slack.com/...")
.BoostWithCorrelationId()
.BoostWithMachineName()
.BoostWithUser();
});
public class DiagnosticsService
{
private readonly ILoggerFactory _loggerFactory;
public DiagnosticsService(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
}
public void PrintDiagnostics()
{
var diagnostics = _loggerFactory.GetDiagnostics();
Console.WriteLine($"Total Logged: {diagnostics.TotalLogged}");
Console.WriteLine($"Total Dropped: {diagnostics.TotalDropped}");
Console.WriteLine($"Total Exceptions: {diagnostics.TotalExceptions}");
}
}
EonaCat.LogStack provides comprehensive statistics and metrics tracking for monitoring and optimizing your logging infrastructure.
Track events logged at each level:
var metrics = loggerFactory.GetMetrics();
Console.WriteLine($"Trace: {metrics.TraceCount}");
Console.WriteLine($"Debug: {metrics.DebugCount}");
Console.WriteLine($"Information: {metrics.InformationCount}");
Console.WriteLine($"Warning: {metrics.WarningCount}");
Console.WriteLine($"Error: {metrics.ErrorCount}");
Console.WriteLine($"Critical: {metrics.CriticalCount}");
Monitor throughput and latency:
var metrics = loggerFactory.GetMetrics();
Console.WriteLine($"Events/Second: {metrics.WritesPerSecond:F2}");
Console.WriteLine($"Total Bytes: {metrics.TotalBytes:N0}");
Console.WriteLine($"Avg Bytes/Event: {metrics.AverageBytesPerEvent:F2}");
Console.WriteLine($"Success Rate: {metrics.SuccessRate:F2}%");
Console.WriteLine($"Uptime: {TimeSpan.FromMilliseconds(metrics.UptimeMilliseconds):hh\\:mm\\:ss}");
Monitor exceptions logged:
var metrics = loggerFactory.GetMetrics();
Console.WriteLine($"Total Exceptions: {metrics.TotalExceptions}");
Console.WriteLine($"Exceptions in Errors: {metrics.ErrorCount} errors logged");
Each flow tracks its own performance:
var metrics = loggerFactory.GetMetrics();
foreach (var flow in metrics.FlowMetrics)
{
Console.WriteLine($"{flow.FlowName} ({flow.FlowType}):");
Console.WriteLine($" Processed: {flow.EventsProcessed}");
Console.WriteLine($" Failed: {flow.EventsFailed}");
Console.WriteLine($" Success Rate: {flow.SuccessRate:F2}%");
Console.WriteLine($" Events/Second: {flow.EventsPerSecond:F2}");
Console.WriteLine($" P95 Latency: {flow.P95LatencyMs:F3}ms");
Console.WriteLine($" P99 Latency: {flow.P99LatencyMs:F3}ms");
}
Use AdvancedMetricsCollector to aggregate metrics across multiple loggers:
var collector = app.Services.GetRequiredService<AdvancedMetricsCollector>();
// Register loggers for collection
var logStack = app.Services.GetRequiredService<EonaCatLogStack>();
collector.RegisterLogger(logStack);
// Get aggregated metrics
var aggregated = collector.GetAggregatedMetrics();
Console.WriteLine($"Total Logged (All): {aggregated.TotalLoggedAcrossAll:N0}");
Console.WriteLine($"Overall Success Rate: {aggregated.OverallSuccessRate:F2}%");
// Get performance comparison
var comparison = collector.GetPerformanceComparison();
Console.WriteLine($"Highest Throughput: {comparison.HighestThroughputLogger?.WritesPerSecond:F2} events/sec");
Console.WriteLine($"Average Throughput: {comparison.AverageWritesPerSecond:F2} events/sec");
// Get health report
var health = collector.GetHealthReport();
Console.WriteLine($"System Status: {health.OverallStatus}");
foreach (var warning in health.Warnings)
Console.WriteLine($" β οΈ {warning}");
EonaCat.LogStack seamlessly integrates with Microsoft Dependency Injection for ASP.NET Core and other .NET applications.
var builder = Host.CreateApplicationBuilder();
builder.Services.AddEonaCatLogging();
This registers:
ILoggerFactory (EonaCat interface)Microsoft.Extensions.Logging.ILoggerFactory (standard interface)ILogger (EonaCat interface)Microsoft.Extensions.Logging.ILogger (standard interface)var builder = Host.CreateApplicationBuilder();
builder.Services.AddEonaCatLogging(b =>
{
b.WithMinimumLevel(LogLevel.Information)
.WriteToConsole()
.WriteToFile("./logs")
.BoostWithCorrelationId();
});
var builder = Host.CreateApplicationBuilder();
builder.AddEonaCatLogging(b =>
{
b.WithMinimumLevel(LogLevel.Information)
.WriteToConsole()
.WriteToFile("./logs");
});
var app = builder.Build();
For category-specific configuration and context propagation:
var builder = Host.CreateApplicationBuilder();
builder.Services.AddAdvancedEonaCatLogging(factory =>
{
factory.ConfigureCategory("Database", config =>
{
config.MinimumLevel = LogLevel.Debug;
config.Boosters.Add(new CorrelationIdBooster());
});
factory.ConfigureCategory("Security", config =>
{
config.MinimumLevel = LogLevel.Warning;
});
});
var app = builder.Build();
// Get loggers
var dbLogger = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger("Database");
var secLogger = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger("Security");
var builder = Host.CreateApplicationBuilder();
builder.AddEonaCatLogging(b =>
{
b.WithMinimumLevel(LogLevel.Information)
.WriteToConsole()
.WriteToFile("./logs");
});
builder.AddEonaCatMetricsCollection();
var app = builder.Build();
// Access metrics
var collector = app.Services.GetRequiredService<AdvancedMetricsCollector>();
var metrics = collector.GetAggregatedMetrics();
Console.WriteLine(metrics);
The registered adapters allow you to use standard Microsoft logging interfaces:
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
private readonly Microsoft.Extensions.Logging.ILogger<UserController> _logger;
public UserController(Microsoft.Extensions.Logging.ILogger<UserController> logger)
{
_logger = logger;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
_logger.LogInformation("Fetching user {UserId}", id);
// ...
}
}
The request is automatically routed through EonaCat.LogStack flows (file, console, Slack, etc.).
With AdvancedLoggerFactory, you can propagate context across async operations:
var factory = app.Services.GetRequiredService<AdvancedLoggerFactory>();
// Set context in request scope
factory.SetContextData("RequestId", context.TraceIdentifier);
factory.SetContextData("UserId", user.Id);
// Logger can access this context
var logger = factory.CreateLogger("MyCategory");
logger.Log(LogLevel.Information, "Processing request");
// Context is included automatically
// Clear context when done
factory.ClearContextData();
The logger is registered as a Singleton in the DI container, so it will be automatically disposed when the application shuts down. You can also manually access and dispose it:
var loggerFactory = app.Services.GetRequiredService<ILoggerFactory>();
await loggerFactory.DisposeAsync();
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 net5.0 was computed. net5.0-windows net5.0-windows was computed. net6.0 net6.0 was computed. net6.0-android net6.0-android was computed. net6.0-ios net6.0-ios was computed. net6.0-maccatalyst net6.0-maccatalyst was computed. net6.0-macos net6.0-macos was computed. net6.0-tvos net6.0-tvos was computed. net6.0-windows net6.0-windows was computed. net7.0 net7.0 was computed. net7.0-android net7.0-android was computed. net7.0-ios net7.0-ios was computed. net7.0-maccatalyst net7.0-maccatalyst was computed. net7.0-macos net7.0-macos was computed. net7.0-tvos net7.0-tvos was computed. net7.0-windows net7.0-windows was computed. net8.0 net8.0 is compatible. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 was computed. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 was computed. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 netcoreapp3.0 was computed. netcoreapp3.1 netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 netstandard2.1 is compatible. |
| .NET Framework | net48 net48 is compatible. net481 net481 was computed. |
| MonoAndroid | monoandroid monoandroid was computed. |
| MonoMac | monomac monomac was computed. |
| MonoTouch | monotouch monotouch was computed. |
| Tizen | tizen60 tizen60 was computed. |
| Xamarin.iOS | xamarinios xamarinios was computed. |
| Xamarin.Mac | xamarinmac xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos xamarinwatchos was computed. |
Showing the top 2 NuGet packages that depend on EonaCat.LogStack:
| Package | Downloads |
|---|---|
|
EonaCat.LogStack.OpenTelemetryFlow
EonaCat OpenTelemetry Flow for LogStack |
|
|
EonaCat.LogStack.Flows.WindowsEventLog
EonaCat Windows EventLog Flow for LogStack |
This package is not used by any popular GitHub repositories.
Public release version