EonaCat.LogStack 0.0.9

Prefix Reserved
There is a newer version of this package available.
See the version list below for details.
dotnet add package EonaCat.LogStack --version 0.0.9
                    
NuGet\Install-Package EonaCat.LogStack -Version 0.0.9
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="EonaCat.LogStack" Version="0.0.9" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EonaCat.LogStack" Version="0.0.9" />
                    
Directory.Packages.props
<PackageReference Include="EonaCat.LogStack" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add EonaCat.LogStack --version 0.0.9
                    
#r "nuget: EonaCat.LogStack, 0.0.9"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package EonaCat.LogStack@0.0.9
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=EonaCat.LogStack&version=0.0.9
                    
Install as a Cake Addin
#tool nuget:?package=EonaCat.LogStack&version=0.0.9
                    
Install as a Cake Tool

EonaCat.LogStack

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.

Features

Core Architecture

  • Flow-based system - Route log events to multiple destinations simultaneously. Each flow is independent and can be configured with its own level filters and batching strategy.
  • Booster system - Automatically enrich log events with contextual metadata like machine name, process ID, thread info, memory usage, uptime, correlation IDs, and custom properties.
  • Pre-build modifiers - Intercept and mutate log events before they reach any flow using a chainable modifier system.
  • Zero-allocation hot path - AggressiveInlining throughout, StringBuilder pooling, and ref-based builder pattern for minimal GC pressure during logging.
  • Async-first design - All flows implement IAsyncDisposable and FlushAsync() for proper async resource cleanup and batching.

Advanced Features

  • Resilience patterns - Built-in RetryFlow with exponential backoff, FailoverFlow for primary/secondary failover, and ThrottledFlow for rate limiting with deduplication.
  • Tamper-evident audit - SHA-256 hash-chained audit files where deleting or modifying any past entry invalidates all subsequent hashes. Built-in integrity verification.
  • Encrypted file logging - AES-encrypted log files with password protection. Includes DecryptFile() utility to decrypt files on demand.
  • Log compression - Automatic GZip compression of rolled-over log files to conserve disk space.
  • Category/level-based routing - Split logs into separate files per category or log level for better organization.
  • Rolling buffers - Circular buffer with trigger-based flushing. When a critical event occurs, buffer context (previous N lines) is forwarded to a secondary flow.
  • Diagnostics & metrics - Real-time counters for total logged, total dropped, per-flow statistics, and flow-specific performance data.
  • Live level control - Dynamically change log level at runtime without recreating the logger.
  • Structured logging - First-class support for structured properties (tuples, dictionaries) that are included in JSON output where applicable.

Performance & Reliability

  • Configurable backpressure - Choose between Wait, Drop, or Block strategies when log queues are full.
  • Batch processing - Most flows support configurable batch sizes and intervals for efficient I/O.
  • Rate limiting & deduplication - Protect high-latency sinks (email, Slack, HTTP) from log storms using token-bucket rate limiting and optional message deduplication.
  • Memory flow - Store recent logs in a circular in-memory buffer for quick diagnostics or fallback output.
  • Lazy initialization - Flows are only initialized when first used, reducing startup overhead.

Supported Targets

  • .NET Standard 2.1
  • .NET 8.0
  • .NET 9.0
  • .NET 10.0
  • .NET Framework 4.8

Installation

dotnet add package EonaCat.LogStack

Quick Start

Minimal - Console + File

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.

Fluent Configuration

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();
}

Logging Methods

Basic Logging

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");

Logging with Exceptions

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");
}

Structured Properties

// 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`

Custom Log Event Modification

logger.AddModifier((ref LogEventBuilder builder) =>
{
    builder.WithProperty("RequestId", HttpContext.TraceIdentifier);
    builder.WithProperty("UserId", User.Id);
});

logger.Information("Processing request");  // RequestId and UserId added automatically

Available Flows

Flows are the destinations where log events are written. Each flow is independent and can have its own level filter and configuration.

Flows Overview

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

Flow Examples

