Removed some memory leaks

This commit is contained in:
2026-01-12 22:09:07 +01:00
parent 37c6609ba9
commit 576a590104
28 changed files with 1117 additions and 589 deletions

View File

@@ -3,7 +3,7 @@
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>EonaCat.Logger.LogClient</PackageId> <PackageId>EonaCat.Logger.LogClient</PackageId>
<Version>1.0.1</Version> <Version>1.0.2</Version>
<Authors>EonaCat (Jeroen Saey)</Authors> <Authors>EonaCat (Jeroen Saey)</Authors>
<Description>Logging client for the EonaCat Logger LogServer</Description> <Description>Logging client for the EonaCat Logger LogServer</Description>
<PackageTags>logging;monitoring;analytics;diagnostics</PackageTags> <PackageTags>logging;monitoring;analytics;diagnostics</PackageTags>
@@ -25,7 +25,7 @@
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="EonaCat.Logger" Version="1.5.2" /> <PackageReference Include="EonaCat.Logger" Version="1.5.3" />
<PackageReference Include="System.Net.Http.Json" Version="10.0.1" /> <PackageReference Include="System.Net.Http.Json" Version="10.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -93,7 +93,10 @@ namespace EonaCat.Logger.LogClient
private async Task FlushAsync() private async Task FlushAsync()
{ {
if (_logQueue.IsEmpty) return; if (_logQueue.IsEmpty)
{
return;
}
await _flushSemaphore.WaitAsync(); await _flushSemaphore.WaitAsync();
try try
@@ -166,7 +169,10 @@ namespace EonaCat.Logger.LogClient
public void Dispose() public void Dispose()
{ {
if (_disposed) return; if (_disposed)
{
return;
}
_flushTimer?.Dispose(); _flushTimer?.Dispose();
FlushAsync().GetAwaiter().GetResult(); FlushAsync().GetAwaiter().GetResult();

View File

@@ -23,7 +23,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<EVRevisionFormat>1.5.2+{chash:10}.{c:ymd}</EVRevisionFormat> <EVRevisionFormat>1.5.3+{chash:10}.{c:ymd}</EVRevisionFormat>
<EVDefault>true</EVDefault> <EVDefault>true</EVDefault>
<EVInfo>true</EVInfo> <EVInfo>true</EVInfo>
<EVTagMatch>v[0-9]*</EVTagMatch> <EVTagMatch>v[0-9]*</EVTagMatch>

View File

@@ -1,26 +1,32 @@
using Microsoft.Extensions.Logging; using EonaCat.Logger.EonaCatCoreLogger;
using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.Common; using System.Data.Common;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace EonaCat.Logger.EonaCatCoreLogger namespace EonaCat.Logger.EonaCatCoreLogger
{
public sealed class BatchingDatabaseLogger : ILogger, IDisposable
{ {
// This file is part of the EonaCat project(s) which is released under the Apache License. // 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. // See the LICENSE file or go to https://EonaCat.com/License for full license details.
public class BatchingDatabaseLogger : ILogger, IDisposable
{
private readonly string _categoryName; private readonly string _categoryName;
private readonly BatchingDatabaseLoggerOptions _options; private readonly BatchingDatabaseLoggerOptions _options;
private readonly LoggerScopedContext _context = new(); private readonly LoggerScopedContext _context = new();
private readonly BlockingCollection<LogEntry> _queue; private readonly BlockingCollection<LogEntry> _queue;
private readonly CancellationTokenSource _cts; private readonly CancellationTokenSource _cts = new();
private readonly Task _processingTask; private readonly Task _processingTask;
public bool IncludeCorrelationId { get; set; } private volatile bool _disposed;
private const int MaxQueueSize = 10_000;
public bool IncludeCorrelationId { get; }
public event EventHandler<Exception> OnException; public event EventHandler<Exception> OnException;
public BatchingDatabaseLogger(string categoryName, BatchingDatabaseLoggerOptions options) public BatchingDatabaseLogger(string categoryName, BatchingDatabaseLoggerOptions options)
@@ -29,22 +35,22 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_options = options; _options = options;
IncludeCorrelationId = options.IncludeCorrelationId; IncludeCorrelationId = options.IncludeCorrelationId;
_queue = new BlockingCollection<LogEntry>(new ConcurrentQueue<LogEntry>()); _queue = new BlockingCollection<LogEntry>(new ConcurrentQueue<LogEntry>(), MaxQueueSize);
_cts = new CancellationTokenSource(); _processingTask = Task.Run(() => ProcessQueueAsync(_cts.Token));
_processingTask = Task.Run(ProcessQueueAsync);
} }
public IDisposable BeginScope<TState>(TState state) => null; public IDisposable BeginScope<TState>(TState state) => _context.BeginScope(state);
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void SetContext(string key, string value) => _context.Set(key, value); public bool IsEnabled(LogLevel logLevel) => !_disposed && _options.IsEnabled;
public void ClearContext() => _context.Clear();
public string GetContext(string key) => _context.Get(key);
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, public void Log<TState>(
Exception exception, Func<TState, Exception, string> formatter) LogLevel logLevel,
EventId eventId,
TState state,
Exception exception,
Func<TState, Exception, string> formatter)
{ {
if (!IsEnabled(logLevel) || formatter == null) if (_disposed || !IsEnabled(logLevel) || formatter == null)
{ {
return; return;
} }
@@ -60,7 +66,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_context.Set("CorrelationId", correlationId); _context.Set("CorrelationId", correlationId);
} }
_queue.Add(new LogEntry var entry = new LogEntry
{ {
Timestamp = DateTime.UtcNow, Timestamp = DateTime.UtcNow,
LogLevel = logLevel.ToString(), LogLevel = logLevel.ToString(),
@@ -68,53 +74,52 @@ namespace EonaCat.Logger.EonaCatCoreLogger
Message = message, Message = message,
Exception = exception?.ToString(), Exception = exception?.ToString(),
CorrelationId = correlationId CorrelationId = correlationId
}); };
// Drop oldest when full (non-blocking)
while (!_queue.TryAdd(entry))
{
_queue.TryTake(out _); // discard oldest
}
} }
private async Task ProcessQueueAsync() private async Task ProcessQueueAsync(CancellationToken token)
{ {
var batch = new List<LogEntry>(); var batch = new List<LogEntry>(_options.BatchSize);
var timeoutMs = (int)Math.Min(_options.BatchInterval.TotalMilliseconds, int.MaxValue);
while (!_cts.Token.IsCancellationRequested)
{
try try
{ {
if (_queue.TryTake(out var logEntry, timeoutMs, _cts.Token)) foreach (var entry in _queue.GetConsumingEnumerable(token))
{ {
batch.Add(logEntry); batch.Add(entry);
// Drain the queue quickly without waiting if (batch.Count >= _options.BatchSize)
while (_queue.TryTake(out var additionalEntry))
{ {
batch.Add(additionalEntry); await FlushBatchAsync(batch);
} }
} }
if (batch.Count > 0)
{
await InsertBatchSafelyAsync(batch);
}
}
catch (OperationCanceledException)
{
break;
} }
catch (OperationCanceledException) { }
catch (Exception ex) catch (Exception ex)
{ {
OnException?.Invoke(this, ex); OnException?.Invoke(this, ex);
} }
} finally
{
// Final flush outside the loop
if (batch.Count > 0) if (batch.Count > 0)
{ {
await InsertBatchSafelyAsync(batch); await FlushBatchAsync(batch); // flush remaining
}
} }
} }
private async Task InsertBatchSafelyAsync(List<LogEntry> batch) private async Task FlushBatchAsync(List<LogEntry> batch)
{ {
if (batch.Count == 0)
{
return;
}
try try
{ {
await InsertBatchAsync(batch); await InsertBatchAsync(batch);
@@ -131,20 +136,16 @@ namespace EonaCat.Logger.EonaCatCoreLogger
private async Task InsertBatchAsync(List<LogEntry> batch) private async Task InsertBatchAsync(List<LogEntry> batch)
{ {
using var connection = _options.DbProviderFactory.CreateConnection(); using var connection = _options.DbProviderFactory.CreateConnection()
if (connection == null) ?? throw new InvalidOperationException("Failed to create database connection.");
{
throw new InvalidOperationException("Failed to create database connection.");
}
connection.ConnectionString = _options.ConnectionString; connection.ConnectionString = _options.ConnectionString;
await connection.OpenAsync(); await connection.OpenAsync(_cts.Token);
foreach (var entry in batch) foreach (var entry in batch)
{ {
using var command = connection.CreateCommand(); using var command = connection.CreateCommand();
command.CommandText = _options.InsertCommand; command.CommandText = _options.InsertCommand;
command.Parameters.Clear();
command.Parameters.Add(CreateParameter(command, "Timestamp", entry.Timestamp)); command.Parameters.Add(CreateParameter(command, "Timestamp", entry.Timestamp));
command.Parameters.Add(CreateParameter(command, "LogLevel", entry.LogLevel)); command.Parameters.Add(CreateParameter(command, "LogLevel", entry.LogLevel));
@@ -153,11 +154,11 @@ namespace EonaCat.Logger.EonaCatCoreLogger
command.Parameters.Add(CreateParameter(command, "Exception", entry.Exception)); command.Parameters.Add(CreateParameter(command, "Exception", entry.Exception));
command.Parameters.Add(CreateParameter(command, "CorrelationId", entry.CorrelationId)); command.Parameters.Add(CreateParameter(command, "CorrelationId", entry.CorrelationId));
await command.ExecuteNonQueryAsync(); await command.ExecuteNonQueryAsync(_cts.Token);
} }
} }
private DbParameter CreateParameter(DbCommand command, string name, object value) private static DbParameter CreateParameter(DbCommand command, string name, object value)
{ {
var param = command.CreateParameter(); var param = command.CreateParameter();
param.ParameterName = $"@{name}"; param.ParameterName = $"@{name}";
@@ -167,26 +168,34 @@ namespace EonaCat.Logger.EonaCatCoreLogger
public void Dispose() public void Dispose()
{ {
if (_disposed)
{
return;
}
_disposed = true;
_cts.Cancel(); _cts.Cancel();
_queue.CompleteAdding(); _queue.CompleteAdding();
try try
{ {
_processingTask.Wait(_options.ShutdownTimeout); _processingTask.Wait(_options.ShutdownTimeout);
} }
catch (Exception ex) catch { /* ignore */ }
{
OnException?.Invoke(this, ex); _queue.Dispose();
} _cts.Dispose();
} }
private class LogEntry private sealed class LogEntry
{ {
public DateTime Timestamp { get; set; } public DateTime Timestamp { get; init; }
public string LogLevel { get; set; } public string LogLevel { get; init; }
public string Category { get; set; } public string Category { get; init; }
public string Message { get; set; } public string Message { get; init; }
public string Exception { get; set; } public string Exception { get; init; }
public string CorrelationId { get; set; } public string CorrelationId { get; init; }
} }
} }
} }

View File

@@ -15,6 +15,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
public bool IsEnabled { get; set; } = true; public bool IsEnabled { get; set; } = true;
public bool IncludeCorrelationId { get; set; } public bool IncludeCorrelationId { get; set; }
public int BatchSize { get; set; } = 50;
public TimeSpan BatchInterval { get; set; } = TimeSpan.FromSeconds(5); // 5 sec batch public TimeSpan BatchInterval { get; set; } = TimeSpan.FromSeconds(5); // 5 sec batch
public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(10); // wait on shutdown public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(10); // wait on shutdown

View File

@@ -6,19 +6,24 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace EonaCat.Logger.EonaCatCoreLogger namespace EonaCat.Logger.EonaCatCoreLogger
{
public sealed class DatabaseLogger : ILogger, IDisposable
{ {
// This file is part of the EonaCat project(s) which is released under the Apache License. // 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. // See the LICENSE file or go to https://EonaCat.com/License for full license details.
public class DatabaseLogger : ILogger, IDisposable
{
private readonly string _categoryName; private readonly string _categoryName;
private readonly DatabaseLoggerOptions _options; private readonly DatabaseLoggerOptions _options;
private readonly LoggerScopedContext _context = new(); private readonly LoggerScopedContext _context = new();
private static readonly ConcurrentQueue<LogEntry> _queue = new(); private static readonly ConcurrentQueue<LogEntry> _queue = new();
private static readonly SemaphoreSlim _flushLock = new(1, 1); private static readonly SemaphoreSlim _flushLock = new(1, 1);
private static bool _flushLoopStarted = false;
private static CancellationTokenSource _cts; private static CancellationTokenSource _cts = new();
private static Task _flushTask;
private static int _refCount;
private const int MaxQueueSize = 10_000;
public bool IncludeCorrelationId { get; set; } public bool IncludeCorrelationId { get; set; }
public event EventHandler<Exception> OnException; public event EventHandler<Exception> OnException;
@@ -29,69 +34,88 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_options = options; _options = options;
IncludeCorrelationId = options.IncludeCorrelationId; IncludeCorrelationId = options.IncludeCorrelationId;
if (!_flushLoopStarted) if (Interlocked.Increment(ref _refCount) == 1)
{ {
_flushLoopStarted = true; _flushTask = Task.Run(() => FlushLoopAsync(_cts.Token));
_cts = new CancellationTokenSource();
Task.Run(() => FlushLoopAsync(_cts.Token));
} }
} }
public IDisposable BeginScope<TState>(TState state) => null; public IDisposable BeginScope<TState>(TState state)
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled; => _context.BeginScope(state);
public void SetContext(string key, string value) => _context.Set(key, value); public bool IsEnabled(LogLevel logLevel)
public void ClearContext() => _context.Clear(); => _options.IsEnabled;
public string GetContext(string key) => _context.Get(key);
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, public void SetContext(string key, string value)
Exception exception, Func<TState, Exception, string> formatter) => _context.Set(key, value);
public void ClearContext()
=> _context.Clear();
public string GetContext(string key)
=> _context.Get(key);
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception exception,
Func<TState, Exception, string> formatter)
{ {
if (!IsEnabled(logLevel) || formatter == null) if (!IsEnabled(logLevel) || formatter == null)
{ {
return; return;
} }
if (_queue.Count >= MaxQueueSize)
{
_queue.TryDequeue(out _); // drop oldest
}
if (IncludeCorrelationId) if (IncludeCorrelationId)
{ {
var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString(); var correlationId =
_context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
_context.Set("CorrelationId", correlationId); _context.Set("CorrelationId", correlationId);
} }
var message = formatter(state, exception); _queue.Enqueue(new LogEntry
var entry = new LogEntry
{ {
Timestamp = DateTime.UtcNow, Timestamp = DateTime.UtcNow,
LogLevel = logLevel.ToString(), LogLevel = logLevel.ToString(),
Category = _categoryName, Category = _categoryName,
Message = message, Message = formatter(state, exception),
Exception = exception?.ToString(), Exception = exception?.ToString(),
CorrelationId = _context.Get("CorrelationId") CorrelationId = _context.Get("CorrelationId")
}; });
_queue.Enqueue(entry);
if (_queue.Count >= _options.FlushBatchSize)
{
_ = FlushBufferAsync();
}
} }
private async Task FlushLoopAsync(CancellationToken token) private async Task FlushLoopAsync(CancellationToken token)
{
try
{ {
while (!token.IsCancellationRequested) while (!token.IsCancellationRequested)
{ {
await Task.Delay(TimeSpan.FromSeconds(_options.FlushIntervalSeconds), token); await Task.Delay(
TimeSpan.FromSeconds(_options.FlushIntervalSeconds),
token);
await FlushBufferAsync(); await FlushBufferAsync();
} }
} }
catch (OperationCanceledException)
{
// Expected on shutdown
}
await FlushBufferAsync(); // final drain
}
private async Task FlushBufferAsync() private async Task FlushBufferAsync()
{ {
if (!await _flushLock.WaitAsync(0)) if (!await _flushLock.WaitAsync(0))
{ {
return; // Another flush is in progress return;
} }
try try
@@ -100,10 +124,12 @@ namespace EonaCat.Logger.EonaCatCoreLogger
{ {
try try
{ {
using var connection = _options.DbProviderFactory.CreateConnection(); using var connection =
_options.DbProviderFactory.CreateConnection();
if (connection == null) if (connection == null)
{ {
throw new InvalidOperationException("Failed to create database connection."); throw new InvalidOperationException("Failed to create DB connection.");
} }
connection.ConnectionString = _options.ConnectionString; connection.ConnectionString = _options.ConnectionString;
@@ -111,7 +137,6 @@ namespace EonaCat.Logger.EonaCatCoreLogger
using var command = connection.CreateCommand(); using var command = connection.CreateCommand();
command.CommandText = _options.InsertCommand; command.CommandText = _options.InsertCommand;
command.Parameters.Clear();
command.Parameters.Add(CreateParameter(command, "Timestamp", entry.Timestamp)); command.Parameters.Add(CreateParameter(command, "Timestamp", entry.Timestamp));
command.Parameters.Add(CreateParameter(command, "LogLevel", entry.LogLevel)); command.Parameters.Add(CreateParameter(command, "LogLevel", entry.LogLevel));
@@ -134,7 +159,10 @@ namespace EonaCat.Logger.EonaCatCoreLogger
} }
} }
private DbParameter CreateParameter(DbCommand command, string name, object value) private static DbParameter CreateParameter(
DbCommand command,
string name,
object value)
{ {
var param = command.CreateParameter(); var param = command.CreateParameter();
param.ParameterName = $"@{name}"; param.ParameterName = $"@{name}";
@@ -144,19 +172,28 @@ namespace EonaCat.Logger.EonaCatCoreLogger
public void Dispose() public void Dispose()
{ {
_cts?.Cancel(); if (Interlocked.Decrement(ref _refCount) == 0)
_flushLock.Dispose(); {
_cts?.Dispose(); _cts.Cancel();
try
{
_flushTask?.Wait();
}
catch (AggregateException) { }
_cts.Dispose();
}
} }
private class LogEntry private sealed class LogEntry
{ {
public DateTime Timestamp { get; set; } public DateTime Timestamp { get; init; }
public string LogLevel { get; set; } public string LogLevel { get; init; }
public string Category { get; set; } public string Category { get; init; }
public string Message { get; set; } public string Message { get; init; }
public string Exception { get; set; } public string Exception { get; init; }
public string CorrelationId { get; set; } public string CorrelationId { get; init; }
} }
} }
} }

View File

@@ -8,36 +8,42 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace EonaCat.Logger.EonaCatCoreLogger
{
// This file is part of the EonaCat project(s) which is released under the Apache License. // 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. // See the LICENSE file or go to https://EonaCat.com/License for full license details.
namespace EonaCat.Logger.EonaCatCoreLogger
{
public class DiscordLogger : ILogger, IDisposable public class DiscordLogger : ILogger, IDisposable
{ {
private readonly string _categoryName; private readonly string _categoryName;
private readonly DiscordLoggerOptions _options; private readonly DiscordLoggerOptions _options;
private readonly LoggerScopedContext _context = new(); private readonly LoggerScopedContext _context = new LoggerScopedContext();
// Static shared resources
private static readonly ConcurrentQueue<string> _messageQueue = new ConcurrentQueue<string>();
private static readonly HttpClient _httpClient = new HttpClient();
private static readonly SemaphoreSlim _flushLock = new SemaphoreSlim(1, 1);
private static readonly ConcurrentQueue<string> _messageQueue = new();
private static readonly HttpClient _httpClient = new();
private static readonly SemaphoreSlim _flushLock = new(1, 1);
private static bool _flushLoopStarted = false; private static bool _flushLoopStarted = false;
private static CancellationTokenSource _cts; private static CancellationTokenSource _cts;
private static Task _flushTask;
public bool IncludeCorrelationId { get; set; } public bool IncludeCorrelationId { get; set; }
public event EventHandler<Exception> OnException; public event EventHandler<Exception> OnException;
public DiscordLogger(string categoryName, DiscordLoggerOptions options) public DiscordLogger(string categoryName, DiscordLoggerOptions options)
{ {
_categoryName = categoryName; _categoryName = categoryName ?? throw new ArgumentNullException(nameof(categoryName));
_options = options; _options = options ?? throw new ArgumentNullException(nameof(options));
IncludeCorrelationId = options.IncludeCorrelationId; IncludeCorrelationId = options.IncludeCorrelationId;
// Start flush loop once
if (!_flushLoopStarted) if (!_flushLoopStarted)
{ {
_flushLoopStarted = true; _flushLoopStarted = true;
_cts = new CancellationTokenSource(); _cts = new CancellationTokenSource();
_ = Task.Run(() => FlushLoopAsync(_cts.Token)); _flushTask = Task.Factory.StartNew(() => FlushLoop(_cts.Token),
_cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
} }
} }
@@ -69,7 +75,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
$"**[{DateTime.UtcNow:u}]**", $"**[{DateTime.UtcNow:u}]**",
$"**[{logLevel}]**", $"**[{logLevel}]**",
$"**[{_categoryName}]**", $"**[{_categoryName}]**",
$"Message: {message}", $"Message: {message}"
}; };
foreach (var kvp in _context.GetAll()) foreach (var kvp in _context.GetAll())
@@ -82,26 +88,34 @@ namespace EonaCat.Logger.EonaCatCoreLogger
logParts.Add($"Exception: {exception}"); logParts.Add($"Exception: {exception}");
} }
string fullMessage = string.Join("\n", logParts); _messageQueue.Enqueue(string.Join("\n", logParts));
// Enqueue and return immediately - non-blocking
_messageQueue.Enqueue(fullMessage);
} }
private async Task FlushLoopAsync(CancellationToken token) private void FlushLoop(CancellationToken token)
{
try
{ {
while (!token.IsCancellationRequested) while (!token.IsCancellationRequested)
{ {
await Task.Delay(TimeSpan.FromSeconds(_options.FlushIntervalSeconds), token); Thread.Sleep(TimeSpan.FromSeconds(_options.FlushIntervalSeconds));
await FlushBufferAsync(); FlushBuffer(token).Wait(token);
}
}
catch (OperationCanceledException)
{
// Expected when cancelling
}
catch (Exception ex)
{
OnException?.Invoke(this, ex);
} }
} }
private async Task FlushBufferAsync() private async Task FlushBuffer(CancellationToken token)
{ {
if (!await _flushLock.WaitAsync(0)) if (!await _flushLock.WaitAsync(0, token))
{ {
return; // Already flushing return;
} }
try try
@@ -111,14 +125,16 @@ namespace EonaCat.Logger.EonaCatCoreLogger
try try
{ {
var payload = new { content = message }; var payload = new { content = message };
var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json"); using (var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json"))
var response = await _httpClient.PostAsync(_options.WebhookUrl, content); {
var response = await _httpClient.PostAsync(_options.WebhookUrl, content, token);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
var error = await response.Content.ReadAsStringAsync(); var error = await response.Content.ReadAsStringAsync();
OnException?.Invoke(this, new Exception($"Discord webhook failed: {response.StatusCode} {error}")); OnException?.Invoke(this, new Exception($"Discord webhook failed: {response.StatusCode} {error}"));
} }
} }
}
catch (Exception ex) catch (Exception ex)
{ {
OnException?.Invoke(this, ex); OnException?.Invoke(this, ex);
@@ -133,9 +149,24 @@ namespace EonaCat.Logger.EonaCatCoreLogger
public void Dispose() public void Dispose()
{ {
_cts?.Cancel(); if (_cts != null && !_cts.IsCancellationRequested)
{
_cts.Cancel();
try
{
_flushTask?.Wait();
}
catch (AggregateException ae)
{
ae.Handle(e => e is OperationCanceledException);
}
_cts.Dispose();
_cts = null;
}
_flushLock.Dispose(); _flushLock.Dispose();
_cts?.Dispose();
} }
} }
} }

