Updated
This commit is contained in:
@@ -6,6 +6,6 @@ public static class Constants
|
|||||||
{
|
{
|
||||||
public static class DateTimeFormats
|
public static class DateTimeFormats
|
||||||
{
|
{
|
||||||
public static string LOGGING { get; } = "yyyy-MM-dd HH:mm:ss";
|
public static string LOGGING { get; set; } = "yyyy-MM-dd HH:mm:ss.fff";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,17 +9,17 @@ 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.
|
||||||
|
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||||
|
|
||||||
public sealed class BatchingDatabaseLogger : ILogger, IDisposable
|
public sealed class BatchingDatabaseLogger : ILogger, IDisposable
|
||||||
{
|
{
|
||||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
|
||||||
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
|
||||||
|
|
||||||
private readonly string _categoryName;
|
private readonly 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 = new();
|
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
|
||||||
private readonly Task _processingTask;
|
private readonly Task _processingTask;
|
||||||
|
|
||||||
private volatile bool _disposed;
|
private volatile bool _disposed;
|
||||||
@@ -31,8 +31,8 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
public BatchingDatabaseLogger(string categoryName, BatchingDatabaseLoggerOptions options)
|
public BatchingDatabaseLogger(string categoryName, BatchingDatabaseLoggerOptions 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;
|
||||||
|
|
||||||
_queue = new BlockingCollection<LogEntry>(new ConcurrentQueue<LogEntry>(), MaxQueueSize);
|
_queue = new BlockingCollection<LogEntry>(new ConcurrentQueue<LogEntry>(), MaxQueueSize);
|
||||||
@@ -43,26 +43,19 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
public bool IsEnabled(LogLevel logLevel) => !_disposed && _options.IsEnabled;
|
public bool IsEnabled(LogLevel logLevel) => !_disposed && _options.IsEnabled;
|
||||||
|
|
||||||
public void Log<TState>(
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||||
LogLevel logLevel,
|
|
||||||
EventId eventId,
|
|
||||||
TState state,
|
|
||||||
Exception exception,
|
|
||||||
Func<TState, Exception, string> formatter)
|
|
||||||
{
|
{
|
||||||
if (_disposed || !IsEnabled(logLevel) || formatter == null)
|
if (_disposed || !IsEnabled(logLevel) || formatter == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var message = formatter(state, exception);
|
string message = formatter(state, exception);
|
||||||
|
|
||||||
var correlationId = IncludeCorrelationId
|
string correlationId = null;
|
||||||
? _context.Get("CorrelationId") ?? Guid.NewGuid().ToString()
|
if (IncludeCorrelationId)
|
||||||
: null;
|
|
||||||
|
|
||||||
if (correlationId != null)
|
|
||||||
{
|
{
|
||||||
|
correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
|
||||||
_context.Set("CorrelationId", correlationId);
|
_context.Set("CorrelationId", correlationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,10 +69,9 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
CorrelationId = correlationId
|
CorrelationId = correlationId
|
||||||
};
|
};
|
||||||
|
|
||||||
// Drop oldest when full (non-blocking)
|
|
||||||
while (!_queue.TryAdd(entry))
|
while (!_queue.TryAdd(entry))
|
||||||
{
|
{
|
||||||
_queue.TryTake(out _); // discard oldest
|
_queue.TryTake(out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,11 +87,14 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
if (batch.Count >= _options.BatchSize)
|
if (batch.Count >= _options.BatchSize)
|
||||||
{
|
{
|
||||||
await FlushBatchAsync(batch);
|
await FlushBatchAsync(batch, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnException?.Invoke(this, ex);
|
OnException?.Invoke(this, ex);
|
||||||
@@ -108,12 +103,12 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
{
|
{
|
||||||
if (batch.Count > 0)
|
if (batch.Count > 0)
|
||||||
{
|
{
|
||||||
await FlushBatchAsync(batch); // flush remaining
|
await FlushBatchAsync(batch, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task FlushBatchAsync(List<LogEntry> batch)
|
private async Task FlushBatchAsync(List<LogEntry> batch, CancellationToken token)
|
||||||
{
|
{
|
||||||
if (batch.Count == 0)
|
if (batch.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -122,7 +117,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await InsertBatchAsync(batch);
|
await InsertBatchAsync(batch, token);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -134,27 +129,54 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InsertBatchAsync(List<LogEntry> batch)
|
private async Task InsertBatchAsync(List<LogEntry> batch, CancellationToken token)
|
||||||
{
|
{
|
||||||
using var connection = _options.DbProviderFactory.CreateConnection()
|
using (var connection = _options.DbProviderFactory.CreateConnection())
|
||||||
?? throw new InvalidOperationException("Failed to create database connection.");
|
|
||||||
|
|
||||||
connection.ConnectionString = _options.ConnectionString;
|
|
||||||
await connection.OpenAsync(_cts.Token);
|
|
||||||
|
|
||||||
foreach (var entry in batch)
|
|
||||||
{
|
{
|
||||||
using var command = connection.CreateCommand();
|
if (connection == null)
|
||||||
command.CommandText = _options.InsertCommand;
|
{
|
||||||
|
throw new InvalidOperationException("Failed to create database connection.");
|
||||||
|
}
|
||||||
|
|
||||||
command.Parameters.Add(CreateParameter(command, "Timestamp", entry.Timestamp));
|
connection.ConnectionString = _options.ConnectionString;
|
||||||
command.Parameters.Add(CreateParameter(command, "LogLevel", entry.LogLevel));
|
await connection.OpenAsync(token);
|
||||||
command.Parameters.Add(CreateParameter(command, "Category", entry.Category));
|
|
||||||
command.Parameters.Add(CreateParameter(command, "Message", entry.Message));
|
|
||||||
command.Parameters.Add(CreateParameter(command, "Exception", entry.Exception));
|
|
||||||
command.Parameters.Add(CreateParameter(command, "CorrelationId", entry.CorrelationId));
|
|
||||||
|
|
||||||
await command.ExecuteNonQueryAsync(_cts.Token);
|
using (var transaction = connection.BeginTransaction())
|
||||||
|
{
|
||||||
|
using (var command = connection.CreateCommand())
|
||||||
|
{
|
||||||
|
command.Transaction = transaction;
|
||||||
|
command.CommandText = _options.InsertCommand;
|
||||||
|
|
||||||
|
var timestampParam = CreateParameter(command, "Timestamp", null);
|
||||||
|
var logLevelParam = CreateParameter(command, "LogLevel", null);
|
||||||
|
var categoryParam = CreateParameter(command, "Category", null);
|
||||||
|
var messageParam = CreateParameter(command, "Message", null);
|
||||||
|
var exceptionParam = CreateParameter(command, "Exception", null);
|
||||||
|
var correlationParam = CreateParameter(command, "CorrelationId", null);
|
||||||
|
|
||||||
|
command.Parameters.Add(timestampParam);
|
||||||
|
command.Parameters.Add(logLevelParam);
|
||||||
|
command.Parameters.Add(categoryParam);
|
||||||
|
command.Parameters.Add(messageParam);
|
||||||
|
command.Parameters.Add(exceptionParam);
|
||||||
|
command.Parameters.Add(correlationParam);
|
||||||
|
|
||||||
|
foreach (var entry in batch)
|
||||||
|
{
|
||||||
|
timestampParam.Value = entry.Timestamp;
|
||||||
|
logLevelParam.Value = entry.LogLevel ?? (object)DBNull.Value;
|
||||||
|
categoryParam.Value = entry.Category ?? (object)DBNull.Value;
|
||||||
|
messageParam.Value = entry.Message ?? (object)DBNull.Value;
|
||||||
|
exceptionParam.Value = entry.Exception ?? (object)DBNull.Value;
|
||||||
|
correlationParam.Value = entry.CorrelationId ?? (object)DBNull.Value;
|
||||||
|
|
||||||
|
await command.ExecuteNonQueryAsync(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,11 +200,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
_cts.Cancel();
|
_cts.Cancel();
|
||||||
_queue.CompleteAdding();
|
_queue.CompleteAdding();
|
||||||
|
|
||||||
try
|
try { _processingTask.Wait(_options.ShutdownTimeout); } catch { }
|
||||||
{
|
|
||||||
_processingTask.Wait(_options.ShutdownTimeout);
|
|
||||||
}
|
|
||||||
catch { /* ignore */ }
|
|
||||||
|
|
||||||
_queue.Dispose();
|
_queue.Dispose();
|
||||||
_cts.Dispose();
|
_cts.Dispose();
|
||||||
@@ -190,12 +208,12 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
private sealed class LogEntry
|
private sealed class LogEntry
|
||||||
{
|
{
|
||||||
public DateTime Timestamp { get; init; }
|
public DateTime Timestamp { get; set; }
|
||||||
public string LogLevel { get; init; }
|
public string LogLevel { get; set; }
|
||||||
public string Category { get; init; }
|
public string Category { get; set; }
|
||||||
public string Message { get; init; }
|
public string Message { get; set; }
|
||||||
public string Exception { get; init; }
|
public string Exception { get; set; }
|
||||||
public string CorrelationId { get; init; }
|
public string CorrelationId { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
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.
|
||||||
|
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||||
|
|
||||||
public sealed class DatabaseLogger : ILogger, IDisposable
|
public sealed class DatabaseLogger : ILogger, IDisposable
|
||||||
{
|
{
|
||||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
|
||||||
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
|
||||||
|
|
||||||
private readonly string _categoryName;
|
private readonly string _categoryName;
|
||||||
private readonly DatabaseLoggerOptions _options;
|
private readonly DatabaseLoggerOptions _options;
|
||||||
private readonly LoggerScopedContext _context = new();
|
private readonly LoggerScopedContext _context = new();
|
||||||
|
|
||||||
private readonly ConcurrentQueue<LogEntry> _queue = new();
|
private readonly ConcurrentQueue<LogEntry> _queue = new();
|
||||||
private readonly SemaphoreSlim _flushLock = new(1, 1);
|
private readonly SemaphoreSlim _flushLock = new(1, 1);
|
||||||
|
|
||||||
private CancellationTokenSource _cts = new();
|
private CancellationTokenSource _cts = new();
|
||||||
private Task _flushTask;
|
private Task _flushTask;
|
||||||
private int _refCount;
|
private int _refCount;
|
||||||
@@ -30,8 +30,8 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
public DatabaseLogger(string categoryName, DatabaseLoggerOptions options)
|
public DatabaseLogger(string categoryName, DatabaseLoggerOptions 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;
|
||||||
|
|
||||||
if (Interlocked.Increment(ref _refCount) == 1)
|
if (Interlocked.Increment(ref _refCount) == 1)
|
||||||
@@ -43,24 +43,13 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
public IDisposable BeginScope<TState>(TState state)
|
public IDisposable BeginScope<TState>(TState state)
|
||||||
=> _context.BeginScope(state);
|
=> _context.BeginScope(state);
|
||||||
|
|
||||||
public bool IsEnabled(LogLevel logLevel)
|
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
|
||||||
=> _options.IsEnabled;
|
|
||||||
|
|
||||||
public void SetContext(string key, string value)
|
public void SetContext(string key, string value) => _context.Set(key, value);
|
||||||
=> _context.Set(key, value);
|
public void ClearContext() => _context.Clear();
|
||||||
|
public string GetContext(string key) => _context.Get(key);
|
||||||
|
|
||||||
public void ClearContext()
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||||
=> _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)
|
||||||
{
|
{
|
||||||
@@ -69,13 +58,13 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
if (_queue.Count >= MaxQueueSize)
|
if (_queue.Count >= MaxQueueSize)
|
||||||
{
|
{
|
||||||
_queue.TryDequeue(out _); // drop oldest
|
_queue.TryDequeue(out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string correlationId = null;
|
||||||
if (IncludeCorrelationId)
|
if (IncludeCorrelationId)
|
||||||
{
|
{
|
||||||
var correlationId =
|
correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
|
||||||
_context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
|
|
||||||
_context.Set("CorrelationId", correlationId);
|
_context.Set("CorrelationId", correlationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +75,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
Category = _categoryName,
|
Category = _categoryName,
|
||||||
Message = formatter(state, exception),
|
Message = formatter(state, exception),
|
||||||
Exception = exception?.ToString(),
|
Exception = exception?.ToString(),
|
||||||
CorrelationId = _context.Get("CorrelationId")
|
CorrelationId = correlationId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,19 +85,16 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
{
|
{
|
||||||
while (!token.IsCancellationRequested)
|
while (!token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await Task.Delay(
|
await Task.Delay(TimeSpan.FromSeconds(_options.FlushIntervalSeconds), token);
|
||||||
TimeSpan.FromSeconds(_options.FlushIntervalSeconds),
|
|
||||||
token);
|
|
||||||
|
|
||||||
await FlushBufferAsync();
|
await FlushBufferAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// Expected on shutdown
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
await FlushBufferAsync(); // final drain
|
await FlushBufferAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task FlushBufferAsync()
|
private async Task FlushBufferAsync()
|
||||||
@@ -120,49 +106,75 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var batch = new List<LogEntry>();
|
||||||
while (_queue.TryDequeue(out var entry))
|
while (_queue.TryDequeue(out var entry))
|
||||||
{
|
{
|
||||||
try
|
batch.Add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batch.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var connection = _options.DbProviderFactory.CreateConnection())
|
||||||
|
{
|
||||||
|
if (connection == null)
|
||||||
{
|
{
|
||||||
using var connection =
|
throw new InvalidOperationException("Failed to create DB connection.");
|
||||||
_options.DbProviderFactory.CreateConnection();
|
}
|
||||||
|
|
||||||
if (connection == null)
|
connection.ConnectionString = _options.ConnectionString;
|
||||||
{
|
await connection.OpenAsync();
|
||||||
throw new InvalidOperationException("Failed to create DB connection.");
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.ConnectionString = _options.ConnectionString;
|
using (var transaction = connection.BeginTransaction())
|
||||||
await connection.OpenAsync();
|
using (var command = connection.CreateCommand())
|
||||||
|
{
|
||||||
using var command = connection.CreateCommand();
|
command.Transaction = transaction;
|
||||||
command.CommandText = _options.InsertCommand;
|
command.CommandText = _options.InsertCommand;
|
||||||
|
|
||||||
command.Parameters.Add(CreateParameter(command, "Timestamp", entry.Timestamp));
|
// Prepare parameters once
|
||||||
command.Parameters.Add(CreateParameter(command, "LogLevel", entry.LogLevel));
|
var timestampParam = CreateParameter(command, "Timestamp", null);
|
||||||
command.Parameters.Add(CreateParameter(command, "Category", entry.Category));
|
var logLevelParam = CreateParameter(command, "LogLevel", null);
|
||||||
command.Parameters.Add(CreateParameter(command, "Message", entry.Message));
|
var categoryParam = CreateParameter(command, "Category", null);
|
||||||
command.Parameters.Add(CreateParameter(command, "Exception", entry.Exception));
|
var messageParam = CreateParameter(command, "Message", null);
|
||||||
command.Parameters.Add(CreateParameter(command, "CorrelationId", entry.CorrelationId));
|
var exceptionParam = CreateParameter(command, "Exception", null);
|
||||||
|
var correlationParam = CreateParameter(command, "CorrelationId", null);
|
||||||
|
|
||||||
await command.ExecuteNonQueryAsync();
|
command.Parameters.Add(timestampParam);
|
||||||
}
|
command.Parameters.Add(logLevelParam);
|
||||||
catch (Exception ex)
|
command.Parameters.Add(categoryParam);
|
||||||
{
|
command.Parameters.Add(messageParam);
|
||||||
OnException?.Invoke(this, ex);
|
command.Parameters.Add(exceptionParam);
|
||||||
|
command.Parameters.Add(correlationParam);
|
||||||
|
|
||||||
|
foreach (var entry in batch)
|
||||||
|
{
|
||||||
|
timestampParam.Value = entry.Timestamp;
|
||||||
|
logLevelParam.Value = entry.LogLevel ?? (object)DBNull.Value;
|
||||||
|
categoryParam.Value = entry.Category ?? (object)DBNull.Value;
|
||||||
|
messageParam.Value = entry.Message ?? (object)DBNull.Value;
|
||||||
|
exceptionParam.Value = entry.Exception ?? (object)DBNull.Value;
|
||||||
|
correlationParam.Value = entry.CorrelationId ?? (object)DBNull.Value;
|
||||||
|
|
||||||
|
await command.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnException?.Invoke(this, ex);
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_flushLock.Release();
|
_flushLock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DbParameter CreateParameter(
|
private static DbParameter CreateParameter(DbCommand command, string name, object value)
|
||||||
DbCommand command,
|
|
||||||
string name,
|
|
||||||
object value)
|
|
||||||
{
|
{
|
||||||
var param = command.CreateParameter();
|
var param = command.CreateParameter();
|
||||||
param.ParameterName = $"@{name}";
|
param.ParameterName = $"@{name}";
|
||||||
@@ -175,13 +187,14 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
if (Interlocked.Decrement(ref _refCount) == 0)
|
if (Interlocked.Decrement(ref _refCount) == 0)
|
||||||
{
|
{
|
||||||
_cts.Cancel();
|
_cts.Cancel();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_flushTask?.Wait();
|
_flushTask?.Wait();
|
||||||
}
|
}
|
||||||
catch (AggregateException) { }
|
catch
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
_cts.Dispose();
|
_cts.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -10,6 +9,9 @@ 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.
|
||||||
|
// 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;
|
||||||
@@ -20,9 +22,11 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
private readonly SemaphoreSlim _flushLock = new SemaphoreSlim(1, 1);
|
private readonly SemaphoreSlim _flushLock = new SemaphoreSlim(1, 1);
|
||||||
private readonly ConcurrentQueue<string> _messageQueue = new ConcurrentQueue<string>();
|
private readonly ConcurrentQueue<string> _messageQueue = new ConcurrentQueue<string>();
|
||||||
|
|
||||||
private bool _flushLoopStarted = false;
|
|
||||||
private CancellationTokenSource _cts;
|
private CancellationTokenSource _cts;
|
||||||
private Task _flushTask;
|
private Task _flushTask;
|
||||||
|
private int _flushStarted;
|
||||||
|
|
||||||
|
private const int MaxQueueSize = 1000;
|
||||||
|
|
||||||
public bool IncludeCorrelationId { get; set; }
|
public bool IncludeCorrelationId { get; set; }
|
||||||
public event EventHandler<Exception> OnException;
|
public event EventHandler<Exception> OnException;
|
||||||
@@ -33,10 +37,9 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
IncludeCorrelationId = options.IncludeCorrelationId;
|
IncludeCorrelationId = options.IncludeCorrelationId;
|
||||||
|
|
||||||
// Start flush loop once
|
// Start flush loop exactly once
|
||||||
if (!_flushLoopStarted)
|
if (Interlocked.CompareExchange(ref _flushStarted, 1, 0) == 0)
|
||||||
{
|
{
|
||||||
_flushLoopStarted = true;
|
|
||||||
_cts = new CancellationTokenSource();
|
_cts = new CancellationTokenSource();
|
||||||
_flushTask = Task.Run(() => FlushLoopAsync(_cts.Token));
|
_flushTask = Task.Run(() => FlushLoopAsync(_cts.Token));
|
||||||
}
|
}
|
||||||
@@ -57,35 +60,33 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string correlationId = null;
|
||||||
if (IncludeCorrelationId)
|
if (IncludeCorrelationId)
|
||||||
{
|
{
|
||||||
var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
|
correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
|
||||||
_context.Set("CorrelationId", correlationId);
|
_context.Set("CorrelationId", correlationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var message = formatter(state, exception);
|
// Build message efficiently
|
||||||
var logParts = new List<string>
|
var sb = new StringBuilder();
|
||||||
{
|
sb.AppendLine($"**[{DateTime.UtcNow:u}]**");
|
||||||
$"**[{DateTime.UtcNow:u}]**",
|
sb.AppendLine($"**[{logLevel}]**");
|
||||||
$"**[{logLevel}]**",
|
sb.AppendLine($"**[{_categoryName}]**");
|
||||||
$"**[{_categoryName}]**",
|
sb.AppendLine($"Message: {formatter(state, exception)}");
|
||||||
$"Message: {message}"
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var kvp in _context.GetAll())
|
foreach (var kvp in _context.GetAll())
|
||||||
{
|
{
|
||||||
logParts.Add($"`{kvp.Key}`: {kvp.Value}");
|
sb.AppendLine($"`{kvp.Key}`: {kvp.Value}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exception != null)
|
if (exception != null)
|
||||||
{
|
{
|
||||||
logParts.Add($"Exception: {exception}");
|
sb.AppendLine($"Exception: {exception}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit queue size to prevent memory growth
|
if (_messageQueue.Count < MaxQueueSize)
|
||||||
if (_messageQueue.Count < 1000)
|
|
||||||
{
|
{
|
||||||
_messageQueue.Enqueue(string.Join("\n", logParts));
|
_messageQueue.Enqueue(sb.ToString());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -106,12 +107,14 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// Expected on cancel
|
// Do nothing, expected on cancellation
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnException?.Invoke(this, ex);
|
OnException?.Invoke(this, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await FlushBufferAsync(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task FlushBufferAsync(CancellationToken token)
|
private async Task FlushBufferAsync(CancellationToken token)
|
||||||
@@ -123,26 +126,32 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var batchBuilder = new StringBuilder();
|
||||||
while (_messageQueue.TryDequeue(out var message))
|
while (_messageQueue.TryDequeue(out var message))
|
||||||
{
|
{
|
||||||
try
|
batchBuilder.AppendLine(message);
|
||||||
{
|
|
||||||
var payload = new { content = message };
|
|
||||||
using var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json");
|
|
||||||
var response = await _httpClient.PostAsync(_options.WebhookUrl, content, token)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
||||||
OnException?.Invoke(this, new Exception($"Discord webhook failed: {response.StatusCode} {error}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
OnException?.Invoke(this, ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (batchBuilder.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = new { content = batchBuilder.ToString() };
|
||||||
|
using var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync(_options.WebhookUrl, content, token)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
OnException?.Invoke(this, new Exception($"Discord webhook failed: {response.StatusCode} {error}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnException?.Invoke(this, ex);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -152,25 +161,12 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_cts != null && !_cts.IsCancellationRequested)
|
if (_cts == null)
|
||||||
{
|
{
|
||||||
_cts.Cancel();
|
return;
|
||||||
try
|
|
||||||
{
|
|
||||||
_flushTask?.Wait();
|
|
||||||
}
|
|
||||||
catch (AggregateException ae)
|
|
||||||
{
|
|
||||||
ae.Handle(e => e is OperationCanceledException);
|
|
||||||
}
|
|
||||||
_cts.Dispose();
|
|
||||||
_cts = null;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void Shutdown()
|
_cts.Cancel();
|
||||||
{
|
|
||||||
_cts?.Cancel();
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_flushTask?.Wait();
|
_flushTask?.Wait();
|
||||||
@@ -179,8 +175,10 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
{
|
{
|
||||||
ae.Handle(e => e is OperationCanceledException);
|
ae.Handle(e => e is OperationCanceledException);
|
||||||
}
|
}
|
||||||
_flushLock.Dispose();
|
|
||||||
|
_cts.Dispose();
|
||||||
_httpClient.Dispose();
|
_httpClient.Dispose();
|
||||||
|
_flushLock.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.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
@@ -12,14 +13,12 @@ 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 ElasticSearchLogger : ILogger, IDisposable
|
public class ElasticSearchLogger : ILogger, IDisposable
|
||||||
{
|
{
|
||||||
private readonly string _categoryName;
|
private readonly string _categoryName;
|
||||||
private readonly ElasticSearchLoggerOptions _options;
|
private readonly ElasticSearchLoggerOptions _options;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly List<string> _buffer = new();
|
private readonly ConcurrentQueue<string> _queue = new();
|
||||||
private readonly object _lock = new();
|
|
||||||
private readonly CancellationTokenSource _cts = new();
|
private readonly CancellationTokenSource _cts = new();
|
||||||
private readonly Task _flushTask;
|
private readonly Task _flushTask;
|
||||||
|
|
||||||
@@ -30,8 +29,8 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
public ElasticSearchLogger(string categoryName, ElasticSearchLoggerOptions options)
|
public ElasticSearchLogger(string categoryName, ElasticSearchLoggerOptions 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;
|
||||||
|
|
||||||
_httpClient = new HttpClient();
|
_httpClient = new HttpClient();
|
||||||
@@ -41,7 +40,6 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
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) => _context.BeginScope(state);
|
||||||
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
|
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
|
||||||
|
|
||||||
@@ -53,9 +51,10 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string correlationId = null;
|
||||||
if (IncludeCorrelationId)
|
if (IncludeCorrelationId)
|
||||||
{
|
{
|
||||||
var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
|
correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
|
||||||
_context.Set("CorrelationId", correlationId);
|
_context.Set("CorrelationId", correlationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,16 +69,13 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
customContext = _context.GetAll()
|
customContext = _context.GetAll()
|
||||||
};
|
};
|
||||||
|
|
||||||
string json = JsonHelper.ToJson(logDoc);
|
var json = JsonHelper.ToJson(logDoc);
|
||||||
|
|
||||||
lock (_lock)
|
_queue.Enqueue(json);
|
||||||
|
|
||||||
|
while (_queue.Count > _options.MaxBufferSize && _queue.TryDequeue(out _))
|
||||||
{
|
{
|
||||||
_buffer.Add(json);
|
// Discard oldest log entries to maintain buffer size
|
||||||
// Optional: drop oldest if buffer is too large
|
|
||||||
if (_buffer.Count > _options.MaxBufferSize)
|
|
||||||
{
|
|
||||||
_buffer.RemoveAt(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,27 +86,40 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
while (!token.IsCancellationRequested)
|
while (!token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromSeconds(_options.FlushIntervalSeconds), token);
|
await Task.Delay(TimeSpan.FromSeconds(_options.FlushIntervalSeconds), token);
|
||||||
await FlushBufferAsync();
|
await FlushBufferAsync(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
await FlushBufferAsync(); // flush remaining logs on shutdown
|
await FlushBufferAsync(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task FlushBufferAsync()
|
private async Task FlushBufferAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
List<string> toSend;
|
if (_queue.IsEmpty)
|
||||||
|
|
||||||
lock (_lock)
|
|
||||||
{
|
{
|
||||||
if (_buffer.Count == 0)
|
return;
|
||||||
{
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toSend = new List<string>(_buffer);
|
var sb = new StringBuilder();
|
||||||
_buffer.Clear();
|
int batchSize = 0;
|
||||||
|
|
||||||
|
while (_queue.TryDequeue(out var log))
|
||||||
|
{
|
||||||
|
if (_options.UseBulkInsert)
|
||||||
|
{
|
||||||
|
sb.AppendLine("{\"index\":{}}");
|
||||||
|
}
|
||||||
|
sb.AppendLine(log);
|
||||||
|
batchSize++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batchSize == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string indexName = $"{_options.IndexName}-{DateTime.UtcNow:yyyy.MM.dd}";
|
string indexName = $"{_options.IndexName}-{DateTime.UtcNow:yyyy.MM.dd}";
|
||||||
@@ -121,7 +130,9 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,11 +141,12 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dynamic headers
|
||||||
var dynamicHeaders = new Dictionary<string, string>
|
var dynamicHeaders = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
{ "index", _options.IndexName },
|
{ "index", _options.IndexName },
|
||||||
{ "date", DateTime.UtcNow.ToString("yyyy-MM-dd") },
|
{ "date", DateTime.UtcNow.ToString("yyyy-MM-dd") },
|
||||||
{ "timestamp", DateTime.UtcNow.ToString("o") },
|
{ "timestamp", DateTime.UtcNow.ToString("o") }
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var header in _options.TemplateHeaders)
|
foreach (var header in _options.TemplateHeaders)
|
||||||
@@ -148,20 +160,14 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
request.Headers.TryAddWithoutValidation($"X-Context-{kv.Key}", kv.Value);
|
request.Headers.TryAddWithoutValidation($"X-Context-{kv.Key}", kv.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
request.Content = new StringContent(
|
request.Content = new StringContent(sb.ToString(), Encoding.UTF8, "application/json");
|
||||||
_options.UseBulkInsert
|
|
||||||
? string.Join("\n", toSend.Select(d => $"{{\"index\":{{}}}}\n{d}")) + "\n"
|
|
||||||
: string.Join("\n", toSend),
|
|
||||||
Encoding.UTF8,
|
|
||||||
"application/json"
|
|
||||||
);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await _httpClient.SendAsync(request);
|
var response = await _httpClient.SendAsync(request, token);
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var errorContent = await response.Content.ReadAsStringAsync();
|
var errorContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
OnInvalidStatusCode?.Invoke(this, $"ElasticSearch request failed: {response.StatusCode}, {errorContent}");
|
OnInvalidStatusCode?.Invoke(this, $"ElasticSearch request failed: {response.StatusCode}, {errorContent}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,7 +190,15 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_cts.Cancel();
|
_cts.Cancel();
|
||||||
_flushTask.Wait();
|
try
|
||||||
|
{
|
||||||
|
_flushTask.Wait();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
_cts.Dispose();
|
_cts.Dispose();
|
||||||
_httpClient.Dispose();
|
_httpClient.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,24 @@ using System;
|
|||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for registering a batching database logger with an <see cref="ILoggingBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>These extension methods enable the integration of a batching database logger into the logging
|
||||||
|
/// pipeline using dependency injection. Use these methods to configure and add the logger to your application's
|
||||||
|
/// logging system.</remarks>
|
||||||
public static class BatchingDatabaseLoggerFactoryExtensions
|
public static class BatchingDatabaseLoggerFactoryExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a batching database logger to the logging builder, allowing log messages to be written to a database in
|
||||||
|
/// batches.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Use this method to enable efficient, batched logging to a database. Batching can
|
||||||
|
/// improve performance by reducing the number of database writes. Configure batching behavior and database
|
||||||
|
/// connection settings using the provided options.</remarks>
|
||||||
|
/// <param name="builder">The logging builder to which the batching database logger will be added.</param>
|
||||||
|
/// <param name="configure">An action to configure the batching database logger options. Cannot be null.</param>
|
||||||
|
/// <returns>The same instance of <see cref="ILoggingBuilder"/> for chaining.</returns>
|
||||||
public static ILoggingBuilder AddEonaCatBatchingDatabaseLogger(this ILoggingBuilder builder, Action<BatchingDatabaseLoggerOptions> configure)
|
public static ILoggingBuilder AddEonaCatBatchingDatabaseLogger(this ILoggingBuilder builder, Action<BatchingDatabaseLoggerOptions> configure)
|
||||||
{
|
{
|
||||||
var options = new BatchingDatabaseLoggerOptions();
|
var options = new BatchingDatabaseLoggerOptions();
|
||||||
|
|||||||
@@ -4,8 +4,23 @@ using System;
|
|||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for registering the EonaCat database logger with an <see cref="ILoggingBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>These extension methods enable integration of the EonaCat database logger into the logging
|
||||||
|
/// pipeline using dependency injection. Use these methods to configure and add database logging capabilities to
|
||||||
|
/// your application's logging setup.</remarks>
|
||||||
public static class DatabaseLoggerFactoryExtensions
|
public static class DatabaseLoggerFactoryExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a database logger provider to the logging builder, allowing log messages to be written to a database.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Call this method to enable logging to a database using custom configuration. This
|
||||||
|
/// method registers a singleton <see cref="ILoggerProvider"/> that writes log entries to the configured
|
||||||
|
/// database.</remarks>
|
||||||
|
/// <param name="builder">The logging builder to which the database logger provider will be added.</param>
|
||||||
|
/// <param name="configure">An action to configure the database logger options. This parameter cannot be null.</param>
|
||||||
|
/// <returns>The same instance of <see cref="ILoggingBuilder"/> for chaining.</returns>
|
||||||
public static ILoggingBuilder AddEonaCatDatabaseLogger(this ILoggingBuilder builder, Action<DatabaseLoggerOptions> configure)
|
public static ILoggingBuilder AddEonaCatDatabaseLogger(this ILoggingBuilder builder, Action<DatabaseLoggerOptions> configure)
|
||||||
{
|
{
|
||||||
var options = new DatabaseLoggerOptions();
|
var options = new DatabaseLoggerOptions();
|
||||||
|
|||||||
@@ -4,8 +4,23 @@ using System;
|
|||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for registering a Discord logger with an <see cref="ILoggingBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>These extensions enable logging to Discord by adding a custom logger provider to the
|
||||||
|
/// application's logging pipeline. Use these methods to configure and enable Discord-based logging within your
|
||||||
|
/// application's logging setup.</remarks>
|
||||||
public static class DiscordLoggerFactoryExtensions
|
public static class DiscordLoggerFactoryExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a logger that sends log messages to Discord using the EonaCat Discord Logger provider.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Call this method to enable logging to a Discord channel via the EonaCat Discord
|
||||||
|
/// Logger. The <paramref name="configure"/> delegate allows customization of logger options such as webhook URL
|
||||||
|
/// and log level filtering.</remarks>
|
||||||
|
/// <param name="builder">The logging builder to which the Discord logger provider will be added.</param>
|
||||||
|
/// <param name="configure">A delegate to configure the options for the Discord logger. Cannot be null.</param>
|
||||||
|
/// <returns>The same instance of <see cref="ILoggingBuilder"/> for chaining.</returns>
|
||||||
public static ILoggingBuilder AddEonaCatDiscordLogger(this ILoggingBuilder builder, Action<DiscordLoggerOptions> configure)
|
public static ILoggingBuilder AddEonaCatDiscordLogger(this ILoggingBuilder builder, Action<DiscordLoggerOptions> configure)
|
||||||
{
|
{
|
||||||
var options = new DiscordLoggerOptions();
|
var options = new DiscordLoggerOptions();
|
||||||
|
|||||||
@@ -7,8 +7,20 @@ using System;
|
|||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for configuring ElasticSearch logging for an <see cref="ILoggingBuilder"/> instance.
|
||||||
|
/// </summary>
|
||||||
public static class ElasticSearchLoggerFactoryExtensions
|
public static class ElasticSearchLoggerFactoryExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an ElasticSearch-based logger to the logging builder, allowing log messages to be sent to an
|
||||||
|
/// ElasticSearch instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Call this method during application startup to enable logging to ElasticSearch. The
|
||||||
|
/// provided configuration delegate allows customization of connection settings and logging behavior.</remarks>
|
||||||
|
/// <param name="builder">The logging builder to which the ElasticSearch logger will be added.</param>
|
||||||
|
/// <param name="configure">A delegate to configure the options for the ElasticSearch logger. Cannot be null.</param>
|
||||||
|
/// <returns>The same instance of <see cref="ILoggingBuilder"/> for chaining.</returns>
|
||||||
public static ILoggingBuilder AddEonaCatElasticSearchLogger(this ILoggingBuilder builder, Action<ElasticSearchLoggerOptions> configure)
|
public static ILoggingBuilder AddEonaCatElasticSearchLogger(this ILoggingBuilder builder, Action<ElasticSearchLoggerOptions> configure)
|
||||||
{
|
{
|
||||||
var options = new ElasticSearchLoggerOptions();
|
var options = new ElasticSearchLoggerOptions();
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ namespace EonaCat.Logger.EonaCatCoreLogger.Extensions;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class FileLoggerFactoryExtensions
|
public static class FileLoggerFactoryExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a file-based logger provider to the logging builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This method registers a singleton <see cref="ILoggerProvider"/> that writes log messages to
|
||||||
|
/// files. Call this method during logging configuration to enable file logging in the application.</remarks>
|
||||||
|
/// <param name="builder">The logging builder to which the file logger provider will be added.</param>
|
||||||
|
/// <returns>The same instance of <see cref="ILoggingBuilder"/> for chaining.</returns>
|
||||||
private static ILoggingBuilder AddEonaCatFileLogger(this ILoggingBuilder builder)
|
private static ILoggingBuilder AddEonaCatFileLogger(this ILoggingBuilder builder)
|
||||||
{
|
{
|
||||||
builder.Services.AddSingleton<ILoggerProvider, FileLoggerProvider>();
|
builder.Services.AddSingleton<ILoggerProvider, FileLoggerProvider>();
|
||||||
|
|||||||
@@ -7,8 +7,24 @@ using System;
|
|||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for registering the EonaCat HTTP logger with an <see cref="ILoggingBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This class contains extension methods that enable integration of the EonaCat HTTP logger into
|
||||||
|
/// the logging pipeline using dependency injection. These methods are intended to be used during application
|
||||||
|
/// startup when configuring logging services.</remarks>
|
||||||
public static class HttpLoggerFactoryExtensions
|
public static class HttpLoggerFactoryExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an HTTP logger provider to the logging builder, allowing log messages to be sent over HTTP with
|
||||||
|
/// configurable options.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Use this method to enable logging to an HTTP endpoint by configuring the <see
|
||||||
|
/// cref="HttpLoggerOptions"/>. This method should be called as part of the logging setup in your application's
|
||||||
|
/// dependency injection configuration.</remarks>
|
||||||
|
/// <param name="builder">The logging builder to which the HTTP logger provider will be added.</param>
|
||||||
|
/// <param name="configure">An action to configure the HTTP logger options. This parameter cannot be null.</param>
|
||||||
|
/// <returns>The same instance of <see cref="ILoggingBuilder"/> for chaining.</returns>
|
||||||
public static ILoggingBuilder AddEonaCatHttpLogger(this ILoggingBuilder builder, Action<HttpLoggerOptions> configure)
|
public static ILoggingBuilder AddEonaCatHttpLogger(this ILoggingBuilder builder, Action<HttpLoggerOptions> configure)
|
||||||
{
|
{
|
||||||
var options = new HttpLoggerOptions();
|
var options = new HttpLoggerOptions();
|
||||||
|
|||||||
@@ -7,8 +7,23 @@ using System;
|
|||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for registering a JSON file logger with an <see cref="ILoggingBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>These extension methods enable logging to JSON-formatted files by adding the necessary logger
|
||||||
|
/// provider to the application's dependency injection container. Use these methods to configure and enable JSON
|
||||||
|
/// file logging in ASP.NET Core or other .NET applications that use Microsoft.Extensions.Logging.</remarks>
|
||||||
public static class JsonFileLoggerFactoryExtensions
|
public static class JsonFileLoggerFactoryExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a JSON file logger to the logging builder with the specified configuration options.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This method registers a singleton <see cref="ILoggerProvider"/> that writes log
|
||||||
|
/// entries to a JSON file using the specified options. Call this method during application startup to enable
|
||||||
|
/// JSON file logging.</remarks>
|
||||||
|
/// <param name="builder">The logging builder to which the JSON file logger will be added.</param>
|
||||||
|
/// <param name="configure">An action to configure the options for the JSON file logger. Cannot be null.</param>
|
||||||
|
/// <returns>The same instance of <see cref="ILoggingBuilder"/> for chaining.</returns>
|
||||||
public static ILoggingBuilder AddEonaCatJsonFileLogger(this ILoggingBuilder builder, Action<JsonFileLoggerOptions> configure)
|
public static ILoggingBuilder AddEonaCatJsonFileLogger(this ILoggingBuilder builder, Action<JsonFileLoggerOptions> configure)
|
||||||
{
|
{
|
||||||
var options = new JsonFileLoggerOptions();
|
var options = new JsonFileLoggerOptions();
|
||||||
|
|||||||
@@ -4,8 +4,24 @@ using System;
|
|||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for registering the EonaCat Slack logger with an <see cref="ILoggingBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>These extension methods enable integration of Slack-based logging into applications using
|
||||||
|
/// Microsoft.Extensions.Logging. Use these methods to configure and add the EonaCat Slack logger provider to the
|
||||||
|
/// logging pipeline.</remarks>
|
||||||
public static class SlackLoggerFactoryExtensions
|
public static class SlackLoggerFactoryExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a Slack-based logger to the logging builder, allowing log messages to be sent to Slack using the
|
||||||
|
/// specified configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Call this method to enable logging to Slack within your application's logging
|
||||||
|
/// pipeline. The provided configuration action allows you to specify Slack webhook URLs and other logger
|
||||||
|
/// options. This method registers the Slack logger as a singleton service.</remarks>
|
||||||
|
/// <param name="builder">The logging builder to which the Slack logger will be added.</param>
|
||||||
|
/// <param name="configure">An action to configure the Slack logger options. Cannot be null.</param>
|
||||||
|
/// <returns>The same instance of <see cref="ILoggingBuilder"/> for chaining.</returns>
|
||||||
public static ILoggingBuilder AddEonaCatSlackLogger(this ILoggingBuilder builder, Action<SlackLoggerOptions> configure)
|
public static ILoggingBuilder AddEonaCatSlackLogger(this ILoggingBuilder builder, Action<SlackLoggerOptions> configure)
|
||||||
{
|
{
|
||||||
var options = new SlackLoggerOptions();
|
var options = new SlackLoggerOptions();
|
||||||
|
|||||||
@@ -7,8 +7,20 @@ using System;
|
|||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for registering the EonaCat TCP logger with an <see cref="ILoggingBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
public static class TcpLoggerFactoryExtensions
|
public static class TcpLoggerFactoryExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a TCP-based logger provider to the logging builder, allowing log messages to be sent over TCP using the
|
||||||
|
/// specified configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Use this method to enable logging to a remote endpoint over TCP. The <paramref
|
||||||
|
/// name="configure"/> action allows customization of connection settings and other logger options.</remarks>
|
||||||
|
/// <param name="builder">The logging builder to which the TCP logger provider will be added.</param>
|
||||||
|
/// <param name="configure">An action to configure the TCP logger options. Cannot be null.</param>
|
||||||
|
/// <returns>The same instance of <see cref="ILoggingBuilder"/> for chaining.</returns>
|
||||||
public static ILoggingBuilder AddEonaCatTcpLogger(this ILoggingBuilder builder, Action<TcpLoggerOptions> configure)
|
public static ILoggingBuilder AddEonaCatTcpLogger(this ILoggingBuilder builder, Action<TcpLoggerOptions> configure)
|
||||||
{
|
{
|
||||||
var options = new TcpLoggerOptions();
|
var options = new TcpLoggerOptions();
|
||||||
|
|||||||
@@ -4,8 +4,23 @@ using System;
|
|||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for registering the EonaCat Telegram logger with an <see cref="ILoggingBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>These extension methods enable integration of Telegram-based logging into applications using
|
||||||
|
/// Microsoft.Extensions.Logging. Use these methods to configure and add the EonaCat Telegram logger provider to the
|
||||||
|
/// logging pipeline.</remarks>
|
||||||
public static class TelegramLoggerFactoryExtensions
|
public static class TelegramLoggerFactoryExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a Telegram-based logger to the logging builder using the specified configuration options.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Use this method to enable logging to Telegram by configuring the required options,
|
||||||
|
/// such as bot token and chat ID. This logger allows log messages to be sent directly to a Telegram
|
||||||
|
/// chat.</remarks>
|
||||||
|
/// <param name="builder">The logging builder to which the Telegram logger will be added.</param>
|
||||||
|
/// <param name="configure">An action to configure the Telegram logger options. Cannot be null.</param>
|
||||||
|
/// <returns>The same instance of <see cref="ILoggingBuilder"/> for chaining.</returns>
|
||||||
public static ILoggingBuilder AddEonaCatTelegramLogger(this ILoggingBuilder builder, Action<TelegramLoggerOptions> configure)
|
public static ILoggingBuilder AddEonaCatTelegramLogger(this ILoggingBuilder builder, Action<TelegramLoggerOptions> configure)
|
||||||
{
|
{
|
||||||
var options = new TelegramLoggerOptions();
|
var options = new TelegramLoggerOptions();
|
||||||
|
|||||||
@@ -7,8 +7,22 @@ using System;
|
|||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for registering the EonaCat UDP logger with an <see cref="ILoggingBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>These extension methods enable integration of the EonaCat UDP logger into the logging
|
||||||
|
/// pipeline using dependency injection. Use these methods to configure and add UDP-based logging to your
|
||||||
|
/// application's logging providers.</remarks>
|
||||||
public static class UdpLoggerFactoryExtensions
|
public static class UdpLoggerFactoryExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a UDP-based logger to the logging builder using the specified configuration options.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Call this method to enable logging over UDP in your application. The provided
|
||||||
|
/// configuration action allows customization of UDP logger settings such as endpoint and formatting.</remarks>
|
||||||
|
/// <param name="builder">The logging builder to which the UDP logger will be added.</param>
|
||||||
|
/// <param name="configure">An action to configure the <see cref="UdpLoggerOptions"/> for the UDP logger. Cannot be null.</param>
|
||||||
|
/// <returns>The <see cref="ILoggingBuilder"/> instance for chaining.</returns>
|
||||||
public static ILoggingBuilder AddEonaCatUdpLogger(this ILoggingBuilder builder, Action<UdpLoggerOptions> configure)
|
public static ILoggingBuilder AddEonaCatUdpLogger(this ILoggingBuilder builder, Action<UdpLoggerOptions> configure)
|
||||||
{
|
{
|
||||||
var options = new UdpLoggerOptions();
|
var options = new UdpLoggerOptions();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using EonaCat.Logger.EonaCatCoreLogger.Internal;
|
using EonaCat.Logger.EonaCatCoreLogger.Internal;
|
||||||
@@ -11,339 +12,235 @@ using Microsoft.Extensions.Logging;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger;
|
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.
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// An <see cref="ILoggerProvider" /> that writes logs to a file
|
|
||||||
/// </summary>
|
|
||||||
[ProviderAlias("EonaCatFileLogger")]
|
[ProviderAlias("EonaCatFileLogger")]
|
||||||
public class FileLoggerProvider : BatchingLoggerProvider
|
public sealed class FileLoggerProvider : BatchingLoggerProvider
|
||||||
{
|
{
|
||||||
|
private readonly string _path;
|
||||||
private readonly string _fileNamePrefix;
|
private readonly string _fileNamePrefix;
|
||||||
private readonly int _maxFileSize;
|
private readonly int _maxFileSize;
|
||||||
private readonly int _maxRetainedFiles;
|
private readonly int _maxRetainedFiles;
|
||||||
private readonly int _maxRolloverFiles;
|
private readonly int _maxRolloverFiles;
|
||||||
private readonly int _maxTries;
|
|
||||||
private readonly string _path;
|
private readonly ConcurrentDictionary<string, StringBuilder> _buffer = new();
|
||||||
private string _logFile;
|
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||||
private ConcurrentDictionary<string,string> _buffer = new ConcurrentDictionary<string, string>();
|
private readonly SemaphoreSlim _rolloverLock = new(1, 1);
|
||||||
private readonly LoggerScopedContext _context = new();
|
private readonly LoggerScopedContext _context = new();
|
||||||
|
|
||||||
|
private string _logFile;
|
||||||
|
private long _currentFileSize;
|
||||||
|
private int _isWriting;
|
||||||
|
|
||||||
public event EventHandler<ErrorMessage> OnError;
|
public event EventHandler<ErrorMessage> OnError;
|
||||||
|
|
||||||
/// <summary>
|
public string LogFile => _logFile;
|
||||||
/// Creates an instance of the <see cref="FileLoggerProvider" />
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options">The options object controlling the logger</param>
|
|
||||||
public FileLoggerProvider(IOptions<FileLoggerOptions> options) : base(options)
|
public FileLoggerProvider(IOptions<FileLoggerOptions> options) : base(options)
|
||||||
{
|
{
|
||||||
var loggerOptions = options.Value;
|
var o = options.Value;
|
||||||
_path = loggerOptions.LogDirectory;
|
_path = o.LogDirectory;
|
||||||
_fileNamePrefix = loggerOptions.FileNamePrefix;
|
_fileNamePrefix = o.FileNamePrefix;
|
||||||
_maxFileSize = loggerOptions.FileSizeLimit;
|
_maxFileSize = o.FileSizeLimit;
|
||||||
_maxRetainedFiles = loggerOptions.RetainedFileCountLimit;
|
_maxRetainedFiles = o.RetainedFileCountLimit;
|
||||||
_maxRolloverFiles = loggerOptions.MaxRolloverFiles;
|
_maxRolloverFiles = o.MaxRolloverFiles;
|
||||||
_maxTries = loggerOptions.MaxWriteTries;
|
IncludeCorrelationId = o.IncludeCorrelationId;
|
||||||
IncludeCorrelationId = loggerOptions.IncludeCorrelationId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public bool IncludeCorrelationId { get; }
|
||||||
/// The file to which log messages should be appended.
|
|
||||||
/// </summary>
|
protected override async Task WriteMessagesAsync(
|
||||||
public string LogFile
|
IReadOnlyList<LogMessage> messages,
|
||||||
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
get => _logFile;
|
if (Interlocked.Exchange(ref _isWriting, 1) == 1)
|
||||||
set
|
|
||||||
{
|
|
||||||
_logFile = value;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(_logFile))
|
|
||||||
{
|
|
||||||
var dir = Path.GetDirectoryName(_logFile);
|
|
||||||
if (!string.IsNullOrEmpty(dir))
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(dir))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetContext(string key, string value) => _context.Set(key, value);
|
|
||||||
public void ClearContext() => _context.Clear();
|
|
||||||
public string GetContext(string key) => _context.Get(key);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override async Task WriteMessagesAsync(IEnumerable<LogMessage> messages,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (IsWriting)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IsWriting = true;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(_path);
|
Directory.CreateDirectory(_path);
|
||||||
|
|
||||||
foreach (var group in messages.GroupBy(GetGrouping))
|
foreach (var group in messages.GroupBy(GetGrouping))
|
||||||
{
|
{
|
||||||
LogFile = GetFullName(group.Key);
|
var file = GetFullName(group.Key);
|
||||||
var currentMessages = string.Join(string.Empty, group.Select(item =>
|
InitializeFile(file);
|
||||||
|
|
||||||
|
var stringBuilder = _buffer.GetOrAdd(file, _ => new StringBuilder(4096));
|
||||||
|
lock (stringBuilder)
|
||||||
{
|
{
|
||||||
if (IncludeCorrelationId)
|
foreach (var message in group)
|
||||||
{
|
{
|
||||||
var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
|
AppendMessage(stringBuilder, message);
|
||||||
_context.Set("CorrelationId", correlationId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var contextData = _context.GetAll();
|
|
||||||
var contextInfo = contextData.Count > 0
|
|
||||||
? string.Join(" ", contextData.Select(kvp => $"{kvp.Key}={kvp.Value}"))
|
|
||||||
: string.Empty;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(contextInfo))
|
|
||||||
{
|
|
||||||
// if the message ends with a new line, remove it
|
|
||||||
if (item.Message.EndsWith(Environment.NewLine))
|
|
||||||
{
|
|
||||||
item.Message = item.Message.Substring(0, item.Message.Length - Environment.NewLine.Length);
|
|
||||||
}
|
|
||||||
return $"{item.Message} [{contextInfo}]{Environment.NewLine}";
|
|
||||||
}
|
|
||||||
return $"{item.Message}";
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!_buffer.TryAdd(LogFile, currentMessages))
|
|
||||||
{
|
|
||||||
_buffer[LogFile] += currentMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check file size
|
|
||||||
FileInfo fileInfo = new FileInfo(LogFile);
|
|
||||||
if (fileInfo.Exists && fileInfo.Length >= _maxFileSize)
|
|
||||||
{
|
|
||||||
// Roll over the log file
|
|
||||||
await RollOverLogFile().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await TryWriteToFileAsync(cancellationToken))
|
|
||||||
{
|
|
||||||
// Clear buffer on success
|
|
||||||
_buffer.Clear();
|
|
||||||
}
|
|
||||||
else if (await WriteToTempFileAsync(cancellationToken))
|
|
||||||
{
|
|
||||||
// Fallback to temp file
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await FlushAsync(file, stringBuilder, token).ConfigureAwait(false);
|
||||||
DeleteOldLogFiles();
|
DeleteOldLogFiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnError?.Invoke(this, new ErrorMessage { Exception = exception, Message = "Cannot write to file"});
|
OnError?.Invoke(this, new ErrorMessage
|
||||||
|
{
|
||||||
|
Exception = ex,
|
||||||
|
Message = "Failed to write log file"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
IsWriting = false;
|
Interlocked.Exchange(ref _isWriting, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsWriting { get; set; }
|
private void AppendMessage(StringBuilder sb, LogMessage msg)
|
||||||
public bool IncludeCorrelationId { get; set; }
|
|
||||||
|
|
||||||
private async Task<bool> TryWriteToFileAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
if (!_buffer.ContainsKey(LogFile))
|
// Ensure correlation id exists (once per scope)
|
||||||
|
if (IncludeCorrelationId)
|
||||||
{
|
{
|
||||||
return true;
|
var cid = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
|
||||||
|
_context.Set("CorrelationId", cid);
|
||||||
}
|
}
|
||||||
|
|
||||||
var tries = 0;
|
// 1. Append the already-formatted message FIRST
|
||||||
var completed = false;
|
sb.Append(msg.Message);
|
||||||
|
|
||||||
while (!completed)
|
// 2. Append context AFTER the message
|
||||||
|
var ctx = _context.GetAll();
|
||||||
|
if (ctx.Count > 0)
|
||||||
{
|
{
|
||||||
try
|
sb.Append(" [");
|
||||||
|
bool first = true;
|
||||||
|
foreach (var kv in ctx)
|
||||||
{
|
{
|
||||||
using (var file = new StreamWriter(LogFile, true))
|
if (!first)
|
||||||
{
|
{
|
||||||
await file.WriteAsync(_buffer[LogFile]).ConfigureAwait(false);
|
sb.Append(' ');
|
||||||
}
|
|
||||||
completed = true;
|
|
||||||
_buffer.TryRemove(LogFile, out _);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
tries++;
|
|
||||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
|
||||||
if (tries >= _maxTries)
|
|
||||||
{
|
|
||||||
OnError?.Invoke(this, new ErrorMessage { Message = "Cannot write to log folder"});
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sb.Append(kv.Key)
|
||||||
|
.Append('=')
|
||||||
|
.Append(kv.Value);
|
||||||
|
|
||||||
|
first = false;
|
||||||
}
|
}
|
||||||
|
sb.Append(']');
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
// 3. End the line
|
||||||
|
sb.AppendLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> WriteToTempFileAsync(CancellationToken cancellationToken)
|
private async Task FlushAsync(string file, StringBuilder sb, CancellationToken token)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(LogFile) || !_buffer.ContainsKey(LogFile))
|
if (sb.Length == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tempLogFolder = Path.Combine(Path.GetTempPath(), "EonaCatLogs");
|
await _writeLock.WaitAsync(token).ConfigureAwait(false);
|
||||||
var tempLogFile = $"{Path.Combine(tempLogFolder, Path.GetFileNameWithoutExtension(LogFile))}.log";
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(tempLogFolder);
|
using var fs = new FileStream(
|
||||||
|
file,
|
||||||
|
FileMode.Append,
|
||||||
|
FileAccess.Write,
|
||||||
|
FileShare.Read,
|
||||||
|
64 * 1024,
|
||||||
|
useAsync: true);
|
||||||
|
|
||||||
// Create new temp file
|
using var writer = new StreamWriter(fs);
|
||||||
using (var file = new StreamWriter(tempLogFile, true))
|
var text = sb.ToString();
|
||||||
|
await writer.WriteAsync(text).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_currentFileSize += Encoding.UTF8.GetByteCount(text);
|
||||||
|
sb.Clear();
|
||||||
|
|
||||||
|
if (_currentFileSize >= _maxFileSize)
|
||||||
{
|
{
|
||||||
await file.WriteAsync(_buffer[LogFile]).ConfigureAwait(false);
|
await RollOverAsync(file).ConfigureAwait(false);
|
||||||
}
|
|
||||||
|
|
||||||
_buffer.TryRemove(LogFile, out _);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
OnError?.Invoke(this, new ErrorMessage { Message = "Cannot write to temp folder ", Exception = exception});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetFullName((int Year, int Month, int Day) group)
|
|
||||||
{
|
|
||||||
var hasPrefix = !string.IsNullOrWhiteSpace(_fileNamePrefix);
|
|
||||||
if (hasPrefix)
|
|
||||||
{
|
|
||||||
return Path.Combine(_path, $"{_fileNamePrefix}_{group.Year:0000}{group.Month:00}{group.Day:00}.log");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Path.Combine(_path, $"{group.Year:0000}{group.Month:00}{group.Day:00}.log");
|
|
||||||
}
|
|
||||||
|
|
||||||
private (int Year, int Month, int Day) GetGrouping(LogMessage message)
|
|
||||||
{
|
|
||||||
return (message.Timestamp.Year, message.Timestamp.Month, message.Timestamp.Day);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly object lockObj = new object();
|
|
||||||
|
|
||||||
private async Task RollOverLogFile()
|
|
||||||
{
|
|
||||||
lock (lockObj)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string directoryPath = Path.GetDirectoryName(LogFile);
|
|
||||||
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(LogFile);
|
|
||||||
string fileExtension = Path.GetExtension(LogFile);
|
|
||||||
|
|
||||||
// Rename existing files
|
|
||||||
for (int i = _maxRolloverFiles - 1; i >= 1; i--)
|
|
||||||
{
|
|
||||||
string currentFilePath = Path.Combine(directoryPath, $"{fileNameWithoutExtension}.{i}{fileExtension}");
|
|
||||||
string newFilePath = Path.Combine(directoryPath, $"{fileNameWithoutExtension}.{(i + 1)}{fileExtension}");
|
|
||||||
|
|
||||||
if (File.Exists(currentFilePath))
|
|
||||||
{
|
|
||||||
if (File.Exists(newFilePath))
|
|
||||||
{
|
|
||||||
File.Delete(newFilePath);
|
|
||||||
}
|
|
||||||
File.Move(currentFilePath, newFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i != 1)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentFilePath = Path.Combine(directoryPath, $"{fileNameWithoutExtension}{fileExtension}");
|
|
||||||
newFilePath = Path.Combine(directoryPath, $"{fileNameWithoutExtension}.{i}{fileExtension}");
|
|
||||||
File.Move(currentFilePath, newFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rename current log file
|
|
||||||
var rolloverFilePath = Path.Combine(directoryPath, $"{fileNameWithoutExtension}.1{fileExtension}");
|
|
||||||
|
|
||||||
// Write end message and start processing in background
|
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await WriteEndMessageAsync(rolloverFilePath).ConfigureAwait(false);
|
|
||||||
await ProcessAsync().ConfigureAwait(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Log or handle the exception
|
|
||||||
Console.WriteLine($"Error occurred during log file rollover: {ex.Message}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
finally
|
||||||
|
|
||||||
private async Task WriteEndMessageAsync(string logFilePath)
|
|
||||||
{
|
|
||||||
var stopMessage = LogHelper.GetStopMessage();
|
|
||||||
stopMessage = LogHelper.FormatMessageWithHeader(LoggerSettings, ELogType.INFO, stopMessage, CurrentDateTme, Category);
|
|
||||||
|
|
||||||
using (var file = new StreamWriter(logFilePath, true))
|
|
||||||
{
|
{
|
||||||
await file.WriteAsync(stopMessage).ConfigureAwait(false);
|
_writeLock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessAsync()
|
private async Task RollOverAsync(string file)
|
||||||
{
|
{
|
||||||
|
await _rolloverLock.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await WriteStartMessage().ConfigureAwait(false);
|
var dir = Path.GetDirectoryName(file);
|
||||||
|
var name = Path.GetFileNameWithoutExtension(file);
|
||||||
|
var ext = Path.GetExtension(file);
|
||||||
|
|
||||||
|
for (int i = _maxRolloverFiles - 1; i >= 1; i--)
|
||||||
|
{
|
||||||
|
var src = Path.Combine(dir, $"{name}.{i}{ext}");
|
||||||
|
var dst = Path.Combine(dir, $"{name}.{i + 1}{ext}");
|
||||||
|
|
||||||
|
if (File.Exists(dst))
|
||||||
|
{
|
||||||
|
File.Delete(dst);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(src))
|
||||||
|
{
|
||||||
|
File.Move(src, dst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Move(file, Path.Combine(dir, $"{name}.1{ext}"));
|
||||||
|
_currentFileSize = 0;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
finally
|
||||||
{
|
{
|
||||||
// Log or handle the exception
|
_rolloverLock.Release();
|
||||||
Console.WriteLine($"Error occurred during log file rollover: {ex.Message}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private void InitializeFile(string file)
|
||||||
/// Deletes old log files, keeping a number of files defined by <see cref="FileLoggerOptions.RetainedFileCountLimit" />
|
{
|
||||||
/// </summary>
|
if (_logFile == file)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logFile = file;
|
||||||
|
_currentFileSize = File.Exists(file)
|
||||||
|
? new FileInfo(file).Length
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (int Year, int Month, int Day) GetGrouping(LogMessage m) =>
|
||||||
|
(m.Timestamp.Year, m.Timestamp.Month, m.Timestamp.Day);
|
||||||
|
|
||||||
|
private string GetFullName((int Year, int Month, int Day) g) =>
|
||||||
|
string.IsNullOrWhiteSpace(_fileNamePrefix)
|
||||||
|
? Path.Combine(_path, $"{g.Year:0000}{g.Month:00}{g.Day:00}.log")
|
||||||
|
: Path.Combine(_path, $"{_fileNamePrefix}_{g.Year:0000}{g.Month:00}{g.Day:00}.log");
|
||||||
|
|
||||||
protected void DeleteOldLogFiles()
|
protected void DeleteOldLogFiles()
|
||||||
{
|
{
|
||||||
if (_maxRetainedFiles > 0)
|
if (_maxRetainedFiles <= 0)
|
||||||
{
|
{
|
||||||
var hasPrefix = !string.IsNullOrWhiteSpace(_fileNamePrefix);
|
return;
|
||||||
IEnumerable<FileInfo> files = null;
|
}
|
||||||
|
|
||||||
if (hasPrefix)
|
var files = new DirectoryInfo(_path)
|
||||||
{
|
.GetFiles($"{_fileNamePrefix}*")
|
||||||
files = new DirectoryInfo(_path).GetFiles(_fileNamePrefix + "*");
|
.Where(f => !f.FullName.Equals(_logFile, StringComparison.OrdinalIgnoreCase))
|
||||||
}
|
.OrderByDescending(f => f.CreationTimeUtc)
|
||||||
else
|
.Skip(_maxRetainedFiles);
|
||||||
{
|
|
||||||
files = new DirectoryInfo(_path).GetFiles("*");
|
|
||||||
}
|
|
||||||
|
|
||||||
files = files.OrderByDescending(file => file.Name).Skip(_maxRetainedFiles);
|
foreach (var f in files)
|
||||||
|
{
|
||||||
foreach (var item in files)
|
f.Delete();
|
||||||
{
|
|
||||||
item.Delete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
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.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
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.
|
||||||
|
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||||
public sealed class HttpLogger : ILogger, IDisposable
|
public sealed class HttpLogger : ILogger, IDisposable
|
||||||
{
|
{
|
||||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
|
||||||
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
|
||||||
|
|
||||||
private readonly string _categoryName;
|
private readonly string _categoryName;
|
||||||
private readonly HttpLoggerOptions _options;
|
private readonly HttpLoggerOptions _options;
|
||||||
private readonly LoggerScopedContext _context = new();
|
private readonly LoggerScopedContext _context = new();
|
||||||
@@ -27,14 +26,16 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
public event EventHandler<Exception> OnException;
|
public event EventHandler<Exception> OnException;
|
||||||
public event EventHandler<string> OnInvalidStatusCode;
|
public event EventHandler<string> OnInvalidStatusCode;
|
||||||
|
|
||||||
|
private const int MaxQueueSize = 10_000;
|
||||||
|
|
||||||
public HttpLogger(string categoryName, HttpLoggerOptions options)
|
public HttpLogger(string categoryName, HttpLoggerOptions 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;
|
||||||
|
|
||||||
_client = new HttpClient();
|
_client = new HttpClient();
|
||||||
_queue = new BlockingCollection<Dictionary<string, object>>(boundedCapacity: 10000);
|
_queue = new BlockingCollection<Dictionary<string, object>>(boundedCapacity: MaxQueueSize);
|
||||||
_cts = new CancellationTokenSource();
|
_cts = new CancellationTokenSource();
|
||||||
_processingTask = Task.Run(() => ProcessQueueAsync(_cts.Token));
|
_processingTask = Task.Run(() => ProcessQueueAsync(_cts.Token));
|
||||||
}
|
}
|
||||||
@@ -54,19 +55,20 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string correlationId = null;
|
||||||
if (IncludeCorrelationId)
|
if (IncludeCorrelationId)
|
||||||
{
|
{
|
||||||
var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
|
correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
|
||||||
_context.Set("CorrelationId", correlationId);
|
_context.Set("CorrelationId", correlationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload = new Dictionary<string, object>
|
var payload = new Dictionary<string, object>(6)
|
||||||
{
|
{
|
||||||
{ "timestamp", DateTime.UtcNow },
|
["timestamp"] = DateTime.UtcNow,
|
||||||
{ "level", logLevel.ToString() },
|
["level"] = logLevel.ToString(),
|
||||||
{ "category", _categoryName },
|
["category"] = _categoryName,
|
||||||
{ "message", formatter(state, exception) },
|
["message"] = formatter(state, exception),
|
||||||
{ "eventId", eventId.Id }
|
["eventId"] = eventId.Id
|
||||||
};
|
};
|
||||||
|
|
||||||
var contextData = _context.GetAll();
|
var contextData = _context.GetAll();
|
||||||
@@ -75,10 +77,10 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
payload["context"] = contextData;
|
payload["context"] = contextData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to queue, drop oldest if full
|
// Drop oldest when full
|
||||||
while (!_queue.TryAdd(payload))
|
while (!_queue.TryAdd(payload))
|
||||||
{
|
{
|
||||||
_queue.TryTake(out _); // drop oldest
|
_queue.TryTake(out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,11 +92,11 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var content = _options.SendAsJson
|
using var content = _options.SendAsJson
|
||||||
? new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json")
|
? new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json")
|
||||||
: new StringContent(payload["message"].ToString(), Encoding.UTF8, "text/plain");
|
: new StringContent(payload["message"].ToString(), Encoding.UTF8, "text/plain");
|
||||||
|
|
||||||
var response = await _client.PostAsync(_options.Endpoint, content, token);
|
using var response = await _client.PostAsync(_options.Endpoint, content, token);
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -107,18 +109,25 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Do nothing, normal shutdown
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_cts.Cancel();
|
_cts.Cancel();
|
||||||
_queue.CompleteAdding();
|
_queue.CompleteAdding();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_processingTask.Wait();
|
_processingTask.Wait();
|
||||||
}
|
}
|
||||||
catch { /* ignore */ }
|
catch
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
_queue.Dispose();
|
_queue.Dispose();
|
||||||
_cts.Dispose();
|
_cts.Dispose();
|
||||||
|
|||||||
@@ -6,31 +6,40 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger.Internal
|
namespace EonaCat.Logger.EonaCatCoreLogger.Internal
|
||||||
{
|
{
|
||||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
public sealed class BatchingLogger : ILogger
|
||||||
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
|
||||||
|
|
||||||
public class BatchingLogger : ILogger
|
|
||||||
{
|
{
|
||||||
private readonly string _category;
|
private readonly string _category;
|
||||||
private readonly BatchingLoggerProvider _provider;
|
private readonly BatchingLoggerProvider _provider;
|
||||||
private LoggerSettings _loggerSettings;
|
private readonly LoggerSettings _settings;
|
||||||
|
|
||||||
public BatchingLogger(BatchingLoggerProvider loggerProvider, string categoryName, LoggerSettings loggerSettings)
|
public BatchingLogger(
|
||||||
|
BatchingLoggerProvider provider,
|
||||||
|
string category,
|
||||||
|
LoggerSettings settings)
|
||||||
{
|
{
|
||||||
_loggerSettings = loggerSettings ?? throw new ArgumentNullException(nameof(loggerSettings));
|
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
|
||||||
_provider = loggerProvider ?? throw new ArgumentNullException(nameof(loggerProvider));
|
_category = category ?? throw new ArgumentNullException(nameof(category));
|
||||||
_category = categoryName ?? throw new ArgumentNullException(nameof(categoryName));
|
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
private DateTimeOffset CurrentDateTimeOffset => CurrentDateTime;
|
private DateTimeOffset Now
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// Avoid DateTimeOffset.Now if UseLocalTime is false
|
||||||
|
return _settings.UseLocalTime ? DateTimeOffset.Now : DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private DateTime CurrentDateTime => _loggerSettings.UseLocalTime ? DateTime.Now : DateTime.UtcNow;
|
public IDisposable BeginScope<TState>(TState state) => NullScope.Instance;
|
||||||
|
|
||||||
public IDisposable BeginScope<TState>(TState state) => null;
|
|
||||||
|
|
||||||
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
|
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
|
||||||
|
|
||||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
|
public void Log<TState>(
|
||||||
|
LogLevel logLevel,
|
||||||
|
EventId eventId,
|
||||||
|
TState state,
|
||||||
|
Exception exception,
|
||||||
Func<TState, Exception, string> formatter)
|
Func<TState, Exception, string> formatter)
|
||||||
{
|
{
|
||||||
if (!IsEnabled(logLevel))
|
if (!IsEnabled(logLevel))
|
||||||
@@ -38,35 +47,59 @@ namespace EonaCat.Logger.EonaCatCoreLogger.Internal
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var timestamp = CurrentDateTimeOffset;
|
string rawMessage;
|
||||||
Log(timestamp, logLevel, eventId, state, exception, formatter, _category);
|
if (exception != null)
|
||||||
}
|
{
|
||||||
|
rawMessage = exception.FormatExceptionToMessage();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rawMessage = formatter != null ? formatter(state, null) : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
public void Log<TState>(DateTimeOffset timestamp, LogLevel logLevel, EventId eventId, TState state,
|
if (string.IsNullOrEmpty(rawMessage))
|
||||||
Exception exception, Func<TState, Exception, string> formatter, string category)
|
|
||||||
{
|
|
||||||
if (!IsEnabled(logLevel))
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string message = exception != null
|
var timestamp = Now;
|
||||||
? exception.FormatExceptionToMessage() + Environment.NewLine
|
LogInternal(timestamp, logLevel, rawMessage, exception);
|
||||||
: formatter(state, exception);
|
}
|
||||||
|
|
||||||
message = LogHelper.FormatMessageWithHeader(_loggerSettings, logLevel.FromLogLevel(), message, timestamp.DateTime, category) + Environment.NewLine;
|
private void LogInternal(
|
||||||
|
DateTimeOffset timestamp,
|
||||||
|
LogLevel logLevel,
|
||||||
|
string message,
|
||||||
|
Exception exception)
|
||||||
|
{
|
||||||
|
string formatted = LogHelper.FormatMessageWithHeader(
|
||||||
|
_settings,
|
||||||
|
logLevel.FromLogLevel(),
|
||||||
|
message,
|
||||||
|
timestamp.DateTime,
|
||||||
|
_category);
|
||||||
|
|
||||||
var currentMessage = new EonaCatLogMessage
|
var writtenMessage = _provider.AddMessage(timestamp, formatted, _category);
|
||||||
|
var onLogEvent = _settings.OnLogEvent;
|
||||||
|
|
||||||
|
if (onLogEvent != null)
|
||||||
{
|
{
|
||||||
DateTime = timestamp.DateTime,
|
onLogEvent(new EonaCatLogMessage
|
||||||
Message = _provider.AddMessage(timestamp, message),
|
{
|
||||||
LogType = logLevel.FromLogLevel(),
|
DateTime = timestamp.DateTime,
|
||||||
Category = category,
|
Message = writtenMessage,
|
||||||
Exception = exception,
|
LogType = logLevel.FromLogLevel(),
|
||||||
Origin = string.IsNullOrWhiteSpace(_loggerSettings.LogOrigin) ? "BatchingLogger" : _loggerSettings.LogOrigin
|
Category = _category,
|
||||||
};
|
Exception = exception,
|
||||||
|
Origin = string.IsNullOrWhiteSpace(_settings.LogOrigin) ? "BatchingLogger" : _settings.LogOrigin
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_loggerSettings.OnLogEvent(currentMessage);
|
private sealed class NullScope : IDisposable
|
||||||
|
{
|
||||||
|
public static readonly NullScope Instance = new NullScope();
|
||||||
|
public void Dispose() { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,60 +2,55 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Channels;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger.Internal;
|
namespace EonaCat.Logger.EonaCatCoreLogger.Internal;
|
||||||
// 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 abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
|
public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
|
||||||
{
|
{
|
||||||
private int _batchSize;
|
private readonly int _batchSize;
|
||||||
|
private readonly BlockingCollection<LogMessage> _queue;
|
||||||
|
private readonly CancellationTokenSource _cts = new();
|
||||||
|
private readonly Task _worker;
|
||||||
|
|
||||||
private readonly Channel<LogMessage> _channel =
|
private bool _disposed;
|
||||||
Channel.CreateUnbounded<LogMessage>(new UnboundedChannelOptions
|
|
||||||
{
|
|
||||||
SingleReader = true,
|
|
||||||
SingleWriter = false
|
|
||||||
});
|
|
||||||
|
|
||||||
private CancellationTokenSource _cancellationTokenSource;
|
|
||||||
private Task _outputTask;
|
|
||||||
private bool _isDisposed;
|
|
||||||
private LoggerSettings _loggerSettings;
|
private LoggerSettings _loggerSettings;
|
||||||
|
|
||||||
protected BatchingLoggerProvider(IOptions<BatchingLoggerOptions> options)
|
protected BatchingLoggerProvider(IOptions<BatchingLoggerOptions> options)
|
||||||
{
|
{
|
||||||
var loggerOptions = options.Value ?? throw new ArgumentNullException(nameof(options));
|
var o = options.Value ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
if (loggerOptions.FlushPeriod <= TimeSpan.Zero)
|
if (o.FlushPeriod <= TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException(nameof(loggerOptions.FlushPeriod),
|
throw new ArgumentOutOfRangeException(nameof(o.FlushPeriod));
|
||||||
$"{nameof(loggerOptions.FlushPeriod)} must be longer than zero.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.Value is FileLoggerOptions fileLoggerOptions)
|
_batchSize = o.BatchSize > 0 ? o.BatchSize : 100;
|
||||||
|
_queue = new BlockingCollection<LogMessage>(new ConcurrentQueue<LogMessage>());
|
||||||
|
|
||||||
|
if (o is FileLoggerOptions file)
|
||||||
{
|
{
|
||||||
UseLocalTime = fileLoggerOptions.UseLocalTime;
|
UseLocalTime = file.UseLocalTime;
|
||||||
UseMask = fileLoggerOptions.UseMask;
|
UseMask = file.UseMask;
|
||||||
LoggerSettings = fileLoggerOptions.LoggerSettings;
|
LoggerSettings = file.LoggerSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
_batchSize = loggerOptions.BatchSize > 0 ? loggerOptions.BatchSize : 100;
|
_worker = Task.Factory.StartNew(
|
||||||
StartAsync();
|
ProcessLoop,
|
||||||
|
_cts.Token,
|
||||||
|
TaskCreationOptions.LongRunning,
|
||||||
|
TaskScheduler.Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected DateTimeOffset CurrentDateTimeOffset => UseLocalTime ? DateTimeOffset.Now : DateTimeOffset.UtcNow;
|
protected bool UseLocalTime { get; }
|
||||||
protected DateTime CurrentDateTme => UseLocalTime ? DateTime.Now : DateTime.UtcNow;
|
public bool UseMask { get; }
|
||||||
|
|
||||||
protected bool UseLocalTime { get; set; }
|
protected DateTimeOffset NowOffset =>
|
||||||
|
UseLocalTime ? DateTimeOffset.Now : DateTimeOffset.UtcNow;
|
||||||
public bool IsStarted { get; set; }
|
|
||||||
public bool UseMask { get; set; }
|
|
||||||
public string Category { get; set; } = "General";
|
|
||||||
|
|
||||||
protected LoggerSettings LoggerSettings
|
protected LoggerSettings LoggerSettings
|
||||||
{
|
{
|
||||||
@@ -63,338 +58,98 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
|
|||||||
{
|
{
|
||||||
if (_loggerSettings == null)
|
if (_loggerSettings == null)
|
||||||
{
|
{
|
||||||
_loggerSettings = new LoggerSettings();
|
_loggerSettings = new LoggerSettings
|
||||||
_loggerSettings.UseLocalTime = UseLocalTime;
|
{
|
||||||
_loggerSettings.UseMask = UseMask;
|
UseLocalTime = UseLocalTime,
|
||||||
//TODO: Add the tokens and custom tokens
|
UseMask = UseMask
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return _loggerSettings;
|
return _loggerSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
set => _loggerSettings = value;
|
set => _loggerSettings = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new logger instance for the specified category name.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="categoryName">The category name for messages produced by the logger. Cannot be null.</param>
|
|
||||||
/// <returns>An <see cref="ILogger"/> instance that logs messages for the specified category.</returns>
|
|
||||||
public ILogger CreateLogger(string categoryName)
|
public ILogger CreateLogger(string categoryName)
|
||||||
|
=> new BatchingLogger(this, categoryName, LoggerSettings);
|
||||||
|
|
||||||
|
protected abstract Task WriteMessagesAsync(
|
||||||
|
IReadOnlyList<LogMessage> messages,
|
||||||
|
CancellationToken token);
|
||||||
|
|
||||||
|
internal string AddMessage(DateTimeOffset timestamp, string message, string category)
|
||||||
{
|
{
|
||||||
Category = categoryName;
|
var log = CreateLogMessage(message, timestamp, category);
|
||||||
return new BatchingLogger(this, categoryName, LoggerSettings);
|
_queue.Add(log);
|
||||||
|
return log.Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract Task WriteMessagesAsync(IEnumerable<LogMessage> messages, CancellationToken token);
|
private LogMessage CreateLogMessage(string message, DateTimeOffset ts, string category)
|
||||||
|
|
||||||
private async Task ProcessLogQueueAsync()
|
|
||||||
{
|
{
|
||||||
var batchSize = _batchSize > 0 ? _batchSize : 100;
|
if (LoggerSettings.UseMask)
|
||||||
var batch = new List<LogMessage>(batchSize);
|
{
|
||||||
|
SensitiveDataMasker sensitiveDataMasker = new SensitiveDataMasker(LoggerSettings);
|
||||||
|
message = sensitiveDataMasker.MaskSensitiveInformation(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LogMessage
|
||||||
|
{
|
||||||
|
Message = message,
|
||||||
|
Timestamp = ts,
|
||||||
|
Category = category
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ProcessLoop()
|
||||||
|
{
|
||||||
|
var batch = new List<LogMessage>(_batchSize);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var token = _cancellationTokenSource.Token;
|
foreach (var item in _queue.GetConsumingEnumerable(_cts.Token))
|
||||||
while (await _channel.Reader.WaitToReadAsync(token).ConfigureAwait(false))
|
|
||||||
{
|
{
|
||||||
|
batch.Add(item);
|
||||||
|
|
||||||
|
if (batch.Count < _batchSize)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await WriteMessagesAsync(batch, _cts.Token).ConfigureAwait(false);
|
||||||
batch.Clear();
|
batch.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
while (batch.Count < batchSize && _channel.Reader.TryRead(out var message))
|
if (batch.Count > 0)
|
||||||
{
|
{
|
||||||
batch.Add(message);
|
await WriteMessagesAsync(batch, _cts.Token).ConfigureAwait(false);
|
||||||
}
|
|
||||||
|
|
||||||
if (batch.Count > 0)
|
|
||||||
{
|
|
||||||
await WriteMessagesAsync(batch, token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// normal shutdown
|
// normal shutdown
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
throw;
|
// last-resort logging
|
||||||
|
Console.Error.WriteLine(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task WriteStartMessage()
|
|
||||||
{
|
|
||||||
var message = LogHelper.GetStartupMessage();
|
|
||||||
var token = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
|
||||||
await WriteMessagesAsync(new List<LogMessage> { CreateLoggerMessage(message, CurrentDateTimeOffset) }, token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private LogMessage CreateLoggerMessage(string message, DateTimeOffset currentDateTimeOffset)
|
|
||||||
{
|
|
||||||
var result = new LogMessage() { Message = message, Timestamp = currentDateTimeOffset };
|
|
||||||
|
|
||||||
if (LoggerSettings != null && LoggerSettings.UseMask)
|
|
||||||
{
|
|
||||||
// Masking sensitive information
|
|
||||||
result.Message = MaskSensitiveInformation(result.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Masks sensitive information within the provided message string.
|
|
||||||
/// This method is virtual and can be overridden to customize masking behavior.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">The log message potentially containing sensitive information.</param>
|
|
||||||
/// <returns>The masked log message.</returns>
|
|
||||||
protected virtual string MaskSensitiveInformation(string message)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(message))
|
|
||||||
{
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LoggerSettings != null && LoggerSettings.UseDefaultMasking)
|
|
||||||
{
|
|
||||||
// Mask IP addresses
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b(?:\d{1,3}\.){3}\d{1,3}\b(?!\d)",
|
|
||||||
LoggerSettings.Mask);
|
|
||||||
|
|
||||||
// Mask MAC addresses
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b",
|
|
||||||
LoggerSettings.Mask);
|
|
||||||
|
|
||||||
// Mask emails
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b",
|
|
||||||
LoggerSettings.Mask,
|
|
||||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
// Mask passwords
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"(?i)(password\s*[:= ]\s*|pwd\s*[:= ]\s*)[^\s]+",
|
|
||||||
$"password={LoggerSettings.Mask}");
|
|
||||||
|
|
||||||
// Mask credit card numbers
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b(?:\d{4}[ -]?){3}\d{4}\b",
|
|
||||||
LoggerSettings.Mask);
|
|
||||||
|
|
||||||
// Mask social security numbers (SSN) and BSN (Dutch Citizen Service Number)
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b\d{3}-\d{2}-\d{4}\b|\b\d{9}\b",
|
|
||||||
LoggerSettings.Mask); // SSN (USA)
|
|
||||||
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b\d{9}\b",
|
|
||||||
LoggerSettings.Mask); // BSN (Dutch)
|
|
||||||
|
|
||||||
// Mask passwords (Dutch)
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"(?i)(wachtwoord\s*[:= ]\s*|ww\s*=\s*)[^\s]+",
|
|
||||||
$"wachtwoord={LoggerSettings.Mask}");
|
|
||||||
|
|
||||||
// Mask API keys/tokens
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b[A-Za-z0-9-_]{20,}\b",
|
|
||||||
LoggerSettings.Mask);
|
|
||||||
|
|
||||||
// Mask phone numbers (generic and Dutch specific)
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b(\+?\d{1,4}[-.\s]?)?(\(?\d{3}\)?[-.\s]?)?\d{3}[-.\s]?\d{4}\b",
|
|
||||||
LoggerSettings.Mask); // Generic
|
|
||||||
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b(\+31|0031|0|06)[-\s]?\d{8}\b",
|
|
||||||
LoggerSettings.Mask); // Dutch
|
|
||||||
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b(\+32|0032|0|06)[-\s]?\d{8}\b",
|
|
||||||
LoggerSettings.Mask); // Belgium
|
|
||||||
|
|
||||||
// Mask dates of birth (DOB) or other date formats
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b\d{2}[/-]\d{2}[/-]\d{4}\b",
|
|
||||||
LoggerSettings.Mask);
|
|
||||||
|
|
||||||
// Mask Dutch postal codes
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b\d{4}\s?[A-Z]{2}\b",
|
|
||||||
LoggerSettings.Mask);
|
|
||||||
|
|
||||||
// Mask IBAN/Bank account numbers (generic and Dutch specific)
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b[A-Z]{2}\d{2}[A-Z0-9]{1,30}\b",
|
|
||||||
LoggerSettings.Mask); // Generic for EU and other country IBANs
|
|
||||||
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\bNL\d{2}[A-Z]{4}\d{10}\b",
|
|
||||||
LoggerSettings.Mask); // Dutch IBAN
|
|
||||||
|
|
||||||
// Mask JWT Tokens
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b[A-Za-z0-9-_]{16,}\.[A-Za-z0-9-_]{16,}\.[A-Za-z0-9-_]{16,}\b",
|
|
||||||
LoggerSettings.Mask);
|
|
||||||
|
|
||||||
// Mask URLs with sensitive query strings
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\bhttps?:\/\/[^\s?]+(\?[^\s]+?[\&=](password|key|token|wachtwoord|sleutel))[^&\s]*",
|
|
||||||
LoggerSettings.Mask,
|
|
||||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
// Mask license keys
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}\b",
|
|
||||||
LoggerSettings.Mask);
|
|
||||||
|
|
||||||
// Mask public and private keys (e.g., PEM format)
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"-----BEGIN [A-Z ]+KEY-----[\s\S]+?-----END [A-Z ]+KEY-----",
|
|
||||||
LoggerSettings.Mask);
|
|
||||||
|
|
||||||
// Mask Dutch KVK number (8 or 12 digits)
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b\d{8}|\d{12}\b",
|
|
||||||
LoggerSettings.Mask);
|
|
||||||
|
|
||||||
// Mask Dutch BTW-nummer (VAT number)
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\bNL\d{9}B\d{2}\b",
|
|
||||||
LoggerSettings.Mask);
|
|
||||||
|
|
||||||
// Mask Dutch driving license number (10-12 characters)
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b[A-Z0-9]{10,12}\b",
|
|
||||||
LoggerSettings.Mask);
|
|
||||||
|
|
||||||
// Mask Dutch health insurance number (Zorgnummer)
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b\d{9}\b",
|
|
||||||
LoggerSettings.Mask);
|
|
||||||
|
|
||||||
// Mask other Dutch Bank Account numbers (9-10 digits)
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b\d{9,10}\b",
|
|
||||||
LoggerSettings.Mask);
|
|
||||||
|
|
||||||
// Mask Dutch Passport Numbers (9 alphanumeric characters)
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b[A-Z0-9]{9}\b",
|
|
||||||
LoggerSettings.Mask);
|
|
||||||
|
|
||||||
// Mask Dutch Identification Document Numbers (varying formats)
|
|
||||||
message = System.Text.RegularExpressions.Regex.Replace(message,
|
|
||||||
@"\b[A-Z]{2}\d{6,7}\b",
|
|
||||||
LoggerSettings.Mask);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mask custom keywords specified in LoggerSettings
|
|
||||||
if (LoggerSettings?.MaskedKeywords != null)
|
|
||||||
{
|
|
||||||
foreach (var keyword in LoggerSettings.MaskedKeywords)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(keyword))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
message = message.Replace(keyword, LoggerSettings.Mask);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal string AddMessage(DateTimeOffset timestamp, string message)
|
|
||||||
{
|
|
||||||
var result = CreateLoggerMessage(message, timestamp);
|
|
||||||
if (!_channel.Writer.TryWrite(result))
|
|
||||||
{
|
|
||||||
_ = _channel.Writer.WriteAsync(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Message;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartAsync()
|
|
||||||
{
|
|
||||||
if (_cancellationTokenSource != null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_cancellationTokenSource = new CancellationTokenSource();
|
|
||||||
_outputTask = Task.Run(ProcessLogQueueAsync);
|
|
||||||
IsStarted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StopAsync()
|
|
||||||
{
|
|
||||||
if (_cancellationTokenSource == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_cancellationTokenSource.Cancel();
|
|
||||||
_channel.Writer.Complete();
|
|
||||||
|
|
||||||
if (_outputTask != null)
|
|
||||||
{
|
|
||||||
await _outputTask.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
// expected on shutdown
|
|
||||||
}
|
|
||||||
catch (AggregateException exception) when (exception.InnerExceptions.Count == 1 && exception.InnerExceptions[0] is TaskCanceledException)
|
|
||||||
{
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_cancellationTokenSource.Dispose();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
_cancellationTokenSource = null;
|
|
||||||
_outputTask = null;
|
|
||||||
IsStarted = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Dispose(true);
|
if (_disposed)
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (_isDisposed)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_isDisposed = true;
|
_disposed = true;
|
||||||
|
|
||||||
if (disposing)
|
_cts.Cancel();
|
||||||
{
|
_queue.CompleteAdding();
|
||||||
try
|
|
||||||
{
|
try { _worker.Wait(); } catch { }
|
||||||
StopAsync().GetAwaiter().GetResult();
|
_cts.Dispose();
|
||||||
}
|
_queue.Dispose();
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,4 +8,5 @@ public struct LogMessage
|
|||||||
{
|
{
|
||||||
public DateTimeOffset Timestamp { get; set; }
|
public DateTimeOffset Timestamp { get; set; }
|
||||||
public string Message { get; set; }
|
public string Message { get; set; }
|
||||||
|
public string Category { get; set; }
|
||||||
}
|
}
|
||||||
100
EonaCat.Logger/EonaCatCoreLogger/Internal/SensitiveDataMasker.cs
Normal file
100
EonaCat.Logger/EonaCatCoreLogger/Internal/SensitiveDataMasker.cs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
using global::EonaCat.Logger.Managers;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace EonaCat.Logger.EonaCatCoreLogger.Internal
|
||||||
|
{
|
||||||
|
internal class SensitiveDataMasker
|
||||||
|
{
|
||||||
|
internal LoggerSettings LoggerSettings { get; }
|
||||||
|
|
||||||
|
internal SensitiveDataMasker(LoggerSettings settings)
|
||||||
|
{
|
||||||
|
LoggerSettings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Precompiled Regexes
|
||||||
|
private static readonly Regex IpRegex = new Regex(@"\b(?:\d{1,3}\.){3}\d{1,3}\b(?!\d)", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex MacRegex = new Regex(@"\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex EmailRegex = new Regex(@"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
private static readonly Regex PasswordRegex = new Regex(@"(?i)(password\s*[:= ]\s*|pwd\s*[:= ]\s*)[^\s]+", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex DutchPasswordRegex = new Regex(@"(?i)(wachtwoord\s*[:= ]\s*|ww\s*=\s*)[^\s]+", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex CreditCardRegex = new Regex(@"\b(?:\d{4}[ -]?){3}\d{4}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex SsnBsnRegex = new Regex(@"\b\d{3}-\d{2}-\d{4}\b|\b\d{9}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex ApiKeyRegex = new Regex(@"\b[A-Za-z0-9-_]{20,}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex PhoneGenericRegex = new Regex(@"\b(\+?\d{1,4}[-.\s]?)?(\(?\d{3}\)?[-.\s]?)?\d{3}[-.\s]?\d{4}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex PhoneDutchRegex = new Regex(@"\b(\+31|0031|0|06)[-\s]?\d{8}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex PhoneBelgiumRegex = new Regex(@"\b(\+32|0032|0|06)[-\s]?\d{8}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex DobRegex = new Regex(@"\b\d{2}[/-]\d{2}[/-]\d{4}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex PostalCodeDutchRegex = new Regex(@"\b\d{4}\s?[A-Z]{2}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex IbanGenericRegex = new Regex(@"\b[A-Z]{2}\d{2}[A-Z0-9]{1,30}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex IbanDutchRegex = new Regex(@"\bNL\d{2}[A-Z]{4}\d{10}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex JwtRegex = new Regex(@"\b[A-Za-z0-9-_]{16,}\.[A-Za-z0-9-_]{16,}\.[A-Za-z0-9-_]{16,}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex UrlSensitiveRegex = new Regex(@"\bhttps?:\/\/[^\s?]+(\?[^\s]+?[\&=](password|key|token|wachtwoord|sleutel))[^&\s]*", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
private static readonly Regex LicenseKeyRegex = new Regex(@"\b[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex PemKeyRegex = new Regex(@"-----BEGIN [A-Z ]+KEY-----[\s\S]+?-----END [A-Z ]+KEY-----", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex KvkRegex = new Regex(@"\b\d{8}|\d{12}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex BtwRegex = new Regex(@"\bNL\d{9}B\d{2}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex DutchDrivingLicenseRegex = new Regex(@"\b[A-Z0-9]{10,12}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex ZorgnummerRegex = new Regex(@"\b\d{9}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex DutchBankAccountRegex = new Regex(@"\b\d{9,10}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex DutchPassportRegex = new Regex(@"\b[A-Z0-9]{9}\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex DutchIdDocRegex = new Regex(@"\b[A-Z]{2}\d{6,7}\b", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
internal virtual string MaskSensitiveInformation(string message)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(message))
|
||||||
|
{
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LoggerSettings != null && LoggerSettings.UseDefaultMasking)
|
||||||
|
{
|
||||||
|
string mask = LoggerSettings.Mask ?? "***";
|
||||||
|
|
||||||
|
message = IpRegex.Replace(message, mask);
|
||||||
|
message = MacRegex.Replace(message, mask);
|
||||||
|
message = EmailRegex.Replace(message, mask);
|
||||||
|
message = PasswordRegex.Replace(message, $"password={mask}");
|
||||||
|
message = DutchPasswordRegex.Replace(message, $"wachtwoord={mask}");
|
||||||
|
message = CreditCardRegex.Replace(message, mask);
|
||||||
|
message = SsnBsnRegex.Replace(message, mask);
|
||||||
|
message = ApiKeyRegex.Replace(message, mask);
|
||||||
|
message = PhoneGenericRegex.Replace(message, mask);
|
||||||
|
message = PhoneDutchRegex.Replace(message, mask);
|
||||||
|
message = PhoneBelgiumRegex.Replace(message, mask);
|
||||||
|
message = DobRegex.Replace(message, mask);
|
||||||
|
message = PostalCodeDutchRegex.Replace(message, mask);
|
||||||
|
message = IbanGenericRegex.Replace(message, mask);
|
||||||
|
message = IbanDutchRegex.Replace(message, mask);
|
||||||
|
message = JwtRegex.Replace(message, mask);
|
||||||
|
message = UrlSensitiveRegex.Replace(message, mask);
|
||||||
|
message = LicenseKeyRegex.Replace(message, mask);
|
||||||
|
message = PemKeyRegex.Replace(message, mask);
|
||||||
|
message = KvkRegex.Replace(message, mask);
|
||||||
|
message = BtwRegex.Replace(message, mask);
|
||||||
|
message = DutchDrivingLicenseRegex.Replace(message, mask);
|
||||||
|
message = ZorgnummerRegex.Replace(message, mask);
|
||||||
|
message = DutchBankAccountRegex.Replace(message, mask);
|
||||||
|
message = DutchPassportRegex.Replace(message, mask);
|
||||||
|
message = DutchIdDocRegex.Replace(message, mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mask custom keywords
|
||||||
|
if (LoggerSettings?.MaskedKeywords != null)
|
||||||
|
{
|
||||||
|
foreach (var keyword in LoggerSettings.MaskedKeywords)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(keyword))
|
||||||
|
{
|
||||||
|
message = message.Replace(keyword, LoggerSettings.Mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,34 +6,34 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger
|
namespace EonaCat.Logger.EonaCatCoreLogger
|
||||||
{
|
{
|
||||||
public sealed class JsonFileLogger : ILogger, IDisposable
|
public sealed class JsonFileLogger : ILogger, IDisposable
|
||||||
{
|
{
|
||||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
|
||||||
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
|
||||||
|
|
||||||
private readonly string _categoryName;
|
private readonly 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 readonly BlockingCollection<string> _queue;
|
private readonly BlockingCollection<string> _queue;
|
||||||
private readonly Thread _workerThread;
|
private readonly CancellationTokenSource _cts = new();
|
||||||
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
|
private readonly Task _workerTask;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
public JsonFileLogger(string categoryName, JsonFileLoggerOptions options)
|
public JsonFileLogger(string categoryName, JsonFileLoggerOptions 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;
|
||||||
|
|
||||||
_filePath = Path.Combine(_options.LogDirectory, _options.FileName);
|
_filePath = Path.Combine(_options.LogDirectory, _options.FileName);
|
||||||
_queue = new BlockingCollection<string>(10000); // bounded queue
|
_queue = new BlockingCollection<string>(MaxQueueSize);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -44,9 +44,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
OnException?.Invoke(this, ex);
|
OnException?.Invoke(this, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start background thread
|
_workerTask = Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);
|
||||||
_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);
|
||||||
@@ -71,14 +69,14 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
_context.Set("CorrelationId", correlationId);
|
_context.Set("CorrelationId", correlationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var logObject = new Dictionary<string, object>
|
var logObject = new Dictionary<string, object>(6)
|
||||||
{
|
{
|
||||||
{ "timestamp", DateTime.UtcNow },
|
["timestamp"] = DateTime.UtcNow,
|
||||||
{ "level", logLevel.ToString() },
|
["level"] = logLevel.ToString(),
|
||||||
{ "category", _categoryName },
|
["category"] = _categoryName,
|
||||||
{ "message", formatter(state, exception) },
|
["message"] = formatter(state, exception),
|
||||||
{ "exception", exception?.ToString() },
|
["exception"] = exception?.ToString(),
|
||||||
{ "eventId", eventId.Id }
|
["eventId"] = eventId.Id
|
||||||
};
|
};
|
||||||
|
|
||||||
var contextData = _context.GetAll();
|
var contextData = _context.GetAll();
|
||||||
@@ -89,10 +87,10 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
string json = JsonHelper.ToJson(logObject);
|
string json = JsonHelper.ToJson(logObject);
|
||||||
|
|
||||||
// Enqueue, drop oldest if full
|
// Drop oldest if full
|
||||||
while (!_queue.TryAdd(json))
|
while (!_queue.TryAdd(json))
|
||||||
{
|
{
|
||||||
_queue.TryTake(out _); // drop oldest
|
_queue.TryTake(out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -117,14 +115,24 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// normal shutdown
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnException?.Invoke(this, ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flush all pending logs to file.
|
||||||
|
/// </summary>
|
||||||
public void Flush()
|
public void Flush()
|
||||||
{
|
{
|
||||||
while (_queue.Count > 0)
|
while (_queue.Count > 0)
|
||||||
{
|
{
|
||||||
Thread.Sleep(100);
|
Thread.Sleep(50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,9 +143,12 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_workerThread.Join();
|
_workerTask.Wait();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
}
|
}
|
||||||
catch { /* ignore */ }
|
|
||||||
|
|
||||||
_queue.Dispose();
|
_queue.Dispose();
|
||||||
_cts.Dispose();
|
_cts.Dispose();
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ 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.
|
||||||
|
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||||
|
|
||||||
internal sealed class LoggerScopedContext
|
internal sealed class LoggerScopedContext
|
||||||
{
|
{
|
||||||
private static readonly AsyncLocal<Stack<Dictionary<string, string>>> _scopes = new();
|
private static readonly AsyncLocal<Stack<Dictionary<string, string>>> _scopes = new();
|
||||||
|
|
||||||
// Ensure there is always a scope to write into
|
|
||||||
private void EnsureScope()
|
private void EnsureScope()
|
||||||
{
|
{
|
||||||
_scopes.Value ??= new Stack<Dictionary<string, string>>();
|
_scopes.Value ??= new Stack<Dictionary<string, string>>();
|
||||||
@@ -35,12 +37,16 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
public string Get(string key)
|
public string Get(string key)
|
||||||
{
|
{
|
||||||
if (_scopes.Value == null)
|
if (_scopes.Value == null)
|
||||||
|
{
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var scope in _scopes.Value)
|
foreach (var scope in _scopes.Value)
|
||||||
{
|
{
|
||||||
if (scope.TryGetValue(key, out var value))
|
if (scope.TryGetValue(key, out var value))
|
||||||
|
{
|
||||||
return value;
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -56,7 +62,9 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
foreach (var kv in scope)
|
foreach (var kv in scope)
|
||||||
{
|
{
|
||||||
if (!result.ContainsKey(kv.Key))
|
if (!result.ContainsKey(kv.Key))
|
||||||
|
{
|
||||||
result[kv.Key] = kv.Value;
|
result[kv.Key] = kv.Value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ using System.Threading;
|
|||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
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 sealed class SlackLogger : ILogger, IDisposable
|
public sealed class SlackLogger : ILogger, IDisposable
|
||||||
{
|
{
|
||||||
private readonly string _categoryName;
|
private readonly string _categoryName;
|
||||||
@@ -23,25 +23,28 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
private readonly CancellationTokenSource _cts = new();
|
private readonly CancellationTokenSource _cts = new();
|
||||||
private readonly Task _processingTask;
|
private readonly Task _processingTask;
|
||||||
|
|
||||||
|
private const int MaxQueueSize = 1000;
|
||||||
|
|
||||||
public bool IncludeCorrelationId { get; set; }
|
public bool IncludeCorrelationId { get; set; }
|
||||||
public event EventHandler<Exception> OnException;
|
public event EventHandler<Exception> OnException;
|
||||||
|
|
||||||
public SlackLogger(string categoryName, SlackLoggerOptions options)
|
public SlackLogger(string categoryName, SlackLoggerOptions 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;
|
||||||
|
|
||||||
_httpClient = new HttpClient();
|
_httpClient = new HttpClient();
|
||||||
|
|
||||||
// Bounded channel to prevent unbounded memory growth
|
// Bounded channel to prevent unbounded memory growth
|
||||||
_logChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(1000)
|
_logChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(MaxQueueSize)
|
||||||
{
|
{
|
||||||
FullMode = BoundedChannelFullMode.DropOldest,
|
FullMode = BoundedChannelFullMode.DropOldest,
|
||||||
SingleReader = true,
|
SingleReader = true,
|
||||||
SingleWriter = false
|
SingleWriter = false
|
||||||
});
|
});
|
||||||
|
|
||||||
_processingTask = Task.Run(() => ProcessLogQueueAsync(_cts.Token));
|
_processingTask = Task.Run(() => ProcessLogQueueAsync(_cts.Token), _cts.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDisposable BeginScope<TState>(TState state) => null;
|
public IDisposable BeginScope<TState>(TState state) => null;
|
||||||
@@ -61,40 +64,37 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var message = formatter(state, exception);
|
|
||||||
|
|
||||||
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 logParts = new List<string>
|
var message = formatter(state, exception);
|
||||||
{
|
|
||||||
$"*[{DateTime.UtcNow:u}]*",
|
// Build Slack message
|
||||||
$"*[{logLevel}]*",
|
var sb = new StringBuilder(256);
|
||||||
$"*[{_categoryName}]*",
|
sb.AppendLine($"*[{DateTime.UtcNow:u}]*");
|
||||||
$"Message: {message}"
|
sb.AppendLine($"*[{logLevel}]*");
|
||||||
};
|
sb.AppendLine($"*[{_categoryName}]*");
|
||||||
|
sb.AppendLine($"Message: {message}");
|
||||||
|
|
||||||
foreach (var kvp in _context.GetAll())
|
foreach (var kvp in _context.GetAll())
|
||||||
{
|
{
|
||||||
logParts.Add($"_{kvp.Key}_: {kvp.Value}");
|
sb.AppendLine($"_{kvp.Key}_: {kvp.Value}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exception != null)
|
if (exception != null)
|
||||||
{
|
{
|
||||||
logParts.Add($"Exception: {exception}");
|
sb.AppendLine($"Exception: {exception}");
|
||||||
}
|
}
|
||||||
|
|
||||||
string fullMessage = string.Join("\n", logParts);
|
// Drops oldest if full
|
||||||
|
_logChannel.Writer.TryWrite(sb.ToString());
|
||||||
// non-blocking, drops oldest if full
|
|
||||||
_logChannel.Writer.TryWrite(fullMessage);
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnException?.Invoke(this, e);
|
OnException?.Invoke(this, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,8 +107,14 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var payload = new { text = message };
|
var payload = new { text = message };
|
||||||
var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json");
|
using var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json");
|
||||||
await _httpClient.PostAsync(_options.WebhookUrl, content, token);
|
|
||||||
|
var response = await _httpClient.PostAsync(_options.WebhookUrl, content, token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
OnException?.Invoke(this, new Exception($"Slack webhook failed: {response.StatusCode}"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -116,7 +122,14 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// normal shutdown
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnException?.Invoke(this, ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -128,7 +141,10 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
{
|
{
|
||||||
_processingTask.Wait();
|
_processingTask.Wait();
|
||||||
}
|
}
|
||||||
catch { /* ignore */ }
|
catch
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
_cts.Dispose();
|
_cts.Dispose();
|
||||||
_httpClient.Dispose();
|
_httpClient.Dispose();
|
||||||
|
|||||||
@@ -19,31 +19,33 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
private readonly CancellationTokenSource _cts = new();
|
private readonly CancellationTokenSource _cts = new();
|
||||||
private readonly Task _processingTask;
|
private readonly Task _processingTask;
|
||||||
|
|
||||||
|
private const int MaxQueueSize = 1000;
|
||||||
|
|
||||||
public bool IncludeCorrelationId { get; set; }
|
public bool IncludeCorrelationId { get; set; }
|
||||||
public event EventHandler<Exception> OnException;
|
public event EventHandler<Exception> OnException;
|
||||||
|
|
||||||
public TcpLogger(string categoryName, TcpLoggerOptions options)
|
public TcpLogger(string categoryName, TcpLoggerOptions 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;
|
||||||
|
|
||||||
// Bounded channel to prevent unbounded memory growth
|
_logChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(MaxQueueSize)
|
||||||
_logChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(1000)
|
|
||||||
{
|
{
|
||||||
FullMode = BoundedChannelFullMode.DropOldest,
|
FullMode = BoundedChannelFullMode.DropOldest,
|
||||||
SingleReader = true,
|
SingleReader = true,
|
||||||
SingleWriter = false
|
SingleWriter = false
|
||||||
});
|
});
|
||||||
|
|
||||||
_processingTask = Task.Run(() => ProcessLogQueueAsync(_cts.Token));
|
_processingTask = Task.Run(() => ProcessLogQueueAsync(_cts.Token), _cts.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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 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)
|
||||||
@@ -55,76 +57,92 @@ 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();
|
||||||
_context.Set("CorrelationId", correlationId);
|
_context.Set("CorrelationId", correlationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var logParts = new List<string>
|
string message = formatter(state, exception);
|
||||||
{
|
|
||||||
$"[{DateTime.UtcNow:u}]",
|
// Build message
|
||||||
$"[{logLevel}]",
|
var sb = new StringBuilder(256);
|
||||||
$"[{_categoryName}]",
|
sb.Append('[').Append(DateTime.UtcNow.ToString("u")).Append("] | ");
|
||||||
$"Message: {message}"
|
sb.Append('[').Append(logLevel).Append("] | ");
|
||||||
};
|
sb.Append('[').Append(_categoryName).Append("] | ");
|
||||||
|
sb.Append("Message: ").Append(message);
|
||||||
|
|
||||||
var contextData = _context.GetAll();
|
var contextData = _context.GetAll();
|
||||||
if (contextData.Count > 0)
|
if (contextData.Count > 0)
|
||||||
{
|
{
|
||||||
logParts.Add("Context:");
|
sb.Append(" | Context: ");
|
||||||
foreach (var kvp in contextData)
|
foreach (var kvp in contextData)
|
||||||
{
|
{
|
||||||
logParts.Add($"{kvp.Key}: {kvp.Value}");
|
sb.Append(kvp.Key).Append("=").Append(kvp.Value).Append("; ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exception != null)
|
if (exception != null)
|
||||||
{
|
{
|
||||||
logParts.Add($"Exception: {exception}");
|
sb.Append(" | Exception: ").Append(exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
string fullLog = string.Join(" | ", logParts);
|
_logChannel.Writer.TryWrite(sb.ToString());
|
||||||
|
|
||||||
// Non-blocking, drop oldest if full
|
|
||||||
_logChannel.Writer.TryWrite(fullLog);
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnException?.Invoke(this, e);
|
OnException?.Invoke(this, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessLogQueueAsync(CancellationToken token)
|
private async Task ProcessLogQueueAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
|
TcpClient client = null;
|
||||||
|
StreamWriter writer = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Keep one TCP connection alive if possible
|
client = new TcpClient();
|
||||||
using var client = new TcpClient();
|
await client.ConnectAsync(_options.Host, _options.Port).ConfigureAwait(false);
|
||||||
await client.ConnectAsync(_options.Host, _options.Port);
|
|
||||||
|
|
||||||
using var stream = client.GetStream();
|
writer = new StreamWriter(client.GetStream(), Encoding.UTF8) { AutoFlush = true };
|
||||||
using var writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true };
|
|
||||||
|
|
||||||
await foreach (var log in _logChannel.Reader.ReadAllAsync(token))
|
await foreach (var log in _logChannel.Reader.ReadAllAsync(token))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await writer.WriteLineAsync(log);
|
await writer.WriteLineAsync(log).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (IOException ioEx)
|
||||||
{
|
{
|
||||||
OnException?.Invoke(this, e);
|
OnException?.Invoke(this, ioEx);
|
||||||
|
|
||||||
|
// Attempt to reconnect
|
||||||
|
writer.Dispose();
|
||||||
|
client.Dispose();
|
||||||
|
client = new TcpClient();
|
||||||
|
await client.ConnectAsync(_options.Host, _options.Port).ConfigureAwait(false);
|
||||||
|
writer = new StreamWriter(client.GetStream(), Encoding.UTF8) { AutoFlush = true };
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnException?.Invoke(this, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// normal shutdown
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnException?.Invoke(this, ex);
|
OnException?.Invoke(this, ex);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
writer?.Dispose();
|
||||||
|
client?.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -136,7 +154,10 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
{
|
{
|
||||||
_processingTask.Wait();
|
_processingTask.Wait();
|
||||||
}
|
}
|
||||||
catch { /* ignore */ }
|
catch
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
_cts.Dispose();
|
_cts.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
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;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Net.Http;
|
|
||||||
|
// 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
|
||||||
{
|
{
|
||||||
@@ -15,27 +18,28 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
private readonly TelegramLoggerOptions _options;
|
private readonly TelegramLoggerOptions _options;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly Channel<string> _logChannel;
|
private readonly Channel<string> _logChannel;
|
||||||
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
|
private readonly CancellationTokenSource _cts = new();
|
||||||
private readonly Task _processingTask;
|
private readonly Task _processingTask;
|
||||||
|
|
||||||
|
private const int MaxQueueSize = 1000;
|
||||||
|
|
||||||
public event EventHandler<Exception> OnException;
|
public event EventHandler<Exception> OnException;
|
||||||
|
|
||||||
public TelegramLogger(string categoryName, TelegramLoggerOptions options)
|
public TelegramLogger(string categoryName, TelegramLoggerOptions options)
|
||||||
{
|
{
|
||||||
_categoryName = categoryName;
|
_categoryName = categoryName ?? throw new ArgumentNullException(nameof(categoryName));
|
||||||
_options = options;
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
_httpClient = new HttpClient();
|
_httpClient = new HttpClient();
|
||||||
|
|
||||||
// Bounded channel to prevent memory leaks
|
_logChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(MaxQueueSize)
|
||||||
_logChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(1000)
|
|
||||||
{
|
{
|
||||||
FullMode = BoundedChannelFullMode.DropOldest,
|
FullMode = BoundedChannelFullMode.DropOldest,
|
||||||
SingleReader = true,
|
SingleReader = true,
|
||||||
SingleWriter = false
|
SingleWriter = false
|
||||||
});
|
});
|
||||||
|
|
||||||
_processingTask = Task.Run(() => ProcessLogQueueAsync(_cts.Token));
|
_processingTask = Task.Run(() => ProcessLogQueueAsync(_cts.Token), _cts.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDisposable BeginScope<TState>(TState state) => null;
|
public IDisposable BeginScope<TState>(TState state) => null;
|
||||||
@@ -44,21 +48,22 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
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) || formatter == null)
|
if (!IsEnabled(logLevel) || formatter == null) return;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var message = $"[{DateTime.UtcNow:u}] [{logLevel}] {_categoryName}: {formatter(state, exception)}";
|
var sb = new StringBuilder(256);
|
||||||
|
sb.Append('[').Append(DateTime.UtcNow.ToString("u")).Append("] ");
|
||||||
|
sb.Append('[').Append(logLevel).Append("] ");
|
||||||
|
sb.Append(_categoryName).Append(": ");
|
||||||
|
sb.Append(formatter(state, exception));
|
||||||
|
|
||||||
if (exception != null)
|
if (exception != null)
|
||||||
{
|
{
|
||||||
message += $"\nException: {exception}";
|
sb.AppendLine().Append("Exception: ").Append(exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-blocking, drop oldest if full
|
_logChannel.Writer.TryWrite(sb.ToString());
|
||||||
_logChannel.Writer.TryWrite(message);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -70,15 +75,22 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var url = $"https://api.telegram.org/bot{_options.BotToken}/sendMessage";
|
||||||
|
|
||||||
await foreach (var message in _logChannel.Reader.ReadAllAsync(token))
|
await foreach (var message in _logChannel.Reader.ReadAllAsync(token))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var url = $"https://api.telegram.org/bot{_options.BotToken}/sendMessage";
|
|
||||||
var payload = new { chat_id = _options.ChatId, text = message };
|
var payload = new { chat_id = _options.ChatId, text = message };
|
||||||
var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json");
|
using var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
await _httpClient.PostAsync(url, content, token);
|
var response = await _httpClient.PostAsync(url, content, token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
OnException?.Invoke(this, new Exception($"Telegram API failed: {response.StatusCode} {error}"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -86,7 +98,10 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// normal shutdown
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -98,7 +113,10 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
{
|
{
|
||||||
_processingTask.Wait();
|
_processingTask.Wait();
|
||||||
}
|
}
|
||||||
catch { /* ignore */ }
|
catch
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
_cts.Dispose();
|
_cts.Dispose();
|
||||||
_httpClient.Dispose();
|
_httpClient.Dispose();
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
_udpClient = new UdpClient();
|
_udpClient = new UdpClient();
|
||||||
|
|
||||||
// Bounded channel to avoid unbounded memory growth
|
|
||||||
_logChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(1000)
|
_logChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(1000)
|
||||||
{
|
{
|
||||||
FullMode = BoundedChannelFullMode.DropOldest,
|
FullMode = BoundedChannelFullMode.DropOldest,
|
||||||
@@ -93,7 +92,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
string fullMessage = string.Join(" | ", logParts);
|
string fullMessage = string.Join(" | ", logParts);
|
||||||
|
|
||||||
// Non-blocking, drop oldest if full
|
// Drop oldest if full
|
||||||
_logChannel.Writer.TryWrite(fullMessage);
|
_logChannel.Writer.TryWrite(fullMessage);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -119,7 +118,10 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// normal shutdown
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnException?.Invoke(this, ex);
|
OnException?.Invoke(this, ex);
|
||||||
@@ -135,7 +137,10 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
{
|
{
|
||||||
_processingTask.Wait();
|
_processingTask.Wait();
|
||||||
}
|
}
|
||||||
catch { /* ignore */ }
|
catch
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
_udpClient.Dispose();
|
_udpClient.Dispose();
|
||||||
_cts.Dispose();
|
_cts.Dispose();
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger
|
namespace EonaCat.Logger.EonaCatCoreLogger
|
||||||
{
|
{
|
||||||
public sealed class XmlFileLogger : ILogger, IDisposable
|
public sealed class XmlFileLogger : ILogger, IDisposable
|
||||||
{
|
{
|
||||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
|
||||||
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
|
||||||
|
|
||||||
private readonly string _categoryName;
|
private readonly 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 readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1);
|
private readonly SemaphoreSlim _fileLock = new(1, 1);
|
||||||
|
|
||||||
public bool IncludeCorrelationId { get; set; }
|
public bool IncludeCorrelationId { get; set; }
|
||||||
public event EventHandler<Exception> OnException;
|
public event EventHandler<Exception> OnException;
|
||||||
@@ -51,6 +50,11 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = LogAsync(logLevel, state, exception, formatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LogAsync<TState>(LogLevel logLevel, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (IncludeCorrelationId)
|
if (IncludeCorrelationId)
|
||||||
@@ -66,14 +70,15 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
new XElement("message", formatter(state, exception))
|
new XElement("message", formatter(state, exception))
|
||||||
);
|
);
|
||||||
|
|
||||||
var context = _context.GetAll();
|
var contextData = _context.GetAll();
|
||||||
if (context.Count > 0)
|
if (contextData.Count > 0)
|
||||||
{
|
{
|
||||||
var contextElement = new XElement("context");
|
var contextElement = new XElement("context");
|
||||||
foreach (var item in context)
|
foreach (var kv in contextData)
|
||||||
{
|
{
|
||||||
contextElement.Add(new XElement(item.Key, item.Value));
|
contextElement.Add(new XElement(kv.Key, kv.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
logElement.Add(contextElement);
|
logElement.Add(contextElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,10 +87,17 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
logElement.Add(new XElement("exception", exception.ToString()));
|
logElement.Add(new XElement("exception", exception.ToString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
_fileLock.Wait();
|
string logString = logElement + Environment.NewLine;
|
||||||
|
|
||||||
|
await _fileLock.WaitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.AppendAllText(_filePath, logElement + Environment.NewLine);
|
using (var fs = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.Read, 4096, true))
|
||||||
|
using (var sw = new StreamWriter(fs, Encoding.UTF8))
|
||||||
|
{
|
||||||
|
await sw.WriteAsync(logString).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -98,9 +110,6 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose() => _fileLock.Dispose();
|
||||||
{
|
|
||||||
_fileLock.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,12 +133,14 @@ public static class LogHelper
|
|||||||
string category = null)
|
string category = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(message))
|
if (string.IsNullOrWhiteSpace(message))
|
||||||
|
{
|
||||||
return message;
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
var ctx = new HeaderContext
|
var ctx = new HeaderContext
|
||||||
{
|
{
|
||||||
Timestamp = dateTime,
|
Timestamp = dateTime,
|
||||||
TimestampFormat = settings?.TimestampFormat ?? "yyyy-MM-dd HH:mm:ss",
|
TimestampFormat = settings?.TimestampFormat ?? "yyyy-MM-dd HH:mm:ss.fff",
|
||||||
HostName = HostName,
|
HostName = HostName,
|
||||||
Category = category ?? "General",
|
Category = category ?? "General",
|
||||||
LogType = logType,
|
LogType = logType,
|
||||||
@@ -160,7 +162,9 @@ public static class LogHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(header))
|
if (!string.IsNullOrEmpty(header))
|
||||||
|
{
|
||||||
header += " ";
|
header += " ";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return header + message;
|
return header + message;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public class LoggerSettings
|
|||||||
private FileLoggerOptions _fileLoggerOptions;
|
private FileLoggerOptions _fileLoggerOptions;
|
||||||
|
|
||||||
private string _headerFormat = "{ts} {tz} {host} {category} {thread} {logtype}";
|
private string _headerFormat = "{ts} {tz} {host} {category} {thread} {logtype}";
|
||||||
private string _timestampFormat = "yyyy-MM-dd HH:mm:ss";
|
private string _timestampFormat = "yyyy-MM-dd HH:mm:ss.fff";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines if we need to use the local time in the logging or UTC (default:false)
|
/// Determines if we need to use the local time in the logging or UTC (default:false)
|
||||||
|
|||||||
Reference in New Issue
Block a user