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:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user