Darshan
Darshan

Reputation: 71

In Serilog dynamically changing log file path?

I am having feature of dynamically changing log file path. But when I am changing the path which is configurable in Consul, it writes partial log at both places i.e. at old path as well at new path. Changing log file path should work without any service restart. How we can archive that?

We are writing in log file as follow:

.WriteTo.File(logFolderFullPath + "\\" + applicationName + "_.txt",
                         LogEventLevel.Error, shared: true,
                         fileSizeLimitBytes: fileSizeLimitBytes, rollOnFileSizeLimit: true, rollingInterval: RollingInterval.Day,
                          outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] [{MachineName}] [{SourceContext}] {RequestId} {CorrelationId} {Message}{NewLine}{Exception}{properties}")

logFolderFullPath is configurable path from appsetting.json. When we are changing the path it creates a log files at new path, but at the same time keeps writing in old path files also.

So we want it should stop writing to old path.

Upvotes: 7

Views: 11513

Answers (2)

Mathieu DSTP
Mathieu DSTP

Reputation: 153

The Serilog FileSink does not allow the path to be modified once it is set. I still prefer to use appsettings.json to store the serilog config but I hack the configuration before using it.

My appsettings.json looks like this:

...
        "WriteTo": [
            {
                "Name": "File",
                "Args": {
                    "path": "../logs/log-.txt",
                    "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact",
                    "rollingInterval": "Day",
                    "buffered": true
                }
            }
        ]
...

I have created an extension method to override the configuration from the appsettings.json before configuring Serilog.

public static class IHostBuilderExtensions
    {

        public static IHostBuilder ConfigureSerilog(this IHostBuilder hostBuilder, string appName)
        {
            return hostBuilder.ConfigureAppConfiguration((hostCtx, configBuilder) =>
             {
                 var config = configBuilder.Build();

                 var pid = Process.GetCurrentProcess().Id;
                 var logFilePath = $@"{MyLogFolder}\\{appName}_pid_{pid}_.txt";
                 var logFileNameWithPidPattern = $"{appName}_pid_{pid}_.txt";
                 const string serilogWriteConfigPattern = "Serilog:WriteTo:";
                 const string serilogFilePathConfigPattern = ":Args:path";

                 var serilogFoundKvpFilePathFromConfig = config
                     .AsEnumerable()
                     .FirstOrDefault(kvp =>
                         kvp.Key.Contains(serilogWriteConfigPattern, StringComparison.InvariantCultureIgnoreCase)
                         && kvp.Key.Contains(serilogFilePathConfigPattern, StringComparison.InvariantCultureIgnoreCase))
                     ;
                 var keyToReplace = serilogFoundKvpFilePathFromConfig.Key;
                 var overridenValue = serilogFoundKvpFilePathFromConfig.Value
                     .Replace("log-.txt", logFileNameWithPidPattern);

                 var serilogWriteToFilePathOverride = KeyValuePair.Create(keyToReplace, overridenValue);
                 configBuilder.AddInMemoryCollection(new[] { serilogWriteToFilePathOverride });
             })
            .UseSerilog((ctx, lc) =>
            {
                lc
                    // At this point, the config has been updated
                    // and the file name contains the Process Id:
                    // eg.: MyName_pid_15940_20220826.txt
                    .ReadFrom.Configuration(ctx.Configuration)
                    .WriteTo
                    .Console();
            });
        }
}

I use it in Program.cs like so:

...
 hostBuilder
  .ConfigureAppConfiguration((hostCtx, configBuilder) => { /* other config */ })
  .ConfigureSerilog(appName)
...

Upvotes: 6

C. Augusto Proiete
C. Augusto Proiete

Reputation: 27868

You can try to use Serilog.Settings.Reloader which can swap your logger instance at run-time when your configuration changes.


Another common way to change properties of a logger at run-time, is to use Serilog.Sinks.Map, a sink that dispatches events based on properties of log events.

The example below uses a log event property called FileName to decide the name of the log file it'll write to, so whenever this property changes, the log file changes accordingly:

Log.Logger = new LoggerConfiguration()
    .WriteTo.Map("FileName", "IDontKnow", (fileName, wt) => wt.File($"{fileName}.txt"))
    .CreateLogger();

Log.ForContext("FileName", "Alice").Information("Hey!"); // writes to Alice.txt
Log.ForContext("FileName", "Bob").Information("Hello!"); // writes to Bob.txt
Log.Information("Hi Again!"); // writes to IDontKnow.txt (default if property is missing)

Log.CloseAndFlush();

In your case, you want to change this property name dynamically based on changes to your configuration. A simple way of doing that is by creating a custom enricher that can change the values of a property like the one above based on your configuration settings.

Your custom enricher would look something like this:

internal class LogFilePathEnricher : ILogEventEnricher
{
    private string _cachedLogFilePath;
    private LogEventProperty _cachedLogFilePathProperty;

    public const string LogFilePathPropertyName = "LogFilePath";

    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
    {
        var logFilePath = // Read path from your appsettings.json
        // Check for null, etc...

        LogEventProperty logFilePathProperty;

        if (logFilePath.Equals(_cachedLogFilePath))
        {
            // Path hasn't changed, so let's use the cached property
            logFilePathProperty = _cachedLogFilePathProperty;
        }
        else
        {
            // We've got a new path for the log. Let's create a new property
            // and cache it for future log events to use
            _cachedLogFilePath = logFilePath;

            _cachedLogFilePathProperty = logFilePathProperty =
                propertyFactory.CreateProperty(LogFilePathPropertyName, logFilePath);
        }

        logEvent.AddPropertyIfAbsent(logFilePathProperty);
    }
}

NB: The example enricher above could be more efficient if you use the Options pattern, instead of checking the configuration every time a log message is written.

With an enricher that can dynamically set the LogFilePath property for you based on the configuration, you just have to configure the logging pipeline to map based on that property.

Log.Logger = new LoggerConfiguration()
    .Enrich.FromLogContext()
    .Enrich.With<LogFileNameEnricher>()
    .WriteTo.Map(LogFileNameEnricher.LogFilePathPropertyName,
        (logFilePath, wt) => wt.File($"{logFilePath}"), sinkMapCountLimit: 1)
    .CreateLogger();

// ...

Log.CloseAndFlush();

Upvotes: 7

Related Questions