using System.IO.Compression;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Models;
using Cleanuparr.Persistence;
using Cleanuparr.Persistence.Models.Configuration.General;
using Cleanuparr.Shared.Helpers;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using Serilog.Templates;
using Serilog.Templates.Themes;
namespace Cleanuparr.Infrastructure.Logging;
///
/// Manages logging configuration and provides dynamic log level control
///
public static class LoggingConfigManager
{
///
/// The level switch used to dynamically control log levels
///
public static LoggingLevelSwitch LevelSwitch { get; } = new();
///
/// Creates a logger configuration for startup before DI is available
///
/// Configured LoggerConfiguration
public static LoggerConfiguration CreateLoggerConfiguration()
{
using var context = DataContext.CreateStaticInstance();
var config = context.GeneralConfigs.AsNoTracking().First();
SetLogLevel(config.Log.Level);
const string categoryTemplate = "{#if Category is not null} {Concat('[',Category,']'),CAT_PAD}{#end}";
const string jobNameTemplate = "{#if JobName is not null} {Concat('[',JobName,']'),JOB_PAD}{#end}";
const string consoleOutputTemplate = $"[{{@t:yyyy-MM-dd HH:mm:ss.fff}} {{@l:u3}}]{jobNameTemplate}{categoryTemplate} {{@m}}\n{{@x}}";
const string fileOutputTemplate = $"{{@t:yyyy-MM-dd HH:mm:ss.fff zzz}} [{{@l:u3}}]{jobNameTemplate}{categoryTemplate} {{@m:lj}}\n{{@x}}";
// Determine job name padding
List jobNames = [nameof(JobType.QueueCleaner), nameof(JobType.MalwareBlocker), nameof(JobType.DownloadCleaner)];
int jobPadding = jobNames.Max(x => x.Length) + 2;
// Determine instance name padding
List categoryNames = [
InstanceType.Sonarr.ToString(),
InstanceType.Radarr.ToString(),
InstanceType.Lidarr.ToString(),
InstanceType.Readarr.ToString(),
InstanceType.Whisparr.ToString(),
"SYSTEM"
];
int catPadding = categoryNames.Max(x => x.Length) + 2;
// Apply padding values to templates
string consoleTemplate = consoleOutputTemplate
.Replace("JOB_PAD", jobPadding.ToString())
.Replace("CAT_PAD", catPadding.ToString());
string fileTemplate = fileOutputTemplate
.Replace("JOB_PAD", jobPadding.ToString())
.Replace("CAT_PAD", catPadding.ToString());
LoggerConfiguration logConfig = new LoggerConfiguration()
.MinimumLevel.ControlledBy(LevelSwitch)
.Enrich.FromLogContext()
.WriteTo.Console(new ExpressionTemplate(consoleTemplate, theme: TemplateTheme.Literate));
// Create the logs directory
string logsPath = Path.Combine(ConfigurationPathProvider.GetConfigPath(), "logs");
if (!Directory.Exists(logsPath))
{
try
{
Directory.CreateDirectory(logsPath);
}
catch (Exception exception)
{
throw new Exception($"Failed to create logs directory | {logsPath}", exception);
}
}
ArchiveHooks? archiveHooks = config.Log.ArchiveEnabled
? new ArchiveHooks(
retainedFileCountLimit: config.Log.ArchiveRetainedCount,
retainedFileTimeLimit: config.Log.ArchiveTimeLimitHours > 0 ? TimeSpan.FromHours(config.Log.ArchiveTimeLimitHours) : null,
compressionLevel: CompressionLevel.SmallestSize
)
: null;
// Add file sink with archive hooks
logConfig.WriteTo.File(
path: Path.Combine(logsPath, "cleanuparr-.txt"),
formatter: new ExpressionTemplate(fileTemplate),
fileSizeLimitBytes: config.Log.RollingSizeMB is 0 ? null : config.Log.RollingSizeMB * 1024L * 1024L,
rollingInterval: RollingInterval.Day,
rollOnFileSizeLimit: config.Log.RollingSizeMB > 0,
retainedFileCountLimit: config.Log.RetainedFileCount is 0 ? null : config.Log.RetainedFileCount,
retainedFileTimeLimit: config.Log.TimeLimitHours is 0 ? null : TimeSpan.FromHours(config.Log.TimeLimitHours),
hooks: archiveHooks
);
// Add SignalR sink for real-time log updates
logConfig.WriteTo.Sink(SignalRLogSink.Instance);
// Apply standard overrides
logConfig
.MinimumLevel.Override("MassTransit", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Quartz", LogEventLevel.Warning)
.MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Error)
.Enrich.WithProperty("ApplicationName", "Cleanuparr");
return logConfig;
}
///
/// Updates the global log level and persists the change to configuration
///
/// The new log level
public static void SetLogLevel(LogEventLevel level)
{
// Change the level in the switch
LevelSwitch.MinimumLevel = level;
}
///
/// Reconfigures the entire logging system with new settings
///
/// The new general configuration
public static void ReconfigureLogging(GeneralConfig config)
{
try
{
// Create new logger configuration
var newLoggerConfig = CreateLoggerConfiguration();
// Apply the new configuration to the global logger
Log.Logger = newLoggerConfig.CreateLogger();
// Update the level switch with the new level
LevelSwitch.MinimumLevel = config.Log.Level;
}
catch (Exception ex)
{
// Log the error but don't throw to avoid breaking the application
Log.Error(ex, "Failed to reconfigure logger");
}
}
}