Initial version
This commit is contained in:
186
EonaCat.LogStack.LogClient/LogCentralClient.cs
Normal file
186
EonaCat.LogStack.LogClient/LogCentralClient.cs
Normal file
@@ -0,0 +1,186 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user