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;