9.6 KiB
EonaCat.LogStack
EonaCat.LogStack flow-based 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.
Features
- Flow-based architecture — route log events to one or many output destinations simultaneously
- Booster system — enrich every log event with contextual metadata (machine name, process ID, thread info, memory, uptime, correlation IDs, and more)
- Pre-build modifiers — intercept and mutate log events before they are written
- Zero-allocation hot path —
AggressiveInliningthroughout,StringBuilderpooling, andref-based builder pattern - Async-first — all flows implement
IAsyncDisposableandFlushAsync - Resilience built-in — retry, failover, throttling, and rolling buffer flows
- Tamper-evident audit trail — SHA-256 hash-chained audit files
- Encrypted file logging — AES-encrypted log files with a built-in decrypt utility
- Compression — GZip-compressed rolled log files
- Category routing — split logs into separate files per category or log level
- Diagnostics — live counters (total logged, total dropped, per-flow stats)
Supported Targets
- .NET Standard 2.1
- .NET 8.0
- .NET Framework 4.8
Installation
dotnet add package EonaCat.LogStack
Quick Start
await using var logger = LogBuilder.CreateDefault("MyApp");
logger.Information("Application started");
logger.Warning("Low memory warning");
logger.Error(ex, "Unexpected error occurred");
CreateDefault creates a logger writing to both the console and a ./logs directory, enriched with machine name and process ID.
Fluent Configuration
Build a fully customised 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();
Logging Methods
logger.Trace("Verbose trace message");
logger.Debug("Debug detail");
logger.Information("Something happened");
logger.Warning("Potential problem");
logger.Warning(ex, "Warning with exception");
logger.Error("Something failed");
logger.Error(ex, "Error with exception");
logger.Critical("System is going down");
logger.Critical(ex, "Critical failure");
// With structured properties
logger.Log(LogLevel.Information, "User logged in",
("UserId", 42),
("IP", "192.168.1.1"));
Available Flows
Flows can be extended with custom implementations of IFlow, but here are the built-in options:
| Flow | Method | Description |
|---|---|---|
| Console | WriteToConsole() |
Colored console output |
| File | WriteToFile() |
Batched, rotated, compressed file output |
| Encrypted File | WriteToEncryptedFile() |
AES-encrypted log files |
| Memory | WriteToMemory() |
In-memory ring buffer |
| Audit | WriteToAudit() |
Tamper-evident hash-chained audit trail |
| Database | WriteToDatabase() |
ADO.NET database sink |
| HTTP | WriteToHttp() |
Generic HTTP endpoint (batched) |
| Webhook | WriteToWebhook() |
Generic webhook POST |
WriteToEmail() |
HTML digest emails via SMTP | |
| Slack | WriteToSlack() |
Slack incoming webhooks |
| Discord | WriteToDiscord() |
Discord webhooks |
| Microsoft Teams | WriteToMicrosoftTeams() |
Teams incoming webhooks |
| Telegram | WriteToTelegram() |
Telegram bot messages |
| SignalR | WriteToSignalR() |
Real-time SignalR hub push |
| Redis | RedisFlow() |
Redis Pub/Sub + optional List persistence |
| Elasticsearch | WriteToElasticSearch() |
Elasticsearch index |
| Splunk | WriteToSplunkFlow() |
Splunk HEC |
| 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) |
| UDP | WriteToUdp() |
Raw UDP datagrams |
| SNMP Trap | WriteToSnmpTrap() |
SNMP v2c traps |
| Zabbix | WriteToZabbixFlow() |
Zabbix trapper protocol |
| EventLog | WriteToEventLogFlow() |
Remote event log forwarding |
| Rolling Buffer | WriteToRollingBuffer() |
Circular buffer with trigger-based flush |
| Throttled | WriteToThrottled() |
Token-bucket rate limiting + deduplication |
| Retry | WriteToRetry() |
Automatic retry with exponential back-off |
| Failover | WriteToFailover() |
Primary/secondary failover |
| Diagnostics | WriteDiagnostics() |
Periodic diagnostic snapshots |
| Status | WriteToStatusFlow() |
Service health monitoring |
Available Boosters
Boosters enrich every log event with additional properties before it reaches any flow.
new LogBuilder("MyApp")
.BoostWithMachineName() // host name
.BoostWithProcessId() // PID
.BoostWithThreadId() // managed thread ID
.BoostWithThreadName() // thread name
.BoostWithUser() // current OS user
.BoostWithApp() // app name and base directory
.BoostWithApplication("MyApp", "2.0.0") // explicit name + version
.BoostWithEnvironment("Production")
.BoostWithOS() // OS description
.BoostWithFramework() // .NET runtime description
.BoostWithMemory() // working set in MB
.BoostWithUptime() // process uptime in seconds
.BoostWithProcStart() // process start time
.BoostWithDate() // current date (yyyy-MM-dd)
.BoostWithTime() // current time (HH:mm:ss.fff)
.BoostWithTicks() // current timestamp ticks
.BoostWithCorrelationId() // Activity.Current correlation ID
.BoostWithCustomText("env", "prod") // arbitrary key/value
.Boost("myBooster", () => new Dictionary<string, object?> { ["key"] = "val" })
...
Pre-Build Modifiers
Modifiers run after boosters and can mutate or cancel a log event before it is dispatched to flows:
logger.AddModifier((ref LogEventBuilder builder) =>
{
builder.WithProperty("RequestId", Guid.NewGuid().ToString());
});
Resilience Patterns
Retry with exponential back-off
.WriteToRetry(
primaryFlow: new HttpFlow("https://logs.example.com"),
maxRetries: 5,
initialDelay: TimeSpan.FromMilliseconds(200),
exponentialBackoff: true)
Primary / secondary failover
.WriteToFailover(
primaryFlow: new ElasticSearchFlow("https://es-prod:9200"),
secondaryFlow: new FileFlow("./fallback-logs"))
Token-bucket throttling with deduplication
.WriteToThrottled(
inner: new SlackFlow(webhookUrl),
burstCapacity: 10,
refillPerSecond: 1.0,
deduplicate: true,
dedupWindow: TimeSpan.FromSeconds(60))
Rolling buffer — flush context on error
.WriteToRollingBuffer(
capacity: 500,
triggerLevel: LogLevel.Error,
triggerTarget: new FileFlow("./error-context"))
Encrypted File Logging
.WriteToEncryptedFile("./secure-logs", password: "s3cr3t")
To decrypt later:
LogBuilder.DecryptFile(
encryptedPath: "./secure-logs/log.enc",
outputPath: "./secure-logs/log.txt",
password: "s3cr3t");
Audit Trail
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",
auditLevel: AuditLevel.WarningAndAbove,
includeProperties: true)
Verify integrity at any time:
bool intact = AuditFlow.Verify("./audit/audit.audit");
Log Message Template
Both ConsoleFlow and FileFlow accept a customisable template string:
[{ts}] [{tz}] [Host: {host}] [Category: {category}] [Thread: {thread}] [{logtype}] {message}{props}
| Token | Description |
|---|---|
{ts} |
Timestamp (yyyy-MM-dd HH:mm:ss.fff) |
{tz} |
Timezone (UTC or local name) |
{host} |
Machine name |
{category} |
Logger category |
{thread} |
Managed thread ID |
{pid} |
Process ID |
{logtype} |
Log level label (INFO, WARN, ERROR, …) |
{message} |
Log message text |
{props} |
Structured properties as key=value pairs |
{newline} |
Line break |
Diagnostics
var diag = logger.GetDiagnostics();
Console.WriteLine($"Logged: {diag.TotalLogged}, Dropped: {diag.TotalDropped}");
Flushing and Disposal
// Flush all pending events
await logger.FlushAsync();
// Dispose (flushes automatically)
await logger.DisposeAsync();
Events
logger.OnLog += (sender, msg) =>
{
// Fired for every log event that passes filters
Console.WriteLine($"[Event] {msg.Level}: {msg.Message}");
};
Custom Flows
Implement IFlow (or extend FlowBase) to create your own destination:
public class MyFlow : FlowBase
{
public MyFlow() : base("MyFlow", LogLevel.Trace) { }
public override Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken ct = default)
{
// Write logEvent somewhere
return Task.FromResult(WriteResult.Success);
}
public override Task FlushAsync(CancellationToken ct = default) => Task.CompletedTask;
}
// Register with:
new LogBuilder("App").WriteTo(new MyFlow()).Build();