View File

@@ -1,32 +1,32 @@
using System.Linq; using EonaCat.Json;
// 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.Logger.EonaCatCoreLogger
{
using EonaCat.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Text.Json; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
public class ElasticSearchLogger : ILogger namespace EonaCat.Logger.EonaCatCoreLogger
{ {
// 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.
public class ElasticSearchLogger : ILogger, IDisposable
{
private readonly string _categoryName; private readonly string _categoryName;
private readonly ElasticSearchLoggerOptions _options; private readonly ElasticSearchLoggerOptions _options;
private static readonly HttpClient _httpClient = new HttpClient(); private readonly HttpClient _httpClient;
private static readonly List<string> _buffer = new List<string>(); private readonly List<string> _buffer = new();
private static readonly object _lock = new object(); private readonly object _lock = new();
private static bool _flushLoopStarted = false; private readonly CancellationTokenSource _cts = new();
public event EventHandler<Exception> OnException; private readonly Task _flushTask;
public event EventHandler<string> OnInvalidStatusCode;
private readonly LoggerScopedContext _context = new(); private readonly LoggerScopedContext _context = new();
public bool IncludeCorrelationId { get; set; } public bool IncludeCorrelationId { get; set; }
public event EventHandler<Exception> OnException;
public event EventHandler<string> OnInvalidStatusCode;
public ElasticSearchLogger(string categoryName, ElasticSearchLoggerOptions options) public ElasticSearchLogger(string categoryName, ElasticSearchLoggerOptions options)
{ {
@@ -34,18 +34,15 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_options = options; _options = options;
IncludeCorrelationId = options.IncludeCorrelationId; IncludeCorrelationId = options.IncludeCorrelationId;
if (!_flushLoopStarted) _httpClient = new HttpClient();
{ _flushTask = Task.Run(() => FlushLoopAsync(_cts.Token));
_flushLoopStarted = true;
Task.Run(FlushLoopAsync);
}
} }
public void SetContext(string key, string value) => _context.Set(key, value); public void SetContext(string key, string value) => _context.Set(key, value);
public void ClearContext() => _context.Clear(); public void ClearContext() => _context.Clear();
public string GetContext(string key) => _context.Get(key); public string GetContext(string key) => _context.Get(key);
public IDisposable BeginScope<TState>(TState state) => null; public IDisposable BeginScope<TState>(TState state) => _context.BeginScope(state);
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled; public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
@@ -56,14 +53,12 @@ namespace EonaCat.Logger.EonaCatCoreLogger
return; return;
} }
// Get correlation ID from context or generate new one
if (IncludeCorrelationId) if (IncludeCorrelationId)
{ {
var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString(); var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
_context.Set("CorrelationId", correlationId); _context.Set("CorrelationId", correlationId);
} }
// Prepare the log entry
var logDoc = new var logDoc = new
{ {
timestamp = DateTime.UtcNow, timestamp = DateTime.UtcNow,
@@ -75,32 +70,38 @@ namespace EonaCat.Logger.EonaCatCoreLogger
customContext = _context.GetAll() customContext = _context.GetAll()
}; };
// Serialize log to JSON
string json = JsonHelper.ToJson(logDoc); string json = JsonHelper.ToJson(logDoc);
// Add to the buffer
lock (_lock) lock (_lock)
{ {
_buffer.Add(json); _buffer.Add(json);
if (_buffer.Count >= _options.RetryBufferSize) // Optional: drop oldest if buffer is too large
if (_buffer.Count > _options.MaxBufferSize)
{ {
_ = FlushBufferAsync(); _buffer.RemoveAt(0);
} }
} }
} }
private async Task FlushLoopAsync() private async Task FlushLoopAsync(CancellationToken token)
{ {
while (true) try
{ {
await Task.Delay(TimeSpan.FromSeconds(_options.FlushIntervalSeconds)); while (!token.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(_options.FlushIntervalSeconds), token);
await FlushBufferAsync(); await FlushBufferAsync();
} }
} }
catch (OperationCanceledException) { }
await FlushBufferAsync(); // flush remaining logs on shutdown
}
private async Task FlushBufferAsync() private async Task FlushBufferAsync()
{ {
List<string> toSend; List<string> toSend;
lock (_lock) lock (_lock)
{ {
if (_buffer.Count == 0) if (_buffer.Count == 0)
@@ -112,27 +113,23 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_buffer.Clear(); _buffer.Clear();
} }
// Elasticsearch URL with dynamic index
string indexName = $"{_options.IndexName}-{DateTime.UtcNow:yyyy.MM.dd}"; string indexName = $"{_options.IndexName}-{DateTime.UtcNow:yyyy.MM.dd}";
string url = $"{_options.Uri.TrimEnd('/')}/{(_options.UseBulkInsert ? "_bulk" : indexName + "/_doc")}"; string url = $"{_options.Uri.TrimEnd('/')}/{(_options.UseBulkInsert ? "_bulk" : indexName + "/_doc")}";
var request = new HttpRequestMessage(HttpMethod.Post, url); using var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Headers.Accept.ParseAdd("application/json"); request.Headers.Accept.ParseAdd("application/json");
// Add authentication headers if configured
if (!string.IsNullOrWhiteSpace(_options.Username)) if (!string.IsNullOrWhiteSpace(_options.Username))
{ {
var authToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_options.Username}:{_options.Password}")); var authToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_options.Username}:{_options.Password}"));
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", authToken); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", authToken);
} }
// Add custom headers (static ones)
foreach (var header in _options.CustomHeaders) foreach (var header in _options.CustomHeaders)
{ {
request.Headers.TryAddWithoutValidation(header.Key, header.Value); request.Headers.TryAddWithoutValidation(header.Key, header.Value);
} }
// Add template headers (dynamic ones based on log data)
var dynamicHeaders = new Dictionary<string, string> var dynamicHeaders = new Dictionary<string, string>
{ {
{ "index", _options.IndexName }, { "index", _options.IndexName },
@@ -146,13 +143,11 @@ namespace EonaCat.Logger.EonaCatCoreLogger
request.Headers.TryAddWithoutValidation(header.Key, value); request.Headers.TryAddWithoutValidation(header.Key, value);
} }
// Add context headers (correlationId, custom context)
foreach (var kv in _context.GetAll()) foreach (var kv in _context.GetAll())
{ {
request.Headers.TryAddWithoutValidation($"X-Context-{kv.Key}", kv.Value); request.Headers.TryAddWithoutValidation($"X-Context-{kv.Key}", kv.Value);
} }
// Prepare the content for the request
request.Content = new StringContent( request.Content = new StringContent(
_options.UseBulkInsert _options.UseBulkInsert
? string.Join("\n", toSend.Select(d => $"{{\"index\":{{}}}}\n{d}")) + "\n" ? string.Join("\n", toSend.Select(d => $"{{\"index\":{{}}}}\n{d}")) + "\n"
@@ -170,9 +165,9 @@ namespace EonaCat.Logger.EonaCatCoreLogger
OnInvalidStatusCode?.Invoke(this, $"ElasticSearch request failed: {response.StatusCode}, {errorContent}"); OnInvalidStatusCode?.Invoke(this, $"ElasticSearch request failed: {response.StatusCode}, {errorContent}");
} }
} }
catch (Exception exception) catch (Exception ex)
{ {
OnException?.Invoke(this, exception); OnException?.Invoke(this, ex);
} }
} }
@@ -182,7 +177,16 @@ namespace EonaCat.Logger.EonaCatCoreLogger
{ {
template = template.Replace($"{{{kv.Key}}}", kv.Value); template = template.Replace($"{{{kv.Key}}}", kv.Value);
} }
return template; return template;
} }
public void Dispose()
{
_cts.Cancel();
_flushTask.Wait();
_cts.Dispose();
_httpClient.Dispose();
}
} }
} }

