using EonaCat.Json; using EonaCat.LogStack.Extensions; using EonaCat.LogStack.LogClient.Models; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Json; using System.Text; using System.Threading; using System.Threading.Tasks; namespace EonaCat.LogStack.LogClient { public class EonaCatPayLoad { public List? Events { get; set; } } public class EonaCatLogEvent { public string Timestamp { get; set; } = default!; public string Level { get; set; } = default!; public string Message { get; set; } = default!; public string Category { get; set; } = default!; public ExceptionDto? Exception { get; set; } public Dictionary? Properties { get; set; } } public class ExceptionDto { public string Type { get; set; } = default!; public string Message { get; set; } = default!; public string? StackTrace { get; set; } } public class LogCentralClient : IDisposable { private readonly HttpClient _httpClient; private readonly LogCentralOptions _options; private readonly ConcurrentQueue _logQueue; private readonly Timer _flushTimer; private readonly SemaphoreSlim _flushSemaphore; private bool _disposed; public LogCentralClient(LogCentralOptions options) { _options = options ?? throw new ArgumentNullException(nameof(options)); _httpClient = new HttpClient { BaseAddress = new Uri(_options.ServerUrl) }; _httpClient.DefaultRequestHeaders.Add("X-API-Key", _options.ApiKey); _logQueue = new ConcurrentQueue(); _flushSemaphore = new SemaphoreSlim(1, 1); _flushTimer = new Timer(async _ => await FlushAsync(), null, TimeSpan.FromSeconds(_options.FlushIntervalSeconds), TimeSpan.FromSeconds(_options.FlushIntervalSeconds)); } public async Task LogAsync(LogEntry entry) { entry.Source = _options.ApplicationName; entry.Timestamp = DateTime.UtcNow; entry.Message ??= ""; var properties = new Dictionary(); if (_options.ApplicationName != null) properties.Add("ApplicationName", _options.ApplicationName); if (_options.ApplicationVersion != null) properties.Add("ApplicationVersion", _options.ApplicationVersion); if (_options.Environment != null) properties.Add("Environment", _options.Environment); if (!string.IsNullOrEmpty(entry.TraceId)) properties.Add("TraceId", entry.TraceId); entry.Properties = JsonHelper.ToJson(properties); _logQueue.Enqueue(entry); if (_logQueue.Count >= _options.BatchSize) { await FlushAsync(); } } private async Task FlushAsync() { if (_logQueue.IsEmpty) return; await _flushSemaphore.WaitAsync(); try { var batch = new List(); while (batch.Count < _options.BatchSize && _logQueue.TryDequeue(out var entry)) { batch.Add(entry); } if (batch.Count > 0) { await SendBatchToEonaCatAsync(batch); } } finally { _flushSemaphore.Release(); } } private async Task SendBatchToEonaCatAsync(List batch) { try { var eventsArray = batch.Select(e => new { timestamp = e.Timestamp.ToString("O"), level = e.Level, message = e.Message ?? "", // empty message is fine exception = string.IsNullOrEmpty(e.Exception) ? null : new { type = "Exception", message = e.Exception, stackTrace = e.StackTrace }, properties = string.IsNullOrEmpty(e.Properties) ? new Dictionary() // <-- same type now : JsonHelper.ToObject>(e.Properties) }).ToArray(); var json = JsonHelper.ToJson(eventsArray); using var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync("api/logs/eonacat", content); var responseContent = await response.Content.ReadAsStringAsync(); response.EnsureSuccessStatusCode(); } catch (Exception ex) { if (_options.EnableFallbackLogging) { Console.WriteLine($"[LogCentral] Failed to send logs to EonaCat: {ex.Message}"); } foreach (var entry in batch) { _logQueue.Enqueue(entry); } } } public async Task FlushAndDisposeAsync() { await FlushAsync(); Dispose(); } public void Dispose() { if (_disposed) return; _flushTimer?.Dispose(); FlushAsync().GetAwaiter().GetResult(); _httpClient?.Dispose(); _flushSemaphore?.Dispose(); _disposed = true; GC.SuppressFinalize(this); } } }