Removed some memory leaks
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
public sealed class BatchingDatabaseLogger : ILogger, IDisposable
|
||||||
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
|
||||||
public 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 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
|
||||||
});
|
};
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ProcessQueueAsync()
|
// Drop oldest when full (non-blocking)
|
||||||
{
|
while (!_queue.TryAdd(entry))
|
||||||
var batch = new List<LogEntry>();
|
|
||||||
var timeoutMs = (int)Math.Min(_options.BatchInterval.TotalMilliseconds, int.MaxValue);
|
|
||||||
|
|
||||||
while (!_cts.Token.IsCancellationRequested)
|
|
||||||
{
|
{
|
||||||
try
|
_queue.TryTake(out _); // discard oldest
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InsertBatchSafelyAsync(List<LogEntry> batch)
|
private async Task ProcessQueueAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
|
var batch = new List<LogEntry>(_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<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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -7,18 +7,23 @@ 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 DatabaseLogger : ILogger, IDisposable
|
||||||
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
|
||||||
public 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 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)
|
||||||
{
|
{
|
||||||
while (!token.IsCancellationRequested)
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromSeconds(_options.FlushIntervalSeconds), token);
|
while (!token.IsCancellationRequested)
|
||||||
await FlushBufferAsync();
|
{
|
||||||
|
await Task.Delay(
|
||||||
|
TimeSpan.FromSeconds(_options.FlushIntervalSeconds),
|
||||||
|
token);
|
||||||
|
|
||||||
|
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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,36 +8,42 @@ using System.Text;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
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
|
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
|
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)
|
||||||
{
|
{
|
||||||
while (!token.IsCancellationRequested)
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromSeconds(_options.FlushIntervalSeconds), token);
|
while (!token.IsCancellationRequested)
|
||||||
await FlushBufferAsync();
|
{
|
||||||
|
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
|
try
|
||||||
@@ -111,12 +125,14 @@ 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);
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
{
|
||||||
var error = await response.Content.ReadAsStringAsync();
|
var response = await _httpClient.PostAsync(_options.WebhookUrl, content, token);
|
||||||
OnException?.Invoke(this, new Exception($"Discord webhook failed: {response.StatusCode} {error}"));
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync();
|
||||||
|
OnException?.Invoke(this, new Exception($"Discord webhook failed: {response.StatusCode} {error}"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception 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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
using System.Linq;
|
using EonaCat.Json;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
using System;
|
||||||
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
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
|
namespace EonaCat.Logger.EonaCatCoreLogger
|
||||||
{
|
{
|
||||||
using EonaCat.Json;
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||||
using Microsoft.Extensions.Logging;
|
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
public class ElasticSearchLogger : ILogger
|
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 FlushBufferAsync();
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(_options.FlushIntervalSeconds), token);
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
public sealed class HttpLogger : ILogger, IDisposable
|
||||||
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
|
||||||
public class HttpLogger : ILogger
|
|
||||||
{
|
{
|
||||||
|
// 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 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))
|
||||||
{
|
{
|
||||||
try
|
_queue.TryTake(out _); // drop oldest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessQueueAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var payload in _queue.GetConsumingEnumerable(token))
|
||||||
{
|
{
|
||||||
var content = _options.SendAsJson
|
try
|
||||||
? 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)
|
|
||||||
{
|
{
|
||||||
var statusCode = response.StatusCode;
|
var content = _options.SendAsJson
|
||||||
var statusCodeMessage = statusCode switch
|
? new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json")
|
||||||
{
|
: new StringContent(payload["message"].ToString(), Encoding.UTF8, "text/plain");
|
||||||
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);
|
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)
|
}
|
||||||
{
|
catch (OperationCanceledException) { }
|
||||||
OnException?.Invoke(this, ex);
|
}
|
||||||
}
|
|
||||||
});
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_cts.Cancel();
|
||||||
|
_queue.CompleteAdding();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_processingTask.Wait();
|
||||||
|
}
|
||||||
|
catch { /* ignore */ }
|
||||||
|
|
||||||
|
_queue.Dispose();
|
||||||
|
_cts.Dispose();
|
||||||
|
_client.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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;
|
||||||
@@ -8,15 +9,19 @@ using System.Threading;
|
|||||||
|
|
||||||
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 JsonFileLogger : ILogger, IDisposable
|
||||||
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
|
||||||
public class JsonFileLogger : ILogger
|
|
||||||
{
|
{
|
||||||
|
// 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 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
private sealed class ScopePopper : IDisposable
|
||||||
{
|
{
|
||||||
Clear();
|
public void Dispose()
|
||||||
}
|
{
|
||||||
|
if (_scopes.Value?.Count > 0)
|
||||||
public IDisposable CreateScope()
|
{
|
||||||
{
|
_scopes.Value.Pop();
|
||||||
return this;
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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 };
|
try
|
||||||
var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json");
|
{
|
||||||
await _httpClient.PostAsync(_options.WebhookUrl, content);
|
var payload = new { text = message };
|
||||||
}
|
var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json");
|
||||||
catch (Exception ex)
|
await _httpClient.PostAsync(_options.WebhookUrl, content, token);
|
||||||
{
|
}
|
||||||
OnException?.Invoke(this, ex);
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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
|
// 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 };
|
||||||
|
|
||||||
|
await foreach (var log in _logChannel.Reader.ReadAllAsync(token))
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
AutoFlush = true
|
await writer.WriteLineAsync(log);
|
||||||
};
|
}
|
||||||
|
catch (Exception e)
|
||||||
await writer.WriteLineAsync(log);
|
{
|
||||||
}
|
OnException?.Invoke(this, e);
|
||||||
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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";
|
try
|
||||||
var payload = new
|
|
||||||
{
|
{
|
||||||
chat_id = _options.ChatId,
|
var url = $"https://api.telegram.org/bot{_options.BotToken}/sendMessage";
|
||||||
text = message
|
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, token);
|
||||||
await _httpClient.PostAsync(url, content);
|
}
|
||||||
}
|
catch (Exception ex)
|
||||||
catch (Exception ex)
|
{
|
||||||
{
|
OnException?.Invoke(this, ex);
|
||||||
OnException?.Invoke(this, ex);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_cts.Cancel();
|
||||||
|
_logChannel.Writer.Complete();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_processingTask.Wait();
|
||||||
|
}
|
||||||
|
catch { /* ignore */ }
|
||||||
|
|
||||||
|
_cts.Dispose();
|
||||||
|
_httpClient.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
public sealed class UdpLogger : ILogger, IDisposable
|
||||||
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
|
||||||
public class UdpLogger : ILogger
|
|
||||||
{
|
{
|
||||||
|
// 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 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,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);
|
try
|
||||||
await _udpClient.SendAsync(bytes, bytes.Length, _options.Host, _options.Port);
|
{
|
||||||
}
|
byte[] bytes = Encoding.UTF8.GetBytes(message);
|
||||||
catch (Exception ex)
|
await _udpClient.SendAsync(bytes, bytes.Length, _options.Host, _options.Port);
|
||||||
{
|
}
|
||||||
OnException?.Invoke(this, ex);
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,24 @@ using System.Xml.Linq;
|
|||||||
|
|
||||||
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 XmlFileLogger : ILogger, IDisposable
|
||||||
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
|
||||||
public class XmlFileLogger : ILogger
|
|
||||||
{
|
{
|
||||||
|
// 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 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
IsConnected = false;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
IsConnected = false;
|
||||||
{
|
|
||||||
Udp?.Close();
|
try
|
||||||
Udp.Dispose();
|
{
|
||||||
}
|
Udp.Close();
|
||||||
catch
|
Udp.Dispose();
|
||||||
{
|
}
|
||||||
// Do nothing
|
catch
|
||||||
}
|
{
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
throw new ObjectDisposedException(nameof(Splunk));
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (payload == null)
|
if (payload == null || !HasHecToken || !HasHecUrl)
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
{
|
{
|
||||||
_udp?.Close();
|
try
|
||||||
_udp?.Dispose();
|
{
|
||||||
|
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)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -146,14 +161,29 @@ namespace EonaCat.Logger.Servers.Tcp
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
DisposeTcp();
|
Dispose(true);
|
||||||
_sendLock.Dispose();
|
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
DisposeTcp();
|
||||||
|
_sendLock.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
~Tcp()
|
~Tcp()
|
||||||
{
|
{
|
||||||
Dispose();
|
Dispose(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -167,14 +182,29 @@ namespace EonaCat.Logger.Servers.Udp
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
DisposeUdp();
|
Dispose(true);
|
||||||
_sendLock.Dispose();
|
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
DisposeUdp();
|
||||||
|
_sendLock.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
~Udp()
|
~Udp()
|
||||||
{
|
{
|
||||||
Dispose();
|
Dispose(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,41 +42,73 @@ 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())
|
using var tcpClient = new TcpClient();
|
||||||
|
var connectTask = tcpClient.ConnectAsync(server, port);
|
||||||
|
if (await Task.WhenAny(connectTask, Task.Delay(timeout)).ConfigureAwait(false) != connectTask)
|
||||||
{
|
{
|
||||||
byte[] header = Encoding.ASCII.GetBytes("ZBXD\x01");
|
throw new TimeoutException("Connection timed out.");
|
||||||
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<ZabbixResponse>(jsonResult);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<ZabbixResponse>(jsonResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user