question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

AddQuartz extension does not correctly switch to Microsoft.Extensions.Logging.Abstractions subsystem

See original GitHub issue

Issue 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:closed
  • Created 2 years ago
  • Comments:7 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
lahmacommented, Dec 21, 2021

@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

0reactions
puschie286commented, Dec 21, 2021

@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 ?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Could not load file or assembly Microsoft.Extensions. ...
In my case the installed version of Microsoft. ... \NuGetFallbackFolder\microsoft.extensions.logging.abstractions\2.2.0" ... Extensions is:
Read more >
Could not load file or assembly Microsoft.Extensions. ...
I have a Visual Studio 2019 extension that references Microsoft.Extensions.Configuration v3.1.1. This assembly gets loaded by a code ...
Read more >
Windows 10 .Net 6 or .Net 7 runtime getting error Could not ...
Windows 10 .Net 6 or .Net 7 runtime getting error Could not load file or assembly 'Microsoft.Extensions.Logging.Abstractions, Version=7.0.0.0.
Read more >
Microsoft DI Integration | Quartz.NET
You can add Quartz configuration by invoking an extension method AddQuartz on IServiceCollection . The configuration building wraps various ...
Read more >
Using Quartz.NET with ASP.NET Core and worker services
In this post I show how to run Quartz.NET jobs in ASP.NET Core and worker service apps using the Quartz.Extensions.Hosting package.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found