Browse Source

add minimum log level setting (#877)

pull/878/head
Jason Dove 4 years ago committed by GitHub
parent
commit
5ed0184bca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      CHANGELOG.md
  2. 5
      ErsatzTV.Application/Configuration/Commands/UpdateGeneralSettings.cs
  3. 32
      ErsatzTV.Application/Configuration/Commands/UpdateGeneralSettingsHandler.cs
  4. 8
      ErsatzTV.Application/Configuration/GeneralSettingsViewModel.cs
  5. 3
      ErsatzTV.Application/Configuration/Queries/GetGeneralSettings.cs
  6. 24
      ErsatzTV.Application/Configuration/Queries/GetGeneralSettingsHandler.cs
  7. 1
      ErsatzTV.Core/Domain/ConfigElementKey.cs
  8. 19
      ErsatzTV.Infrastructure/Data/Repositories/ConfigElementRepository.cs
  9. 50
      ErsatzTV/Pages/Settings.razor
  10. 10
      ErsatzTV/Program.cs
  11. 31
      ErsatzTV/Services/RunOnce/LoadLoggingLevelService.cs
  12. 1
      ErsatzTV/Startup.cs

4
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 - In previous versions, some libraries would incorrectly display only one item
- Properly display old versions of renamed items in trash - 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 ## [0.6.2-beta] - 2022-06-18
### Fixed ### Fixed
- Fix content repeating for up to a minute near the top of every hour - Fix content repeating for up to a minute near the top of every hour

5
ErsatzTV.Application/Configuration/Commands/UpdateGeneralSettings.cs

@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Configuration;
public record UpdateGeneralSettings(GeneralSettingsViewModel GeneralSettings) : IRequest<Either<BaseError, Unit>>;

32
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<UpdateGeneralSettings, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly LoggingLevelSwitch _loggingLevelSwitch;
public UpdateGeneralSettingsHandler(
LoggingLevelSwitch loggingLevelSwitch,
IConfigElementRepository configElementRepository)
{
_loggingLevelSwitch = loggingLevelSwitch;
_configElementRepository = configElementRepository;
}
public async Task<Either<BaseError, Unit>> Handle(
UpdateGeneralSettings request,
CancellationToken cancellationToken) => await ApplyUpdate(request.GeneralSettings);
private async Task<Unit> ApplyUpdate(GeneralSettingsViewModel generalSettings)
{
await _configElementRepository.Upsert(ConfigElementKey.MinimumLogLevel, generalSettings.MinimumLogLevel);
_loggingLevelSwitch.MinimumLevel = generalSettings.MinimumLogLevel;
return Unit.Default;
}
}

8
ErsatzTV.Application/Configuration/GeneralSettingsViewModel.cs

@ -0,0 +1,8 @@
using Serilog.Events;
namespace ErsatzTV.Application.Configuration;
public class GeneralSettingsViewModel
{
public LogEventLevel MinimumLogLevel { get; set; }
}

3
ErsatzTV.Application/Configuration/Queries/GetGeneralSettings.cs

@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Configuration;
public record GetGeneralSettings : IRequest<GeneralSettingsViewModel>;

24
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<GetGeneralSettings, GeneralSettingsViewModel>
{
private readonly IConfigElementRepository _configElementRepository;
public GetGeneralSettingsHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public async Task<GeneralSettingsViewModel> Handle(GetGeneralSettings request, CancellationToken cancellationToken)
{
Option<LogEventLevel> maybeLogLevel =
await _configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevel);
return new GeneralSettingsViewModel
{
MinimumLogLevel = await maybeLogLevel.IfNoneAsync(LogEventLevel.Information)
};
}
}

1
ErsatzTV.Core/Domain/ConfigElementKey.cs

@ -6,6 +6,7 @@ public class ConfigElementKey
public string Key { get; } public string Key { get; }
public static ConfigElementKey MinimumLogLevel => new("log.minimum_level");
public static ConfigElementKey FFmpegPath => new("ffmpeg.ffmpeg_path"); public static ConfigElementKey FFmpegPath => new("ffmpeg.ffmpeg_path");
public static ConfigElementKey FFprobePath => new("ffmpeg.ffprobe_path"); public static ConfigElementKey FFprobePath => new("ffmpeg.ffprobe_path");
public static ConfigElementKey FFmpegDefaultProfileId => new("ffmpeg.default_profile_id"); public static ConfigElementKey FFmpegDefaultProfileId => new("ffmpeg.default_profile_id");

19
ErsatzTV.Infrastructure/Data/Repositories/ConfigElementRepository.cs

