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;