using IBKRTrader.Core.Time; using System.Runtime.CompilerServices; namespace IBKRTrader.Core.Logging; /// /// Thread-sicherer Logging-Service. /// – Schreibt in Logs\{Module}\{Level}-dd-MM-yy.txt sowie strukturiert nach Logs\{Datum}.jsonl /// – Meldet jeden Eintrag über an interessierte Senken (z. B. die /// Live-Log-Ansicht der Oberfläche) /// /// Bewusst ohne UI-Bezug: Früher hielt dieser Dienst direkt eine /// RichTextBox samt System.Drawing.Color und marshallte selbst auf den UI-Thread. /// Damit hing der Core an WinForms. Jetzt kennt er nur noch das Ereignis; Einfärbung und /// Thread-Wechsel sind Sache der jeweiligen Oberfläche. /// public class LoggingService { private AppLogLevel _minLevel = AppLogLevel.Info; private readonly object _fileLock = new(); private readonly object _jsonlLock = new(); private static readonly string LogBaseDir = Path.Combine(AppContext.BaseDirectory, "Logs"); // ─── Konfiguration ──────────────────────────────────────────────────────── /// /// Feuert für jeden geschriebenen Eintrag (nach der Mindest-Level-Prüfung). Die Oberfläche /// hängt sich hier ein; das Marshalling auf den UI-Thread übernimmt sie selbst, weil dieser /// Dienst aus beliebigen Worker-Threads schreibt. /// public event Action? EntryWritten; public void SetMinLevel(AppLogLevel level) => _minLevel = level; // ─── Öffentliche API ───────────────────────────────────────────────────── public void Info (string module, string message, Exception? ex = null) => Write(AppLogLevel.Info, module, message, ex); public void Warn (string module, string message, Exception? ex = null) => Write(AppLogLevel.Warn, module, message, ex); public void Error(string module, string message, Exception? ex = null) => Write(AppLogLevel.Error, module, message, ex); // Überladungen mit CorrelationId (SignalId) – schreiben zusätzlich strukturiert ins JSONL, // sodass der Supervisor die komplette Kette „alles zu diesem Signal" filtern kann. public void Info (string module, string message, string? cid, Exception? ex = null) => Write(AppLogLevel.Info, module, message, ex, cid); public void Warn (string module, string message, string? cid, Exception? ex = null) => Write(AppLogLevel.Warn, module, message, ex, cid); public void Error(string module, string message, string? cid, Exception? ex = null) => Write(AppLogLevel.Error, module, message, ex, cid); public void Write(AppLogLevel level, string module, string message, Exception? ex = null, string? cid = null) { if (level < _minLevel) return; // Zwei Zeitformen, bewusst getrennt: // • entry.Timestamp = Betriebszeitzone → Anzeige und Dateinamen (…-dd-MM-yy.txt, // {yyyy-MM-dd}.jsonl). Die Tagesgrenzen sollen an der Instanz hängen, nicht am Host – // ein UTC-Container hätte sonst andere Grenzen als der Windows-Desktop, und der // Supervisor liest die JSONL-Dateien über genau diese Namen. // • utc = maschinenlesbares Feld im JSONL. Muss separat mitgeführt werden, weil die // umgerechnete Ortszeit Kind=Unspecified trägt: ein ToUniversalTime() darauf würde sie // als Zeit des HOSTS deuten und bei abweichender Rechnerzeitzone falsch verschieben. var utc = DateTime.UtcNow; var entry = new LogEntry(AppTimeZone.ToDisplay(utc), level, module, message, ex); WriteToFile(entry); WriteToJsonl(entry, utc, cid); NotifySinks(entry); } private void NotifySinks(LogEntry e) { // Eine hängende Senke darf den schreibenden Worker nicht mitreißen. try { EntryWritten?.Invoke(e); } catch { /* Logging darf niemals abstürzen */ } } // ─── Datei ──────────────────────────────────────────────────────────────── private void WriteToFile(LogEntry e) { try { var dir = Path.Combine(LogBaseDir, e.Module); Directory.CreateDirectory(dir); var file = Path.Combine(dir, $"{e.Level}-{e.Timestamp:dd-MM-yy}.txt"); var line = $"[{e.Timestamp:HH:mm:ss}] {e.Message}"; if (e.Exception != null) line += $"{Environment.NewLine} {e.Exception}"; lock (_fileLock) File.AppendAllText(file, line + Environment.NewLine); } catch { /* Logging darf niemals abstürzen */ } } // ─── JSONL (KI-freundlicher Zweit-Sink) ───────────────────────────────────── /// /// Schreibt zusätzlich eine JSON-Zeile nach Logs\{yyyy-MM-dd}.jsonl (Dual-Sink). Zeilenweise /// filter-/parsebar (Datum/Level/Quelle/Text/CorrelationId) – Grundlage für Log Viewer + Supervisor. /// Dateiname nach Ortszeit (Tagesgrenze), ts-Feld in UTC (maschinenlesbar). /// private void WriteToJsonl(LogEntry e, DateTime utc, string? cid) { try { Directory.CreateDirectory(LogBaseDir); var file = Path.Combine(LogBaseDir, $"{e.Timestamp:yyyy-MM-dd}.jsonl"); var message = e.Exception != null ? $"{e.Message} | {e.Exception.Message}" : e.Message; var json = LogJson.WriteLine(utc, e.Level, e.Module, message, cid); lock (_jsonlLock) File.AppendAllText(file, json + "\n"); } catch { /* Logging darf niemals abstürzen */ } } // ─── Anzeigeformat ──────────────────────────────────────────────────────── /// /// Einzeilige Darstellung für Log-Ansichten. Liegt hier, damit jede Oberfläche dieselbe Zeile /// zeigt. ToUpperInvariant ist Absicht: ToUpper() würde unter tr-TR aus "info" /// ein "İNFO" machen. /// public static string Format(LogEntry e) { var text = $"[{e.Timestamp:HH:mm:ss}] [{e.Level.ToString().ToUpperInvariant(),-5}] [{e.Module}] {e.Message}"; if (e.Exception != null) text += $"{Environment.NewLine} {e.Exception.Message}"; return text; } }