Console Output
// 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)
File Output
// 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 })
Encrypted File Output
// 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!");
In-Memory Buffer
// Store last 1000 events in memory
.WriteToMemory(capacity: 1000, minimumLevel: LogLevel.Information)

// Access stored events
var memoryFlow = logger.GetFlowOfType<MemoryFlow>();
var events = memoryFlow.GetEvents();
Slack
.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)
Discord
.WriteToDiscord(
    webHookUrl: "https://discordapp.com/api/webhooks/YOUR/WEBHOOK",
    botName: "ErrorBot")
Email Digest
.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)
Database (SQL Server, PostgreSQL, MySQL, etc.)
.WriteToDatabase(
    connectionFactory: () => new SqlConnection("connection-string"),
    tableName: "ApplicationLogs",
    batchSize: 10)
Elasticsearch
.WriteToElasticSearch(
    elasticSearchUrl: "https://elasticsearch.company.com:9200",
    indexName: "myapp-logs",
    batchSize: 100)
Redis (Pub/Sub + List)
// 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)
Syslog
// 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)
HTTP Endpoint
.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"
    })
Audit Trail (Tamper-Evident)
// 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!");
Rolling Buffer (Context-on-Error)
// 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)

Available Boosters

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.

System Information Boosters

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

Date/Time Boosters

.BoostWithDate()             // Add current date (yyyy-MM-dd)
.BoostWithTime()             // Add current time (HH:mm:ss.fff)
.BoostWithTicks()            // Add current timestamp ticks (for precise timing)

Application Context Boosters

.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

Custom Data Boosters

// 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())

Complete Booster Configuration Example

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();

What Boosters Add to Logs

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"
}

Pre-Build Modifiers

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.

Basic Modifier Example

logger.AddModifier((ref LogEventBuilder builder) =>
{
    // Add request context
    builder.WithProperty("RequestId", HttpContext.TraceIdentifier);
});

// Now every log will include RequestId automatically
logger.Information("Processing request");

Multiple Modifiers

// 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]");
});

Canceling Events with Modifiers

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
});

Request Context Integration (ASP.NET Core / Razor Pages)

// 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");
    }
}

Resilience Patterns

Retry with Exponential Back-off

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

Primary / Secondary Failover

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

Token-Bucket Rate Limiting with Deduplication

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)
  • Next error within 1 second is queued (rate-limited to 1/sec)
  • Identical errors within 60s → counted, then sent as "N duplicate messages"

Rolling Buffer - Context-on-Error

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

Circuit Breaker

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

Combining Patterns

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();

Encrypted File Logging

Encrypt sensitive logs with AES encryption and password protection.

Writing Encrypted Logs

.WriteToEncryptedFile(
    directory: "./secure-logs",
    filePrefix: "encrypted",
    password: "YourStrongPassword123!",
    maxFileSize: 50 * 1024 * 1024)

Decrypting Logs

// 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");

Use Cases

  • Compliance requirements (HIPAA, PCI-DSS)
  • Storing sensitive user data or API keys in logs
  • Secure log storage on shared infrastructure
  • Audit trails with encryption

Audit Trail (Tamper-Evident)

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.

Writing Audit Logs

.WriteToAudit(
    directory: "./audit",
    filePrefix: "audit",
    auditLevel: AuditLevel.WarningAndAbove,  // Warning, Error, Critical
    includeProperties: true)

Verification

// 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.)
}

Audit Levels

AuditLevel.All              // Every log event
AuditLevel.WarningAndAbove  // Warning, Error, Critical
AuditLevel.ErrorAndAbove    // Error, Critical
AuditLevel.CriticalOnly     // Critical only

Example Audit File

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.

Log Message Template

Both ConsoleFlow and FileFlow accept a customizable template string for formatting output.

Default Template

[{ts}] [Host: {host}] [Category: {category}] [Thread: {thread}] [{logtype}] {message}{props}

Available Tokens

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)

Custom Templates

// 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}")

