初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
namespace PCL.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// 事件/意外行为等级。
|
||||
/// </summary>
|
||||
public enum ActionLevel
|
||||
{
|
||||
TraceLog = 00,
|
||||
NormalLog = 10,
|
||||
Hint = 20,
|
||||
HintErr = 21,
|
||||
MsgBox = 30,
|
||||
MsgBoxErr = 31,
|
||||
MsgBoxFatal = 32,
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using PCL.Core.App.Localization;
|
||||
|
||||
namespace PCL.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// 将异常信息格式化为面向用户的消息,同时不改变原始的失败原因。
|
||||
/// </summary>
|
||||
public static partial class ExceptionDetails
|
||||
{
|
||||
private const int MaxSingleMessageLength = 4096;
|
||||
private const int MaxCombinedMessageLength = 8192;
|
||||
|
||||
/// <summary>
|
||||
/// 返回异常链中去重后的消息,从最外层异常依次到内部异常。
|
||||
/// </summary>
|
||||
public static string GetUserReason(Exception exception)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(exception);
|
||||
|
||||
var messages = new List<string>();
|
||||
var seenMessages = new HashSet<string>(StringComparer.Ordinal);
|
||||
var current = exception;
|
||||
|
||||
// 异常链通常很短。这里的深度限制也能避免格式异常的链破坏 UI 展示。
|
||||
for (var depth = 0; current is not null && depth < 32; depth++, current = current.InnerException)
|
||||
{
|
||||
var message = current.Message?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(message) || string.Equals(message, "$$", StringComparison.Ordinal))
|
||||
continue;
|
||||
message = _TruncateForUser(_RedactSensitiveText(message));
|
||||
if (seenMessages.Add(message))
|
||||
messages.Add(message);
|
||||
}
|
||||
|
||||
var result = string.Join(Environment.NewLine, messages);
|
||||
return _TruncateForUser(result, MaxCombinedMessageLength);
|
||||
}
|
||||
|
||||
private static string _TruncateForUser(string text, int? maxLength = null)
|
||||
{
|
||||
var limit = maxLength ?? MaxSingleMessageLength;
|
||||
return text.Length <= limit
|
||||
? text
|
||||
: text[..limit] + Environment.NewLine + "…";
|
||||
}
|
||||
|
||||
[GeneratedRegex(
|
||||
"""(\b(?:access[_-]?token|refresh[_-]?token|client[_-]?secret|password|authorization)\b\s*[:=]\s*)(?:"[^"]*"|\S+)""",
|
||||
RegexOptions.IgnoreCase)]
|
||||
private static partial Regex _KeyValueCredentialPattern();
|
||||
|
||||
[GeneratedRegex(
|
||||
@"([?&](?:access_token|refresh_token|token|password|client_secret)=)[^&\s]+",
|
||||
RegexOptions.IgnoreCase)]
|
||||
private static partial Regex _QueryStringCredentialPattern();
|
||||
|
||||
[GeneratedRegex(
|
||||
@"\bBearer\s+[A-Za-z0-9._~+/=-]+",
|
||||
RegexOptions.IgnoreCase)]
|
||||
private static partial Regex _BearerTokenPattern();
|
||||
|
||||
private static string _RedactSensitiveText(string text)
|
||||
{
|
||||
// 在保留失败原因可见的前提下,避免在普通用户界面中直接暴露明显的凭据信息。
|
||||
text = _KeyValueCredentialPattern().Replace(text, "$1[redacted]");
|
||||
text = _QueryStringCredentialPattern().Replace(text, "$1[redacted]");
|
||||
text = _BearerTokenPattern().Replace(text, "Bearer [redacted]");
|
||||
return text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回完整的异常文本,用于调试和日志记录。
|
||||
/// </summary>
|
||||
public static string GetDebugDetails(Exception exception)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(exception);
|
||||
return exception.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将完整的原始异常文本附加到一个稳定的本地化摘要后面。
|
||||
/// </summary>
|
||||
public static string Compose(string summary, Exception? exception = null)
|
||||
{
|
||||
return Compose(summary, exception is null ? null : GetDebugDetails(exception));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将调用方提供的详细文本附加到稳定的本地化摘要后面,而不修改该详细文本。
|
||||
/// </summary>
|
||||
public static string Compose(string summary, string? details)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(summary);
|
||||
|
||||
return string.IsNullOrWhiteSpace(details)
|
||||
? summary
|
||||
: Lang.Text("SystemDialog.Error.Detail.WithException", summary.TrimEnd(), details);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PCL.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// 日志等级。将十进制值 <c>% 100</c> 并显式转换可得到该等级默认对应的 <see cref="ActionLevel"/>
|
||||
/// </summary>
|
||||
public enum LogLevel
|
||||
{
|
||||
Trace = 000 + ActionLevel.TraceLog,
|
||||
Debug = 100 + ActionLevel.NormalLog,
|
||||
Info = 200 + ActionLevel.NormalLog,
|
||||
#if TRACE
|
||||
Warning = 300 + ActionLevel.HintErr,
|
||||
Error = 400 + ActionLevel.MsgBoxErr,
|
||||
#else
|
||||
Warning = 300 + ActionLevel.NormalLog,
|
||||
Error = 400 + ActionLevel.HintErr,
|
||||
#endif
|
||||
Fatal = 500 + ActionLevel.MsgBoxFatal,
|
||||
}
|
||||
|
||||
public static class LogLevelExtensions
|
||||
{
|
||||
private static readonly Dictionary<LogLevel, string> _LevelNameMap = new()
|
||||
{
|
||||
[LogLevel.Trace] = "TRA",
|
||||
[LogLevel.Debug] = "DBG",
|
||||
[LogLevel.Info] = "INFO",
|
||||
[LogLevel.Warning] = "WARN",
|
||||
[LogLevel.Error] = "ERR!",
|
||||
[LogLevel.Fatal] = "FTL!"
|
||||
};
|
||||
|
||||
extension(LogLevel level)
|
||||
{
|
||||
public string PrintName() => _LevelNameMap[level];
|
||||
public ActionLevel DefaultActionLevel() => (ActionLevel)((int)level % 100);
|
||||
|
||||
public LogLevel RealLevel() => (int)level switch
|
||||
{
|
||||
< 100 => LogLevel.Trace,
|
||||
< 200 => LogLevel.Debug,
|
||||
< 300 => LogLevel.Info,
|
||||
< 400 => LogLevel.Warning,
|
||||
< 500 => LogLevel.Error,
|
||||
_ => LogLevel.Fatal,
|
||||
};
|
||||
|
||||
public int Header() => (int)level / 100 * 100;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Essentials;
|
||||
using PCL.Core.App.IoC;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.UI;
|
||||
|
||||
namespace PCL.Core.Logging;
|
||||
|
||||
[LifecycleService(LifecycleState.Loading, Priority = int.MaxValue)]
|
||||
public class LogService : ILifecycleLogService
|
||||
{
|
||||
private static LifecycleContext? _context;
|
||||
|
||||
private static Logger? _logger;
|
||||
|
||||
private static bool _wrapperRegistered;
|
||||
|
||||
private LogService()
|
||||
{
|
||||
_context = Lifecycle.GetContext(this);
|
||||
}
|
||||
|
||||
private static LifecycleContext Context => _context!;
|
||||
public static Logger Logger => _logger!;
|
||||
public string Identifier => "log";
|
||||
public string Name => "日志服务";
|
||||
public bool SupportAsync => false;
|
||||
|
||||
public Task StartAsync()
|
||||
{
|
||||
Context.Trace("正在初始化 Logger 实例");
|
||||
var config = new LoggerConfiguration(Path.Combine(Basics.ExecutableDirectory, "PCL", "Log"));
|
||||
_logger = new Logger(config);
|
||||
Context.Trace("正在注册日志事件");
|
||||
LogWrapper.OnLog += _OnWrapperLog;
|
||||
_wrapperRegistered = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task StopAsync()
|
||||
{
|
||||
if (_wrapperRegistered)
|
||||
LogWrapper.OnLog -= _OnWrapperLog;
|
||||
if (_logger is not null)
|
||||
await _logger.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void OnLog(LifecycleLogItem item)
|
||||
{
|
||||
_LogAction(item.Level, item.ActionLevel, item.ComposeMessage(), item.Message, item.Exception);
|
||||
}
|
||||
|
||||
private static void _LogAction(
|
||||
LogLevel level,
|
||||
ActionLevel actionLevel,
|
||||
string formatted,
|
||||
string plain,
|
||||
Exception? ex)
|
||||
{
|
||||
if (ex is not null)
|
||||
TelemetryService.ReportException(ex, plain, level);
|
||||
|
||||
// log
|
||||
#if !TRACE
|
||||
if (actionLevel != ActionLevel.TraceLog)
|
||||
#endif
|
||||
Logger.Log(formatted);
|
||||
|
||||
switch (actionLevel)
|
||||
{
|
||||
case <= ActionLevel.NormalLog:
|
||||
return;
|
||||
|
||||
// hint
|
||||
case ActionLevel.Hint or ActionLevel.HintErr:
|
||||
HintWrapper.Show(
|
||||
plain,
|
||||
actionLevel == ActionLevel.Hint ? HintTheme.Info : HintTheme.Error);
|
||||
break;
|
||||
|
||||
// message box
|
||||
case ActionLevel.MsgBox or ActionLevel.MsgBoxErr:
|
||||
{
|
||||
var theme = actionLevel == ActionLevel.MsgBoxErr
|
||||
? MsgBoxTheme.Error
|
||||
: MsgBoxTheme.Info;
|
||||
|
||||
var caption = ex is null
|
||||
? null
|
||||
: Lang.Text("SystemDialog.Error.Unexpected.Title");
|
||||
|
||||
var message = _ComposeUserError(plain, ex);
|
||||
|
||||
if (actionLevel == ActionLevel.MsgBoxErr)
|
||||
message = Lang.Text("SystemDialog.Error.Message.WithLogExportGuidance", message);
|
||||
|
||||
MsgBoxWrapper.Show(message, caption, theme, false);
|
||||
break;
|
||||
}
|
||||
|
||||
// fatal message box
|
||||
case ActionLevel.MsgBoxFatal:
|
||||
{
|
||||
var message = Lang.Text(
|
||||
"SystemDialog.Fatal.Message.WithFeedbackGuidance",
|
||||
_ComposeUserError(plain, ex));
|
||||
|
||||
MessageBox.Show(
|
||||
message,
|
||||
Lang.Text("SystemDialog.Fatal.Title"),
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static string _ComposeUserError(string plain, Exception? exception)
|
||||
{
|
||||
var summary = string.IsNullOrWhiteSpace(plain)
|
||||
? Lang.Text("SystemDialog.Error.Unexpected.Message")
|
||||
: plain;
|
||||
|
||||
return exception is null
|
||||
? summary
|
||||
: ExceptionDetails.Compose(summary, exception);
|
||||
}
|
||||
|
||||
private static void _OnWrapperLog(
|
||||
LogLevel level,
|
||||
string msg,
|
||||
string? module,
|
||||
Exception? ex)
|
||||
{
|
||||
var thread = Thread.CurrentThread.Name ?? $"#{Environment.CurrentManagedThreadId}";
|
||||
if (module is not null) module = $"[{module}] ";
|
||||
var result = $"[{DateTime.Now:HH:mm:ss.fff}] [{level.PrintName()}] [{thread}] {module}{msg}";
|
||||
_LogAction(
|
||||
level,
|
||||
level.DefaultActionLevel(),
|
||||
ex is null
|
||||
? result
|
||||
: $"{result}\n{ex}",
|
||||
msg,
|
||||
ex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
|
||||
namespace PCL.Core.Logging;
|
||||
|
||||
public delegate void LogHandler(LogLevel level, string msg, string? module = null, Exception? ex = null);
|
||||
|
||||
public static class LogWrapper
|
||||
{
|
||||
public static event LogHandler? OnLog;
|
||||
|
||||
// Fatal: can handle exceptions
|
||||
public static void Fatal(Exception? ex, string? module, string msg) => OnLog?.Invoke(LogLevel.Fatal, msg, module, ex);
|
||||
public static void Fatal(Exception? ex, string msg) => Fatal(ex, null, msg);
|
||||
public static void Fatal(string? module, string msg) => Fatal(null, module, msg);
|
||||
public static void Fatal(string msg) => Fatal((string?)null, msg);
|
||||
|
||||
// Error: can handle exceptions
|
||||
public static void Error(Exception? ex, string? module, string msg) => OnLog?.Invoke(LogLevel.Error, msg, module, ex);
|
||||
public static void Error(Exception? ex, string msg) => Error(ex, null, msg);
|
||||
public static void Error(string? module, string msg) => Error(null, module, msg);
|
||||
public static void Error(string msg) => Error((string?)null, msg);
|
||||
|
||||
// Warn: can handle exceptions
|
||||
public static void Warn(Exception? ex, string? module, string msg) => OnLog?.Invoke(LogLevel.Warning, msg, module, ex);
|
||||
public static void Warn(Exception? ex, string msg) => Warn(ex, null, msg);
|
||||
public static void Warn(string? module, string msg) => Warn(null, module, msg);
|
||||
public static void Warn(string msg) => Warn((string?)null, msg);
|
||||
|
||||
// Info
|
||||
public static void Info(string? module, string msg) => OnLog?.Invoke(LogLevel.Info, msg, module);
|
||||
public static void Info(string msg) => Info(null, msg);
|
||||
|
||||
// Debug
|
||||
public static void Debug(string? module, string msg) => OnLog?.Invoke(LogLevel.Debug, msg, module);
|
||||
public static void Debug(string msg) => Debug(null, msg);
|
||||
|
||||
public static void Debug(Exception ex, string module, string message) => Debug(module, $"{message}: {ex.ToString()}");
|
||||
|
||||
// Trace
|
||||
public static void Trace(string? module, string msg) => OnLog?.Invoke(LogLevel.Trace, msg, module);
|
||||
public static void Trace(string msg) => Trace(null, msg);
|
||||
|
||||
public static Logger CurrentLogger => LogService.Logger;
|
||||
|
||||
private static readonly Lazy<LoggerFactoryAdapter> _LoggerFactory = new(static () =>
|
||||
{
|
||||
return new LoggerFactoryAdapter(CurrentLogger);
|
||||
});
|
||||
public static LoggerFactoryAdapter LoggerFactory => _LoggerFactory.Value;
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.Utils.Exts;
|
||||
|
||||
namespace PCL.Core.Logging;
|
||||
|
||||
public sealed class Logger : IAsyncDisposable
|
||||
{
|
||||
public Logger(LoggerConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
_CreateNewFile();
|
||||
_processingTask = _ProcessLogQueueAsync();
|
||||
}
|
||||
// Data stream
|
||||
private StreamWriter? _currentStream;
|
||||
private FileStream? _currentFile;
|
||||
private readonly List<string> _files = [];
|
||||
// Statis
|
||||
private long _droppedCount;
|
||||
public long DroppedLogCount => Interlocked.Read(ref _droppedCount);
|
||||
// Processor
|
||||
private readonly Task _processingTask;
|
||||
private readonly Channel<string> _logChannel = Channel.CreateUnbounded<string>(new UnboundedChannelOptions()
|
||||
{
|
||||
SingleReader = true
|
||||
});
|
||||
|
||||
public ReadOnlyCollection<string> CurrentLogFiles => _files.AsReadOnly();
|
||||
|
||||
public LoggerConfiguration Configuration { get; }
|
||||
|
||||
private void _CreateNewFile()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var nameFormat = (Configuration.FileNameFormat ?? $"Launch-{now.ToString("yyyy-M-d", CultureInfo.InvariantCulture)}-{{0}}") + ".log";
|
||||
var filename = nameFormat.Replace("{0}", now.ToString("HHmmssfff", CultureInfo.InvariantCulture));
|
||||
var filePath = Path.Combine(Configuration.StoreFolder, filename);
|
||||
_files.Add(filePath);
|
||||
var lastWriter = _currentStream;
|
||||
var lastFile = _currentFile;
|
||||
Directory.CreateDirectory(Configuration.StoreFolder);
|
||||
|
||||
_currentFile = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read);
|
||||
_currentStream = new StreamWriter(_currentFile);
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
if (lastWriter is not null)
|
||||
try
|
||||
{
|
||||
await lastWriter.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception) { /* Don't care */ }
|
||||
|
||||
if (lastFile is not null)
|
||||
try
|
||||
{
|
||||
await lastFile.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception) { /* Don't care */ }
|
||||
|
||||
if (!Configuration.AutoDeleteOldFile)
|
||||
return;
|
||||
|
||||
var logFiles = Directory.GetFiles(
|
||||
Configuration.StoreFolder,
|
||||
"*.log",
|
||||
SearchOption.TopDirectoryOnly);
|
||||
var needToDelete = logFiles.Select(x => new FileInfo(x))
|
||||
.OrderBy(x => x.CreationTime)
|
||||
.Take(logFiles.Length - Configuration.MaxKeepOldFile);
|
||||
foreach (var logFile in needToDelete)
|
||||
logFile.Delete();
|
||||
});
|
||||
}
|
||||
|
||||
public void Trace(string message) => Log($"[{_GetTimeFormatted()}] [TRA] {message}");
|
||||
public void Debug(string message) => Log($"[{_GetTimeFormatted()}] [DBG] {message}");
|
||||
public void Info(string message) => Log($"[{_GetTimeFormatted()}] [INFO] {message}");
|
||||
public void Warn(string message) => Log($"[{_GetTimeFormatted()}] [WARN] {message}");
|
||||
public void Error(string message) => Log($"[{_GetTimeFormatted()}] [ERR!] {message}");
|
||||
public void Fatal(string message) => Log($"[{_GetTimeFormatted()}] [FTL!] {message}");
|
||||
|
||||
private static string _GetTimeFormatted() => $"{DateTime.Now:HH:mm:ss.fff}";
|
||||
|
||||
public void Log(string message)
|
||||
{
|
||||
if (_disposed) return;
|
||||
if (!_logChannel.Writer.TryWrite(message))
|
||||
{
|
||||
Interlocked.Increment(ref _droppedCount);
|
||||
Console.WriteLine($"Log dropped error: {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task _ProcessLogQueueAsync()
|
||||
{
|
||||
const int maxBatchLines = 198;
|
||||
var writeTimeout = TimeSpan.FromMilliseconds(325);
|
||||
var batch = new StringBuilder(4096);
|
||||
var lineCount = 0u;
|
||||
var lastFlush = Stopwatch.GetTimestamp();
|
||||
|
||||
try
|
||||
{
|
||||
while (!_disposed || _logChannel.Reader.TryPeek(out _))
|
||||
{
|
||||
if (_logChannel.Reader.TryRead(out var message))
|
||||
{
|
||||
#if DEBUG
|
||||
message = message.ReplaceLineBreak("\r\n");
|
||||
Console.WriteLine(message);
|
||||
System.Diagnostics.Debug.WriteLine(message);
|
||||
#endif
|
||||
batch.AppendLine(message);
|
||||
lineCount++;
|
||||
|
||||
var elapsed = Stopwatch.GetElapsedTime(lastFlush);
|
||||
if (lineCount >= maxBatchLines || elapsed > writeTimeout)
|
||||
{
|
||||
await DoRefreshAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (lineCount != 0)
|
||||
{
|
||||
await DoRefreshAsync().ConfigureAwait(false);
|
||||
}
|
||||
await Task.Delay(80).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
async Task DoRefreshAsync()
|
||||
{
|
||||
await _DoWriteAsync(batch).ConfigureAwait(false);
|
||||
batch.Clear();
|
||||
lineCount = 0;
|
||||
lastFlush = Stopwatch.GetTimestamp();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// 出错了先干到标准输出流中吧 Orz
|
||||
Console.WriteLine($"[{_GetTimeFormatted()}] [ERROR] An error occurred while processing log queue: {e.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task _DoWriteAsync(StringBuilder ctx)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_currentFile?.Length >= Configuration.MaxFileSize)
|
||||
{
|
||||
_CreateNewFile();
|
||||
}
|
||||
await _currentStream!.WriteAsync(ctx).ConfigureAwait(false);
|
||||
await _currentStream.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"[{_GetTimeFormatted()}] [ERROR] An error occurred while writing log file: {e.Message}");
|
||||
await File.AppendAllTextAsync(Path.Combine(Configuration.StoreFolder, "Error.log"), $"[{_GetTimeFormatted}] LogCycle Error: {e}\n");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
_logChannel.Writer.Complete();
|
||||
await _processingTask.ConfigureAwait(false);
|
||||
|
||||
if (_currentStream is not null)
|
||||
await _currentStream.DisposeAsync().ConfigureAwait(false);
|
||||
if (_currentFile is not null)
|
||||
await _currentFile.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace PCL.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Microsoft.Extensions.Logging.ILogger 适配器
|
||||
/// 将现有的 Logger 包装为标准的 ILogger 接口实现
|
||||
/// </summary>
|
||||
public class LoggerAdapter(Logger logger, string categoryName) : ILogger
|
||||
{
|
||||
private readonly Logger _innerLogger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
private readonly string _categoryName = categoryName ?? throw new ArgumentNullException(nameof(categoryName));
|
||||
|
||||
private static readonly AsyncLocal<Stack<object>> _ScopeStack = new();
|
||||
|
||||
IDisposable ILogger.BeginScope<TState>(TState state)
|
||||
{
|
||||
_ScopeStack.Value ??= new Stack<object>();
|
||||
_ScopeStack.Value.Push(state);
|
||||
return new ScopeDisposable(state);
|
||||
}
|
||||
|
||||
#pragma warning disable CS9113 // 参数未读。
|
||||
private class ScopeDisposable(object state) : IDisposable
|
||||
{
|
||||
private bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (_ScopeStack.Value is { Count: > 0 })
|
||||
{
|
||||
#if DEBUG
|
||||
var popped = _ScopeStack.Value.Pop();
|
||||
if (!ReferenceEquals(popped, state))
|
||||
{
|
||||
throw new InvalidOperationException("Scope disposal order mismatch.");
|
||||
}
|
||||
#else
|
||||
_ = _ScopeStack.Value.Pop();
|
||||
#endif
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel level) => true;
|
||||
|
||||
public void Log<TState>(Microsoft.Extensions.Logging.LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel))
|
||||
return;
|
||||
|
||||
ArgumentNullException.ThrowIfNull(formatter);
|
||||
|
||||
var originalMessage = formatter(state, exception);
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// 类别名称
|
||||
if (!string.IsNullOrEmpty(_categoryName))
|
||||
{
|
||||
sb.Append('[').Append(_categoryName).Append("] ");
|
||||
}
|
||||
|
||||
// 事件 ID
|
||||
if (eventId.Id != 0 || !string.IsNullOrEmpty(eventId.Name))
|
||||
{
|
||||
sb.Append("[EventId:");
|
||||
if (!string.IsNullOrEmpty(eventId.Name))
|
||||
{
|
||||
sb.Append(eventId.Id).Append(':').Append(eventId.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(eventId.Id);
|
||||
}
|
||||
sb.Append("] ");
|
||||
}
|
||||
|
||||
// 上下文信息
|
||||
var scopeContext = _BuildScopeContext();
|
||||
if (!string.IsNullOrEmpty(scopeContext))
|
||||
{
|
||||
sb.Append('[').Append(_categoryName).Append("] ");
|
||||
}
|
||||
|
||||
sb.Append(originalMessage);
|
||||
var finalMessage = sb.ToString();
|
||||
|
||||
// 日志级别
|
||||
switch (logLevel)
|
||||
{
|
||||
case Microsoft.Extensions.Logging.LogLevel.Trace:
|
||||
_innerLogger.Trace(finalMessage);
|
||||
break;
|
||||
case Microsoft.Extensions.Logging.LogLevel.Debug:
|
||||
_innerLogger.Debug(finalMessage);
|
||||
break;
|
||||
case Microsoft.Extensions.Logging.LogLevel.Information:
|
||||
_innerLogger.Info(finalMessage);
|
||||
break;
|
||||
case Microsoft.Extensions.Logging.LogLevel.Warning:
|
||||
_innerLogger.Warn(finalMessage);
|
||||
break;
|
||||
case Microsoft.Extensions.Logging.LogLevel.Error:
|
||||
_innerLogger.Error(finalMessage);
|
||||
break;
|
||||
case Microsoft.Extensions.Logging.LogLevel.Critical:
|
||||
_innerLogger.Fatal(finalMessage);
|
||||
break;
|
||||
}
|
||||
|
||||
if (exception is not null)
|
||||
{
|
||||
var exceptionMessage = $"Exception: {exception}";
|
||||
_innerLogger.Log($"[{_categoryName}] {exceptionMessage}");
|
||||
}
|
||||
}
|
||||
|
||||
private string _BuildScopeContext()
|
||||
{
|
||||
var stack = _ScopeStack.Value;
|
||||
if (stack is null || stack.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
var scopes = stack.AsEnumerable().Reverse();
|
||||
return string.Join(" => ", scopes.Select(s => s.ToString()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace PCL.Core.Logging;
|
||||
|
||||
public record LoggerConfiguration(
|
||||
string StoreFolder,
|
||||
long MaxFileSize = 32 * 1024 * 1024,
|
||||
string? FileNameFormat = null,
|
||||
bool AutoDeleteOldFile = true,
|
||||
int MaxKeepOldFile = 16,
|
||||
LogLevel MinLogLevel = LogLevel.Info
|
||||
);
|
||||
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace PCL.Core.Logging;
|
||||
|
||||
public static class LoggerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建 ILogger 实例
|
||||
/// </summary>
|
||||
/// <param name="logger">现有的 Logger 实例</param>
|
||||
/// <param name="categoryName">日志类别名称</param>
|
||||
/// <returns>ILogger 实例</returns>
|
||||
public static ILogger CreateLogger(this Logger logger, string categoryName)
|
||||
{
|
||||
return new LoggerAdapter(logger, categoryName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 ILogger 工厂
|
||||
/// </summary>
|
||||
/// <param name="logger">现有的 Logger 实例</param>
|
||||
/// <returns>ILoggerFactory 实例</returns>
|
||||
public static ILoggerFactory CreateLoggerFactory(this Logger logger)
|
||||
{
|
||||
return new LoggerFactoryAdapter(logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用结构化日志记录的扩展方法
|
||||
/// </summary>
|
||||
public static void LogInformation<T0>(this ILogger logger, string message, T0 arg0)
|
||||
{
|
||||
logger.Log(Microsoft.Extensions.Logging.LogLevel.Information, message, arg0);
|
||||
}
|
||||
|
||||
public static void LogInformation<T0, T1>(this ILogger logger, string message, T0 arg0, T1 arg1)
|
||||
{
|
||||
logger.Log(Microsoft.Extensions.Logging.LogLevel.Information, message, arg0, arg1);
|
||||
}
|
||||
|
||||
public static void LogInformation<T0, T1, T2>(this ILogger logger, string message, T0 arg0, T1 arg1, T2 arg2)
|
||||
{
|
||||
logger.Log(Microsoft.Extensions.Logging.LogLevel.Information, message, arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
public static void LogWarning<T0>(this ILogger logger, string message, T0 arg0)
|
||||
{
|
||||
logger.Log(Microsoft.Extensions.Logging.LogLevel.Warning, message, arg0);
|
||||
}
|
||||
|
||||
public static void LogWarning<T0, T1>(this ILogger logger, string message, T0 arg0, T1 arg1)
|
||||
{
|
||||
logger.Log(Microsoft.Extensions.Logging.LogLevel.Warning, message, arg0, arg1);
|
||||
}
|
||||
|
||||
public static void LogError<T0>(this ILogger logger, Exception? exception, string message, T0 arg0)
|
||||
{
|
||||
logger.Log(Microsoft.Extensions.Logging.LogLevel.Error, exception, message, arg0);
|
||||
}
|
||||
|
||||
public static void LogError<T0, T1>(this ILogger logger, Exception? exception, string message, T0 arg0, T1 arg1)
|
||||
{
|
||||
logger.Log(Microsoft.Extensions.Logging.LogLevel.Error, exception, message, arg0, arg1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 条件日志记录扩展方法
|
||||
/// </summary>
|
||||
public static void LogIf(this ILogger logger, bool condition, Microsoft.Extensions.Logging.LogLevel level, string message)
|
||||
{
|
||||
if (condition)
|
||||
{
|
||||
logger.Log(level, message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogIf(this ILogger logger, bool condition, Microsoft.Extensions.Logging.LogLevel level, Exception? exception, string message)
|
||||
{
|
||||
if (condition)
|
||||
{
|
||||
logger.Log(level, exception, message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 性能计时日志记录
|
||||
/// </summary>
|
||||
public static IDisposable LogPerformance(this ILogger logger, string operationName)
|
||||
{
|
||||
logger.LogInformation("开始执行: {OperationName}", operationName);
|
||||
|
||||
return new PerformanceLoggerDisposable(logger, operationName);
|
||||
}
|
||||
|
||||
private class PerformanceLoggerDisposable(ILogger logger, string operationName) : IDisposable
|
||||
{
|
||||
private readonly long _startTime = Stopwatch.GetTimestamp();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
var elapsed = Stopwatch.GetElapsedTime(_startTime);
|
||||
logger.LogInformation("完成执行: {OperationName}, 耗时: {ElapsedMs}ms", operationName, elapsed.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PCL.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// ILoggerFactory 实现,用于创建 LoggerAdapter 实例
|
||||
/// </summary>
|
||||
public class LoggerFactoryAdapter(Logger logger) : ILoggerFactory
|
||||
{
|
||||
private readonly Logger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
private readonly List<IDisposable> _disposables = [];
|
||||
|
||||
public void AddProvider(ILoggerProvider provider)
|
||||
{
|
||||
_disposables.Add(provider);
|
||||
// 不需要实现,因为我们只有一个固定的 Logger
|
||||
}
|
||||
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return new LoggerAdapter(_logger, categoryName);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var disposable in _disposables)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
_disposables.Clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user