adams85 / filelogger

A lightweight yet feature-rich file logger implementation for the Microsoft.Extensions.Logging framework.
MIT License
147 stars 22 forks source link

Can you use filelogger with XML configuration? #8

Closed deanwiles closed 3 years ago

deanwiles commented 4 years ago

Thank you for sharing your file logger.

I would like to use this in a WinForm app. My business layer uses EF Core, so I have been using Microsoft.Extension.Logging.Console in development and wanted to add file logging.

WinForm apps use the XML-based App.config file instead of appsetting.json, so I was looking to include the logging configuration in App.config.

Configurable settings are normally defined in the VS-generated .Properties.Settings class. I defined an XML node for the logging parameters and loaded them via ConfigurationBuilder().AddXmlStream(stream).Build(). Console logging works OK using those settings, but filelogger raises exceptions when I call CreateLogger().

Do you have any examples or guidance for using XML configuration, particularly within App.config? Below are some snippets for context, but the simple test project is at WFAppLogger if you want to reproduce the exception.

Program.cs:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Windows.Forms;
using WFAppLogger.Properties;

namespace WFAppLogger
{
    internal class Program
    {
        // Global Service Collection for dependency injection
        static IServiceCollection services;

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            // FYI: the roaming configuration that applies to the current user is at
            // %LOCALAPPDATA%\\<Company Name>\<appdomainname>_<eid>_<hash>\<version>\user.config
            //string userConfig = System.Configuration.ConfigurationManager.OpenExeConfiguration(
            //    System.Configuration.ConfigurationUserLevel.PerUserRoamingAndLocal).FilePath;
            // If no user.config file exists, then the application.exe.config file is used
            //string appConfig = System.Configuration.ConfigurationManager.OpenExeConfiguration(
            //    System.Configuration.ConfigurationUserLevel.None).FilePath;

            // Get logging configuration from settings in app.config (or user.config)
            var loggingConfig = Settings.Default.LoggingConfig;
            var stream = new MemoryStream();
            loggingConfig.Save(stream);
            stream.Position = 0;
            var configuration = new ConfigurationBuilder()
                .AddXmlStream(stream)
                .Build();
            var config = configuration.GetSection("Logging");

            // Initialize application logging via dependency injection
            services = new ServiceCollection();
            services.AddLogging(builder =>
            {
                builder.AddConfiguration(config);

                builder.AddConsole();

                builder.AddFile(o => o.RootPath = AppContext.BaseDirectory);
            });

            // Create logger for Program class and log that we're starting up
            var logger = CreateLogger<Program>();
            logger.LogInformation($"Starting {Application.ProductName}...");

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            // Run the application
            Application.Run(new Form1(CreateLogger<Form1>()));

            // Log that we're exiting
            logger.LogInformation($"Exiting {Application.ProductName}.");
        }

        /// <summary>
        /// Creates a new Microsoft.Extensions.Logging.ILogger instance using the full name of the given type
        /// </summary>
        /// <typeparam name="T">The class type to create a logger for</typeparam>
        /// <returns>The Microsoft.Extensions.Logging.ILogger that was created</returns>
        public static ILogger<T> CreateLogger<T>()
        {
            // Create and return Logger instance for the given type using global dependency injection for logger factory
            using (ServiceProvider sp = services.BuildServiceProvider())
            { 
                return sp.GetRequiredService<ILoggerFactory>().CreateLogger<T>();
            }
        }
    }
}

Snippet from App.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <configSections>
        <sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
            <section name="WFAppLogger.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
        </sectionGroup>
    </configSections>
  <!-- some sections omitted for brevity ... !-->
  <userSettings>
    <WFAppLogger.Properties.Settings>
      <setting name="DefaultLogMessage" serializeAs="String">
        <value>Sample log message</value>
      </setting>
      <setting name="LoggingConfig" serializeAs="Xml">
        <value>
          <Root>
            <Logging>
              <LogLevel>
                <Default>Debug</Default>
                <WFAppLogger>Trace</WFAppLogger>
                <Microsoft>Warning</Microsoft>
                <System>Warning</System>
              </LogLevel>
              <Console>
                <IncludeScopes>true</IncludeScopes>
              </Console>
              <File>
                <IncludeScopes>true</IncludeScopes>
                <BasePath>Logs</BasePath>
                <Files>
                  <Path>&lt;appname&gt;-&lt;counter:000&gt;.log</Path>
                  <MaxFileSize>10000</MaxFileSize>
                </Files>
              </File>
            </Logging>
          </Root>
        </value>
      </setting>
    </WFAppLogger.Properties.Settings>
  </userSettings>