Advanced Message Template Features

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);
Nested Property Access

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
Array/Collection Indexing

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
Alignment and Padding

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)
String Filters

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
Fallback Values

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)
Conditional Rendering

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)
Advanced Conditionals

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);
Loops and Collections

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)
Math Filters

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
String Manipulation Filters

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
Comparison Filters

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
Date & Time Filters

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
Chaining Multiple Filters

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)
Complete Advanced Template Examples
// 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
Template Features Summary

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}
Format Specifiers

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
Destructuring

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: [...]}
Complete Advanced Template Examples
// 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

Real-World Configuration Scenarios

Scenario 1: Development Environment

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();

Scenario 2: Production - Multi-Destination with Resilience

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();

Scenario 3: Microservices / Distributed Tracing

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();

Scenario 4: Real-Time Dashboard

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();

Scenario 5: Compliance & Audit

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!");
        }
    }
});

Scenario 6: High-Volume with Batching & Rate Limiting

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();

Diagnostics

Real-Time Metrics

// 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}");
}

Diagnostic Snapshots

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()
    })

Monitoring Health

// 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");
        }
    }
});

Flushing and Disposal

All flows support batching, so events may not be written immediately. Use FlushAsync() to ensure all pending events are written before shutdown.

Explicit Flushing

// Flush all pending events synchronously (up to 5 seconds)
await logger.FlushAsync();

// After flush, you can safely dispose
await logger.DisposeAsync();

Automatic Flushing on Disposal

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()

Best Practices

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
}

Custom Disposal Logic

var logger = new LogBuilder("MyApp").Build();

try
{
    logger.Information("Processing...");
}
finally
{
    // Manual control
    await logger.FlushAsync();

    // Perform custom cleanup
    CleanupResources();

    await logger.DisposeAsync();
}

Events

Subscribe to log events for real-time processing or custom handling.

Basic Event Handler

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");

Advanced Event Handling

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);
    }
};

Multiple Event Handlers

// 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);
};

Custom Flows

Create custom flows by implementing IFlow or extending FlowBase for complex scenarios.

Simple Custom Flow

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();

Advanced Custom Flow with Batching

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();
    }
}

Custom Flow with Retry Logic

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;
}

Best Practices & Tips

1. Use Appropriate Log Levels

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");

2. Use Structured Properties for Queryability

// 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}");

3. Use Categories for Filtering

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

4. Always Handle Exceptions

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
}

5. Use Correlation IDs for Tracing

builder.Services.AddEonaCatLogging("MyApp", logBuilder =>
{
    logBuilder.BoostWithCorrelationId();  // Automatic tracing
});

// All logs in the same request/operation automatically get the same correlation ID

6. Configure Different Levels per Flow

.WithMinimumLevel(LogLevel.Information)  // Global minimum
.WriteToConsole(minimumLevel: LogLevel.Debug)  // More verbose
.WriteToFile(minimumLevel: LogLevel.Warning)   // Less verbose
.WriteToSlack(minimumLevel: LogLevel.Error)    // Errors only

7. Use Async Flushing Before Shutdown

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();

8. Monitor Drop Rates

_ = 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));
        }
    }
});

9. Use Rolling Buffers for Post-Mortem

// Before: error happens, context is lost
// After: rolling buffer captures previous 100 logs
.WriteToRollingBuffer(
    capacity: 500,
    triggerLevel: LogLevel.Error,
    triggerTarget: new FileFlow("./error-context"))

10. Encrypt Sensitive Data

.WriteToEncryptedFile(
    directory: "./secure-logs",
    password: GetEncryptionPasswordFromVault())

Troubleshooting

High CPU Usage

Symptoms: Excessive CPU while logging

Solutions:

  • Increase batch size to reduce I/O operations
  • Use WriteToThrottled() to rate limit
  • Check if remote endpoints are responding (network latency)
.WriteToFile("./logs", batchSize: 100, flushIntervalInMilliSeconds: 5000)
.WriteToThrottled(inner: new HttpFlow(url), burstCapacity: 50)