@ -14,7 +14,7 @@ public class ConfigElementRepository : IConfigElementRepository
public async Task<Unit> Upsert<T>(ConfigElementKey configElementKey, T value) public async Task<Unit> Upsert<T>(ConfigElementKey configElementKey, T value)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
Option<ConfigElement> maybeElement = await dbContext.ConfigElements Option<ConfigElement> maybeElement = await dbContext.ConfigElements
.SelectOneAsync(c => c.Key, c => c.Key == configElementKey.Key); .SelectOneAsync(c => c.Key, c => c.Key == configElementKey.Key);
@ -42,7 +42,7 @@ public class ConfigElementRepository : IConfigElementRepository
public async Task<Option<ConfigElement>> Get(ConfigElementKey key) public async Task<Option<ConfigElement>> Get(ConfigElementKey key)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.ConfigElements return await dbContext.ConfigElements
.OrderBy(ce => ce.Key) .OrderBy(ce => ce.Key)
.SingleOrDefaultAsync(ce => ce.Key == key.Key) .SingleOrDefaultAsync(ce => ce.Key == key.Key)
@ -50,18 +50,27 @@ public class ConfigElementRepository : IConfigElementRepository
} }
public Task<Option<T>> GetValue<T>(ConfigElementKey key) => public Task<Option<T>> GetValue<T>(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) public async Task Delete(ConfigElement configElement)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
dbContext.ConfigElements.Remove(configElement); dbContext.ConfigElements.Remove(configElement);
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
} }
public async Task<Unit> Delete(ConfigElementKey configElementKey) public async Task<Unit> Delete(ConfigElementKey configElementKey)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
Option<ConfigElement> maybeExisting = await dbContext.ConfigElements Option<ConfigElement> maybeExisting = await dbContext.ConfigElements
.SelectOneAsync(ce => ce.Key, ce => ce.Key == configElementKey.Key); .SelectOneAsync(ce => ce.Key, ce => ce.Key == configElementKey.Key);
foreach (ConfigElement element in maybeExisting) foreach (ConfigElement element in maybeExisting)

50
ErsatzTV/Pages/Settings.razor

@ -6,6 +6,7 @@
@using ErsatzTV.Application.Watermarks @using ErsatzTV.Application.Watermarks
@using System.Globalization @using System.Globalization
@using ErsatzTV.Application.Configuration @using ErsatzTV.Application.Configuration
@using Serilog.Events
@using ErsatzTV.Core.Domain.Filler @using ErsatzTV.Core.Domain.Filler
@implements IDisposable @implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@ -13,7 +14,7 @@
@inject ILogger<Settings> _logger @inject ILogger<Settings> _logger
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8" Style="display: flex; flex-direction: row"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8" Style="display: flex; flex-direction: row">
<div style="display: flex; flex-direction: row; flex-wrap: wrap"> <MudGrid>
<MudCard Class="mr-6 mb-6" Style="max-width: 400px"> <MudCard Class="mr-6 mb-6" Style="max-width: 400px">
<MudCardHeader> <MudCardHeader>
<CardHeaderContent> <CardHeaderContent>
@ -100,7 +101,31 @@
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_success)" OnClick="@(_ => SaveFFmpegSettings())">Save Settings</MudButton> <MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_success)" OnClick="@(_ => SaveFFmpegSettings())">Save Settings</MudButton>
</MudCardActions> </MudCardActions>
</MudCard> </MudCard>
<MudCard Class="mr-6 mb-auto" Style="width: 350px"> <MudStack Class="mr-6">
<MudCard Class="mb-6" Style="width: 350px">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">General Settings</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudForm>
<MudSelect Class="mt-3"
Label="Minimum Log Level"
@bind-Value="_generalSettings.MinimumLogLevel"
For="@(() => _generalSettings.MinimumLogLevel)">
<MudSelectItem Value="@LogEventLevel.Debug">Debug</MudSelectItem>
<MudSelectItem Value="@LogEventLevel.Information">Information</MudSelectItem>
<MudSelectItem Value="@LogEventLevel.Warning">Warning</MudSelectItem>
<MudSelectItem Value="@LogEventLevel.Error">Error</MudSelectItem>
</MudSelect>
</MudForm>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveGeneralSettings())">Save Settings</MudButton>
</MudCardActions>
</MudCard>
<MudCard Class="mb-6" Style="width: 350px">
<MudCardHeader> <MudCardHeader>
<CardHeaderContent> <CardHeaderContent>
<MudText Typo="Typo.h6">HDHomeRun Settings</MudText> <MudText Typo="Typo.h6">HDHomeRun Settings</MudText>
@ -115,7 +140,7 @@
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_hdhrSuccess)" OnClick="@(_ => SaveHDHRSettings())">Save Settings</MudButton> <MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_hdhrSuccess)" OnClick="@(_ => SaveHDHRSettings())">Save Settings</MudButton>
</MudCardActions> </MudCardActions>
</MudCard> </MudCard>
<MudCard Class="mr-6" Style="width: 350px"> <MudCard Class="mb-6" Style="width: 350px">
<MudCardHeader> <MudCardHeader>
<CardHeaderContent> <CardHeaderContent>
<MudText Typo="Typo.h6">Scanner Settings</MudText> <MudText Typo="Typo.h6">Scanner Settings</MudText>
@ -137,7 +162,7 @@
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_scannerSuccess)" OnClick="@(_ => SaveScannerSettings())">Save Settings</MudButton> <MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_scannerSuccess)" OnClick="@(_ => SaveScannerSettings())">Save Settings</MudButton>
</MudCardActions> </MudCardActions>
</MudCard> </MudCard>
<MudCard Style="width: 350px"> <MudCard Class="mb-6" Style="width: 350px">
<MudCardHeader> <MudCardHeader>
<CardHeaderContent> <CardHeaderContent>
<MudText Typo="Typo.h6">Playout Settings</MudText> <MudText Typo="Typo.h6">Playout Settings</MudText>
@ -166,7 +191,8 @@
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_playoutSuccess)" OnClick="@(_ => SavePlayoutSettings())">Save Settings</MudButton> <MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_playoutSuccess)" OnClick="@(_ => SavePlayoutSettings())">Save Settings</MudButton>
</MudCardActions> </MudCardActions>
</MudCard> </MudCard>
</div> </MudStack>
</MudGrid>
</MudContainer> </MudContainer>
@code { @code {
@ -184,6 +210,7 @@
private int _tunerCount; private int _tunerCount;
private int _libraryRefreshInterval; private int _libraryRefreshInterval;
private PlayoutSettingsViewModel _playoutSettings; private PlayoutSettingsViewModel _playoutSettings;
private GeneralSettingsViewModel _generalSettings;
public void Dispose() public void Dispose()
{ {
@ -207,6 +234,7 @@
_scannerSuccess = _libraryRefreshInterval > 0; _scannerSuccess = _libraryRefreshInterval > 0;
_playoutSettings = await _mediator.Send(new GetPlayoutSettings(), _cts.Token); _playoutSettings = await _mediator.Send(new GetPlayoutSettings(), _cts.Token);
_playoutSuccess = _playoutSettings.DaysToBuild > 0; _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; 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)); Right: _ => _snackbar.Add("Successfully saved playout settings", Severity.Success));
} }
private async Task SaveGeneralSettings()
{
Either<BaseError, Unit> 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));
}
} }

