diff --git a/EonaCat.LogStack/EonaCat.LogStack.csproj b/EonaCat.LogStack/EonaCat.LogStack.csproj index 3bc7426..a40e77b 100644 --- a/EonaCat.LogStack/EonaCat.LogStack.csproj +++ b/EonaCat.LogStack/EonaCat.LogStack.csproj @@ -14,7 +14,7 @@ It features a rich fluent API for routing log events to dozens of destinations f EonaCat (Jeroen Saey) EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey - 0.2.1 + 0.2.2 README.md True LICENSE @@ -25,7 +25,7 @@ It features a rich fluent API for routing log events to dozens of destinations f - 0.2.1+{chash:10}.{c:ymd} + 0.2.2+{chash:10}.{c:ymd} true true v[0-9]* @@ -36,7 +36,7 @@ It features a rich fluent API for routing log events to dozens of destinations f - 0.2.1 + 0.2.2 EonaCat.LogStack EonaCat.LogStack https://git.saey.me/EonaCat/EonaCat.LogStack diff --git a/EonaCat.LogStack/EonaCatLogger.cs b/EonaCat.LogStack/EonaCatLogger.cs index ce918dd..1dea17a 100644 --- a/EonaCat.LogStack/EonaCatLogger.cs +++ b/EonaCat.LogStack/EonaCatLogger.cs @@ -177,6 +177,13 @@ namespace EonaCat.LogStack private LogLevel EffectiveMinLevel() => _dynamicLevel != null ? _dynamicLevel.CurrentLevel : _minimumLevel; + /// + /// Checks whether an event at the specified level would be accepted. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsEnabled(LogLevel level) => + Volatile.Read(ref _disposed) == 0 && level >= EffectiveMinLevel(); + /// /// Adds a flow (output destination) to this logger /// @@ -216,10 +223,10 @@ namespace EonaCat.LogStack { var keep = _concurrentFlows.Where(f => f.Name != name).ToArray(); while (_concurrentFlows.TryTake(out _)) { } - foreach (var f in keep) - { - _concurrentFlows.Add(f); - } + foreach (var f in keep) + { + _concurrentFlows.Add(f); + } } return this; } @@ -248,26 +255,26 @@ 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); - } - } + 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)] @@ -357,15 +364,15 @@ namespace EonaCat.LogStack .WithException(exception) .WithTimestamp(GetTimestamp()); - ProcessLogEvent(ref builder); - - RaiseOnLog(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 }); } @@ -418,14 +425,65 @@ namespace EonaCat.LogStack ProcessLogEvent(ref builder); } + /// + /// Logs an exception together with structured properties. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log(LogLevel level, Exception exception, string message, params (string Key, object Value)[] properties) + { + if (Volatile.Read(ref _disposed) == 1 || level < EffectiveMinLevel()) + { + return; + } + + TrackLevel(level); + Interlocked.Increment(ref _totalExceptionsCount); + + var builder = new LogEventBuilder() + .WithLevel(level) + .WithCategory(_category) + .WithMessage(message) + .WithException(exception) + .WithTimestamp(GetTimestamp()); + + foreach (var (key, value) in properties) + { + builder.WithProperty(key, value); + } + + ProcessLogEvent(ref builder); + } + private void Write(LogLevel level, string template, params object[] args) { - Log(level, string.Format(template, args)); + Log(level, FormatTemplate(template, args)); } private void Write(LogLevel level, Exception ex, string template, params object[] args) { - Log(level, ex, string.Format(template, args)); + Log(level, ex, FormatTemplate(template, args)); + } + + /// + /// Formats a message template, supporting both Serilog-style named holes (e.g. "{UserId}") + /// and classic string.Format-style positional holes (e.g. "{0}"). Never throws on malformed + /// templates; falls back to the raw template text instead. + /// + private static string FormatTemplate(string template, object[] args) + { + if (string.IsNullOrEmpty(template)) + { + return template; + } + + try + { + return MessageTemplate.FromCache(template).Render(args, out _); + } + catch + { + return template; + } } public void Trace(string template, params object[] args) => Write(LogLevel.Trace, template, args); @@ -608,17 +666,17 @@ namespace EonaCat.LogStack .Where(d => d != null) .ToList(); - int flowCount; - int boosterCount; - - lock (_flows) - { - flowCount = _flows.Count; - } - - lock (_boosters) - { - boosterCount = _boosters.Count; + int flowCount; + int boosterCount; + + lock (_flows) + { + flowCount = _flows.Count; + } + + lock (_boosters) + { + boosterCount = _boosters.Count; } return new LoggerDiagnostics @@ -627,8 +685,8 @@ namespace EonaCat.LogStack MinimumLevel = _minimumLevel, TotalLogged = Interlocked.Read(ref _totalLoggedCount), TotalDropped = Interlocked.Read(ref _totalDroppedCount), - TotalExceptions = Interlocked.Read(ref _totalExceptionsCount), - FlowCount = flowCount, + TotalExceptions = Interlocked.Read(ref _totalExceptionsCount), + FlowCount = flowCount, BoosterCount = boosterCount, Flows = flowDiagnostics }; diff --git a/EonaCat.LogStack/EonaCatLoggerCore/EonaCatLoggerProvider.cs b/EonaCat.LogStack/EonaCatLoggerCore/EonaCatLoggerProvider.cs index ef7d2d8..cf5c046 100644 --- a/EonaCat.LogStack/EonaCatLoggerCore/EonaCatLoggerProvider.cs +++ b/EonaCat.LogStack/EonaCatLoggerCore/EonaCatLoggerProvider.cs @@ -1,5 +1,7 @@ using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; +using System.Linq; namespace EonaCat.LogStack.Logging; @@ -40,7 +42,7 @@ internal sealed class CategoryLogger : Microsoft.Extensions.Logging.ILogger => _scopes?.Push(state) ?? NullScope.Instance; public bool IsEnabled(LogLevel logLevel) - => logLevel != LogLevel.None; + => logLevel != LogLevel.None && _EonaCatLogStack.IsEnabled(logLevel.FromLogLevel()); public void Log( LogLevel logLevel, @@ -55,18 +57,55 @@ internal sealed class CategoryLogger : Microsoft.Extensions.Logging.ILogger } var message = formatter(state, exception); - - _scopes?.ForEachScope((scope, _) => + var properties = new List<(string Key, object Value)>(4) { - message = $"{message} | Scope={scope}"; - }, null); + ("Category", _category), + ("EventId", eventId.Id) + }; - if (exception != null) + if (!string.IsNullOrEmpty(eventId.Name)) { - message = $"{message} | Exception={exception}"; + properties.Add(("EventName", eventId.Name)); } - _EonaCatLogStack.Log($"[{logLevel}] [{_category}] {message}"); + if (state is IEnumerable> structuredState) + { + foreach (var pair in structuredState) + { + if (!string.IsNullOrEmpty(pair.Key) && pair.Value != null) + { + properties.Add((pair.Key, pair.Value)); + } + } + } + + _scopes?.ForEachScope>((scope, stateProperties) => + { + if (scope is IEnumerable> structuredScope) + { + foreach (var pair in structuredScope) + { + if (!string.IsNullOrEmpty(pair.Key) && pair.Value != null) + { + stateProperties.Add((pair.Key, pair.Value)); + } + } + } + else if (scope != null) + { + stateProperties.Add(("Scope", scope)); + } + }, properties); + + var level = logLevel.FromLogLevel(); + if (exception == null) + { + _EonaCatLogStack.Log(level, message, properties.ToArray()); + } + else + { + _EonaCatLogStack.Log(level, exception, message, properties.ToArray()); + } } }