View File

@@ -20,5 +20,6 @@ namespace EonaCat.Logger.EonaCatCoreLogger
public int FlushIntervalSeconds { get; set; } = 5; public int FlushIntervalSeconds { get; set; } = 5;
public bool UseBulkInsert { get; set; } = true; public bool UseBulkInsert { get; set; } = true;
public bool IncludeCorrelationId { get; set; } public bool IncludeCorrelationId { get; set; }
public int MaxBufferSize { get; set; } = 1000;
} }
} }

View File

@@ -3,22 +3,27 @@ using Microsoft.Extensions.Logging;
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace EonaCat.Logger.EonaCatCoreLogger namespace EonaCat.Logger.EonaCatCoreLogger
{
public sealed class HttpLogger : ILogger, IDisposable
{ {
// This file is part of the EonaCat project(s) which is released under the Apache License. // 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. // See the LICENSE file or go to https://EonaCat.com/License for full license details.
public class HttpLogger : ILogger
{
private readonly string _categoryName; private readonly string _categoryName;
private readonly HttpLoggerOptions _options; private readonly HttpLoggerOptions _options;
private readonly LoggerScopedContext _context = new(); private readonly LoggerScopedContext _context = new();
private static readonly HttpClient _client = new(); private readonly HttpClient _client;
private readonly BlockingCollection<Dictionary<string, object>> _queue;
private readonly CancellationTokenSource _cts;
private readonly Task _processingTask;
public bool IncludeCorrelationId { get; set; } public bool IncludeCorrelationId { get; set; }
public event EventHandler<Exception> OnException; public event EventHandler<Exception> OnException;
public event EventHandler<string> OnInvalidStatusCode; public event EventHandler<string> OnInvalidStatusCode;
@@ -27,13 +32,18 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_categoryName = categoryName; _categoryName = categoryName;
_options = options; _options = options;
IncludeCorrelationId = options.IncludeCorrelationId; IncludeCorrelationId = options.IncludeCorrelationId;
_client = new HttpClient();
_queue = new BlockingCollection<Dictionary<string, object>>(boundedCapacity: 10000);
_cts = new CancellationTokenSource();
_processingTask = Task.Run(() => ProcessQueueAsync(_cts.Token));
} }
public void SetContext(string key, string value) => _context.Set(key, value); public void SetContext(string key, string value) => _context.Set(key, value);
public void ClearContext() => _context.Clear(); public void ClearContext() => _context.Clear();
public string GetContext(string key) => _context.Get(key); public string GetContext(string key) => _context.Get(key);
public IDisposable BeginScope<TState>(TState state) => null; public IDisposable BeginScope<TState>(TState state) => _context.BeginScope(state);
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled; public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
@@ -44,8 +54,6 @@ namespace EonaCat.Logger.EonaCatCoreLogger
return; return;
} }
string message = formatter(state, exception);
if (IncludeCorrelationId) if (IncludeCorrelationId)
{ {
var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString(); var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
@@ -57,7 +65,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
{ "timestamp", DateTime.UtcNow }, { "timestamp", DateTime.UtcNow },
{ "level", logLevel.ToString() }, { "level", logLevel.ToString() },
{ "category", _categoryName }, { "category", _categoryName },
{ "message", message }, { "message", formatter(state, exception) },
{ "eventId", eventId.Id } { "eventId", eventId.Id }
}; };
@@ -67,66 +75,54 @@ namespace EonaCat.Logger.EonaCatCoreLogger
payload["context"] = contextData; payload["context"] = contextData;
} }
Task.Run(async () => // Add to queue, drop oldest if full
while (!_queue.TryAdd(payload))
{
_queue.TryTake(out _); // drop oldest
}
}
private async Task ProcessQueueAsync(CancellationToken token)
{
try
{
foreach (var payload in _queue.GetConsumingEnumerable(token))
{ {
try try
{ {
var content = _options.SendAsJson var content = _options.SendAsJson
? new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json") ? new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json")
: new StringContent(message, Encoding.UTF8, "text/plain"); : new StringContent(payload["message"].ToString(), Encoding.UTF8, "text/plain");
var response = await _client.PostAsync(_options.Endpoint, content); var response = await _client.PostAsync(_options.Endpoint, content, token);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
var statusCode = response.StatusCode; OnInvalidStatusCode?.Invoke(this, response.StatusCode.ToString());
var statusCodeMessage = statusCode switch
{
System.Net.HttpStatusCode.BadRequest => "400 Bad Request",
System.Net.HttpStatusCode.Unauthorized => "401 Unauthorized",
System.Net.HttpStatusCode.Forbidden => "403 Forbidden",
System.Net.HttpStatusCode.NotFound => "404 Not Found",
System.Net.HttpStatusCode.MethodNotAllowed => "405 Method Not Allowed",
System.Net.HttpStatusCode.NotAcceptable => "406 Not Acceptable",
System.Net.HttpStatusCode.ProxyAuthenticationRequired => "407 Proxy Authentication Required",
System.Net.HttpStatusCode.RequestTimeout => "408 Request Timeout",
System.Net.HttpStatusCode.Conflict => "409 Conflict",
System.Net.HttpStatusCode.Gone => "410 Gone",
System.Net.HttpStatusCode.LengthRequired => "411 Length Required",
System.Net.HttpStatusCode.PreconditionFailed => "412 Precondition Failed",
System.Net.HttpStatusCode.RequestEntityTooLarge => "413 Request Entity Too Large",
System.Net.HttpStatusCode.RequestUriTooLong => "414 Request URI Too Long",
System.Net.HttpStatusCode.UnsupportedMediaType => "415 Unsupported Media Type",
System.Net.HttpStatusCode.RequestedRangeNotSatisfiable => "416 Requested Range Not Satisfiable",
System.Net.HttpStatusCode.ExpectationFailed => "417 Expectation Failed",
(System.Net.HttpStatusCode)418 => "418 I'm a teapot",
(System.Net.HttpStatusCode)421 => "421 Misdirected Request",
(System.Net.HttpStatusCode)422 => "422 Unprocessable Entity",
(System.Net.HttpStatusCode)423 => "423 Locked",
(System.Net.HttpStatusCode)424 => "424 Failed Dependency",
(System.Net.HttpStatusCode)425 => "425 Too Early",
(System.Net.HttpStatusCode)426 => "426 Upgrade Required",
(System.Net.HttpStatusCode)428 => "428 Precondition Required",
(System.Net.HttpStatusCode)429 => "429 Too Many Requests",
(System.Net.HttpStatusCode)431 => "431 Request Header Fields Too Large",
(System.Net.HttpStatusCode)451 => "451 Unavailable For Legal Reasons",
System.Net.HttpStatusCode.InternalServerError => "500 Internal Server Error",
System.Net.HttpStatusCode.NotImplemented => "501 Not Implemented",
System.Net.HttpStatusCode.BadGateway => "502 Bad Gateway",
System.Net.HttpStatusCode.ServiceUnavailable => "503 Service Unavailable",
System.Net.HttpStatusCode.GatewayTimeout => "504 Gateway Timeout",
System.Net.HttpStatusCode.HttpVersionNotSupported => "505 HTTP Version Not Supported",
_ => statusCode.ToString()
};
OnInvalidStatusCode?.Invoke(this, statusCodeMessage);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
OnException?.Invoke(this, ex); OnException?.Invoke(this, ex);
} }
}); }
}
catch (OperationCanceledException) { }
}
public void Dispose()
{
_cts.Cancel();
_queue.CompleteAdding();
try
{
_processingTask.Wait();
}
catch { /* ignore */ }
_queue.Dispose();
_cts.Dispose();
_client.Dispose();
} }
} }
} }

