- 使用 ConcurrentQueue + 后台线程批量写入 - 新增 LogLevel、LogCategory、LogEntry、LogFormatter - 新增 GameLog 业务日志入口 - 新增 LogSettingsProfiles 平台化配置 - 支持按 session 分文件、大小轮转与队列满时低优先级丢弃
405 lines
13 KiB
C#
405 lines
13 KiB
C#
using System;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using AibisDream.Utility;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
|
|
namespace AibisDream.Kit
|
|
{
|
|
/// <summary>
|
|
/// Runtime file logging entry point. Captures Unity logs and routes them through
|
|
/// an asynchronous file writer so mobile builds do not block on every log line.
|
|
/// </summary>
|
|
internal static class LogKit
|
|
{
|
|
public const int SaveDays = 30;
|
|
|
|
private static FileLogger _fileLogger;
|
|
private static LogSettings _settings;
|
|
private static bool _isInitialized;
|
|
private static bool _isForwardingToUnity;
|
|
|
|
internal static bool IsInitialized => _isInitialized;
|
|
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
|
private static void Initialize()
|
|
{
|
|
if (_isInitialized)
|
|
return;
|
|
|
|
_settings = LogSettings.CreateDefault();
|
|
if (!_settings.Enabled)
|
|
return;
|
|
|
|
LogEntry.SetMainThreadId(Thread.CurrentThread.ManagedThreadId);
|
|
_fileLogger = new FileLogger(ConstRef.LogFilePath, _settings);
|
|
_fileLogger.Write(LogEntry.System(LogLevel.Info, LogCategory.General, BuildStartupHeader()));
|
|
|
|
Application.logMessageReceivedThreaded += LogMessage;
|
|
Application.quitting += Shutdown;
|
|
Application.focusChanged += OnFocusChanged;
|
|
_isInitialized = true;
|
|
}
|
|
|
|
internal static void Write(LogEntry entry)
|
|
{
|
|
if (!_isInitialized || _fileLogger == null)
|
|
return;
|
|
|
|
_fileLogger.Write(entry);
|
|
}
|
|
|
|
internal static void ForwardToUnity(Action logAction)
|
|
{
|
|
if (logAction == null)
|
|
return;
|
|
|
|
_isForwardingToUnity = true;
|
|
try
|
|
{
|
|
logAction.Invoke();
|
|
}
|
|
finally
|
|
{
|
|
_isForwardingToUnity = false;
|
|
}
|
|
}
|
|
|
|
internal static bool ShouldWriteToConsole(LogLevel level)
|
|
{
|
|
return _settings == null || _settings.EchoToConsole && level >= _settings.ConsoleMinLevel;
|
|
}
|
|
|
|
private static void LogMessage(string condition, string stackTrace, LogType type)
|
|
{
|
|
if (_isForwardingToUnity)
|
|
return;
|
|
|
|
Write(LogEntry.FromUnity(condition, stackTrace, type, _settings.ShouldIncludeStackTrace));
|
|
}
|
|
|
|
private static void OnFocusChanged(bool hasFocus)
|
|
{
|
|
if (!hasFocus)
|
|
_fileLogger?.Flush(TimeSpan.FromMilliseconds(_settings?.ShutdownDrainMilliseconds ?? 500));
|
|
}
|
|
|
|
private static void Shutdown()
|
|
{
|
|
if (!_isInitialized)
|
|
return;
|
|
|
|
Application.logMessageReceivedThreaded -= LogMessage;
|
|
Application.quitting -= Shutdown;
|
|
Application.focusChanged -= OnFocusChanged;
|
|
|
|
_fileLogger?.Write(LogEntry.System(LogLevel.Info, LogCategory.General, "System shutdown."));
|
|
_fileLogger?.OnDestroy();
|
|
_fileLogger = null;
|
|
_isInitialized = false;
|
|
}
|
|
|
|
private static string BuildStartupHeader()
|
|
{
|
|
return string.Join(
|
|
" | ",
|
|
"System start",
|
|
$"version={Application.version}",
|
|
$"platform={Application.platform}",
|
|
$"unity={Application.unityVersion}",
|
|
$"device={SystemInfo.deviceModel}",
|
|
$"os={SystemInfo.operatingSystem}",
|
|
$"graphics={SystemInfo.graphicsDeviceName}",
|
|
$"maxTexture={SystemInfo.maxTextureSize}",
|
|
$"memory={SystemInfo.systemMemorySize}MB");
|
|
}
|
|
}
|
|
|
|
public enum LogLevel
|
|
{
|
|
Debug = 0,
|
|
Info = 1,
|
|
Warning = 2,
|
|
Error = 3,
|
|
Fatal = 4
|
|
}
|
|
|
|
public enum LogCategory
|
|
{
|
|
General,
|
|
Unity,
|
|
Save,
|
|
Yarn,
|
|
Resource,
|
|
Scene,
|
|
UI,
|
|
Audio,
|
|
FixSystem,
|
|
MiniGame
|
|
}
|
|
|
|
public static class GameLog
|
|
{
|
|
public static void Debug(LogCategory category, string message, string context = null)
|
|
{
|
|
Write(LogLevel.Debug, category, message, context, null);
|
|
}
|
|
|
|
public static void Info(LogCategory category, string message, string context = null)
|
|
{
|
|
Write(LogLevel.Info, category, message, context, null);
|
|
}
|
|
|
|
public static void Warn(LogCategory category, string message, string context = null)
|
|
{
|
|
Write(LogLevel.Warning, category, message, context, null);
|
|
}
|
|
|
|
public static void Error(LogCategory category, string message, string context = null)
|
|
{
|
|
Write(LogLevel.Error, category, message, context, null);
|
|
}
|
|
|
|
public static void Exception(LogCategory category, Exception exception, string context = null)
|
|
{
|
|
var message = exception == null ? "Unknown exception" : exception.Message;
|
|
Write(LogLevel.Error, category, message, context, exception);
|
|
}
|
|
|
|
private static void Write(LogLevel level, LogCategory category, string message, string context, Exception exception)
|
|
{
|
|
var entry = LogEntry.FromGame(level, category, message, context, exception);
|
|
LogKit.Write(entry);
|
|
|
|
if (!LogKit.ShouldWriteToConsole(level))
|
|
return;
|
|
|
|
var consoleMessage = $"[{category}] {message}";
|
|
if (!string.IsNullOrEmpty(context))
|
|
consoleMessage += $" | {context}";
|
|
|
|
LogKit.ForwardToUnity(() =>
|
|
{
|
|
if (exception != null)
|
|
{
|
|
UnityEngine.Debug.LogException(exception);
|
|
return;
|
|
}
|
|
|
|
switch (level)
|
|
{
|
|
case LogLevel.Warning:
|
|
UnityEngine.Debug.LogWarning(consoleMessage);
|
|
break;
|
|
case LogLevel.Error:
|
|
case LogLevel.Fatal:
|
|
UnityEngine.Debug.LogError(consoleMessage);
|
|
break;
|
|
default:
|
|
UnityEngine.Debug.Log(consoleMessage);
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
internal sealed class LogSettings
|
|
{
|
|
public bool Enabled { get; private set; }
|
|
public bool EchoToConsole { get; private set; }
|
|
public LogLevel MinLevel { get; private set; }
|
|
public LogLevel ConsoleMinLevel { get; private set; }
|
|
public int QueueCapacity { get; private set; }
|
|
public int BatchSize { get; private set; }
|
|
public int FlushIntervalMilliseconds { get; private set; }
|
|
public int ShutdownDrainMilliseconds { get; private set; }
|
|
public long MaxFileBytes { get; private set; }
|
|
public long MaxTotalBytes { get; private set; }
|
|
public int MaxSessionFiles { get; private set; }
|
|
public bool IncludeWarningStackTrace { get; private set; }
|
|
public string SessionId { get; private set; }
|
|
public string AppVersion { get; private set; }
|
|
public string Platform { get; private set; }
|
|
|
|
public static string PlatformName
|
|
{
|
|
get
|
|
{
|
|
#if UNITY_ANDROID
|
|
return "android";
|
|
#elif UNITY_STANDALONE_WIN
|
|
return "windows";
|
|
#elif UNITY_STANDALONE_OSX
|
|
return "mac";
|
|
#elif UNITY_STANDALONE_LINUX
|
|
return "linux";
|
|
#else
|
|
return UnityEngine.Application.platform.ToString().ToLowerInvariant();
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public bool ShouldWrite(LogLevel level)
|
|
{
|
|
return Enabled && level >= MinLevel;
|
|
}
|
|
|
|
public bool ShouldIncludeStackTrace(LogLevel level)
|
|
{
|
|
return level >= LogLevel.Error || IncludeWarningStackTrace && level == LogLevel.Warning;
|
|
}
|
|
|
|
public static LogSettings CreateDefault()
|
|
{
|
|
return Create(LogSettingsProfiles.Current);
|
|
}
|
|
|
|
private static LogSettings Create(LogSettingsProfile profile)
|
|
{
|
|
return new LogSettings
|
|
{
|
|
Enabled = profile.Enabled,
|
|
EchoToConsole = profile.EchoToConsole,
|
|
MinLevel = profile.MinLevel,
|
|
ConsoleMinLevel = profile.ConsoleMinLevel,
|
|
QueueCapacity = profile.QueueCapacity,
|
|
BatchSize = profile.BatchSize,
|
|
FlushIntervalMilliseconds = profile.FlushIntervalMilliseconds,
|
|
ShutdownDrainMilliseconds = profile.ShutdownDrainMilliseconds,
|
|
MaxFileBytes = profile.MaxFileBytes,
|
|
MaxTotalBytes = profile.MaxTotalBytes,
|
|
MaxSessionFiles = profile.MaxSessionFiles,
|
|
IncludeWarningStackTrace = profile.IncludeWarningStackTrace,
|
|
SessionId = Guid.NewGuid().ToString("N").Substring(0, 8),
|
|
AppVersion = UnityEngine.Application.version,
|
|
Platform = PlatformName
|
|
};
|
|
}
|
|
}
|
|
|
|
internal readonly struct LogEntry
|
|
{
|
|
private static int _mainThreadId;
|
|
|
|
public readonly DateTime TimestampUtc;
|
|
public readonly LogLevel Level;
|
|
public readonly LogCategory Category;
|
|
public readonly string Message;
|
|
public readonly string StackTrace;
|
|
public readonly string Context;
|
|
public readonly string SceneName;
|
|
public readonly int FrameCount;
|
|
public readonly int ThreadId;
|
|
|
|
private LogEntry(LogLevel level, LogCategory category, string message, string stackTrace, string context)
|
|
{
|
|
TimestampUtc = DateTime.UtcNow;
|
|
Level = level;
|
|
Category = category;
|
|
Message = message ?? string.Empty;
|
|
StackTrace = stackTrace ?? string.Empty;
|
|
Context = context ?? string.Empty;
|
|
var isMainThread = Thread.CurrentThread.ManagedThreadId == _mainThreadId;
|
|
SceneName = isMainThread ? GetSceneName() : string.Empty;
|
|
FrameCount = isMainThread ? Time.frameCount : -1;
|
|
ThreadId = Thread.CurrentThread.ManagedThreadId;
|
|
}
|
|
|
|
public static void SetMainThreadId(int threadId)
|
|
{
|
|
_mainThreadId = threadId;
|
|
}
|
|
|
|
public static LogEntry System(LogLevel level, LogCategory category, string message)
|
|
{
|
|
return new LogEntry(level, category, message, string.Empty, string.Empty);
|
|
}
|
|
|
|
public static LogEntry FromUnity(string condition, string stackTrace, LogType type, Func<LogLevel, bool> shouldIncludeStackTrace)
|
|
{
|
|
var level = ToLogLevel(type);
|
|
var stack = shouldIncludeStackTrace != null && shouldIncludeStackTrace(level) ? stackTrace : string.Empty;
|
|
return new LogEntry(level, LogCategory.Unity, condition, stack, string.Empty);
|
|
}
|
|
|
|
public static LogEntry FromGame(LogLevel level, LogCategory category, string message, string context, Exception exception)
|
|
{
|
|
var stack = exception == null ? string.Empty : exception.ToString();
|
|
return new LogEntry(level, category, message, stack, context);
|
|
}
|
|
|
|
private static LogLevel ToLogLevel(LogType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case LogType.Warning:
|
|
return LogLevel.Warning;
|
|
case LogType.Error:
|
|
return LogLevel.Error;
|
|
case LogType.Exception:
|
|
case LogType.Assert:
|
|
return LogLevel.Fatal;
|
|
default:
|
|
return LogLevel.Info;
|
|
}
|
|
}
|
|
|
|
private static string GetSceneName()
|
|
{
|
|
try
|
|
{
|
|
return SceneManager.GetActiveScene().name;
|
|
}
|
|
catch
|
|
{
|
|
return string.Empty;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static class LogFormatter
|
|
{
|
|
public static string Format(LogEntry entry)
|
|
{
|
|
var builder = new StringBuilder(256);
|
|
builder.Append(entry.TimestampUtc.ToString("O"));
|
|
builder.Append(" | ");
|
|
builder.Append(entry.Level);
|
|
builder.Append(" | ");
|
|
builder.Append(entry.Category);
|
|
builder.Append(" | scene=");
|
|
builder.Append(Escape(entry.SceneName));
|
|
builder.Append(" | frame=");
|
|
builder.Append(entry.FrameCount);
|
|
builder.Append(" | thread=");
|
|
builder.Append(entry.ThreadId);
|
|
|
|
if (!string.IsNullOrEmpty(entry.Context))
|
|
{
|
|
builder.Append(" | context=");
|
|
builder.Append(Escape(entry.Context));
|
|
}
|
|
|
|
builder.Append(" | ");
|
|
builder.Append(Escape(entry.Message));
|
|
|
|
if (!string.IsNullOrEmpty(entry.StackTrace))
|
|
{
|
|
builder.Append(" | stack=");
|
|
builder.Append(Escape(entry.StackTrace));
|
|
}
|
|
|
|
return builder.ToString();
|
|
}
|
|
|
|
private static string Escape(string value)
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
return string.Empty;
|
|
|
|
return value.Replace("\r", "\\r").Replace("\n", "\\n");
|
|
}
|
|
}
|
|
}
|