</configuration>
deanwiles commented 4 years ago

I discovered that the exception was caused by disposing of the ServiceProvider too soon. Console logger didn't mind that but filelogger did. Now filelogger just fails silently, but debug window repeatedly shows Exception thrown: 'System.ArgumentException' in mscorlib.dll about once every second.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Windows.Forms;
using WFAppLogger.Properties;

namespace WFAppLogger
{
    internal class Program
    {
        // Global Service Provider for dependency injection
        static ServiceProvider serviceProvider;

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            // FYI: the roaming configuration that applies to the current user is at
            // %LOCALAPPDATA%\\<Company Name>\<appdomainname>_<eid>_<hash>\<version>\user.config
            //string userConfig = System.Configuration.ConfigurationManager.OpenExeConfiguration(
            //    System.Configuration.ConfigurationUserLevel.PerUserRoamingAndLocal).FilePath;
            // If no user.config file exists, then the application.exe.config file is used
            //string appConfig = System.Configuration.ConfigurationManager.OpenExeConfiguration(
            //    System.Configuration.ConfigurationUserLevel.None).FilePath;

            // Get logging configuration from settings in app.config (or user.config)
            var loggingConfig = Settings.Default.LoggingConfig;
            var stream = new MemoryStream();
            loggingConfig.Save(stream);
            stream.Position = 0;
            var configuration = new ConfigurationBuilder()
                .AddXmlStream(stream)
                .Build();
            var config = configuration.GetSection("Logging");

            // Initialize application logging via dependency injection
            var services = new ServiceCollection();
            services.AddLogging(builder =>
            {
                builder.AddConfiguration(config);

                builder.AddConsole();

                // Utilize Karambolo.Extensions.Logging.File from https://github.com/adams85/filelogger
                builder.AddFile(o => o.RootPath = AppContext.BaseDirectory);
            });
            serviceProvider = services.BuildServiceProvider();

            // Create logger for Program class and log that we're starting up
            var logger = CreateLogger<Program>();
            logger.LogTrace("This is a trace message.");
            logger.LogDebug("This is a debug message.");
            logger.LogInformation("This is an info message.");
            logger.LogWarning("This is a warning message.");
            logger.LogError("This is an error message.");
            logger.LogCritical("This is a critical message.");
            logger.LogInformation($"Starting {Application.ProductName}...");

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            // Run the application
            Application.Run(new Form1(CreateLogger<Form1>()));

            // Log that we're exiting
            logger.LogInformation($"Exiting {Application.ProductName}.");
        }

        /// <summary>
        /// Creates a new Microsoft.Extensions.Logging.ILogger instance using the full name of the given type
        /// </summary>
        /// <typeparam name="T">The class type to create a logger for</typeparam>
        /// <returns>The Microsoft.Extensions.Logging.ILogger that was created</returns>
        public static ILogger<T> CreateLogger<T>()
        {
            // Create and return Logger instance for the given type using global dependency injection for logger factory
            return serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger<T>();
        }
    }
}
deanwiles commented 4 years ago

After a LOT of debugging and trial & error, I got filelogger working with an XML configuration. Many of the problems could have been located/resolved quicker if there was some validation on the configuration parameters. The working simple test project is at WFAppLogger, with the main code snippets below. Note that I also set RootPath = Path.GetTempPath() since that folder should always be writable.

Program.cs:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Windows.Forms;
using WFAppLogger.Properties;

namespace WFAppLogger
{
    internal class Program
    {
        // Global Service Provider for dependency injection
        static ServiceProvider serviceProvider;

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            // FYI: the roaming configuration that applies to the current user is at
            // %LOCALAPPDATA%\\<Company Name>\<appdomainname>_<eid>_<hash>\<version>\user.config
            //string userConfig = System.Configuration.ConfigurationManager.OpenExeConfiguration(
            //    System.Configuration.ConfigurationUserLevel.PerUserRoamingAndLocal).FilePath;
            // If no user.config file exists, then the application.exe.config file is used
            //string appConfig = System.Configuration.ConfigurationManager.OpenExeConfiguration(
            //    System.Configuration.ConfigurationUserLevel.None).FilePath;

            // Get logging configuration from settings in app.config (or user.config)
            var loggingConfig = Settings.Default.LoggingConfig;
            var stream = new MemoryStream();
            loggingConfig.Save(stream);
            stream.Position = 0;
            var configuration = new ConfigurationBuilder()
                .AddXmlStream(stream)
                .Build();
            var config = configuration.GetSection("Logging");

