Lifecycle System Refactor & Logging Centralization (#56)
## ManagedBehaviour System Refactor - **Sealed `Awake()`** to prevent override mistakes that break singleton registration - **Added `OnManagedAwake()`** for early initialization (fires during registration) - **Renamed lifecycle hook:** `OnManagedAwake()` → `OnManagedStart()` (fires after boot, mirrors Unity's Awake→Start) - **40 files migrated** to new pattern (2 core, 38 components) - Eliminated all fragile `private new void Awake()` patterns - Zero breaking changes - backward compatible ## Centralized Logging System - **Automatic tagging** via `CallerMemberName` and `CallerFilePath` - logs auto-tagged as `[ClassName][MethodName] message` - **Unified API:** Single `Logging.Debug/Info/Warning/Error()` replaces custom `LogDebugMessage()` implementations - **~90 logging call sites** migrated across 10 files - **10 redundant helper methods** removed - All logs broadcast via `Logging.OnLogEntryAdded` event for real-time monitoring ## Custom Log Console (Editor Window) - **Persistent filter popups** for multi-selection (classes, methods, log levels) - windows stay open during selection - **Search** across class names, methods, and message content - **Time range filter** with MinMaxSlider - **Export** filtered logs to timestamped `.txt` files - **Right-click context menu** for quick filtering and copy actions - **Visual improvements:** White text, alternating row backgrounds, color-coded log levels - **Multiple instances** supported for simultaneous system monitoring - Open via `AppleHills > Custom Log Console` Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: #56
This commit is contained in:
@@ -1,17 +1,144 @@
|
||||
namespace Core
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Centralized logging system with automatic class/method tagging and editor integration.
|
||||
/// Broadcasts log entries to custom editor windows for filtering and analysis.
|
||||
/// </summary>
|
||||
public static class Logging
|
||||
{
|
||||
[System.Diagnostics.Conditional("ENABLE_LOG")]
|
||||
public static void Debug(object message)
|
||||
/// <summary>
|
||||
/// Event fired when a new log entry is added. Subscribe to this in editor windows.
|
||||
/// </summary>
|
||||
public static event Action<LogEntry> OnLogEntryAdded;
|
||||
|
||||
// Store recent logs for late-subscriber editor windows (e.g., windows opened after play mode started)
|
||||
private static readonly List<LogEntry> RecentLogs = new List<LogEntry>();
|
||||
private const int MaxStoredLogs = 5000; // Prevent memory bloat
|
||||
|
||||
/// <summary>
|
||||
/// Get all recent logs. Used by editor windows when they first open.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<LogEntry> GetRecentLogs() => RecentLogs;
|
||||
|
||||
/// <summary>
|
||||
/// Clear all stored logs. Useful for editor windows "Clear" button.
|
||||
/// </summary>
|
||||
public static void ClearLogs()
|
||||
{
|
||||
UnityEngine.Debug.Log(message);
|
||||
RecentLogs.Clear();
|
||||
}
|
||||
|
||||
[System.Diagnostics.Conditional("ENABLE_LOG")]
|
||||
public static void Warning(object message)
|
||||
public static void Debug(string message,
|
||||
[CallerFilePath] string filePath = "",
|
||||
[CallerMemberName] string memberName = "")
|
||||
{
|
||||
UnityEngine.Debug.LogWarning(message);
|
||||
LogInternal(LogLevel.Debug, message, filePath, memberName);
|
||||
}
|
||||
|
||||
[System.Diagnostics.Conditional("ENABLE_LOG")]
|
||||
public static void Info(string message,
|
||||
[CallerFilePath] string filePath = "",
|
||||
[CallerMemberName] string memberName = "")
|
||||
{
|
||||
LogInternal(LogLevel.Info, message, filePath, memberName);
|
||||
}
|
||||
|
||||
[System.Diagnostics.Conditional("ENABLE_LOG")]
|
||||
public static void Warning(string message,
|
||||
[CallerFilePath] string filePath = "",
|
||||
[CallerMemberName] string memberName = "")
|
||||
{
|
||||
LogInternal(LogLevel.Warning, message, filePath, memberName);
|
||||
}
|
||||
|
||||
public static void Error(string message,
|
||||
[CallerFilePath] string filePath = "",
|
||||
[CallerMemberName] string memberName = "")
|
||||
{
|
||||
LogInternal(LogLevel.Error, message, filePath, memberName);
|
||||
}
|
||||
|
||||
private static void LogInternal(LogLevel level, string message, string filePath, string memberName)
|
||||
{
|
||||
string className = Path.GetFileNameWithoutExtension(filePath);
|
||||
string formattedMessage = $"[{className}][{memberName}] {message}";
|
||||
|
||||
// Create log entry
|
||||
var entry = new LogEntry(className, memberName, message, level, Time.realtimeSinceStartup);
|
||||
|
||||
// Store for late subscribers
|
||||
RecentLogs.Add(entry);
|
||||
if (RecentLogs.Count > MaxStoredLogs)
|
||||
RecentLogs.RemoveAt(0);
|
||||
|
||||
// Broadcast to editor windows (editor-only, won't fire in builds)
|
||||
OnLogEntryAdded?.Invoke(entry);
|
||||
|
||||
// Also log to Unity console
|
||||
switch (level)
|
||||
{
|
||||
case LogLevel.Debug:
|
||||
case LogLevel.Info:
|
||||
UnityEngine.Debug.Log(formattedMessage);
|
||||
break;
|
||||
case LogLevel.Warning:
|
||||
UnityEngine.Debug.LogWarning(formattedMessage);
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
UnityEngine.Debug.LogError(formattedMessage);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single log entry with class, method, message, level, and timestamp.
|
||||
/// </summary>
|
||||
public class LogEntry
|
||||
{
|
||||
public string ClassName { get; }
|
||||
public string MethodName { get; }
|
||||
public string Message { get; }
|
||||
public LogLevel Level { get; }
|
||||
public float Timestamp { get; }
|
||||
|
||||
public LogEntry(string className, string methodName, string message, LogLevel level, float timestamp)
|
||||
{
|
||||
ClassName = className;
|
||||
MethodName = methodName;
|
||||
Message = message;
|
||||
Level = level;
|
||||
Timestamp = timestamp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formatted message with class and method tags.
|
||||
/// Format: [ClassName][MethodName] Message
|
||||
/// </summary>
|
||||
public string FormattedMessage => $"[{ClassName}][{MethodName}] {Message}";
|
||||
|
||||
/// <summary>
|
||||
/// Full formatted message with timestamp and level.
|
||||
/// Format: [12.34s][Debug][ClassName][MethodName] Message
|
||||
/// </summary>
|
||||
public string FullFormattedMessage => $"[{Timestamp:F2}s][{Level}]{FormattedMessage}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log severity levels.
|
||||
/// </summary>
|
||||
public enum LogLevel
|
||||
{
|
||||
Debug,
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user