Add independent AppLog message types and explicit Class 1/2 severity. Wrap and color log rows, pin Class 1 above Class 2, and let acknowledgement return entries to the current-launch timeline without changing file format or rotation. Ref: https://logtape.org/manual/levels
123 lines
4.2 KiB
C#
123 lines
4.2 KiB
C#
using System.IO;
|
|
|
|
namespace ytLive.Helpers;
|
|
|
|
/// <summary>Presentation category; independent from the entry's attention class.</summary>
|
|
public enum AppLogMessageType
|
|
{
|
|
Information,
|
|
Success,
|
|
Warning,
|
|
Error,
|
|
}
|
|
|
|
/// <summary>Explicit attention priority, assigned by the writer rather than inferred from text or source.</summary>
|
|
public enum AppLogErrorClass
|
|
{
|
|
/// <summary>Fatal or unhandled failure; highest priority.</summary>
|
|
Class1 = 1,
|
|
/// <summary>Recoverable failure that still requires attention.</summary>
|
|
Class2 = 2,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Minimal file logger used to diagnose startup/shutdown crashes on Windows,
|
|
/// where the WPF app has no visible console. Writes to
|
|
/// %APPDATA%\ytLlive\startup.log (or a per-instance subfolder in DEBUG — see
|
|
/// InstanceProfile), appending synchronously so entries survive a hard crash.
|
|
/// </summary>
|
|
public static class AppLog
|
|
{
|
|
/// <summary>The cap for the live log file. Past this, it rolls to <c>startup.1.log</c>.</summary>
|
|
private const long DefaultMaxBytes = 5 * 1024 * 1024;
|
|
private const int CurrentLaunchEntryLimit = 500;
|
|
|
|
/// <summary>How many rolled copies to keep (startup.1.log, startup.2.log, …).</summary>
|
|
private const int RetainedFiles = 1;
|
|
|
|
private static readonly string DefaultLogPath = Path.Combine(
|
|
InstanceProfile.DataRoot, "startup.log");
|
|
|
|
private static readonly object Gate = new();
|
|
private static readonly List<AppLogEntry> CurrentLaunchEntries = new();
|
|
private static long _nextSequence;
|
|
|
|
/// <summary>Test seam: point the log somewhere else. Null = the real profile's log.</summary>
|
|
internal static string? PathOverride { get; set; }
|
|
|
|
/// <summary>Test seam: the cap that triggers a roll.</summary>
|
|
internal static long MaxBytes { get; set; } = DefaultMaxBytes;
|
|
|
|
private static string LogPath => PathOverride ?? DefaultLogPath;
|
|
|
|
internal static IReadOnlyList<AppLogEntry> SnapshotCurrentLaunch()
|
|
{
|
|
lock (Gate)
|
|
return CurrentLaunchEntries.ToArray();
|
|
}
|
|
|
|
public static void Write(string message)
|
|
=> Write(message, AppLogMessageType.Information);
|
|
|
|
public static void WriteWarning(string message)
|
|
=> Write(message, AppLogMessageType.Warning);
|
|
|
|
public static void WriteSuccess(string message)
|
|
=> Write(message, AppLogMessageType.Success);
|
|
|
|
public static void Write(string message, AppLogMessageType type, AppLogErrorClass? errorClass = null)
|
|
{
|
|
try
|
|
{
|
|
lock (Gate)
|
|
{
|
|
var entry = new AppLogEntry(
|
|
++_nextSequence,
|
|
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {message}",
|
|
type,
|
|
errorClass);
|
|
CurrentLaunchEntries.Add(entry);
|
|
if (CurrentLaunchEntries.Count > CurrentLaunchEntryLimit)
|
|
CurrentLaunchEntries.RemoveAt(0);
|
|
|
|
var path = LogPath;
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
|
LogRotation.RollIfNeeded(path, MaxBytes, RetainedFiles);
|
|
File.AppendAllText(path, $"{entry.Text}{Environment.NewLine}");
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Never let logging itself crash the app.
|
|
}
|
|
}
|
|
|
|
public static void Write(Exception exception, string message)
|
|
{
|
|
Write($"{message}: {exception}", AppLogMessageType.Error);
|
|
}
|
|
|
|
public static void Write(Exception exception, string message, AppLogErrorClass errorClass)
|
|
=> Write($"{message}: {exception}", AppLogMessageType.Error, errorClass);
|
|
}
|
|
|
|
public sealed record AppLogEntry(long Sequence, string Text, AppLogMessageType Type,
|
|
AppLogErrorClass? ErrorClass)
|
|
{
|
|
public string? ErrorClassLabel => ErrorClass switch
|
|
{
|
|
AppLogErrorClass.Class1 => "CLASS 1",
|
|
AppLogErrorClass.Class2 => "CLASS 2",
|
|
_ => null,
|
|
};
|
|
|
|
public string TypeLabel => ErrorClassLabel ?? Type switch
|
|
{
|
|
AppLogMessageType.Information => "INFO",
|
|
AppLogMessageType.Success => "SUCCESS",
|
|
AppLogMessageType.Warning => "WARNING",
|
|
AppLogMessageType.Error => "ERROR",
|
|
_ => Type.ToString().ToUpperInvariant(),
|
|
};
|
|
}
|