Updated (added more header options)
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EonaCat.Logger" Version="1.5.5" />
|
||||
<PackageReference Include="EonaCat.Logger" Version="1.5.7" />
|
||||
<PackageReference Include="System.Net.Http.Json" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
<Copyright>EonaCat (Jeroen Saey)</Copyright>
|
||||
<PackageTags>EonaCat;Logger;EonaCatLogger;Log;Writer;Jeroen;Saey</PackageTags>
|
||||
<PackageIconUrl />
|
||||
<Version>1.5.7</Version>
|
||||
<FileVersion>1.5.7</FileVersion>
|
||||
<Version>1.5.8</Version>
|
||||
<FileVersion>1.5.8</FileVersion>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
@@ -25,7 +25,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<EVRevisionFormat>1.5.7+{chash:10}.{c:ymd}</EVRevisionFormat>
|
||||
<EVRevisionFormat>1.5.8+{chash:10}.{c:ymd}</EVRevisionFormat>
|
||||
<EVDefault>true</EVDefault>
|
||||
<EVInfo>true</EVInfo>
|
||||
<EVTagMatch>v[0-9]*</EVTagMatch>
|
||||
|
||||
@@ -53,7 +53,9 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
||||
Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel) || formatter == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IncludeCorrelationId)
|
||||
{
|
||||
@@ -71,16 +73,24 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
||||
};
|
||||
|
||||
foreach (var kvp in _context.GetAll())
|
||||
{
|
||||
logParts.Add($"`{kvp.Key}`: {kvp.Value}");
|
||||
}
|
||||
|
||||
if (exception != null)
|
||||
{
|
||||
logParts.Add($"Exception: {exception}");
|
||||
}
|
||||
|
||||
// Limit queue size to prevent memory growth
|
||||
if (_messageQueue.Count < 1000)
|
||||
{
|
||||
_messageQueue.Enqueue(string.Join("\n", logParts));
|
||||
}
|
||||
else
|
||||
{
|
||||
OnException?.Invoke(this, new Exception("DiscordLogger queue overflow"));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FlushLoopAsync(CancellationToken token)
|
||||
@@ -107,7 +117,9 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
||||
private async Task FlushBufferAsync(CancellationToken token)
|
||||
{
|
||||
if (!await _flushLock.WaitAsync(0, token).ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -54,8 +54,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger.Internal
|
||||
? exception.FormatExceptionToMessage() + Environment.NewLine
|
||||
: formatter(state, exception);
|
||||
|
||||
message = LogHelper.FormatMessageWithHeader(_loggerSettings, logLevel.FromLogLevel(), message, timestamp.DateTime, category)
|
||||
+ Environment.NewLine;
|
||||
message = LogHelper.FormatMessageWithHeader(_loggerSettings, logLevel.FromLogLevel(), message, timestamp.DateTime, category) + Environment.NewLine;
|
||||
|
||||
var currentMessage = new EonaCatLogMessage
|
||||
{
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.SymbolStore;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using EonaCat.Logger.Managers;
|
||||
using EonaCat.Logger.Managers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EonaCat.Logger.EonaCatCoreLogger.Internal;
|
||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||
@@ -14,21 +13,22 @@ namespace EonaCat.Logger.EonaCatCoreLogger.Internal;
|
||||
|
||||
public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
|
||||
{
|
||||
private readonly int _batchSize;
|
||||
private int _batchSize;
|
||||
|
||||
private readonly Channel<LogMessage> _channel =
|
||||
Channel.CreateUnbounded<LogMessage>(new UnboundedChannelOptions
|
||||
{
|
||||
SingleReader = true,
|
||||
SingleWriter = false
|
||||
});
|
||||
|
||||
private readonly List<LogMessage> _currentBatch = new();
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
private LoggerSettings _loggerSettings;
|
||||
|
||||
private ConcurrentQueue<LogMessage> _messageQueue;
|
||||
private Task _outputTask;
|
||||
private object _writeLock = new object();
|
||||
private bool _isDisposing;
|
||||
protected string Category;
|
||||
private bool _isDisposed;
|
||||
|
||||
protected BatchingLoggerProvider(IOptions<BatchingLoggerOptions> options)
|
||||
{
|
||||
var loggerOptions = options.Value;
|
||||
var loggerOptions = options.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
|
||||
if (loggerOptions.FlushPeriod <= TimeSpan.Zero)
|
||||
{
|
||||
@@ -42,9 +42,8 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
|
||||
UseMask = fileLoggerOptions.UseMask;
|
||||
}
|
||||
|
||||
_batchSize = loggerOptions.BatchSize;
|
||||
|
||||
StartAsync().ConfigureAwait(false);
|
||||
_batchSize = loggerOptions.BatchSize > 0 ? loggerOptions.BatchSize : 100;
|
||||
StartAsync();
|
||||
}
|
||||
|
||||
protected DateTimeOffset CurrentDateTimeOffset => UseLocalTime ? DateTimeOffset.Now : DateTimeOffset.UtcNow;
|
||||
@@ -70,73 +69,56 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
|
||||
set => _loggerSettings = value;
|
||||
}
|
||||
|
||||
private SemaphoreSlim _writeSemaphore = new SemaphoreSlim(1, 1);
|
||||
|
||||
public bool IsStarted { get; set; }
|
||||
public bool UseMask { get; set; }
|
||||
|
||||
public async void Dispose()
|
||||
{
|
||||
while (!_messageQueue.IsEmpty)
|
||||
{
|
||||
// Do nothing, just wait for the queue to be empty
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
_isDisposing = true;
|
||||
await StopAsync().ConfigureAwait(false);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
private LoggerSettings _loggerSettings;
|
||||
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
Category = categoryName;
|
||||
return new BatchingLogger(this, categoryName, LoggerSettings);
|
||||
}
|
||||
|
||||
protected abstract Task WriteMessagesAsync(IEnumerable<LogMessage> messages, CancellationToken token);
|
||||
private async Task ProcessLogQueueAsync(object state)
|
||||
|
||||
private async Task ProcessLogQueueAsync()
|
||||
{
|
||||
while (!_cancellationTokenSource.IsCancellationRequested)
|
||||
var batchSize = _batchSize > 0 ? _batchSize : 100;
|
||||
var batch = new List<LogMessage>(batchSize);
|
||||
|
||||
try
|
||||
{
|
||||
var limit = _batchSize <= 0 ? int.MaxValue : _batchSize;
|
||||
while (limit > 0 && _messageQueue.TryDequeue(out var message))
|
||||
var token = _cancellationTokenSource.Token;
|
||||
while (await _channel.Reader.WaitToReadAsync(token).ConfigureAwait(false))
|
||||
{
|
||||
_currentBatch.Add(message);
|
||||
limit--;
|
||||
}
|
||||
batch.Clear();
|
||||
|
||||
if (_currentBatch.Count > 0)
|
||||
{
|
||||
try
|
||||
while (batch.Count < batchSize && _channel.Reader.TryRead(out var message))
|
||||
{
|
||||
if (_isDisposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
batch.Add(message);
|
||||
}
|
||||
|
||||
await _writeSemaphore.WaitAsync();
|
||||
await WriteMessagesAsync(_currentBatch, _cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
_currentBatch.Clear();
|
||||
}
|
||||
catch
|
||||
if (batch.Count > 0)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeSemaphore.Release();
|
||||
await WriteMessagesAsync(batch, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
await Task.Delay(500);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// normal shutdown
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected async Task WriteStartMessage()
|
||||
{
|
||||
var message = LogHelper.GetStartupMessage();
|
||||
await WriteMessagesAsync(new List<LogMessage> { CreateLoggerMessage(message, CurrentDateTimeOffset) }, _cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
var token = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
||||
await WriteMessagesAsync(new List<LogMessage> { CreateLoggerMessage(message, CurrentDateTimeOffset) }, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private LogMessage CreateLoggerMessage(string message, DateTimeOffset currentDateTimeOffset)
|
||||
@@ -320,46 +302,93 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
|
||||
internal string AddMessage(DateTimeOffset timestamp, string message)
|
||||
{
|
||||
var result = CreateLoggerMessage(message, timestamp);
|
||||
_messageQueue.Enqueue(result);
|
||||
if (!_channel.Writer.TryWrite(result))
|
||||
{
|
||||
_ = _channel.Writer.WriteAsync(result);
|
||||
}
|
||||
|
||||
return result.Message;
|
||||
}
|
||||
|
||||
private Task StartAsync()
|
||||
private void StartAsync()
|
||||
{
|
||||
IsStarted = true;
|
||||
_messageQueue = new ConcurrentQueue<LogMessage>();
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
if (_cancellationTokenSource != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_outputTask = Task.Factory.StartNew(
|
||||
ProcessLogQueueAsync,
|
||||
null,
|
||||
TaskCreationOptions.LongRunning);
|
||||
return Task.CompletedTask;
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_outputTask = Task.Run(ProcessLogQueueAsync);
|
||||
IsStarted = true;
|
||||
}
|
||||
|
||||
private async Task StopAsync()
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
if (_cancellationTokenSource == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
while (_outputTask.Status != TaskStatus.RanToCompletion && _outputTask.Status != TaskStatus.Canceled)
|
||||
_cancellationTokenSource.Cancel();
|
||||
_channel.Writer.Complete();
|
||||
|
||||
if (_outputTask != null)
|
||||
{
|
||||
await _outputTask.ConfigureAwait(false);
|
||||
await Task.Delay(100);
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// expected on shutdown
|
||||
}
|
||||
catch (AggregateException exception) when (exception.InnerExceptions.Count == 1 &&
|
||||
exception.InnerExceptions[0] is TaskCanceledException)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
~BatchingLoggerProvider()
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose();
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
try
|
||||
{
|
||||
StopAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public static class ExceptionExtensions
|
||||
var sb = new StringBuilderChill();
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"--- Exception details provided by {DllInfo.ApplicationName} ---");
|
||||
sb.AppendLine($"--- Exception details provided by {DllInfo.ApplicationName} on {Environment.MachineName} ---");
|
||||
if (!string.IsNullOrEmpty(module))
|
||||
{
|
||||
sb.AppendLine(" Module : " + module);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using EonaCat.Json;
|
||||
using EonaCat.Json;
|
||||
using EonaCat.Logger.Extensions;
|
||||
using EonaCat.Logger.Servers.GrayLog;
|
||||
using EonaCat.Logger.Servers.Splunk.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
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.
|
||||
@@ -23,6 +25,34 @@ public class ErrorMessage
|
||||
|
||||
public static class LogHelper
|
||||
{
|
||||
public sealed class HeaderContext
|
||||
{
|
||||
public DateTime Timestamp { get; set; }
|
||||
public string TimestampFormat { get; set; }
|
||||
public string HostName { get; set; }
|
||||
public string Category { get; set; }
|
||||
public ELogType LogType { get; set; }
|
||||
public LoggerSettings Settings { get; set; }
|
||||
public string EnvironmentName { get; set; }
|
||||
}
|
||||
|
||||
internal static Dictionary<string, Func<HeaderContext, string>> TokenResolvers =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["ts"] = ctx => ctx.Timestamp.ToString(ctx.TimestampFormat),
|
||||
["host"] = ctx => ctx.HostName,
|
||||
["machine"] = ctx => Environment.MachineName,
|
||||
["category"] = ctx => ctx.Category,
|
||||
["thread"] = ctx => Environment.CurrentManagedThreadId.ToString(),
|
||||
["process"] = ctx => Process.GetCurrentProcess().ProcessName,
|
||||
["pid"] = ctx => Process.GetCurrentProcess().Id.ToString(),
|
||||
["sev"] = ctx => ctx.LogType.ToString(),
|
||||
["user"] = ctx => Environment.UserName,
|
||||
["env"] = ctx => ctx.EnvironmentName,
|
||||
["newline"] = _ => Environment.NewLine
|
||||
};
|
||||
|
||||
|
||||
internal static event EventHandler<ErrorMessage> OnException;
|
||||
|
||||
internal static event EventHandler<ErrorMessage> OnLogLevelDisabled;
|
||||
@@ -73,34 +103,71 @@ public static class LogHelper
|
||||
return syslogMessage;
|
||||
}
|
||||
|
||||
|
||||
internal static string FormatMessageWithHeader(LoggerSettings settings, ELogType logType, string currentMessage,
|
||||
DateTime dateTime, string category = null)
|
||||
private static string ResolveHeader(string format, HeaderContext ctx)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(currentMessage))
|
||||
if (string.IsNullOrWhiteSpace(format))
|
||||
{
|
||||
return currentMessage;
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
category ??= "General";
|
||||
var sb = new StringBuilder(settings?.HeaderFormat);
|
||||
var timestamp = dateTime.ToString(settings?.TimestampFormat ?? "yyyy-MM-dd HH:mm:ss");
|
||||
var timeLabel = settings?.UseLocalTime ?? false ? "[LOCAL]" : "[UTC]";
|
||||
|
||||
sb.Replace("{ts}", $"{timestamp} {timeLabel}")
|
||||
.Replace("{host}", $"[Host:{HostName}]")
|
||||
.Replace("{category}", $"[Category:{category}]")
|
||||
.Replace("{thread}", $"[ThreadId:{Environment.CurrentManagedThreadId}]")
|
||||
.Replace("{sev}", $"[{logType}]");
|
||||
|
||||
if (sb.Length > 0)
|
||||
return Regex.Replace(format, @"\{([^}]+)\}", match =>
|
||||
{
|
||||
sb.Append(" ");
|
||||
}
|
||||
sb.Append(currentMessage);
|
||||
return sb.ToString();
|
||||
var token = match.Groups[1].Value;
|
||||
var parts = token.Split(new[] { ':' }, 2);
|
||||
var key = parts[0];
|
||||
|
||||
if (!TokenResolvers.TryGetValue(key, out var resolver))
|
||||
{
|
||||
return match.Value;
|
||||
}
|
||||
|
||||
if (key.Equals("ts", StringComparison.OrdinalIgnoreCase) && parts.Length == 2)
|
||||
{
|
||||
ctx.TimestampFormat = parts[1];
|
||||
}
|
||||
|
||||
return resolver(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
internal static string FormatMessageWithHeader(
|
||||
LoggerSettings settings,
|
||||
ELogType logType,
|
||||
string message,
|
||||
DateTime dateTime,
|
||||
string category = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
var ctx = new HeaderContext
|
||||
{
|
||||
Timestamp = dateTime,
|
||||
TimestampFormat = settings?.TimestampFormat ?? "yyyy-MM-dd HH:mm:ss",
|
||||
HostName = HostName,
|
||||
Category = category ?? "General",
|
||||
LogType = logType,
|
||||
Settings = settings,
|
||||
EnvironmentName = settings?.EnvironmentName
|
||||
};
|
||||
|
||||
string header = settings?.CustomHeaderFormatter != null
|
||||
? settings.CustomHeaderFormatter(ctx)
|
||||
: ResolveHeader(settings?.HeaderFormat, ctx);
|
||||
|
||||
if (!string.IsNullOrEmpty(header))
|
||||
{
|
||||
header += " ";
|
||||
}
|
||||
|
||||
return header + message;
|
||||
}
|
||||
|
||||
|
||||
internal static void SendToConsole(LoggerSettings settings, ELogType logType, string message)
|
||||
{
|
||||
if (settings == null || string.IsNullOrWhiteSpace(message))
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.RegularExpressions;
|
||||
using EonaCat.Json.Linq;
|
||||
using EonaCat.Json.Linq;
|
||||
using EonaCat.Logger.EonaCatCoreLogger;
|
||||
using EonaCat.Logger.EonaCatCoreLogger.Models;
|
||||
using EonaCat.Logger.Servers.GrayLog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.RegularExpressions;
|
||||
using static EonaCat.Logger.Managers.LogHelper;
|
||||
|
||||
namespace EonaCat.Logger.Managers;
|
||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||
@@ -60,6 +61,10 @@ public class LoggerSettings
|
||||
}
|
||||
}
|
||||
|
||||
public Func<HeaderContext, string> CustomHeaderFormatter { get; set; }
|
||||
|
||||
public string EnvironmentName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp format.
|
||||
/// </summary>
|
||||
|
||||
@@ -38,6 +38,16 @@ public class Logger
|
||||
},
|
||||
};
|
||||
|
||||
LoggerSettings.CustomHeaderFormatter = ctx =>
|
||||
{
|
||||
if (ctx.LogType == ELogType.Error)
|
||||
{
|
||||
return $"{ctx.Timestamp:HH:mm:ss} [{ctx.LogType}]";
|
||||
}
|
||||
|
||||
return $"{ctx.Timestamp:HH:mm:ss} [{ctx.LogType}]";
|
||||
};
|
||||
|
||||
_logManager = new LogManager(LoggerSettings);
|
||||
_logManager.Settings.TypesToLog.Clear();
|
||||
_logManager.Settings.LogInfo();
|
||||
|
||||
Reference in New Issue
Block a user