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