From 5ed0184bcad4548cc72d9394828bca1e225c80d3 Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Mon, 27 Jun 2022 10:29:04 -0500 Subject: [PATCH] add minimum log level setting (#877) --- CHANGELOG.md | 4 + .../Commands/UpdateGeneralSettings.cs | 5 + .../Commands/UpdateGeneralSettingsHandler.cs | 32 ++++ .../Configuration/GeneralSettingsViewModel.cs | 8 + .../Queries/GetGeneralSettings.cs | 3 + .../Queries/GetGeneralSettingsHandler.cs | 24 +++ ErsatzTV.Core/Domain/ConfigElementKey.cs | 1 + .../Repositories/ConfigElementRepository.cs | 19 +- ErsatzTV/Pages/Settings.razor | 176 +++++++++++------- ErsatzTV/Program.cs | 10 + .../RunOnce/LoadLoggingLevelService.cs | 31 +++ ErsatzTV/Startup.cs | 1 + 12 files changed, 241 insertions(+), 73 deletions(-) create mode 100644 ErsatzTV.Application/Configuration/Commands/UpdateGeneralSettings.cs create mode 100644 ErsatzTV.Application/Configuration/Commands/UpdateGeneralSettingsHandler.cs create mode 100644 ErsatzTV.Application/Configuration/GeneralSettingsViewModel.cs create mode 100644 ErsatzTV.Application/Configuration/Queries/GetGeneralSettings.cs create mode 100644 ErsatzTV.Application/Configuration/Queries/GetGeneralSettingsHandler.cs create mode 100644 ErsatzTV/Services/RunOnce/LoadLoggingLevelService.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 40e0c0b71..c7de1574e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - In previous versions, some libraries would incorrectly display only one item - Properly display old versions of renamed items in trash +### Added +- Add `Minimum Log Level` option to `Settings` page + - Other methods of configuring the log level will no longer work + ## [0.6.2-beta] - 2022-06-18 ### Fixed - Fix content repeating for up to a minute near the top of every hour diff --git a/ErsatzTV.Application/Configuration/Commands/UpdateGeneralSettings.cs b/ErsatzTV.Application/Configuration/Commands/UpdateGeneralSettings.cs new file mode 100644 index 000000000..f4df2c81a --- /dev/null +++ b/ErsatzTV.Application/Configuration/Commands/UpdateGeneralSettings.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.Configuration; + +public record UpdateGeneralSettings(GeneralSettingsViewModel GeneralSettings) : IRequest>; diff --git a/ErsatzTV.Application/Configuration/Commands/UpdateGeneralSettingsHandler.cs b/ErsatzTV.Application/Configuration/Commands/UpdateGeneralSettingsHandler.cs new file mode 100644 index 000000000..fa2887727 --- /dev/null +++ b/ErsatzTV.Application/Configuration/Commands/UpdateGeneralSettingsHandler.cs @@ -0,0 +1,32 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using Serilog.Core; + +namespace ErsatzTV.Application.Configuration; + +public class UpdateGeneralSettingsHandler : IRequestHandler> +{ + private readonly IConfigElementRepository _configElementRepository; + private readonly LoggingLevelSwitch _loggingLevelSwitch; + + public UpdateGeneralSettingsHandler( + LoggingLevelSwitch loggingLevelSwitch, + IConfigElementRepository configElementRepository) + { + _loggingLevelSwitch = loggingLevelSwitch; + _configElementRepository = configElementRepository; + } + + public async Task> Handle( + UpdateGeneralSettings request, + CancellationToken cancellationToken) => await ApplyUpdate(request.GeneralSettings); + + private async Task ApplyUpdate(GeneralSettingsViewModel generalSettings) + { + await _configElementRepository.Upsert(ConfigElementKey.MinimumLogLevel, generalSettings.MinimumLogLevel); + _loggingLevelSwitch.MinimumLevel = generalSettings.MinimumLogLevel; + + return Unit.Default; + } +} diff --git a/ErsatzTV.Application/Configuration/GeneralSettingsViewModel.cs b/ErsatzTV.Application/Configuration/GeneralSettingsViewModel.cs new file mode 100644 index 000000000..27b5e582e --- /dev/null +++ b/ErsatzTV.Application/Configuration/GeneralSettingsViewModel.cs @@ -0,0 +1,8 @@ +using Serilog.Events; + +namespace ErsatzTV.Application.Configuration; + +public class GeneralSettingsViewModel +{ + public LogEventLevel MinimumLogLevel { get; set; } +} diff --git a/ErsatzTV.Application/Configuration/Queries/GetGeneralSettings.cs b/ErsatzTV.Application/Configuration/Queries/GetGeneralSettings.cs new file mode 100644 index 000000000..4cfcfc041 --- /dev/null +++ b/ErsatzTV.Application/Configuration/Queries/GetGeneralSettings.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Configuration; + +public record GetGeneralSettings : IRequest; diff --git a/ErsatzTV.Application/Configuration/Queries/GetGeneralSettingsHandler.cs b/ErsatzTV.Application/Configuration/Queries/GetGeneralSettingsHandler.cs new file mode 100644 index 000000000..6de6d6a49 --- /dev/null +++ b/ErsatzTV.Application/Configuration/Queries/GetGeneralSettingsHandler.cs @@ -0,0 +1,24 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using Serilog.Events; + +namespace ErsatzTV.Application.Configuration; + +public class GetGeneralSettingsHandler : IRequestHandler +{ + private readonly IConfigElementRepository _configElementRepository; + + public GetGeneralSettingsHandler(IConfigElementRepository configElementRepository) => + _configElementRepository = configElementRepository; + + public async Task Handle(GetGeneralSettings request, CancellationToken cancellationToken) + { + Option maybeLogLevel = + await _configElementRepository.GetValue(ConfigElementKey.MinimumLogLevel); + + return new GeneralSettingsViewModel + { + MinimumLogLevel = await maybeLogLevel.IfNoneAsync(LogEventLevel.Information) + }; + } +} diff --git a/ErsatzTV.Core/Domain/ConfigElementKey.cs b/ErsatzTV.Core/Domain/ConfigElementKey.cs index 6387bc027..4f8ee887d 100644 --- a/ErsatzTV.Core/Domain/ConfigElementKey.cs +++ b/ErsatzTV.Core/Domain/ConfigElementKey.cs @@ -6,6 +6,7 @@ public class ConfigElementKey public string Key { get; } + public static ConfigElementKey MinimumLogLevel => new("log.minimum_level"); public static ConfigElementKey FFmpegPath => new("ffmpeg.ffmpeg_path"); public static ConfigElementKey FFprobePath => new("ffmpeg.ffprobe_path"); public static ConfigElementKey FFmpegDefaultProfileId => new("ffmpeg.default_profile_id"); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/ConfigElementRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/ConfigElementRepository.cs index 106750518..7c71e6170 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/ConfigElementRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/ConfigElementRepository.cs @@ -14,7 +14,7 @@ public class ConfigElementRepository : IConfigElementRepository public async Task Upsert(ConfigElementKey configElementKey, T value) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); Option maybeElement = await dbContext.ConfigElements .SelectOneAsync(c => c.Key, c => c.Key == configElementKey.Key); @@ -42,7 +42,7 @@ public class ConfigElementRepository : IConfigElementRepository public async Task> Get(ConfigElementKey key) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.ConfigElements .OrderBy(ce => ce.Key) .SingleOrDefaultAsync(ce => ce.Key == key.Key) @@ -50,18 +50,27 @@ public class ConfigElementRepository : IConfigElementRepository } public Task> GetValue(ConfigElementKey key) => - Get(key).MapT(ce => (T)Convert.ChangeType(ce.Value, typeof(T))); + Get(key).MapT( + ce => + { + if (typeof(T).IsEnum) + { + return (T)Enum.Parse(typeof(T), ce.Value); + } + + return (T)Convert.ChangeType(ce.Value, typeof(T)); + }); public async Task Delete(ConfigElement configElement) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); dbContext.ConfigElements.Remove(configElement); await dbContext.SaveChangesAsync(); } public async Task Delete(ConfigElementKey configElementKey) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); Option maybeExisting = await dbContext.ConfigElements .SelectOneAsync(ce => ce.Key, ce => ce.Key == configElementKey.Key); foreach (ConfigElement element in maybeExisting) diff --git a/ErsatzTV/Pages/Settings.razor b/ErsatzTV/Pages/Settings.razor index facb8f1df..17c5b890f 100644 --- a/ErsatzTV/Pages/Settings.razor +++ b/ErsatzTV/Pages/Settings.razor @@ -6,6 +6,7 @@ @using ErsatzTV.Application.Watermarks @using System.Globalization @using ErsatzTV.Application.Configuration +@using Serilog.Events @using ErsatzTV.Core.Domain.Filler @implements IDisposable @inject IMediator _mediator @@ -13,7 +14,7 @@ @inject ILogger _logger -
+ @@ -100,73 +101,98 @@ Save Settings - - - - HDHomeRun Settings - - - - - - - - - Save Settings - - - - - - Scanner Settings - - - - - - - - - Save Settings - - - - - - Playout Settings - - - - - - - - - - - - - - Save Settings - - -
+ + + + + General Settings + + + + + + Debug + Information + Warning + Error + + + + + Save Settings + + + + + + HDHomeRun Settings + + + + + + + + + Save Settings + + + + + + Scanner Settings + + + + + + + + + Save Settings + + + + + + Playout Settings + + + + + + + + + + + + + + Save Settings + + + +
@code { @@ -184,6 +210,7 @@ private int _tunerCount; private int _libraryRefreshInterval; private PlayoutSettingsViewModel _playoutSettings; + private GeneralSettingsViewModel _generalSettings; public void Dispose() { @@ -207,6 +234,7 @@ _scannerSuccess = _libraryRefreshInterval > 0; _playoutSettings = await _mediator.Send(new GetPlayoutSettings(), _cts.Token); _playoutSuccess = _playoutSettings.DaysToBuild > 0; + _generalSettings = await _mediator.Send(new GetGeneralSettings(), _cts.Token); } private static string ValidatePathExists(string path) => !File.Exists(path) ? "Path does not exist" : null; @@ -274,4 +302,16 @@ Right: _ => _snackbar.Add("Successfully saved playout settings", Severity.Success)); } + private async Task SaveGeneralSettings() + { + Either result = await _mediator.Send(new UpdateGeneralSettings(_generalSettings), _cts.Token); + result.Match( + Left: error => + { + _snackbar.Add(error.Value, Severity.Error); + _logger.LogError("Unexpected error saving general settings: {Error}", error.Value); + }, + Right: _ => _snackbar.Add("Successfully saved general settings", Severity.Success)); + } + } \ No newline at end of file diff --git a/ErsatzTV/Program.cs b/ErsatzTV/Program.cs index 00f2ca002..be718dcff 100644 --- a/ErsatzTV/Program.cs +++ b/ErsatzTV/Program.cs @@ -2,6 +2,8 @@ using System.Diagnostics; using Destructurama; using ErsatzTV.Core; using Serilog; +using Serilog.Core; +using Serilog.Events; namespace ErsatzTV; @@ -29,14 +31,21 @@ public class Program true) .AddEnvironmentVariables() .Build(); + + LoggingLevelSwitch = new LoggingLevelSwitch(); } private static IConfiguration Configuration { get; } + private static LoggingLevelSwitch LoggingLevelSwitch { get; } + public static async Task Main(string[] args) { + LoggingLevelSwitch.MinimumLevel = LogEventLevel.Information; + Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(Configuration) + .MinimumLevel.ControlledBy(LoggingLevelSwitch) .Destructure.UsingAttributes() .Enrich.FromLogContext() .WriteTo.SQLite(FileSystemLayout.LogDatabasePath, retentionPeriod: TimeSpan.FromDays(1)) @@ -61,6 +70,7 @@ public class Program private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) + .ConfigureServices(services => services.AddSingleton(LoggingLevelSwitch)) .ConfigureWebHostDefaults( webBuilder => webBuilder.UseStartup() .UseConfiguration(Configuration) diff --git a/ErsatzTV/Services/RunOnce/LoadLoggingLevelService.cs b/ErsatzTV/Services/RunOnce/LoadLoggingLevelService.cs new file mode 100644 index 000000000..115dc191b --- /dev/null +++ b/ErsatzTV/Services/RunOnce/LoadLoggingLevelService.cs @@ -0,0 +1,31 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using Serilog.Core; +using Serilog.Events; + +namespace ErsatzTV.Services.RunOnce; + +public class LoadLoggingLevelService : IHostedService +{ + private readonly IServiceScopeFactory _serviceScopeFactory; + + public LoadLoggingLevelService(IServiceScopeFactory serviceScopeFactory) => + _serviceScopeFactory = serviceScopeFactory; + + public async Task StartAsync(CancellationToken cancellationToken) + { + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + IConfigElementRepository configElementRepository = + scope.ServiceProvider.GetRequiredService(); + + Option maybeLogLevel = + await configElementRepository.GetValue(ConfigElementKey.MinimumLogLevel); + foreach (LogEventLevel logLevel in maybeLogLevel) + { + LoggingLevelSwitch loggingLevelSwitch = scope.ServiceProvider.GetRequiredService(); + loggingLevelSwitch.MinimumLevel = logLevel; + } + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index b4e1cb30a..ad42d1c83 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -427,6 +427,7 @@ public class Startup // services.AddTransient(typeof(IRequestHandler<,>), typeof(GetRecentLogEntriesHandler<>)); // run-once/blocking startup services + services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); services.AddHostedService();