diff options
Diffstat (limited to 'Jellyfin.Server')
| -rw-r--r-- | Jellyfin.Server/CoreAppHost.cs | 7 | ||||
| -rw-r--r-- | Jellyfin.Server/Jellyfin.Server.csproj | 20 | ||||
| -rw-r--r-- | Jellyfin.Server/Migrations/IMigrationRoutine.cs | 28 | ||||
| -rw-r--r-- | Jellyfin.Server/Migrations/MigrationOptions.cs | 24 | ||||
| -rw-r--r-- | Jellyfin.Server/Migrations/MigrationRunner.cs | 73 | ||||
| -rw-r--r-- | Jellyfin.Server/Migrations/MigrationsFactory.cs | 20 | ||||
| -rw-r--r-- | Jellyfin.Server/Migrations/MigrationsListStore.cs | 24 | ||||
| -rw-r--r-- | Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs | 73 | ||||
| -rw-r--r-- | Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs | 35 | ||||
| -rw-r--r-- | Jellyfin.Server/Program.cs | 115 | ||||
| -rw-r--r-- | Jellyfin.Server/Resources/Configuration/logging.json | 15 | ||||
| -rw-r--r-- | Jellyfin.Server/Startup.cs | 1 |
12 files changed, 368 insertions, 67 deletions
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 8b4b61e290..ed5968ad6f 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -23,23 +23,20 @@ namespace Jellyfin.Server /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="imageEncoder">The <see cref="IImageEncoder" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param> - /// <param name="configuration">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param> public CoreAppHost( ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, StartupOptions options, IFileSystem fileSystem, IImageEncoder imageEncoder, - INetworkManager networkManager, - IConfiguration configuration) + INetworkManager networkManager) : base( applicationPaths, loggerFactory, options, fileSystem, imageEncoder, - networkManager, - configuration) + networkManager) { } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index a411121916..bc18f11fde 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -6,9 +6,6 @@ <TargetFramework>netcoreapp3.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> - </PropertyGroup> - - <PropertyGroup> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> </PropertyGroup> @@ -27,10 +24,10 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" /> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> @@ -38,15 +35,16 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="CommandLineParser" Version="2.6.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.0.1" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.1" /> + <PackageReference Include="CommandLineParser" Version="2.7.82" /> + <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.1" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.1" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" /> + <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" /> <PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> - <PackageReference Include="Serilog.Sinks.Graylog" Version="2.1.1" /> + <PackageReference Include="Serilog.Sinks.Graylog" Version="2.1.2" /> <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.2" /> <PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.14" /> </ItemGroup> diff --git a/Jellyfin.Server/Migrations/IMigrationRoutine.cs b/Jellyfin.Server/Migrations/IMigrationRoutine.cs new file mode 100644 index 0000000000..eab995d67e --- /dev/null +++ b/Jellyfin.Server/Migrations/IMigrationRoutine.cs @@ -0,0 +1,28 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations +{ + /// <summary> + /// Interface that describes a migration routine. + /// </summary> + internal interface IMigrationRoutine + { + /// <summary> + /// Gets the unique id for this migration. This should never be modified after the migration has been created. + /// </summary> + public Guid Id { get; } + + /// <summary> + /// Gets the display name of the migration. + /// </summary> + public string Name { get; } + + /// <summary> + /// Execute the migration routine. + /// </summary> + /// <param name="host">Host that hosts current version.</param> + /// <param name="logger">Host logger.</param> + public void Perform(CoreAppHost host, ILogger logger); + } +} diff --git a/Jellyfin.Server/Migrations/MigrationOptions.cs b/Jellyfin.Server/Migrations/MigrationOptions.cs new file mode 100644 index 0000000000..816dd9ee74 --- /dev/null +++ b/Jellyfin.Server/Migrations/MigrationOptions.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace Jellyfin.Server.Migrations +{ + /// <summary> + /// Configuration part that holds all migrations that were applied. + /// </summary> + public class MigrationOptions + { + /// <summary> + /// Initializes a new instance of the <see cref="MigrationOptions"/> class. + /// </summary> + public MigrationOptions() + { + Applied = new List<(Guid Id, string Name)>(); + } + + /// <summary> + /// Gets the list of applied migration routine names. + /// </summary> + public List<(Guid Id, string Name)> Applied { get; } + } +} diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs new file mode 100644 index 0000000000..b5ea04dcac --- /dev/null +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using MediaBrowser.Common.Configuration; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations +{ + /// <summary> + /// The class that knows which migrations to apply and how to apply them. + /// </summary> + public sealed class MigrationRunner + { + /// <summary> + /// The list of known migrations, in order of applicability. + /// </summary> + internal static readonly IMigrationRoutine[] Migrations = + { + new Routines.DisableTranscodingThrottling(), + new Routines.CreateUserLoggingConfigFile() + }; + + /// <summary> + /// Run all needed migrations. + /// </summary> + /// <param name="host">CoreAppHost that hosts current version.</param> + /// <param name="loggerFactory">Factory for making the logger.</param> + public static void Run(CoreAppHost host, ILoggerFactory loggerFactory) + { + var logger = loggerFactory.CreateLogger<MigrationRunner>(); + var migrationOptions = ((IConfigurationManager)host.ServerConfigurationManager).GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey); + + if (!host.ServerConfigurationManager.Configuration.IsStartupWizardCompleted && migrationOptions.Applied.Count == 0) + { + // If startup wizard is not finished, this is a fresh install. + // Don't run any migrations, just mark all of them as applied. + logger.LogInformation("Marking all known migrations as applied because this is a fresh install"); + migrationOptions.Applied.AddRange(Migrations.Select(m => (m.Id, m.Name))); + host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions); + return; + } + + var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet(); + + for (var i = 0; i < Migrations.Length; i++) + { + var migrationRoutine = Migrations[i]; + if (appliedMigrationIds.Contains(migrationRoutine.Id)) + { + logger.LogDebug("Skipping migration '{Name}' since it is already applied", migrationRoutine.Name); + continue; + } + + logger.LogInformation("Applying migration '{Name}'", migrationRoutine.Name); + + try + { + migrationRoutine.Perform(host, logger); + } + catch (Exception ex) + { + logger.LogError(ex, "Could not apply migration '{Name}'", migrationRoutine.Name); + throw; + } + + // Mark the migration as completed + logger.LogInformation("Migration '{Name}' applied successfully", migrationRoutine.Name); + migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name)); + host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions); + logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name); + } + } + } +} diff --git a/Jellyfin.Server/Migrations/MigrationsFactory.cs b/Jellyfin.Server/Migrations/MigrationsFactory.cs new file mode 100644 index 0000000000..23c1b1ee6f --- /dev/null +++ b/Jellyfin.Server/Migrations/MigrationsFactory.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; + +namespace Jellyfin.Server.Migrations +{ + /// <summary> + /// A factory that can find a persistent file of the migration configuration, which lists all applied migrations. + /// </summary> + public class MigrationsFactory : IConfigurationFactory + { + /// <inheritdoc/> + public IEnumerable<ConfigurationStore> GetConfigurations() + { + return new[] + { + new MigrationsListStore() + }; + } + } +} diff --git a/Jellyfin.Server/Migrations/MigrationsListStore.cs b/Jellyfin.Server/Migrations/MigrationsListStore.cs new file mode 100644 index 0000000000..7a1ca66714 --- /dev/null +++ b/Jellyfin.Server/Migrations/MigrationsListStore.cs @@ -0,0 +1,24 @@ +using MediaBrowser.Common.Configuration; + +namespace Jellyfin.Server.Migrations +{ + /// <summary> + /// A configuration that lists all the migration routines that were applied. + /// </summary> + public class MigrationsListStore : ConfigurationStore + { + /// <summary> + /// The name of the configuration in the storage. + /// </summary> + public static readonly string StoreKey = "migrations"; + + /// <summary> + /// Initializes a new instance of the <see cref="MigrationsListStore"/> class. + /// </summary> + public MigrationsListStore() + { + ConfigurationType = typeof(MigrationOptions); + Key = StoreKey; + } + } +} diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs new file mode 100644 index 0000000000..3bc32c0478 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using MediaBrowser.Common.Configuration; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; + +namespace Jellyfin.Server.Migrations.Routines +{ + /// <summary> + /// Migration to initialize the user logging configuration file "logging.user.json". + /// If the deprecated logging.json file exists and has a custom config, it will be used as logging.user.json, + /// otherwise a blank file will be created. + /// </summary> + internal class CreateUserLoggingConfigFile : IMigrationRoutine + { + /// <summary> + /// File history for logging.json as existed during this migration creation. The contents for each has been minified. + /// </summary> + private readonly List<string> _defaultConfigHistory = new List<string> + { + // 9a6c27947353585391e211aa88b925f81e8cd7b9 + @"{""Serilog"":{""MinimumLevel"":{""Default"":""Information"",""Override"":{""Microsoft"":""Warning"",""System"":""Warning""}},""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}", + // 71bdcd730705a714ee208eaad7290b7c68df3885 + @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}", + // a44936f97f8afc2817d3491615a7cfe1e31c251c + @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}""}},{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}""}}]}}", + // 7af3754a11ad5a4284f107997fb5419a010ce6f3 + @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}""}}]}}]}}", + // 60691349a11f541958e0b2247c9abc13cb40c9fb + @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}""}}]}}]}}", + // 65fe243afbcc4b596cf8726708c1965cd34b5f68 + @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] {ThreadId} {SourceContext}: {Message:lj} {NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {ThreadId} {SourceContext}:{Message} {NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}", + // 96c9af590494aa8137d5a061aaf1e68feee60b67 + @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}:{Message}{NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}", + }; + + /// <inheritdoc/> + public Guid Id => Guid.Parse("{EF103419-8451-40D8-9F34-D1A8E93A1679}"); + + /// <inheritdoc/> + public string Name => "CreateLoggingConfigHeirarchy"; + + /// <inheritdoc/> + public void Perform(CoreAppHost host, ILogger logger) + { + var logDirectory = host.Resolve<IApplicationPaths>().ConfigurationDirectoryPath; + var existingConfigPath = Path.Combine(logDirectory, "logging.json"); + + // If the existing logging.json config file is unmodified, then 'reset' it by moving it to 'logging.old.json' + // NOTE: This config file has 'reloadOnChange: true', so this change will take effect immediately even though it has already been loaded + if (File.Exists(existingConfigPath) && ExistingConfigUnmodified(existingConfigPath)) + { + File.Move(existingConfigPath, Path.Combine(logDirectory, "logging.old.json")); + } + } + + /// <summary> + /// Check if the existing logging.json file has not been modified by the user by comparing it to all the + /// versions in our git history. Until now, the file has never been migrated after first creation so users + /// could have any version from the git history. + /// </summary> + /// <exception cref="IOException"><paramref name="oldConfigPath"/> does not exist or could not be read.</exception> + private bool ExistingConfigUnmodified(string oldConfigPath) + { + var existingConfigJson = JToken.Parse(File.ReadAllText(oldConfigPath)); + return _defaultConfigHistory + .Select(historicalConfigText => JToken.Parse(historicalConfigText)) + .Any(historicalConfigJson => JToken.DeepEquals(existingConfigJson, historicalConfigJson)); + } + } +} diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs new file mode 100644 index 0000000000..673f0e4155 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.Routines +{ + /// <summary> + /// Disable transcode throttling for all installations since it is currently broken for certain video formats. + /// </summary> + internal class DisableTranscodingThrottling : IMigrationRoutine + { + /// <inheritdoc/> + public Guid Id => Guid.Parse("{4124C2CD-E939-4FFB-9BE9-9B311C413638}"); + + /// <inheritdoc/> + public string Name => "DisableTranscodingThrottling"; + + /// <inheritdoc/> + public void Perform(CoreAppHost host, ILogger logger) + { + // Set EnableThrottling to false since it wasn't used before and may introduce issues + var encoding = ((IConfigurationManager)host.ServerConfigurationManager).GetConfiguration<EncodingOptions>("encoding"); + if (encoding.EnableThrottling) + { + logger.LogInformation("Disabling transcoding throttling during migration"); + encoding.EnableThrottling = false; + + host.ServerConfigurationManager.SaveConfiguration("encoding", encoding); + } + } + } +} diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 712990a1ee..e9e852349c 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -4,7 +4,6 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; -using System.Net.Security; using System.Reflection; using System.Runtime.InteropServices; using System.Text; @@ -27,6 +26,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Serilog; +using Serilog.Events; using Serilog.Extensions.Logging; using SQLitePCL; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -38,6 +38,16 @@ namespace Jellyfin.Server /// </summary> public static class Program { + /// <summary> + /// The name of logging configuration file containing application defaults. + /// </summary> + public static readonly string LoggingConfigFileDefault = "logging.default.json"; + + /// <summary> + /// The name of the logging configuration file containing the system-specific override settings. + /// </summary> + public static readonly string LoggingConfigFileSystem = "logging.json"; + private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource(); private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory(); private static ILogger _logger = NullLogger.Instance; @@ -102,10 +112,12 @@ namespace Jellyfin.Server // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); - IConfiguration appConfig = await CreateConfiguration(appPaths).ConfigureAwait(false); - - CreateLogger(appConfig, appPaths); + // Create an instance of the application configuration to use for application startup + await InitLoggingConfigFile(appPaths).ConfigureAwait(false); + IConfiguration startupConfig = CreateAppConfiguration(appPaths); + // Initialize logging framework + InitializeLoggingFramework(startupConfig, appPaths); _logger = _loggerFactory.CreateLogger("Main"); // Log uncaught exceptions to the logging instead of std error @@ -169,23 +181,23 @@ namespace Jellyfin.Server _loggerFactory, options, new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths), - new NullImageEncoder(), - new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()), - appConfig); + GetImageEncoder(appPaths), + new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>())); try { ServiceCollection serviceCollection = new ServiceCollection(); - await appHost.InitAsync(serviceCollection).ConfigureAwait(false); + await appHost.InitAsync(serviceCollection, startupConfig).ConfigureAwait(false); - var host = CreateWebHostBuilder(appHost, serviceCollection).Build(); + var webHost = CreateWebHostBuilder(appHost, serviceCollection, appPaths).Build(); // A bit hacky to re-use service provider since ASP.NET doesn't allow a custom service collection. - appHost.ServiceProvider = host.Services; + appHost.ServiceProvider = webHost.Services; appHost.FindParts(); + Migrations.MigrationRunner.Run(appHost, _loggerFactory); try { - await host.StartAsync().ConfigureAwait(false); + await webHost.StartAsync().ConfigureAwait(false); } catch { @@ -193,8 +205,6 @@ namespace Jellyfin.Server throw; } - appHost.ImageProcessor.ImageEncoder = GetImageEncoder(appPaths, appHost.LocalizationManager); - await appHost.RunStartupTasksAsync().ConfigureAwait(false); stopWatch.Stop(); @@ -223,7 +233,7 @@ namespace Jellyfin.Server } } - private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection) + private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection, IApplicationPaths appPaths) { return new WebHostBuilder() .UseKestrel(options => @@ -238,7 +248,7 @@ namespace Jellyfin.Server { foreach (var address in addresses) { - _logger.LogInformation("Kestrel listening on {ipaddr}", address); + _logger.LogInformation("Kestrel listening on {IpAddress}", address); options.Listen(address, appHost.HttpPort); if (appHost.EnableHttps && appHost.Certificate != null) @@ -263,6 +273,8 @@ namespace Jellyfin.Server } } }) + .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(appPaths)) + .UseSerilog() .UseContentRoot(appHost.ContentRoot) .ConfigureServices(services => { @@ -435,39 +447,51 @@ namespace Jellyfin.Server return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir); } - private static async Task<IConfiguration> CreateConfiguration(IApplicationPaths appPaths) + /// <summary> + /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist + /// already. + /// </summary> + private static async Task InitLoggingConfigFile(IApplicationPaths appPaths) { - const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json"; - string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, "logging.json"); - - if (!File.Exists(configPath)) + // Do nothing if the config file already exists + string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, LoggingConfigFileDefault); + if (File.Exists(configPath)) { - // For some reason the csproj name is used instead of the assembly name - using (Stream? resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)) - { - if (resource == null) - { - throw new InvalidOperationException( - string.Format( - CultureInfo.InvariantCulture, - "Invalid resource path: '{0}'", - ResourcePath)); - } - - using Stream dst = File.Open(configPath, FileMode.CreateNew); - await resource.CopyToAsync(dst).ConfigureAwait(false); - } + return; } + // Get a stream of the resource contents + // NOTE: The .csproj name is used instead of the assembly name in the resource path + const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json"; + await using Stream? resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath) + ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'"); + + // Copy the resource contents to the expected file path for the config file + await using Stream dst = File.Open(configPath, FileMode.CreateNew); + await resource.CopyToAsync(dst).ConfigureAwait(false); + } + + private static IConfiguration CreateAppConfiguration(IApplicationPaths appPaths) + { return new ConfigurationBuilder() + .ConfigureAppConfiguration(appPaths) + .Build(); + } + + private static IConfigurationBuilder ConfigureAppConfiguration(this IConfigurationBuilder config, IApplicationPaths appPaths) + { + return config .SetBasePath(appPaths.ConfigurationDirectoryPath) .AddInMemoryCollection(ConfigurationOptions.Configuration) - .AddJsonFile("logging.json", false, true) - .AddEnvironmentVariables("JELLYFIN_") - .Build(); + .AddJsonFile(LoggingConfigFileDefault, optional: false, reloadOnChange: true) + .AddJsonFile(LoggingConfigFileSystem, optional: true, reloadOnChange: true) + .AddEnvironmentVariables("JELLYFIN_"); } - private static void CreateLogger(IConfiguration configuration, IApplicationPaths appPaths) + /// <summary> + /// Initialize Serilog using configuration and fall back to defaults on failure. + /// </summary> + private static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths) { try { @@ -475,26 +499,26 @@ namespace Jellyfin.Server Serilog.Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(configuration) .Enrich.FromLogContext() + .Enrich.WithThreadId() .CreateLogger(); } catch (Exception ex) { Serilog.Log.Logger = new LoggerConfiguration() - .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}") + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}") .WriteTo.Async(x => x.File( Path.Combine(appPaths.LogDirectoryPath, "log_.log"), rollingInterval: RollingInterval.Day, - outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}")) + outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}")) .Enrich.FromLogContext() + .Enrich.WithThreadId() .CreateLogger(); Serilog.Log.Logger.Fatal(ex, "Failed to create/read logger configuration"); } } - private static IImageEncoder GetImageEncoder( - IApplicationPaths appPaths, - ILocalizationManager localizationManager) + private static IImageEncoder GetImageEncoder(IApplicationPaths appPaths) { try { @@ -503,8 +527,7 @@ namespace Jellyfin.Server return new SkiaEncoder( _loggerFactory.CreateLogger<SkiaEncoder>(), - appPaths, - localizationManager); + appPaths); } catch (Exception ex) { diff --git a/Jellyfin.Server/Resources/Configuration/logging.json b/Jellyfin.Server/Resources/Configuration/logging.json index e85ef05afd..f64a85219d 100644 --- a/Jellyfin.Server/Resources/Configuration/logging.json +++ b/Jellyfin.Server/Resources/Configuration/logging.json @@ -1,11 +1,17 @@ { "Serilog": { - "MinimumLevel": "Information", + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, "WriteTo": [ { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}" + "outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}" } }, { @@ -20,12 +26,13 @@ "retainedFileCountLimit": 3, "rollOnFileSizeLimit": true, "fileSizeLimitBytes": 100000000, - "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}" + "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}" } } ] } } - ] + ], + "Enrich": [ "FromLogContext", "WithThreadId" ] } } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 3ee5fb8b50..4d7d56e9d4 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -3,7 +3,6 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; |
