Added rolling log files features based on file size (#342)

* Added rolling log files features based on file size

* made properties readonly

---------

Co-authored-by: Alex <clodanSPT@hotmail.com>
This commit is contained in:
clodanSPT
2025-06-02 17:40:40 +01:00
committed by GitHub
parent ccb55bd025
commit 2ba11fc789
6 changed files with 262 additions and 15 deletions
@@ -0,0 +1,17 @@
using SPTarkov.DI.Annotations;
namespace SPTarkov.Server.Core.Utils.Logger.Handlers.File;
[Injectable(InjectionType.Singleton)]
public class DateFilePatternReplacer : IFilePatternReplacer
{
public string Pattern
{
get { return "%DATE%"; }
}
public string ReplacePattern(FileSptLoggerReference config, string fileWithPattern)
{
return fileWithPattern.Replace(Pattern, DateTime.UtcNow.ToString("yyyyMMdd"));
}
}
@@ -0,0 +1,7 @@
namespace SPTarkov.Server.Core.Utils.Logger.Handlers.File;
public interface IFilePatternReplacer
{
string Pattern { get; }
string ReplacePattern(FileSptLoggerReference config, string filePattern);
}
@@ -1,35 +1,206 @@
using System.Collections.Concurrent;
using System.Text;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.DI;
using SPTarkov.Server.Core.Utils.Logger.Handlers.File;
namespace SPTarkov.Server.Core.Utils.Logger.Handlers;
[Injectable(InjectionType.Singleton)]
public class FileLogHandler : BaseLogHandler
public class FileLogHandler(IEnumerable<IFilePatternReplacer> replacers) : BaseLogHandler, IOnLoad
{
private static ConcurrentDictionary<string, Lock> _fileLocks = new();
private const int FileSystemPollMonitorTimeMs = 5000;
// To be more efficient and avoid creating extra strings we will cache file patterns to the current processed pattern
// That way we dont need to process them twice and generate extra garbage
// _cacheFileNames[config.FilePath][config.FilePattern] will give you the current file pattern
private readonly Dictionary<string, Dictionary<string, string>> _cachedFileNames = new();
// This section needs to be fully locked as it is a double dictionary lookup
private readonly Lock _cachedFileNamesLocks = new();
private readonly Dictionary<string, Dictionary<string, string>> _cachedWipedPatterns = new();
private readonly Dictionary<string, IFilePatternReplacer> _replacers = replacers.ToDictionary(kv => kv.Pattern, kv => kv);
private readonly ConcurrentDictionary<string, Lock> _fileLocks = new();
private readonly ConcurrentDictionary<string, FileInfo> _fileInfos = new();
private readonly ConcurrentDictionary<string, FileSptLoggerReference> _fileConfigs = new();
public override LoggerType LoggerType => LoggerType.File;
public override void Log(SptLogMessage message, BaseSptLoggerReference reference)
{
var config = reference as FileSptLoggerReference;
var config = (reference as FileSptLoggerReference)!;
if (!_fileLocks.TryGetValue(config.FilePath, out var lockObject))
if (string.IsNullOrEmpty(config.FilePath) || string.IsNullOrEmpty(config.FilePattern))
{
lockObject = new Lock();
while (!_fileLocks.TryAdd(config.FilePath, lockObject)) ;
throw new Exception("FilePath and FilePattern are required to use FileLogger");
}
var targetFile = GetParsedTargetFile(config);
if (!_fileLocks.TryGetValue(targetFile, out var lockObject))
{
lockObject = new Lock();
while (!_fileLocks.TryAdd(targetFile, lockObject)) ;
}
lock (lockObject)
{
if (!Directory.Exists(Path.GetDirectoryName(config.FilePath)))
if (!Directory.Exists(config.FilePath))
{
Directory.CreateDirectory(Path.GetDirectoryName(config.FilePath));
Directory.CreateDirectory(config.FilePath);
}
// The AppendAllText will create the file as long as the directory exists
File.AppendAllText(config.FilePath, FormatMessage(message.Message + "\n", message, reference));
System.IO.File.AppendAllText(targetFile, FormatMessage(message.Message + "\n", message, reference));
if (!_fileInfos.TryGetValue(targetFile, out _))
{
var fileInfo = new FileInfo(targetFile);
while (!_fileInfos.TryAdd(targetFile, fileInfo)) ;
}
if (!_fileConfigs.TryGetValue(targetFile, out _))
{
while (!_fileConfigs.TryAdd(targetFile, config)) ;
}
}
}
protected string GetParsedTargetFile(FileSptLoggerReference? config)
{
lock (_cachedFileNamesLocks)
{
if (!_cachedFileNames.TryGetValue(config.FilePath, out var cachedFileNames))
{
cachedFileNames = new Dictionary<string, string>();
_cachedFileNames.Add(config.FilePath, cachedFileNames);
}
if (!cachedFileNames.TryGetValue(config.FilePattern, out var cachedFile))
{
cachedFile = $"{config.FilePath}{ProcessPattern(config)}";
cachedFileNames.Add(config.FilePattern, cachedFile);
}
return cachedFile;
}
}
protected string ProcessPattern(FileSptLoggerReference? configFilePattern)
{
var finalFile = configFilePattern.FilePattern;
foreach (var filePatternReplacer in _replacers)
{
if (finalFile.Contains(filePatternReplacer.Key))
{
finalFile = filePatternReplacer.Value.ReplacePattern(configFilePattern, finalFile);
}
}
return finalFile;
}
public Task OnLoad()
{
Task.Factory.StartNew(FileSystemWatcherMonitor, TaskCreationOptions.LongRunning);
return Task.CompletedTask;
}
protected void FileSystemWatcherMonitor()
{
while (true)
{
if (!_fileInfos.IsEmpty)
{
foreach (var fileInfosKvp in _fileInfos)
{
if (!_fileLocks.TryGetValue(fileInfosKvp.Key, out var fileLock))
{
continue;
}
lock (fileLock)
{
ValidateAndRollFile(fileInfosKvp.Key, fileInfosKvp.Value);
}
}
}
Thread.Sleep(FileSystemPollMonitorTimeMs);
}
}
protected void ValidateAndRollFile(string key, FileInfo fileInfo)
{
if (!_fileConfigs.TryGetValue(key, out var fileConfig))
{
return;
}
// MaxFileSizeMb == 0 means no max file size
if (fileConfig.MaxFileSizeMb == 0)
{
return;
}
fileInfo.Refresh();
if (fileInfo.Length / 1024D / 1024D > fileConfig.MaxFileSizeMb)
{
RollFile(fileConfig, fileInfo);
}
}
protected void RollFile(FileSptLoggerReference fileConfig, FileInfo fileInfo)
{
if (fileConfig.MaxRollingFiles > 0)
{
var unpatternedFileName = GetWipedPattern(fileConfig);
var lastFile = $"{unpatternedFileName}.{fileConfig.MaxRollingFiles - 1}";
if (System.IO.File.Exists(lastFile))
{
System.IO.File.Delete(lastFile);
}
for (var i = fileConfig.MaxRollingFiles - 1; i > 0; i--)
{
var oldReference = i - 1;
var oldFile = oldReference == 0 ? fileInfo.FullName : $"{unpatternedFileName}.{i - 1}";
var newFile = $"{unpatternedFileName}.{i}";
if (System.IO.File.Exists(oldFile))
{
System.IO.File.Copy(oldFile, newFile, true);
}
}
}
var stream = System.IO.File.Open(fileInfo.FullName, FileMode.Open);
stream.SetLength(0);
stream.Close();
}
protected string GetWipedPattern(FileSptLoggerReference fileConfig)
{
if (!_cachedWipedPatterns.TryGetValue(fileConfig.FilePath, out var wipePatterns))
{
wipePatterns = new Dictionary<string, string>();
_cachedWipedPatterns.Add(fileConfig.FilePath, wipePatterns);
}
if (!wipePatterns.TryGetValue(fileConfig.FilePattern, out var wipedPattern))
{
wipedPattern = $"{fileConfig.FilePath}{WipePattern(fileConfig.FilePattern)}";
wipePatterns.Add(fileConfig.FilePattern, wipedPattern);
}
return wipedPattern;
}
protected string WipePattern(string fileConfigFilePattern)
{
var finalUnpatternedFilename = fileConfigFilePattern;
foreach (var replacersKey in _replacers.Keys)
{
finalUnpatternedFilename = finalUnpatternedFilename.Replace(replacersKey, "");
}
return finalUnpatternedFilename;
}
}
@@ -119,6 +119,43 @@ public class FileSptLoggerReference : BaseSptLoggerReference
get;
set;
}
[JsonPropertyName("filePattern")]
public string FilePattern
{
get;
set;
}
private int _maxFileSizeMb;
[JsonPropertyName("maxFileSizeMB")]
public int MaxFileSizeMb
{
get => _maxFileSizeMb;
set
{
if (value < 0)
{
throw new Exception("Invalid value for MaxFileSizeMb, must be >= 0");
}
_maxFileSizeMb = value;
}
}
private int _maxRollingFiles;
[JsonPropertyName("maxRollingFiles")]
public int MaxRollingFiles
{
get => _maxRollingFiles;
set
{
if (value < 0)
{
throw new Exception("Invalid value for MaxRollingFiles, must be >= 0");
}
_maxRollingFiles = value;
}
}
}
public class ConsoleSptLoggerReference : BaseSptLoggerReference
+12 -3
View File
@@ -4,7 +4,10 @@
"type": "File",
"logLevel": "Trace",
"format": "[%date% %time%][%level%][%logger%] %message%",
"filePath": "./user/logs/kestrel/kestrel.txt",
"filePath": "./user/logs/kestrel/",
"filePattern": "kestrel%DATE%.txt",
"maxFileSizeMB": 10,
"maxRollingFiles": 10,
"filters": [
{
@@ -18,7 +21,10 @@
"type": "File",
"logLevel": "Trace",
"format": "[%date% %time%][%level%][%logger%] %message%",
"filePath": "./user/logs/spt/spt.txt",
"filePath": "./user/logs/spt/",
"filePattern": "spt%DATE%.txt",
"maxFileSizeMB": 10,
"maxRollingFiles": 10,
"filters": [
{
"type": "Exclude",
@@ -36,7 +42,10 @@
"type": "File",
"logLevel": "Trace",
"format": "[%date% %time%][%level%][%logger%] %message%",
"filePath": "./user/logs/requests/requests.txt",
"filePath": "./user/logs/requests/",
"filePattern": "requests%DATE%.txt",
"maxFileSizeMB": 25,
"maxRollingFiles": 10,
"filters": [
{
"type": "Include",
+8 -2
View File
@@ -4,7 +4,10 @@
"type": "File",
"logLevel": "Trace",
"format": "[%date% %time%][%level%][%logger%] %message%",
"filePath": "./user/logs/spt/spt.txt",
"filePath": "./user/logs/spt/",
"filePattern": "spt%DATE%.txt",
"maxFileSizeMB": 10,
"maxRollingFiles": 10,
"filters": [
{
"type": "Exclude",
@@ -22,7 +25,10 @@
"type": "File",
"logLevel": "Trace",
"format": "[%date% %time%][%level%][%logger%] %message%",
"filePath": "./user/logs/requests/requests.txt",
"filePath": "./user/logs/requests/",
"filePattern": "requests%DATE%.txt",
"maxFileSizeMB": 25,
"maxRollingFiles": 10,
"filters": [
{
"type": "Include",