High Memory Usage

Symptoms: Memory grows linearly with time

Solutions:

  • Check if logs are being flushed (batches aren't sent)
  • Reduce rolling buffer capacity
  • Enable compression for file logs
.WriteToFile(compression: CompressionFormat.GZip)
.WriteToRollingBuffer(capacity: 250)  // Reduce from 500

Events Being Dropped

Symptoms: TotalDropped > 0 in diagnostics

Causes & Solutions:

  • Backpressure queue full → increase capacity or reduce send rate
  • Flow errors → check flow configuration and connectivity
  • Log level filters → verify minimumLevel settings
// 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");
}

Logs Not Reaching Remote Destination

Symptoms: Local logs exist, but remote endpoint has nothing

Troubleshooting Steps:

  1. Verify endpoint connectivity: telnet host port
  2. Check authentication (API keys, certificates)
  3. Enable retry and failover patterns
  4. Ensure events pass level filters
.WriteToRetry(
    primaryFlow: new HttpFlow(endpoint),
    maxRetries: 5)

// Or use failover:
.WriteToFailover(
    primaryFlow: new HttpFlow(endpoint),
    secondaryFlow: new FileFlow("./fallback"))

File Size Growing Too Large

Symptoms: Log files grow without bound

Solutions:

  • Configure maxFileSize and maxDirectorySize
  • Enable compression
  • Set retention policy
.WriteToFile(
    directory: "./logs",
    maxFileSize: 100 * 1024 * 1024,  // 100 MB
    maxDirectorySize: 10L * 1024 * 1024 * 1024,  // 10 GB
    compression: CompressionFormat.GZip,
    fileRetentionPolicy: new FileRetentionPolicy { RetentionDays = 30 })

EonaCat.LogStack.Server

A lightweight, multi-transport log server for the EonaCat LogStack ecosystem.

Quick start

// 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();

Transports

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.

HTTP endpoints (when UseHttp = true)

Method Path Description
POST /ingest Accept a JSON log entry or array
GET /metrics Return live server metrics as JSON

POST /ingest - single entry

{ "level": "info", "message": "Hello world", "source": "MyApp", "host": "srv-01" }

POST /ingest - batch

[
  { "level": "warn",  "message": "Disk at 80%",  "source": "monitor" },
  { "level": "error", "message": "DB timeout",   "source": "api", "exception": "TimeoutException…" }
]

GET /metrics response

{
  "totalReceived": 12345,
  "totalWritten": 12300,
  "totalDropped": 45,
  "totalBytes": 4096000,
  "activeTcpConnections": 3,
  "uptimeSeconds": 3600,
  "startedAt": "2026-03-27T08:00:00Z"
}

Structured JSON parsing

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.

Log level filtering

MinimumLevel = ServerLogLevel.Warning  // only Warning / Error / Critical are stored

Levels in order: Trace → Debug → Information → Warning → Error → Critical

Rate limiting

RateLimitPerSecond = 100  // per remote IP:port, 0 = disabled

Dropped messages are counted in Metrics.TotalDropped and raise the LogDropped event.

Metrics

var m = server.GetMetrics();
Console.WriteLine($"Written={m.TotalWritten} Dropped={m.TotalDropped} Uptime={m.Uptime}");

Log file layout

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.

Events

server.LogWritten += line => NotifyDashboard(line);
server.LogDropped += line => Metrics.Increment("dropped");

Graceful shutdown

Console.CancelKeyPress += (_, e) => { e.Cancel = true; server.Stop(); };

Stop() prints a throughput summary and disposes all listeners cleanly.

Dependency Injection (DI) registration methods

1. Basic Registration (Simplest)

Register with default settings:

services.AddEonaCatLogging();

This registers:

  • ILoggerFactory - For creating category-specific loggers
  • Microsoft.Extensions.Logging.ILoggerFactory - For Microsoft.Extensions.Logging compatibility
  • ILogger - For injecting the default logger

2. Registration with Log Level and Timestamp Mode

