Files
SPT-Server-Build/Libraries/SPTarkov.Server.Core/Utils/Logger/Handlers/FileLogHandler.cs
T
2025-07-28 19:39:29 +00:00

214 lines
7.2 KiB
C#

using System.Collections.Concurrent;
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(IEnumerable<IFilePatternReplacer> replacers) : BaseLogHandler, IOnLoad
{
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
{
get { return LoggerType.File; }
}
public override void Log(SptLogMessage message, BaseSptLoggerReference reference)
{
var config = (reference as FileSptLoggerReference)!;
if (string.IsNullOrEmpty(config.FilePath) || string.IsNullOrEmpty(config.FilePattern))
{
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(config.FilePath))
{
Directory.CreateDirectory(config.FilePath);
}
// The AppendAllText will create the file as long as the directory exists
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;
}
}