From fb1efb6de4ee88d350418e21bd48785b90715e6d Mon Sep 17 00:00:00 2001 From: "grzegorz.russek" Date: Fri, 29 Sep 2023 20:06:48 +0200 Subject: [PATCH] Added pattern{date} and {seq} to file path in rolling sink. --- .../Sinks/File/PathRoller.cs | 92 ++++++++-- .../Sinks/File/PathTokenizer.cs | 161 ++++++++++++++++++ .../Sinks/File/RollingFileSink.cs | 8 +- .../TemplatedPathRollerTests.cs | 40 +++++ 4 files changed, 280 insertions(+), 21 deletions(-) create mode 100644 src/Serilog.Sinks.File/Sinks/File/PathTokenizer.cs diff --git a/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs b/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs index 79a6915..deae89c 100644 --- a/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs +++ b/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs @@ -16,6 +16,8 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; +using System.Text; using System.Text.RegularExpressions; namespace Serilog.Sinks.File @@ -26,36 +28,94 @@ class PathRoller const string SequenceNumberMatchGroup = "sequence"; readonly string _directory; - readonly string _filenamePrefix; - readonly string _filenameSuffix; + readonly string _filenameFormat; + readonly Regex _filenameMatcher; readonly RollingInterval _interval; + private readonly List _tokenized; readonly string _periodFormat; + public PathRoller(string path, RollingInterval interval) { if (path == null) throw new ArgumentNullException(nameof(path)); _interval = interval; + + _tokenized = PathTokenizer.Tokenize(path); + + StringBuilder formatBuilder = new StringBuilder(); + + var dateFound = false; + var seqFound = false; + _periodFormat = interval.GetFormat(); - var pathDirectory = Path.GetDirectoryName(path); + var i = _tokenized.FindIndex(x => x.Type == PathTokenizer.Token.TokenType.Parameter); + if (i < 0) + i = _tokenized.Count - 1; + + var lastSeparator = _tokenized.FindLastIndex(i, x => x.Type == PathTokenizer.Token.TokenType.DirectorySeparator); + + string? pathDirectory = null; + if (lastSeparator > 0) + pathDirectory = Path.Combine(_tokenized.Take(lastSeparator).Where(x => x.Type == PathTokenizer.Token.TokenType.Text).Select(x => x.ToString()).ToArray()); + if (string.IsNullOrEmpty(pathDirectory)) pathDirectory = Directory.GetCurrentDirectory(); + foreach (var t in _tokenized.Skip(lastSeparator+1)) + { + if (t.Type == PathTokenizer.Token.TokenType.Extension) + break; + + if (t.Type == PathTokenizer.Token.TokenType.Parameter) + { + if (t.Value.ToLower() == "date") + { + dateFound = true; + formatBuilder.Append("{0}"); + if (t.Argument != null && t.Argument != string.Empty) + _periodFormat = t.Argument; + } + else if (t.Value.ToLower() == "seq") + { + seqFound = true; + formatBuilder.Append("{1}"); + } + else + formatBuilder.Append(t.ToString()); + } + else + formatBuilder.Append(t.ToString()); + } + + if (!dateFound) formatBuilder.Append("{0}"); + if (!seqFound) formatBuilder.Append("{1}"); + + var ext = _tokenized.FirstOrDefault(x => x.Type == PathTokenizer.Token.TokenType.Extension); + + if (ext != null) + formatBuilder.Append(ext.ToString()); + _filenameFormat = formatBuilder.ToString(); + _directory = Path.GetFullPath(pathDirectory); - _filenamePrefix = Path.GetFileNameWithoutExtension(path); - _filenameSuffix = Path.GetExtension(path); - _filenameMatcher = new Regex( - "^" + - Regex.Escape(_filenamePrefix) + - "(?<" + PeriodMatchGroup + ">\\d{" + _periodFormat.Length + "})" + - "(?<" + SequenceNumberMatchGroup + ">_[0-9]{3,}){0,1}" + - Regex.Escape(_filenameSuffix) + - "$", + + _filenameMatcher = new Regex(string.Format(_filenameFormat.Replace("\\", "\\\\"), + "(?<" + PeriodMatchGroup + ">.{" + _periodFormat.Length + "})", + "(?<" + SequenceNumberMatchGroup + ">_[0-9]{3,}){0,1}") + "$", RegexOptions.Compiled); - DirectorySearchPattern = $"{_filenamePrefix}*{_filenameSuffix}"; + //_filenameMatcher = new Regex( + // "^" + + // Regex.Escape(_filenamePrefix) + + // "(?<" + PeriodMatchGroup + ">\\d{" + _periodFormat.Length + "})" + + // "(?<" + SequenceNumberMatchGroup + ">_[0-9]{3,}){0,1}" + + // Regex.Escape(_filenameSuffix) + + // "$", + // RegexOptions.Compiled); + + DirectorySearchPattern = string.Format(_filenameFormat, "*", "*").Replace("**", "*"); } public string LogFileDirectory => _directory; @@ -67,11 +127,11 @@ public void GetLogFilePath(DateTime date, int? sequenceNumber, out string path) var currentCheckpoint = GetCurrentCheckpoint(date); var tok = currentCheckpoint?.ToString(_periodFormat, CultureInfo.InvariantCulture) ?? ""; - + string seq = string.Empty; if (sequenceNumber != null) - tok += "_" + sequenceNumber.Value.ToString("000", CultureInfo.InvariantCulture); + seq = "_" + sequenceNumber.Value.ToString("000", CultureInfo.InvariantCulture); - path = Path.Combine(_directory, _filenamePrefix + tok + _filenameSuffix); + path = Path.Combine(_directory, string.Format(_filenameFormat, tok, seq)); } public IEnumerable SelectMatches(IEnumerable filenames) diff --git a/src/Serilog.Sinks.File/Sinks/File/PathTokenizer.cs b/src/Serilog.Sinks.File/Sinks/File/PathTokenizer.cs new file mode 100644 index 0000000..4886c78 --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/File/PathTokenizer.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Serilog.Sinks.File +{ + internal class PathTokenizer + { + public class Token + { + public enum TokenType { Text, DirectorySeparator, Parameter, Extension } + + public TokenType Type { get; set; } + + public string Value { get; set; } = string.Empty; + + public string? Argument { get; set; } + + public override string ToString() + { + switch (Type) + { + default: + case TokenType.Text: + return Value.Replace("{", "{{").Replace("}", "}}"); + case TokenType.DirectorySeparator: + return Path.DirectorySeparatorChar.ToString(); + case TokenType.Parameter: + if (string.IsNullOrEmpty(Argument)) + return string.Format("{{{0}}}", Value.Replace("{", "{{").Replace("}", "}}")); + else + return string.Format("{{{0}:{1}}}", Value.Replace("{", "{{").Replace("}", "}}"), Argument?.Replace("{", "{{").Replace("}", "}}")); + case TokenType.Extension: + return "." + Value.Replace("{", "{{").Replace("}", "}}"); + } + } + } + + public static List Tokenize(string str) + { + var res = new List(); + var type = Token.TokenType.Text; + var current = new StringBuilder(); + var inception = 0; + + foreach (var c in str) + { + if (c == '{') + { + if (type != Token.TokenType.Parameter) + { + if (current.Length > 0) + { + res.Add(new Token + { + Type = type, + Value = current.ToString(), + }); + current.Length = 0; + } + + type = Token.TokenType.Parameter; + continue; + } + else + inception++; + } + else if (c == '}' && type == Token.TokenType.Parameter) + { + if (inception > 0) + inception--; + else + { + if (current.Length > 0) + { + string? a = null; + var v = current.ToString(); + var i = v.IndexOf(':'); + if (i > 0) + { + a = v.Substring(i + 1); + v = v.Substring(0, i); + } + res.Add(new Token + { + Type = type, + Value = v, + Argument = a, + }); + current.Length = 0; + } + + type = Token.TokenType.Text; + + continue; + } + } + else if (c == Path.DirectorySeparatorChar && type == Token.TokenType.Text) + { + if (current.Length > 0) + { + res.Add(new Token + { + Type = type, + Value = current.ToString(), + }); + + current.Length = 0; + } + + res.Add(new Token + { + Type = Token.TokenType.DirectorySeparator, + }); + + type = Token.TokenType.Text; + + continue; + } + + current.Append(c); + } + + if (current.Length > 0) + { + res.Add(new Token + { + Type = type, + Value = current.ToString(), + }); + + current.Length = 0; + } + + var l = res.Last(); + if (l != null && l.Type == Token.TokenType.Text) + { + var i = l.Value.LastIndexOf("."); + if (i >= 0) + { + var e = l.Value.Substring(i + 1); + if (i > 0) + l.Value = l.Value.Substring(0, i); + else + res.Remove(l); + + res.Add(new Token + { + Type = Token.TokenType.Extension, + Value = e, + }); + } + } + + return res; + } + } +} diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index dccb802..042eb4a 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -124,8 +124,7 @@ void OpenFile(DateTime now, int? minSequence = null) { if (Directory.Exists(_roller.LogFileDirectory)) { - existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern) - .Select(f => Path.GetFileName(f)); + existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern, SearchOption.AllDirectories); } } catch (DirectoryNotFoundException) { } @@ -183,8 +182,7 @@ void ApplyRetentionPolicy(string currentFilePath, DateTime now) // We consider the current file to exist, even if nothing's been written yet, // because files are only opened on response to an event being processed. - var potentialMatches = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern) - .Select(f => Path.GetFileName(f)) + var potentialMatches = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern, SearchOption.AllDirectories) .Union(new[] { currentFileName }); var newestFirst = _roller @@ -215,7 +213,7 @@ void ApplyRetentionPolicy(string currentFilePath, DateTime now) bool ShouldRetainFile(RollingLogFile file, int index, DateTime now) { - if (_retainedFileCountLimit.HasValue && index >= _retainedFileCountLimit.Value - 1) + if (_retainedFileCountLimit.HasValue && index >= _retainedFileCountLimit.Value) return false; if (_retainedFileTimeLimit.HasValue && file.DateTime.HasValue && diff --git a/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs b/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs index 65a974c..6cdc0b0 100644 --- a/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs +++ b/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs @@ -16,6 +16,24 @@ public void TheLogFileIncludesDateToken() AssertEqualAbsolute(Path.Combine("Logs", "log-20130714.txt"), path); } + [Fact] + public void TheLogFileIncludesDateTokenInPath() + { + var roller = new PathRoller(Path.Combine("Logs", "logs-{date}", "log.txt"), RollingInterval.Day); + var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); + roller.GetLogFilePath(now, null, out var path); + AssertEqualAbsolute(Path.Combine("Logs", "logs-20130714", "log.txt"), path); + } + + [Fact] + public void TheLogFileIncludesFormattedTokenInPath() + { + var roller = new PathRoller(Path.Combine("Logs", "{date:yyyy-MM-dd}", "log.txt"), RollingInterval.Day); + var now = new DateTime(2013, 7, 14, 3, 24, 9, 980); + roller.GetLogFilePath(now, null, out var path); + AssertEqualAbsolute(Path.Combine("Logs", "2013-07-14", "log.txt"), path); + } + [Fact] public void ANonZeroIncrementIsIncludedAndPadded() { @@ -74,6 +92,18 @@ public void TheDirectorSearchPatternUsesWildcardInPlaceOfDate() Assert.Equal("log-*.txt", roller.DirectorySearchPattern); } + [Theory] + [InlineData("logs\\{date}\\log.txt", "logs\\20131210\\log.txt", "logs\\20131210\\log_031.txt", RollingInterval.Day)] + [InlineData("logs\\{date}\\log.txt", "logs\\2013121013\\log.txt", "logs\\2013121013\\log_031.txt", RollingInterval.Hour)] + public void MatchingSelectsFilesWithPathFormat(string template, string zeroth, string thirtyFirst, RollingInterval interval) + { + var roller = new PathRoller(template, interval); + var matched = roller.SelectMatches(new[] { zeroth, thirtyFirst }).ToArray(); + Assert.Equal(2, matched.Length); + Assert.Null(matched[0].SequenceNumber); + Assert.Equal(31, matched[1].SequenceNumber); + } + [Theory] [InlineData("log-.txt", "log-20131210.txt", "log-20131210_031.txt", RollingInterval.Day)] [InlineData("log-.txt", "log-2013121013.txt", "log-2013121013_031.txt", RollingInterval.Hour)] @@ -86,6 +116,16 @@ public void MatchingSelectsFiles(string template, string zeroth, string thirtyFi Assert.Equal(31, matched[1].SequenceNumber); } + [Theory] + [InlineData("logs\\{date}\\log.txt", "logs\\20150101\\log.txt", "logs\\20141231\\log.txt", RollingInterval.Day)] + [InlineData("logs\\{date}\\log.txt", "logs\\2015010110\\log.txt", "logs\\2015010109\\log.txt", RollingInterval.Hour)] + public void MatchingParsesSubstitutionsWithPathFormat(string template, string newer, string older, RollingInterval interval) + { + var roller = new PathRoller(template, interval); + var matched = roller.SelectMatches(new[] { older, newer }).OrderByDescending(m => m.DateTime).Select(m => m.Filename).ToArray(); + Assert.Equal(new[] { newer, older }, matched); + } + [Theory] [InlineData("log-.txt", "log-20150101.txt", "log-20141231.txt", RollingInterval.Day)] [InlineData("log-.txt", "log-2015010110.txt", "log-2015010109.txt", RollingInterval.Hour)]