            // Initialize application logging via dependency injection
            var services = new ServiceCollection();
            services.AddLogging(builder =>
            {
                builder.AddConfiguration(config);

                builder.AddConsole();

                // Utilize Karambolo.Extensions.Logging.File from https://github.com/adams85/filelogger
                builder.AddFile<CustomFileLoggerProvider>(configure: o => o.RootPath = Path.GetTempPath());
            });
            serviceProvider = services.BuildServiceProvider();

            // Create logger for Program class and log that we're starting up
            var logger = CreateLogger<Program>();
            logger.LogInformation($"Starting {Application.ProductName}...");
            logger.LogTrace("This is a trace message.");
            logger.LogDebug("This is a debug message.");
            logger.LogInformation("This is an info message.");
            logger.LogWarning("This is a warning message.");
            logger.LogError("This is an error message.");
            logger.LogCritical("This is a critical message.");

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            // Run the application
            Application.Run(new Form1(CreateLogger<Form1>()));

            // Log that we're exiting
            logger.LogInformation($"Exiting {Application.ProductName}.");
        }

        /// <summary>
        /// Creates a new Microsoft.Extensions.Logging.ILogger instance using the full name of the given type
        /// </summary>
        /// <typeparam name="T">The class type to create a logger for</typeparam>
        /// <returns>The Microsoft.Extensions.Logging.ILogger that was created</returns>
        public static ILogger<T> CreateLogger<T>()
        {
            // Create and return Logger instance for the given type using global dependency injection for logger factory
            return serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger<T>();
        }
    }
}

Snippet from App.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <configSections>
        <sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
            <section name="WFAppLogger.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
        </sectionGroup>
    </configSections>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
    </startup>
    <!-- some sections omitted for brevity ... !-->
  <userSettings>
    <WFAppLogger.Properties.Settings>
      <setting name="DefaultLogMessage" serializeAs="String">
        <value>Sample log message</value>
      </setting>
      <setting name="LoggingConfig" serializeAs="Xml">
        <value>
          <Root>
            <Logging>
              <LogLevel>
                <Default>Debug</Default>
                <WFAppLogger>Trace</WFAppLogger>
                <Microsoft>Warning</Microsoft>
                <System>Warning</System>
              </LogLevel>
              <Console>
                <IncludeScopes>true</IncludeScopes>
              </Console>
              <File>
                <IncludeScopes>true</IncludeScopes>
                <!-- Log files will be written to %TEMP%[\%BasePath%]\<appname>-<date>-<counter>.log -->
                <!--<BasePath>Logs</BasePath>-->
                <Files>
                  <File>
                    <Path>&lt;appname&gt;-&lt;date&gt;-&lt;counter:000&gt;.log</Path>
                    <MaxFileSize>100000</MaxFileSize>
                  </File>
                </Files>
              </File>
            </Logging>
          </Root>
        </value>
      </setting>
    </WFAppLogger.Properties.Settings>
  </userSettings>
</configuration>
adams85 commented 4 years ago

Hi!

Sorry, I just got to review your issue.

Disposing the service provider too early is definitely a problem at your end because the SP disposes the services resolved by it (including the file logger). I recommend this pattern:


// Initialize application logging via dependency injection
services = new ServiceCollection();
services.AddLogging(builder =>
{
    builder.AddConfiguration(config);
    builder.AddConsole();
    builder.AddFile(o => o.RootPath = AppContext.BaseDirectory);
});

using (var sp = services.BuildServiceProvider())
{
    var loggerFactory = sp.GetRequiredService<ILoggerFactory>();

    // Create logger for Program class and log that we're starting up
    var logger = loggerFactory.CreateLogger<Program>();
    logger.LogInformation($"Starting {Application.ProductName}...");

    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    // Run the application
    Application.Run(new Form1(loggerFactory.CreateLogger<Form1>()));

    // Log that we're exiting
    logger.LogInformation($"Exiting {Application.ProductName}.");
}

The other problem is a bit more tricky. When the logger fails to write an entry due to some file system error it keeps retrying until success. (This is the reason behind the repeated exception you got.) The System.ArgumentException is not too helpful for sure but unfortunately, the exception thrown depends on the underlying file system abstraction (IFileAppender, IFileProvider). Of course, the exception details could be logged by the lib IF it wasn't a logger component itself... So it seems there's not much I can do at this point. The log root path could be checked at startup but this would be cumbersome again. The file system abstraction provides no functionality to check permissions so the only option would be creating a dummy file, checking for success and deleting it eventually. And even this procedure would not solve the problem completely because permissions can be tinkered with during the run of the application... In the end, the user's/maintainer's responsibility is to ensure that the log root path is writable. I should definitely point this out in the docs but I'm afraid I cannot do much more. However, I'll give some more thought to the problem.