View File

@@ -1,22 +1,27 @@
using EonaCat.Json; using EonaCat.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
namespace EonaCat.Logger.EonaCatCoreLogger namespace EonaCat.Logger.EonaCatCoreLogger
{
public sealed class JsonFileLogger : ILogger, IDisposable
{ {
// This file is part of the EonaCat project(s) which is released under the Apache License. // 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. // See the LICENSE file or go to https://EonaCat.com/License for full license details.
public class JsonFileLogger : ILogger
{
private readonly string _categoryName; private readonly string _categoryName;
private readonly JsonFileLoggerOptions _options; private readonly JsonFileLoggerOptions _options;
private readonly string _filePath; private readonly string _filePath;
private readonly LoggerScopedContext _context = new(); private readonly LoggerScopedContext _context = new();
private static readonly SemaphoreSlim _fileLock = new(1, 1);
private readonly BlockingCollection<string> _queue;
private readonly Thread _workerThread;
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
public bool IncludeCorrelationId { get; set; } public bool IncludeCorrelationId { get; set; }
public event EventHandler<Exception> OnException; public event EventHandler<Exception> OnException;
@@ -25,9 +30,11 @@ namespace EonaCat.Logger.EonaCatCoreLogger
{ {
_categoryName = categoryName; _categoryName = categoryName;
_options = options; _options = options;
_filePath = Path.Combine(_options.LogDirectory, _options.FileName);
IncludeCorrelationId = options.IncludeCorrelationId; IncludeCorrelationId = options.IncludeCorrelationId;
_filePath = Path.Combine(_options.LogDirectory, _options.FileName);
_queue = new BlockingCollection<string>(10000); // bounded queue
try try
{ {
Directory.CreateDirectory(_options.LogDirectory); Directory.CreateDirectory(_options.LogDirectory);
@@ -36,13 +43,16 @@ namespace EonaCat.Logger.EonaCatCoreLogger
{ {
OnException?.Invoke(this, ex); OnException?.Invoke(this, ex);
} }
// Start background thread
_workerThread = new Thread(ProcessQueue) { IsBackground = true };
_workerThread.Start();
} }
public void SetContext(string key, string value) => _context.Set(key, value); public void SetContext(string key, string value) => _context.Set(key, value);
public void ClearContext() => _context.Clear(); public void ClearContext() => _context.Clear();
public string GetContext(string key) => _context.Get(key); public string GetContext(string key) => _context.Get(key);
public IDisposable BeginScope<TState>(TState state) => _context.BeginScope(state);
public IDisposable BeginScope<TState>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled; public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
@@ -55,8 +65,6 @@ namespace EonaCat.Logger.EonaCatCoreLogger
try try
{ {
string message = formatter(state, exception);
if (IncludeCorrelationId) if (IncludeCorrelationId)
{ {
var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString(); var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
@@ -68,7 +76,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
{ "timestamp", DateTime.UtcNow }, { "timestamp", DateTime.UtcNow },
{ "level", logLevel.ToString() }, { "level", logLevel.ToString() },
{ "category", _categoryName }, { "category", _categoryName },
{ "message", message }, { "message", formatter(state, exception) },
{ "exception", exception?.ToString() }, { "exception", exception?.ToString() },
{ "eventId", eventId.Id } { "eventId", eventId.Id }
}; };
@@ -81,14 +89,10 @@ namespace EonaCat.Logger.EonaCatCoreLogger
string json = JsonHelper.ToJson(logObject); string json = JsonHelper.ToJson(logObject);
_fileLock.Wait(); // Enqueue, drop oldest if full
try while (!_queue.TryAdd(json))
{ {
File.AppendAllText(_filePath, json + Environment.NewLine, Encoding.UTF8); _queue.TryTake(out _); // drop oldest
}
finally
{
_fileLock.Release();
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -96,5 +100,39 @@ namespace EonaCat.Logger.EonaCatCoreLogger
OnException?.Invoke(this, ex); OnException?.Invoke(this, ex);
} }
} }
private void ProcessQueue()
{
try
{
foreach (var log in _queue.GetConsumingEnumerable(_cts.Token))
{
try
{
File.AppendAllText(_filePath, log + Environment.NewLine, Encoding.UTF8);
}
catch (Exception ex)
{
OnException?.Invoke(this, ex);
}
}
}
catch (OperationCanceledException) { }
}
public void Dispose()
{
_cts.Cancel();
_queue.CompleteAdding();
try
{
_workerThread.Join();
}
catch { /* ignore */ }
_queue.Dispose();
_cts.Dispose();
}
} }
} }

View File

@@ -1,47 +1,89 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
namespace EonaCat.Logger.EonaCatCoreLogger namespace EonaCat.Logger.EonaCatCoreLogger
{ {
public class LoggerScopedContext : IDisposable internal sealed class LoggerScopedContext
{ {
private readonly ConcurrentDictionary<string, string> _context = new(); private static readonly AsyncLocal<Stack<Dictionary<string, string>>> _scopes
private readonly string _correlationId; = new();
public LoggerScopedContext(string correlationId = null) public IDisposable BeginScope<TState>(TState state)
{ {
_correlationId = correlationId; _scopes.Value ??= new Stack<Dictionary<string, string>>();
_scopes.Value.Push(new Dictionary<string, string>());
return new ScopePopper();
} }
public void Set(string key, string value) public void Set(string key, string value)
{ {
_context[key] = value; if (_scopes.Value == null || _scopes.Value.Count == 0)
{
return;
}
_scopes.Value.Peek()[key] = value;
} }
public string Get(string key) public string Get(string key)
{ {
return _context.TryGetValue(key, out var value) ? value : null; if (_scopes.Value == null)
{
return null;
}
foreach (var scope in _scopes.Value)
{
if (scope.TryGetValue(key, out var value))
{
return value;
}
}
return null;
} }
public IReadOnlyDictionary<string, string> GetAll() public IReadOnlyDictionary<string, string> GetAll()
{ {
return new Dictionary<string, string>(_context); var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (_scopes.Value == null)
{
return result;
} }
// Iterate from top of stack to bottom
foreach (var scope in _scopes.Value)
{
foreach (var kv in scope)
{
if (!result.ContainsKey(kv.Key))
{
result[kv.Key] = kv.Value;
}
}
}
return result;
}
public void Clear() public void Clear()
{ {
_context.Clear(); _scopes.Value?.Clear();
} }
private sealed class ScopePopper : IDisposable
{
public void Dispose() public void Dispose()
{ {
Clear(); if (_scopes.Value?.Count > 0)
}
public IDisposable CreateScope()
{ {
return this; _scopes.Value.Pop();
}
}
} }
} }
} }

View File

@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Channels; using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -11,13 +12,16 @@ namespace EonaCat.Logger.EonaCatCoreLogger
{ {
// This file is part of the EonaCat project(s) which is released under the Apache License. // 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. // See the LICENSE file or go to https://EonaCat.com/License for full license details.
public class SlackLogger : ILogger
public sealed class SlackLogger : ILogger, IDisposable
{ {
private readonly string _categoryName; private readonly string _categoryName;
private readonly SlackLoggerOptions _options; private readonly SlackLoggerOptions _options;
private readonly LoggerScopedContext _context = new(); private readonly LoggerScopedContext _context = new();
private readonly Channel<string> _logChannel = Channel.CreateUnbounded<string>(); private readonly Channel<string> _logChannel;
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly CancellationTokenSource _cts = new();
private readonly Task _processingTask;
public bool IncludeCorrelationId { get; set; } public bool IncludeCorrelationId { get; set; }
public event EventHandler<Exception> OnException; public event EventHandler<Exception> OnException;
@@ -29,7 +33,15 @@ namespace EonaCat.Logger.EonaCatCoreLogger
IncludeCorrelationId = options.IncludeCorrelationId; IncludeCorrelationId = options.IncludeCorrelationId;
_httpClient = new HttpClient(); _httpClient = new HttpClient();
Task.Run(ProcessLogQueueAsync); // Bounded channel to prevent unbounded memory growth
_logChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(1000)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = false
});
_processingTask = Task.Run(() => ProcessLogQueueAsync(_cts.Token));
} }
public IDisposable BeginScope<TState>(TState state) => null; public IDisposable BeginScope<TState>(TState state) => null;
@@ -62,7 +74,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
$"*[{DateTime.UtcNow:u}]*", $"*[{DateTime.UtcNow:u}]*",
$"*[{logLevel}]*", $"*[{logLevel}]*",
$"*[{_categoryName}]*", $"*[{_categoryName}]*",
$"Message: {message}", $"Message: {message}"
}; };
foreach (var kvp in _context.GetAll()) foreach (var kvp in _context.GetAll())
@@ -76,7 +88,9 @@ namespace EonaCat.Logger.EonaCatCoreLogger
} }
string fullMessage = string.Join("\n", logParts); string fullMessage = string.Join("\n", logParts);
_logChannel.Writer.TryWrite(fullMessage); // non-blocking
// non-blocking, drops oldest if full
_logChannel.Writer.TryWrite(fullMessage);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -84,15 +98,17 @@ namespace EonaCat.Logger.EonaCatCoreLogger
} }
} }
private async Task ProcessLogQueueAsync() private async Task ProcessLogQueueAsync(CancellationToken token)
{ {
await foreach (var message in _logChannel.Reader.ReadAllAsync()) try
{
await foreach (var message in _logChannel.Reader.ReadAllAsync(token))
{ {
try try
{ {
var payload = new { text = message }; var payload = new { text = message };
var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json"); var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json");
await _httpClient.PostAsync(_options.WebhookUrl, content); await _httpClient.PostAsync(_options.WebhookUrl, content, token);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -100,5 +116,22 @@ namespace EonaCat.Logger.EonaCatCoreLogger
} }
} }
} }
catch (OperationCanceledException) { }
}
public void Dispose()
{
_cts.Cancel();
_logChannel.Writer.Complete();
try
{
_processingTask.Wait();
}
catch { /* ignore */ }
_cts.Dispose();
_httpClient.Dispose();
}
} }
} }

View File

@@ -4,19 +4,20 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Channels; using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace EonaCat.Logger.EonaCatCoreLogger namespace EonaCat.Logger.EonaCatCoreLogger
{ {
// This file is part of the EonaCat project(s) which is released under the Apache License. public sealed class TcpLogger : ILogger, IDisposable
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
public class TcpLogger : ILogger
{ {
private readonly string _categoryName; private readonly string _categoryName;
private readonly TcpLoggerOptions _options; private readonly TcpLoggerOptions _options;
private readonly LoggerScopedContext _context = new(); private readonly LoggerScopedContext _context = new();
private readonly Channel<string> _logChannel = Channel.CreateUnbounded<string>(); private readonly Channel<string> _logChannel;
private readonly CancellationTokenSource _cts = new();
private readonly Task _processingTask;
public bool IncludeCorrelationId { get; set; } public bool IncludeCorrelationId { get; set; }
public event EventHandler<Exception> OnException; public event EventHandler<Exception> OnException;
@@ -27,21 +28,27 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_options = options; _options = options;
IncludeCorrelationId = options.IncludeCorrelationId; IncludeCorrelationId = options.IncludeCorrelationId;
// Start background log writer // Bounded channel to prevent unbounded memory growth
Task.Run(ProcessLogQueueAsync); _logChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(1000)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = false
});
_processingTask = Task.Run(() => ProcessLogQueueAsync(_cts.Token));
} }
public void SetContext(string key, string value) => _context.Set(key, value); public void SetContext(string key, string value) => _context.Set(key, value);
public void ClearContext() => _context.Clear(); public void ClearContext() => _context.Clear();
public string GetContext(string key) => _context.Get(key); public string GetContext(string key) => _context.Get(key);
public IDisposable BeginScope<TState>(TState state) => _context.BeginScope(state);
public IDisposable BeginScope<TState>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled; public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
Exception exception, Func<TState, Exception, string> formatter) Exception exception, Func<TState, Exception, string> formatter)
{ {
if (!IsEnabled(logLevel)) if (!IsEnabled(logLevel) || formatter == null)
{ {
return; return;
} }
@@ -61,7 +68,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
$"[{DateTime.UtcNow:u}]", $"[{DateTime.UtcNow:u}]",
$"[{logLevel}]", $"[{logLevel}]",
$"[{_categoryName}]", $"[{_categoryName}]",
$"Message: {message}", $"Message: {message}"
}; };
var contextData = _context.GetAll(); var contextData = _context.GetAll();
@@ -81,6 +88,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
string fullLog = string.Join(" | ", logParts); string fullLog = string.Join(" | ", logParts);
// Non-blocking, drop oldest if full
_logChannel.Writer.TryWrite(fullLog); _logChannel.Writer.TryWrite(fullLog);
} }
catch (Exception e) catch (Exception e)
@@ -89,21 +97,21 @@ namespace EonaCat.Logger.EonaCatCoreLogger
} }
} }
private async Task ProcessLogQueueAsync() private async Task ProcessLogQueueAsync(CancellationToken token)
{
await foreach (var log in _logChannel.Reader.ReadAllAsync())
{ {
try try
{ {
// Keep one TCP connection alive if possible
using var client = new TcpClient(); using var client = new TcpClient();
await client.ConnectAsync(_options.Host, _options.Port); await client.ConnectAsync(_options.Host, _options.Port);
using var stream = client.GetStream(); using var stream = client.GetStream();
using var writer = new StreamWriter(stream, Encoding.UTF8) using var writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true };
{
AutoFlush = true
};
await foreach (var log in _logChannel.Reader.ReadAllAsync(token))
{
try
{
await writer.WriteLineAsync(log); await writer.WriteLineAsync(log);
} }
catch (Exception e) catch (Exception e)
@@ -112,5 +120,25 @@ namespace EonaCat.Logger.EonaCatCoreLogger
} }
} }
} }
catch (OperationCanceledException) { }
catch (Exception ex)
{
OnException?.Invoke(this, ex);
}
}
public void Dispose()
{
_cts.Cancel();
_logChannel.Writer.Complete();
try
{
_processingTask.Wait();
}
catch { /* ignore */ }
_cts.Dispose();
}
} }
} }

