274 lines
10 KiB
C#
274 lines
10 KiB
C#
using System;
|
|
using System.Globalization;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using FinlyticCore.Dtos.Logging;
|
|
using FinlyticCore.Models.Settings;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace FinlyticCore.Services;
|
|
|
|
/// <summary>
|
|
/// Globaler Broadcaster für strukturierte Logs in Echtzeit.
|
|
/// </summary>
|
|
public static class FinlyticLogBroadcaster
|
|
{
|
|
public static Func<LogMessageDto, Task>? OnLogPublished { get; set; }
|
|
|
|
public static void Broadcast(LogMessageDto dto)
|
|
{
|
|
if (OnLogPublished != null)
|
|
{
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await OnLogPublished(dto);
|
|
}
|
|
catch
|
|
{
|
|
// Ignore broadcast errors to never disrupt execution
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bietet kanalbasierte, dynamisch steuerbare Logging-Funktionalitäten über den <see cref="ISettingsService"/>.
|
|
/// </summary>
|
|
/// <typeparam name="TContextClass">Die aufrufende Klasse (für Log-Kategorien).</typeparam>
|
|
public interface IFinlyticLogger<TContextClass>
|
|
{
|
|
// --- Debug ---
|
|
Task LogDebugAsync(SettingKey<bool> channelKey, string message, params object[] args);
|
|
Task LogDebugAsync(SettingKey<bool> channelKey, Exception? exception, string message, params object[] args);
|
|
|
|
// --- Info ---
|
|
Task LogInfoAsync(SettingKey<bool> channelKey, string message, params object[] args);
|
|
Task LogInfoAsync(SettingKey<bool> channelKey, Exception? exception, string message, params object[] args);
|
|
|
|
// --- Warning ---
|
|
Task LogWarningAsync(SettingKey<bool> channelKey, string message, params object[] args);
|
|
Task LogWarningAsync(SettingKey<bool> channelKey, Exception? exception, string message, params object[] args);
|
|
|
|
// --- Error ---
|
|
Task LogErrorAsync(SettingKey<bool> channelKey, string message, params object[] args);
|
|
Task LogErrorAsync(SettingKey<bool> channelKey, Exception? exception, string message, params object[] args);
|
|
|
|
// --- Trace & Critical ---
|
|
Task LogTraceAsync(SettingKey<bool> channelKey, string message, params object[] args);
|
|
Task LogCriticalAsync(SettingKey<bool> channelKey, Exception? exception, string message, params object[] args);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Kanalbasierte Logger-Implementierung, die Einstellungen und Stummschaltungen
|
|
/// in Echtzeit aus dem <see cref="ISettingsService"/> bezieht.
|
|
/// </summary>
|
|
public class FinlyticLogger<TContextClass> : IFinlyticLogger<TContextClass>
|
|
{
|
|
private static readonly string ServiceName = typeof(TContextClass).Assembly.GetName().Name ?? "Finlytic";
|
|
private readonly ILogger<TContextClass> _logger;
|
|
private readonly ISettingsService _settingsService;
|
|
|
|
public FinlyticLogger(
|
|
ILogger<TContextClass> logger,
|
|
ISettingsService settingsService)
|
|
{
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
|
|
}
|
|
|
|
// Matches a structured-logging placeholder like "{CorrelationId}" or "{Score:F1}" - named-placeholder
|
|
// syntax as consumed by ILogger.Log's message templates, NOT .NET's positional composite-format syntax
|
|
// ("{0}", "{1}") that string.Format expects.
|
|
private static readonly Regex PlaceholderPattern = new(@"\{([^{}:]+)(:[^{}]+)?\}", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// Substitutes every named placeholder in <paramref name="message"/> with the corresponding entry of
|
|
/// <paramref name="args"/>, in order of appearance - the same positional mapping
|
|
/// <c>ILogger.LogInformation(message, args)</c> itself performs internally for structured-logging message
|
|
/// templates. <c>string.Format(message, args)</c> (the previous implementation) expects numeric
|
|
/// placeholders ("{0}") instead, throws a <see cref="FormatException"/> on a named one like
|
|
/// "{CorrelationId}", and the broadcast silently fell back to the raw, unsubstituted template - which is
|
|
/// exactly what showed up in the live log console instead of the real value.
|
|
/// </summary>
|
|
private static string FormatLogMessage(string message, object[]? args)
|
|
{
|
|
if (string.IsNullOrEmpty(message) || args == null || args.Length == 0) return message;
|
|
|
|
int argIndex = 0;
|
|
return PlaceholderPattern.Replace(message, match =>
|
|
{
|
|
if (argIndex >= args.Length) return match.Value;
|
|
|
|
var value = args[argIndex++];
|
|
var formatSpec = match.Groups[2].Value; // e.g. ":F2", or "" when the template has no format spec.
|
|
|
|
if (!string.IsNullOrEmpty(formatSpec) && value is IFormattable formattable)
|
|
{
|
|
try
|
|
{
|
|
return formattable.ToString(formatSpec.TrimStart(':'), CultureInfo.InvariantCulture);
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
// Fall through to a plain ToString() rather than losing the value entirely.
|
|
}
|
|
}
|
|
|
|
return value?.ToString() ?? "null";
|
|
});
|
|
}
|
|
|
|
private void DispatchBroadcast(SettingKey<bool> channelKey, LogLevel level, string message, Exception? exception, params object[] args)
|
|
{
|
|
FinlyticLogBroadcaster.Broadcast(new LogMessageDto(
|
|
Timestamp: DateTime.UtcNow,
|
|
ServiceName: ServiceName,
|
|
Channel: channelKey.Name,
|
|
Level: level.ToString(),
|
|
Message: FormatLogMessage(message, args),
|
|
Exception: exception?.ToString()
|
|
));
|
|
}
|
|
|
|
#region Debug
|
|
|
|
public async Task LogDebugAsync(SettingKey<bool> channelKey, string message, params object[] args)
|
|
{
|
|
if (await ShouldLogAsync(channelKey, LogLevel.Debug))
|
|
{
|
|
_logger.LogDebug(message, args);
|
|
DispatchBroadcast(channelKey, LogLevel.Debug, message, null, args);
|
|
}
|
|
}
|
|
|
|
public async Task LogDebugAsync(SettingKey<bool> channelKey, Exception? exception, string message, params object[] args)
|
|
{
|
|
if (await ShouldLogAsync(channelKey, LogLevel.Debug))
|
|
{
|
|
if (exception != null)
|
|
_logger.LogDebug(exception, message, args);
|
|
else
|
|
_logger.LogDebug(message, args);
|
|
DispatchBroadcast(channelKey, LogLevel.Debug, message, exception, args);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Info
|
|
|
|
public async Task LogInfoAsync(SettingKey<bool> channelKey, string message, params object[] args)
|
|
{
|
|
if (await ShouldLogAsync(channelKey, LogLevel.Information))
|
|
{
|
|
_logger.LogInformation(message, args);
|
|
DispatchBroadcast(channelKey, LogLevel.Information, message, null, args);
|
|
}
|
|
}
|
|
|
|
public async Task LogInfoAsync(SettingKey<bool> channelKey, Exception? exception, string message, params object[] args)
|
|
{
|
|
if (await ShouldLogAsync(channelKey, LogLevel.Information))
|
|
{
|
|
if (exception != null)
|
|
_logger.LogInformation(exception, message, args);
|
|
else
|
|
_logger.LogInformation(message, args);
|
|
DispatchBroadcast(channelKey, LogLevel.Information, message, exception, args);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Warning
|
|
|
|
public async Task LogWarningAsync(SettingKey<bool> channelKey, string message, params object[] args)
|
|
{
|
|
if (await ShouldLogAsync(channelKey, LogLevel.Warning))
|
|
{
|
|
_logger.LogWarning(message, args);
|
|
DispatchBroadcast(channelKey, LogLevel.Warning, message, null, args);
|
|
}
|
|
}
|
|
|
|
public async Task LogWarningAsync(SettingKey<bool> channelKey, Exception? exception, string message, params object[] args)
|
|
{
|
|
if (await ShouldLogAsync(channelKey, LogLevel.Warning))
|
|
{
|
|
if (exception != null)
|
|
_logger.LogWarning(exception, message, args);
|
|
else
|
|
_logger.LogWarning(message, args);
|
|
DispatchBroadcast(channelKey, LogLevel.Warning, message, exception, args);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Error
|
|
|
|
public async Task LogErrorAsync(SettingKey<bool> channelKey, string message, params object[] args)
|
|
{
|
|
if (await ShouldLogAsync(channelKey, LogLevel.Error))
|
|
{
|
|
_logger.LogError(message, args);
|
|
DispatchBroadcast(channelKey, LogLevel.Error, message, null, args);
|
|
}
|
|
}
|
|
|
|
public async Task LogErrorAsync(SettingKey<bool> channelKey, Exception? exception, string message, params object[] args)
|
|
{
|
|
if (await ShouldLogAsync(channelKey, LogLevel.Error))
|
|
{
|
|
if (exception != null)
|
|
_logger.LogError(exception, message, args);
|
|
else
|
|
_logger.LogError(message, args);
|
|
DispatchBroadcast(channelKey, LogLevel.Error, message, exception, args);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Trace & Critical
|
|
|
|
public async Task LogTraceAsync(SettingKey<bool> channelKey, string message, params object[] args)
|
|
{
|
|
if (await ShouldLogAsync(channelKey, LogLevel.Trace))
|
|
{
|
|
_logger.LogTrace(message, args);
|
|
DispatchBroadcast(channelKey, LogLevel.Trace, message, null, args);
|
|
}
|
|
}
|
|
|
|
public async Task LogCriticalAsync(SettingKey<bool> channelKey, Exception? exception, string message, params object[] args)
|
|
{
|
|
if (await ShouldLogAsync(channelKey, LogLevel.Critical))
|
|
{
|
|
if (exception != null)
|
|
_logger.LogCritical(exception, message, args);
|
|
else
|
|
_logger.LogCritical(message, args);
|
|
DispatchBroadcast(channelKey, LogLevel.Critical, message, exception, args);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
private async Task<bool> ShouldLogAsync(SettingKey<bool> channelKey, LogLevel level)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(channelKey);
|
|
|
|
try
|
|
{
|
|
return await _settingsService.GetSettingAsync(channelKey);
|
|
}
|
|
catch
|
|
{
|
|
return channelKey.DefaultValue;
|
|
}
|
|
}
|
|
} |