diff --git a/code/Common/Configuration/General/GeneralConfig.cs b/code/Common/Configuration/General/GeneralConfig.cs index defce52e..a64d9432 100644 --- a/code/Common/Configuration/General/GeneralConfig.cs +++ b/code/Common/Configuration/General/GeneralConfig.cs @@ -1,6 +1,7 @@ -using Common.Enums; +using Common.Enums; using Common.Exceptions; using Newtonsoft.Json; +using Serilog.Events; namespace Common.Configuration.General; @@ -18,6 +19,8 @@ public sealed record GeneralConfig : IConfig public bool SearchEnabled { get; init; } = true; public ushort SearchDelay { get; init; } = 30; + + public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information; public void Validate() { diff --git a/code/Common/Configuration/Logging/LoggingConfig.cs b/code/Common/Configuration/Logging/LoggingConfig.cs deleted file mode 100644 index 38c6a56b..00000000 --- a/code/Common/Configuration/Logging/LoggingConfig.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Serilog.Events; - -namespace Common.Configuration.Logging; - -public class LoggingConfig : IConfig -{ - public const string SectionName = "Logging"; - - public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information; - - public bool Enhanced { get; set; } - - public FileLogConfig? File { get; set; } - - public SignalRLogConfig? SignalR { get; set; } - - public void Validate() - { - } -} \ No newline at end of file diff --git a/code/Executable/Controllers/LoggingController.cs b/code/Executable/Controllers/LoggingController.cs new file mode 100644 index 00000000..06ff8b79 --- /dev/null +++ b/code/Executable/Controllers/LoggingController.cs @@ -0,0 +1,76 @@ +using Infrastructure.Logging; +using Microsoft.AspNetCore.Mvc; +using Serilog.Events; + +namespace Executable.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class LoggingController : ControllerBase +{ + private readonly LoggingConfigManager _loggingConfigManager; + private readonly ILogger _logger; + + public LoggingController(LoggingConfigManager loggingConfigManager, ILogger logger) + { + _loggingConfigManager = loggingConfigManager; + _logger = logger; + } + + /// + /// Gets the current global log level + /// + /// Current log level + [HttpGet("level")] + public IActionResult GetLogLevel() + { + return Ok(new { level = _loggingConfigManager.GetLogLevel().ToString() }); + } + + /// + /// Sets the global log level + /// + /// Log level request containing the new level + /// Result with the new log level + [HttpPut("level")] + public async Task SetLogLevel([FromBody] LogLevelRequest request) + { + if (!Enum.TryParse(request.Level, true, out var logLevel)) + { + return BadRequest(new + { + error = "Invalid log level", + validLevels = Enum.GetNames() + }); + } + + await _loggingConfigManager.SetLogLevel(logLevel); + + // Log at the new level to confirm it's working + _logger.WithCategory(LoggingCategoryConstants.System) + .LogInformation("Log level changed to {Level}", logLevel); + + return Ok(new { level = logLevel.ToString() }); + } + + /// + /// Get a list of valid log levels + /// + /// All valid log level values + [HttpGet("levels")] + public IActionResult GetValidLogLevels() + { + return Ok(new + { + levels = Enum.GetNames() + }); + } +} + +/// +/// Request model for changing log level +/// +public class LogLevelRequest +{ + public string Level { get; set; } +} diff --git a/code/Executable/DependencyInjection/LoggingDI.cs b/code/Executable/DependencyInjection/LoggingDI.cs index ec586050..f7b4dc06 100644 --- a/code/Executable/DependencyInjection/LoggingDI.cs +++ b/code/Executable/DependencyInjection/LoggingDI.cs @@ -1,3 +1,4 @@ +using Common.Configuration.General; using Common.Configuration.Logging; using Domain.Enums; using Infrastructure.Configuration; @@ -5,6 +6,7 @@ using Infrastructure.Logging; using Infrastructure.Verticals.ContentBlocker; using Infrastructure.Verticals.DownloadCleaner; using Infrastructure.Verticals.QueueCleaner; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.AspNetCore.SignalR; using Serilog; using Serilog.Core; @@ -16,13 +18,25 @@ namespace Executable.DependencyInjection; public static class LoggingDI { - public static ILoggingBuilder AddLogging(this ILoggingBuilder builder, IConfiguration configuration, IServiceProvider serviceProvider) + public static ILoggingBuilder AddLogging(this ILoggingBuilder builder, IServiceProvider serviceProvider) { - // Get the logging configuration - LoggingConfig? config = configuration.GetSection(LoggingConfig.SectionName).Get(); - + // Register LoggingConfigManager as a singleton + serviceProvider.GetRequiredService() + .TryAddSingleton(); + + // Get LoggingConfigManager (will be created if not already registered) + var configManager = serviceProvider.GetRequiredService(); + + + // Get the dynamic level switch for controlling log levels + var levelSwitch = configManager.GetLevelSwitch(); + // Get the configuration path provider var pathProvider = serviceProvider.GetRequiredService(); + + // Get logging config from the config manager + var config = serviceProvider.GetRequiredService() + .GetConfiguration().Logging; // Create the logs directory string logsPath = Path.Combine(pathProvider.GetConfigPath(), "logs"); @@ -58,8 +72,7 @@ public static class LoggingDI List instanceNames = [InstanceType.Sonarr.ToString(), InstanceType.Radarr.ToString(), InstanceType.Lidarr.ToString()]; int arrPadding = instanceNames.Max(x => x.Length) + 2; - // Set the minimum log level - LogEventLevel level = config?.LogLevel ?? LogEventLevel.Information; + // Log level is controlled by the LoggingConfigManager's level switch // Apply padding values to templates string consoleTemplate = consoleOutputTemplate @@ -72,9 +85,9 @@ public static class LoggingDI .Replace("JOB_PAD", jobPadding.ToString()) .Replace("ARR_PAD", arrPadding.ToString()); - // Configure base logger + // Configure base logger with dynamic level control logConfig - .MinimumLevel.Is(level) + .MinimumLevel.ControlledBy(levelSwitch) .Enrich.FromLogContext() .WriteTo.Console(new ExpressionTemplate(consoleTemplate, theme: TemplateTheme.Literate)); diff --git a/code/Executable/Program.cs b/code/Executable/Program.cs index 04c270b1..28dc37d3 100644 --- a/code/Executable/Program.cs +++ b/code/Executable/Program.cs @@ -8,7 +8,12 @@ builder.Services .AddInfrastructure(builder.Configuration) .AddApiServices(); -builder.Logging.AddLogging(builder.Configuration, builder.Services.BuildServiceProvider()); +// Register services needed for logging first +builder.Services.AddSingleton(); + +// Add logging with proper service provider +var serviceProvider = builder.Services.BuildServiceProvider(); +await builder.Logging.AddLogging(serviceProvider); var app = builder.Build(); diff --git a/code/Infrastructure/Logging/LoggingConfigManager.cs b/code/Infrastructure/Logging/LoggingConfigManager.cs new file mode 100644 index 00000000..e73eb27e --- /dev/null +++ b/code/Infrastructure/Logging/LoggingConfigManager.cs @@ -0,0 +1,83 @@ +using Common.Configuration.General; +using Infrastructure.Configuration; +using Microsoft.Extensions.Logging; +using Serilog.Core; +using Serilog.Events; + +namespace Infrastructure.Logging; + +/// +/// Manages logging configuration and provides dynamic log level control +/// +public class LoggingConfigManager +{ + private readonly ConfigManager _configManager; + private readonly LoggingLevelSwitch _levelSwitch; + private readonly ILogger _logger; + + public LoggingConfigManager(ConfigManager configManager, ILogger logger) + { + _configManager = configManager; + _logger = logger; + + // Initialize with default level + _levelSwitch = new LoggingLevelSwitch(); + + // Load settings from configuration + LoadConfiguration(); + } + + /// + /// Gets the level switch used to dynamically control log levels + /// + public LoggingLevelSwitch GetLevelSwitch() => _levelSwitch; + + /// + /// Updates the global log level and persists the change to configuration + /// + /// The new log level + public async Task SetLogLevel(LogEventLevel level) + { + // Change the level in the switch + _levelSwitch.MinimumLevel = level; + + _logger.LogInformation("Setting global log level to {level}", level); + + // Update configuration + try + { + var config = await _configManager.GetConfigurationAsync(); + + config.LogLevel = level; + + // TODO use SetProp + await _configManager.SaveConfigurationAsync(config); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update log level in configuration"); + } + } + + /// + /// Gets the current global log level + /// + public LogEventLevel GetLogLevel() => _levelSwitch.MinimumLevel; + + /// + /// Loads logging settings from configuration + /// + private void LoadConfiguration() + { + try + { + var config = _configManager.GetConfiguration(); + _levelSwitch.MinimumLevel = config.LogLevel; + } + catch (Exception ex) + { + // Just log and continue with defaults + _logger.LogError(ex, "Failed to load logging configuration, using defaults"); + } + } +} diff --git a/code/Infrastructure/Logging/LoggingExtensions.cs b/code/Infrastructure/Logging/LoggingExtensions.cs index 0e40b0ff..25e8f362 100644 --- a/code/Infrastructure/Logging/LoggingExtensions.cs +++ b/code/Infrastructure/Logging/LoggingExtensions.cs @@ -17,7 +17,8 @@ public static class LoggingExtensions /// Enriched logger instance public static ILogger WithCategory(this ILogger logger, string category) { - return logger.BeginScope(new Dictionary { ["Category"] = category }); + logger.BeginScope(new Dictionary { ["Category"] = category }); + return logger; } /// @@ -28,7 +29,8 @@ public static class LoggingExtensions /// Enriched logger instance public static ILogger WithJob(this ILogger logger, string jobName) { - return logger.BeginScope(new Dictionary { ["JobName"] = jobName }); + logger.BeginScope(new Dictionary { ["JobName"] = jobName }); + return logger; } /// @@ -39,7 +41,8 @@ public static class LoggingExtensions /// Enriched logger instance public static ILogger WithInstance(this ILogger logger, string instanceName) { - return logger.BeginScope(new Dictionary { ["InstanceName"] = instanceName }); + logger.BeginScope(new Dictionary { ["InstanceName"] = instanceName }); + return logger; } /// diff --git a/code/Infrastructure/Logging/LoggingInitializer.cs b/code/Infrastructure/Logging/LoggingInitializer.cs index d2cc70cb..efa7049c 100644 --- a/code/Infrastructure/Logging/LoggingInitializer.cs +++ b/code/Infrastructure/Logging/LoggingInitializer.cs @@ -1,7 +1,6 @@ -using Microsoft.Extensions.DependencyInjection; +using System.Collections; using Microsoft.Extensions.Hosting; using Serilog; -using Serilog.Core; namespace Infrastructure.Logging; @@ -20,26 +19,21 @@ public class LoggingInitializer : BackgroundService protected override Task ExecuteAsync(CancellationToken stoppingToken) { // Find and initialize any deferred sinks - var deferredSink = Log.Logger as ILoggerPropertyEnricher; - - if (deferredSink != null) - { - var sinks = deferredSink.GetType() + var deferredSink = Log.Logger; + + if (deferredSink.GetType() .GetProperty("Sinks", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.GetValue(deferredSink) as System.Collections.IEnumerable; - - if (sinks != null) + ?.GetValue(deferredSink) is IEnumerable sinks) + { + foreach (var sink in sinks) { - foreach (var sink in sinks) + if (sink is DeferredSignalRSink deferredSignalRSink) { - if (sink is DeferredSignalRSink deferredSignalRSink) - { - deferredSignalRSink.Initialize(_serviceProvider); - } + deferredSignalRSink.Initialize(_serviceProvider); } } } - + // We only need to run this once at startup return Task.CompletedTask; }