From 028c80db947c00c8f3daed315156ed43430d1793 Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 9 Jan 2026 11:12:35 +0100 Subject: [PATCH] Integrate AcLogger with Microsoft.Extensions.Logging AyCode.Core loggers now implement the ILogger interface, enabling direct integration with Microsoft.Extensions.Logging. Added AcLoggerProvider and extension methods for easy DI registration. Internal LogLevel usages are now AcLogLevel to avoid confusion. This allows seamless use of AyCode loggers in ASP.NET Core and other .NET apps using standard logging abstractions. --- AyCode.Core.Server/AyCode.Core.Server.csproj | 1 + AyCode.Core/AyCode.Core.csproj | 1 + AyCode.Core/Loggers/AcLoggerAdapter.cs | 75 +++++++++ AyCode.Core/Loggers/AcLoggerBase.cs | 153 ++++++++++++++++--- AyCode.Core/Loggers/IAcLoggerBase.cs | 6 +- 5 files changed, 216 insertions(+), 20 deletions(-) create mode 100644 AyCode.Core/Loggers/AcLoggerAdapter.cs diff --git a/AyCode.Core.Server/AyCode.Core.Server.csproj b/AyCode.Core.Server/AyCode.Core.Server.csproj index 54de42a..5364cf2 100644 --- a/AyCode.Core.Server/AyCode.Core.Server.csproj +++ b/AyCode.Core.Server/AyCode.Core.Server.csproj @@ -7,6 +7,7 @@ + diff --git a/AyCode.Core/AyCode.Core.csproj b/AyCode.Core/AyCode.Core.csproj index 6496fa0..0e81e47 100644 --- a/AyCode.Core/AyCode.Core.csproj +++ b/AyCode.Core/AyCode.Core.csproj @@ -14,6 +14,7 @@ + diff --git a/AyCode.Core/Loggers/AcLoggerAdapter.cs b/AyCode.Core/Loggers/AcLoggerAdapter.cs new file mode 100644 index 0000000..f958538 --- /dev/null +++ b/AyCode.Core/Loggers/AcLoggerAdapter.cs @@ -0,0 +1,75 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; + +namespace AyCode.Core.Loggers; + +/// +/// ILoggerProvider implementation that creates logger instances using AcLoggerBase. +/// Since AcLoggerBase now implements ILogger directly, this provider just wraps +/// the base logger with category-specific instances. +/// +public sealed class AcLoggerProvider : ILoggerProvider where TLogger : AcLoggerBase +{ + private readonly Func _loggerFactory; + private readonly ConcurrentDictionary _loggers = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Creates a provider that uses a factory function to create category-specific loggers. + /// + /// Factory function that creates a logger for a given category name. + public AcLoggerProvider(Func loggerFactory) + { + ArgumentNullException.ThrowIfNull(loggerFactory); + _loggerFactory = loggerFactory; + } + + public ILogger CreateLogger(string categoryName) + { + return _loggers.GetOrAdd(categoryName, _loggerFactory); + } + + public void Dispose() + { + _loggers.Clear(); + } +} + +/// +/// Extension methods for registering AcLogger with Microsoft's DI and logging infrastructure. +/// +public static class AcLoggerExtensions +{ + /// + /// Adds AcLogger as a logging provider using a factory function. + /// The factory receives the category name and should return a configured logger instance. + /// + /// + /// + /// builder.Logging.AddAcLogger(categoryName => new MyLogger(categoryName)); + /// + /// + public static ILoggingBuilder AddAcLogger(this ILoggingBuilder builder, Func loggerFactory) + where TLogger : AcLoggerBase + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(loggerFactory); + + builder.AddProvider(new AcLoggerProvider(loggerFactory)); + return builder; + } + + /// + /// Clears default providers and adds only AcLogger. + /// Use this if you want ONLY your logger, not Microsoft's console/debug loggers. + /// + public static ILoggingBuilder UseOnlyAcLogger(this ILoggingBuilder builder, Func loggerFactory) + where TLogger : AcLoggerBase + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(loggerFactory); + + builder.ClearProviders(); + builder.AddProvider(new AcLoggerProvider(loggerFactory)); + return builder; + } +} diff --git a/AyCode.Core/Loggers/AcLoggerBase.cs b/AyCode.Core/Loggers/AcLoggerBase.cs index 70188ac..1a9e10e 100644 --- a/AyCode.Core/Loggers/AcLoggerBase.cs +++ b/AyCode.Core/Loggers/AcLoggerBase.cs @@ -4,7 +4,10 @@ using System.Security.AccessControl; using AyCode.Core.Consts; using AyCode.Core.Enums; using AyCode.Utils.Extensions; +using Microsoft.Extensions.Logging; using static System.Net.Mime.MediaTypeNames; +using AcLogLevel = AyCode.Core.Loggers.LogLevel; +using MsLogLevel = Microsoft.Extensions.Logging.LogLevel; namespace AyCode.Core.Loggers; @@ -12,11 +15,18 @@ public abstract class AcLoggerBase : IAcLoggerBase { protected readonly List LogWriters = []; - public LogLevel LogLevel { get; set; } = LogLevel.Error; + public AcLogLevel LogLevel { get; set; } = AcLogLevel.Error; public AppType AppType { get; set; } = AppType.Server; public string? CategoryName { get; set; } + /// + /// If true, long category names (e.g., "Microsoft.AspNetCore.SignalR.HubConnectionHandler") + /// will be shortened to just the class name (e.g., "HubConnectionHandler"). + /// Default is true for consistency with typeof(T).Name usage. + /// + public bool ShortenCategoryNames { get; set; } = true; + protected AcLoggerBase() : this(null) { } @@ -26,7 +36,7 @@ public abstract class AcLoggerBase : IAcLoggerBase CategoryName = categoryName ?? "..."; AppType = AcEnv.AppConfiguration.GetEnum("AyCode:Logger:AppType"); - LogLevel = AcEnv.AppConfiguration.GetEnum("AyCode:Logger:LogLevel"); + LogLevel = AcEnv.AppConfiguration.GetEnum("AyCode:Logger:LogLevel"); foreach (var logWriterSection in AcEnv.AppConfiguration.GetSection("AyCode:Logger:LogWriters").GetChildren()) { @@ -41,7 +51,7 @@ public abstract class AcLoggerBase : IAcLoggerBase continue; } - var logWriterLogLevel = logWriterSection.GetEnum("LogLevel"); + var logWriterLogLevel = logWriterSection.GetEnum("LogLevel"); if (Activator.CreateInstance(logWriterType, AppType, logWriterLogLevel, CategoryName) is IAcLogWriterBase logWriter) LogWriters.Add(logWriter); else Console.Error.WriteLine($"{GetType().Name}; Can't create logWriterType instance; logWriterType: {logWriterType?.AssemblyQualifiedName};"); @@ -54,11 +64,11 @@ public abstract class AcLoggerBase : IAcLoggerBase } protected AcLoggerBase(string? categoryName, params IAcLogWriterBase[] logWriters) : - this(AcEnv.AppConfiguration.GetEnum("AyCode:Logger:AppType"), AcEnv.AppConfiguration.GetEnum("AyCode:Logger:LogLevel"), categoryName, logWriters) + this(AcEnv.AppConfiguration.GetEnum("AyCode:Logger:AppType"), AcEnv.AppConfiguration.GetEnum("AyCode:Logger:LogLevel"), categoryName, logWriters) { } - protected AcLoggerBase(AppType appType, LogLevel logLevel, string? categoryName, params IAcLogWriterBase[] logWriters) + protected AcLoggerBase(AppType appType, AcLogLevel logLevel, string? categoryName, params IAcLogWriterBase[] logWriters) { AppType = appType; LogLevel = logLevel; @@ -72,9 +82,11 @@ public abstract class AcLoggerBase : IAcLoggerBase public List GetWriters => [.. LogWriters]; public TLogWriter? Writer() where TLogWriter : IAcLogWriterBase => LogWriters.OfType().FirstOrDefault(); + #region IAcLogWriterBase Implementation + public virtual void Detail(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null) { - if (LogLevel <= LogLevel.Detail) LogWriters.ForEach(x => x.Detail(text, categoryName ?? CategoryName, memberName)); + if (LogLevel <= AcLogLevel.Detail) LogWriters.ForEach(x => x.Detail(text, categoryName ?? CategoryName, memberName)); } [Conditional("DEBUG")] @@ -83,7 +95,7 @@ public abstract class AcLoggerBase : IAcLoggerBase public virtual void Debug(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null) { - if (LogLevel <= LogLevel.Debug) LogWriters.ForEach(x => x.Debug(text, categoryName ?? CategoryName, memberName)); + if (LogLevel <= AcLogLevel.Debug) LogWriters.ForEach(x => x.Debug(text, categoryName ?? CategoryName, memberName)); } [Conditional("DEBUG")] @@ -92,7 +104,7 @@ public abstract class AcLoggerBase : IAcLoggerBase public virtual void Info(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null) { - if (LogLevel <= LogLevel.Info) LogWriters.ForEach(x => x.Info(text, categoryName ?? CategoryName, memberName)); + if (LogLevel <= AcLogLevel.Info) LogWriters.ForEach(x => x.Info(text, categoryName ?? CategoryName, memberName)); } [Conditional("DEBUG")] @@ -101,7 +113,7 @@ public abstract class AcLoggerBase : IAcLoggerBase public virtual void Warning(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null) { - if (LogLevel <= LogLevel.Warning) LogWriters.ForEach(x => x.Warning(text, categoryName ?? CategoryName, memberName)); + if (LogLevel <= AcLogLevel.Warning) LogWriters.ForEach(x => x.Warning(text, categoryName ?? CategoryName, memberName)); } [Conditional("DEBUG")] @@ -110,7 +122,7 @@ public abstract class AcLoggerBase : IAcLoggerBase public virtual void Suggest(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null) { - if (LogLevel <= LogLevel.Suggest) LogWriters.ForEach(x => x.Suggest(text, categoryName ?? CategoryName, memberName)); + if (LogLevel <= AcLogLevel.Suggest) LogWriters.ForEach(x => x.Suggest(text, categoryName ?? CategoryName, memberName)); } [Conditional("DEBUG")] @@ -119,37 +131,36 @@ public abstract class AcLoggerBase : IAcLoggerBase public virtual void Error(string? text, Exception? ex = null, string? categoryName = null, [CallerMemberName] string? memberName = null) { - if (LogLevel <= LogLevel.Error) LogWriters.ForEach(x => x.Error(text, ex, categoryName ?? CategoryName, memberName)); + if (LogLevel <= AcLogLevel.Error) LogWriters.ForEach(x => x.Error(text, ex, categoryName ?? CategoryName, memberName)); } [Conditional("DEBUG")] public void ErrorConditional(string? text, Exception? ex = null, string? categoryName = null, [CallerMemberName] string? memberName = null) => Error(text, ex, categoryName, memberName); - public void Write(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName) + public void Write(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName) => Write(appType, logLevel, logText, callerMemberName, categoryName, null, null); [Conditional("DEBUG")] - public void WriteConditional(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName) + public void WriteConditional(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName) => Write(appType, logLevel, logText, callerMemberName, categoryName, null, null); - public void Write(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, Exception? ex) + public void Write(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, Exception? ex) => Write(appType, logLevel, logText, callerMemberName, categoryName, ex?.GetType().Name, ex?.ToString()); [Conditional("DEBUG")] - public void WriteConditional(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, Exception? ex) + public void WriteConditional(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, Exception? ex) => Write(appType, logLevel, logText, callerMemberName, categoryName, ex); - public virtual void Write(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, string? errorType, string? exMessage) + public virtual void Write(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, string? errorType, string? exMessage) { if (LogLevel <= logLevel) LogWriters.ForEach(x => x.Write(appType, logLevel, logText, callerMemberName, categoryName ?? CategoryName, errorType, exMessage)); } [Conditional("DEBUG")] - public void WriteConditional(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, string? errorType, string? exMessage) + public void WriteConditional(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, string? errorType, string? exMessage) => Write(appType, logLevel, logText, callerMemberName, categoryName, errorType, exMessage); - public void Write(IAcLogItemClient logItem) { if (LogLevel <= logItem.LogLevel) LogWriters.ForEach(x => x.Write(logItem)); @@ -157,4 +168,110 @@ public abstract class AcLoggerBase : IAcLoggerBase [Conditional("DEBUG")] public void WriteConditional(IAcLogItemClient logItem) => Write(logItem); + + #endregion + + #region ILogger Implementation + + /// + /// ILogger.BeginScope - AcLoggerBase doesn't support scopes, returns no-op disposable. + /// + public IDisposable? BeginScope(TState state) where TState : notnull => NullScope.Instance; + + /// + /// ILogger.IsEnabled - Checks if the specified Microsoft log level is enabled. + /// + public bool IsEnabled(MsLogLevel logLevel) + { + var acLogLevel = MapFromMsLogLevel(logLevel); + return LogLevel <= acLogLevel; + } + + /// + /// ILogger.Log - Main logging method called by Microsoft services. + /// + public void Log(MsLogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + return; + + var message = formatter(state, exception); + + if (string.IsNullOrEmpty(message) && exception == null) + return; + + var fullMessage = eventId.Id != 0 + ? $"[{eventId.Id}:{eventId.Name}] {message}" + : message; + + var category = ShortenCategoryNames ? GetShortCategoryName(CategoryName) : CategoryName; + + switch (logLevel) + { + case MsLogLevel.Trace: + Detail(fullMessage, category); + break; + case MsLogLevel.Debug: + Debug(fullMessage, category); + break; + case MsLogLevel.Information: + Info(fullMessage, category); + break; + case MsLogLevel.Warning: + Warning(fullMessage, category); + break; + case MsLogLevel.Error: + Error(fullMessage, exception, category); + break; + case MsLogLevel.Critical: + Error($"[CRITICAL] {fullMessage}", exception, category); + break; + case MsLogLevel.None: + default: + break; + } + } + + /// + /// Shortens a fully qualified type name to just the class name. + /// E.g., "Microsoft.AspNetCore.SignalR.HubConnectionHandler" -> "HubConnectionHandler" + /// + private static string? GetShortCategoryName(string? categoryName) + { + if (string.IsNullOrEmpty(categoryName)) + return categoryName; + + var lastDot = categoryName.LastIndexOf('.'); + return lastDot >= 0 ? categoryName[(lastDot + 1)..] : categoryName; + } + + /// + /// Maps Microsoft.Extensions.Logging.LogLevel to AyCode.Core.Loggers.LogLevel. + /// + private static AcLogLevel MapFromMsLogLevel(MsLogLevel msLogLevel) + { + return msLogLevel switch + { + MsLogLevel.Trace => AcLogLevel.Detail, + MsLogLevel.Debug => AcLogLevel.Debug, + MsLogLevel.Information => AcLogLevel.Info, + MsLogLevel.Warning => AcLogLevel.Warning, + MsLogLevel.Error => AcLogLevel.Error, + MsLogLevel.Critical => AcLogLevel.Error, + MsLogLevel.None => AcLogLevel.Disabled, + _ => AcLogLevel.Info + }; + } + + /// + /// No-op scope implementation for ILogger.BeginScope. + /// + private sealed class NullScope : IDisposable + { + public static NullScope Instance { get; } = new(); + private NullScope() { } + public void Dispose() { } + } + + #endregion } \ No newline at end of file diff --git a/AyCode.Core/Loggers/IAcLoggerBase.cs b/AyCode.Core/Loggers/IAcLoggerBase.cs index 16a5715..d558300 100644 --- a/AyCode.Core/Loggers/IAcLoggerBase.cs +++ b/AyCode.Core/Loggers/IAcLoggerBase.cs @@ -1,6 +1,8 @@ -namespace AyCode.Core.Loggers; +using Microsoft.Extensions.Logging; -public interface IAcLoggerBase : IAcLogWriterBase +namespace AyCode.Core.Loggers; + +public interface IAcLoggerBase : IAcLogWriterBase, ILogger { public List GetWriters { get; } public TLogWriter? Writer() where TLogWriter : IAcLogWriterBase;