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; /// /// Globaler Broadcaster für strukturierte Logs in Echtzeit. /// public static class FinlyticLogBroadcaster { public static Func? 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 } }); } } } /// /// Bietet kanalbasierte, dynamisch steuerbare Logging-Funktionalitäten über den . /// /// Die aufrufende Klasse (für Log-Kategorien). public interface IFinlyticLogger { // --- Debug --- Task LogDebugAsync(SettingKey channelKey, string message, params object[] args); Task LogDebugAsync(SettingKey channelKey, Exception? exception, string message, params object[] args); // --- Info --- Task LogInfoAsync(SettingKey channelKey, string message, params object[] args); Task LogInfoAsync(SettingKey channelKey, Exception? exception, string message, params object[] args); // --- Warning --- Task LogWarningAsync(SettingKey channelKey, string message, params object[] args); Task LogWarningAsync(SettingKey channelKey, Exception? exception, string message, params object[] args); // --- Error --- Task LogErrorAsync(SettingKey channelKey, string message, params object[] args); Task LogErrorAsync(SettingKey channelKey, Exception? exception, string message, params object[] args); // --- Trace & Critical --- Task LogTraceAsync(SettingKey channelKey, string message, params object[] args); Task LogCriticalAsync(SettingKey channelKey, Exception? exception, string message, params object[] args); } /// /// Kanalbasierte Logger-Implementierung, die Einstellungen und Stummschaltungen /// in Echtzeit aus dem bezieht. /// public class FinlyticLogger : IFinlyticLogger { private static readonly string ServiceName = typeof(TContextClass).Assembly.GetName().Name ?? "Finlytic"; private readonly ILogger _logger; private readonly ISettingsService _settingsService; public FinlyticLogger( ILogger 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); /// /// Substitutes every named placeholder in with the corresponding entry of /// , in order of appearance - the same positional mapping /// ILogger.LogInformation(message, args) itself performs internally for structured-logging message /// templates. string.Format(message, args) (the previous implementation) expects numeric /// placeholders ("{0}") instead, throws a 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. /// 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 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 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 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 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 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 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 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 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 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 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 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 ShouldLogAsync(SettingKey channelKey, LogLevel level) { ArgumentNullException.ThrowIfNull(channelKey); try { return await _settingsService.GetSettingAsync(channelKey); } catch { return channelKey.DefaultValue; } } }