333 lines
14 KiB
C#
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 (<Method>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
|
|
} |