diff --git a/EonaCat.Logger.LogClient/EonaCat.Logger.LogClient.csproj b/EonaCat.Logger.LogClient/EonaCat.Logger.LogClient.csproj
index 2a2117f..5c8e159 100644
--- a/EonaCat.Logger.LogClient/EonaCat.Logger.LogClient.csproj
+++ b/EonaCat.Logger.LogClient/EonaCat.Logger.LogClient.csproj
@@ -3,7 +3,7 @@
netstandard2.1
true
EonaCat.Logger.LogClient
- 1.0.1
+ 1.0.2
EonaCat (Jeroen Saey)
Logging client for the EonaCat Logger LogServer
logging;monitoring;analytics;diagnostics
@@ -25,7 +25,7 @@
-
+
diff --git a/EonaCat.Logger.LogClient/LogCentralClient.cs b/EonaCat.Logger.LogClient/LogCentralClient.cs
index 30973cb..3165b39 100644
--- a/EonaCat.Logger.LogClient/LogCentralClient.cs
+++ b/EonaCat.Logger.LogClient/LogCentralClient.cs
@@ -93,7 +93,10 @@ namespace EonaCat.Logger.LogClient
private async Task FlushAsync()
{
- if (_logQueue.IsEmpty) return;
+ if (_logQueue.IsEmpty)
+ {
+ return;
+ }
await _flushSemaphore.WaitAsync();
try
@@ -166,7 +169,10 @@ namespace EonaCat.Logger.LogClient
public void Dispose()
{
- if (_disposed) return;
+ if (_disposed)
+ {
+ return;
+ }
_flushTimer?.Dispose();
FlushAsync().GetAwaiter().GetResult();
diff --git a/EonaCat.Logger/EonaCat.Logger.csproj b/EonaCat.Logger/EonaCat.Logger.csproj
index da005ff..1ef5afb 100644
--- a/EonaCat.Logger/EonaCat.Logger.csproj
+++ b/EonaCat.Logger/EonaCat.Logger.csproj
@@ -23,7 +23,7 @@
- 1.5.2+{chash:10}.{c:ymd}
+ 1.5.3+{chash:10}.{c:ymd}
true
true
v[0-9]*
diff --git a/EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLogger.cs
index 11a60e9..537eab4 100644
--- a/EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLogger.cs
+++ b/EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLogger.cs
@@ -1,26 +1,32 @@
-using Microsoft.Extensions.Logging;
+using EonaCat.Logger.EonaCatCoreLogger;
+using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data.Common;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
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 BatchingDatabaseLogger : ILogger, IDisposable
+ public sealed class BatchingDatabaseLogger : ILogger, IDisposable
{
+ // 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.
+
private readonly string _categoryName;
private readonly BatchingDatabaseLoggerOptions _options;
private readonly LoggerScopedContext _context = new();
+
private readonly BlockingCollection _queue;
- private readonly CancellationTokenSource _cts;
+ private readonly CancellationTokenSource _cts = new();
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 OnException;
public BatchingDatabaseLogger(string categoryName, BatchingDatabaseLoggerOptions options)
@@ -29,22 +35,22 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_options = options;
IncludeCorrelationId = options.IncludeCorrelationId;
- _queue = new BlockingCollection(new ConcurrentQueue());
- _cts = new CancellationTokenSource();
- _processingTask = Task.Run(ProcessQueueAsync);
+ _queue = new BlockingCollection(new ConcurrentQueue(), MaxQueueSize);
+ _processingTask = Task.Run(() => ProcessQueueAsync(_cts.Token));
}
- public IDisposable BeginScope(TState state) => null;
- public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
+ public IDisposable BeginScope(TState state) => _context.BeginScope(state);
- public void SetContext(string key, string value) => _context.Set(key, value);
- public void ClearContext() => _context.Clear();
- public string GetContext(string key) => _context.Get(key);
+ public bool IsEnabled(LogLevel logLevel) => !_disposed && _options.IsEnabled;
- public void Log(LogLevel logLevel, EventId eventId, TState state,
- Exception exception, Func formatter)
+ public void Log(
+ LogLevel logLevel,
+ EventId eventId,
+ TState state,
+ Exception exception,
+ Func formatter)
{
- if (!IsEnabled(logLevel) || formatter == null)
+ if (_disposed || !IsEnabled(logLevel) || formatter == null)
{
return;
}
@@ -60,7 +66,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_context.Set("CorrelationId", correlationId);
}
- _queue.Add(new LogEntry
+ var entry = new LogEntry
{
Timestamp = DateTime.UtcNow,
LogLevel = logLevel.ToString(),
@@ -68,53 +74,52 @@ namespace EonaCat.Logger.EonaCatCoreLogger
Message = message,
Exception = exception?.ToString(),
CorrelationId = correlationId
- });
- }
+ };
- private async Task ProcessQueueAsync()
- {
- var batch = new List();
- var timeoutMs = (int)Math.Min(_options.BatchInterval.TotalMilliseconds, int.MaxValue);
-
- while (!_cts.Token.IsCancellationRequested)
+ // Drop oldest when full (non-blocking)
+ while (!_queue.TryAdd(entry))
{
- try
- {
- if (_queue.TryTake(out var logEntry, timeoutMs, _cts.Token))
- {
- batch.Add(logEntry);
-
- // Drain the queue quickly without waiting
- while (_queue.TryTake(out var additionalEntry))
- {
- batch.Add(additionalEntry);
- }
- }
-
- if (batch.Count > 0)
- {
- await InsertBatchSafelyAsync(batch);
- }
- }
- catch (OperationCanceledException)
- {
- break;
- }
- catch (Exception ex)
- {
- OnException?.Invoke(this, ex);
- }
- }
-
- // Final flush outside the loop
- if (batch.Count > 0)
- {
- await InsertBatchSafelyAsync(batch);
+ _queue.TryTake(out _); // discard oldest
}
}
- private async Task InsertBatchSafelyAsync(List batch)
+ private async Task ProcessQueueAsync(CancellationToken token)
{
+ var batch = new List(_options.BatchSize);
+
+ try
+ {
+ foreach (var entry in _queue.GetConsumingEnumerable(token))
+ {
+ batch.Add(entry);
+
+ if (batch.Count >= _options.BatchSize)
+ {
+ await FlushBatchAsync(batch);
+ }
+ }
+ }
+ catch (OperationCanceledException) { }
+ catch (Exception ex)
+ {
+ OnException?.Invoke(this, ex);
+ }
+ finally
+ {
+ if (batch.Count > 0)
+ {
+ await FlushBatchAsync(batch); // flush remaining
+ }
+ }
+ }
+
+ private async Task FlushBatchAsync(List batch)
+ {
+ if (batch.Count == 0)
+ {
+ return;
+ }
+
try
{
await InsertBatchAsync(batch);
@@ -131,20 +136,16 @@ namespace EonaCat.Logger.EonaCatCoreLogger
private async Task InsertBatchAsync(List batch)
{
- using var connection = _options.DbProviderFactory.CreateConnection();
- if (connection == null)
- {
- throw new InvalidOperationException("Failed to create database connection.");
- }
+ using var connection = _options.DbProviderFactory.CreateConnection()
+ ?? throw new InvalidOperationException("Failed to create database connection.");
connection.ConnectionString = _options.ConnectionString;
- await connection.OpenAsync();
+ await connection.OpenAsync(_cts.Token);
foreach (var entry in batch)
{
using var command = connection.CreateCommand();
command.CommandText = _options.InsertCommand;
- command.Parameters.Clear();
command.Parameters.Add(CreateParameter(command, "Timestamp", entry.Timestamp));
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, "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();
param.ParameterName = $"@{name}";
@@ -167,26 +168,34 @@ namespace EonaCat.Logger.EonaCatCoreLogger
public void Dispose()
{
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
_cts.Cancel();
_queue.CompleteAdding();
+
try
{
_processingTask.Wait(_options.ShutdownTimeout);
}
- catch (Exception ex)
- {
- OnException?.Invoke(this, ex);
- }
+ catch { /* ignore */ }
+
+ _queue.Dispose();
+ _cts.Dispose();
}
- private class LogEntry
+ private sealed class LogEntry
{
- public DateTime Timestamp { get; set; }
- public string LogLevel { get; set; }
- public string Category { get; set; }
- public string Message { get; set; }
- public string Exception { get; set; }
- public string CorrelationId { get; set; }
+ public DateTime Timestamp { get; init; }
+ public string LogLevel { get; init; }
+ public string Category { get; init; }
+ public string Message { get; init; }
+ public string Exception { get; init; }
+ public string CorrelationId { get; init; }
}
}
-}
+}
\ No newline at end of file
diff --git a/EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLoggerOptions.cs
index f77607b..6b2a8d5 100644
--- a/EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLoggerOptions.cs
+++ b/EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLoggerOptions.cs
@@ -15,6 +15,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
public bool IsEnabled { get; set; } = true;
public bool IncludeCorrelationId { get; set; }
+ public int BatchSize { get; set; } = 50;
public TimeSpan BatchInterval { get; set; } = TimeSpan.FromSeconds(5); // 5 sec batch
public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(10); // wait on shutdown
diff --git a/EonaCat.Logger/EonaCatCoreLogger/DatabaseLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/DatabaseLogger.cs
index 648fb48..9570ed3 100644
--- a/EonaCat.Logger/EonaCatCoreLogger/DatabaseLogger.cs
+++ b/EonaCat.Logger/EonaCatCoreLogger/DatabaseLogger.cs
@@ -7,18 +7,23 @@ using System.Threading.Tasks;
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 DatabaseLogger : ILogger, IDisposable
+ public sealed class DatabaseLogger : ILogger, IDisposable
{
+ // 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.
+
private readonly string _categoryName;
private readonly DatabaseLoggerOptions _options;
private readonly LoggerScopedContext _context = new();
private static readonly ConcurrentQueue _queue = new();
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 event EventHandler OnException;
@@ -29,69 +34,88 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_options = options;
IncludeCorrelationId = options.IncludeCorrelationId;
- if (!_flushLoopStarted)
+ if (Interlocked.Increment(ref _refCount) == 1)
{
- _flushLoopStarted = true;
- _cts = new CancellationTokenSource();
- Task.Run(() => FlushLoopAsync(_cts.Token));
+ _flushTask = Task.Run(() => FlushLoopAsync(_cts.Token));
}
}
- public IDisposable BeginScope(TState state) => null;
- public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
+ public IDisposable BeginScope(TState state)
+ => _context.BeginScope(state);
- public void SetContext(string key, string value) => _context.Set(key, value);
- public void ClearContext() => _context.Clear();
- public string GetContext(string key) => _context.Get(key);
+ public bool IsEnabled(LogLevel logLevel)
+ => _options.IsEnabled;
- public void Log(LogLevel logLevel, EventId eventId, TState state,
- Exception exception, Func formatter)
+ public void SetContext(string key, string value)
+ => _context.Set(key, value);
+
+ public void ClearContext()
+ => _context.Clear();
+
+ public string GetContext(string key)
+ => _context.Get(key);
+
+ public void Log(
+ LogLevel logLevel,
+ EventId eventId,
+ TState state,
+ Exception exception,
+ Func formatter)
{
if (!IsEnabled(logLevel) || formatter == null)
{
return;
}
+ if (_queue.Count >= MaxQueueSize)
+ {
+ _queue.TryDequeue(out _); // drop oldest
+ }
+
if (IncludeCorrelationId)
{
- var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
+ var correlationId =
+ _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
_context.Set("CorrelationId", correlationId);
}
- var message = formatter(state, exception);
-
- var entry = new LogEntry
+ _queue.Enqueue(new LogEntry
{
Timestamp = DateTime.UtcNow,
LogLevel = logLevel.ToString(),
Category = _categoryName,
- Message = message,
+ Message = formatter(state, exception),
Exception = exception?.ToString(),
CorrelationId = _context.Get("CorrelationId")
- };
-
- _queue.Enqueue(entry);
-
- if (_queue.Count >= _options.FlushBatchSize)
- {
- _ = FlushBufferAsync();
- }
+ });
}
private async Task FlushLoopAsync(CancellationToken token)
{
- while (!token.IsCancellationRequested)
+ try
{
- await Task.Delay(TimeSpan.FromSeconds(_options.FlushIntervalSeconds), token);
- await FlushBufferAsync();
+ while (!token.IsCancellationRequested)
+ {
+ await Task.Delay(
+ TimeSpan.FromSeconds(_options.FlushIntervalSeconds),
+ token);
+
+ await FlushBufferAsync();
+ }
}
+ catch (OperationCanceledException)
+ {
+ // Expected on shutdown
+ }
+
+ await FlushBufferAsync(); // final drain
}
private async Task FlushBufferAsync()
{
if (!await _flushLock.WaitAsync(0))
{
- return; // Another flush is in progress
+ return;
}
try
@@ -100,10 +124,12 @@ namespace EonaCat.Logger.EonaCatCoreLogger
{
try
{
- 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 DB connection.");
}
connection.ConnectionString = _options.ConnectionString;
@@ -111,7 +137,6 @@ namespace EonaCat.Logger.EonaCatCoreLogger
using var command = connection.CreateCommand();
command.CommandText = _options.InsertCommand;
- command.Parameters.Clear();
command.Parameters.Add(CreateParameter(command, "Timestamp", entry.Timestamp));
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();
param.ParameterName = $"@{name}";
@@ -144,19 +172,28 @@ namespace EonaCat.Logger.EonaCatCoreLogger
public void Dispose()
{
- _cts?.Cancel();
- _flushLock.Dispose();
- _cts?.Dispose();
+ if (Interlocked.Decrement(ref _refCount) == 0)
+ {
+ _cts.Cancel();
+
+ try
+ {
+ _flushTask?.Wait();
+ }
+ catch (AggregateException) { }
+
+ _cts.Dispose();
+ }
}
- private class LogEntry
+ private sealed class LogEntry
{
- public DateTime Timestamp { get; set; }
- public string LogLevel { get; set; }
- public string Category { get; set; }
- public string Message { get; set; }
- public string Exception { get; set; }
- public string CorrelationId { get; set; }
+ public DateTime Timestamp { get; init; }
+ public string LogLevel { get; init; }
+ public string Category { get; init; }
+ public string Message { get; init; }
+ public string Exception { get; init; }
+ public string CorrelationId { get; init; }
}
- }
+ }
}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/DiscordLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/DiscordLogger.cs
index fb2872e..8c0030a 100644
--- a/EonaCat.Logger/EonaCatCoreLogger/DiscordLogger.cs
+++ b/EonaCat.Logger/EonaCatCoreLogger/DiscordLogger.cs
@@ -8,36 +8,42 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
+// This file is part of the EonaCat project(s) which is released under the Apache License.
+// See the LICENSE file or go to https://EonaCat.com/License for full license details.
+
namespace EonaCat.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 DiscordLogger : ILogger, IDisposable
{
private readonly string _categoryName;
private readonly DiscordLoggerOptions _options;
- private readonly LoggerScopedContext _context = new();
+ private readonly LoggerScopedContext _context = new LoggerScopedContext();
+
+ // Static shared resources
+ private static readonly ConcurrentQueue _messageQueue = new ConcurrentQueue();
+ private static readonly HttpClient _httpClient = new HttpClient();
+ private static readonly SemaphoreSlim _flushLock = new SemaphoreSlim(1, 1);
- private static readonly ConcurrentQueue _messageQueue = new();
- private static readonly HttpClient _httpClient = new();
- private static readonly SemaphoreSlim _flushLock = new(1, 1);
private static bool _flushLoopStarted = false;
private static CancellationTokenSource _cts;
+ private static Task _flushTask;
public bool IncludeCorrelationId { get; set; }
public event EventHandler OnException;
public DiscordLogger(string categoryName, DiscordLoggerOptions options)
{
- _categoryName = categoryName;
- _options = options;
+ _categoryName = categoryName ?? throw new ArgumentNullException(nameof(categoryName));
+ _options = options ?? throw new ArgumentNullException(nameof(options));
IncludeCorrelationId = options.IncludeCorrelationId;
+ // Start flush loop once
if (!_flushLoopStarted)
{
_flushLoopStarted = true;
_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}]**",
$"**[{logLevel}]**",
$"**[{_categoryName}]**",
- $"Message: {message}",
+ $"Message: {message}"
};
foreach (var kvp in _context.GetAll())
@@ -82,26 +88,34 @@ namespace EonaCat.Logger.EonaCatCoreLogger
logParts.Add($"Exception: {exception}");
}
- string fullMessage = string.Join("\n", logParts);
-
- // Enqueue and return immediately - non-blocking
- _messageQueue.Enqueue(fullMessage);
+ _messageQueue.Enqueue(string.Join("\n", logParts));
}
- private async Task FlushLoopAsync(CancellationToken token)
+ private void FlushLoop(CancellationToken token)
{
- while (!token.IsCancellationRequested)
+ try
{
- await Task.Delay(TimeSpan.FromSeconds(_options.FlushIntervalSeconds), token);
- await FlushBufferAsync();
+ while (!token.IsCancellationRequested)
+ {
+ Thread.Sleep(TimeSpan.FromSeconds(_options.FlushIntervalSeconds));
+ 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
@@ -111,12 +125,14 @@ namespace EonaCat.Logger.EonaCatCoreLogger
try
{
var payload = new { content = message };
- var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json");
- var response = await _httpClient.PostAsync(_options.WebhookUrl, content);
- if (!response.IsSuccessStatusCode)
+ using (var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json"))
{
- var error = await response.Content.ReadAsStringAsync();
- OnException?.Invoke(this, new Exception($"Discord webhook failed: {response.StatusCode} {error}"));
+ var response = await _httpClient.PostAsync(_options.WebhookUrl, content, token);
+ if (!response.IsSuccessStatusCode)
+ {
+ var error = await response.Content.ReadAsStringAsync();
+ OnException?.Invoke(this, new Exception($"Discord webhook failed: {response.StatusCode} {error}"));
+ }
}
}
catch (Exception ex)
@@ -133,9 +149,24 @@ namespace EonaCat.Logger.EonaCatCoreLogger
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();
- _cts?.Dispose();
}
}
}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/ElasticSearchLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/ElasticSearchLogger.cs
index 07e1550..04726a5 100644
--- a/EonaCat.Logger/EonaCatCoreLogger/ElasticSearchLogger.cs
+++ b/EonaCat.Logger/EonaCatCoreLogger/ElasticSearchLogger.cs
@@ -1,32 +1,32 @@
-using System.Linq;
-
-// 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.
+using EonaCat.Json;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
namespace EonaCat.Logger.EonaCatCoreLogger
{
- using EonaCat.Json;
- using Microsoft.Extensions.Logging;
- using System;
- using System.Collections.Generic;
- using System.Net.Http;
- using System.Text;
- using System.Text.Json;
- using System.Threading.Tasks;
+ // This file is part of the EonaCat project(s) which is released under the Apache License.
+ // See the LICENSE file or go to https://EonaCat.com/License for full license details.
- public class ElasticSearchLogger : ILogger
+ public class ElasticSearchLogger : ILogger, IDisposable
{
-
private readonly string _categoryName;
private readonly ElasticSearchLoggerOptions _options;
- private static readonly HttpClient _httpClient = new HttpClient();
- private static readonly List _buffer = new List();
- private static readonly object _lock = new object();
- private static bool _flushLoopStarted = false;
- public event EventHandler OnException;
- public event EventHandler OnInvalidStatusCode;
+ private readonly HttpClient _httpClient;
+ private readonly List _buffer = new();
+ private readonly object _lock = new();
+ private readonly CancellationTokenSource _cts = new();
+ private readonly Task _flushTask;
+
private readonly LoggerScopedContext _context = new();
public bool IncludeCorrelationId { get; set; }
+ public event EventHandler OnException;
+ public event EventHandler OnInvalidStatusCode;
public ElasticSearchLogger(string categoryName, ElasticSearchLoggerOptions options)
{
@@ -34,18 +34,15 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_options = options;
IncludeCorrelationId = options.IncludeCorrelationId;
- if (!_flushLoopStarted)
- {
- _flushLoopStarted = true;
- Task.Run(FlushLoopAsync);
- }
+ _httpClient = new HttpClient();
+ _flushTask = Task.Run(() => FlushLoopAsync(_cts.Token));
}
public void SetContext(string key, string value) => _context.Set(key, value);
public void ClearContext() => _context.Clear();
public string GetContext(string key) => _context.Get(key);
- public IDisposable BeginScope(TState state) => null;
+ public IDisposable BeginScope(TState state) => _context.BeginScope(state);
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log(LogLevel logLevel, EventId eventId, TState state,
@@ -56,14 +53,12 @@ namespace EonaCat.Logger.EonaCatCoreLogger
return;
}
- // Get correlation ID from context or generate new one
if (IncludeCorrelationId)
{
var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
_context.Set("CorrelationId", correlationId);
}
- // Prepare the log entry
var logDoc = new
{
timestamp = DateTime.UtcNow,
@@ -72,35 +67,41 @@ namespace EonaCat.Logger.EonaCatCoreLogger
message = formatter(state, exception),
exception = exception?.ToString(),
eventId = eventId.Id,
- customContext = _context.GetAll()
+ customContext = _context.GetAll()
};
- // Serialize log to JSON
string json = JsonHelper.ToJson(logDoc);
- // Add to the buffer
lock (_lock)
{
_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));
- await FlushBufferAsync();
+ while (!token.IsCancellationRequested)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(_options.FlushIntervalSeconds), token);
+ await FlushBufferAsync();
+ }
}
+ catch (OperationCanceledException) { }
+
+ await FlushBufferAsync(); // flush remaining logs on shutdown
}
private async Task FlushBufferAsync()
{
List toSend;
+
lock (_lock)
{
if (_buffer.Count == 0)
@@ -112,27 +113,23 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_buffer.Clear();
}
- // Elasticsearch URL with dynamic index
string indexName = $"{_options.IndexName}-{DateTime.UtcNow:yyyy.MM.dd}";
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");
- // Add authentication headers if configured
if (!string.IsNullOrWhiteSpace(_options.Username))
{
var authToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_options.Username}:{_options.Password}"));
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", authToken);
}
- // Add custom headers (static ones)
foreach (var header in _options.CustomHeaders)
{
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
- // Add template headers (dynamic ones based on log data)
var dynamicHeaders = new Dictionary
{
{ "index", _options.IndexName },
@@ -146,13 +143,11 @@ namespace EonaCat.Logger.EonaCatCoreLogger
request.Headers.TryAddWithoutValidation(header.Key, value);
}
- // Add context headers (correlationId, custom context)
foreach (var kv in _context.GetAll())
{
request.Headers.TryAddWithoutValidation($"X-Context-{kv.Key}", kv.Value);
}
- // Prepare the content for the request
request.Content = new StringContent(
_options.UseBulkInsert
? 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}");
}
}
- 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);
}
+
return template;
}
+
+ public void Dispose()
+ {
+ _cts.Cancel();
+ _flushTask.Wait();
+ _cts.Dispose();
+ _httpClient.Dispose();
+ }
}
}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/ElasticSearchLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/ElasticSearchLoggerOptions.cs
index ede4602..87ca835 100644
--- a/EonaCat.Logger/EonaCatCoreLogger/ElasticSearchLoggerOptions.cs
+++ b/EonaCat.Logger/EonaCatCoreLogger/ElasticSearchLoggerOptions.cs
@@ -20,5 +20,6 @@ namespace EonaCat.Logger.EonaCatCoreLogger
public int FlushIntervalSeconds { get; set; } = 5;
public bool UseBulkInsert { get; set; } = true;
public bool IncludeCorrelationId { get; set; }
+ public int MaxBufferSize { get; set; } = 1000;
}
}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/HttpLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/HttpLogger.cs
index 08dd6ba..8b4f3f5 100644
--- a/EonaCat.Logger/EonaCatCoreLogger/HttpLogger.cs
+++ b/EonaCat.Logger/EonaCatCoreLogger/HttpLogger.cs
@@ -3,22 +3,27 @@ using Microsoft.Extensions.Logging;
using System;
using System.Net.Http;
using System.Text;
+using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
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 HttpLogger : ILogger
+ public sealed class HttpLogger : ILogger, IDisposable
{
+ // 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.
+
private readonly string _categoryName;
private readonly HttpLoggerOptions _options;
private readonly LoggerScopedContext _context = new();
- private static readonly HttpClient _client = new();
+ private readonly HttpClient _client;
+ private readonly BlockingCollection> _queue;
+ private readonly CancellationTokenSource _cts;
+ private readonly Task _processingTask;
public bool IncludeCorrelationId { get; set; }
-
public event EventHandler OnException;
public event EventHandler OnInvalidStatusCode;
@@ -27,13 +32,18 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_categoryName = categoryName;
_options = options;
IncludeCorrelationId = options.IncludeCorrelationId;
+
+ _client = new HttpClient();
+ _queue = new BlockingCollection>(boundedCapacity: 10000);
+ _cts = new CancellationTokenSource();
+ _processingTask = Task.Run(() => ProcessQueueAsync(_cts.Token));
}
public void SetContext(string key, string value) => _context.Set(key, value);
public void ClearContext() => _context.Clear();
public string GetContext(string key) => _context.Get(key);
- public IDisposable BeginScope(TState state) => null;
+ public IDisposable BeginScope(TState state) => _context.BeginScope(state);
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log(LogLevel logLevel, EventId eventId, TState state,
@@ -44,8 +54,6 @@ namespace EonaCat.Logger.EonaCatCoreLogger
return;
}
- string message = formatter(state, exception);
-
if (IncludeCorrelationId)
{
var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
@@ -57,7 +65,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
{ "timestamp", DateTime.UtcNow },
{ "level", logLevel.ToString() },
{ "category", _categoryName },
- { "message", message },
+ { "message", formatter(state, exception) },
{ "eventId", eventId.Id }
};
@@ -67,66 +75,54 @@ namespace EonaCat.Logger.EonaCatCoreLogger
payload["context"] = contextData;
}
- Task.Run(async () =>
+ // Add to queue, drop oldest if full
+ while (!_queue.TryAdd(payload))
{
- try
+ _queue.TryTake(out _); // drop oldest
+ }
+ }
+
+ private async Task ProcessQueueAsync(CancellationToken token)
+ {
+ try
+ {
+ foreach (var payload in _queue.GetConsumingEnumerable(token))
{
- var content = _options.SendAsJson
- ? new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json")
- : new StringContent(message, Encoding.UTF8, "text/plain");
-
- var response = await _client.PostAsync(_options.Endpoint, content);
-
- if (!response.IsSuccessStatusCode)
+ try
{
- var statusCode = response.StatusCode;
- 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()
- };
+ var content = _options.SendAsJson
+ ? new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json")
+ : new StringContent(payload["message"].ToString(), Encoding.UTF8, "text/plain");
- OnInvalidStatusCode?.Invoke(this, statusCodeMessage);
+ var response = await _client.PostAsync(_options.Endpoint, content, token);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ OnInvalidStatusCode?.Invoke(this, response.StatusCode.ToString());
+ }
+ }
+ catch (Exception ex)
+ {
+ OnException?.Invoke(this, ex);
}
}
- catch (Exception 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();
}
}
}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/JsonFileLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/JsonFileLogger.cs
index ef25a27..15098f9 100644
--- a/EonaCat.Logger/EonaCatCoreLogger/JsonFileLogger.cs
+++ b/EonaCat.Logger/EonaCatCoreLogger/JsonFileLogger.cs
@@ -1,6 +1,7 @@
using EonaCat.Json;
using Microsoft.Extensions.Logging;
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Text;
@@ -8,15 +9,19 @@ using System.Threading;
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 JsonFileLogger : ILogger
+ public sealed class JsonFileLogger : ILogger, IDisposable
{
+ // 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.
+
private readonly string _categoryName;
private readonly JsonFileLoggerOptions _options;
private readonly string _filePath;
private readonly LoggerScopedContext _context = new();
- private static readonly SemaphoreSlim _fileLock = new(1, 1);
+
+ private readonly BlockingCollection _queue;
+ private readonly Thread _workerThread;
+ private readonly CancellationTokenSource _cts = new CancellationTokenSource();
public bool IncludeCorrelationId { get; set; }
public event EventHandler OnException;
@@ -25,9 +30,11 @@ namespace EonaCat.Logger.EonaCatCoreLogger
{
_categoryName = categoryName;
_options = options;
- _filePath = Path.Combine(_options.LogDirectory, _options.FileName);
IncludeCorrelationId = options.IncludeCorrelationId;
+ _filePath = Path.Combine(_options.LogDirectory, _options.FileName);
+ _queue = new BlockingCollection(10000); // bounded queue
+
try
{
Directory.CreateDirectory(_options.LogDirectory);
@@ -36,13 +43,16 @@ namespace EonaCat.Logger.EonaCatCoreLogger
{
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 ClearContext() => _context.Clear();
public string GetContext(string key) => _context.Get(key);
-
- public IDisposable BeginScope(TState state) => null;
+ public IDisposable BeginScope(TState state) => _context.BeginScope(state);
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log(LogLevel logLevel, EventId eventId, TState state,
@@ -55,8 +65,6 @@ namespace EonaCat.Logger.EonaCatCoreLogger
try
{
- string message = formatter(state, exception);
-
if (IncludeCorrelationId)
{
var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
@@ -68,7 +76,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
{ "timestamp", DateTime.UtcNow },
{ "level", logLevel.ToString() },
{ "category", _categoryName },
- { "message", message },
+ { "message", formatter(state, exception) },
{ "exception", exception?.ToString() },
{ "eventId", eventId.Id }
};
@@ -81,14 +89,10 @@ namespace EonaCat.Logger.EonaCatCoreLogger
string json = JsonHelper.ToJson(logObject);
- _fileLock.Wait();
- try
+ // Enqueue, drop oldest if full
+ while (!_queue.TryAdd(json))
{
- File.AppendAllText(_filePath, json + Environment.NewLine, Encoding.UTF8);
- }
- finally
- {
- _fileLock.Release();
+ _queue.TryTake(out _); // drop oldest
}
}
catch (Exception ex)
@@ -96,5 +100,39 @@ namespace EonaCat.Logger.EonaCatCoreLogger
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();
+ }
}
}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/LogContext.cs b/EonaCat.Logger/EonaCatCoreLogger/LogContext.cs
index 44361e6..ed11c05 100644
--- a/EonaCat.Logger/EonaCatCoreLogger/LogContext.cs
+++ b/EonaCat.Logger/EonaCatCoreLogger/LogContext.cs
@@ -1,47 +1,89 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Threading;
namespace EonaCat.Logger.EonaCatCoreLogger
{
- public class LoggerScopedContext : IDisposable
+ internal sealed class LoggerScopedContext
{
- private readonly ConcurrentDictionary _context = new();
- private readonly string _correlationId;
+ private static readonly AsyncLocal>> _scopes
+ = new();
- public LoggerScopedContext(string correlationId = null)
+ public IDisposable BeginScope(TState state)
{
- _correlationId = correlationId;
+ _scopes.Value ??= new Stack>();
+ _scopes.Value.Push(new Dictionary());
+ return new ScopePopper();
}
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)
{
- 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 GetAll()
{
- return new Dictionary(_context);
+ var result = new Dictionary(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()
{
- _context.Clear();
+ _scopes.Value?.Clear();
}
- public void Dispose()
+ private sealed class ScopePopper : IDisposable
{
- Clear();
- }
-
- public IDisposable CreateScope()
- {
- return this;
+ public void Dispose()
+ {
+ if (_scopes.Value?.Count > 0)
+ {
+ _scopes.Value.Pop();
+ }
+ }
}
}
}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/SlackLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/SlackLogger.cs
index de1b34f..8c52379 100644
--- a/EonaCat.Logger/EonaCatCoreLogger/SlackLogger.cs
+++ b/EonaCat.Logger/EonaCatCoreLogger/SlackLogger.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
+using System.Threading;
using System.Threading.Channels;
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.
// 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 SlackLoggerOptions _options;
private readonly LoggerScopedContext _context = new();
- private readonly Channel _logChannel = Channel.CreateUnbounded();
+ private readonly Channel _logChannel;
private readonly HttpClient _httpClient;
+ private readonly CancellationTokenSource _cts = new();
+ private readonly Task _processingTask;
public bool IncludeCorrelationId { get; set; }
public event EventHandler OnException;
@@ -29,7 +33,15 @@ namespace EonaCat.Logger.EonaCatCoreLogger
IncludeCorrelationId = options.IncludeCorrelationId;
_httpClient = new HttpClient();
- Task.Run(ProcessLogQueueAsync);
+ // Bounded channel to prevent unbounded memory growth
+ _logChannel = Channel.CreateBounded(new BoundedChannelOptions(1000)
+ {
+ FullMode = BoundedChannelFullMode.DropOldest,
+ SingleReader = true,
+ SingleWriter = false
+ });
+
+ _processingTask = Task.Run(() => ProcessLogQueueAsync(_cts.Token));
}
public IDisposable BeginScope(TState state) => null;
@@ -62,7 +74,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
$"*[{DateTime.UtcNow:u}]*",
$"*[{logLevel}]*",
$"*[{_categoryName}]*",
- $"Message: {message}",
+ $"Message: {message}"
};
foreach (var kvp in _context.GetAll())
@@ -76,7 +88,9 @@ namespace EonaCat.Logger.EonaCatCoreLogger
}
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)
{
@@ -84,21 +98,40 @@ namespace EonaCat.Logger.EonaCatCoreLogger
}
}
- private async Task ProcessLogQueueAsync()
+ private async Task ProcessLogQueueAsync(CancellationToken token)
{
- await foreach (var message in _logChannel.Reader.ReadAllAsync())
+ try
{
- try
+ await foreach (var message in _logChannel.Reader.ReadAllAsync(token))
{
- var payload = new { text = message };
- var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json");
- await _httpClient.PostAsync(_options.WebhookUrl, content);
- }
- catch (Exception ex)
- {
- OnException?.Invoke(this, ex);
+ try
+ {
+ var payload = new { text = message };
+ var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json");
+ await _httpClient.PostAsync(_options.WebhookUrl, content, token);
+ }
+ catch (Exception ex)
+ {
+ OnException?.Invoke(this, ex);
+ }
}
}
+ catch (OperationCanceledException) { }
+ }
+
+ public void Dispose()
+ {
+ _cts.Cancel();
+ _logChannel.Writer.Complete();
+
+ try
+ {
+ _processingTask.Wait();
+ }
+ catch { /* ignore */ }
+
+ _cts.Dispose();
+ _httpClient.Dispose();
}
}
}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/TcpLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/TcpLogger.cs
index e83f4bd..5496c85 100644
--- a/EonaCat.Logger/EonaCatCoreLogger/TcpLogger.cs
+++ b/EonaCat.Logger/EonaCatCoreLogger/TcpLogger.cs
@@ -4,19 +4,20 @@ using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using System.Text;
+using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
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 TcpLogger : ILogger
+ public sealed class TcpLogger : ILogger, IDisposable
{
private readonly string _categoryName;
private readonly TcpLoggerOptions _options;
private readonly LoggerScopedContext _context = new();
- private readonly Channel _logChannel = Channel.CreateUnbounded();
+ private readonly Channel _logChannel;
+ private readonly CancellationTokenSource _cts = new();
+ private readonly Task _processingTask;
public bool IncludeCorrelationId { get; set; }
public event EventHandler OnException;
@@ -27,21 +28,27 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_options = options;
IncludeCorrelationId = options.IncludeCorrelationId;
- // Start background log writer
- Task.Run(ProcessLogQueueAsync);
+ // Bounded channel to prevent unbounded memory growth
+ _logChannel = Channel.CreateBounded(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 ClearContext() => _context.Clear();
public string GetContext(string key) => _context.Get(key);
-
- public IDisposable BeginScope(TState state) => null;
+ public IDisposable BeginScope(TState state) => _context.BeginScope(state);
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log(LogLevel logLevel, EventId eventId, TState state,
Exception exception, Func formatter)
{
- if (!IsEnabled(logLevel))
+ if (!IsEnabled(logLevel) || formatter == null)
{
return;
}
@@ -61,7 +68,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
$"[{DateTime.UtcNow:u}]",
$"[{logLevel}]",
$"[{_categoryName}]",
- $"Message: {message}",
+ $"Message: {message}"
};
var contextData = _context.GetAll();
@@ -81,6 +88,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
string fullLog = string.Join(" | ", logParts);
+ // Non-blocking, drop oldest if full
_logChannel.Writer.TryWrite(fullLog);
}
catch (Exception e)
@@ -89,28 +97,48 @@ namespace EonaCat.Logger.EonaCatCoreLogger
}
}
- private async Task ProcessLogQueueAsync()
+ private async Task ProcessLogQueueAsync(CancellationToken token)
{
- await foreach (var log in _logChannel.Reader.ReadAllAsync())
+ try
{
- try
- {
- using var client = new TcpClient();
- await client.ConnectAsync(_options.Host, _options.Port);
+ // Keep one TCP connection alive if possible
+ using var client = new TcpClient();
+ await client.ConnectAsync(_options.Host, _options.Port);
- using var stream = client.GetStream();
- using var writer = new StreamWriter(stream, Encoding.UTF8)
+ using var stream = client.GetStream();
+ using var writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true };
+
+ await foreach (var log in _logChannel.Reader.ReadAllAsync(token))
+ {
+ try
{
- AutoFlush = true
- };
-
- await writer.WriteLineAsync(log);
- }
- catch (Exception e)
- {
- OnException?.Invoke(this, e);
+ await writer.WriteLineAsync(log);
+ }
+ catch (Exception e)
+ {
+ OnException?.Invoke(this, e);
+ }
}
}
+ catch (OperationCanceledException) { }
+ catch (Exception ex)
+ {
+ OnException?.Invoke(this, ex);
+ }
+ }
+
+ public void Dispose()
+ {
+ _cts.Cancel();
+ _logChannel.Writer.Complete();
+
+ try
+ {
+ _processingTask.Wait();
+ }
+ catch { /* ignore */ }
+
+ _cts.Dispose();
}
}
}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/TelegramLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/TelegramLogger.cs
index 8f81304..efdce79 100644
--- a/EonaCat.Logger/EonaCatCoreLogger/TelegramLogger.cs
+++ b/EonaCat.Logger/EonaCatCoreLogger/TelegramLogger.cs
@@ -1,21 +1,22 @@
using EonaCat.Json;
using Microsoft.Extensions.Logging;
using System;
-using System.Net.Http;
using System.Text;
+using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
+using System.Net.Http;
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 TelegramLogger : ILogger
+ public sealed class TelegramLogger : ILogger, IDisposable
{
private readonly string _categoryName;
private readonly TelegramLoggerOptions _options;
- private readonly HttpClient _httpClient = new();
- private readonly Channel _logChannel = Channel.CreateUnbounded();
+ private readonly HttpClient _httpClient;
+ private readonly Channel _logChannel;
+ private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+ private readonly Task _processingTask;
public event EventHandler OnException;
@@ -24,14 +25,24 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_categoryName = categoryName;
_options = options;
- // Start background task
- Task.Run(ProcessLogQueueAsync);
+ _httpClient = new HttpClient();
+
+ // Bounded channel to prevent memory leaks
+ _logChannel = Channel.CreateBounded(new BoundedChannelOptions(1000)
+ {
+ FullMode = BoundedChannelFullMode.DropOldest,
+ SingleReader = true,
+ SingleWriter = false
+ });
+
+ _processingTask = Task.Run(() => ProcessLogQueueAsync(_cts.Token));
}
public IDisposable BeginScope(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
- public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ public void Log(LogLevel logLevel, EventId eventId, TState state,
+ Exception exception, Func formatter)
{
if (!IsEnabled(logLevel) || formatter == null)
{
@@ -46,6 +57,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
message += $"\nException: {exception}";
}
+ // Non-blocking, drop oldest if full
_logChannel.Writer.TryWrite(message);
}
catch (Exception ex)
@@ -54,27 +66,42 @@ namespace EonaCat.Logger.EonaCatCoreLogger
}
}
- private async Task ProcessLogQueueAsync()
+ private async Task ProcessLogQueueAsync(CancellationToken token)
{
- await foreach (var message in _logChannel.Reader.ReadAllAsync())
+ try
{
- try
+ await foreach (var message in _logChannel.Reader.ReadAllAsync(token))
{
- var url = $"https://api.telegram.org/bot{_options.BotToken}/sendMessage";
- var payload = new
+ try
{
- chat_id = _options.ChatId,
- text = message
- };
+ var url = $"https://api.telegram.org/bot{_options.BotToken}/sendMessage";
+ var payload = new { 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);
- }
- catch (Exception ex)
- {
- OnException?.Invoke(this, ex);
+ await _httpClient.PostAsync(url, content, token);
+ }
+ catch (Exception ex)
+ {
+ OnException?.Invoke(this, ex);
+ }
}
}
+ catch (OperationCanceledException) { }
+ }
+
+ public void Dispose()
+ {
+ _cts.Cancel();
+ _logChannel.Writer.Complete();
+
+ try
+ {
+ _processingTask.Wait();
+ }
+ catch { /* ignore */ }
+
+ _cts.Dispose();
+ _httpClient.Dispose();
}
}
}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/UdpLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/UdpLogger.cs
index f161ab5..6a4bf23 100644
--- a/EonaCat.Logger/EonaCatCoreLogger/UdpLogger.cs
+++ b/EonaCat.Logger/EonaCatCoreLogger/UdpLogger.cs
@@ -3,20 +3,24 @@ using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
+using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
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 UdpLogger : ILogger
+ public sealed class UdpLogger : ILogger, IDisposable
{
+ // 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.
+
private readonly string _categoryName;
private readonly UdpLoggerOptions _options;
private readonly LoggerScopedContext _context = new();
- private readonly Channel _logChannel = Channel.CreateUnbounded();
- private readonly UdpClient _udpClient = new();
+ private readonly Channel _logChannel;
+ private readonly UdpClient _udpClient;
+ private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+ private readonly Task _processingTask;
public bool IncludeCorrelationId { get; set; }
public event EventHandler OnException;
@@ -27,15 +31,23 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_options = options;
IncludeCorrelationId = options.IncludeCorrelationId;
- Task.Run(ProcessLogQueueAsync);
+ _udpClient = new UdpClient();
+
+ // Bounded channel to avoid unbounded memory growth
+ _logChannel = Channel.CreateBounded(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 ClearContext() => _context.Clear();
public string GetContext(string key) => _context.Get(key);
-
- public IDisposable BeginScope(TState state) => null;
-
+ public IDisposable BeginScope(TState state) => _context.BeginScope(state);
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log(LogLevel logLevel, EventId eventId, TState state,
@@ -61,7 +73,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
$"[{DateTime.UtcNow:u}]",
$"[{logLevel}]",
$"[{_categoryName}]",
- $"Message: {message}",
+ $"Message: {message}"
};
var contextData = _context.GetAll();
@@ -80,6 +92,8 @@ namespace EonaCat.Logger.EonaCatCoreLogger
}
string fullMessage = string.Join(" | ", logParts);
+
+ // Non-blocking, drop oldest if full
_logChannel.Writer.TryWrite(fullMessage);
}
catch (Exception ex)
@@ -88,20 +102,43 @@ namespace EonaCat.Logger.EonaCatCoreLogger
}
}
- private async Task ProcessLogQueueAsync()
+ private async Task ProcessLogQueueAsync(CancellationToken token)
{
- await foreach (var message in _logChannel.Reader.ReadAllAsync())
+ try
{
- try
+ await foreach (var message in _logChannel.Reader.ReadAllAsync(token))
{
- byte[] bytes = Encoding.UTF8.GetBytes(message);
- await _udpClient.SendAsync(bytes, bytes.Length, _options.Host, _options.Port);
- }
- catch (Exception ex)
- {
- OnException?.Invoke(this, ex);
+ try
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes(message);
+ await _udpClient.SendAsync(bytes, bytes.Length, _options.Host, _options.Port);
+ }
+ catch (Exception ex)
+ {
+ OnException?.Invoke(this, ex);
+ }
}
}
+ 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();
}
}
}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/XmlFileLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/XmlFileLogger.cs
index 7729630..00fb1ea 100644
--- a/EonaCat.Logger/EonaCatCoreLogger/XmlFileLogger.cs
+++ b/EonaCat.Logger/EonaCatCoreLogger/XmlFileLogger.cs
@@ -6,23 +6,24 @@ using System.Xml.Linq;
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 XmlFileLogger : ILogger
+ public sealed class XmlFileLogger : ILogger, IDisposable
{
+ // 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.
+
private readonly string _categoryName;
private readonly XmlFileLoggerOptions _options;
private readonly string _filePath;
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 event EventHandler OnException;
public XmlFileLogger(string categoryName, XmlFileLoggerOptions options)
{
- _categoryName = categoryName;
- _options = options;
+ _categoryName = categoryName ?? throw new ArgumentNullException(nameof(categoryName));
+ _options = options ?? throw new ArgumentNullException(nameof(options));
_filePath = Path.Combine(_options.LogDirectory, _options.FileName);
IncludeCorrelationId = options.IncludeCorrelationId;
@@ -30,18 +31,16 @@ namespace EonaCat.Logger.EonaCatCoreLogger
{
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 ClearContext() => _context.Clear();
public string GetContext(string key) => _context.Get(key);
-
- public IDisposable BeginScope(TState state) => null;
-
+ public IDisposable BeginScope(TState state) => _context.BeginScope(state);
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log(LogLevel logLevel, EventId eventId, TState state,
@@ -93,10 +92,15 @@ namespace EonaCat.Logger.EonaCatCoreLogger
_fileLock.Release();
}
}
- catch (Exception e)
+ catch (Exception ex)
{
- OnException?.Invoke(this, e);
+ OnException?.Invoke(this, ex);
}
}
+
+ public void Dispose()
+ {
+ _fileLock.Dispose();
+ }
}
}
diff --git a/EonaCat.Logger/Managers/LogManager.cs b/EonaCat.Logger/Managers/LogManager.cs
index 4582e2b..ea43d2b 100644
--- a/EonaCat.Logger/Managers/LogManager.cs
+++ b/EonaCat.Logger/Managers/LogManager.cs
@@ -17,6 +17,9 @@ using EonaCat.Logger.Servers.Udp;
using Microsoft.Extensions.DependencyInjection;
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
{
public class LogManager : ILogManager, IDisposable
@@ -59,18 +62,20 @@ namespace EonaCat.Logger.Managers
public event EventHandler OnException;
public event EventHandler OnLogLevelDisabled;
+
public async Task WriteAsync(object currentObject, ELogType logType = ELogType.INFO, bool? writeToConsole = null,
string customSplunkSourceType = null, string grayLogFacility = null,
string grayLogSource = null, string grayLogVersion = "1.1", bool disableSplunkSSL = false,
DumpFormat dumpFormat = DumpFormat.Json, bool isDetailedDump = false, int? dumpDepth = null, int? maxCollectionItems = null)
{
- if (currentObject == null)
+ if (_isDisposing || currentObject == null)
{
return;
}
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,
@@ -78,39 +83,41 @@ namespace EonaCat.Logger.Managers
string customSplunkSourceType = null, string grayLogFacility = null,
string grayLogSource = null, string grayLogVersion = "1.1", bool disableSplunkSSL = false)
{
- if (exception == null)
+ if (_isDisposing || exception == null)
{
return;
}
await WriteAsync(exception.FormatExceptionToMessage(module, method),
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,
string customSplunkSourceType = null, string grayLogFacility = null, string grayLogSource = null,
string grayLogVersion = "1.1", bool disableSplunkSSL = false)
{
- if (logType == ELogType.NONE)
+ if (_isDisposing || string.IsNullOrWhiteSpace(message) || logType == ELogType.NONE)
{
return;
}
await InternalWriteAsync(CurrentDateTime, message, logType, writeToConsole,
- customSplunkSourceType, grayLogFacility, grayLogSource, grayLogVersion, disableSplunkSSL);
+ customSplunkSourceType, grayLogFacility, grayLogSource, grayLogVersion, disableSplunkSSL)
+ .ConfigureAwait(false);
}
public async Task StartNewLogAsync()
{
- if (_tokenSource.IsCancellationRequested)
+ if (_isDisposing || _tokenSource.IsCancellationRequested)
{
return;
}
if (IsRunning && CurrentDateTime.Date > _logDate.Date)
{
- await StopLoggingAsync();
+ await StopLoggingAsync().ConfigureAwait(false);
}
IsRunning = true;
@@ -119,11 +126,17 @@ namespace EonaCat.Logger.Managers
_logDate = CurrentDateTime;
}
+
private async Task StopLoggingAsync()
{
+ if (_isDisposing)
+ {
+ return;
+ }
+
WriteStopMessage();
IsRunning = false;
- await Task.CompletedTask;
+ await Task.CompletedTask.ConfigureAwait(false);
}
private void WriteStopMessage()
@@ -134,7 +147,11 @@ namespace EonaCat.Logger.Managers
private void CreateLogger()
{
- // Dispose previous providers safely
+ if (_isDisposing)
+ {
+ return;
+ }
+
LoggerProvider?.Dispose();
LoggerFactory?.Dispose();
@@ -182,7 +199,7 @@ namespace EonaCat.Logger.Managers
bool? writeToConsole = null, string customSplunkSourceType = null, string grayLogFacility = null,
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;
}
@@ -244,6 +261,7 @@ namespace EonaCat.Logger.Managers
private void LogHelper_OnException(object sender, ErrorMessage e) => OnException?.Invoke(sender, e);
private void LogHelper_OnLogLevelDisabled(object sender, ErrorMessage e) => OnLogLevelDisabled?.Invoke(sender, e);
+
public void Dispose()
{
DisposeAsync(true).GetAwaiter().GetResult();
@@ -261,21 +279,17 @@ namespace EonaCat.Logger.Managers
{
_isDisposing = true;
- await StopLoggingAsync();
+ await StopLoggingAsync().ConfigureAwait(false);
- // Unsubscribe events
LogHelper.OnException -= LogHelper_OnException;
LogHelper.OnLogLevelDisabled -= LogHelper_OnLogLevelDisabled;
- // Cancel token
- _tokenSource.Cancel();
- _tokenSource.Dispose();
+ _tokenSource?.Cancel();
+ _tokenSource?.Dispose();
_tokenSource = null;
LoggerProvider?.Dispose();
LoggerFactory?.Dispose();
-
- await Task.Delay(50);
}
}
@@ -286,6 +300,7 @@ namespace EonaCat.Logger.Managers
_instance.Value.Dispose();
}
}
+
public void DeleteCurrentLogFile()
{
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)
{
settings ??= Settings;
-
if (!settings.EnableFileLogging)
{
return;
@@ -318,7 +330,8 @@ namespace EonaCat.Logger.Managers
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 typesToLog = null, bool convertToRfc5424 = false, bool convertToRfc3164 = false)
{
Settings.SysLogServers ??= new List();
diff --git a/EonaCat.Logger/Servers/GrayLog/Graylog.cs b/EonaCat.Logger/Servers/GrayLog/Graylog.cs
index 9673a92..57c4046 100644
--- a/EonaCat.Logger/Servers/GrayLog/Graylog.cs
+++ b/EonaCat.Logger/Servers/GrayLog/Graylog.cs
@@ -16,6 +16,7 @@ public class Graylog : IDisposable
private string _Hostname = "127.0.0.1";
private int _Port = 12201;
internal UdpClient Udp;
+ private bool _disposed;
public bool SupportsTcp { get; set; }
public string Nickname { get; set; }
@@ -23,22 +24,13 @@ public class Graylog : IDisposable
public Graylog() { }
- ///
- /// Graylog server
- ///
- ///
- ///
- ///
- ///
- ///
- ///
public Graylog(string hostname = "127.0.0.1", int port = 12201, string nickName = null, List typesToLog = null)
{
_Hostname = hostname ?? throw new ArgumentNullException(nameof(hostname));
_Port = port >= 0 ? port : throw new ArgumentException("Port must be zero or greater.");
TypesToLog = typesToLog;
-
Nickname = nickName ?? IpPort;
+
SetUdp();
}
@@ -73,10 +65,15 @@ public class Graylog : IDisposable
}
public List TypesToLog { get; set; }
- public bool IsConnected { get; set; }
+ public bool IsConnected { get; private set; }
internal void SetUdp()
{
+ if (_disposed)
+ {
+ return;
+ }
+
try
{
DisposeUdp();
@@ -91,31 +88,51 @@ public class Graylog : IDisposable
internal void DisposeUdp()
{
- if (Udp != null)
+ if (Udp == null)
{
- IsConnected = false;
+ return;
+ }
- try
- {
- Udp?.Close();
- Udp.Dispose();
- }
- catch
- {
- // Do nothing
- }
+ IsConnected = false;
+
+ try
+ {
+ Udp.Close();
+ Udp.Dispose();
+ }
+ catch
+ {
+ // Do nothing
+ }
+ finally
+ {
Udp = null;
}
}
public void Dispose()
{
- DisposeUdp();
+ Dispose(true);
GC.SuppressFinalize(this);
}
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ if (disposing)
+ {
+ DisposeUdp();
+ }
+ }
+
~Graylog()
{
- Dispose();
+ Dispose(false);
}
-}
\ No newline at end of file
+}
diff --git a/EonaCat.Logger/Servers/Splunk/Models/SplunkPayload.cs b/EonaCat.Logger/Servers/Splunk/Models/SplunkPayload.cs
index 4fa59d9..f4c225d 100644
--- a/EonaCat.Logger/Servers/Splunk/Models/SplunkPayload.cs
+++ b/EonaCat.Logger/Servers/Splunk/Models/SplunkPayload.cs
@@ -1,5 +1,8 @@
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
{
///
diff --git a/EonaCat.Logger/Servers/Splunk/Splunk.cs b/EonaCat.Logger/Servers/Splunk/Splunk.cs
index 37d6380..89aab9e 100644
--- a/EonaCat.Logger/Servers/Splunk/Splunk.cs
+++ b/EonaCat.Logger/Servers/Splunk/Splunk.cs
@@ -6,6 +6,9 @@ using System.Threading.Tasks;
using EonaCat.Json;
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
{
///
@@ -16,6 +19,8 @@ namespace EonaCat.Logger.Servers.Splunk
private string _splunkHecUrl = "https://127.0.0.1:8088/services/collector/event";
private HttpClient _httpClient;
private HttpClientHandler _httpClientHandler;
+ private bool _disposed;
+
public event EventHandler OnException;
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 HasHecUrl => !string.IsNullOrWhiteSpace(_splunkHecUrl);
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 typesToLog = null)
{
@@ -51,6 +56,7 @@ namespace EonaCat.Logger.Servers.Splunk
SplunkHecUrl = splunkHecUrl ?? throw new ArgumentNullException(nameof(splunkHecUrl));
Nickname = nickName ?? $"{splunkHecUrl}_{splunkHecToken}";
TypesToLog = typesToLog;
+
_httpClientHandler = handler ?? new HttpClientHandler();
CreateHttpClient();
}
@@ -90,14 +96,14 @@ namespace EonaCat.Logger.Servers.Splunk
public async Task SendAsync(SplunkPayload payload, bool disableSplunkSSL = false)
{
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(nameof(Splunk));
+ }
+
try
{
- if (payload == null)
- {
- return null;
- }
-
- if (!HasHecToken || !HasHecUrl)
+ if (payload == null || !HasHecToken || !HasHecUrl)
{
return null;
}
@@ -121,12 +127,12 @@ namespace EonaCat.Logger.Servers.Splunk
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")
};
- return await _httpClient.SendAsync(request);
+ return await _httpClient.SendAsync(request).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -155,13 +161,28 @@ namespace EonaCat.Logger.Servers.Splunk
public void Dispose()
{
- DisposeHttpClient();
+ Dispose(true);
GC.SuppressFinalize(this);
}
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ if (disposing)
+ {
+ DisposeHttpClient();
+ }
+ }
+
~Splunk()
{
- Dispose();
+ Dispose(false);
}
}
}
diff --git a/EonaCat.Logger/Servers/Syslog/Syslog.cs b/EonaCat.Logger/Servers/Syslog/Syslog.cs
index e2eadb8..d287736 100644
--- a/EonaCat.Logger/Servers/Syslog/Syslog.cs
+++ b/EonaCat.Logger/Servers/Syslog/Syslog.cs
@@ -7,13 +7,17 @@ using System.Threading.Tasks;
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
{
private const int MaxUdpPacketSize = 4096;
private string _hostname = "127.0.0.1";
private int _port = 514;
private UdpClient _udp;
+ private bool _disposed;
+
public event EventHandler OnException;
public bool IsConnected { get; private set; }
@@ -26,7 +30,8 @@ namespace EonaCat.Logger.Servers.Syslog
public Syslog() { }
- public Syslog(string hostname = "127.0.0.1", int port = 514, string nickName = null, List typesToLog = null, bool convertToRfc5424 = false, bool convertToRfc3164 = false)
+ public Syslog(string hostname = "127.0.0.1", int port = 514, string nickName = null,
+ List typesToLog = null, bool convertToRfc5424 = false, bool convertToRfc3164 = false)
{
Hostname = hostname;
Port = port;
@@ -68,6 +73,11 @@ namespace EonaCat.Logger.Servers.Syslog
internal void SetUdp()
{
+ if (_disposed)
+ {
+ return;
+ }
+
try
{
DisposeUdp();
@@ -82,24 +92,28 @@ namespace EonaCat.Logger.Servers.Syslog
internal void DisposeUdp()
{
- IsConnected = false;
-
- try
+ if (_udp != null)
{
- _udp?.Close();
- _udp?.Dispose();
+ try
+ {
+ IsConnected = false;
+ _udp.Close();
+ _udp.Dispose();
+ }
+ catch
+ {
+ // Swallow cleanup exceptions
+ }
+ finally
+ {
+ _udp = null;
+ }
}
- catch
- {
- // Swallow cleanup exceptions
- }
-
- _udp = null;
}
public async Task WriteAsync(string message)
{
- if (string.IsNullOrWhiteSpace(message))
+ if (_disposed || string.IsNullOrWhiteSpace(message))
{
return;
}
@@ -110,15 +124,17 @@ namespace EonaCat.Logger.Servers.Syslog
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)
{
- if (_udp == null)
+ if (_disposed || _udp == null)
{
return;
}
@@ -147,6 +163,11 @@ namespace EonaCat.Logger.Servers.Syslog
private async Task SendViaTcpAsync(byte[] data)
{
+ if (_disposed)
+ {
+ return;
+ }
+
try
{
using var tcpClient = new TcpClient();
@@ -158,12 +179,17 @@ namespace EonaCat.Logger.Servers.Syslog
}
catch
{
- // TCP failure fallback is silent here
+ // TCP failure is silent
}
}
private async Task SendUdpInChunksAsync(byte[] data, int chunkSize)
{
+ if (_disposed)
+ {
+ return;
+ }
+
int offset = 0;
byte[] buffer = ArrayPool.Shared.Rent(chunkSize);
@@ -189,13 +215,28 @@ namespace EonaCat.Logger.Servers.Syslog
public void Dispose()
{
- DisposeUdp();
+ Dispose(true);
GC.SuppressFinalize(this);
}
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ if (disposing)
+ {
+ DisposeUdp();
+ }
+ }
+
~Syslog()
{
- Dispose();
+ Dispose(false);
}
}
}
diff --git a/EonaCat.Logger/Servers/Tcp/Tcp.cs b/EonaCat.Logger/Servers/Tcp/Tcp.cs
index 6ac8f2b..aeac49e 100644
--- a/EonaCat.Logger/Servers/Tcp/Tcp.cs
+++ b/EonaCat.Logger/Servers/Tcp/Tcp.cs
@@ -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.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
+
public class Tcp : IDisposable
{
private string _hostname = "127.0.0.1";
private int _port = 514;
private TcpClient _tcp;
private readonly SemaphoreSlim _sendLock = new(1, 1);
+ private bool _disposed;
+
public event EventHandler OnException;
public bool IsConnected { get; private set; }
@@ -64,6 +67,11 @@ namespace EonaCat.Logger.Servers.Tcp
internal void SetTcp()
{
+ if (_disposed)
+ {
+ return;
+ }
+
try
{
DisposeTcp();
@@ -89,13 +97,15 @@ namespace EonaCat.Logger.Servers.Tcp
{
// Silent fail
}
-
- _tcp = null;
+ finally
+ {
+ _tcp = null;
+ }
}
public async Task WriteAsync(byte[] data)
{
- if (data == null || data.Length == 0)
+ if (_disposed || data == null || data.Length == 0)
{
return;
}
@@ -105,7 +115,7 @@ namespace EonaCat.Logger.Servers.Tcp
public async Task WriteAsync(string data)
{
- if (string.IsNullOrWhiteSpace(data))
+ if (_disposed || string.IsNullOrWhiteSpace(data))
{
return;
}
@@ -116,6 +126,11 @@ namespace EonaCat.Logger.Servers.Tcp
private async Task InternalWriteAsync(byte[] data)
{
+ if (_disposed)
+ {
+ return;
+ }
+
await _sendLock.WaitAsync();
try
{
@@ -129,7 +144,7 @@ namespace EonaCat.Logger.Servers.Tcp
return;
}
- var stream = _tcp.GetStream();
+ using var stream = _tcp.GetStream();
await stream.WriteAsync(data, 0, data.Length);
await stream.FlushAsync();
}
@@ -146,14 +161,29 @@ namespace EonaCat.Logger.Servers.Tcp
public void Dispose()
{
- DisposeTcp();
- _sendLock.Dispose();
+ Dispose(true);
GC.SuppressFinalize(this);
}
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ if (disposing)
+ {
+ DisposeTcp();
+ _sendLock.Dispose();
+ }
+ }
+
~Tcp()
{
- Dispose();
+ Dispose(false);
}
}
}
diff --git a/EonaCat.Logger/Servers/Udp/Udp.cs b/EonaCat.Logger/Servers/Udp/Udp.cs
index bafa52a..c82ce1d 100644
--- a/EonaCat.Logger/Servers/Udp/Udp.cs
+++ b/EonaCat.Logger/Servers/Udp/Udp.cs
@@ -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.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
+
public class Udp : IDisposable
{
private const int MaxUdpPacketSize = 4096;
@@ -17,6 +18,8 @@ namespace EonaCat.Logger.Servers.Udp
private int _port = 514;
private UdpClient _udp;
private readonly SemaphoreSlim _sendLock = new(1, 1);
+ private bool _disposed;
+
public event EventHandler OnException;
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.");
TypesToLog = typesToLog;
Nickname = nickname ?? IpPort;
+
SetUdp();
}
@@ -65,6 +69,11 @@ namespace EonaCat.Logger.Servers.Udp
internal void SetUdp()
{
+ if (_disposed)
+ {
+ return;
+ }
+
try
{
DisposeUdp();
@@ -93,13 +102,15 @@ namespace EonaCat.Logger.Servers.Udp
{
// Silently ignore
}
-
- _udp = null;
+ finally
+ {
+ _udp = null;
+ }
}
public async Task WriteAsync(byte[] data, bool dontFragment = false)
{
- if (data == null || data.Length == 0)
+ if (_disposed || data == null || data.Length == 0)
{
return;
}
@@ -118,7 +129,6 @@ namespace EonaCat.Logger.Servers.Udp
}
_udp.DontFragment = dontFragment;
-
await SendChunksAsync(data);
}
finally
@@ -129,7 +139,7 @@ namespace EonaCat.Logger.Servers.Udp
public async Task WriteAsync(string data, bool dontFragment = false)
{
- if (string.IsNullOrWhiteSpace(data))
+ if (_disposed || string.IsNullOrWhiteSpace(data))
{
return;
}
@@ -140,6 +150,11 @@ namespace EonaCat.Logger.Servers.Udp
private async Task SendChunksAsync(byte[] data)
{
+ if (_disposed || _udp == null)
+ {
+ return;
+ }
+
int offset = 0;
int length = data.Length;
byte[] buffer = ArrayPool.Shared.Rent(MaxUdpPacketSize);
@@ -167,14 +182,29 @@ namespace EonaCat.Logger.Servers.Udp
public void Dispose()
{
- DisposeUdp();
- _sendLock.Dispose();
+ Dispose(true);
GC.SuppressFinalize(this);
}
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ if (disposing)
+ {
+ DisposeUdp();
+ _sendLock.Dispose();
+ }
+ }
+
~Udp()
{
- Dispose();
+ Dispose(false);
}
}
}
diff --git a/EonaCat.Logger/Servers/Zabbix/API/ZabbixApi.cs b/EonaCat.Logger/Servers/Zabbix/API/ZabbixApi.cs
index 429114e..27d777c 100644
--- a/EonaCat.Logger/Servers/Zabbix/API/ZabbixApi.cs
+++ b/EonaCat.Logger/Servers/Zabbix/API/ZabbixApi.cs
@@ -5,20 +5,24 @@ using System.IO;
using System.Net;
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
{
///
/// Zabbix API client for authentication and communication with Zabbix server.
///
- public class ZabbixApi
+ public class ZabbixApi : IDisposable
{
private readonly string _user;
private readonly string _password;
private readonly string _zabbixUrl;
private readonly string _basicAuth;
private string _auth;
- public event EventHandler OnException;
+ private bool _disposed;
+ public event EventHandler OnException;
public bool IsLoggedIn { get; private set; }
public ZabbixApi(string user, string password, string zabbixUrl, bool useBasicAuth = false)
@@ -37,6 +41,11 @@ namespace EonaCat.Logger.Servers.Zabbix.API
///
public void Login()
{
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(nameof(ZabbixApi));
+ }
+
dynamic authParams = new ExpandoObject();
authParams.user = _user;
authParams.password = _password;
@@ -51,6 +60,11 @@ namespace EonaCat.Logger.Servers.Zabbix.API
///
public bool Logout()
{
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(nameof(ZabbixApi));
+ }
+
var response = ResponseAsObject("user.logout", Array.Empty());
return response?.Result ?? false;
}
@@ -60,6 +74,11 @@ namespace EonaCat.Logger.Servers.Zabbix.API
///
public string ResponseAsJson(string method, object parameters)
{
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(nameof(ZabbixApi));
+ }
+
var request = CreateRequest(method, parameters);
var jsonParams = JsonHelper.ToJson(request);
return SendRequest(jsonParams);
@@ -70,6 +89,11 @@ namespace EonaCat.Logger.Servers.Zabbix.API
///
public ZabbixApiResponse ResponseAsObject(string method, object parameters)
{
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(nameof(ZabbixApi));
+ }
+
var request = CreateRequest(method, parameters);
var jsonParams = JsonHelper.ToJson(request);
var responseJson = SendRequest(jsonParams);
@@ -81,9 +105,14 @@ namespace EonaCat.Logger.Servers.Zabbix.API
private string SendRequest(string jsonParams)
{
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(nameof(ZabbixApi));
+ }
+
try
{
- var request = WebRequest.Create(_zabbixUrl);
+ var request = (HttpWebRequest)WebRequest.Create(_zabbixUrl);
request.Method = "POST";
request.ContentType = "application/json-rpc";
@@ -92,13 +121,22 @@ namespace EonaCat.Logger.Servers.Zabbix.API
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);
}
- using var response = request.GetResponse();
- using var reader = new StreamReader(response.GetResponseStream());
+ // Read JSON response
+ 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();
}
catch (Exception ex)
@@ -107,5 +145,22 @@ namespace EonaCat.Logger.Servers.Zabbix.API
return null;
}
}
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ GC.SuppressFinalize(this);
+ }
+
+ ~ZabbixApi()
+ {
+ Dispose();
+ }
}
}
diff --git a/EonaCat.Logger/Servers/Zabbix/ZabbixRequest.cs b/EonaCat.Logger/Servers/Zabbix/ZabbixRequest.cs
index bbac506..0d4aa2e 100644
--- a/EonaCat.Logger/Servers/Zabbix/ZabbixRequest.cs
+++ b/EonaCat.Logger/Servers/Zabbix/ZabbixRequest.cs
@@ -1,90 +1,114 @@
-using EonaCat.Json;
-using System;
-using System.Net.Sockets;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace EonaCat.Logger.Servers.Zabbix
-{
- // 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 ZabbixRequest
- {
- private const int BUFFER_SIZE = 1024;
-
- ///
- /// The request to send to the Zabbix server
- ///
- public string Request { get; set; }
-
- ///
- /// The data to send to the Zabbix server
- ///
- public ZabbixData[] Data { get; set; }
-
- ///
- /// Zabbix request
- ///
- ///
- ///
- ///
- public ZabbixRequest(string host, string key, string value)
- {
- Request = "sender data";
- Data = [new ZabbixData(host, key, value)];
- }
-
- ///
- /// Sends the request to the Zabbix server
- ///
- ///
- ///
- ///
- ///
- ///
- public async Task SendAsync(string server, int port = 10051, int timeout = 500)
- {
+using EonaCat.Json;
+using System;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EonaCat.Logger.Servers.Zabbix
+{
+ // 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 ZabbixRequest
+ {
+ private const int BUFFER_SIZE = 1024;
+
+ ///
+ /// The request to send to the Zabbix server
+ ///
+ public string Request { get; set; }
+
+ ///
+ /// The data to send to the Zabbix server
+ ///
+ public ZabbixData[] Data { get; set; }
+
+ ///
+ /// Zabbix request
+ ///
+ public ZabbixRequest(string host, string key, string value)
+ {
+ Request = "sender data";
+ Data = new[] { new ZabbixData(host, key, value) };
+ }
+
+ ///
+ /// Sends the request to the Zabbix server
+ ///
+ public async Task SendAsync(string server, int port = 10051, int timeout = 500)
+ {
if (string.IsNullOrWhiteSpace(server))
{
return new ZabbixResponse();
- }
-
- 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];
-
- Buffer.BlockCopy(header, 0, message, 0, header.Length);
- Buffer.BlockCopy(dataLength, 0, message, header.Length, dataLength.Length);
- Buffer.BlockCopy(content, 0, message, header.Length + dataLength.Length, content.Length);
-
- stream.Write(message, 0, message.Length);
- stream.Flush();
-
- int counter = 0;
- while (!stream.DataAvailable)
- {
- if (counter < timeout / 50)
- {
- counter++;
- await Task.Delay(50).ConfigureAwait(false);
- }
- else
- {
- throw new TimeoutException();
- }
- }
-
- byte[] responseBytes = new byte[BUFFER_SIZE];
- stream.Read(responseBytes, 0, responseBytes.Length);
- string responseAsString = Encoding.UTF8.GetString(responseBytes);
- string jsonResult = responseAsString.Substring(responseAsString.IndexOf('{'));
- return JsonHelper.ToObject(jsonResult);
- }
- }
- }
-}
\ No newline at end of file
+ }
+
+ string json = JsonHelper.ToJson(new ZabbixRequest(Data[0].Host, Data[0].Key, Data[0].Value));
+
+ 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(dataLength, 0, message, header.Length, dataLength.Length);
+ Buffer.BlockCopy(content, 0, message, header.Length + dataLength.Length, content.Length);
+
+ await stream.WriteAsync(message, 0, message.Length).ConfigureAwait(false);
+ await stream.FlushAsync().ConfigureAwait(false);
+
+ // Wait for data available
+ int counter = 0;
+ while (!stream.DataAvailable)
+ {
+ if (counter < timeout / 50)
+ {
+ counter++;
+ await Task.Delay(50).ConfigureAwait(false);
+ }
+ else
+ {
+ throw new TimeoutException("No response received from Zabbix server.");
+ }
+ }
+
+ // Read response safely
+ using var memoryStream = new System.IO.MemoryStream();
+ byte[] buffer = new byte[BUFFER_SIZE];
+ 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(jsonResult);
+ }
+ }
+}
diff --git a/Testers/EonaCat.Logger.Test.Web/EonaCat.Logger.Test.Web.csproj b/Testers/EonaCat.Logger.Test.Web/EonaCat.Logger.Test.Web.csproj
index 9ba1976..b5aafed 100644
--- a/Testers/EonaCat.Logger.Test.Web/EonaCat.Logger.Test.Web.csproj
+++ b/Testers/EonaCat.Logger.Test.Web/EonaCat.Logger.Test.Web.csproj
@@ -7,7 +7,7 @@
-
+
all
diff --git a/Testers/EonaCat.Logger.Test.Web/Program.cs b/Testers/EonaCat.Logger.Test.Web/Program.cs
index 72d2c84..4f457ad 100644
--- a/Testers/EonaCat.Logger.Test.Web/Program.cs
+++ b/Testers/EonaCat.Logger.Test.Web/Program.cs
@@ -6,7 +6,6 @@
using EonaCat.Logger.EonaCatCoreLogger.Models;
using EonaCat.Logger.LogClient;
using EonaCat.Logger.Managers;
- using EonaCat.Logger.Test.Web;
using EonaCat.MemoryGuard;
using EonaCat.Versioning.Helpers;
using EonaCat.Web.RateLimiter;
@@ -24,6 +23,7 @@
PredictionInterval = TimeSpan.FromSeconds(15),
LeakDetectionThreshold = TimeSpan.FromSeconds(5),
SuspiciousObjectThreshold = TimeSpan.FromSeconds(3),
+ BackgroundReportingInterval = TimeSpan.FromMinutes(1.5),
CaptureStackTraces = true,
EnableAutoRemediation = true,
AutoSaveReports = true,