205 lines
6.7 KiB
C#
205 lines
6.7 KiB
C#
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<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; }
|
|
}
|
|
|
|
}
|