Files
EonaCat.LogStack/EonaCat.LogStack.LogClient/LogCentralClient.cs
EonaCat 977374ce02 Added EonaCat.LogStack.Status
Updated EonaCat.LogStack.LogClient to support EonaCat.LogStack.Status
2026-03-12 21:15:33 +01:00

164 lines
5.6 KiB
C#

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<EonaCatLogEvent>? 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<string, object?>? 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<LogEntry> _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<LogEntry>();
_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<string, object?>();
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<LogEntry>();
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<LogEntry> 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<string, object?>() // <-- same type now
: JsonHelper.ToObject<Dictionary<string, object?>>(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);
}
}
}