services.AddEonaCatLogging(
    minimumLevel: LogLevel.Information,
    timestampMode: TimestampMode.Local);

3. Registration with Pre-built EonaCatLogStack

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");
});

6. Registration with Factory Method (Advanced)

For advanced scenarios where you need access to the service provider:

services.AddEonaCatLoggingFactory("MyApplication", builder =>
{
    builder
        .WriteToConsole()
        .WriteToFile("./logs");
});

Using in Your Application

Injecting ILoggerFactory

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");
    }
}

Injecting ILogger

public class MyService
{
    private readonly ILogger _logger;

    public MyService(ILogger logger)
    {
        _logger = logger;
    }

    public void DoSomething()
    {
        _logger.Log(LogLevel.Information, "Doing something");
    }
}

Using with Microsoft.Extensions.Logging.ILogger

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");
    }
}

ASP.NET Core / Razor Pages Integration

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();

In Razor Page Code-Behind

public class IndexModel : PageModel
{
    private readonly ILogger _logger;

    public IndexModel(ILogger logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
        _logger.Log(LogLevel.Information, "Index page loaded");
    }
}

Features

Multiple Output Destinations (Flows)

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
});

Enriching Log Events (Boosters)

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
});

Log Level Filtering

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
});

Best Practices

  1. Use LogBuilder for Configuration: The fluent LogBuilder API is the most readable and maintainable approach.

  2. Register Early: Register logging in Program.cs before other services that depend on logging.

  3. Use Appropriate Log Levels:

    • Trace - Very detailed diagnostic info
    • Debug - Debug-level diagnostic info
    • Information - General informational messages
    • Warning - Warning messages
    • Error - Error messages
    • Critical - Critical failures
  4. Inject Specific Types: Prefer injecting ILoggerFactory to create category-specific loggers rather than injecting a single shared logger.

  5. Use Categories: Create loggers with meaningful category names:

    var logger = loggerFactory.CreateLogger("MyApp.Services.UserService");
    
  6. Enable Correlation IDs: For distributed tracing scenarios:

    builder.BoostWithCorrelationId()
    

Configuration Examples

Minimal Setup (Console Only)

services.AddEonaCatLogging(b => b.WriteToConsole());

Development Environment

services.AddEonaCatLogging(builder =>
{
    builder
        .WithMinimumLevel(LogLevel.Debug)
        .WriteToConsole(useColors: true)
        .WriteToFile("./logs")
        .BoostWithMachineName()
        .BoostWithThreadId();
});

Production Environment

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();
});

Diagnostics

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}");
    }
}

Statistics & Metrics

EonaCat.LogStack provides comprehensive statistics and metrics tracking for monitoring and optimizing your logging infrastructure.

Per-Level Metrics

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}");

Performance Metrics

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}");

Exception Tracking

Monitor exceptions logged:

var metrics = loggerFactory.GetMetrics();
Console.WriteLine($"Total Exceptions:    {metrics.TotalExceptions}");
Console.WriteLine($"Exceptions in Errors: {metrics.ErrorCount} errors logged");

Flow-Specific Statistics

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");
}

System-Wide Metrics Collection

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}");

Dependency Injection Integration

EonaCat.LogStack seamlessly integrates with Microsoft Dependency Injection for ASP.NET Core and other .NET applications.

Basic Registration

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)

With Fluent Configuration

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();

Advanced Features with AdvancedLoggerFactory

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");

With Metrics Collection

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);

Using Microsoft.Extensions.Logging

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

Context Propagation

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();

Disposing of the Logger

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 Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
.NET Framework net48 is compatible.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

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

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.1 56 6/16/2026
0.1.0 50 6/16/2026
0.0.9 89 6/11/2026
0.0.8 103 6/9/2026
0.0.7 104 6/2/2026
0.0.6 103 6/1/2026
0.0.4 498 4/29/2026
0.0.3 132 4/3/2026
0.0.2 251 3/5/2026
0.0.1 209 2/27/2026

Public release version