Files
aibis-dream/Assets/Scripts/Framework/LogKit/LogKit.cs
T
2026-07-20 14:29:47 +08:00

420 lines
14 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;
private static readonly RuntimeLogBuffer RuntimeBuffer = new();
internal static bool IsInitialized => _isInitialized;
internal static bool IsFileLoggingEnabled => _fileLogger != null;
internal static string CurrentLogFilePath => _fileLogger?.CurrentFilePath;
internal static int RuntimeLogVersion => RuntimeBuffer.Version;
internal static int RuntimeLogCount => RuntimeBuffer.Count;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Initialize()
{
if (_isInitialized)
return;
_settings = LogSettings.CreateDefault();
LogEntry.SetMainThreadId(Thread.CurrentThread.ManagedThreadId);
if (_settings.Enabled)
{
_fileLogger = new FileLogger(ConstRef.LogFilePath, _settings);
}
Application.logMessageReceivedThreaded += LogMessage;
Application.quitting += Shutdown;
Application.focusChanged += OnFocusChanged;
_isInitialized = true;
Write(LogEntry.System(LogLevel.Info, LogCategory.General, BuildStartupHeader()));
}
internal static void Write(LogEntry entry)
{
if (!_isInitialized)
return;
if (DeveloperModeGate.IsEnabled)
{
RuntimeBuffer.Add(entry);
}
_fileLogger?.Write(entry);
}
internal static RuntimeLogRecord[] GetRuntimeLogSnapshot() => RuntimeBuffer.Snapshot();
internal static void ClearRuntimeLogs() => RuntimeBuffer.Clear();
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;
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");
}
}
}