Files
EonaCat.LogStack/EonaCat.LogStack/LogBuilder.cs
T
EonaCat 1560e282b5 Updated README.md
Added more telemetry tooling
2026-06-22 18:58:32 +02:00

1282 lines
37 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
/// <summary>
/// Fluent builder for configuring the logger with flows and boosters
/// </summary>
public sealed class LogBuilder
{
private readonly string _category;
private LogLevel _minimumLevel = LogLevel.Trace;
private TimestampMode _timestampMode = TimestampMode.Utc;
private readonly List<IFlow> _flows = new();
private readonly List<IBooster> _boosters = new();
private DynamicLevelController? _dynamicLevel;
private bool _useAsyncPipeline;
private int _asyncPipelineCapacity = 65536;
public event EventHandler<LogMessage> OnLog;
public LogBuilder(string category = "Application")
{
_category = category ?? throw new ArgumentNullException(nameof(category));
UseAsyncPipeline();
}
/// <summary>
/// Sets the minimum log level
/// </summary>
public LogBuilder WithMinimumLevel(LogLevel level)
{
_minimumLevel = level;
return this;
}
/// <summary>
/// Sets the timestamp mode
/// </summary>
public LogBuilder WithTimestampMode(TimestampMode mode)
{
_timestampMode = mode;
return this;
}
/// <summary>
/// Adds console output
/// </summary>
public LogBuilder WriteToConsole(
LogLevel minimumLevel = LogLevel.Trace,
bool useColors = true)
{
_flows.Add(new ConsoleFlow(minimumLevel, useColors, _timestampMode));
return this;
}
/// <summary>
/// Adds diagnostics
/// </summary>
public LogBuilder WriteDiagnostics(
TimeSpan snapshotInterval = default(TimeSpan),
bool injectIntoEvents = false,
bool writeSnapshotEvents = true,
string snapshotCategory = "Diagnostics",
IFlow forwardTo = null,
LogLevel minimumLevel = LogLevel.Trace,
Func<Dictionary<string, object>> customMetrics = null)
{
_flows.Add(new DiagnosticsFlow(
snapshotInterval,
injectIntoEvents,
writeSnapshotEvents,
snapshotCategory,
forwardTo,
minimumLevel,
customMetrics));
return this;
}
/// <summary>
/// Adds file output
/// </summary>
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;
}
/// <summary>
/// Write to a rolling buffer
/// </summary>
/// <param name="capacity">Maximum number of events to retain.</param>
/// <param name="minimumLevel">Minimum level to store in the buffer.</param>
/// <param name="triggerLevel">
/// When a log event reaches this level or above, the current buffer
/// contents are immediately forwarded to <paramref name="triggerTarget"/>.
/// Set to <c>LogLevel.None</c> (or omit) to disable.
/// </param>
/// <param name="triggerTarget">
/// Flow to forward the buffered context to when the trigger fires.
/// Can be null even when <paramref name="triggerLevel"/> is set.
/// </param>
/// <param name="preContextLines">
/// How many buffered lines to forward before the triggering event.
/// Defaults to entire buffer (int.MaxValue).
/// </param>
/// <returns></returns>
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;
}
/// <summary>
/// Decrypt a file which is encrypted by EonaCat Logger
/// </summary>
/// <param name="encryptedPath">encrypted file source path</param>
/// <param name="outputPath">destination path for decrypted file</param>
/// <param name="password">password used by encryption</param>
/// <returns></returns>
public static bool DecryptFile(string encryptedPath, string outputPath, string password)
{
return EncryptedFileFlow.DecryptToFile(encryptedPath, outputPath, password);
}
/// <summary>
/// Adds Database output
/// </summary>
public LogBuilder WriteToDatabase(
Func<DbConnection> connectionFactory,
string tableName = "logs",
int batchSize = 1,
LogLevel minimumLevel = LogLevel.Trace)
{
_flows.Add(new DatabaseFlow(
connectionFactory,
tableName,
batchSize,
minimumLevel));
return this;
}
/// <summary>
/// Adds Snmp traps
/// </summary>
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;
}
/// <summary>
/// Adds Discord
/// </summary>
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;
}
/// <summary>
/// Adds ElasticSearch
/// </summary>
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;
}
/// <summary>
/// Adds Telegram
/// </summary>
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;
}
/// <summary>
/// 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 <see cref="AuditFlow.Verify(string)"/> to verify file integrity at any time.
/// </summary>
/// <param name="directory">Directory where the .audit file is written.</param>
/// <param name="filePrefix">File name prefix (default: "audit").</param>
/// <param name="auditLevel">
/// Which severity levels are recorded in the audit trail:
/// <list type="bullet">
/// <item><see cref="AuditLevel.All"/> every log event (default)</item>
/// <item><see cref="AuditLevel.WarningAndAbove"/> Warning, Error, Critical</item>
/// <item><see cref="AuditLevel.ErrorAndAbove"/> Error and Critical only</item>
/// <item><see cref="AuditLevel.CriticalOnly"/> Critical only</item>
/// </list>
/// </param>
/// <param name="minimumLevel">Minimum <see cref="LogLevel"/> for audit capture.</param>
/// <param name="includeProperties">Whether structured properties are appended to each entry.</param>
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;
}
/// <summary>
/// Adds Slack
/// </summary>
public LogBuilder WriteToSlack(string webhookUrl, int batchSize = 1, LogLevel minimumLevel = LogLevel.Trace)
{
_flows.Add(new SlackFlow(
webhookUrl,
batchSize,
minimumLevel));
return this;
}
/// <summary>
/// Adds Slack
/// </summary>
public LogBuilder WriteToMicrosoftTeams(string webhookUrl, int batchSize = 1, LogLevel minimumLevel = LogLevel.Trace)
{
_flows.Add(new MicrosoftTeamsFlow(
webhookUrl,
batchSize,
minimumLevel));
return this;
}
/// <summary>
/// Adds a TCP flow.
/// </summary>
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;
}
/// <summary>
/// Adds a Retry flow to retry failed log writes.
/// </summary>
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;
}
/// <summary>
/// Adds an EventLog flow for sending logs to a remote destination.
/// </summary>
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;
}
/// <summary>
/// Adds a Failover flow that switches to a secondary flow if the primary fails.
/// </summary>
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;
}
/// <summary>
/// Adds a Syslog TCP flow.
/// </summary>
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;
}
/// <summary>
/// Adds a Status Monitoring flow.
/// </summary>
public LogBuilder WriteToStatusFlow(
List<ServiceStatus> servicesToMonitor,
TimeSpan? checkInterval = null,
string statusDirectory = null,
Action<ServiceStatus> statusChangedTrigger = null)
{
_flows.Add(new StatusFlow(
servicesToMonitor,
checkInterval,
statusDirectory,
statusChangedTrigger));
return this;
}
/// <summary>
/// Adds Syslog Tcp
/// </summary>
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;
}
/// <summary>
/// Adds Zabbix
/// </summary>
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;
}
/// <summary>
/// Adds Graylog
/// </summary>
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;
}
/// <summary>
/// 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)
/// </summary>
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;
}
/// <summary>
/// Wraps any flow with token-bucket rate limiting and optional message deduplication.
/// Ideal for protecting high-latency sinks (email, Slack, HTTP) from log storms.
/// </summary>
/// <param name="inner">The downstream flow to protect.</param>
/// <param name="burstCapacity">
/// Max events that can be emitted in a burst per level (token bucket capacity).
/// </param>
/// <param name="refillPerSecond">
/// How many tokens are added per second per level. E.g. 5.0 = 5 events/second steady state.
/// </param>
/// <param name="deduplicate">
/// If true, identical messages within <paramref name="dedupWindow"/> are collapsed.
/// The suppressed count is appended to the message when the window expires.
/// </param>
/// <param name="dedupWindow">Deduplication window (default 60 s).</param>
/// <param name="dedupMaxKeys">Maximum number of distinct messages tracked (default 1000).</param>
/// <param name="minimumLevel">Minimum level this flow processes.</param>
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;
}
/// <summary>
/// Adds Splunk
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// Sends log events as HTML email digests via SMTP.
/// Batches events for <paramref name="digestMinutes"/> before sending,
/// unless flushOnCritical is true (Critical events bypass batching).
/// </summary>
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;
}
/// <summary>
/// Adds Udp
/// </summary>
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;
}
/// <summary>
/// Adds in-memory buffer output
/// </summary>
public LogBuilder WriteToMemory(
int capacity = 10000,
LogLevel minimumLevel = LogLevel.Trace)
{
_flows.Add(new MemoryFlow(capacity, minimumLevel));
return this;
}
/// <summary>
/// Adds HTTP endpoint output
/// </summary>
public LogBuilder WriteToHttp(
string endpoint,
HttpClient? httpClient = null,
int batchSize = 1,
LogLevel minimumLevel = LogLevel.Trace,
TimeSpan? batchInterval = null,
Dictionary<string, string>? headers = null)
{
_flows.Add(new HttpFlow(
endpoint,
httpClient,
batchSize,
minimumLevel,
batchInterval,
headers));
return this;
}
/// <summary>
/// Adds a custom flow
/// </summary>
public LogBuilder WriteTo(IFlow flow)
{
_flows.Add(flow ?? throw new ArgumentNullException(nameof(flow)));
return this;
}
/// <summary>
/// Boost logs with machine name
/// </summary>
public LogBuilder BoostWithMachineName()
{
_boosters.Add(new MachineNameBooster());
return this;
}
/// <summary>
/// Boost logs with process ID
/// </summary>
public LogBuilder BoostWithProcessId()
{
_boosters.Add(new ProcessIdBooster());
return this;
}
/// <summary>
/// Boost logs with the current date (yyyy-MM-dd)
/// </summary>
public LogBuilder BoostWithDate()
{
_boosters.Add(new DateBooster());
return this;
}
/// <summary>
/// Boost logs with the current time (HH:mm:ss.fff)
/// </summary>
public LogBuilder BoostWithTime()
{
_boosters.Add(new TimeBooster());
return this;
}
/// <summary>
/// Boost logs with the current timestamp ticks
/// </summary>
public LogBuilder BoostWithTicks()
{
_boosters.Add(new TicksBooster());
return this;
}
/// <summary>
/// Boost logs with the process start time
/// </summary>
public LogBuilder BoostWithProcStart()
{
_boosters.Add(new ProcStartBooster());
return this;
}
/// <summary>
/// Boost logs with the uptime of the process in seconds
/// </summary>
public LogBuilder BoostWithUptime()
{
_boosters.Add(new UptimeBooster());
return this;
}
/// <summary>
/// Boost logs with the current thread name
/// </summary>
public LogBuilder BoostWithThreadName()
{
_boosters.Add(new ThreadNameBooster());
return this;
}
/// <summary>
/// Boost logs with memory usage in MB
/// </summary>
public LogBuilder BoostWithMemory()
{
_boosters.Add(new MemoryBooster());
return this;
}
/// <summary>
/// Boost logs with the operating system description
/// </summary>
public LogBuilder BoostWithOS()
{
_boosters.Add(new OSBooster());
return this;
}
/// <summary>
/// Boost logs with the runtime/framework description
/// </summary>
public LogBuilder BoostWithFramework()
{
_boosters.Add(new FrameworkBooster());
return this;
}
/// <summary>
/// Boost logs with the application name and base directory
/// </summary>
public LogBuilder BoostWithApp()
{
_boosters.Add(new AppBooster());
return this;
}
/// <summary>
/// Boost logs with the current user name
/// </summary>
public LogBuilder BoostWithUser()
{
_boosters.Add(new UserBooster());
return this;
}
/// <summary>
/// Boost logs with the current thread ID
/// </summary>
public LogBuilder BoostWithThreadId()
{
_boosters.Add(new ThreadIdBooster());
return this;
}
/// <summary>
/// Boost logs with custom text
/// </summary>
public LogBuilder BoostWithCustomText(string key, string value)
{
_boosters.Add(new CustomTextBooster(key, value));
return this;
}
/// <summary>
/// Boost logs with environment name
/// </summary>
public LogBuilder BoostWithEnvironment(string environmentName)
{
_boosters.Add(new EnvironmentBooster(environmentName));
return this;
}
/// <summary>
/// Boost logs with application name and version
/// </summary>
public LogBuilder BoostWithApplication(string applicationName, string? version = null)
{
_boosters.Add(new ApplicationBooster(applicationName, version));
return this;
}
/// <summary>
/// Boost logs with correlation ID from Activity
/// </summary>
public LogBuilder BoostWithCorrelationId()
{
_boosters.Add(new CorrelationIdBooster());
return this;
}
/// <summary>
/// Adds a custom booster
/// </summary>
public LogBuilder Boost(IBooster booster)
{
_boosters.Add(booster ?? throw new ArgumentNullException(nameof(booster)));
return this;
}
/// <summary>
/// Adds a callback-based booster
/// </summary>
public LogBuilder Boost(string name, Func<Dictionary<string, object?>> callback)
{
_boosters.Add(new CallbackBooster(name, callback));
return this;
}
/// <summary>
/// Builds the configured logger
/// </summary>
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);
}
if (_useAsyncPipeline)
{
logger.UseAsyncPipeline(_asyncPipelineCapacity);
}
return logger;
}
/// <summary>
/// Creates a default logger with console and file output
/// </summary>
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();
}
/// <summary>
/// Get a flow by name
/// </summary>
/// <param name="name"></param>
public IFlow GetFlow(string name)
{
lock (_flows)
{
var flow = _flows.Find(x => x.Name == name);
return flow;
}
}
/// <summary>
/// Get a flow by type
/// </summary>
/// <param name="type"></param>
public IFlow GetFlow(Type type)
{
lock (_flows)
{
var flow = _flows.Find(x => x.GetType() == type);
return flow;
}
}
/// <summary>
/// Add a flow to the logBuilder
/// </summary>
/// <param name="flow"></param>
public void AddFlow(IFlow flow)
{
lock (_flows)
{
_flows.Add(flow);
}
}
/// <summary>
/// Removes a flow from the logBuilder
/// </summary>
/// <param name="flow">To be removed flow</param>
public void RemoveFlow(IFlow flow)
{
lock (_flows)
{
_flows.Remove(flow);
}
}
/// <summary>
/// Removes a flow from the logBuilder by name
/// </summary>
/// <param name="name">To be removed flow name</param>
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); }
}
/// <summary>
/// Enables the Channel-based async dispatch pipeline for zero-blocking logging.
/// Events are enqueued to a <see cref="System.Threading.Channels.Channel{T}"/> and
/// consumed by a dedicated background Task.
/// </summary>
/// <param name="capacity">Bounded capacity (0 = unbounded).</param>
public LogBuilder UseAsyncPipeline(int capacity = 65536)
{
_useAsyncPipeline = true;
_asyncPipelineCapacity = capacity;
return this;
}
/// <summary>
/// Disables the async dispatch pipeline and uses a synchronous flow instead.
/// </summary>
/// <param name="capacity"></param>
/// <returns></returns>
public LogBuilder UseSyncPipeline(int capacity = 65536)
{
_useAsyncPipeline = false;
_asyncPipelineCapacity = capacity;
return this;
}
/// <summary>
/// Attaches a <see cref="DynamicLevelController"/> so the minimum log level can
/// be changed at runtime without restarting the application.
/// </summary>
public LogBuilder WithDynamicLevelController(DynamicLevelController controller)
{
_dynamicLevel = controller ?? throw new ArgumentNullException(nameof(controller));
return this;
}
/// <summary>
/// Wraps any flow with a Circuit Breaker that opens after repeated failures
/// and probes for recovery after a configurable timeout.
/// </summary>
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;
}
/// <summary>
/// Wraps any flow with a predicate - events are forwarded only when the predicate returns true.
/// </summary>
public LogBuilder WriteToConditional(
IFlow inner,
Func<LogEvent, bool> predicate,
LogLevel minimumLevel = LogLevel.Trace)
{
if (inner == null)
{
throw new ArgumentNullException(nameof(inner));
}
if (predicate == null)
{
throw new ArgumentNullException(nameof(predicate));
}
_flows.Add(new ConditionalFlow(inner, predicate, minimumLevel));
return this;
}
/// <summary>
/// Adds a <see cref="MulticastFlow"/> that fans events out to multiple inner flows in parallel.
/// Use <see cref="MulticastFlow.Add"/> to attach targets.
/// </summary>
public LogBuilder WriteToMulticast(
MulticastFlow multicast)
{
if (multicast == null)
{
throw new ArgumentNullException(nameof(multicast));
}
_flows.Add(multicast);
return this;
}
/// <summary>
/// Pushes log events to a Grafana Loki instance.
/// </summary>
public LogBuilder WriteToLoki(
string lokiUrl,
Dictionary<string, string>? 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;
}
/// <summary>
/// Adds the <see cref="CallerInfoBooster"/> which captures caller member/file/line.
/// </summary>
public LogBuilder BoostWithCallerInfo()
{
_boosters.Add(new CallerInfoBooster());
return this;
}
/// <summary>Adds a stable event fingerprint for grouping failures.</summary>
public LogBuilder BoostWithExceptionFingerprint()
{
_boosters.Add(new ExceptionFingerprintBooster());
return this;
}
/// <summary>Adds runtime health information to every event.</summary>
public LogBuilder BoostWithHealthSnapshot()
{
_boosters.Add(new HealthSnapshotBooster());
return this;
}
/// <summary>Adds a schema version field to every log event.</summary>
public LogBuilder BoostWithSchemaVersion(string version = "1.0")
{
_boosters.Add(new SchemaVersionBooster(version));
return this;
}
/// <summary>Adds a monotonic sequence number to every event.</summary>
public LogBuilder BoostWithSequence()
{
_boosters.Add(new SequenceBooster());
return this;
}
/// <summary>
/// Creates a dependency-free compatibility facade exposing adapters for
/// Serilog, log4net and NLog style integrations.
/// </summary>
public Compatibility.LoggingCompatibilityFacade AsCompatibility()
{
return new Compatibility.LoggingCompatibilityFacade(Build());
}
/// <summary>
/// Creates a Serilog compatible adapter without adding Serilog dependency.
/// </summary>
public Compatibility.EonaCatSerilogAdapter AsSerilog()
{
return new Compatibility.EonaCatSerilogAdapter(Build());
}
/// <summary>
/// Creates a log4net compatible adapter without adding log4net dependency.
/// </summary>
public Compatibility.EonaCatLog4NetAdapter AsLog4Net()
{
return new Compatibility.EonaCatLog4NetAdapter(Build());
}
/// <summary>
/// Creates an NLog compatible adapter without adding NLog dependency.
/// </summary>
public Compatibility.EonaCatNLogAdapter AsNLog()
{
return new Compatibility.EonaCatNLogAdapter(Build());
}
}