Closed deanwiles closed 3 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>();
}
}
}
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><appname>-<date>-<counter:000>.log</Path>
<MaxFileSize>100000</MaxFileSize>
</File>
</Files>
</File>
</Logging>
</Root>
</value>
</setting>
</WFAppLogger.Properties.Settings>
</userSettings>
</configuration>
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.
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:
Snippet from App.config