Search code examples
c#.net-coreconsoleilogger

C# Console App, pass multiple parameters to custom ILogger


Searching I've not found the answer I'm looking for. I will only put code snippets here to help ask my question, supplying ALL of the code would not be executable without the entire system.

I'm using the built-in logging in C# to log to Windows Event Viewer and/or the Console. I also wanted to write to a file but not third-party logging, so I wrote my own simple logger that logs the same data to files and works.

My appsettings.json as a second for some configuration parameters like a working folder. I also have a folder path for the logger. What I would like to be able to do is use the same holder path from the setting and not have 2.

appsettings.json

{
  "AppSettings": {
    "WorkingFolderPath": "C:\\mypath\\",
    "HeartbeatIntervalMinutes": 0, // This is seconds when in debug mode.
    "FileRetryDelayMinutes": 1,
    "PingTimeoutMilliseconds": 200
  },
  "Logging": {
    "LogLevel": {
      "Default": "Error",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Warning"
    },
    "Debug": {
      "LogLevel": {
        "Default": "Debug"
      }
    },
    "Console": {
      "IncludeScopes": true,
      "LogLevel": {
        "Default": "Debug"
      }
    },
    "EventLog": {
      "LogLevel": {
        "Default": "Warning"
      }
    },
    "FileLog": {
      "Options": {
        "FolderPath": "C:\\mypath\\",
        "RetentionDays": 5
      },
      "LogLevel": {
        "Default": "Debug"
      }
    }
  }
}

As you see in this file I have a "WorkingFolderPath" setting I'd like to use that as my path in my "FileLog" logger and not have to specify a path there as well.

Program.cs

public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
            .UseWindowsService()
            .ConfigureLogging((context, logging) =>
            {
                logging.ClearProviders();
                logging.AddConsole();
                logging.AddDebug();
                logging.AddEventLog(new EventLogSettings()
                {
                    SourceName = "FreshIQAppMessagingService"
                });
                logging.AddFileLogger(options =>
                {
                    context.Configuration.GetSection("Logging").GetSection("FileLog").GetSection("Options").Bind(options);
                });
            })
            .ConfigureAppConfiguration((hostContext, config) =>
            {
                config
                    .SetBasePath(ApplicationPath)
                    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                    .AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: true);

                config.AddEnvironmentVariables();
            })
            .ConfigureServices((hostContext, services) =>
            {
                services.Configure<Configuration>(hostContext.Configuration.GetSection("AppSettings"));
                services.AddHostedService<Service1>();
                services.AddHostedService<Service2>();
                services.AddHostedService<Service3>();
                services.AddHostedService<Service4>();
            });

Here it's getting the options and passing them in. What I've not been able to figure out is how to change my classes so that I can send in the "WorkingFolderPath" from the top of the appsettings instead of the one with the options with the FileLog logger.

logging.AddFileLogger(options =>
                    {
                        context.Configuration.GetSection("Logging").GetSection("FileLog").GetSection("Options").Bind(options);
                    });

FileLoggerExtensions.cs

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;

namespace FreshIQAppMessaging.Logging
{
    public static class FileLoggerExtensions
    {
        public static ILoggingBuilder AddFileLogger(this ILoggingBuilder loggingBuilder, Action<FileLoggerOptions> configure)
        {
            loggingBuilder.Services.AddSingleton<ILoggerProvider, FileLoggerProvider>();
            loggingBuilder.Services.Configure(configure);

            return loggingBuilder;
        }
    }
}

FileLoggerProvider.cs

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.IO;

namespace FreshIQAppMessaging.Logging
{
    [ProviderAlias("FileLog")]
    public class FileLoggerProvider : ILoggerProvider
    {
        public readonly FileLoggerOptions Options;
        public FileLoggerProvider(IOptions<FileLoggerOptions> options)
        {
            Options = options.Value;

            if (!Directory.Exists(Options.FolderPath))
            {
                Directory.CreateDirectory(Options.FolderPath);
            }
        }

        public ILogger CreateLogger(string categoryName)
        {
            return new FileLogger(this);
        }

        public void Dispose() { }
    }
}

FileLogger.cs

using Microsoft.Extensions.Logging;
using System;
using System.Globalization;
using System.IO;

namespace FreshIQAppMessaging.Logging
{
    public class FileLogger : ILogger
    {
        protected readonly FileLoggerProvider _fileLoggerProvider;

        public FileLogger(FileLoggerProvider fileLoggerProvider)
        {
            _fileLoggerProvider = fileLoggerProvider;
        }

        public IDisposable? BeginScope<TState>(TState state)
        {
            return null;
        }

        public bool IsEnabled(LogLevel logLevel)
        {
            return logLevel != LogLevel.None;
        }

        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }

            // Clean up old log files
            var logFiles = Directory.GetFiles(_fileLoggerProvider.Options.FolderPath, "*-MyApp.log");
            foreach (var logFilePath in logFiles)
            {
                var logFileName = new FileInfo(logFilePath).Name;
                if (DateTime.TryParseExact(logFileName.Substring(0, 8), "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var logFileDate))
                {
                    if (logFileDate.AddDays(_fileLoggerProvider.Options.RetentionDays) < DateTime.Now)
                    {
                        File.Delete(logFilePath);
                    }
                }
            }

            var fullFilePath = $"{_fileLoggerProvider.Options.FolderPath}\\{DateTime.Now.ToString("yyyyMMdd")}-MyApp.log";
            var logRecord = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} [{logLevel.ToString()}] {formatter(state, exception)} {(exception != null ? exception.StackTrace : "")}";

            using (var streamWriter = new StreamWriter(fullFilePath, true))
            {
                streamWriter.WriteLine(logRecord);
            }
        }
    }
}

FileLoggerOptions.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace FreshIQAppMessaging.Logging
{
    public  class FileLoggerOptions
    {
        public virtual string? FolderPath { get; set; }
        public virtual int RetentionDays { get; set; } = 5;
    }
}

Solution

  • You typically don't want to include custom options for your logger inside the Logging section. Most people do something like this instead:

    {
      "AppSettings": {
        "WorkingFolderPath": "C:\\mypath\\",
        ...
      },
      "FileLog": {
        "FolderPath": "C:\\mypath\\",
        "RetentionDays": 5
      },
      "Logging": {
        ...
        "FileLog": {
          "LogLevel": {
            "Default": "Debug"
          }
        }
      }
    }
    

    You didn't share your options class but I assume you have a property on it named FolderPath. You could then configure your file logger like this:

    logging.Services.Configure<FileLoggerOptions>(builder.Configuration.GetSection("FileLog"));
    logging.AddFileLogger(options =>
    {
        options.FolderPath = builder.Configuration["AppSettings:WorkingFolderPath"];
    });
    

    You can now remove the FolderPath property from the FileLog object in the appsettings.json file since that value will be set using this line when adding the file logger:

    options.FolderPath = builder.Configuration["AppSettings:WorkingFolderPath"];
    

    The options would then look like this:

    {
      "AppSettings": {
        "WorkingFolderPath": "C:\\mypath\\",
        ...
      },
      "FileLog": {
        "RetentionDays": 5
      },
      "Logging": {
        ...
        "FileLog": {
          "LogLevel": {
            "Default": "Debug"
          }
        }
      }
    }
    

    I've written a similar logging provider in the Elmah.Io.Extensions.Logging repository and I copied some of the code from there. I tried mapping it to your original code in the question, but there might be something that doesn't match exactly. I hope that you still see the intent behind what I'm trying. You can look inside this repository for inspiration.