10
ErsatzTV/Program.cs

@ -2,6 +2,8 @@ using System.Diagnostics;
using Destructurama; using Destructurama;
using ErsatzTV.Core; using ErsatzTV.Core;
using Serilog; using Serilog;
using Serilog.Core;
using Serilog.Events;
namespace ErsatzTV; namespace ErsatzTV;
@ -29,14 +31,21 @@ public class Program
true) true)
.AddEnvironmentVariables() .AddEnvironmentVariables()
.Build(); .Build();
LoggingLevelSwitch = new LoggingLevelSwitch();
} }
private static IConfiguration Configuration { get; } private static IConfiguration Configuration { get; }
private static LoggingLevelSwitch LoggingLevelSwitch { get; }
public static async Task<int> Main(string[] args) public static async Task<int> Main(string[] args)
{ {
LoggingLevelSwitch.MinimumLevel = LogEventLevel.Information;
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(Configuration) .ReadFrom.Configuration(Configuration)
.MinimumLevel.ControlledBy(LoggingLevelSwitch)
.Destructure.UsingAttributes() .Destructure.UsingAttributes()
.Enrich.FromLogContext() .Enrich.FromLogContext()
.WriteTo.SQLite(FileSystemLayout.LogDatabasePath, retentionPeriod: TimeSpan.FromDays(1)) .WriteTo.SQLite(FileSystemLayout.LogDatabasePath, retentionPeriod: TimeSpan.FromDays(1))
@ -61,6 +70,7 @@ public class Program
private static IHostBuilder CreateHostBuilder(string[] args) => private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args) Host.CreateDefaultBuilder(args)
.ConfigureServices(services => services.AddSingleton(LoggingLevelSwitch))
.ConfigureWebHostDefaults( .ConfigureWebHostDefaults(
webBuilder => webBuilder.UseStartup<Startup>() webBuilder => webBuilder.UseStartup<Startup>()
.UseConfiguration(Configuration) .UseConfiguration(Configuration)

31
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<IConfigElementRepository>();
Option<LogEventLevel> maybeLogLevel =
await configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevel);
foreach (LogEventLevel logLevel in maybeLogLevel)
{
LoggingLevelSwitch loggingLevelSwitch = scope.ServiceProvider.GetRequiredService<LoggingLevelSwitch>();
loggingLevelSwitch.MinimumLevel = logLevel;
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

1
ErsatzTV/Startup.cs

@ -427,6 +427,7 @@ public class Startup
// services.AddTransient(typeof(IRequestHandler<,>), typeof(GetRecentLogEntriesHandler<>)); // services.AddTransient(typeof(IRequestHandler<,>), typeof(GetRecentLogEntriesHandler<>));
// run-once/blocking startup services // run-once/blocking startup services
services.AddHostedService<LoadLoggingLevelService>();
services.AddHostedService<EndpointValidatorService>(); services.AddHostedService<EndpointValidatorService>();
services.AddHostedService<DatabaseMigratorService>(); services.AddHostedService<DatabaseMigratorService>();
services.AddHostedService<CacheCleanerService>(); services.AddHostedService<CacheCleanerService>();

Loading…
Cancel
Save