diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index 5cb19e9..c38a308 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -12,9 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.ComponentModel; -using System.Text; using Serilog.Configuration; using Serilog.Core; using Serilog.Debugging; @@ -23,6 +20,10 @@ using Serilog.Formatting.Display; using Serilog.Formatting.Json; using Serilog.Sinks.File; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; // ReSharper disable MethodOverloadWithOptionalParameter @@ -140,6 +141,7 @@ public static LoggerConfiguration File( /// The maximum number of log files that will be retained, /// including the current log file. For unlimited retention, pass null. The default is 31. /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// This action calls when a new log file created. It enables you to write any header text to the log file. /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( @@ -156,7 +158,8 @@ public static LoggerConfiguration File( RollingInterval rollingInterval = RollingInterval.Infinite, bool rollOnFileSizeLimit = false, int? retainedFileCountLimit = DefaultRetainedFileCountLimit, - Encoding encoding = null) + Encoding encoding = null, + Func> logFileHeaders = null) { if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); if (path == null) throw new ArgumentNullException(nameof(path)); @@ -165,7 +168,7 @@ public static LoggerConfiguration File( var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider); return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered, shared, flushToDiskInterval, - rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding); + rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, logFileHeaders); } /// @@ -174,7 +177,7 @@ public static LoggerConfiguration File( /// Logger sink configuration. /// A formatter, such as , to convert the log events into /// text for the file. If control of regular text formatting is required, use the other - /// overload of + /// overload of /// and specify the outputTemplate parameter instead. /// /// Path to the file. @@ -195,6 +198,7 @@ public static LoggerConfiguration File( /// The maximum number of log files that will be retained, /// including the current log file. For unlimited retention, pass null. The default is 31. /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// This action calls when a new log file created. It enables you to write any header text to the log file. /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( @@ -210,10 +214,11 @@ public static LoggerConfiguration File( RollingInterval rollingInterval = RollingInterval.Infinite, bool rollOnFileSizeLimit = false, int? retainedFileCountLimit = DefaultRetainedFileCountLimit, - Encoding encoding = null) + Encoding encoding = null, + Func> logFileHeaders = null) { return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, - buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit); + buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, logFileHeaders); } /// @@ -228,6 +233,7 @@ public static LoggerConfiguration File( /// Supplies culture-specific formatting information, or null. /// A message template describing the format used to write to the sink. /// the default is "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}". + /// This action calls when a new log file created. It enables you to write any header text to the log file. /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( @@ -236,14 +242,15 @@ public static LoggerConfiguration File( LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, string outputTemplate = DefaultOutputTemplate, IFormatProvider formatProvider = null, - LoggingLevelSwitch levelSwitch = null) + LoggingLevelSwitch levelSwitch = null, + Func> logFileHeaders = null) { if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); if (path == null) throw new ArgumentNullException(nameof(path)); if (outputTemplate == null) throw new ArgumentNullException(nameof(outputTemplate)); var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider); - return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, levelSwitch); + return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, levelSwitch, logFileHeaders); } /// @@ -252,7 +259,7 @@ public static LoggerConfiguration File( /// Logger sink configuration. /// A formatter, such as , to convert the log events into /// text for the file. If control of regular text formatting is required, use the other - /// overload of + /// overload of /// and specify the outputTemplate parameter instead. /// /// Path to the file. @@ -260,6 +267,7 @@ public static LoggerConfiguration File( /// events passed through the sink. Ignored when is specified. /// A switch allowing the pass-through minimum level /// to be changed at runtime. + /// This action calls when a new log file created. It enables you to write any header text to the log file. /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. public static LoggerConfiguration File( @@ -267,10 +275,11 @@ public static LoggerConfiguration File( ITextFormatter formatter, string path, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - LoggingLevelSwitch levelSwitch = null) + LoggingLevelSwitch levelSwitch = null, + Func> logFileHeaders = null) { return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, levelSwitch, false, true, - false, null, null, RollingInterval.Infinite, false, null); + false, null, null, RollingInterval.Infinite, false, null, logFileHeaders); } static LoggerConfiguration ConfigureFile( @@ -287,7 +296,8 @@ static LoggerConfiguration ConfigureFile( Encoding encoding, RollingInterval rollingInterval, bool rollOnFileSizeLimit, - int? retainedFileCountLimit) + int? retainedFileCountLimit, + Func> logFileHeaders) { if (addSink == null) throw new ArgumentNullException(nameof(addSink)); if (formatter == null) throw new ArgumentNullException(nameof(formatter)); @@ -300,7 +310,7 @@ static LoggerConfiguration ConfigureFile( if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite) { - sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit); + sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, logFileHeaders); } else { @@ -309,11 +319,11 @@ static LoggerConfiguration ConfigureFile( #pragma warning disable 618 if (shared) { - sink = new SharedFileSink(path, formatter, fileSizeLimitBytes); + sink = new SharedFileSink(path, formatter, fileSizeLimitBytes, logFileHeaders: logFileHeaders); } else { - sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered); + sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered, logFileHeaders: logFileHeaders); } #pragma warning restore 618 } diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs index bfd288f..07009ab 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs @@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Serilog.Events; +using Serilog.Formatting; using System; +using System.Collections.Generic; using System.IO; using System.Text; -using Serilog.Events; -using Serilog.Formatting; namespace Serilog.Sinks.File { @@ -33,6 +34,8 @@ public sealed class FileSink : IFileSink, IDisposable readonly bool _buffered; readonly object _syncRoot = new object(); readonly WriteCountingStream _countingStreamWrapper; + readonly Func> _logFileHeaders; + bool _appendHeaderLogs = false; /// Construct a . /// Path to the file. @@ -43,10 +46,11 @@ public sealed class FileSink : IFileSink, IDisposable /// Character encoding used to write the text file. The default is UTF-8 without BOM. /// Indicates if flushing to the output file can be buffered or not. The default /// is false. + /// This action calls when a new log file created. It enables you to write any header text to the log file. /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. /// - public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, bool buffered = false) + public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, bool buffered = false, Func> logFileHeaders = null) { if (path == null) throw new ArgumentNullException(nameof(path)); if (textFormatter == null) throw new ArgumentNullException(nameof(textFormatter)); @@ -55,6 +59,7 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy _textFormatter = textFormatter; _fileSizeLimitBytes = fileSizeLimitBytes; _buffered = buffered; + _logFileHeaders = logFileHeaders; var directory = Path.GetDirectoryName(path); if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) @@ -62,6 +67,9 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy Directory.CreateDirectory(directory); } + if (logFileHeaders != null && FileNotFoundOrEmpty(path)) + _appendHeaderLogs = true; + Stream outputStream = _underlyingStream = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.Read); if (_fileSizeLimitBytes != null) { @@ -71,6 +79,13 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy _output = new StreamWriter(outputStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } + bool FileNotFoundOrEmpty(string path) + { + var fileInfo = new FileInfo(path); + + return !fileInfo.Exists || fileInfo.Length == 0; + } + bool IFileSink.EmitOrOverflow(LogEvent logEvent) { if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); @@ -82,21 +97,36 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent) return false; } - _textFormatter.Format(logEvent, _output); - if (!_buffered) - _output.Flush(); + if (_appendHeaderLogs) + { + var logFileHeaderCollection = _logFileHeaders.Invoke(); + + foreach (var item in logFileHeaderCollection) + AppendToOutput(item); + + _appendHeaderLogs = false; + } + + AppendToOutput(logEvent); return true; } } + private void AppendToOutput(LogEvent logEvent) + { + _textFormatter.Format(logEvent, _output); + if (!_buffered) + _output.Flush(); + } + /// /// Emit the provided log event to the sink. /// /// The log event to write. public void Emit(LogEvent logEvent) { - ((IFileSink) this).EmitOrOverflow(logEvent); + ((IFileSink)this).EmitOrOverflow(logEvent); } /// diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index 6593e68..5ca1af1 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -14,14 +14,15 @@ #pragma warning disable 618 -using System; -using System.IO; -using System.Linq; -using System.Text; using Serilog.Core; using Serilog.Debugging; using Serilog.Events; using Serilog.Formatting; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; namespace Serilog.Sinks.File { @@ -35,6 +36,7 @@ sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable readonly bool _buffered; readonly bool _shared; readonly bool _rollOnFileSizeLimit; + readonly Func> _logFileHeaders; readonly object _syncRoot = new object(); bool _isDisposed; @@ -50,7 +52,8 @@ public RollingFileSink(string path, bool buffered, bool shared, RollingInterval rollingInterval, - bool rollOnFileSizeLimit) + bool rollOnFileSizeLimit, + Func> logFileHeaders) { if (path == null) throw new ArgumentNullException(nameof(path)); if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative"); @@ -64,6 +67,7 @@ public RollingFileSink(string path, _buffered = buffered; _shared = shared; _rollOnFileSizeLimit = rollOnFileSizeLimit; + _logFileHeaders = logFileHeaders; } public void Emit(LogEvent logEvent) @@ -146,8 +150,8 @@ void OpenFile(DateTime now, int? minSequence = null) try { _currentFile = _shared ? - (IFileSink)new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding) : - new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered); + (IFileSink)new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _logFileHeaders) : + new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _logFileHeaders); _currentFileSequence = sequence; } catch (IOException ex) @@ -177,7 +181,7 @@ void ApplyRetentionPolicy(string currentFilePath) // because files are only opened on response to an event being processed. var potentialMatches = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern) .Select(Path.GetFileName) - .Union(new [] { currentFileName }); + .Union(new[] { currentFileName }); var newestFirst = _roller .SelectMatches(potentialMatches) diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs index 805e786..16111f5 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs @@ -14,13 +14,14 @@ #if ATOMIC_APPEND +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting; using System; +using System.Collections.Generic; using System.IO; using System.Security.AccessControl; using System.Text; -using Serilog.Core; -using Serilog.Events; -using Serilog.Formatting; namespace Serilog.Sinks.File { @@ -36,6 +37,8 @@ public sealed class SharedFileSink : IFileSink, IDisposable readonly ITextFormatter _textFormatter; readonly long? _fileSizeLimitBytes; readonly object _syncRoot = new object(); + readonly Func> _logFileHeaders; + bool _appendHeaderLogs = false; // The stream is reopened with a larger buffer if atomic writes beyond the current buffer size are needed. FileStream _fileOutput; @@ -50,10 +53,11 @@ public sealed class SharedFileSink : IFileSink, IDisposable /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit /// will be written in full even if it exceeds the limit. /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// This action calls when a new log file created. It enables you to write any header text to the log file. /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. /// - public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null) + public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, Func> logFileHeaders = null) { if (path == null) throw new ArgumentNullException(nameof(path)); if (textFormatter == null) throw new ArgumentNullException(nameof(textFormatter)); @@ -63,6 +67,7 @@ public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeL _path = path; _textFormatter = textFormatter; _fileSizeLimitBytes = fileSizeLimitBytes; + _logFileHeaders = logFileHeaders; var directory = Path.GetDirectoryName(path); if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) @@ -70,6 +75,9 @@ public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeL Directory.CreateDirectory(directory); } + if (logFileHeaders != null && FileNotFoundOrEmpty(path)) + _appendHeaderLogs = true; + // FileSystemRights.AppendData sets the Win32 FILE_APPEND_DATA flag. On Linux this is O_APPEND, but that API is not yet // exposed by .NET Core. _fileOutput = new FileStream( @@ -85,6 +93,13 @@ public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeL encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } + bool FileNotFoundOrEmpty(string path) + { + var fileInfo = new FileInfo(path); + + return !fileInfo.Exists || fileInfo.Length == 0; + } + bool IFileSink.EmitOrOverflow(LogEvent logEvent) { if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); @@ -93,10 +108,20 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent) { try { - _textFormatter.Format(logEvent, _output); - _output.Flush(); + if (_appendHeaderLogs) + { + var logFileHeaderCollection = _logFileHeaders.Invoke(); + + foreach (var item in logFileHeaderCollection) + AppendToOutput(item); + + _appendHeaderLogs = false; + } + + AppendToOutput(logEvent); + var bytes = _writeBuffer.GetBuffer(); - var length = (int) _writeBuffer.Length; + var length = (int)_writeBuffer.Length; if (length > _fileStreamBufferLength) { var oldOutput = _fileOutput; @@ -141,6 +166,12 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent) } } + private void AppendToOutput(LogEvent logEvent) + { + _textFormatter.Format(logEvent, _output); + _output.Flush(); + } + /// /// Emit the provided log event to the sink. /// diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs index a779bda..82e8a5d 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.OSMutex.cs @@ -15,6 +15,7 @@ #if OS_MUTEX using System; +using System.Collections.Generic; using System.IO; using System.Text; using Serilog.Core; @@ -35,6 +36,8 @@ public sealed class SharedFileSink : IFileSink, IDisposable readonly ITextFormatter _textFormatter; readonly long? _fileSizeLimitBytes; readonly object _syncRoot = new object(); + readonly Func> _logFileHeaders; + bool _appendHeaderLogs = false; const string MutexNameSuffix = ".serilog"; const int MutexWaitTimeout = 10000; @@ -47,10 +50,11 @@ public sealed class SharedFileSink : IFileSink, IDisposable /// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit /// will be written in full even if it exceeds the limit. /// Character encoding used to write the text file. The default is UTF-8 without BOM. + /// This action calls when a new log file created. It enables you to write any header text to the log file. /// Configuration object allowing method chaining. /// The file will be written using the UTF-8 character set. /// - public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null) + public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null, Func> logFileHeaders = null) { if (path == null) throw new ArgumentNullException(nameof(path)); if (textFormatter == null) throw new ArgumentNullException(nameof(textFormatter)); @@ -59,6 +63,7 @@ public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeL _textFormatter = textFormatter; _fileSizeLimitBytes = fileSizeLimitBytes; + _logFileHeaders = logFileHeaders; var directory = Path.GetDirectoryName(path); if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) @@ -66,12 +71,22 @@ public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeL Directory.CreateDirectory(directory); } + if (logFileHeaders != null && FileNotFoundOrEmpty(path)) + _appendHeaderLogs = true; + var mutexName = Path.GetFullPath(path).Replace(Path.DirectorySeparatorChar, ':') + MutexNameSuffix; _mutex = new Mutex(false, mutexName); _underlyingStream = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); _output = new StreamWriter(_underlyingStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } + bool FileNotFoundOrEmpty(string path) + { + var fileInfo = new FileInfo(path); + + return !fileInfo.Exists || fileInfo.Length == 0; + } + bool IFileSink.EmitOrOverflow(LogEvent logEvent) { if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); @@ -90,9 +105,18 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent) return false; } - _textFormatter.Format(logEvent, _output); - _output.Flush(); - _underlyingStream.Flush(); + if (_appendHeaderLogs) + { + var logFileHeaderCollection = _logFileHeaders.Invoke(); + + foreach (var item in logFileHeaderCollection) + AppendToOutput(item); + + _appendHeaderLogs = false; + } + + AppendToOutput(logEvent); + return true; } finally @@ -102,6 +126,13 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent) } } + private void AppendToOutput(LogEvent logEvent) + { + _textFormatter.Format(logEvent, _output); + _output.Flush(); + _underlyingStream.Flush(); + } + /// /// Emit the provided log event to the sink. ///