using System.IO;
namespace ytLive.Helpers;
/// Presentation category; independent from the entry's attention class.
public enum AppLogMessageType
{
Information,
Success,
Warning,
Error,
}
/// Explicit attention priority, assigned by the writer rather than inferred from text or source.
public enum AppLogErrorClass
{
/// Fatal or unhandled failure; highest priority.
Class1 = 1,
/// Recoverable failure that still requires attention.
Class2 = 2,
}
///
/// 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.
///
public static class AppLog
{
/// The cap for the live log file. Past this, it rolls to startup.1.log.
private const long DefaultMaxBytes = 5 * 1024 * 1024;
private const int CurrentLaunchEntryLimit = 500;
/// How many rolled copies to keep (startup.1.log, startup.2.log, …).
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 CurrentLaunchEntries = new();
private static long _nextSequence;
/// Test seam: point the log somewhere else. Null = the real profile's log.
internal static string? PathOverride { get; set; }
/// Test seam: the cap that triggers a roll.
internal static long MaxBytes { get; set; } = DefaultMaxBytes;
private static string LogPath => PathOverride ?? DefaultLogPath;
internal static IReadOnlyList 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(),
};
}