diff --git a/Libraries/SPTarkov.Server.Core/Utils/Logger/Handlers/File/DateFilePatternReplacer.cs b/Libraries/SPTarkov.Server.Core/Utils/Logger/Handlers/File/DateFilePatternReplacer.cs new file mode 100644 index 00000000..12101cb3 --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Utils/Logger/Handlers/File/DateFilePatternReplacer.cs @@ -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")); + } +} diff --git a/Libraries/SPTarkov.Server.Core/Utils/Logger/Handlers/File/IFilePatternReplacer.cs b/Libraries/SPTarkov.Server.Core/Utils/Logger/Handlers/File/IFilePatternReplacer.cs new file mode 100644 index 00000000..a44c998e --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Utils/Logger/Handlers/File/IFilePatternReplacer.cs @@ -0,0 +1,7 @@ +namespace SPTarkov.Server.Core.Utils.Logger.Handlers.File; + +public interface IFilePatternReplacer +{ + string Pattern { get; } + string ReplacePattern(FileSptLoggerReference config, string filePattern); +} diff --git a/Libraries/SPTarkov.Server.Core/Utils/Logger/Handlers/FileLogHandler.cs b/Libraries/SPTarkov.Server.Core/Utils/Logger/Handlers/FileLogHandler.cs index 43f9e04f..8ec091cb 100644 --- a/Libraries/SPTarkov.Server.Core/Utils/Logger/Handlers/FileLogHandler.cs +++ b/Libraries/SPTarkov.Server.Core/Utils/Logger/Handlers/FileLogHandler.cs @@ -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 replacers) : BaseLogHandler, IOnLoad { - private static ConcurrentDictionary _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> _cachedFileNames = new(); + // This section needs to be fully locked as it is a double dictionary lookup + private readonly Lock _cachedFileNamesLocks = new(); + + private readonly Dictionary> _cachedWipedPatterns = new(); + + private readonly Dictionary _replacers = replacers.ToDictionary(kv => kv.Pattern, kv => kv); + + private readonly ConcurrentDictionary _fileLocks = new(); + private readonly ConcurrentDictionary _fileInfos = new(); + private readonly ConcurrentDictionary _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(); + _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(); + _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; + } } diff --git a/Libraries/SPTarkov.Server.Core/Utils/Logger/SptLoggerConfiguration.cs b/Libraries/SPTarkov.Server.Core/Utils/Logger/SptLoggerConfiguration.cs index ae453784..d0815aa5 100644 --- a/Libraries/SPTarkov.Server.Core/Utils/Logger/SptLoggerConfiguration.cs +++ b/Libraries/SPTarkov.Server.Core/Utils/Logger/SptLoggerConfiguration.cs @@ -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 diff --git a/SPTarkov.Server/sptLogger.Development.json b/SPTarkov.Server/sptLogger.Development.json index df22c7c4..d6714f73 100644 --- a/SPTarkov.Server/sptLogger.Development.json +++ b/SPTarkov.Server/sptLogger.Development.json @@ -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", diff --git a/SPTarkov.Server/sptLogger.json b/SPTarkov.Server/sptLogger.json index 646f2b1a..2f2e11ff 100644 --- a/SPTarkov.Server/sptLogger.json +++ b/SPTarkov.Server/sptLogger.json @@ -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",