2795 lines
77 KiB
Markdown
2795 lines
77 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
dotnet add package EonaCat.LogStack
|
|
```
|
|
|
|
## Quick Start
|
|
|
|
### Minimal - Console + File
|
|
|
|
```csharp
|
|
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`:
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
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
|
|
```csharp
|
|
// 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
|
|
```csharp
|
|
// 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
|
|
```csharp
|
|
// 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
|
|
```csharp
|
|
// 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
|
|
```csharp
|
|
.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
|
|
```csharp
|
|
.WriteToDiscord(
|
|
webHookUrl: "https://discordapp.com/api/webhooks/YOUR/WEBHOOK",
|
|
botName: "ErrorBot")
|
|
```
|
|
|
|
#### Email Digest
|
|
```csharp
|
|
.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.)
|
|
```csharp
|
|
.WriteToDatabase(
|
|
connectionFactory: () => new SqlConnection("connection-string"),
|
|
tableName: "ApplicationLogs",
|
|
batchSize: 10)
|
|
```
|
|
|
|
#### Elasticsearch
|
|
```csharp
|
|
.WriteToElasticSearch(
|
|
elasticSearchUrl: "https://elasticsearch.company.com:9200",
|
|
indexName: "myapp-logs",
|
|
batchSize: 100)
|
|
```
|
|
|
|
#### Redis (Pub/Sub + List)
|
|
```csharp
|
|
// 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
|
|
```csharp
|
|
// 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
|
|
```csharp
|
|
.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)
|
|
```csharp
|
|
// 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)
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
.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
|
|
|
|
```csharp
|
|
.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
|
|
|
|
```csharp
|
|
.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
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
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)
|
|
|
|
```csharp
|
|
// 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.
|
|
|
|
```csharp
|
|
.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.
|
|
|
|
```csharp
|
|
.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.
|
|
|
|
```csharp
|
|
.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.
|
|
|
|
```csharp
|
|
.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.
|
|
|
|
```csharp
|
|
.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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
.WriteToEncryptedFile(
|
|
directory: "./secure-logs",
|
|
filePrefix: "encrypted",
|
|
password: "YourStrongPassword123!",
|
|
maxFileSize: 50 * 1024 * 1024)
|
|
```
|
|
|
|
### Decrypting Logs
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
.WriteToAudit(
|
|
directory: "./audit",
|
|
filePrefix: "audit",
|
|
auditLevel: AuditLevel.WarningAndAbove, // Warning, Error, Critical
|
|
includeProperties: true)
|
|
```
|
|
|
|
### Verification
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
// 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:
|
|
|
|
```csharp
|
|
logger.Information("User {User} logged in from {IP}", user, ipAddress);
|
|
```
|
|
|
|
#### Nested Property Access
|
|
|
|
Access properties of objects using dot notation:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
// 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:
|
|
|
|
```csharp
|
|
// 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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
// 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:
|
|
|
|
```csharp
|
|
// 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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
// 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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
// 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:
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
// 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:
|
|
|
|
```csharp
|
|
// 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:
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
// 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.
|
|
|
|
```csharp
|
|
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.
|
|
|
|
```csharp
|
|
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.
|
|
|
|
```csharp
|
|
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.
|
|
|
|
```csharp
|
|
// 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.
|
|
|
|
```csharp
|
|
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.
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
// 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.
|
|
|
|
```csharp
|
|
.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
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
// 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:
|
|
|
|
```csharp
|
|
await using var logger = new LogBuilder("MyApp")
|
|
.WriteToFile("./logs")
|
|
.Build();
|
|
|
|
logger.Information("Application running");
|
|
|
|
// At scope exit: FlushAsync() called automatically, then DisposeAsync()
|
|
```
|
|
|
|
### Best Practices
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```csharp
|
|
// 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**
|
|
|
|
```csharp
|
|
// 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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```csharp
|
|
.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**
|
|
|
|
```csharp
|
|
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**
|
|
|
|
```csharp
|
|
_ = 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**
|
|
|
|
```csharp
|
|
// 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**
|
|
|
|
```csharp
|
|
.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)
|
|
|
|
```csharp
|
|
.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
|
|
|
|
```csharp
|
|
.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
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
.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
|
|
|
|
```csharp
|
|
.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
|
|
|
|
```csharp
|
|
// 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
|
|
```json
|
|
{ "level": "info", "message": "Hello world", "source": "MyApp", "host": "srv-01" }
|
|
```
|
|
|
|
### POST /ingest - batch
|
|
```json
|
|
[
|
|
{ "level": "warn", "message": "Disk at 80%", "source": "monitor" },
|
|
{ "level": "error", "message": "DB timeout", "source": "api", "exception": "TimeoutException…" }
|
|
]
|
|
```
|
|
|
|
### GET /metrics response
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```csharp
|
|
MinimumLevel = ServerLogLevel.Warning // only Warning / Error / Critical are stored
|
|
```
|
|
|
|
Levels in order: `Trace → Debug → Information → Warning → Error → Critical`
|
|
|
|
## Rate limiting
|
|
|
|
```csharp
|
|
RateLimitPerSecond = 100 // per remote IP:port, 0 = disabled
|
|
```
|
|
|
|
Dropped messages are counted in `Metrics.TotalDropped` and raise the `LogDropped` event.
|
|
|
|
## Metrics
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
server.LogWritten += line => NotifyDashboard(line);
|
|
server.LogDropped += line => Metrics.Increment("dropped");
|
|
```
|
|
|
|
## Graceful shutdown
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
services.AddEonaCatLogging(
|
|
minimumLevel: LogLevel.Information,
|
|
timestampMode: TimestampMode.Local);
|
|
```
|
|
|
|
### 3. Registration with Pre-built EonaCatLogStack
|
|
|
|
If you've already created an `EonaCatLogStack` instance:
|
|
|
|
```csharp
|
|
var logStack = new EonaCatLogStack("MyApp");
|
|
logStack.AddFlow(new ConsoleFlow());
|
|
|
|
services.AddEonaCatLogging(logStack);
|
|
```
|
|
|
|
### 4. Registration with Configuration Callback (Recommended)
|
|
|
|
Configure the logger directly with an `Action<EonaCatLogStack>`:
|
|
|
|
```csharp
|
|
services.AddEonaCatLogging(logStack =>
|
|
{
|
|
logStack.AddFlow(new ConsoleFlow());
|
|
logStack.AddFlow(new FileFlow("./logs"));
|
|
logStack.AddBooster(new MachineNameBooster());
|
|
});
|
|
```
|
|
|
|
### 5. Registration with LogBuilder (Most Fluent - Recommended)
|
|
|
|
Use the fluent `LogBuilder` API for the most intuitive configuration:
|
|
|
|
```csharp
|
|
services.AddEonaCatLogging("MyApplication", builder =>
|
|
{
|
|
builder
|
|
.WithMinimumLevel(LogLevel.Information)
|
|
.WithTimestampMode(TimestampMode.Local)
|
|
.WriteToConsole()
|
|
.WriteToFile("./logs")
|
|
.BoostWithMachineName()
|
|
.BoostWithProcessId()
|
|
.BoostWithCorrelationId();
|
|
});
|
|
```
|
|
|
|
Or with the default "Application" category:
|
|
|
|
```csharp
|
|
services.AddEonaCatLogging(builder =>
|
|
{
|
|
builder
|
|
.WriteToConsole()
|
|
.WriteToFile("./logs");
|
|
});
|
|
```
|
|
|
|
### 6. Registration with Factory Method (Advanced)
|
|
|
|
For advanced scenarios where you need access to the service provider:
|
|
|
|
```csharp
|
|
services.AddEonaCatLoggingFactory("MyApplication", builder =>
|
|
{
|
|
builder
|
|
.WriteToConsole()
|
|
.WriteToFile("./logs");
|
|
});
|
|
```
|
|
|
|
## Using in Your Application
|
|
|
|
### Injecting ILoggerFactory
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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`:
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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)
|
|
|
|
```csharp
|
|
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)
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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:
|
|
```csharp
|
|
var logger = loggerFactory.CreateLogger("MyApp.Services.UserService");
|
|
```
|
|
|
|
6. **Enable Correlation IDs**: For distributed tracing scenarios:
|
|
```csharp
|
|
builder.BoostWithCorrelationId()
|
|
```
|
|
|
|
## Configuration Examples
|
|
|
|
### Minimal Setup (Console Only)
|
|
|
|
```csharp
|
|
services.AddEonaCatLogging(b => b.WriteToConsole());
|
|
```
|
|
|
|
### Development Environment
|
|
|
|
```csharp
|
|
services.AddEonaCatLogging(builder =>
|
|
{
|
|
builder
|
|
.WithMinimumLevel(LogLevel.Debug)
|
|
.WriteToConsole(useColors: true)
|
|
.WriteToFile("./logs")
|
|
.BoostWithMachineName()
|
|
.BoostWithThreadId();
|
|
});
|
|
```
|
|
|
|
### Production Environment
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
var builder = Host.CreateApplicationBuilder();
|
|
builder.Services.AddEonaCatLogging(b =>
|
|
{
|
|
b.WithMinimumLevel(LogLevel.Information)
|
|
.WriteToConsole()
|
|
.WriteToFile("./logs")
|
|
.BoostWithCorrelationId();
|
|
});
|
|
```
|
|
|
|
### With HostApplicationBuilder (Recommended)
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
[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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
var loggerFactory = app.Services.GetRequiredService<ILoggerFactory>();
|
|
await loggerFactory.DisposeAsync();
|
|
``` |