diff --git a/EonaCat.LogStack/EonaCat.LogStack.csproj b/EonaCat.LogStack/EonaCat.LogStack.csproj index 01b09f3..1ab53a9 100644 --- a/EonaCat.LogStack/EonaCat.LogStack.csproj +++ b/EonaCat.LogStack/EonaCat.LogStack.csproj @@ -1,103 +1,103 @@ - - - .netstandard2.1; net8.0; net4.8; - icon.ico - latest - EonaCat (Jeroen Saey) - true - EonaCat (Jeroen Saey) - icon.png - https://www.nuget.org/packages/EonaCat.LogStack/ - flow-based logging library for .NET, designed for zero-allocation logging paths and superior memory efficiency. -It features a rich fluent API for routing log events to dozens of destinations from console and file to Slack, Discord, Redis, Elasticsearch, and beyond. - Public release version - EonaCat (Jeroen Saey) - EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey - - 0.0.9 - README.md - True - LICENSE - - True - EonaCat.LogStack - git - - - - 0.0.9+{chash:10}.{c:ymd} - true - true - v[0-9]* - true - git - true - true - - - - 0.0.9 - EonaCat.LogStack - EonaCat.LogStack - https://git.saey.me/EonaCat/EonaCat.LogStack - 09d73ec1-ee68-42bb-b2bd-b0e31b635098 - - - - - - $(GeneratedVersion) - - - + + + .netstandard2.1; net8.0; net4.8; + icon.ico + latest + EonaCat (Jeroen Saey) + true + EonaCat (Jeroen Saey) + icon.png + https://www.nuget.org/packages/EonaCat.LogStack/ + flow-based logging library for .NET, designed for zero-allocation logging paths and superior memory efficiency. +It features a rich fluent API for routing log events to dozens of destinations from console and file to Slack, Discord, Redis, Elasticsearch, and beyond. + Public release version + EonaCat (Jeroen Saey) + EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey + + 0.1.0 + README.md + True + LICENSE + + True + EonaCat.LogStack + git + + + + 0.1.0+{chash:10}.{c:ymd} + true + true + v[0-9]* + true + git + true + true + + + + 0.1.0 + EonaCat.LogStack + EonaCat.LogStack + https://git.saey.me/EonaCat/EonaCat.LogStack + 09d73ec1-ee68-42bb-b2bd-b0e31b635098 + + + + + + $(GeneratedVersion) + + + - - - - - - True - \ - - - True - \ - - - True - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - True - \ - - - True - \ - - + + + + + + True + \ + + + True + \ + + + True + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + True + \ + + + True + \ + + - + \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLogger.cs b/EonaCat.LogStack/EonaCatLogger.cs index aa44544..20cbe60 100644 --- a/EonaCat.LogStack/EonaCatLogger.cs +++ b/EonaCat.LogStack/EonaCatLogger.cs @@ -30,7 +30,7 @@ namespace EonaCat.LogStack private readonly LogLevel _minimumLevel; private readonly TimestampMode _timestampMode; - private volatile bool _isDisposed; + private int _disposed; private long _totalLoggedCount; private long _totalDroppedCount; private long _totalExceptionsCount; @@ -73,7 +73,10 @@ namespace EonaCat.LogStack { _category = category ?? throw new ArgumentNullException(nameof(category)); _minimumLevel = minimumLevel; - _timestampMode = timestampMode; + _timestampMode = timestampMode; + + // Enable async pipeline by default + UseAsyncPipeline(); } /// @@ -83,11 +86,11 @@ namespace EonaCat.LogStack /// Bounded channel capacity (0 = unbounded). public EonaCatLogStack UseAsyncPipeline(int capacity = 65536) { - if (_asyncChannel != null) - { - return this; - } - + if (_asyncChannel != null) + { + return this; + } + _asyncCts = new CancellationTokenSource(); _asyncChannel = capacity > 0 ? Channel.CreateBounded(new BoundedChannelOptions(capacity) @@ -155,6 +158,12 @@ namespace EonaCat.LogStack public EonaCatLogStack RemoveFlow(string name) { lock (_flows) { _flows.RemoveAll(f => f.Name == name); } + lock (_concurrentFlows) + { + var keep = _concurrentFlows.Where(f => f.Name != name).ToArray(); + while (_concurrentFlows.TryTake(out _)) { } + foreach (var f in keep) _concurrentFlows.Add(f); + } return this; } @@ -182,10 +191,32 @@ namespace EonaCat.LogStack return this; } + private void RaiseOnLog(LogMessage message) + { + var handlers = OnLog; + + if (handlers == null) + { + return; + } + + foreach (EventHandler handler in handlers.GetInvocationList()) + { + try + { + handler(this, message); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(ex); + } + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Log(string message, LogLevel level = LogLevel.Information) { - if (_isDisposed || level < EffectiveMinLevel()) + if (Volatile.Read(ref _disposed) == 1 || level < EffectiveMinLevel()) { return; } @@ -207,11 +238,11 @@ namespace EonaCat.LogStack [MethodImpl(MethodImplOptions.AggressiveInlining)] public void LogTemplate(LogLevel level, string template, params object?[] args) { - if (_isDisposed || level < EffectiveMinLevel()) - { - return; - } - + if (Volatile.Read(ref _disposed) == 1 || level < EffectiveMinLevel()) + { + return; + } + var parsed = MessageTemplate.FromCache(template); var builder = new LogEventBuilder() .WithLevel(level) @@ -230,11 +261,11 @@ namespace EonaCat.LogStack [MethodImpl(MethodImplOptions.AggressiveInlining)] public void LogTemplate(LogLevel level, Exception exception, string template, params object?[] args) { - if (_isDisposed || level < EffectiveMinLevel()) - { - return; - } - + if (Volatile.Read(ref _disposed) == 1 || level < EffectiveMinLevel()) + { + return; + } + var parsed = MessageTemplate.FromCache(template); var builder = new LogEventBuilder() .WithLevel(level) @@ -251,17 +282,17 @@ namespace EonaCat.LogStack [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Log(LogLevel level, Exception exception, string message) { - if (_isDisposed || level < EffectiveMinLevel()) + if (Volatile.Read(ref _disposed) == 1 || level < EffectiveMinLevel()) { return; } TrackLevel(level); - if (exception != null) - { - Interlocked.Increment(ref _totalExceptionsCount); - } - + if (exception != null) + { + Interlocked.Increment(ref _totalExceptionsCount); + } + var builder = new LogEventBuilder() .WithLevel(level) .WithCategory(_category) @@ -269,15 +300,15 @@ namespace EonaCat.LogStack .WithException(exception) .WithTimestamp(GetTimestamp()); - ProcessLogEvent(ref builder); - - OnLog?.Invoke(this, new LogMessage - { - Level = level, - Exception = exception, - Message = message, - Category = _category, - Origin = null + ProcessLogEvent(ref builder); + + RaiseOnLog(new LogMessage + { + Level = level, + Exception = exception, + Message = message, + Category = _category, + Origin = null }); } @@ -310,7 +341,7 @@ namespace EonaCat.LogStack [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Log(LogLevel level, string message, params (string Key, object Value)[] properties) { - if (_isDisposed || level < EffectiveMinLevel()) + if (Volatile.Read(ref _disposed) == 1 || level < EffectiveMinLevel()) { return; } @@ -396,7 +427,10 @@ namespace EonaCat.LogStack } // Apply modifiers - foreach (var mod in _modifiers) + ActionRef[] modifierSnapshot; + lock (_modifiersLock) { modifierSnapshot = _modifiers.ToArray(); } + + foreach (var mod in modifierSnapshot) { try { @@ -406,36 +440,11 @@ namespace EonaCat.LogStack } var logEvent = builder.Build(); - Interlocked.Increment(ref _totalLoggedCount); - - // Async channel pipeline - if (_asyncChannel != null) - { - if (!_asyncChannel.Writer.TryWrite(logEvent)) - { - Interlocked.Increment(ref _totalDroppedCount); - } + Interlocked.Increment(ref _totalLoggedCount); - return; - } - - // Synchronous blast to flows - DispatchToFlows(logEvent); - } - - private void DispatchToFlows(LogEvent logEvent) - { - foreach (var flow in _concurrentFlows) - { - try - { - var result = flow.BlastAsync(logEvent).GetAwaiter().GetResult(); - if (result == WriteResult.Dropped) - { - Interlocked.Increment(ref _totalDroppedCount); - } - } - catch { } + if (!_asyncChannel!.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref _totalDroppedCount); } } @@ -452,10 +461,10 @@ namespace EonaCat.LogStack try { var result = await flow.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false); - if (result == WriteResult.Dropped) - { - Interlocked.Increment(ref _totalDroppedCount); - } + if (result == WriteResult.Dropped) + { + Interlocked.Increment(ref _totalDroppedCount); + } } catch { } } @@ -488,15 +497,28 @@ namespace EonaCat.LogStack .Where(d => d != null) .ToList(); + int flowCount; + int boosterCount; + + lock (_flows) + { + flowCount = _flows.Count; + } + + lock (_boosters) + { + boosterCount = _boosters.Count; + } + return new LoggerDiagnostics { Category = _category, MinimumLevel = _minimumLevel, TotalLogged = Interlocked.Read(ref _totalLoggedCount), TotalDropped = Interlocked.Read(ref _totalDroppedCount), - TotalExceptions = Interlocked.Read(ref _totalExceptionsCount), - FlowCount = _flows.Count, - BoosterCount = _boosters.Count, + TotalExceptions = Interlocked.Read(ref _totalExceptionsCount), + FlowCount = flowCount, + BoosterCount = boosterCount, Flows = flowDiagnostics }; } @@ -535,19 +557,27 @@ namespace EonaCat.LogStack public async ValueTask DisposeAsync() { - if (_isDisposed) + if (Interlocked.Exchange(ref _disposed, 1) == 1) { return; } - _isDisposed = true; - // Drain and stop the async channel pipeline if active - if (_asyncChannel != null) - { - _asyncChannel.Writer.TryComplete(); - _asyncCts?.Cancel(); - try { if (_asyncConsumer != null) { await _asyncConsumer.ConfigureAwait(false); } } catch { } + if (_asyncChannel != null) + { + _asyncChannel.Writer.TryComplete(); + + if (_asyncConsumer != null) + { + try + { + await _asyncConsumer.ConfigureAwait(false); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(ex); + } + } } await FlushAsync().ConfigureAwait(false); diff --git a/EonaCat.LogStack/LogBuilder.cs b/EonaCat.LogStack/LogBuilder.cs index b07d8f1..e62a3c0 100644 --- a/EonaCat.LogStack/LogBuilder.cs +++ b/EonaCat.LogStack/LogBuilder.cs @@ -1,999 +1,999 @@ -using EonaCat.LogStack.Boosters; -using EonaCat.LogStack.Core; -using EonaCat.LogStack.EonaCatLogStackCore; -using EonaCat.LogStack.EonaCatLogStackCore.Policies; -using EonaCat.LogStack.Flows; -using ServiceMonitoring; -using System; -using System.Collections.Generic; -using System.Data.Common; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; - -namespace EonaCat.LogStack.Configuration; - -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -/// -/// Fluent builder for configuring the logger with flows and boosters -/// -public sealed class LogBuilder -{ - private readonly string _category; - private LogLevel _minimumLevel = LogLevel.Trace; - private TimestampMode _timestampMode = TimestampMode.Utc; - private readonly List _flows = new(); - private readonly List _boosters = new(); - private DynamicLevelController? _dynamicLevel; - private bool _useAsyncPipeline; - private int _asyncPipelineCapacity = 65536; - - public event EventHandler OnLog; - - public LogBuilder(string category = "Application") - { - _category = category ?? throw new ArgumentNullException(nameof(category)); - } - - /// - /// Sets the minimum log level - /// - public LogBuilder WithMinimumLevel(LogLevel level) - { - _minimumLevel = level; - return this; - } - - /// - /// Sets the timestamp mode - /// - public LogBuilder WithTimestampMode(TimestampMode mode) - { - _timestampMode = mode; - return this; - } - - /// - /// Adds console output - /// - public LogBuilder WriteToConsole( - LogLevel minimumLevel = LogLevel.Trace, - bool useColors = true) - { - _flows.Add(new ConsoleFlow(minimumLevel, useColors, _timestampMode)); - return this; - } - - /// - /// Adds diagnostics - /// - public LogBuilder WriteDiagnostics( - TimeSpan snapshotInterval = default(TimeSpan), - bool injectIntoEvents = false, - bool writeSnapshotEvents = true, - string snapshotCategory = "Diagnostics", - IFlow forwardTo = null, - LogLevel minimumLevel = LogLevel.Trace, - Func> customMetrics = null) - { - _flows.Add(new DiagnosticsFlow( - snapshotInterval, - injectIntoEvents, - writeSnapshotEvents, - snapshotCategory, - forwardTo, - minimumLevel, - customMetrics)); - return this; - } - - /// - /// Adds file output - /// - public LogBuilder WriteToFile( - string directory, - string filePrefix = "log", - long maxFileSize = 100 * 1024 * 1024, - long maxDirectorySize = 2L * 1024 * 1024 * 1024, - FileRetentionPolicy fileRetentionPolicy = null, - int flushIntervalInMilliSeconds = 2000, - bool useCategoryRouting = false, - int batchSize = 1, - LogLevel[]? logLevelsForSeparateFiles = null, - LogLevel minimumLevel = LogLevel.Trace, - BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait, - FileOutputFormat outputFormat = FileOutputFormat.Text, - CompressionFormat compression = CompressionFormat.GZip, - string template = "[{ts}] [Host: {host}] [Category: {category}] [Thread: {thread}] [{logtype}] {message}{props}") - { - _flows.Add(new FileFlow( - directory, - filePrefix, - maxFileSize, - maxDirectorySize, - fileRetentionPolicy, - flushIntervalInMilliSeconds, - batchSize, - minimumLevel, - useCategoryRouting, - logLevelsForSeparateFiles, - _timestampMode, - backpressureStrategy, - outputFormat, - compression, - template)); - return this; - } - - public LogBuilder WriteToEncryptedFile( - string directory, - string filePrefix = "log", - string password = "EonaCat", - long maxFileSize = 100 * 1024 * 1024, - FileRetentionPolicy fileRetentionPolicy = null, - int flushIntervalInMilliSeconds = 2000, - bool useCategoryRouting = false, - LogLevel[]? logLevelsForSeparateFiles = null, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new EncryptedFileFlow( - directory, - password, - filePrefix, - maxFileSize, - maxDirectorySize: 2L * 1024 * 1024 * 1024, - retention: fileRetentionPolicy, - flushIntervalMs: flushIntervalInMilliSeconds, - batchSize: 1, - minimumLevel: minimumLevel, - useCategoryRouting: useCategoryRouting, - logLevelsForSeparateFiles: logLevelsForSeparateFiles, - tsMode: _timestampMode)); - return this; - } - - /// - /// Write to a rolling buffer - /// - /// Maximum number of events to retain. - /// Minimum level to store in the buffer. - /// - /// When a log event reaches this level or above, the current buffer - /// contents are immediately forwarded to . - /// Set to LogLevel.None (or omit) to disable. - /// - /// - /// Flow to forward the buffered context to when the trigger fires. - /// Can be null even when is set. - /// - /// - /// How many buffered lines to forward before the triggering event. - /// Defaults to entire buffer (int.MaxValue). - /// - /// - public LogBuilder WriteToRollingBuffer( - int capacity = 500, - LogLevel minimumLevel = LogLevel.Trace, - LogLevel triggerLevel = LogLevel.Error, - IFlow triggerTarget = null, - int preContextLines = int.MaxValue) - { - _flows.Add(new RollingBufferFlow( - capacity, - minimumLevel, - triggerLevel, - triggerTarget, - preContextLines)); - return this; - } - - /// - /// Decrypt a file which is encrypted by EonaCat Logger - /// - /// encrypted file source path - /// destination path for decrypted file - /// password used by encryption - /// - public static bool DecryptFile(string encryptedPath, string outputPath, string password) - { - return EncryptedFileFlow.DecryptToFile(encryptedPath, outputPath, password); - } - - /// - /// Adds Database output - /// - public LogBuilder WriteToDatabase( - Func connectionFactory, - string tableName = "logs", - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new DatabaseFlow( - connectionFactory, - tableName, - batchSize, - minimumLevel)); - return this; - } - - /// - /// Adds Snmp traps - /// - public LogBuilder WriteToSnmpTrap(string host, int port = 162, string oid = "1.3.6.1.4.1.9999", LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new SnmpTrapFlow( - host, - port, - oid, - minimumLevel)); - return this; - } - - /// - /// Adds Discord - /// - public LogBuilder WriteToDiscord( - string webHookUrl, - string botName = "EonaCatBot", - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new DiscordFlow( - webHookUrl, - botName, - batchSize, - minimumLevel)); - return this; - } - - /// - /// Adds ElasticSearch - /// - public LogBuilder WriteToElasticSearch( - string elasticSearchUrl, - string indexName = "EonaCatIndex", - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new ElasticSearchFlow( - elasticSearchUrl, - indexName, - batchSize, - minimumLevel)); - return this; - } - - /// - /// Adds Telegram - /// - public LogBuilder WriteToTelegram(string botToken, string chatId = "EonaCat", int batchSize = 1, LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new TelegramFlow( - botToken, - chatId, - batchSize, - minimumLevel)); - return this; - } - - /// - /// Adds a tamper-evident audit trail. - /// - /// Each audit entry is hash-chained: every line stores a SHA-256 of the previous - /// line's hash + the current entry body, so deletion or modification of any past - /// entry invalidates all subsequent hashes. - /// - /// Use to verify file integrity at any time. - /// - /// Directory where the .audit file is written. - /// File name prefix (default: "audit"). - /// - /// Which severity levels are recorded in the audit trail: - /// - /// – every log event (default) - /// – Warning, Error, Critical - /// – Error and Critical only - /// – Critical only - /// - /// - /// Minimum for audit capture. - /// Whether structured properties are appended to each entry. - public LogBuilder WriteToAudit( - string directory, - string filePrefix = "audit", - AuditLevel auditLevel = AuditLevel.All, - LogLevel minimumLevel = LogLevel.Trace, - bool includeProperties = true) - { - _flows.Add(new AuditFlow( - directory, - filePrefix, - auditLevel, - minimumLevel, - includeProperties)); - return this; - } - - /// - /// Adds Slack - /// - public LogBuilder WriteToSlack(string webhookUrl, int batchSize = 1, LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new SlackFlow( - webhookUrl, - batchSize, - minimumLevel)); - return this; - } - - /// - /// Adds Slack - /// - public LogBuilder WriteToMicrosoftTeams(string webhookUrl, int batchSize = 1, LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new MicrosoftTeamsFlow( - webhookUrl, - batchSize, - minimumLevel)); - return this; - } - - /// - /// Adds a TCP flow. - /// - public LogBuilder WriteToTcp( - string host, - int port, - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace, - BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait, - bool useTls = false, - RemoteCertificateValidationCallback certValidationCallback = null, - X509CertificateCollection clientCertificates = null) - { - if (string.IsNullOrWhiteSpace(host)) - { - throw new ArgumentException("Host cannot be null or empty.", nameof(host)); - } - - if (port <= 0 || port > 65535) - { - throw new ArgumentOutOfRangeException(nameof(port), "Port must be between 1 and 65535."); - } - - _flows.Add(new TcpFlow( - host, - port, - batchSize, - minimumLevel, - backpressureStrategy, - useTls, - certValidationCallback, - clientCertificates)); - - return this; - } - - - - /// - /// Adds a Retry flow to retry failed log writes. - /// - public LogBuilder WriteToRetry( - IFlow primaryFlow, - int maxRetries = 3, - TimeSpan? initialDelay = null, - bool exponentialBackoff = true) - { - if (primaryFlow == null) - { - throw new ArgumentNullException(nameof(primaryFlow)); - } - - _flows.Add(new RetryFlow( - primaryFlow, - maxRetries, - initialDelay ?? TimeSpan.FromMilliseconds(200), - exponentialBackoff)); - - return this; - } - - /// - /// Adds an EventLog flow for sending logs to a remote destination. - /// - public LogBuilder WriteToEventLogFlow( - string destination, - int port = 514, - LogLevel minimumLevel = LogLevel.Trace, - int bufferSize = 100, - TimeSpan? flushInterval = null, - bool useTls = false, - RemoteCertificateValidationCallback? certificateValidationCallback = null, - X509CertificateCollection? clientCertificates = null) - { - if (string.IsNullOrWhiteSpace(destination)) - { - throw new ArgumentException("Destination cannot be null or empty.", nameof(destination)); - } - - _flows.Add(new EventLogFlow( - destination, - port, - minimumLevel, - bufferSize, - flushInterval ?? TimeSpan.FromSeconds(5), - useTls, - certificateValidationCallback, - clientCertificates)); - return this; - } - - /// - /// Adds a Failover flow that switches to a secondary flow if the primary fails. - /// - public LogBuilder WriteToFailover(IFlow primaryFlow, IFlow secondaryFlow, TimeSpan? recoveryCheckInterval = null, int failureThreshold = 5) - { - if (primaryFlow == null) - { - throw new ArgumentNullException(nameof(primaryFlow)); - } - - if (secondaryFlow == null) - { - throw new ArgumentNullException(nameof(secondaryFlow)); - } - - _flows.Add(new FailoverFlow(primaryFlow, secondaryFlow, recoveryCheckInterval, failureThreshold)); - - return this; - } - - /// - /// Adds a Syslog TCP flow. - /// - public LogBuilder WriteToSyslogTcp( - string host, - int port = 514, - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace, - BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait, - bool useTls = false, - RemoteCertificateValidationCallback certValidationCallback = null, - X509CertificateCollection clientCertificates = null) - { - if (string.IsNullOrWhiteSpace(host)) - { - throw new ArgumentException("Host cannot be null or empty.", nameof(host)); - } - - if (port <= 0 || port > 65535) - { - throw new ArgumentOutOfRangeException(nameof(port), "Port must be between 1 and 65535."); - } - - _flows.Add(new SyslogTcpFlow( - host, - port, - batchSize, - minimumLevel, - backpressureStrategy, - useTls, - certValidationCallback, - clientCertificates)); - - return this; - } - - /// - /// Adds a Status Monitoring flow. - /// - public LogBuilder WriteToStatusFlow( - List servicesToMonitor, - TimeSpan? checkInterval = null, - string statusDirectory = null, - Action statusChangedTrigger = null) - { - _flows.Add(new StatusFlow( - servicesToMonitor, - checkInterval, - statusDirectory, - statusChangedTrigger)); - - return this; - } - - /// - /// Adds Syslog Tcp - /// - public LogBuilder WriteToSyslogUdp( - string host, - int port, - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace, - BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) - { - _flows.Add(new SyslogUdpFlow( - host, - port, - batchSize, - minimumLevel, - backpressureStrategy)); - return this; - } - - /// - /// Adds Zabbix - /// - public LogBuilder WriteToZabbixFlow( - string host, - int port = 10051, - string zabbixHostname = null, - string zabbixKey = "log_event", - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace, - BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) - { - _flows.Add(new ZabbixFlow( - host, - port, - zabbixHostname, - zabbixKey, - batchSize, - minimumLevel, - backpressureStrategy)); - return this; - } - - /// - /// Adds Graylog - /// - public LogBuilder WriteToGraylogFlow( - string host, - int port = 12201, - bool useTcp = false, - string graylogHostName = null, - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace, - BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) - { - _flows.Add(new GraylogFlow( - host, - port, - useTcp, - graylogHostName, - batchSize, - minimumLevel, - backpressureStrategy)); - return this; - } - - /// - /// Publishes log events to a Redis channel using the PUBLISH command (Pub/Sub) - /// and optionally appends them to a Redis List (LPUSH) for persistence. - /// - /// Uses raw TCP + RESP protocol, so there arent additional dependencies - /// - /// Features: - /// - Reconnect with exponential back-off on connection failure - /// - Optional LPUSH to a list key with LTRIM to cap list length - /// - Optional password authentication (AUTH command) - /// - Optional DB selection (SELECT command) - /// - Background writer thread (non-blocking callers) - /// - public LogBuilder RedisFlow(string host = "localhost", - int port = 6379, - string password = null, - int database = 0, - string channel = "eonacat:logs", - string listKey = null, - int maxListLength = 10000, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new RedisFlow( - host, - port, - password, - database, - channel, - listKey, - maxListLength, - minimumLevel)); - return this; - } - - /// - /// Wraps any flow with token-bucket rate limiting and optional message deduplication. - /// Ideal for protecting high-latency sinks (email, Slack, HTTP) from log storms. - /// - /// The downstream flow to protect. - /// - /// Max events that can be emitted in a burst per level (token bucket capacity). - /// - /// - /// How many tokens are added per second per level. E.g. 5.0 = 5 events/second steady state. - /// - /// - /// If true, identical messages within are collapsed. - /// The suppressed count is appended to the message when the window expires. - /// - /// Deduplication window (default 60 s). - /// Maximum number of distinct messages tracked (default 1000). - /// Minimum level this flow processes. - public LogBuilder WriteToThrottled(IFlow inner, - int burstCapacity = 10, - double refillPerSecond = 1.0, - bool deduplicate = false, - TimeSpan dedupWindow = default(TimeSpan), - int dedupMaxKeys = 1000, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new ThrottledFlow( - inner, - burstCapacity, - refillPerSecond, - deduplicate, - dedupWindow, - dedupMaxKeys, - minimumLevel)); - return this; - } - - /// - /// Adds Splunk - /// - public LogBuilder WriteToSplunkFlow( - string splunkUrl, - string token, - string sourceType = "splunk_logs", - string hostName = null, - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace, - BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) - { - _flows.Add(new SplunkFlow( - splunkUrl, - token, - sourceType, - hostName, - batchSize, - minimumLevel, - backpressureStrategy)); - return this; - } - - /// - /// Pushes log events to a SignalR hub via HTTP POST to the hub's /send endpoint. - /// Works with ASP.NET SignalR (classic) and ASP.NET Core SignalR server-side REST API. - /// - /// A lightweight alternative to the SignalR client library - /// - /// On the server side you need a minimal hub endpoint that accepts POST: - /// POST {hubUrl}/send body: { "target": "...", "arguments": [ { log json } ] } - /// - /// For live dashboards: the hub broadcasts to a "logs" group; clients subscribe and - /// render events in real time. - /// - public LogBuilder WriteToSignalR( - string hubUrl, - string hubMethod = "ReceiveLog", - HttpClient httpClient = null, - int batchSize = 20, - int batchIntervalMs = 500, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new SignalRFlow( - hubUrl, - hubMethod, - httpClient, - batchSize, - batchIntervalMs, - minimumLevel)); - return this; - } - - /// - /// Sends log events as HTML email digests via SMTP. - /// Batches events for before sending, - /// unless flushOnCritical is true (Critical events bypass batching). - /// - public LogBuilder WriteToEmail( - string smtpHost, - int smtpPort = 587, - bool useSsl = true, - string username = null, - string password = null, - string from = null, - string to = null, - string subjectPrefix = "[EonaCatLogStack]", - int digestMinutes = 5, - bool flushOnCritical = true, - int maxEventsPerDigest = 100, - string headerName = null, - LogLevel minimumLevel = LogLevel.Error) - { - _flows.Add(new EmailFlow( - smtpHost, - smtpPort, - useSsl, - username, - password, - from, - to, - subjectPrefix, - digestMinutes, - flushOnCritical, - maxEventsPerDigest, - headerName, - minimumLevel)); - return this; - } - - /// - /// Adds Udp - /// - public LogBuilder WriteToUdp( - string host, - int port, - int flushIntervalInMilliseconds = 1000, - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace, - BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) - { - _flows.Add(new UdpFlow( - host, - port, - flushIntervalInMilliseconds, - batchSize, - minimumLevel, - backpressureStrategy)); - return this; - } - - /// - /// Adds in-memory buffer output - /// - public LogBuilder WriteToMemory( - int capacity = 10000, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new MemoryFlow(capacity, minimumLevel)); - return this; - } - - /// - /// Adds HTTP endpoint output - /// - public LogBuilder WriteToHttp( - string endpoint, - HttpClient? httpClient = null, - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace, - TimeSpan? batchInterval = null, - Dictionary? headers = null) - { - _flows.Add(new HttpFlow( - endpoint, - httpClient, - batchSize, - minimumLevel, - batchInterval, - headers)); - return this; - } - - /// - /// Adds a custom flow - /// - public LogBuilder WriteTo(IFlow flow) - { - _flows.Add(flow ?? throw new ArgumentNullException(nameof(flow))); - return this; - } - - /// - /// Boost logs with machine name - /// - public LogBuilder BoostWithMachineName() - { - _boosters.Add(new MachineNameBooster()); - return this; - } - - /// - /// Boost logs with process ID - /// - public LogBuilder BoostWithProcessId() - { - _boosters.Add(new ProcessIdBooster()); - return this; - } - - /// - /// Boost logs with the current date (yyyy-MM-dd) - /// - public LogBuilder BoostWithDate() - { - _boosters.Add(new DateBooster()); - return this; - } - - /// - /// Boost logs with the current time (HH:mm:ss.fff) - /// - public LogBuilder BoostWithTime() - { - _boosters.Add(new TimeBooster()); - return this; - } - - /// - /// Boost logs with the current timestamp ticks - /// - public LogBuilder BoostWithTicks() - { - _boosters.Add(new TicksBooster()); - return this; - } - - /// - /// Boost logs with the process start time - /// - public LogBuilder BoostWithProcStart() - { - _boosters.Add(new ProcStartBooster()); - return this; - } - - /// - /// Boost logs with the uptime of the process in seconds - /// - public LogBuilder BoostWithUptime() - { - _boosters.Add(new UptimeBooster()); - return this; - } - - /// - /// Boost logs with the current thread name - /// - public LogBuilder BoostWithThreadName() - { - _boosters.Add(new ThreadNameBooster()); - return this; - } - - /// - /// Boost logs with memory usage in MB - /// - public LogBuilder BoostWithMemory() - { - _boosters.Add(new MemoryBooster()); - return this; - } - - /// - /// Boost logs with the operating system description - /// - public LogBuilder BoostWithOS() - { - _boosters.Add(new OSBooster()); - return this; - } - - /// - /// Boost logs with the runtime/framework description - /// - public LogBuilder BoostWithFramework() - { - _boosters.Add(new FrameworkBooster()); - return this; - } - - /// - /// Boost logs with the application name and base directory - /// - public LogBuilder BoostWithApp() - { - _boosters.Add(new AppBooster()); - return this; - } - - /// - /// Boost logs with the current user name - /// - public LogBuilder BoostWithUser() - { - _boosters.Add(new UserBooster()); - return this; - } - - /// - /// Boost logs with the current thread ID - /// - public LogBuilder BoostWithThreadId() - { - _boosters.Add(new ThreadIdBooster()); - return this; - } - - - /// - /// Boost logs with custom text - /// - public LogBuilder BoostWithCustomText(string key, string value) - { - _boosters.Add(new CustomTextBooster(key, value)); - return this; - } - - /// - /// Boost logs with environment name - /// - public LogBuilder BoostWithEnvironment(string environmentName) - { - _boosters.Add(new EnvironmentBooster(environmentName)); - return this; - } - - /// - /// Boost logs with application name and version - /// - public LogBuilder BoostWithApplication(string applicationName, string? version = null) - { - _boosters.Add(new ApplicationBooster(applicationName, version)); - return this; - } - - /// - /// Boost logs with correlation ID from Activity - /// - public LogBuilder BoostWithCorrelationId() - { - _boosters.Add(new CorrelationIdBooster()); - return this; - } - - /// - /// Adds a custom booster - /// - public LogBuilder Boost(IBooster booster) - { - _boosters.Add(booster ?? throw new ArgumentNullException(nameof(booster))); - return this; - } - - /// - /// Adds a callback-based booster - /// - public LogBuilder Boost(string name, Func> callback) - { - _boosters.Add(new CallbackBooster(name, callback)); - return this; - } - - /// - /// Builds the configured logger - /// - public EonaCatLogStack Build() - { - var logger = new EonaCatLogStack(_category, _minimumLevel, _timestampMode); - logger.OnLog += (sender, message) => OnLog?.Invoke(sender, message); - - foreach (var flow in _flows) - { - logger.AddFlow(flow); - } - - foreach (var booster in _boosters) - { - logger.AddBooster(booster); - } - +using EonaCat.LogStack.Boosters; +using EonaCat.LogStack.Core; +using EonaCat.LogStack.EonaCatLogStackCore; +using EonaCat.LogStack.EonaCatLogStackCore.Policies; +using EonaCat.LogStack.Flows; +using ServiceMonitoring; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace EonaCat.LogStack.Configuration; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Fluent builder for configuring the logger with flows and boosters +/// +public sealed class LogBuilder +{ + private readonly string _category; + private LogLevel _minimumLevel = LogLevel.Trace; + private TimestampMode _timestampMode = TimestampMode.Utc; + private readonly List _flows = new(); + private readonly List _boosters = new(); + private DynamicLevelController? _dynamicLevel; + private bool _useAsyncPipeline; + private int _asyncPipelineCapacity = 65536; + + public event EventHandler OnLog; + + public LogBuilder(string category = "Application") + { + _category = category ?? throw new ArgumentNullException(nameof(category)); + } + + /// + /// Sets the minimum log level + /// + public LogBuilder WithMinimumLevel(LogLevel level) + { + _minimumLevel = level; + return this; + } + + /// + /// Sets the timestamp mode + /// + public LogBuilder WithTimestampMode(TimestampMode mode) + { + _timestampMode = mode; + return this; + } + + /// + /// Adds console output + /// + public LogBuilder WriteToConsole( + LogLevel minimumLevel = LogLevel.Trace, + bool useColors = true) + { + _flows.Add(new ConsoleFlow(minimumLevel, useColors, _timestampMode)); + return this; + } + + /// + /// Adds diagnostics + /// + public LogBuilder WriteDiagnostics( + TimeSpan snapshotInterval = default(TimeSpan), + bool injectIntoEvents = false, + bool writeSnapshotEvents = true, + string snapshotCategory = "Diagnostics", + IFlow forwardTo = null, + LogLevel minimumLevel = LogLevel.Trace, + Func> customMetrics = null) + { + _flows.Add(new DiagnosticsFlow( + snapshotInterval, + injectIntoEvents, + writeSnapshotEvents, + snapshotCategory, + forwardTo, + minimumLevel, + customMetrics)); + return this; + } + + /// + /// Adds file output + /// + public LogBuilder WriteToFile( + string directory, + string filePrefix = "log", + long maxFileSize = 100 * 1024 * 1024, + long maxDirectorySize = 2L * 1024 * 1024 * 1024, + FileRetentionPolicy fileRetentionPolicy = null, + int flushIntervalInMilliSeconds = 2000, + bool useCategoryRouting = false, + int batchSize = 1, + LogLevel[]? logLevelsForSeparateFiles = null, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait, + FileOutputFormat outputFormat = FileOutputFormat.Text, + CompressionFormat compression = CompressionFormat.GZip, + string template = "[{ts}] [Host: {host}] [Category: {category}] [Thread: {thread}] [{logtype}] {message}{props}") + { + _flows.Add(new FileFlow( + directory, + filePrefix, + maxFileSize, + maxDirectorySize, + fileRetentionPolicy, + flushIntervalInMilliSeconds, + batchSize, + minimumLevel, + useCategoryRouting, + logLevelsForSeparateFiles, + _timestampMode, + backpressureStrategy, + outputFormat, + compression, + template)); + return this; + } + + public LogBuilder WriteToEncryptedFile( + string directory, + string filePrefix = "log", + string password = "EonaCat", + long maxFileSize = 100 * 1024 * 1024, + FileRetentionPolicy fileRetentionPolicy = null, + int flushIntervalInMilliSeconds = 2000, + bool useCategoryRouting = false, + LogLevel[]? logLevelsForSeparateFiles = null, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new EncryptedFileFlow( + directory, + password, + filePrefix, + maxFileSize, + maxDirectorySize: 2L * 1024 * 1024 * 1024, + retention: fileRetentionPolicy, + flushIntervalMs: flushIntervalInMilliSeconds, + batchSize: 1, + minimumLevel: minimumLevel, + useCategoryRouting: useCategoryRouting, + logLevelsForSeparateFiles: logLevelsForSeparateFiles, + tsMode: _timestampMode)); + return this; + } + + /// + /// Write to a rolling buffer + /// + /// Maximum number of events to retain. + /// Minimum level to store in the buffer. + /// + /// When a log event reaches this level or above, the current buffer + /// contents are immediately forwarded to . + /// Set to LogLevel.None (or omit) to disable. + /// + /// + /// Flow to forward the buffered context to when the trigger fires. + /// Can be null even when is set. + /// + /// + /// How many buffered lines to forward before the triggering event. + /// Defaults to entire buffer (int.MaxValue). + /// + /// + public LogBuilder WriteToRollingBuffer( + int capacity = 500, + LogLevel minimumLevel = LogLevel.Trace, + LogLevel triggerLevel = LogLevel.Error, + IFlow triggerTarget = null, + int preContextLines = int.MaxValue) + { + _flows.Add(new RollingBufferFlow( + capacity, + minimumLevel, + triggerLevel, + triggerTarget, + preContextLines)); + return this; + } + + /// + /// Decrypt a file which is encrypted by EonaCat Logger + /// + /// encrypted file source path + /// destination path for decrypted file + /// password used by encryption + /// + public static bool DecryptFile(string encryptedPath, string outputPath, string password) + { + return EncryptedFileFlow.DecryptToFile(encryptedPath, outputPath, password); + } + + /// + /// Adds Database output + /// + public LogBuilder WriteToDatabase( + Func connectionFactory, + string tableName = "logs", + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new DatabaseFlow( + connectionFactory, + tableName, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds Snmp traps + /// + public LogBuilder WriteToSnmpTrap(string host, int port = 162, string oid = "1.3.6.1.4.1.9999", LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new SnmpTrapFlow( + host, + port, + oid, + minimumLevel)); + return this; + } + + /// + /// Adds Discord + /// + public LogBuilder WriteToDiscord( + string webHookUrl, + string botName = "EonaCatBot", + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new DiscordFlow( + webHookUrl, + botName, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds ElasticSearch + /// + public LogBuilder WriteToElasticSearch( + string elasticSearchUrl, + string indexName = "EonaCatIndex", + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new ElasticSearchFlow( + elasticSearchUrl, + indexName, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds Telegram + /// + public LogBuilder WriteToTelegram(string botToken, string chatId = "EonaCat", int batchSize = 1, LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new TelegramFlow( + botToken, + chatId, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds a tamper-evident audit trail. + /// + /// Each audit entry is hash-chained: every line stores a SHA-256 of the previous + /// line's hash + the current entry body, so deletion or modification of any past + /// entry invalidates all subsequent hashes. + /// + /// Use to verify file integrity at any time. + /// + /// Directory where the .audit file is written. + /// File name prefix (default: "audit"). + /// + /// Which severity levels are recorded in the audit trail: + /// + /// – every log event (default) + /// – Warning, Error, Critical + /// – Error and Critical only + /// – Critical only + /// + /// + /// Minimum for audit capture. + /// Whether structured properties are appended to each entry. + public LogBuilder WriteToAudit( + string directory, + string filePrefix = "audit", + AuditLevel auditLevel = AuditLevel.All, + LogLevel minimumLevel = LogLevel.Trace, + bool includeProperties = true) + { + _flows.Add(new AuditFlow( + directory, + filePrefix, + auditLevel, + minimumLevel, + includeProperties)); + return this; + } + + /// + /// Adds Slack + /// + public LogBuilder WriteToSlack(string webhookUrl, int batchSize = 1, LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new SlackFlow( + webhookUrl, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds Slack + /// + public LogBuilder WriteToMicrosoftTeams(string webhookUrl, int batchSize = 1, LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new MicrosoftTeamsFlow( + webhookUrl, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds a TCP flow. + /// + public LogBuilder WriteToTcp( + string host, + int port, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait, + bool useTls = false, + RemoteCertificateValidationCallback certValidationCallback = null, + X509CertificateCollection clientCertificates = null) + { + if (string.IsNullOrWhiteSpace(host)) + { + throw new ArgumentException("Host cannot be null or empty.", nameof(host)); + } + + if (port <= 0 || port > 65535) + { + throw new ArgumentOutOfRangeException(nameof(port), "Port must be between 1 and 65535."); + } + + _flows.Add(new TcpFlow( + host, + port, + batchSize, + minimumLevel, + backpressureStrategy, + useTls, + certValidationCallback, + clientCertificates)); + + return this; + } + + + + /// + /// Adds a Retry flow to retry failed log writes. + /// + public LogBuilder WriteToRetry( + IFlow primaryFlow, + int maxRetries = 3, + TimeSpan? initialDelay = null, + bool exponentialBackoff = true) + { + if (primaryFlow == null) + { + throw new ArgumentNullException(nameof(primaryFlow)); + } + + _flows.Add(new RetryFlow( + primaryFlow, + maxRetries, + initialDelay ?? TimeSpan.FromMilliseconds(200), + exponentialBackoff)); + + return this; + } + + /// + /// Adds an EventLog flow for sending logs to a remote destination. + /// + public LogBuilder WriteToEventLogFlow( + string destination, + int port = 514, + LogLevel minimumLevel = LogLevel.Trace, + int bufferSize = 100, + TimeSpan? flushInterval = null, + bool useTls = false, + RemoteCertificateValidationCallback? certificateValidationCallback = null, + X509CertificateCollection? clientCertificates = null) + { + if (string.IsNullOrWhiteSpace(destination)) + { + throw new ArgumentException("Destination cannot be null or empty.", nameof(destination)); + } + + _flows.Add(new EventLogFlow( + destination, + port, + minimumLevel, + bufferSize, + flushInterval ?? TimeSpan.FromSeconds(5), + useTls, + certificateValidationCallback, + clientCertificates)); + return this; + } + + /// + /// Adds a Failover flow that switches to a secondary flow if the primary fails. + /// + public LogBuilder WriteToFailover(IFlow primaryFlow, IFlow secondaryFlow, TimeSpan? recoveryCheckInterval = null, int failureThreshold = 5) + { + if (primaryFlow == null) + { + throw new ArgumentNullException(nameof(primaryFlow)); + } + + if (secondaryFlow == null) + { + throw new ArgumentNullException(nameof(secondaryFlow)); + } + + _flows.Add(new FailoverFlow(primaryFlow, secondaryFlow, recoveryCheckInterval, failureThreshold)); + + return this; + } + + /// + /// Adds a Syslog TCP flow. + /// + public LogBuilder WriteToSyslogTcp( + string host, + int port = 514, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait, + bool useTls = false, + RemoteCertificateValidationCallback certValidationCallback = null, + X509CertificateCollection clientCertificates = null) + { + if (string.IsNullOrWhiteSpace(host)) + { + throw new ArgumentException("Host cannot be null or empty.", nameof(host)); + } + + if (port <= 0 || port > 65535) + { + throw new ArgumentOutOfRangeException(nameof(port), "Port must be between 1 and 65535."); + } + + _flows.Add(new SyslogTcpFlow( + host, + port, + batchSize, + minimumLevel, + backpressureStrategy, + useTls, + certValidationCallback, + clientCertificates)); + + return this; + } + + /// + /// Adds a Status Monitoring flow. + /// + public LogBuilder WriteToStatusFlow( + List servicesToMonitor, + TimeSpan? checkInterval = null, + string statusDirectory = null, + Action statusChangedTrigger = null) + { + _flows.Add(new StatusFlow( + servicesToMonitor, + checkInterval, + statusDirectory, + statusChangedTrigger)); + + return this; + } + + /// + /// Adds Syslog Tcp + /// + public LogBuilder WriteToSyslogUdp( + string host, + int port, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) + { + _flows.Add(new SyslogUdpFlow( + host, + port, + batchSize, + minimumLevel, + backpressureStrategy)); + return this; + } + + /// + /// Adds Zabbix + /// + public LogBuilder WriteToZabbixFlow( + string host, + int port = 10051, + string zabbixHostname = null, + string zabbixKey = "log_event", + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) + { + _flows.Add(new ZabbixFlow( + host, + port, + zabbixHostname, + zabbixKey, + batchSize, + minimumLevel, + backpressureStrategy)); + return this; + } + + /// + /// Adds Graylog + /// + public LogBuilder WriteToGraylogFlow( + string host, + int port = 12201, + bool useTcp = false, + string graylogHostName = null, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) + { + _flows.Add(new GraylogFlow( + host, + port, + useTcp, + graylogHostName, + batchSize, + minimumLevel, + backpressureStrategy)); + return this; + } + + /// + /// Publishes log events to a Redis channel using the PUBLISH command (Pub/Sub) + /// and optionally appends them to a Redis List (LPUSH) for persistence. + /// + /// Uses raw TCP + RESP protocol, so there arent additional dependencies + /// + /// Features: + /// - Reconnect with exponential back-off on connection failure + /// - Optional LPUSH to a list key with LTRIM to cap list length + /// - Optional password authentication (AUTH command) + /// - Optional DB selection (SELECT command) + /// - Background writer thread (non-blocking callers) + /// + public LogBuilder RedisFlow(string host = "localhost", + int port = 6379, + string password = null, + int database = 0, + string channel = "eonacat:logs", + string listKey = null, + int maxListLength = 10000, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new RedisFlow( + host, + port, + password, + database, + channel, + listKey, + maxListLength, + minimumLevel)); + return this; + } + + /// + /// Wraps any flow with token-bucket rate limiting and optional message deduplication. + /// Ideal for protecting high-latency sinks (email, Slack, HTTP) from log storms. + /// + /// The downstream flow to protect. + /// + /// Max events that can be emitted in a burst per level (token bucket capacity). + /// + /// + /// How many tokens are added per second per level. E.g. 5.0 = 5 events/second steady state. + /// + /// + /// If true, identical messages within are collapsed. + /// The suppressed count is appended to the message when the window expires. + /// + /// Deduplication window (default 60 s). + /// Maximum number of distinct messages tracked (default 1000). + /// Minimum level this flow processes. + public LogBuilder WriteToThrottled(IFlow inner, + int burstCapacity = 10, + double refillPerSecond = 1.0, + bool deduplicate = false, + TimeSpan dedupWindow = default(TimeSpan), + int dedupMaxKeys = 1000, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new ThrottledFlow( + inner, + burstCapacity, + refillPerSecond, + deduplicate, + dedupWindow, + dedupMaxKeys, + minimumLevel)); + return this; + } + + /// + /// Adds Splunk + /// + public LogBuilder WriteToSplunkFlow( + string splunkUrl, + string token, + string sourceType = "splunk_logs", + string hostName = null, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) + { + _flows.Add(new SplunkFlow( + splunkUrl, + token, + sourceType, + hostName, + batchSize, + minimumLevel, + backpressureStrategy)); + return this; + } + + /// + /// Pushes log events to a SignalR hub via HTTP POST to the hub's /send endpoint. + /// Works with ASP.NET SignalR (classic) and ASP.NET Core SignalR server-side REST API. + /// + /// A lightweight alternative to the SignalR client library + /// + /// On the server side you need a minimal hub endpoint that accepts POST: + /// POST {hubUrl}/send body: { "target": "...", "arguments": [ { log json } ] } + /// + /// For live dashboards: the hub broadcasts to a "logs" group; clients subscribe and + /// render events in real time. + /// + public LogBuilder WriteToSignalR( + string hubUrl, + string hubMethod = "ReceiveLog", + HttpClient httpClient = null, + int batchSize = 20, + int batchIntervalMs = 500, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new SignalRFlow( + hubUrl, + hubMethod, + httpClient, + batchSize, + batchIntervalMs, + minimumLevel)); + return this; + } + + /// + /// Sends log events as HTML email digests via SMTP. + /// Batches events for before sending, + /// unless flushOnCritical is true (Critical events bypass batching). + /// + public LogBuilder WriteToEmail( + string smtpHost, + int smtpPort = 587, + bool useSsl = true, + string username = null, + string password = null, + string from = null, + string to = null, + string subjectPrefix = "[EonaCatLogStack]", + int digestMinutes = 5, + bool flushOnCritical = true, + int maxEventsPerDigest = 100, + string headerName = null, + LogLevel minimumLevel = LogLevel.Error) + { + _flows.Add(new EmailFlow( + smtpHost, + smtpPort, + useSsl, + username, + password, + from, + to, + subjectPrefix, + digestMinutes, + flushOnCritical, + maxEventsPerDigest, + headerName, + minimumLevel)); + return this; + } + + /// + /// Adds Udp + /// + public LogBuilder WriteToUdp( + string host, + int port, + int flushIntervalInMilliseconds = 1000, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) + { + _flows.Add(new UdpFlow( + host, + port, + flushIntervalInMilliseconds, + batchSize, + minimumLevel, + backpressureStrategy)); + return this; + } + + /// + /// Adds in-memory buffer output + /// + public LogBuilder WriteToMemory( + int capacity = 10000, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new MemoryFlow(capacity, minimumLevel)); + return this; + } + + /// + /// Adds HTTP endpoint output + /// + public LogBuilder WriteToHttp( + string endpoint, + HttpClient? httpClient = null, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + TimeSpan? batchInterval = null, + Dictionary? headers = null) + { + _flows.Add(new HttpFlow( + endpoint, + httpClient, + batchSize, + minimumLevel, + batchInterval, + headers)); + return this; + } + + /// + /// Adds a custom flow + /// + public LogBuilder WriteTo(IFlow flow) + { + _flows.Add(flow ?? throw new ArgumentNullException(nameof(flow))); + return this; + } + + /// + /// Boost logs with machine name + /// + public LogBuilder BoostWithMachineName() + { + _boosters.Add(new MachineNameBooster()); + return this; + } + + /// + /// Boost logs with process ID + /// + public LogBuilder BoostWithProcessId() + { + _boosters.Add(new ProcessIdBooster()); + return this; + } + + /// + /// Boost logs with the current date (yyyy-MM-dd) + /// + public LogBuilder BoostWithDate() + { + _boosters.Add(new DateBooster()); + return this; + } + + /// + /// Boost logs with the current time (HH:mm:ss.fff) + /// + public LogBuilder BoostWithTime() + { + _boosters.Add(new TimeBooster()); + return this; + } + + /// + /// Boost logs with the current timestamp ticks + /// + public LogBuilder BoostWithTicks() + { + _boosters.Add(new TicksBooster()); + return this; + } + + /// + /// Boost logs with the process start time + /// + public LogBuilder BoostWithProcStart() + { + _boosters.Add(new ProcStartBooster()); + return this; + } + + /// + /// Boost logs with the uptime of the process in seconds + /// + public LogBuilder BoostWithUptime() + { + _boosters.Add(new UptimeBooster()); + return this; + } + + /// + /// Boost logs with the current thread name + /// + public LogBuilder BoostWithThreadName() + { + _boosters.Add(new ThreadNameBooster()); + return this; + } + + /// + /// Boost logs with memory usage in MB + /// + public LogBuilder BoostWithMemory() + { + _boosters.Add(new MemoryBooster()); + return this; + } + + /// + /// Boost logs with the operating system description + /// + public LogBuilder BoostWithOS() + { + _boosters.Add(new OSBooster()); + return this; + } + + /// + /// Boost logs with the runtime/framework description + /// + public LogBuilder BoostWithFramework() + { + _boosters.Add(new FrameworkBooster()); + return this; + } + + /// + /// Boost logs with the application name and base directory + /// + public LogBuilder BoostWithApp() + { + _boosters.Add(new AppBooster()); + return this; + } + + /// + /// Boost logs with the current user name + /// + public LogBuilder BoostWithUser() + { + _boosters.Add(new UserBooster()); + return this; + } + + /// + /// Boost logs with the current thread ID + /// + public LogBuilder BoostWithThreadId() + { + _boosters.Add(new ThreadIdBooster()); + return this; + } + + + /// + /// Boost logs with custom text + /// + public LogBuilder BoostWithCustomText(string key, string value) + { + _boosters.Add(new CustomTextBooster(key, value)); + return this; + } + + /// + /// Boost logs with environment name + /// + public LogBuilder BoostWithEnvironment(string environmentName) + { + _boosters.Add(new EnvironmentBooster(environmentName)); + return this; + } + + /// + /// Boost logs with application name and version + /// + public LogBuilder BoostWithApplication(string applicationName, string? version = null) + { + _boosters.Add(new ApplicationBooster(applicationName, version)); + return this; + } + + /// + /// Boost logs with correlation ID from Activity + /// + public LogBuilder BoostWithCorrelationId() + { + _boosters.Add(new CorrelationIdBooster()); + return this; + } + + /// + /// Adds a custom booster + /// + public LogBuilder Boost(IBooster booster) + { + _boosters.Add(booster ?? throw new ArgumentNullException(nameof(booster))); + return this; + } + + /// + /// Adds a callback-based booster + /// + public LogBuilder Boost(string name, Func> callback) + { + _boosters.Add(new CallbackBooster(name, callback)); + return this; + } + + /// + /// Builds the configured logger + /// + public EonaCatLogStack Build() + { + var logger = new EonaCatLogStack(_category, _minimumLevel, _timestampMode); + logger.OnLog += (sender, message) => OnLog?.Invoke(sender, message); + + foreach (var flow in _flows) + { + logger.AddFlow(flow); + } + + foreach (var booster in _boosters) + { + logger.AddBooster(booster); + } + if (_dynamicLevel != null) { logger.UseDynamicLevel(_dynamicLevel); @@ -1004,146 +1004,146 @@ public sealed class LogBuilder logger.UseAsyncPipeline(_asyncPipelineCapacity); } - return logger; - } - - /// - /// Creates a default logger with console and file output - /// - public static EonaCatLogStack CreateDefault( - string category = "Application", - string? logDirectory = null) - { - var directory = logDirectory ?? Path.Combine(AppContext.BaseDirectory, "logs"); - - return new LogBuilder(category) - .WithMinimumLevel(LogLevel.Information) - .WriteToConsole() - .WriteToFile(directory) - .BoostWithMachineName() - .BoostWithProcessId() - .Build(); - } - - /// - /// Get a flow by name - /// - /// - public IFlow GetFlow(string name) - { - lock (_flows) - { - var flow = _flows.Find(x => x.Name == name); - return flow; - } - } - - /// - /// Get a flow by type - /// - /// - public IFlow GetFlow(Type type) - { - lock (_flows) - { - var flow = _flows.Find(x => x.GetType() == type); - return flow; - } - } - - /// - /// Add a flow to the logBuilder - /// - /// - public void AddFlow(IFlow flow) - { - lock (_flows) - { - _flows.Add(flow); - } - } - - /// - /// Removes a flow from the logBuilder - /// - /// To be removed flow - public void RemoveFlow(IFlow flow) - { - lock (_flows) - { - _flows.Remove(flow); - } - } - - /// - /// Removes a flow from the logBuilder by name - /// - /// To be removed flow name - public void RemoveFlow(string name) - { - if (name == null) - { - throw new ArgumentNullException("name"); - } - - if (string.IsNullOrWhiteSpace(name)) - { - return; - } - - lock (_flows) { _flows.RemoveAll(f => f.Name == name); } - } - - /// - /// Enables the Channel-based async dispatch pipeline for zero-blocking logging. - /// Events are enqueued to a and - /// consumed by a dedicated background Task. - /// - /// Bounded capacity (0 = unbounded). - public LogBuilder UseAsyncPipeline(int capacity = 65536) - { - _useAsyncPipeline = true; - _asyncPipelineCapacity = capacity; - return this; - } - - /// - /// Attaches a so the minimum log level can - /// be changed at runtime without restarting the application. - /// - public LogBuilder WithDynamicLevelController(DynamicLevelController controller) - { - _dynamicLevel = controller ?? throw new ArgumentNullException(nameof(controller)); - return this; - } - - /// - /// Wraps any flow with a Circuit Breaker that opens after repeated failures - /// and probes for recovery after a configurable timeout. - /// - public LogBuilder WriteToCircuitBreaker( - IFlow inner, - int failureThreshold = 5, - TimeSpan? recoveryTimeout = null, - LogLevel minimumLevel = LogLevel.Trace) - { + return logger; + } + + /// + /// Creates a default logger with console and file output + /// + public static EonaCatLogStack CreateDefault( + string category = "Application", + string? logDirectory = null) + { + var directory = logDirectory ?? Path.Combine(AppContext.BaseDirectory, "logs"); + + return new LogBuilder(category) + .WithMinimumLevel(LogLevel.Information) + .WriteToConsole() + .WriteToFile(directory) + .BoostWithMachineName() + .BoostWithProcessId() + .Build(); + } + + /// + /// Get a flow by name + /// + /// + public IFlow GetFlow(string name) + { + lock (_flows) + { + var flow = _flows.Find(x => x.Name == name); + return flow; + } + } + + /// + /// Get a flow by type + /// + /// + public IFlow GetFlow(Type type) + { + lock (_flows) + { + var flow = _flows.Find(x => x.GetType() == type); + return flow; + } + } + + /// + /// Add a flow to the logBuilder + /// + /// + public void AddFlow(IFlow flow) + { + lock (_flows) + { + _flows.Add(flow); + } + } + + /// + /// Removes a flow from the logBuilder + /// + /// To be removed flow + public void RemoveFlow(IFlow flow) + { + lock (_flows) + { + _flows.Remove(flow); + } + } + + /// + /// Removes a flow from the logBuilder by name + /// + /// To be removed flow name + public void RemoveFlow(string name) + { + if (name == null) + { + throw new ArgumentNullException("name"); + } + + if (string.IsNullOrWhiteSpace(name)) + { + return; + } + + lock (_flows) { _flows.RemoveAll(f => f.Name == name); } + } + + /// + /// Enables the Channel-based async dispatch pipeline for zero-blocking logging. + /// Events are enqueued to a and + /// consumed by a dedicated background Task. + /// + /// Bounded capacity (0 = unbounded). + public LogBuilder UseAsyncPipeline(int capacity = 65536) + { + _useAsyncPipeline = true; + _asyncPipelineCapacity = capacity; + return this; + } + + /// + /// Attaches a so the minimum log level can + /// be changed at runtime without restarting the application. + /// + public LogBuilder WithDynamicLevelController(DynamicLevelController controller) + { + _dynamicLevel = controller ?? throw new ArgumentNullException(nameof(controller)); + return this; + } + + /// + /// Wraps any flow with a Circuit Breaker that opens after repeated failures + /// and probes for recovery after a configurable timeout. + /// + public LogBuilder WriteToCircuitBreaker( + IFlow inner, + int failureThreshold = 5, + TimeSpan? recoveryTimeout = null, + LogLevel minimumLevel = LogLevel.Trace) + { if (inner == null) { throw new ArgumentNullException(nameof(inner)); } - _flows.Add(new CircuitBreakerFlow(inner, failureThreshold, recoveryTimeout, minimumLevel)); - return this; - } - - /// - /// Wraps any flow with a predicate — events are forwarded only when the predicate returns true. - /// - public LogBuilder WriteToConditional( - IFlow inner, - Func predicate, - LogLevel minimumLevel = LogLevel.Trace) - { + _flows.Add(new CircuitBreakerFlow(inner, failureThreshold, recoveryTimeout, minimumLevel)); + return this; + } + + /// + /// Wraps any flow with a predicate — events are forwarded only when the predicate returns true. + /// + public LogBuilder WriteToConditional( + IFlow inner, + Func predicate, + LogLevel minimumLevel = LogLevel.Trace) + { if (inner == null) { throw new ArgumentNullException(nameof(inner)); @@ -1154,52 +1154,52 @@ public sealed class LogBuilder throw new ArgumentNullException(nameof(predicate)); } - _flows.Add(new ConditionalFlow(inner, predicate, minimumLevel)); - return this; - } - - /// - /// Adds a that fans events out to multiple inner flows in parallel. - /// Use to attach targets. - /// - public LogBuilder WriteToMulticast( - MulticastFlow multicast) - { + _flows.Add(new ConditionalFlow(inner, predicate, minimumLevel)); + return this; + } + + /// + /// Adds a that fans events out to multiple inner flows in parallel. + /// Use to attach targets. + /// + public LogBuilder WriteToMulticast( + MulticastFlow multicast) + { if (multicast == null) { throw new ArgumentNullException(nameof(multicast)); } - _flows.Add(multicast); - return this; - } - - /// - /// Pushes log events to a Grafana Loki instance. - /// - public LogBuilder WriteToLoki( - string lokiUrl, - Dictionary? labels = null, - int batchSize = 50, - int batchIntervalMs = 1000, - string? bearerToken = null, - string? basicUser = null, - string? basicPassword = null, - HttpClient? httpClient = null, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new LokiFlow( - lokiUrl, labels, batchSize, batchIntervalMs, - bearerToken, basicUser, basicPassword, httpClient, minimumLevel)); - return this; - } - - /// - /// Adds the which captures caller member/file/line. - /// - public LogBuilder BoostWithCallerInfo() - { - _boosters.Add(new CallerInfoBooster()); - return this; - } + _flows.Add(multicast); + return this; + } + + /// + /// Pushes log events to a Grafana Loki instance. + /// + public LogBuilder WriteToLoki( + string lokiUrl, + Dictionary? labels = null, + int batchSize = 50, + int batchIntervalMs = 1000, + string? bearerToken = null, + string? basicUser = null, + string? basicPassword = null, + HttpClient? httpClient = null, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new LokiFlow( + lokiUrl, labels, batchSize, batchIntervalMs, + bearerToken, basicUser, basicPassword, httpClient, minimumLevel)); + return this; + } + + /// + /// Adds the which captures caller member/file/line. + /// + public LogBuilder BoostWithCallerInfo() + { + _boosters.Add(new CallerInfoBooster()); + return this; + } } \ No newline at end of file diff --git a/Testers/EonaCat.LogStack.Test.Web/Program.cs b/Testers/EonaCat.LogStack.Test.Web/Program.cs index adadf8d..9945fac 100644 --- a/Testers/EonaCat.LogStack.Test.Web/Program.cs +++ b/Testers/EonaCat.LogStack.Test.Web/Program.cs @@ -16,6 +16,11 @@ namespace EonaCat.LogStack.Test.Web { public static async Task Main(string[] args) { + await using var logger = LogBuilder.CreateDefault("MyApp"); + + logger.Information("Application started"); + logger.Warning("Low memory warning"); + logger.Error(new Exception("DIT IS MIJN TEST!"), "Unexpected error occurred"); // Configure the client var centralOptions = new LogCentralOptions @@ -46,6 +51,9 @@ namespace EonaCat.LogStack.Test.Web var logBuilder = new LogBuilder(); logBuilder.WithTimestampMode(TimestampMode.Local); logBuilder.WriteToConsole(); + logBuilder.WriteToFile("./logs", outputFormat: FileOutputFormat.Csv); + logBuilder.WriteToFile("./logs", outputFormat: FileOutputFormat.StructuredJson); + logBuilder.WriteToFile("./logs", outputFormat: FileOutputFormat.Text); logBuilder.WriteToFile("./logs", outputFormat: FileOutputFormat.Json); logBuilder.WriteToFile("./logs", outputFormat: FileOutputFormat.Xml); logBuilder.WriteToTcp("127.0.0.1", 514);