Initial version

This commit is contained in:
2026-02-28 07:19:29 +01:00
parent b5f4af6930
commit 3f3356eb4a
183 changed files with 90406 additions and 63 deletions

View File

@@ -0,0 +1,207 @@
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;
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
namespace EonaCat.LogStack.LogClient
{
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.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<string, object>? 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<string, object>? properties = null)
{
await LogAsync(new LogEntry
{
Level = (int)LogLevel.Security,
Category = "Security",
Message = $"[{eventType}] {message}",
Properties = properties
});
}
public async Task LogAnalyticsAsync(string eventName,
Dictionary<string, object>? 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<LogEntry>();
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<LogEntry> 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<string, object>? Properties { get; set; }
public string? UserId { get; set; }
public string? SessionId { get; set; }
public string? RequestId { get; set; }
public string? CorrelationId { get; set; }
}
}