Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Log File to be Deleted at Runtime #96

Open
rustbomber opened this issue Apr 17, 2019 · 20 comments
Open

Allow Log File to be Deleted at Runtime #96

rustbomber opened this issue Apr 17, 2019 · 20 comments

Comments

@rustbomber
Copy link

rustbomber commented Apr 17, 2019

I use sinks-file on windows system, when web app runing, log write to file, now I want to delete the log file at runtime, windows report:

File is in use
The operation could not be completed because the file was opened in dotnet.exe

I use shared parameter, set true, is not working.

my code :

const string template = "{Timestamp:yyyy-MM-dd HH:mm:ss.ffffff} {Level:u4} {SourceContext} {Message} {Exception}{NewLine}";
Log.Logger = new LoggerConfiguration()
    .ReadFrom.Configuration(config)
    .Enrich.FromLogContext()
    .MinimumLevel.Is(minimumLevel)
    .WriteTo.Console(outputTemplate: template)
    .WriteTo.Debug(outputTemplate: template)
    .WriteTo.File(path: Path.Combine(AppContext.BaseDirectory, "Logs", "log.txt"), outputTemplate: template,
        fileSizeLimitBytes: 5242880, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true, retainedFileCountLimit: 31, shared:true, buffered: false).CreateLogger();
@nblumhardt nblumhardt changed the title can not delete log file at runtime? Allow Log File to be Deleted at Runtime Apr 20, 2019
@nblumhardt
Copy link
Member

Hi - shared: true sets the file to allow read-write sharing; deleting the file is not supported in this mode.

It seems like there are possibly two separate requirements here:

  1. Allow the file to be deleted
  2. Recreate the file when this happens

I think we could allow (1) without initially worrying about (2) - this might be handy, for instance, in the situation where unbounded log file growth occurs and the file needs to be removed before the process can be (safely?) stopped.

I'm not sure how widely valued this scenario is, though - any thoughts?

@lscorcia
Copy link

+1 for this issue and the related #128 . I have migrated a large solution from log4net to Serilog only to discover this issue when moving to production. Back to log4net until this gets resolved.

@dgxhubbard
Copy link

This is definitely an issue I wanted to switch to serilog but found this issue. When the app is allowing the log file to be deleted means there is less to sift through to find a problem. We do this all the time and it saves time finding issues. Why are you locking the log file?

@nblumhardt
Copy link
Member

Thanks for the nudge!

Nothing's happened here because between my:

I'm not sure how widely valued this scenario is, though - any thoughts?

and any response was four years, which somewhat slowed thing down :-)

I think the next step would be for someone to investigate the impact of making this change (part 1, or parts 1 & 2) and can share some info on what would be required, and which other parts of the code might break (especially around rolling, perhaps?).

If the change is small and the impact low and well-understood, I don't think anyone has been opposed to supporting this - it's just a prioritization issue currently.

HTH!

@BTPDarren
Copy link

BTPDarren commented Jul 2, 2024

I have just found this to replace Log4Net due to versioning issues.

My use case is that I have a windows service that is performing a data sync and logging is required to be able to monitor it.

I have the service and a monitoring app that will reload the log files found in a specific location.
I often remove logs while an app or service is running, in fact its common. with log4net I am pretty sure it would re-create the file if its deleted.

I have also found while replacing log4net, the FileSystemWatcher fails to fire events on the log files in the folder because they are locked.

