using System; using System.IO; using System.Threading.Tasks; using System.Threading.Channels; namespace PolyTraderSharp.Services { public enum LogLevel { Debug, Info, Warning, Error, Trade, TradeReasoning } public class LogMessageEventArgs : EventArgs { public string Message { get; } public LogLevel Level { get; } public DateTime Timestamp { get; } /// Korrelations-ID (z. B. SignalId) für die Log-Forensik; leer wenn ohne Kontext. public string CorrelationId { get; } public LogMessageEventArgs(string message, LogLevel level, string correlationId = "") { Message = message; Level = level; Timestamp = DateTime.Now; CorrelationId = correlationId ?? ""; } } /// /// Reiner JSONL-Formatter für Log-Events (S-0, Supervisor-Konzept): eine JSON-Zeile je Event – /// append-fähig, streambar, maschinen-/KI-lesbar. Statisch und seiteneffektfrei → unit-getestet. /// public static class LogJson { public static string Format(LogMessageEventArgs e) { var obj = new { ts = e.Timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffzzz"), level = e.Level.ToString(), cid = string.IsNullOrEmpty(e.CorrelationId) ? null : e.CorrelationId, msg = e.Message }; return System.Text.Json.JsonSerializer.Serialize(obj, new System.Text.Json.JsonSerializerOptions { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }); } /// Geparste JSONL-Zeile für den Log Viewer. public sealed record ParsedLogLine(string Time, string Level, string Cid, string Message); /// /// Parst eine JSONL-Zeile (Gegenstück zu ). Liefert null bei leeren/ /// beschädigten Zeilen (der Viewer überspringt sie, statt zu brechen). /// public static ParsedLogLine? ParseLine(string line) { if (string.IsNullOrWhiteSpace(line)) return null; try { using var doc = System.Text.Json.JsonDocument.Parse(line); var root = doc.RootElement; string ts = root.TryGetProperty("ts", out var t) ? t.GetString() ?? "" : ""; string level = root.TryGetProperty("level", out var l) ? l.GetString() ?? "" : ""; string cid = root.TryGetProperty("cid", out var c) ? c.GetString() ?? "" : ""; string msg = root.TryGetProperty("msg", out var m) ? m.GetString() ?? "" : ""; return new ParsedLogLine(ts, level, cid, msg); } catch (System.Text.Json.JsonException) { return null; } } } public class TerminalLogger { public event EventHandler? OnLogMessage; private readonly List _history = new(); private readonly object _lock = new(); private readonly string _logsDirectory; private readonly Channel _logChannel; public TerminalLogger() { _logsDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); if (!Directory.Exists(_logsDirectory)) { Directory.CreateDirectory(_logsDirectory); } _logChannel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = true }); Task.Run(ProcessLogQueueAsync); } private async Task ProcessLogQueueAsync() { await foreach (var e in _logChannel.Reader.ReadAllAsync()) { try { string dateStr = e.Timestamp.ToString("dd-MM-yyyy"); string fileName = $"{dateStr}-{e.Level}.log"; string fullPath = Path.Combine(_logsDirectory, fileName); // Remove Emojis (Surrogate pairs and common symbols) string safeMsg = System.Text.RegularExpressions.Regex.Replace(e.Message, @"\p{Cs}|[✅❌🌐📈🔴🧪ℹ️🚨🏆💰⬇️⬆️🔹🔸✨🔥📊📝🔄⏸️]", ""); safeMsg = safeMsg.Replace("\r\n", " | ").Replace("\n", " | ").Replace(" ", " ").Trim(); string logLine = $"[{e.Timestamp:HH:mm:ss}] {safeMsg}{Environment.NewLine}"; await File.AppendAllTextAsync(fullPath, logLine); // S-0: zusätzlich JSONL (eine Datei je Tag, alle Level) – maschinen-/KI-lesbar, // Grundlage für den Log Viewer. Dual-Sink; Text-Sink später abschaltbar. string jsonlPath = Path.Combine(_logsDirectory, $"{e.Timestamp:yyyy-MM-dd}.jsonl"); await File.AppendAllTextAsync(jsonlPath, LogJson.Format(e) + Environment.NewLine); } catch { // Ignored to prevent cascading lockups } } } public void Log(string message, LogLevel level = LogLevel.Info, string correlationId = "") { var e = new LogMessageEventArgs(message, level, correlationId); lock (_lock) { _history.Add(e); // Optimize list pruning to avoid heavy O(N) operations per log if (_history.Count > 10500) { // Remove older items efficiently in a batch int itemsToRemove = _history.Count - 9000; _history.RemoveRange(0, itemsToRemove); } } try { OnLogMessage?.Invoke(this, e); } catch { } // Standard Console output as fallback/debug Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [{level}] {message}"); _logChannel.Writer.TryWrite(e); } public void Info(string message) => Log(message, LogLevel.Info); public void Debug(string message) => Log(message, LogLevel.Debug); public void Warning(string message) => Log(message, LogLevel.Warning); public void Error(string message) => Log(message, LogLevel.Error); public void Trade(string message) => Log(message, LogLevel.Trade); public void TradeReasoning(string message) => Log(message, LogLevel.TradeReasoning); public List GetHistory(TimeSpan maxAge) { lock (_lock) { var cutoff = DateTime.Now - maxAge; return _history.Where(x => x.Timestamp >= cutoff).ToList(); } } } }