using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Text; using ZeroLevel.Services.FileSystem; namespace ZeroLevel.Logging { public sealed class TextFileLoggerOptions { public TextFileLoggerOptions() { Folder = null!; LimitFileSize = 0; TextEncoding = DEFAULT_ENCODING; RemoveOlderThen = TimeSpan.MinValue; RemoveOldFiles = false; ZipOldFiles = false; } public static readonly Encoding DEFAULT_ENCODING = Encoding.UTF8; internal string Folder { get; private set; } internal long LimitFileSize { get; private set; } internal Encoding TextEncoding { get; private set; } /// /// Delete files older than (aging period) /// internal TimeSpan RemoveOlderThen { get; private set; } /// /// Delete outdate files /// internal bool RemoveOldFiles { get; private set; } /// /// Archive files /// internal bool ZipOldFiles { get; private set; } internal TextFileLoggerOptions Commit() { if (string.IsNullOrWhiteSpace(Folder)) { throw new ArgumentException("Not set log folder path"); } return this; } /// /// Enable automatic archiving /// public TextFileLoggerOptions EnableAutoArchiving() { this.ZipOldFiles = true; return this; } /// ///Enable automatic deletion of outdate files. /// /// The age of the log file at which removal is required public TextFileLoggerOptions EnableAutoCleaning(TimeSpan age) { this.RemoveOldFiles = true; this.RemoveOlderThen = age; return this; } public TextFileLoggerOptions SetFolderPath(string folder) { if (folder.IndexOf(':') < 0) { this.Folder = Path.Combine(Configuration.BaseDirectory, folder); } else { this.Folder = folder; } return this; } public TextFileLoggerOptions SetMaximumFileSizeInKb(long size) { this.LimitFileSize = size; return this; } public TextFileLoggerOptions SetEncoding(Encoding encoding) { this.TextEncoding = encoding; return this; } internal static TextFileLoggerOptions CreateOptionsBy(IConfiguration config, string path, string logPrefix) { var options = new TextFileLoggerOptions(). SetFolderPath(path); config.DoWithFirst($"{logPrefix}backlog", backlog => { if (backlog > 0) { Log.Backlog(backlog); } }); config.DoWithFirst($"{logPrefix}archive", enable => { if (enable) { options.EnableAutoArchiving(); } }); config.DoWithFirst($"{logPrefix}sizeinkb", size => { if (size >= 1) { options.SetMaximumFileSizeInKb(size); } }); config.DoWithFirst($"{logPrefix}cleanolderdays", days => { if (days > 0) { options.EnableAutoCleaning(TimeSpan.FromDays(days)); } }); return options; } } public sealed class TextFileLogger : ILogger { #region Fields private readonly TextFileLoggerOptions _options; private int _todayCountLogFiles = 0; /// /// Current log file /// private string _currentLogFile; /// /// Stream to output to file /// private TextWriter _writer; /// /// Lock on re-create file /// private readonly object _fileRecreating = new object(); private readonly HashSet _taskKeys = new HashSet(); public static readonly Encoding DEFAULT_ENCODING = Encoding.UTF8; #endregion Fields #region Ctors /// /// Constructor indicating the directory for recording log files, the encoding is set to Unicode by default. /// public TextFileLogger(TextFileLoggerOptions options) { _options = options.Commit(); if (!Directory.Exists(_options.Folder)) { var dir = Directory.CreateDirectory(_options.Folder); if (dir.Exists == false) { throw new ArgumentException($"Can't create or found directory '{_options.Folder}'"); } } RecreateLogFile(); // Maintenance tasks if (_options.LimitFileSize > 0) { _taskKeys.Add(Sheduller.RemindEvery(TimeSpan.FromSeconds(20), CheckRecreateFileLogByOversize)); } if (_options.RemoveOldFiles) { _taskKeys.Add(Sheduller.RemindEvery(TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(60), RemoveOldFiles)); } _taskKeys.Add(Sheduller.RemindEveryNonlinearPeriod(() => (DateTime.Now.AddDays(1).Date - DateTime.Now).Add(TimeSpan.FromMilliseconds(100)), RecreateLogFile)); } #endregion Ctors #region Private member private void RemoveOldFiles() { try { var dir = new DirectoryInfo(_options.Folder); dir. GetFiles(). Do(files => { foreach (var file in files.Where(f => (DateTime.Now - f.CreationTime) > _options.RemoveOlderThen).ToArray()) { file.Delete(); } }); dir = null!; } catch { } } /// /// Closing the current log /// private void CloseCurrentWriter() { if (_writer != null!) { _writer.Flush(); _writer.Close(); _writer.Dispose(); _writer = null!; } } private string GetNextFileName() { string fileName = Path.Combine(_options.Folder, string.Format(CultureInfo.CurrentCulture, "{0:yyyyMMdd}_{1:D4}.txt", DateTime.Now, _todayCountLogFiles)); if (_options.LimitFileSize > 0) { if (!File.Exists(fileName)) { _todayCountLogFiles = 0; } else { while (File.Exists(fileName)) { var length = (new FileInfo(fileName).Length >> 10); if (length >= _options.LimitFileSize) { _todayCountLogFiles++; fileName = Path.Combine(_options.Folder, string.Format(CultureInfo.CurrentCulture, "{0:yyyyMMdd}_{1:D4}.txt", DateTime.Now, _todayCountLogFiles)); } else { break; } } } } return fileName; } private void CheckRecreateFileLogByOversize() { var fi = new FileInfo(_currentLogFile); if (fi.Exists && (fi.Length >> 10) >= _options.LimitFileSize) { RecreateLogFile(); } fi = null!; } /// /// Checking the name of the log file (changes when the date changes to the next day) /// private void RecreateLogFile() { lock (_fileRecreating) { var nextFileName = GetNextFileName(); CloseCurrentWriter(); Stream stream = null!; PackOldLogFile(_currentLogFile); try { _currentLogFile = nextFileName; stream = new FileStream(_currentLogFile, FileMode.Append, FileAccess.Write, FileShare.Read); _writer = new StreamWriter(stream, _options.TextEncoding); stream = null!; } catch { if (stream != null!) { stream.Dispose(); } throw; } } } private void PackOldLogFile(string filePath) { if (null != filePath && File.Exists(filePath) && _options.ZipOldFiles) { using (var stream = new FileStream($"{filePath}.zip", FileMode.Create)) { using (var zipStream = new GZipStream(stream, CompressionLevel.Optimal, false)) { var buffer = new byte[1024 * 1024]; int count = 0; using (BinaryReader reader = new BinaryReader(File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.None))) { while ((count = reader.Read(buffer, 0, buffer.Length)) > 0) { zipStream.Write(buffer, 0, count); } reader.Close(); } buffer = null!; } stream.Close(); } File.Delete(filePath); } } #endregion Private member #region ILog public void Write(LogLevel level, string message) { if (false == _disposed) { lock (_fileRecreating) { if (level == LogLevel.Raw) { _writer.WriteLine(message); } else { var meta = string.Format("[{0:dd'.'MM'.'yyyy HH':'mm':'ss} {1}]\t", DateTime.Now, LogLevelNameMapping.CompactName(level)); _writer.Write(meta); _writer.WriteLine(message); } _writer.Flush(); } } } #endregion ILog #region IDisposable private bool _disposed = false; public void Dispose() { foreach (var tk in _taskKeys) { Sheduller.Remove(tk); } if (false == _disposed) { _disposed = true; CloseCurrentWriter(); } } #endregion IDisposable } public sealed class FileLogger : ILogger { #region Fields /// /// Stream to output to file /// private TextWriter _writer; public static readonly Encoding DEFAULT_ENCODING = Encoding.UTF8; #endregion Fields #region Ctors public FileLogger(string path) { CreateLogFile(PreparePath(path)); } #endregion Ctors #region Private member private static void PrepareFolder(string path) { if (Directory.Exists(path) == false) { Directory.CreateDirectory(path); } } private static string PreparePath(string path) { if (path.IndexOf(':') == -1) { path = Path.Combine(Configuration.BaseDirectory, path); } path = FSUtils.PathCorrection(path); PrepareFolder(Path.GetDirectoryName(path)); return path; } /// /// Closing the current log /// private void CloseCurrentWriter() { if (_writer != null!) { _writer.Flush(); _writer.Close(); _writer.Dispose(); _writer = null!; } } private void CreateLogFile(string path) { Stream stream = null!; try { stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read); _writer = new StreamWriter(stream, DEFAULT_ENCODING); stream = null!; } catch { if (stream != null!) { stream.Dispose(); } throw; } } #endregion Private member #region ILog public void Write(LogLevel level, string message) { if (false == _disposed) { if (level == LogLevel.Raw) { _writer.WriteLine(message); } else { var meta = string.Format("[{0:dd'.'MM'.'yyyy HH':'mm':'ss} {1}]\t", DateTime.Now, LogLevelNameMapping.CompactName(level)); _writer.Write(meta); _writer.WriteLine(message); } _writer.Flush(); } } #endregion ILog #region IDisposable private bool _disposed = false; public void Dispose() { if (false == _disposed) { _disposed = true; CloseCurrentWriter(); } } #endregion IDisposable } }