View File

@@ -1,21 +1,22 @@
using EonaCat.Json; using EonaCat.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Net.Http;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Channels; using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Net.Http;
namespace EonaCat.Logger.EonaCatCoreLogger namespace EonaCat.Logger.EonaCatCoreLogger
{ {
// This file is part of the EonaCat project(s) which is released under the Apache License. public sealed class TelegramLogger : ILogger, IDisposable
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
public class TelegramLogger : ILogger
{ {
private readonly string _categoryName; private readonly string _categoryName;
private readonly TelegramLoggerOptions _options; private readonly TelegramLoggerOptions _options;
private readonly HttpClient _httpClient = new(); private readonly HttpClient _httpClient;
private readonly Channel<string> _logChannel = Channel.CreateUnbounded<string>(); private readonly Channel<string> _logChannel;
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
private readonly Task _processingTask;
public event EventHandler<Exception> OnException; public event EventHandler<Exception> OnException;
@@ -24,14 +25,24 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_categoryName = categoryName; _categoryName = categoryName;
_options = options; _options = options;
// Start background task _httpClient = new HttpClient();
Task.Run(ProcessLogQueueAsync);
// Bounded channel to prevent memory leaks
_logChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(1000)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = false
});
_processingTask = Task.Run(() => ProcessLogQueueAsync(_cts.Token));
} }
public IDisposable BeginScope<TState>(TState state) => null; public IDisposable BeginScope<TState>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled; public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
Exception exception, Func<TState, Exception, string> formatter)
{ {
if (!IsEnabled(logLevel) || formatter == null) if (!IsEnabled(logLevel) || formatter == null)
{ {
@@ -46,6 +57,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
message += $"\nException: {exception}"; message += $"\nException: {exception}";
} }
// Non-blocking, drop oldest if full
_logChannel.Writer.TryWrite(message); _logChannel.Writer.TryWrite(message);
} }
catch (Exception ex) catch (Exception ex)
@@ -54,21 +66,19 @@ namespace EonaCat.Logger.EonaCatCoreLogger
} }
} }
private async Task ProcessLogQueueAsync() private async Task ProcessLogQueueAsync(CancellationToken token)
{ {
await foreach (var message in _logChannel.Reader.ReadAllAsync()) try
{
await foreach (var message in _logChannel.Reader.ReadAllAsync(token))
{ {
try try
{ {
var url = $"https://api.telegram.org/bot{_options.BotToken}/sendMessage"; var url = $"https://api.telegram.org/bot{_options.BotToken}/sendMessage";
var payload = new var payload = new { chat_id = _options.ChatId, text = message };
{
chat_id = _options.ChatId,
text = message
};
var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json"); var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json");
await _httpClient.PostAsync(url, content);
await _httpClient.PostAsync(url, content, token);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -76,5 +86,22 @@ namespace EonaCat.Logger.EonaCatCoreLogger
} }
} }
} }
catch (OperationCanceledException) { }
}
public void Dispose()
{
_cts.Cancel();
_logChannel.Writer.Complete();
try
{
_processingTask.Wait();
}
catch { /* ignore */ }
_cts.Dispose();
_httpClient.Dispose();
}
} }
} }

View File

@@ -3,20 +3,24 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Channels; using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace EonaCat.Logger.EonaCatCoreLogger namespace EonaCat.Logger.EonaCatCoreLogger
{
public sealed class UdpLogger : ILogger, IDisposable
{ {
// This file is part of the EonaCat project(s) which is released under the Apache License. // 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. // See the LICENSE file or go to https://EonaCat.com/License for full license details.
public class UdpLogger : ILogger
{
private readonly string _categoryName; private readonly string _categoryName;
private readonly UdpLoggerOptions _options; private readonly UdpLoggerOptions _options;
private readonly LoggerScopedContext _context = new(); private readonly LoggerScopedContext _context = new();
private readonly Channel<string> _logChannel = Channel.CreateUnbounded<string>(); private readonly Channel<string> _logChannel;
private readonly UdpClient _udpClient = new(); private readonly UdpClient _udpClient;
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
private readonly Task _processingTask;
public bool IncludeCorrelationId { get; set; } public bool IncludeCorrelationId { get; set; }
public event EventHandler<Exception> OnException; public event EventHandler<Exception> OnException;
@@ -27,15 +31,23 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_options = options; _options = options;
IncludeCorrelationId = options.IncludeCorrelationId; IncludeCorrelationId = options.IncludeCorrelationId;
Task.Run(ProcessLogQueueAsync); _udpClient = new UdpClient();
// Bounded channel to avoid unbounded memory growth
_logChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(1000)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = false
});
_processingTask = Task.Run(() => ProcessLogQueueAsync(_cts.Token));
} }
public void SetContext(string key, string value) => _context.Set(key, value); public void SetContext(string key, string value) => _context.Set(key, value);
public void ClearContext() => _context.Clear(); public void ClearContext() => _context.Clear();
public string GetContext(string key) => _context.Get(key); public string GetContext(string key) => _context.Get(key);
public IDisposable BeginScope<TState>(TState state) => _context.BeginScope(state);
public IDisposable BeginScope<TState>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled; public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
@@ -61,7 +73,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
$"[{DateTime.UtcNow:u}]", $"[{DateTime.UtcNow:u}]",
$"[{logLevel}]", $"[{logLevel}]",
$"[{_categoryName}]", $"[{_categoryName}]",
$"Message: {message}", $"Message: {message}"
}; };
var contextData = _context.GetAll(); var contextData = _context.GetAll();
@@ -80,6 +92,8 @@ namespace EonaCat.Logger.EonaCatCoreLogger
} }
string fullMessage = string.Join(" | ", logParts); string fullMessage = string.Join(" | ", logParts);
// Non-blocking, drop oldest if full
_logChannel.Writer.TryWrite(fullMessage); _logChannel.Writer.TryWrite(fullMessage);
} }
catch (Exception ex) catch (Exception ex)
@@ -88,9 +102,11 @@ namespace EonaCat.Logger.EonaCatCoreLogger
} }
} }
private async Task ProcessLogQueueAsync() private async Task ProcessLogQueueAsync(CancellationToken token)
{ {
await foreach (var message in _logChannel.Reader.ReadAllAsync()) try
{
await foreach (var message in _logChannel.Reader.ReadAllAsync(token))
{ {
try try
{ {
@@ -103,5 +119,26 @@ namespace EonaCat.Logger.EonaCatCoreLogger
} }
} }
} }
catch (OperationCanceledException) { }
catch (Exception ex)
{
OnException?.Invoke(this, ex);
}
}
public void Dispose()
{
_cts.Cancel();
_logChannel.Writer.Complete();
try
{
_processingTask.Wait();
}
catch { /* ignore */ }
_udpClient.Dispose();
_cts.Dispose();
}
} }
} }

View File

@@ -5,24 +5,25 @@ using System.Threading;
using System.Xml.Linq; using System.Xml.Linq;
namespace EonaCat.Logger.EonaCatCoreLogger namespace EonaCat.Logger.EonaCatCoreLogger
{
public sealed class XmlFileLogger : ILogger, IDisposable
{ {
// This file is part of the EonaCat project(s) which is released under the Apache License. // 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. // See the LICENSE file or go to https://EonaCat.com/License for full license details.
public class XmlFileLogger : ILogger
{
private readonly string _categoryName; private readonly string _categoryName;
private readonly XmlFileLoggerOptions _options; private readonly XmlFileLoggerOptions _options;
private readonly string _filePath; private readonly string _filePath;
private readonly LoggerScopedContext _context = new(); private readonly LoggerScopedContext _context = new();
private static readonly SemaphoreSlim _fileLock = new(1, 1); private readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1);
public bool IncludeCorrelationId { get; set; } public bool IncludeCorrelationId { get; set; }
public event EventHandler<Exception> OnException; public event EventHandler<Exception> OnException;
public XmlFileLogger(string categoryName, XmlFileLoggerOptions options) public XmlFileLogger(string categoryName, XmlFileLoggerOptions options)
{ {
_categoryName = categoryName; _categoryName = categoryName ?? throw new ArgumentNullException(nameof(categoryName));
_options = options; _options = options ?? throw new ArgumentNullException(nameof(options));
_filePath = Path.Combine(_options.LogDirectory, _options.FileName); _filePath = Path.Combine(_options.LogDirectory, _options.FileName);
IncludeCorrelationId = options.IncludeCorrelationId; IncludeCorrelationId = options.IncludeCorrelationId;
@@ -30,18 +31,16 @@ namespace EonaCat.Logger.EonaCatCoreLogger
{ {
Directory.CreateDirectory(_options.LogDirectory); Directory.CreateDirectory(_options.LogDirectory);
} }
catch (Exception e) catch (Exception ex)
{ {
OnException?.Invoke(this, e); OnException?.Invoke(this, ex);
} }
} }
public void SetContext(string key, string value) => _context.Set(key, value); public void SetContext(string key, string value) => _context.Set(key, value);
public void ClearContext() => _context.Clear(); public void ClearContext() => _context.Clear();
public string GetContext(string key) => _context.Get(key); public string GetContext(string key) => _context.Get(key);
public IDisposable BeginScope<TState>(TState state) => _context.BeginScope(state);
public IDisposable BeginScope<TState>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled; public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
@@ -93,10 +92,15 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_fileLock.Release(); _fileLock.Release();
} }
} }
catch (Exception e) catch (Exception ex)
{ {
OnException?.Invoke(this, e); OnException?.Invoke(this, ex);
} }
}
public void Dispose()
{
_fileLock.Dispose();
} }
} }
} }

View File

