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;
}
}