Skip to content

Latest commit

 

History

History
164 lines (120 loc) · 7.4 KB

File metadata and controls

164 lines (120 loc) · 7.4 KB

Meziantou.Extensions.Logging.FileLogger

An ILogger implementation that writes to a file. The messages are queued and written by a background thread, so logging doesn't block the application.

Usage

using Microsoft.Extensions.Logging;

var logsDirectory = Path.Combine(Path.GetTempPath(), "logs");
using var provider = new FileLoggerProvider(logsDirectory);

using var loggerFactory = LoggerFactory.Create(builder => builder.AddProvider(provider));
var logger = loggerFactory.CreateLogger("Sample");

logger.LogInformation("Hello from file logger");
Console.WriteLine($"Log file: {provider.LogFilePath}");

Dependency Injection

var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddFile(options =>
{
    options.Directory = "logs";
    options.RollInterval = RollInterval.Daily;
    options.MaxRetainedFiles = 7;
    options.CompressRolledFiles = true;
    options.IncludeScopes = true;
});

The options can also be set from the configuration, using the File provider alias:

{
  "Logging": {
    "File": {
      "Directory": "logs",
      "RollInterval": "Daily",
      "MaxFileSizeInBytes": 10485760,
      "MaxRetainedFiles": 7,
      "FormatterName": "json",
      "LogLevel": {
        "Default": "Information"
      }
    }
  }
}
builder.Logging.AddFile();

Note that the options related to the log file itself (Directory, file name, Append, rolling, MaxQueueLength, QueueFullMode) are read when the provider is created. The other options are re-read when the configuration changes.

Rolling and retention

Option Description
RollInterval Creates a new file every hour / day / month
MaxFileSizeInBytes Creates a new file when the current one reaches the size limit
MaxRetainedFiles Deletes the oldest files when a new file is created
Compression Compresses the log files using gzip, Brotli, or Zstandard (.NET 11+)
Append Reuses an existing file instead of creating a new one at startup. See the note below

The log files are named {FileNamePrefix}{timestamp}-{processId}{FileNameExtension}, so they are ordered chronologically by name. When the name is already used, a suffix is added (_001, _002, …).

Append reuses a file only when its name matches the name computed at startup, so it needs a stable file name:

  • RollInterval must be set. RollInterval.None includes the seconds in the name, so the name changes on every start.
  • IncludeProcessIdInFileName must be false to reuse a file across restarts, as the identifier of the process changes.

With the default values of these two options, a new file is created on every start even when Append is true.

options.Append = true;
options.RollInterval = RollInterval.Daily;
options.IncludeProcessIdInFileName = false; // Required to reuse the file after a restart

Compression

options.Compression = LogFileCompression.GZip;      // GZip, Brotli, or Zstandard (.NET 11+)
options.CompressionLevel = CompressionLevel.SmallestSize;
options.CompressionMode = LogFileCompressionMode.Continuous;
CompressionMode Description
Continuous (default) The messages are compressed as they are written, so the file is never written uncompressed. The extension of the compression algorithm is part of the file name (2024-01-02-1234.log.gz).
OnRoll The current log file is a plain text file, and it is compressed once it is rolled.

Continuous doesn't need any extra disk space and doesn't pause the logging to compress a big file, but the compressed stream is only finalized when the file is rolled or when the provider is disposed, so the current log file may not be readable by all the tools while the application is running. Use OnRoll when you need to read the current log file with the usual text tools.

Append is ignored when the messages are compressed continuously, as appending to a compressed file would produce a file that most tools cannot read entirely.

MaxFileSizeInBytes is compared to the size of the file on disk, so it accounts for the compression. As the compressed size is only known once the data is flushed, the file may be rolled slightly before the limit.

Permissions

On Unix, the log files are created with the default mode of the platform, which usually makes them readable by every local user. Set UnixCreateMode when the messages can contain sensitive data:

options.UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite;

The value is ignored on Windows, and it is still filtered by the umask of the process. The mode of the directory is not changed, so create the directory yourself when it must be private too.

Formatters

SimpleFileFormatter (default) writes one human-readable line per entry:

[2024-01-02 03:04:05.006] [INFO] [Sample.Program] => Scope1 Hello world

JsonFileFormatter writes one JSON object per entry, including the message template and its named parameters:

builder.Logging.AddFile("logs", options => options.FormatterName = FileFormatterNames.Json);
{"Timestamp":"2024-01-02 03:04:05.006","LogLevel":"INFO","Category":"Sample.Program","Message":"Hello world","State":{"Name":"world","{OriginalFormat}":"Hello {Name}"},"Scopes":["Scope1"]}

You can also write a custom formatter by inheriting from FileFormatter and setting FileLoggerOptions.Formatter.

Content of the messages

Option Default Description
MinLevel Trace Minimum level of the messages written to the file
TimestampFormat yyyy-MM-dd HH:mm:ss.fff Format of the timestamp, null to omit it
UseUtcTimestamp true Use UTC instead of the local time
IncludeLogLevel true Write the log level
IncludeCategory true Write the category
UseShortCategoryName false Write only the last segment of the category
IncludeScopes false Write the scopes
IncludeEventId false Write the event id
IncludeThreadId false Write the id of the thread that logged the message
IncludeActivityTracking false Write the trace id and the span id of Activity.Current
EscapeControlCharacters false Escape the control characters, so an entry always spans a single line

Untrusted data in the messages

SimpleFileFormatter writes the messages as-is, so a value containing a line break produces several lines. When the value comes from an untrusted source, it can forge entries that look exactly like real ones:

logger.LogInformation("User said: {Text}", "hello\n[2020-01-01 00:00:00.000] [CRIT] [Security] admin login succeeded");

Set EscapeControlCharacters to true to write \n, \r, \t and the other control characters as escape sequences, so an entry always spans a single line. The stack traces are then written on a single line, which is harder to read. JsonFileFormatter is not affected, as the JSON writer already escapes the control characters.

Reliability

  • The messages are queued in a bounded queue. When it is full, QueueFullMode determines if the caller waits for room (default), or if the message is dropped.
  • The pending messages are flushed as soon as the queue is empty, and at most every FlushInterval when the logger cannot keep up.
  • FlushAsync waits for the pending messages to be written.
  • When the log file cannot be created or written, a warning is written on the standard error stream and the application keeps running.