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 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.ApplicationName = _options.ApplicationName; entry.ApplicationVersion = _options.ApplicationVersion; entry.Environment = _options.Environment; entry.Timestamp = DateTime.UtcNow; entry.MachineName ??= Environment.MachineName; entry.Category ??= entry.Category ?? "Default"; entry.Message ??= entry.Message ?? ""; _logQueue.Enqueue(entry); if (_logQueue.Count >= _options.BatchSize) { await FlushAsync(); } } public async Task LogExceptionAsync(Exception ex, string message = "", Dictionary? properties = null) { await LogAsync(new LogEntry { Level = (int)LogLevel.Error, Category = "Exception", Message = message, Exception = ex.ToString(), StackTrace = ex.StackTrace, Properties = properties }); } public async Task LogSecurityEventAsync(string eventType, string message, Dictionary? properties = null) { await LogAsync(new LogEntry { Level = (int)LogLevel.Security, Category = "Security", Message = $"[{eventType}] {message}", Properties = properties }); } public async Task LogAnalyticsAsync(string eventName, Dictionary? properties = null) { await LogAsync(new LogEntry { Level = (int)LogLevel.Analytics, Category = "Analytics", Message = eventName, Properties = properties }); } 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 SendBatchAsync(batch); } } finally { _flushSemaphore.Release(); } } private async Task SendBatchAsync(List entries) { try { // Map EF entities to DTOs for API var dtos = entries.Select(e => new LogEntryDto { Id = e.Id, Timestamp = e.Timestamp, ApplicationName = e.ApplicationName, ApplicationVersion = e.ApplicationVersion, Environment = e.Environment, MachineName = e.MachineName, Level = e.Level, Category = e.Category, Message = e.Message, Exception = e.Exception, StackTrace = e.StackTrace, Properties = e.Properties, UserId = e.UserId, SessionId = e.SessionId, RequestId = e.RequestId, CorrelationId = e.CorrelationId }).ToList(); var response = await _httpClient.PostAsJsonAsync("/api/logs/batch", dtos); response.EnsureSuccessStatusCode(); } catch (Exception ex) { if (_options.EnableFallbackLogging) { Console.WriteLine($"[LogCentral] Failed to send logs: {ex.Message}"); } foreach (var entry in entries) { _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); } } public class LogEntryDto { public string Id { get; set; } = Guid.NewGuid().ToString(); public DateTime Timestamp { get; set; } public string ApplicationName { get; set; } = default!; public string ApplicationVersion { get; set; } = default!; public string Environment { get; set; } = default!; public string MachineName { get; set; } = default!; public int Level { get; set; } public string Category { get; set; } = default!; public string Message { get; set; } = default!; public string? Exception { get; set; } public string? StackTrace { get; set; } public Dictionary? Properties { get; set; } public string? UserId { get; set; } public string? SessionId { get; set; } public string? RequestId { get; set; } public string? CorrelationId { get; set; } } }