@@ -17,6 +17,9 @@ using EonaCat.Logger.Servers.Udp;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
// 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.Logger.Managers namespace EonaCat.Logger.Managers
{ {
public class LogManager : ILogManager, IDisposable public class LogManager : ILogManager, IDisposable
@@ -59,18 +62,20 @@ namespace EonaCat.Logger.Managers
public event EventHandler<ErrorMessage> OnException; public event EventHandler<ErrorMessage> OnException;
public event EventHandler<ErrorMessage> OnLogLevelDisabled; public event EventHandler<ErrorMessage> OnLogLevelDisabled;
public async Task WriteAsync(object currentObject, ELogType logType = ELogType.INFO, bool? writeToConsole = null, public async Task WriteAsync(object currentObject, ELogType logType = ELogType.INFO, bool? writeToConsole = null,
string customSplunkSourceType = null, string grayLogFacility = null, string customSplunkSourceType = null, string grayLogFacility = null,
string grayLogSource = null, string grayLogVersion = "1.1", bool disableSplunkSSL = false, string grayLogSource = null, string grayLogVersion = "1.1", bool disableSplunkSSL = false,
DumpFormat dumpFormat = DumpFormat.Json, bool isDetailedDump = false, int? dumpDepth = null, int? maxCollectionItems = null) DumpFormat dumpFormat = DumpFormat.Json, bool isDetailedDump = false, int? dumpDepth = null, int? maxCollectionItems = null)
{ {
if (currentObject == null) if (_isDisposing || currentObject == null)
{ {
return; return;
} }
await WriteAsync(currentObject.Dump(dumpFormat, isDetailedDump, dumpDepth, maxCollectionItems), await WriteAsync(currentObject.Dump(dumpFormat, isDetailedDump, dumpDepth, maxCollectionItems),
logType, writeToConsole, customSplunkSourceType, grayLogFacility, grayLogSource, grayLogVersion, disableSplunkSSL); logType, writeToConsole, customSplunkSourceType, grayLogFacility, grayLogSource, grayLogVersion, disableSplunkSSL)
.ConfigureAwait(false);
} }
public async Task WriteAsync(Exception exception, string module = null, string method = null, public async Task WriteAsync(Exception exception, string module = null, string method = null,
@@ -78,39 +83,41 @@ namespace EonaCat.Logger.Managers
string customSplunkSourceType = null, string grayLogFacility = null, string customSplunkSourceType = null, string grayLogFacility = null,
string grayLogSource = null, string grayLogVersion = "1.1", bool disableSplunkSSL = false) string grayLogSource = null, string grayLogVersion = "1.1", bool disableSplunkSSL = false)
{ {
if (exception == null) if (_isDisposing || exception == null)
{ {
return; return;
} }
await WriteAsync(exception.FormatExceptionToMessage(module, method), await WriteAsync(exception.FormatExceptionToMessage(module, method),
criticalException ? ELogType.CRITICAL : ELogType.ERROR, criticalException ? ELogType.CRITICAL : ELogType.ERROR,
writeToConsole, customSplunkSourceType, grayLogFacility, grayLogSource, grayLogVersion, disableSplunkSSL); writeToConsole, customSplunkSourceType, grayLogFacility, grayLogSource, grayLogVersion, disableSplunkSSL)
.ConfigureAwait(false);
} }
public async Task WriteAsync(string message, ELogType logType = ELogType.INFO, bool? writeToConsole = null, public async Task WriteAsync(string message, ELogType logType = ELogType.INFO, bool? writeToConsole = null,
string customSplunkSourceType = null, string grayLogFacility = null, string grayLogSource = null, string customSplunkSourceType = null, string grayLogFacility = null, string grayLogSource = null,
string grayLogVersion = "1.1", bool disableSplunkSSL = false) string grayLogVersion = "1.1", bool disableSplunkSSL = false)
{ {
if (logType == ELogType.NONE) if (_isDisposing || string.IsNullOrWhiteSpace(message) || logType == ELogType.NONE)
{ {
return; return;
} }
await InternalWriteAsync(CurrentDateTime, message, logType, writeToConsole, await InternalWriteAsync(CurrentDateTime, message, logType, writeToConsole,
customSplunkSourceType, grayLogFacility, grayLogSource, grayLogVersion, disableSplunkSSL); customSplunkSourceType, grayLogFacility, grayLogSource, grayLogVersion, disableSplunkSSL)
.ConfigureAwait(false);
} }
public async Task StartNewLogAsync() public async Task StartNewLogAsync()
{ {
if (_tokenSource.IsCancellationRequested) if (_isDisposing || _tokenSource.IsCancellationRequested)
{ {
return; return;
} }
if (IsRunning && CurrentDateTime.Date > _logDate.Date) if (IsRunning && CurrentDateTime.Date > _logDate.Date)
{ {
await StopLoggingAsync(); await StopLoggingAsync().ConfigureAwait(false);
} }
IsRunning = true; IsRunning = true;
@@ -119,11 +126,17 @@ namespace EonaCat.Logger.Managers
_logDate = CurrentDateTime; _logDate = CurrentDateTime;
} }
private async Task StopLoggingAsync() private async Task StopLoggingAsync()
{ {
if (_isDisposing)
{
return;
}
WriteStopMessage(); WriteStopMessage();
IsRunning = false; IsRunning = false;
await Task.CompletedTask; await Task.CompletedTask.ConfigureAwait(false);
} }
private void WriteStopMessage() private void WriteStopMessage()
@@ -134,7 +147,11 @@ namespace EonaCat.Logger.Managers
private void CreateLogger() private void CreateLogger()
{ {
// Dispose previous providers safely if (_isDisposing)
{
return;
}
LoggerProvider?.Dispose(); LoggerProvider?.Dispose();
LoggerFactory?.Dispose(); LoggerFactory?.Dispose();
@@ -182,7 +199,7 @@ namespace EonaCat.Logger.Managers
bool? writeToConsole = null, string customSplunkSourceType = null, string grayLogFacility = null, bool? writeToConsole = null, string customSplunkSourceType = null, string grayLogFacility = null,
string grayLogSource = null, string grayLogVersion = "1.1", bool disableSplunkSSL = false) string grayLogSource = null, string grayLogVersion = "1.1", bool disableSplunkSSL = false)
{ {
if (string.IsNullOrWhiteSpace(message) || logType == ELogType.NONE || !IsLogLevelEnabled(logType) || _isDisposing) if (_isDisposing || string.IsNullOrWhiteSpace(message) || logType == ELogType.NONE || !IsLogLevelEnabled(logType))
{ {
return; return;
} }
@@ -244,6 +261,7 @@ namespace EonaCat.Logger.Managers
private void LogHelper_OnException(object sender, ErrorMessage e) => OnException?.Invoke(sender, e); private void LogHelper_OnException(object sender, ErrorMessage e) => OnException?.Invoke(sender, e);
private void LogHelper_OnLogLevelDisabled(object sender, ErrorMessage e) => OnLogLevelDisabled?.Invoke(sender, e); private void LogHelper_OnLogLevelDisabled(object sender, ErrorMessage e) => OnLogLevelDisabled?.Invoke(sender, e);
public void Dispose() public void Dispose()
{ {
DisposeAsync(true).GetAwaiter().GetResult(); DisposeAsync(true).GetAwaiter().GetResult();
@@ -261,21 +279,17 @@ namespace EonaCat.Logger.Managers
{ {
_isDisposing = true; _isDisposing = true;
await StopLoggingAsync(); await StopLoggingAsync().ConfigureAwait(false);
// Unsubscribe events
LogHelper.OnException -= LogHelper_OnException; LogHelper.OnException -= LogHelper_OnException;
LogHelper.OnLogLevelDisabled -= LogHelper_OnLogLevelDisabled; LogHelper.OnLogLevelDisabled -= LogHelper_OnLogLevelDisabled;
// Cancel token _tokenSource?.Cancel();
_tokenSource.Cancel(); _tokenSource?.Dispose();
_tokenSource.Dispose();
_tokenSource = null; _tokenSource = null;
LoggerProvider?.Dispose(); LoggerProvider?.Dispose();
LoggerFactory?.Dispose(); LoggerFactory?.Dispose();
await Task.Delay(50);
} }
} }
@@ -286,6 +300,7 @@ namespace EonaCat.Logger.Managers
_instance.Value.Dispose(); _instance.Value.Dispose();
} }
} }
public void DeleteCurrentLogFile() public void DeleteCurrentLogFile()
{ {
if (!string.IsNullOrEmpty(CurrentLogFile) && File.Exists(CurrentLogFile)) if (!string.IsNullOrEmpty(CurrentLogFile) && File.Exists(CurrentLogFile))
@@ -294,12 +309,9 @@ namespace EonaCat.Logger.Managers
} }
} }
private static LoggerSettings CreateDefaultSettings() => new() { Id = DllInfo.ApplicationName };
private void SetupFileLogger(LoggerSettings settings = null, string logFolder = null, bool defaultPrefix = true) private void SetupFileLogger(LoggerSettings settings = null, string logFolder = null, bool defaultPrefix = true)
{ {
settings ??= Settings; settings ??= Settings;
if (!settings.EnableFileLogging) if (!settings.EnableFileLogging)
{ {
return; return;
@@ -318,7 +330,8 @@ namespace EonaCat.Logger.Managers
private void SetupLogManager() => _logDate = CurrentDateTime; private void SetupLogManager() => _logDate = CurrentDateTime;
// Syslog private static LoggerSettings CreateDefaultSettings() => new() { Id = DllInfo.ApplicationName };
public bool AddSyslogServer(string address, int port, string nickName = null, List<ELogType> typesToLog = null, bool convertToRfc5424 = false, bool convertToRfc3164 = false) public bool AddSyslogServer(string address, int port, string nickName = null, List<ELogType> typesToLog = null, bool convertToRfc5424 = false, bool convertToRfc3164 = false)
{ {
Settings.SysLogServers ??= new List<Syslog>(); Settings.SysLogServers ??= new List<Syslog>();

View File

@@ -16,6 +16,7 @@ public class Graylog : IDisposable
private string _Hostname = "127.0.0.1"; private string _Hostname = "127.0.0.1";
private int _Port = 12201; private int _Port = 12201;
internal UdpClient Udp; internal UdpClient Udp;
private bool _disposed;
public bool SupportsTcp { get; set; } public bool SupportsTcp { get; set; }
public string Nickname { get; set; } public string Nickname { get; set; }
@@ -23,22 +24,13 @@ public class Graylog : IDisposable
public Graylog() { } public Graylog() { }
/// <summary>
/// Graylog server
/// </summary>
/// <param name="hostname"></param>
/// <param name="port"></param>
/// <param name="nickName"></param>
/// <param name="logType"></param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentException"></exception>
public Graylog(string hostname = "127.0.0.1", int port = 12201, string nickName = null, List<ELogType> typesToLog = null) public Graylog(string hostname = "127.0.0.1", int port = 12201, string nickName = null, List<ELogType> typesToLog = null)
{ {
_Hostname = hostname ?? throw new ArgumentNullException(nameof(hostname)); _Hostname = hostname ?? throw new ArgumentNullException(nameof(hostname));
_Port = port >= 0 ? port : throw new ArgumentException("Port must be zero or greater."); _Port = port >= 0 ? port : throw new ArgumentException("Port must be zero or greater.");
TypesToLog = typesToLog; TypesToLog = typesToLog;
Nickname = nickName ?? IpPort; Nickname = nickName ?? IpPort;
SetUdp(); SetUdp();
} }
@@ -73,10 +65,15 @@ public class Graylog : IDisposable
} }
public List<ELogType> TypesToLog { get; set; } public List<ELogType> TypesToLog { get; set; }
public bool IsConnected { get; set; } public bool IsConnected { get; private set; }
internal void SetUdp() internal void SetUdp()
{ {
if (_disposed)
{
return;
}
try try
{ {
DisposeUdp(); DisposeUdp();
@@ -91,31 +88,51 @@ public class Graylog : IDisposable
internal void DisposeUdp() internal void DisposeUdp()
{ {
if (Udp != null) if (Udp == null)
{ {
return;
}
IsConnected = false; IsConnected = false;
try try
{ {
Udp?.Close(); Udp.Close();
Udp.Dispose(); Udp.Dispose();
} }
catch catch
{ {
// Do nothing // Do nothing
} }
finally
{
Udp = null; Udp = null;
} }
} }
public void Dispose() public void Dispose()
{ {
DisposeUdp(); Dispose(true);
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
DisposeUdp();
}
}
~Graylog() ~Graylog()
{ {
Dispose(); Dispose(false);
} }
} }

View File

@@ -1,5 +1,8 @@
namespace EonaCat.Logger.Servers.Splunk.Models; namespace EonaCat.Logger.Servers.Splunk.Models;
// 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.
public class SplunkPayload public class SplunkPayload
{ {
/// <summary> /// <summary>

View File

@@ -6,6 +6,9 @@ using System.Threading.Tasks;
using EonaCat.Json; using EonaCat.Json;
using EonaCat.Logger.Servers.Splunk.Models; using EonaCat.Logger.Servers.Splunk.Models;
// 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.Logger.Servers.Splunk namespace EonaCat.Logger.Servers.Splunk
{ {
/// <summary> /// <summary>
@@ -16,6 +19,8 @@ namespace EonaCat.Logger.Servers.Splunk
private string _splunkHecUrl = "https://127.0.0.1:8088/services/collector/event"; private string _splunkHecUrl = "https://127.0.0.1:8088/services/collector/event";
private HttpClient _httpClient; private HttpClient _httpClient;
private HttpClientHandler _httpClientHandler; private HttpClientHandler _httpClientHandler;
private bool _disposed;
public event EventHandler<Exception> OnException; public event EventHandler<Exception> OnException;
public string SplunkHecToken { get; set; } = "splunk-hec-token"; public string SplunkHecToken { get; set; } = "splunk-hec-token";
@@ -43,7 +48,7 @@ namespace EonaCat.Logger.Servers.Splunk
public bool HasHecToken => !string.IsNullOrWhiteSpace(SplunkHecToken) && SplunkHecToken != "splunk-hec-token"; public bool HasHecToken => !string.IsNullOrWhiteSpace(SplunkHecToken) && SplunkHecToken != "splunk-hec-token";
public bool HasHecUrl => !string.IsNullOrWhiteSpace(_splunkHecUrl); public bool HasHecUrl => !string.IsNullOrWhiteSpace(_splunkHecUrl);
public bool IsHttpsHecUrl => _splunkHecUrl.StartsWith("https", StringComparison.OrdinalIgnoreCase); public bool IsHttpsHecUrl => _splunkHecUrl.StartsWith("https", StringComparison.OrdinalIgnoreCase);
public bool IsLocalHost => _splunkHecUrl.ToLower().Contains("127.0.0.1") || _splunkHecUrl.ToLower().Contains("localhost"); public bool IsLocalHost => _splunkHecUrl.Contains("127.0.0.1") || _splunkHecUrl.Contains("localhost");
public Splunk(string splunkHecUrl, string splunkHecToken, HttpClientHandler handler = null, string nickName = null, List<ELogType> typesToLog = null) public Splunk(string splunkHecUrl, string splunkHecToken, HttpClientHandler handler = null, string nickName = null, List<ELogType> typesToLog = null)
{ {
@@ -51,6 +56,7 @@ namespace EonaCat.Logger.Servers.Splunk
SplunkHecUrl = splunkHecUrl ?? throw new ArgumentNullException(nameof(splunkHecUrl)); SplunkHecUrl = splunkHecUrl ?? throw new ArgumentNullException(nameof(splunkHecUrl));
Nickname = nickName ?? $"{splunkHecUrl}_{splunkHecToken}"; Nickname = nickName ?? $"{splunkHecUrl}_{splunkHecToken}";
TypesToLog = typesToLog; TypesToLog = typesToLog;
_httpClientHandler = handler ?? new HttpClientHandler(); _httpClientHandler = handler ?? new HttpClientHandler();
CreateHttpClient(); CreateHttpClient();
} }
@@ -90,14 +96,14 @@ namespace EonaCat.Logger.Servers.Splunk
public async Task<HttpResponseMessage> SendAsync(SplunkPayload payload, bool disableSplunkSSL = false) public async Task<HttpResponseMessage> SendAsync(SplunkPayload payload, bool disableSplunkSSL = false)
{ {
try if (_disposed)
{ {
if (payload == null) throw new ObjectDisposedException(nameof(Splunk));
{
return null;
} }
if (!HasHecToken || !HasHecUrl) try
{
if (payload == null || !HasHecToken || !HasHecUrl)
{ {
return null; return null;
} }
@@ -121,12 +127,12 @@ namespace EonaCat.Logger.Servers.Splunk
var json = JsonHelper.ToJson(eventObject); var json = JsonHelper.ToJson(eventObject);
var request = new HttpRequestMessage(HttpMethod.Post, "services/collector/event") using var request = new HttpRequestMessage(HttpMethod.Post, "services/collector/event")
{ {
Content = new StringContent(json, Encoding.UTF8, "application/json") Content = new StringContent(json, Encoding.UTF8, "application/json")
}; };
return await _httpClient.SendAsync(request); return await _httpClient.SendAsync(request).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -155,13 +161,28 @@ namespace EonaCat.Logger.Servers.Splunk
public void Dispose() public void Dispose()
{ {
DisposeHttpClient(); Dispose(true);
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
DisposeHttpClient();
}
}
~Splunk() ~Splunk()
{ {
Dispose(); Dispose(false);
} }
} }
} }

View File

@@ -7,13 +7,17 @@ using System.Threading.Tasks;
namespace EonaCat.Logger.Servers.Syslog namespace EonaCat.Logger.Servers.Syslog
{ {
// This file is part of the EonaCat project(s) released under the Apache License. // 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.
public class Syslog : IDisposable public class Syslog : IDisposable
{ {
private const int MaxUdpPacketSize = 4096; private const int MaxUdpPacketSize = 4096;
private string _hostname = "127.0.0.1"; private string _hostname = "127.0.0.1";
private int _port = 514; private int _port = 514;
private UdpClient _udp; private UdpClient _udp;
private bool _disposed;
public event EventHandler<Exception> OnException; public event EventHandler<Exception> OnException;
public bool IsConnected { get; private set; } public bool IsConnected { get; private set; }
@@ -26,7 +30,8 @@ namespace EonaCat.Logger.Servers.Syslog
public Syslog() { } public Syslog() { }
public Syslog(string hostname = "127.0.0.1", int port = 514, string nickName = null, List<ELogType> typesToLog = null, bool convertToRfc5424 = false, bool convertToRfc3164 = false) public Syslog(string hostname = "127.0.0.1", int port = 514, string nickName = null,
List<ELogType> typesToLog = null, bool convertToRfc5424 = false, bool convertToRfc3164 = false)
{ {
Hostname = hostname; Hostname = hostname;
Port = port; Port = port;
@@ -68,6 +73,11 @@ namespace EonaCat.Logger.Servers.Syslog
internal void SetUdp() internal void SetUdp()
{ {
if (_disposed)
{
return;
}
try try
{ {
DisposeUdp(); DisposeUdp();
@@ -82,24 +92,28 @@ namespace EonaCat.Logger.Servers.Syslog
internal void DisposeUdp() internal void DisposeUdp()
{ {
IsConnected = false; if (_udp != null)
{
try try
{ {
_udp?.Close(); IsConnected = false;
_udp?.Dispose(); _udp.Close();
_udp.Dispose();
} }
catch catch
{ {
// Swallow cleanup exceptions // Swallow cleanup exceptions
} }
finally
{
_udp = null; _udp = null;
} }
}
}
public async Task WriteAsync(string message) public async Task WriteAsync(string message)
{ {
if (string.IsNullOrWhiteSpace(message)) if (_disposed || string.IsNullOrWhiteSpace(message))
{ {
return; return;
} }
@@ -110,15 +124,17 @@ namespace EonaCat.Logger.Servers.Syslog
public async Task WriteAsync(byte[] data) public async Task WriteAsync(byte[] data)
{ {
if (data is { Length: > 0 }) if (_disposed || data == null || data.Length == 0)
{ {
await SendAsync(data); return;
} }
await SendAsync(data);
} }
private async Task SendAsync(byte[] data) private async Task SendAsync(byte[] data)
{ {
if (_udp == null) if (_disposed || _udp == null)
{ {
return; return;
} }
@@ -147,6 +163,11 @@ namespace EonaCat.Logger.Servers.Syslog
private async Task SendViaTcpAsync(byte[] data) private async Task SendViaTcpAsync(byte[] data)
{ {
if (_disposed)
{
return;
}
try try
{ {
using var tcpClient = new TcpClient(); using var tcpClient = new TcpClient();
@@ -158,12 +179,17 @@ namespace EonaCat.Logger.Servers.Syslog
} }
catch catch
{ {
// TCP failure fallback is silent here // TCP failure is silent
} }
} }
private async Task SendUdpInChunksAsync(byte[] data, int chunkSize) private async Task SendUdpInChunksAsync(byte[] data, int chunkSize)
{ {
if (_disposed)
{
return;
}
int offset = 0; int offset = 0;
byte[] buffer = ArrayPool<byte>.Shared.Rent(chunkSize); byte[] buffer = ArrayPool<byte>.Shared.Rent(chunkSize);
@@ -189,13 +215,28 @@ namespace EonaCat.Logger.Servers.Syslog
public void Dispose() public void Dispose()
{ {
DisposeUdp(); Dispose(true);
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
DisposeUdp();
}
}
~Syslog() ~Syslog()
{ {
Dispose(); Dispose(false);
} }
} }
} }

View File

@@ -9,12 +9,15 @@ namespace EonaCat.Logger.Servers.Tcp
{ {
// This file is part of the EonaCat project(s) which is released under the Apache License. // 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. // See the LICENSE file or go to https://EonaCat.com/License for full license details.
public class Tcp : IDisposable public class Tcp : IDisposable
{ {
private string _hostname = "127.0.0.1"; private string _hostname = "127.0.0.1";
private int _port = 514; private int _port = 514;
private TcpClient _tcp; private TcpClient _tcp;
private readonly SemaphoreSlim _sendLock = new(1, 1); private readonly SemaphoreSlim _sendLock = new(1, 1);
private bool _disposed;
public event EventHandler<Exception> OnException; public event EventHandler<Exception> OnException;
public bool IsConnected { get; private set; } public bool IsConnected { get; private set; }
@@ -64,6 +67,11 @@ namespace EonaCat.Logger.Servers.Tcp
internal void SetTcp() internal void SetTcp()
{ {
if (_disposed)
{
return;
}
try try
{ {
DisposeTcp(); DisposeTcp();
@@ -89,13 +97,15 @@ namespace EonaCat.Logger.Servers.Tcp
{ {
// Silent fail // Silent fail
} }
finally
{
_tcp = null; _tcp = null;
} }
}
public async Task WriteAsync(byte[] data) public async Task WriteAsync(byte[] data)
{ {
if (data == null || data.Length == 0) if (_disposed || data == null || data.Length == 0)
{ {
return; return;
} }
@@ -105,7 +115,7 @@ namespace EonaCat.Logger.Servers.Tcp
public async Task WriteAsync(string data) public async Task WriteAsync(string data)
{ {
if (string.IsNullOrWhiteSpace(data)) if (_disposed || string.IsNullOrWhiteSpace(data))
{ {
return; return;
} }
@@ -116,6 +126,11 @@ namespace EonaCat.Logger.Servers.Tcp
private async Task InternalWriteAsync(byte[] data) private async Task InternalWriteAsync(byte[] data)
{ {
if (_disposed)
{
return;
}
await _sendLock.WaitAsync(); await _sendLock.WaitAsync();
try try
{ {
@@ -129,7 +144,7 @@ namespace EonaCat.Logger.Servers.Tcp
return; return;
} }
var stream = _tcp.GetStream(); using var stream = _tcp.GetStream();
await stream.WriteAsync(data, 0, data.Length); await stream.WriteAsync(data, 0, data.Length);
await stream.FlushAsync(); await stream.FlushAsync();
} }
@@ -145,15 +160,30 @@ namespace EonaCat.Logger.Servers.Tcp
} }
public void Dispose() public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{ {
DisposeTcp(); DisposeTcp();
_sendLock.Dispose(); _sendLock.Dispose();
GC.SuppressFinalize(this); }
} }
~Tcp() ~Tcp()
{ {
Dispose(); Dispose(false);
} }
} }
} }

View File

@@ -10,6 +10,7 @@ namespace EonaCat.Logger.Servers.Udp
{ {
// This file is part of the EonaCat project(s) which is released under the Apache License. // 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. // See the LICENSE file or go to https://EonaCat.com/License for full license details.
public class Udp : IDisposable public class Udp : IDisposable
{ {
private const int MaxUdpPacketSize = 4096; private const int MaxUdpPacketSize = 4096;
@@ -17,6 +18,8 @@ namespace EonaCat.Logger.Servers.Udp
private int _port = 514; private int _port = 514;
private UdpClient _udp; private UdpClient _udp;
private readonly SemaphoreSlim _sendLock = new(1, 1); private readonly SemaphoreSlim _sendLock = new(1, 1);
private bool _disposed;
public event EventHandler<Exception> OnException; public event EventHandler<Exception> OnException;
public bool IsConnected { get; private set; } public bool IsConnected { get; private set; }
@@ -30,6 +33,7 @@ namespace EonaCat.Logger.Servers.Udp
_port = port >= 0 ? port : throw new ArgumentException("Port must be zero or greater."); _port = port >= 0 ? port : throw new ArgumentException("Port must be zero or greater.");
TypesToLog = typesToLog; TypesToLog = typesToLog;
Nickname = nickname ?? IpPort; Nickname = nickname ?? IpPort;
SetUdp(); SetUdp();
} }
@@ -65,6 +69,11 @@ namespace EonaCat.Logger.Servers.Udp
internal void SetUdp() internal void SetUdp()
{ {
if (_disposed)
{
return;
}
try try
{ {
DisposeUdp(); DisposeUdp();
@@ -93,13 +102,15 @@ namespace EonaCat.Logger.Servers.Udp
{ {
// Silently ignore // Silently ignore
} }
finally
{
_udp = null; _udp = null;
} }
}
public async Task WriteAsync(byte[] data, bool dontFragment = false) public async Task WriteAsync(byte[] data, bool dontFragment = false)
{ {
if (data == null || data.Length == 0) if (_disposed || data == null || data.Length == 0)
{ {
return; return;
} }
@@ -118,7 +129,6 @@ namespace EonaCat.Logger.Servers.Udp
} }
_udp.DontFragment = dontFragment; _udp.DontFragment = dontFragment;
await SendChunksAsync(data); await SendChunksAsync(data);
} }
finally finally
@@ -129,7 +139,7 @@ namespace EonaCat.Logger.Servers.Udp
public async Task WriteAsync(string data, bool dontFragment = false) public async Task WriteAsync(string data, bool dontFragment = false)
{ {
if (string.IsNullOrWhiteSpace(data)) if (_disposed || string.IsNullOrWhiteSpace(data))
{ {
return; return;
} }
@@ -140,6 +150,11 @@ namespace EonaCat.Logger.Servers.Udp
private async Task SendChunksAsync(byte[] data) private async Task SendChunksAsync(byte[] data)
{ {
if (_disposed || _udp == null)
{
return;
}
int offset = 0; int offset = 0;
int length = data.Length; int length = data.Length;
byte[] buffer = ArrayPool<byte>.Shared.Rent(MaxUdpPacketSize); byte[] buffer = ArrayPool<byte>.Shared.Rent(MaxUdpPacketSize);
@@ -166,15 +181,30 @@ namespace EonaCat.Logger.Servers.Udp
} }
public void Dispose() public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{ {
DisposeUdp(); DisposeUdp();
_sendLock.Dispose(); _sendLock.Dispose();
GC.SuppressFinalize(this); }
} }
~Udp() ~Udp()
{ {
Dispose(); Dispose(false);
} }
} }
} }

View File

@@ -5,20 +5,24 @@ using System.IO;
using System.Net; using System.Net;
using System.Text; using System.Text;
// 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.Logger.Servers.Zabbix.API namespace EonaCat.Logger.Servers.Zabbix.API
{ {
/// <summary> /// <summary>
/// Zabbix API client for authentication and communication with Zabbix server. /// Zabbix API client for authentication and communication with Zabbix server.
/// </summary> /// </summary>
public class ZabbixApi public class ZabbixApi : IDisposable
{ {
private readonly string _user; private readonly string _user;
private readonly string _password; private readonly string _password;
private readonly string _zabbixUrl; private readonly string _zabbixUrl;
private readonly string _basicAuth; private readonly string _basicAuth;
private string _auth; private string _auth;
public event EventHandler<Exception> OnException; private bool _disposed;
public event EventHandler<Exception> OnException;
public bool IsLoggedIn { get; private set; } public bool IsLoggedIn { get; private set; }
public ZabbixApi(string user, string password, string zabbixUrl, bool useBasicAuth = false) public ZabbixApi(string user, string password, string zabbixUrl, bool useBasicAuth = false)
@@ -37,6 +41,11 @@ namespace EonaCat.Logger.Servers.Zabbix.API
/// </summary> /// </summary>
public void Login() public void Login()
{ {
if (_disposed)
{
throw new ObjectDisposedException(nameof(ZabbixApi));
}
dynamic authParams = new ExpandoObject(); dynamic authParams = new ExpandoObject();
authParams.user = _user; authParams.user = _user;
authParams.password = _password; authParams.password = _password;
@@ -51,6 +60,11 @@ namespace EonaCat.Logger.Servers.Zabbix.API
/// </summary> /// </summary>
public bool Logout() public bool Logout()
{ {
if (_disposed)
{
throw new ObjectDisposedException(nameof(ZabbixApi));
}
var response = ResponseAsObject("user.logout", Array.Empty<string>()); var response = ResponseAsObject("user.logout", Array.Empty<string>());
return response?.Result ?? false; return response?.Result ?? false;
} }
@@ -60,6 +74,11 @@ namespace EonaCat.Logger.Servers.Zabbix.API
/// </summary> /// </summary>
public string ResponseAsJson(string method, object parameters) public string ResponseAsJson(string method, object parameters)
{ {
if (_disposed)
{
throw new ObjectDisposedException(nameof(ZabbixApi));
}
var request = CreateRequest(method, parameters); var request = CreateRequest(method, parameters);
var jsonParams = JsonHelper.ToJson(request); var jsonParams = JsonHelper.ToJson(request);
return SendRequest(jsonParams); return SendRequest(jsonParams);
@@ -70,6 +89,11 @@ namespace EonaCat.Logger.Servers.Zabbix.API
/// </summary> /// </summary>
public ZabbixApiResponse ResponseAsObject(string method, object parameters) public ZabbixApiResponse ResponseAsObject(string method, object parameters)
{ {
if (_disposed)
{
throw new ObjectDisposedException(nameof(ZabbixApi));
}
var request = CreateRequest(method, parameters); var request = CreateRequest(method, parameters);
var jsonParams = JsonHelper.ToJson(request); var jsonParams = JsonHelper.ToJson(request);
var responseJson = SendRequest(jsonParams); var responseJson = SendRequest(jsonParams);
@@ -81,9 +105,14 @@ namespace EonaCat.Logger.Servers.Zabbix.API
private string SendRequest(string jsonParams) private string SendRequest(string jsonParams)
{ {
if (_disposed)
{
throw new ObjectDisposedException(nameof(ZabbixApi));
}
try try
{ {
var request = WebRequest.Create(_zabbixUrl); var request = (HttpWebRequest)WebRequest.Create(_zabbixUrl);
request.Method = "POST"; request.Method = "POST";
request.ContentType = "application/json-rpc"; request.ContentType = "application/json-rpc";
@@ -92,13 +121,22 @@ namespace EonaCat.Logger.Servers.Zabbix.API
request.Headers.Add("Authorization", $"Basic {_basicAuth}"); request.Headers.Add("Authorization", $"Basic {_basicAuth}");
} }
using (var writer = new StreamWriter(request.GetRequestStream())) // Write JSON request
using (var requestStream = request.GetRequestStream())
using (var writer = new StreamWriter(requestStream, Encoding.UTF8))
{ {
writer.Write(jsonParams); writer.Write(jsonParams);
} }
using var response = request.GetResponse(); // Read JSON response
using var reader = new StreamReader(response.GetResponseStream()); using var response = (HttpWebResponse)request.GetResponse();
using var responseStream = response.GetResponseStream();
if (responseStream == null)
{
return null;
}
using var reader = new StreamReader(responseStream, Encoding.UTF8);
return reader.ReadToEnd(); return reader.ReadToEnd();
} }
catch (Exception ex) catch (Exception ex)
@@ -107,5 +145,22 @@ namespace EonaCat.Logger.Servers.Zabbix.API
return null; return null;
} }
} }
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
GC.SuppressFinalize(this);
}
~ZabbixApi()
{
Dispose();
}
} }
} }

View File

@@ -25,23 +25,15 @@ namespace EonaCat.Logger.Servers.Zabbix
/// <summary> /// <summary>
/// Zabbix request /// Zabbix request
/// </summary> /// </summary>
/// <param name="host"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public ZabbixRequest(string host, string key, string value) public ZabbixRequest(string host, string key, string value)
{ {
Request = "sender data"; Request = "sender data";
Data = [new ZabbixData(host, key, value)]; Data = new[] { new ZabbixData(host, key, value) };
} }
/// <summary> /// <summary>
/// Sends the request to the Zabbix server /// Sends the request to the Zabbix server
/// </summary> /// </summary>
/// <param name="server"></param>
/// <param name="port"></param>
/// <param name="timeout"></param>
/// <returns></returns>
/// <exception cref="TimeoutException"></exception>
public async Task<ZabbixResponse> SendAsync(string server, int port = 10051, int timeout = 500) public async Task<ZabbixResponse> SendAsync(string server, int port = 10051, int timeout = 500)
{ {
if (string.IsNullOrWhiteSpace(server)) if (string.IsNullOrWhiteSpace(server))
@@ -50,21 +42,37 @@ namespace EonaCat.Logger.Servers.Zabbix
} }
string json = JsonHelper.ToJson(new ZabbixRequest(Data[0].Host, Data[0].Key, Data[0].Value)); string json = JsonHelper.ToJson(new ZabbixRequest(Data[0].Host, Data[0].Key, Data[0].Value));
using (TcpClient tcpClient = new TcpClient(server, port))
using (NetworkStream stream = tcpClient.GetStream())
{
byte[] header = Encoding.ASCII.GetBytes("ZBXD\x01");
byte[] dataLength = BitConverter.GetBytes((long)json.Length);
byte[] content = Encoding.ASCII.GetBytes(json);
byte[] message = new byte[header.Length + dataLength.Length + content.Length];
using var tcpClient = new TcpClient();
var connectTask = tcpClient.ConnectAsync(server, port);
if (await Task.WhenAny(connectTask, Task.Delay(timeout)).ConfigureAwait(false) != connectTask)
{
throw new TimeoutException("Connection timed out.");
}
using NetworkStream stream = tcpClient.GetStream();
stream.ReadTimeout = timeout;
stream.WriteTimeout = timeout;
// Header and length
byte[] header = Encoding.ASCII.GetBytes("ZBXD\x01");
byte[] dataLength = BitConverter.GetBytes((long)Encoding.ASCII.GetByteCount(json));
if (!BitConverter.IsLittleEndian)
{
Array.Reverse(dataLength);
}
byte[] content = Encoding.ASCII.GetBytes(json);
byte[] message = new byte[header.Length + dataLength.Length + content.Length];
Buffer.BlockCopy(header, 0, message, 0, header.Length); Buffer.BlockCopy(header, 0, message, 0, header.Length);
Buffer.BlockCopy(dataLength, 0, message, header.Length, dataLength.Length); Buffer.BlockCopy(dataLength, 0, message, header.Length, dataLength.Length);
Buffer.BlockCopy(content, 0, message, header.Length + dataLength.Length, content.Length); Buffer.BlockCopy(content, 0, message, header.Length + dataLength.Length, content.Length);
stream.Write(message, 0, message.Length); await stream.WriteAsync(message, 0, message.Length).ConfigureAwait(false);
stream.Flush(); await stream.FlushAsync().ConfigureAwait(false);
// Wait for data available
int counter = 0; int counter = 0;
while (!stream.DataAvailable) while (!stream.DataAvailable)
{ {
@@ -75,16 +83,32 @@ namespace EonaCat.Logger.Servers.Zabbix
} }
else else
{ {
throw new TimeoutException(); throw new TimeoutException("No response received from Zabbix server.");
} }
} }
byte[] responseBytes = new byte[BUFFER_SIZE]; // Read response safely
stream.Read(responseBytes, 0, responseBytes.Length); using var memoryStream = new System.IO.MemoryStream();
string responseAsString = Encoding.UTF8.GetString(responseBytes); byte[] buffer = new byte[BUFFER_SIZE];
string jsonResult = responseAsString.Substring(responseAsString.IndexOf('{')); int bytesRead;
do
{
bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
if (bytesRead > 0)
{
memoryStream.Write(buffer, 0, bytesRead);
}
} while (bytesRead == buffer.Length);
string responseAsString = Encoding.UTF8.GetString(memoryStream.ToArray());
int jsonStart = responseAsString.IndexOf('{');
if (jsonStart < 0)
{
return new ZabbixResponse();
}
string jsonResult = responseAsString.Substring(jsonStart);
return JsonHelper.ToObject<ZabbixResponse>(jsonResult); return JsonHelper.ToObject<ZabbixResponse>(jsonResult);
} }
} }
} }
}

View File

@@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="EonaCat.MemoryGuard" Version="1.0.5" /> <PackageReference Include="EonaCat.MemoryGuard" Version="1.0.9" />
<PackageReference Include="EonaCat.MemoryGuard.Generator" Version="1.1.0" /> <PackageReference Include="EonaCat.MemoryGuard.Generator" Version="1.1.0" />
<PackageReference Include="EonaCat.Versioning" Version="1.2.8"> <PackageReference Include="EonaCat.Versioning" Version="1.2.8">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

View File

@@ -6,7 +6,6 @@
using EonaCat.Logger.EonaCatCoreLogger.Models; using EonaCat.Logger.EonaCatCoreLogger.Models;
using EonaCat.Logger.LogClient; using EonaCat.Logger.LogClient;
using EonaCat.Logger.Managers; using EonaCat.Logger.Managers;
using EonaCat.Logger.Test.Web;
using EonaCat.MemoryGuard; using EonaCat.MemoryGuard;
using EonaCat.Versioning.Helpers; using EonaCat.Versioning.Helpers;
using EonaCat.Web.RateLimiter; using EonaCat.Web.RateLimiter;
@@ -24,6 +23,7 @@
PredictionInterval = TimeSpan.FromSeconds(15), PredictionInterval = TimeSpan.FromSeconds(15),
LeakDetectionThreshold = TimeSpan.FromSeconds(5), LeakDetectionThreshold = TimeSpan.FromSeconds(5),
SuspiciousObjectThreshold = TimeSpan.FromSeconds(3), SuspiciousObjectThreshold = TimeSpan.FromSeconds(3),
BackgroundReportingInterval = TimeSpan.FromMinutes(1.5),
CaptureStackTraces = true, CaptureStackTraces = true,
EnableAutoRemediation = true, EnableAutoRemediation = true,
AutoSaveReports = true, AutoSaveReports = true,