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