My work around for now (I really don't want to roll back to log4net)

Instead of creating the logger at the app start, I have a method I call to setup the logger and write out the information then re-setup

        private static void LogInfo(string message)
        {
            logger = new LoggerConfiguration()
                   .ReadFrom.AppSettings()
                 .Enrich.WithThreadId()
                 .CreateLogger();

            logger.Information(message);
            logger.Dispose();

            logger = new LoggerConfiguration()
                   .ReadFrom.AppSettings()
                 .Enrich.WithThreadId()
                 .CreateLogger();
        }

While this is not ideal , it solves the problem in that the watcher detects changes and you can delete the file as you want and it will get re-created next time its needed

Shared = true did not work with the watcher either

My app settings if your interested

	<appSettings>
		<add key="serilog:using:File" value="Serilog.Sinks.File" />		
		<add key="serilog:write-to:File.path" value="%APPDATA%\SEPWorkerService\Log\applog.log" />
		<add key="serilog:minimum-level" value="Information" />
		<add key="serilog:write-to:File.rollingInterval" value="Day" />
		<add key="serilog:write-to:File.rollOnFileSizeLimit" value="true" />
		<add key="serilog:write-to:File.shared" value="true" />		
	</appSettings>

@JakubTracz
Copy link

+1 to this issue - I have a scenario in my desktop app where I need a new logger each time a "robot" is launched. Once it's done, I display the executions historical data to the users and they can decide whether to keep the log files and other data pertinent to the specific robot run. The issue is they cannot remove the last entry without restarting the app as it's locked.

@nblumhardt
Copy link
Member

@JakubTracz disposing the per-robot logger before attempting to delete the file should sort this out.

@JakubTracz
Copy link

JakubTracz commented Jan 9, 2025

@nblumhardt I'm not sure how to achieve it with DI. This is my setup:

I use Map to create 2 loggers: 1/app run & 1 per each robot run

image

When running the robot I'm creating a scope by adding the robot key property so it's mapped correctly and creates a robot-run-specific file:
image

Inside Robot.Run, there a lot of different injected services, all of them using an ILogger from the DI and writing logs to a separate log file.

I'm not sure how to dispose of only the logger for this scope, if you know exact steps, it would be much appreciated :)

@nblumhardt
Copy link
Member

It's a bit roundabout, but setting sinkMapCountLimit to 1:

https://github.com/serilog/serilog-sinks-map?tab=readme-ov-file#limiting-the-number-of-open-sinks

and logging at least one non-robot-specific event after each run would do the job.

@starkapandan
Copy link

starkapandan commented Feb 24, 2025

we had another issue, marked as duplicate, adding here for context and to showcase more common and practical scenario where this is an issue. While app is running if the log file is recreated say from external logrotate built into linux or manually by hand eg the log file is recreated with same permission, serilog completely stops logging infinitely until full restart of application.

@nblumhardt
Copy link
Member

@starkapandan if the rolling interval is infinite, the file should be reopened within 30 minutes; otherwise, it'll be reopened at the next roll point. Are you seeing different behavior?

@starkapandan
Copy link

@nblumhardt gonna try it out and see if it reopens in 30 mins

@starkapandan
Copy link

@nblumhardt recreated the file with same permission and same name, waited around 6+ hours, serilog does not manage to log anything anymore (System: Linux Ubuntu 24.04, Runtime: Asp Net Core 9). A full restart of the asp.net core application, and the logging starts working again

@starkapandan
Copy link

saw a mentioning with serilog file sink having option "shared", the documentation was not very detailed but sounds like it tries to append instead of having always an open filehandle, which could potentially fix the solution, but this is mainly a theory. Could we get a little more info for this? An exclusive file handle that reopens in x interval sounds like it would be quite more effecient than "reopening" the file handle for every write? is this the case or does it work in some other way in the background?

@nblumhardt
Copy link
Member

@starkapandan my best guess is that it's a file ownership problem or similar; checking out the SelfLog output and Serilog.Sinks.File code (FileSink is pretty tiny) should shed some light on it. HTH!

@starkapandan
Copy link

@nblumhardt
regarding file ownership that part is tested, should not be an issue, new file has identical permission and works when app is restarted.

just checked the source, it was small indeed. seems filesink.cs opens one permanent filehandle/stream, which would explain why this issue occurs when the log file is recreated, it tries to write to a non existing file handle.

for filesink, would it be possible to add some form of simple check before logging to see if the opened file stream is stil valid/usable? if not then just reopen it again with same filename basically?

@nblumhardt
Copy link
Member

@starkapandan in Infinite mode, the checkpoint is set to 30 mins on each open, and then at each 30 minute interval the file should be reopened:

https://github.com/serilog/serilog-sinks-file/blob/dev/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs#L106

There's an active PR in the repository that we'll merge soon which tweaks this a little. If there's a problem in there somewhere it'd be good to know. Worth checking SelfLog for more error info.

@nblumhardt
Copy link
Member

Ah, sorry, now I understand why this isn't kicking in; we'll only use RollingFileSink if rollOnFileSizeLimit is set. Worth giving this a try, with a very high limit, to check that it resolves the issue for you.

@starkapandan
Copy link

starkapandan commented Mar 10, 2025

@nblumhardt Sounds like RollingFileSink class has some more nice additions sprinkled on top of it. On linux servers (cross platform usage of asp net core) that relies on built in system logrotate for standardization across different frameworks/programs is it possible to also get this features into the standard FileSink.cs? It would be nice to just use serilog for logging to the file and let the system handle the log rotation (this is where the 30 min log interval reopening would be very useful here) to avoid the scenario of it stopping logging infinitely. Otherwise as i understand when using serilog even if we dont need the rotating functionality of RotatingFileSink we would need to still use that class specifically just to get the feature where it reopens the file handle? Which i guess could be used as a work around but would be nice to avoid needing this work around hopefully.

In general sounds like a nice functionality for regular FileSink aswell? Eg when logging check previous datetime of last opening, if more than 30 mins passed just reopening of the same filestream again would mitigate this issue fully (potentially missing 30 mins of logging but thats not to big of an issue to be fair, maybe if this is to basic a little more advanced setting to control the interval of reopening of the filestream could later be introduced, so for some users where logging is crucial they could set this time much lower of 5 min or even 0 which would always open a new filestream and guaranteed always make logging work)

Not sure if dotnet core 9 supports this out of the box but the obvious superior approach would be if it was possible to check if simply the active filestream is valid, and if its not then reopen the filestream, but here i have no clue if dotnet core 9 actually has such a functionality available?


did a quick experimenting, in c# (windows). opened a new filestream, wrote to it, then deleted the file and recreated the file. The filestream in c#, when trying to write it looks like it writes successfully, eg no exceptions thrown and property CanWrite is still true, but nothing is ever actually written out to the file system. Could not find a property or something similar to detect that the filestream was invalid and that we were in reality writing to thin air. So seems like the solution of reopening the file in x minute interval might be the proper approach to be implemented?

@starkapandan
Copy link

short update, seems that this issue is rather straightforward to implement with a custom sink that implements retry interval (one single class does it), where it reopens the stream after x amount of minutes. Which solves this threads issue, not as nice or streamlined as a real integrated file sink, but quickly sketched out something like this and tried it, solves the issue at hand, if wished feel free to take parts of it into the repository as a separate filesink type or integrate it into existing.

If anyone is in a hurry to have this type of feature then feel free to just paste this one class in, and refactor the startup code to instead use

Image

then appsettings:

Image

and lastly the modded sink class itself:

using System;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Extensions.Configuration;
using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting;
using Serilog.Formatting.Json;

namespace Serilog.Sinks.File;


/**
* ---------- custom serilog file sink -------------------
* Fixes issue with serilog, when the file is externally recreated such as from logrotate etc
* reopens filestream every 30 minutes to ensure we recover back a valid filestream and continuing to write to it
*/

/// <summary>
/// Write log events to a disk file.
/// </summary>
public sealed class _Serilog_Custom_FileSink : IDisposable, ILogEventSink
{
    TextWriter? _output;
    readonly ITextFormatter _textFormatter;
    readonly object _syncRoot = new();
    readonly string path;
    DateTime _streamCreationTime = DateTime.Now;
    TimeSpan _reopenInterval = TimeSpan.FromMinutes(30);

    public _Serilog_Custom_FileSink(IConfiguration configuration)
    {
        _textFormatter = new JsonFormatter();

        this.path = configuration.GetSection("Serilog:WriteTo")
            .GetChildren()
            .First(x => x["Name"] == "_Custom_FileSink")?
            .GetSection("Args")["path"]!;
        if (this.path == null) throw new ArgumentNullException(nameof(path));

        var directory = Path.GetDirectoryName(path);
        if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory))
        {
            Directory.CreateDirectory(directory);
        }

        OpenStream();
    }

    /// <summary>
    /// Emit the provided log event to the sink.
    /// </summary>
    /// <param name="logEvent">The log event to write.</param>
    /// <exception cref="ArgumentNullException">When <paramref name="logEvent"/> is <code>null</code></exception>
    public void Emit(LogEvent logEvent)
    {
        lock (_syncRoot)
        {
            if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));

            if ((DateTime.Now - _streamCreationTime) > _reopenInterval)
            {
                _streamCreationTime = DateTime.Now;
                this.Dispose();
                this.OpenStream();
            }

            if (_output == null)
                return;
            _textFormatter.Format(logEvent, _output);
            _output.Flush();
        }
    }

    public void Dispose()
    {
        if (_output == null)
            return;
        _output.Dispose();
        _output = null;
    }

    private void OpenStream()
    {
        try
        {
            Stream outputStream = System.IO.File.Open(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read);
            outputStream.Seek(0, SeekOrigin.End);

            // Parameter reassignment.
            var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
            _output = new StreamWriter(outputStream, encoding);
        }
        catch (Exception)
        {
            _output = null;
            //failed opening stream atm...
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants