diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs index 611e3108f..dc3153a46 100644 --- a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs @@ -54,7 +54,13 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop if (isCliLogging) { log = new ScopedLoggerFactory().CreateLogger( - @"C:\TBF\Logs\CliRunner.txt" + @"C:\TBF\Logs\CliRunner.txt", + 10, // maxFileSizeMB + 7, // maxBackups + log4net.Core.Level.Debug, + true, // zipRolledFiles + true, // singleZipPerDay + TimeSpan.FromMinutes(2) // zipScanInterval ); } } diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/ScopedLoggerFactory.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/ScopedLoggerFactory.cs index b192ea9a7..a4f290401 100644 --- a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/ScopedLoggerFactory.cs +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/ScopedLoggerFactory.cs @@ -1,14 +1,31 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; using log4net; using log4net.Appender; using log4net.Core; using log4net.Layout; using log4net.Repository.Hierarchy; + namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop { - public class ScopedLoggerFactory + public sealed class ScopedLoggerFactory { - public ILog CreateLogger(string logFile, Level level = null) + private static readonly LogZipService ZipService = new LogZipService(); + + public ILog CreateLogger( + string logFile, + int maxFileSizeMB, + int maxBackups, + Level level, + bool zipRolledFiles, + bool singleZipPerDay, + TimeSpan zipScanInterval) { var layout = new PatternLayout("%date %-5level %logger - %message%newline"); layout.ActivateOptions(); @@ -18,8 +35,15 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop Name = typeof(T).Name + "Appender", File = logFile, AppendToFile = true, - RollingStyle = RollingFileAppender.RollingMode.Date, + RollingStyle = RollingFileAppender.RollingMode.Composite, + DatePattern = "'.'yyyyMMdd", + StaticLogFileName = true, + PreserveLogFileNameExtension = true, + MaxSizeRollBackups = maxBackups, + MaximumFileSize = maxFileSizeMB + "MB", + CountDirection = 1, LockingModel = new FileAppender.MinimalLock(), + ImmediateFlush = true, Layout = layout }; appender.ActivateOptions(); @@ -27,12 +51,122 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop var hierarchy = (Hierarchy)LogManager.GetRepository(); var logger = (log4net.Repository.Hierarchy.Logger)hierarchy.GetLogger(typeof(T).FullName); - logger.RemoveAllAppenders(); // isolate this logger + logger.RemoveAllAppenders(); logger.AddAppender(appender); logger.Level = level ?? Level.Debug; - logger.Hierarchy.Configured = true; + logger.Additivity = false; + hierarchy.Configured = true; + + if (zipRolledFiles) + { + ZipService.StartFor(logFile, singleZipPerDay, zipScanInterval); + } return LogManager.GetLogger(typeof(T)); } } + + internal sealed class LogZipService : IDisposable + { + private readonly ConcurrentDictionary _timers = + new ConcurrentDictionary(); + + public void StartFor(string baseLogFile, bool singleZipPerDay, TimeSpan interval) + { + _timers.GetOrAdd(baseLogFile, delegate (string key) + { + // .NET Framework Timer ctor: Timer(Callback, state, dueTimeMs, periodMs) + return new Timer( + new TimerCallback(delegate (object state) { CompressRolledFiles(key, singleZipPerDay); }), + null, + (int)TimeSpan.FromSeconds(20).TotalMilliseconds, + (int)interval.TotalMilliseconds + ); + }); + } + + private static void CompressRolledFiles(string baseLogFile, bool singleZipPerDay) + { + try + { + var dir = Path.GetDirectoryName(baseLogFile); + var fileName = Path.GetFileName(baseLogFile); + var nameNoExt = Path.GetFileNameWithoutExtension(fileName); + var ext = Path.GetExtension(fileName); + + var candidates = Directory.EnumerateFiles(dir) + .Where(f => + !f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) && + !StringComparer.OrdinalIgnoreCase.Equals(f, baseLogFile) && + Regex.IsMatch(Path.GetFileName(f), + "^" + Regex.Escape(nameNoExt) + "\\.\\d{8}(\\.\\d+)?" + Regex.Escape(ext) + "$")); + + foreach (var f in candidates) + { + if ((DateTime.UtcNow - File.GetLastWriteTimeUtc(f)).TotalSeconds < 15) + continue; + + if (!CanOpenExclusive(f)) continue; + + string zipPath = singleZipPerDay + ? Path.Combine(dir, nameNoExt + "-" + ExtractDatePart(f) + ".zip") + : f + ".zip"; + + AddFileToZip(zipPath, f); + File.Delete(f); + } + } + catch + { + // ignore errors + } + } + + private static void AddFileToZip(string zipPath, string fileToAdd) + { + var entryName = Path.GetFileName(fileToAdd); + if (!File.Exists(zipPath)) + { + using (var archive = ZipFile.Open(zipPath, ZipArchiveMode.Create)) + { + archive.CreateEntryFromFile(fileToAdd, entryName, CompressionLevel.Optimal); + } + } + else + { + using (var archive = ZipFile.Open(zipPath, ZipArchiveMode.Update)) + { + var existing = archive.GetEntry(entryName); + if (existing != null) existing.Delete(); + archive.CreateEntryFromFile(fileToAdd, entryName, CompressionLevel.Optimal); + } + } + } + + private static bool CanOpenExclusive(string path) + { + try + { + using (FileStream fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.None)) + { + return true; + } + } + catch { return false; } + } + + private static string ExtractDatePart(string rolledFilePath) + { + var name = Path.GetFileName(rolledFilePath); + var m = Regex.Match(name, @"\.(\d{8})(?:\.\d+)?(\.[^.]+)$"); + if (m.Success) return m.Groups[1].Value; + return DateTime.UtcNow.ToString("yyyyMMdd"); + } + + public void Dispose() + { + foreach (var t in _timers.Values) t.Dispose(); + _timers.Clear(); + } + } } \ No newline at end of file diff --git a/TBF/TBF.csproj b/TBF/TBF.csproj index d5e43107b..890022ecc 100644 --- a/TBF/TBF.csproj +++ b/TBF/TBF.csproj @@ -139,6 +139,8 @@ + +