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

Added pattern {date} and {seq} to file path in rolling sink. #297

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 76 additions & 16 deletions src/Serilog.Sinks.File/Sinks/File/PathRoller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<PathTokenizer.Token> _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;
Expand All @@ -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<RollingLogFile> SelectMatches(IEnumerable<string> filenames)
Expand Down
161 changes: 161 additions & 0 deletions src/Serilog.Sinks.File/Sinks/File/PathTokenizer.cs
Original file line number Diff line number Diff line change
@@ -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<Token> Tokenize(string str)
{
var res = new List<Token>();
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;
}
}
}
8 changes: 3 additions & 5 deletions src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) { }
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 &&
Expand Down
40 changes: 40 additions & 0 deletions test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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)]
Expand All @@ -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)]
Expand Down