AddQuartz extension does not correctly switch to Microsoft.Extensions.Logging.Abstractions subsystem
See original GitHub issueIssue According to the documentation (https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/configuration-resource-usage-and-scheduler-factory.html#logging), the Quartz logging can be switched to the Microsoft implementation using the following code:
services.AddQuartz(q =>
{
// this automatically registers the Microsoft Logging
});
However, doing so does not seem to do so. When running a .NET core worker service (windows service) using NLog via the Microsoft logging subsystem, all Microsoft logging is traced in the console window and written to the logs. However, the Quartz logs do not appear in the console, but go straight to the NLog configured log and also cannot be filtered by changing the log settings in the appsettings.json file or the NLog config rules. It is almost as if the logging is still using the default logging and going straight to the underlying log.
Example logging configuration:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Quartz": "Warning"
}
}
Version Information
.NET Core 3.1 - Worker Service as Windows Service Quartz.Extensions.DependencyInjection 3.3.2 NLog.Web.AspNetCore 4.13.0
To Reproduce
Create a .NET core 3.1 worker service (windows service) application that wires in Quartz and uses NLog via the Microsoft logging subsystem and dependency injection to resolve all components.
Program.cs file excerpt:
public static void Main(string[] args)
{
// NLog: setup the logger first to catch all errors
var logger = NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
Activity.DefaultIdFormat = ActivityIdFormat.W3C;
Activity.ForceDefaultIdFormat = true;
try
{
var host = CreateHostBuilder(args).Build();
//ConfigureQuartzLogging(host);
host.Run();
}
catch (Exception ex)
{
//NLog: catch setup errors
logger.Error(ex, "Stopped program because of exception");
throw;
}
finally
{
// Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
NLog.LogManager.Shutdown();
}
}
private static IHostBuilder CreateHostBuilder(string[] args)
{
var assembly = Assembly.GetExecutingAssembly();
var productName = assembly.GetName().Name;
return Host.CreateDefaultBuilder(args)
.ConfigureAppConfigurationForEncryptedApplicationSettings(args, "Optomany", productName)
.UseWindowsService()
.ConfigureServices((hostContext, services) =>
{
services.UseStartup(hostContext.Configuration);
services.AddHostedService<Worker>();
}).UseNLog();
}
private static void ConfigureQuartzLogging(IHost host)
{
var loggerFactory = host.Services.GetService<ILoggerFactory>();
if (loggerFactory == null)
{
throw new InvalidOperationException("A logger factory is required to be able to configure the Quartz Logging");
}
Quartz.Logging.LogContext.SetCurrentLogProvider(loggerFactory);
}
startup.cs file excerpt:
/// <summary>
/// The configuration that has been loaded
/// </summary>
/// <value>An IConfiguration value representing the configuration</value>
public IConfiguration Configuration { get; }
/// <summary>
/// Creates a new instance of <see cref="Startup"/>
/// </summary>
/// <param name="configuration">The configuration to use</param>
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
/// <summary>
/// This configures the container with the required services
/// </summary>
/// <param name="services">The services collection to configure</param>-
/// <remarks>This method gets called by the runtime.
/// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940</remarks>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> is <see langword="null"/></exception>
public void ConfigureServices(IServiceCollection services)
{
if(services == null)
throw new ArgumentNullException(nameof(services));
services.AddOptions();
services.AddQuartz(q =>
{
q.UseMicrosoftDependencyInjectionJobFactory();
});
services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>();
services.AddSingleton<IScheduler, Scheduler>();
services.AddTransient<RequiredJob>();
services.AddSingleton<IJobFactory, QuartzJobFactory>();
}
}
Illustrative Scheduler dependency Implementation:
public class Scheduler : IScheduler
{
private readonly ISchedulerFactory _schedulerFactory;
private readonly IJobFactory _jobFactory;
private IScheduler _scheduler;
private readonly QuartzScheduleConfig _quartzSchedule;
private bool _shuttingDown;
public Scheduler(ISchedulerFactory schedulerFactory, IOptions<QuartzScheduleConfig> quartzScheduleConfig, IJobFactory jobFactory)
{
_schedulerFactory = schedulerFactory ?? throw new ArgumentNullException(nameof(schedulerFactory));
if(quartzScheduleConfig == null)
throw new ArgumentNullException(nameof(quartzScheduleConfig));
_quartzSchedule = quartzScheduleConfig.Value ?? throw new ArgumentException("Quartz schedule config is null");
if(string.IsNullOrWhiteSpace(_quartzSchedule.QuartzExpression))
throw new ArgumentException("Quartz expression is invalid");
_jobFactory = jobFactory ?? throw new ArgumentNullException(nameof(jobFactory));
}
public async Task StartAsync()
{
if (_scheduler != null)
return;
_scheduler = await _schedulerFactory.GetScheduler();
_scheduler.JobFactory = _jobFactory;
// start the quartz scheduler based on the schedule set up in config.
var jobDetail = JobBuilder.Create<RequiredJob>()
.WithIdentity("requiredJob")
.Build();
var trigger = TriggerBuilder.Create()
.WithIdentity("requiredTrigger")
.WithCronSchedule(_quartzSchedule.QuartzExpression, mf => mf.WithMisfireHandlingInstructionDoNothing())
.Build();
await _scheduler.ScheduleJob(jobDetail, trigger);
await _scheduler.Start();
}
public async Task ShutdownAsync()
{
// shut down all current jobs
if (_shuttingDown)
return;
_shuttingDown = true;
if(_scheduler != null)
await _scheduler.Shutdown(true);
}
public void Dispose()
{
if(!_shuttingDown)
Task.WaitAll(ShutdownAsync());
}
}
Expected behavior
The automatic wire up via DI should correctly switch the logging to use the Microsoft logging subsystem, if that is what it is documented to do. Alternatively, a clearer and more suitable switching mechanism should be provided that does not rely on the resolution of an internal dependency that we have no visibility as to when it would be resolved.
Additional context The AddQuartz method can only switch the logging if something resolves the MicrosoftLoggingProvider class.
services.TryAddSingleton<MicrosoftLoggingProvider?>(serviceProvider =>
{
var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
if (loggerFactory is null)
{
throw new InvalidOperationException($"{nameof(ILoggerFactory)} service is required");
}
LogContext.SetCurrentLogProvider(loggerFactory);
return LogProvider.CurrentLogProvider as MicrosoftLoggingProvider;
});
Any Quartz component that is resolved before that call occurs risks using incorrect settings because they will receive the default log implementation. For example, the StdSchedulerFactory.cs (https://github.com/quartznet/quartznet/blob/aa53de924f0fa73721c76a8f1fc46f223803bbc0/src/Quartz/Impl/StdSchedulerFactory.cs)
private static readonly ILog log = LogProvider.GetLogger(typeof(StdSchedulerFactory));
will resolve the log implementation when the class is constructed for the first time, which could occur before the logging is switched.
The only approach that we found to reliably work is to manually switch the logging by implementing it against the host object:
private static void ConfigureQuartzLogging(IHost host)
{
var loggerFactory = host.Services.GetService<ILoggerFactory>();
if (loggerFactory == null)
{
throw new InvalidOperationException("A logger factory is required to be able to configure the Quartz Logging");
}
Quartz.Logging.LogContext.SetCurrentLogProvider(loggerFactory);
}
To enable this in the above program.cs excerpt uncomment the following line:
//ConfigureQuartzLogging(host);
Issue Analytics
- State:
- Created 2 years ago
- Comments:7 (4 by maintainers)
Top GitHub Comments
@puschie286 there’s a PR for that, haven’t had the time to do the change, it’s quite big: https://github.com/quartznet/quartznet/pull/1086
@lahma any updates on this ? https://github.com/damianh/LibLog is not developed any more so any plans to switch to Microsoft.Extensions.Logging.Abstractions by default ?