AyCode.Core/AyCode.Core/Loggers/AcLoggerBase.cs

333 lines
14 KiB
C#

using System.Diagnostics;
using System.Runtime.CompilerServices;
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;
public abstract class AcLoggerBase : IAcLoggerBase
{
protected readonly List<IAcLogWriterBase> LogWriters = [];
public AcLogLevel LogLevel { get; set; } = AcLogLevel.Error;
public AppType AppType { get; set; } = AppType.Server;
public string? CategoryName { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool ShortenCategoryNames { get; set; } = true;
/// <summary>
/// If true, ILogger bridge calls without an EventId name resolve the real caller from the stack trace
/// (async state machines unwrapped) instead of showing "Log". Costs a few µs per enabled bridge log;
/// named AC methods (Debug/Info/...) are unaffected. Set false to restore the plain "Log" fallback.
/// </summary>
public bool ResolveBridgeCallerNames { get; set; } = true;
protected AcLoggerBase() : this(null)
{
}
protected AcLoggerBase(string? categoryName)
{
CategoryName = categoryName ?? "...";
AppType = AcEnv.AppConfiguration.GetEnum<AppType>("AyCode:Logger:AppType");
LogLevel = AcEnv.AppConfiguration.GetEnum<AcLogLevel>("AyCode:Logger:LogLevel");
foreach (var logWriterSection in AcEnv.AppConfiguration.GetSection("AyCode:Logger:LogWriters").GetChildren())
{
try
{
Type? logWriterType;
var logWriterTypeString = logWriterSection["LogWriterType"];
if (logWriterTypeString.IsNullOrWhiteSpace()|| (logWriterType = Type.GetType(logWriterTypeString)) == null)
{
Console.Error.WriteLine($"{GetType().Name}; logWriterTypeString not valid; logWriterTypeString: {logWriterTypeString};");
continue;
}
var logWriterLogLevel = logWriterSection.GetEnum<AcLogLevel>("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};");
}
catch (Exception ex)
{
Console.Error.WriteLine($"{GetType().Name}; {ex}");
}
}
}
protected AcLoggerBase(string? categoryName, params IAcLogWriterBase[] logWriters) :
this(AcEnv.AppConfiguration.GetEnum<AppType>("AyCode:Logger:AppType"), AcEnv.AppConfiguration.GetEnum<AcLogLevel>("AyCode:Logger:LogLevel"), categoryName, logWriters)
{
}
protected AcLoggerBase(AppType appType, AcLogLevel logLevel, string? categoryName, params IAcLogWriterBase[] logWriters)
{
AppType = appType;
LogLevel = logLevel;
CategoryName = categoryName ?? "...";
foreach (var acLogWriterBase in logWriters)
LogWriters.Add(acLogWriterBase);
}
public List<IAcLogWriterBase> GetWriters => [.. LogWriters];
public TLogWriter? Writer<TLogWriter>() where TLogWriter : IAcLogWriterBase => LogWriters.OfType<TLogWriter>().FirstOrDefault();
#region IAcLogWriterBase Implementation
public virtual void Detail(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
{
if (LogLevel <= AcLogLevel.Detail) LogWriters.ForEach(x => x.Detail(text, categoryName ?? CategoryName, memberName));
}
[Conditional("DEBUG")]
public void DetailConditional(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
=> Detail(text, categoryName, memberName);
public virtual void Debug(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
{
if (LogLevel <= AcLogLevel.Debug) LogWriters.ForEach(x => x.Debug(text, categoryName ?? CategoryName, memberName));
}
[Conditional("DEBUG")]
public void DebugConditional(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
=> Debug(text, categoryName, memberName);
public virtual void Info(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
{
if (LogLevel <= AcLogLevel.Info) LogWriters.ForEach(x => x.Info(text, categoryName ?? CategoryName, memberName));
}
[Conditional("DEBUG")]
public void InfoConditional(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
=> Info(text, categoryName, memberName);
public virtual void Warning(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
{
if (LogLevel <= AcLogLevel.Warning) LogWriters.ForEach(x => x.Warning(text, categoryName ?? CategoryName, memberName));
}
[Conditional("DEBUG")]
public void WarningConditional(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
=> Warning(text, categoryName, memberName);
public virtual void Suggest(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
{
if (LogLevel <= AcLogLevel.Suggest) LogWriters.ForEach(x => x.Suggest(text, categoryName ?? CategoryName, memberName));
}
[Conditional("DEBUG")]
public void SuggestConditional(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
=> Suggest(text, categoryName, memberName);
public virtual void Error(string? text, Exception? ex = null, string? categoryName = null, [CallerMemberName] string? memberName = null)
{
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, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName)
=> Write(appType, logLevel, logText, callerMemberName, categoryName, null, null);
[Conditional("DEBUG")]
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, 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, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, Exception? ex)
=> Write(appType, logLevel, logText, callerMemberName, categoryName, ex);
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, 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));
}
[Conditional("DEBUG")]
public void WriteConditional(IAcLogItemClient logItem) => Write(logItem);
#endregion
#region ILogger Implementation
/// <summary>
/// ILogger.BeginScope - AcLoggerBase doesn't support scopes, returns no-op disposable.
/// </summary>
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
/// <summary>
/// ILogger.IsEnabled - Checks if the specified Microsoft log level is enabled.
/// </summary>
public bool IsEnabled(MsLogLevel logLevel)
{
var acLogLevel = MapFromMsLogLevel(logLevel);
return LogLevel <= acLogLevel;
}
/// <summary>
/// ILogger.Log - Main logging method called by Microsoft services.
/// </summary>
public void Log<TState>(MsLogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> 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 fullMessage = message;
var category = ShortenCategoryNames ? GetShortCategoryName(CategoryName) : CategoryName;
// Explicit EventId name wins; otherwise resolve the real caller from the stack (opt-out: ResolveBridgeCallerNames).
// EventId.Id 0 means "no id" — show the bare name without the ":0" suffix.
var memberName = !string.IsNullOrEmpty(eventId.Name)
? eventId.Id != 0 ? $"{eventId.Name}:{eventId.Id}" : eventId.Name
: ResolveBridgeCallerNames ? ResolveCallerFromStackTrace() : "Log";
switch (logLevel)
{
case MsLogLevel.Trace:
Detail(fullMessage, category, memberName);
break;
case MsLogLevel.Debug:
Debug(fullMessage, category, memberName);
break;
case MsLogLevel.Information:
Info(fullMessage, category, memberName);
break;
case MsLogLevel.Warning:
Warning(fullMessage, category, memberName);
break;
case MsLogLevel.Error:
Error(fullMessage, exception, category, memberName);
break;
case MsLogLevel.Critical:
Error($"[CRITICAL] {fullMessage}", exception, category, memberName);
break;
case MsLogLevel.None:
default:
break;
}
}
/// <summary>
/// Shortens a fully qualified type name to just the class name.
/// E.g., "Microsoft.AspNetCore.SignalR.HubConnectionHandler" -> "HubConnectionHandler"
/// </summary>
private static string? GetShortCategoryName(string? categoryName)
{
if (string.IsNullOrEmpty(categoryName))
return categoryName;
var lastDot = categoryName.LastIndexOf('.');
return lastDot >= 0 ? categoryName[(lastDot + 1)..] : categoryName;
}
/// <summary>
/// Finds the first stack frame outside the logging infrastructure so ILogger bridge calls
/// (LogDebug/LogInformation/... extensions — these cannot carry CallerMemberName) still show
/// the real caller. Async state machines and lambdas are unwrapped (&lt;Method&gt;d__N.MoveNext -> Method).
/// Only runs when the level is enabled and no EventId name was provided.
/// </summary>
private static string ResolveCallerFromStackTrace()
{
var stackTrace = new StackTrace(skipFrames: 1, fNeedFileInfo: false);
var frameCount = Math.Min(stackTrace.FrameCount, 15);
for (var i = 0; i < frameCount; i++)
{
var method = stackTrace.GetFrame(i)?.GetMethod();
var declaringType = method?.DeclaringType;
if (method == null || declaringType == null) continue;
if (declaringType.Namespace?.StartsWith("Microsoft.Extensions.Logging", StringComparison.Ordinal) == true) continue;
if (typeof(AcLoggerBase).IsAssignableFrom(declaringType)) continue;
// Async state machine: compiler-generated "<Method>d__N" type, method is MoveNext.
var typeName = declaringType.Name;
if (typeName.Length > 2 && typeName[0] == '<')
{
var end = typeName.IndexOf('>');
if (end > 1) return typeName[1..end];
}
// Lambda / local function: "<Caller>b__0"-style method name — unwrap to the containing method.
var methodName = method.Name;
if (methodName.Length > 2 && methodName[0] == '<')
{
var end = methodName.IndexOf('>');
if (end > 1) return methodName[1..end];
}
return methodName;
}
return "Log";
}
/// <summary>
/// Maps Microsoft.Extensions.Logging.LogLevel to AyCode.Core.Loggers.LogLevel.
/// </summary>
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
};
}
/// <summary>
/// No-op scope implementation for ILogger.BeginScope.
/// </summary>
private sealed class NullScope : IDisposable
{
public static NullScope Instance { get; } = new();
private NullScope() { }
public void Dispose() { }
}
#endregion
}