diff --git a/EonaCat.Logger.LogClient/EonaCat.Logger.LogClient.csproj b/EonaCat.Logger.LogClient/EonaCat.Logger.LogClient.csproj index 5c2640f..390c14c 100644 --- a/EonaCat.Logger.LogClient/EonaCat.Logger.LogClient.csproj +++ b/EonaCat.Logger.LogClient/EonaCat.Logger.LogClient.csproj @@ -25,7 +25,7 @@ - + diff --git a/EonaCat.Logger/EonaCat.Logger.csproj b/EonaCat.Logger/EonaCat.Logger.csproj index c905100..15b6e89 100644 --- a/EonaCat.Logger/EonaCat.Logger.csproj +++ b/EonaCat.Logger/EonaCat.Logger.csproj @@ -13,8 +13,8 @@ EonaCat (Jeroen Saey) EonaCat;Logger;EonaCatLogger;Log;Writer;Jeroen;Saey - 1.5.8 - 1.5.8 + 1.5.9 + 1.5.9 README.md True LICENSE @@ -25,7 +25,7 @@ - 1.5.8+{chash:10}.{c:ymd} + 1.5.9+{chash:10}.{c:ymd} true true v[0-9]* diff --git a/EonaCat.Logger/Managers/LogHelper.cs b/EonaCat.Logger/Managers/LogHelper.cs index 92cf8f6..efb702a 100644 --- a/EonaCat.Logger/Managers/LogHelper.cs +++ b/EonaCat.Logger/Managers/LogHelper.cs @@ -8,15 +8,132 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net; +using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; +using static EonaCat.Logger.Managers.LogHelper; // 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. namespace EonaCat.Logger.Managers; +/// +/// Provides a collection of predefined and customizable header token resolvers for formatting log or message headers. +/// +/// HeaderTokens exposes a set of standard tokens that can be used to insert runtime, environment, and +/// contextual information into header strings, such as timestamps, process details, thread information, and more. +/// Custom tokens can be added or overridden to support application-specific formatting needs. All token keys are +/// case-insensitive. This class is thread-safe for read operations, but adding or overriding tokens is not guaranteed +/// to be thread-safe and should be synchronized if used concurrently. +public class HeaderTokens +{ + private readonly Dictionary> _tokenResolvers = + new(StringComparer.OrdinalIgnoreCase) + { + ["date"] = ctx => $"[Date: {ctx.Timestamp:yyyy-MM-dd}]", + + ["time"] = ctx => $"[Time: {ctx.Timestamp:HH:mm:ss.fff}]", + + ["ticks"] = ctx => $"[Ticks: {ctx.Timestamp.Ticks}]", + + ["ts"] = ctx => $"[{ctx.Timestamp.ToString(ctx.TimestampFormat)}]", + + ["tz"] = ctx => $"[{(ctx.Timestamp.Kind == DateTimeKind.Utc ? "UTC" : "LOCAL")}]", + + ["unix"] = ctx => $"[Unix: {new DateTimeOffset(ctx.Timestamp).ToUnixTimeSeconds()}]", + + ["procstart"] = _ => + { + var p = Process.GetCurrentProcess(); + return $"[ProcStart: {p.StartTime:O}]"; + }, + + ["uptime"] = _ => + { + var p = Process.GetCurrentProcess(); + return $"[Uptime: {(DateTime.Now - p.StartTime).TotalSeconds:F0}s]"; + }, + + ["framework"] = _ => + $"[Runtime: {RuntimeInformation.FrameworkDescription}]", + + ["os"] = _ => + $"[OS: {RuntimeInformation.OSDescription}]", + + ["arch"] = _ => + $"[Arch: {RuntimeInformation.ProcessArchitecture}]", + + ["mem"] = _ => $"[Memory: {GC.GetTotalMemory(false) / 1024 / 1024}MB]", + + ["gc"] = _ => $"[GC: {GC.CollectionCount(0)}/{GC.CollectionCount(1)}/{GC.CollectionCount(2)}]", + + ["cwd"] = _ => $"[CWD: {Environment.CurrentDirectory}]", + + ["app"] = _ => $"[App: {AppDomain.CurrentDomain.FriendlyName}]", + + ["appbase"] = _ => $"[AppBase: {AppDomain.CurrentDomain.BaseDirectory}]", + + ["domain"] = _ => $"[Domain: {AppDomain.CurrentDomain.Id}]", + + ["threadname"] = _ => $"[ThreadName: {Thread.CurrentThread.Name ?? "n/a"}]", + + ["task"] = _ => $"[TaskId: {Task.CurrentId?.ToString() ?? "n/a"}]", + + ["host"] = ctx => $"[Host: {ctx.HostName}]", + + ["machine"] = _ => $"[Machine: {Environment.MachineName}]", + + ["category"] = ctx => $"[Category: {ctx.Category}]", + + ["thread"] = _ => $"[Thread: {Environment.CurrentManagedThreadId}]", + + ["process"] = _ => + { + var p = Process.GetCurrentProcess(); + return $"[Process: {p.ProcessName}]"; + }, + + ["pid"] = _ => $"[PID: {Process.GetCurrentProcess().Id}]", + + ["sev"] = ctx => $"[Severity: {ctx.LogType}]", + + ["user"] = _ => $"[User: {Environment.UserName}]", + + ["env"] = ctx => $"[Env: {ctx.EnvironmentName}]", + + ["newline"] = _ => Environment.NewLine + }; + + /// + /// Gets a read-only dictionary of token resolver functions used to generate header values based on a token name and + /// header context. + /// + /// Each entry maps a token name to a function that produces the corresponding header value for a + /// given . The dictionary is static and cannot be modified at runtime. + public IReadOnlyDictionary> TokenResolvers => _tokenResolvers; + + /// + /// Adds or overrides a custom token. + /// + public void AddCustomToken(string key, Func resolver) + { + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentException("Token key cannot be null or whitespace.", nameof(key)); + } + + if (resolver == null) + { + throw new ArgumentNullException(nameof(resolver)); + } + + _tokenResolvers[key] = resolver; + } +} + public class ErrorMessage { public Exception Exception { get; set; } @@ -36,23 +153,6 @@ public static class LogHelper public string EnvironmentName { get; set; } } - internal static Dictionary> TokenResolvers = - new(StringComparer.OrdinalIgnoreCase) - { - ["ts"] = ctx => ctx.Timestamp.ToString(ctx.TimestampFormat), - ["host"] = ctx => ctx.HostName, - ["machine"] = ctx => Environment.MachineName, - ["category"] = ctx => ctx.Category, - ["thread"] = ctx => Environment.CurrentManagedThreadId.ToString(), - ["process"] = ctx => Process.GetCurrentProcess().ProcessName, - ["pid"] = ctx => Process.GetCurrentProcess().Id.ToString(), - ["sev"] = ctx => ctx.LogType.ToString(), - ["user"] = ctx => Environment.UserName, - ["env"] = ctx => ctx.EnvironmentName, - ["newline"] = _ => Environment.NewLine - }; - - internal static event EventHandler OnException; internal static event EventHandler OnLogLevelDisabled; @@ -112,11 +212,12 @@ public static class LogHelper return Regex.Replace(format, @"\{([^}]+)\}", match => { - var token = match.Groups[1].Value; - var parts = token.Split(new[] { ':' }, 2); + var tokenText = match.Groups[1].Value; + + var parts = tokenText.Split(new[] { ':' }, 2); var key = parts[0]; - if (!TokenResolvers.TryGetValue(key, out var resolver)) + if (!ctx.Settings.HeaderTokens.TokenResolvers.TryGetValue(key, out var resolver)) { return match.Value; } @@ -125,13 +226,13 @@ public static class LogHelper { ctx.TimestampFormat = parts[1]; } - return resolver(ctx); }); } + internal static string FormatMessageWithHeader(LoggerSettings settings, ELogType logType, string message, DateTime dateTime, string category = null) { if (string.IsNullOrWhiteSpace(message)) diff --git a/EonaCat.Logger/Managers/LoggerSettings.cs b/EonaCat.Logger/Managers/LoggerSettings.cs index ab8bfee..9eda9bf 100644 --- a/EonaCat.Logger/Managers/LoggerSettings.cs +++ b/EonaCat.Logger/Managers/LoggerSettings.cs @@ -24,7 +24,7 @@ public class LoggerSettings private FileLoggerOptions _fileLoggerOptions; - private string _headerFormat = "{ts} {host} {category} {thread} {sev}"; + private string _headerFormat = "{ts} {tz} {host} {category} {thread} {sev}"; private string _timestampFormat = "yyyy-MM-dd HH:mm:ss"; /// @@ -32,8 +32,18 @@ public class LoggerSettings /// public bool UseLocalTime { get; set; } + /// + /// Gets or sets the unique identifier for the application instance. + /// public string Id { get; set; } = DllInfo.ApplicationName; + /// + /// Gets or sets the collection of custom header tokens to be included in outgoing requests. + /// + /// Use this property to specify additional headers that should be sent with each request. + /// Modifying the collection affects all subsequent requests made by this instance. + public HeaderTokens HeaderTokens { get; set; } = new HeaderTokens(); + /// /// Header format. Provide a string that specifies how the preamble of each message should be structured. You can use /// variables including: @@ -42,7 +52,7 @@ public class LoggerSettings /// {category}: Category /// {thread}: Thread ID /// {sev}: Severity - /// Default: {ts} {host} {category} {thread} {sev} + /// Default: {ts} {tz} {host} {category} {thread} {sev} /// A space will be inserted between the header and the message. /// public string HeaderFormat diff --git a/README.md b/README.md index 4f82ddd..c53a91a 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,39 @@ Dutch Passport Numbers (9 alphanumeric characters Dutch Identification Document Numbers (varying formats) Custom keywords specified in LoggerSettings +Header tokens: + +{date} {time} {ts} {tz} {unix} {ticks}{newline} +{sev} {category} {env}{newline} +{host} {machine}{newline} +{process} {pid} {procstart} {uptime}{newline} +{thread} {threadname} {task}{newline} +{user}{newline} +{app} {appbase} {domain}{newline} +{framework} {os} {arch}{newline} +{mem} {gc}{newline} +{cwd} + +Example of custom header: +```csharp +[Date: 2026-01-30] [Time: 14:22:11.384] [2026-01-30 14:22:11.384] [UTC] [Unix: 1738246931] [Ticks: 638422525313840000] +[Severity: Info] [Category: Startup] [Env: Production] +[Host: api-01] [Machine: API-SERVER-01] +[Process: MyService] [PID: 4216] [ProcStart: 2026-01-30T14:20:03.5123456Z] [Uptime: 128s] +[Thread: 9] [ThreadName: worker-1] [TaskId: 42] +[User: svc-api] +[App: MyService.exe] [AppBase: C:\apps\myservice\] [Domain: 1] +[Runtime: .NET 8.0.1] [OS: Linux 6.6.9] [Arch: X64] +[Memory: 128MB] [GC: 12/4/1] +[CWD: /app] +``` + +Add Custom tokens: + +```csharp +Logger.LoggerSettings.HeaderTokens.AddCustomToken("spaceShuttleId", context => $"[Shuttle: {context.SpaceShuttleId}]"); +``` + **Code for enabling GrayLog in the above *advanced* logger class:** diff --git a/Testers/EonaCat.Logger.Test.Web/Logger.cs b/Testers/EonaCat.Logger.Test.Web/Logger.cs index e95da27..3202d1a 100644 --- a/Testers/EonaCat.Logger.Test.Web/Logger.cs +++ b/Testers/EonaCat.Logger.Test.Web/Logger.cs @@ -40,7 +40,7 @@ public class Logger LoggerSettings.CustomHeaderFormatter = ctx => { - if (ctx.LogType == ELogType.Error) + if (ctx.LogType == ELogType.ERROR) { return $"{ctx.Timestamp:HH:mm:ss} [{ctx.LogType}]"; }