From 78483668180df57c3a0128eeb8db0cb5920c3111 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Mon, 21 Jul 2025 21:43:26 -0600 Subject: [PATCH] Write logs to text .log file instead of .zip file The ZipFile sink could cause program hangs. Additionally, the only reason it was ever used was to package verbose AudibleApi account login errors, saving the returned Html page as a file. Otherwise, the zip file only contains a .log text file. - Removed Serilog.Sinks.ZipFile - Add Serilog configuration migration - Added a custom destructure to handle logging files. If any files are logged, they will be written to "LogyyyyMM_AdditionalFiles.zip" --- Source/AppScaffolding/AppScaffolding.csproj | 3 +- Source/AppScaffolding/LibationScaffolding.cs | 25 ++-- .../Controls/Settings/Important.axaml.cs | 5 - .../Settings/ImportantSettingsVM.cs | 8 +- .../Configuration.Logging.cs | 16 +-- Source/LibationFileManager/LogFileFilter.cs | 113 ++++++++++++++++++ .../Dialogs/SettingsDialog.Important.cs | 8 +- Source/Upgrading dotnet version.txt | 1 - 8 files changed, 154 insertions(+), 25 deletions(-) create mode 100644 Source/LibationFileManager/LogFileFilter.cs diff --git a/Source/AppScaffolding/AppScaffolding.csproj b/Source/AppScaffolding/AppScaffolding.csproj index 5d577965..e402c4c4 100644 --- a/Source/AppScaffolding/AppScaffolding.csproj +++ b/Source/AppScaffolding/AppScaffolding.csproj @@ -7,10 +7,9 @@ - + - diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs index 41d25595..2653ab57 100644 --- a/Source/AppScaffolding/LibationScaffolding.cs +++ b/Source/AppScaffolding/LibationScaffolding.cs @@ -115,11 +115,22 @@ namespace AppScaffolding { if (config.GetObject("Serilog") is JObject serilog) { - if (serilog["WriteTo"] is JArray sinks && sinks.FirstOrDefault(s => s["Name"].Value() is "File") is JToken fileSink) + bool fileChanged = false; + if (serilog.SelectToken("$.WriteTo[?(@.Name == 'ZipFile')]", false) is JObject zipFileSink) { - fileSink["Name"] = "ZipFile"; - config.SetNonString(serilog.DeepClone(), "Serilog"); + zipFileSink["Name"] = "File"; + fileChanged = true; } + var hooks = $"{nameof(LibationFileManager)}.{nameof(FileSinkHook)}, {nameof(LibationFileManager)}"; + if (serilog.SelectToken("$.WriteTo[?(@.Name == 'File')].Args", false) is JObject fileSinkArgs + && fileSinkArgs["hooks"]?.Value() != hooks) + { + fileSinkArgs["hooks"] = hooks; + fileChanged = true; + } + + if (fileChanged) + config.SetNonString(serilog.DeepClone(), "Serilog"); return; } @@ -129,17 +140,17 @@ namespace AppScaffolding { "WriteTo", new JArray { // ABOUT SINKS - // Only ZipFile sink is currently used. By user request (June 2024) others packages are included for experimental use. + // Only File sink is currently used. By user request (June 2024) others packages are included for experimental use. // new JObject { {"Name", "Console" } }, // this has caused more problems than it's solved new JObject { - { "Name", "ZipFile" }, + { "Name", "File" }, { "Args", new JObject { // for this sink to work, a path must be provided. we override this below - { "path", Path.Combine(config.LibationFiles, "_Log.log") }, + { "path", Path.Combine(config.LibationFiles, "Log.log") }, { "rollingInterval", "Month" }, // Serilog template formatting examples // - default: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}" @@ -274,7 +285,7 @@ namespace AppScaffolding disableIPv6 = AppContext.TryGetSwitch("System.Net.DisableIPv6", out bool disableIPv6Value), }); - if (InteropFactory.InteropFunctionsType is null) + if (InteropFactory.InteropFunctionsType is null) Serilog.Log.Logger.Warning("WARNING: OSInteropProxy.InteropFunctionsType is null"); } diff --git a/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs b/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs index 03e2f05f..272f5da7 100644 --- a/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs +++ b/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs @@ -58,10 +58,5 @@ namespace LibationAvalonia.Controls.Settings } ThemeComboBox.SelectionChanged += ThemeComboBox_SelectionChanged; } - - public void OpenLogFolderButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName); - } } } diff --git a/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs b/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs index df23a16c..d74710c8 100644 --- a/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs +++ b/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs @@ -57,7 +57,13 @@ namespace LibationAvalonia.ViewModels.Settings config.GridFontScaleFactor = linearRangeToScaleFactor(GridFontScaleFactor); config.GridScaleFactor = linearRangeToScaleFactor(GridScaleFactor); } - public void OpenLogFolderButton() => Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName); + public void OpenLogFolderButton() + { + if (System.IO.File.Exists(LogFileFilter.LogFilePath)) + Go.To.File(LogFileFilter.LogFilePath); + else + Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName); + } public List KnownDirectories { get; } = new() { diff --git a/Source/LibationFileManager/Configuration.Logging.cs b/Source/LibationFileManager/Configuration.Logging.cs index 66ea7cda..595e18d1 100644 --- a/Source/LibationFileManager/Configuration.Logging.cs +++ b/Source/LibationFileManager/Configuration.Logging.cs @@ -1,6 +1,5 @@ using System; using System.ComponentModel; -using System.Linq; using Dinah.Core.Logging; using FileManager; using Microsoft.Extensions.Configuration; @@ -10,7 +9,7 @@ using Serilog.Events; #nullable enable namespace LibationFileManager { - public partial class Configuration + public partial class Configuration { private IConfigurationRoot? configuration; @@ -19,13 +18,14 @@ namespace LibationFileManager configuration = new ConfigurationBuilder() .AddJsonFile(SettingsFilePath, optional: false, reloadOnChange: true) .Build(); - Log.Logger = new LoggerConfiguration() - .ReadFrom.Configuration(configuration) - .Destructure.ByTransforming(lp => lp.Path) - .CreateLogger(); - } + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .Destructure.ByTransforming(lp => lp.Path) + .Destructure.With() + .CreateLogger(); + } - [Description("The importance of a log event")] + [Description("The importance of a log event")] public LogEventLevel LogLevel { get diff --git a/Source/LibationFileManager/LogFileFilter.cs b/Source/LibationFileManager/LogFileFilter.cs new file mode 100644 index 00000000..62c89097 --- /dev/null +++ b/Source/LibationFileManager/LogFileFilter.cs @@ -0,0 +1,113 @@ +using Serilog.Core; +using Serilog.Events; +using Serilog.Sinks.File; +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Reflection; +using System.Text; + +#nullable enable +namespace LibationFileManager; + +/// +/// Hooks the file sink to set the log file path for the LogFileFilter. +/// +public class FileSinkHook : FileLifecycleHooks +{ + public override Stream OnFileOpened(string path, Stream underlyingStream, Encoding encoding) + { + LogFileFilter.SetLogFilePath(path); + return base.OnFileOpened(path, underlyingStream, encoding); + } +} + + +/// +/// Identify log entries which are to be written to files, and save them to a zip file. +/// +/// Files are detected by pattern matching. If the logged type has properties named 'filename' and 'filedata' (case insensitive) +/// with types string and byte[] respectively, the type is destructured and written to the log zip file. +/// +/// The zip file's name will be derived from the active log file's name, with "_AdditionalFiles.zip" appended. +/// +public class LogFileFilter : IDestructuringPolicy +{ + private static readonly object lockObj = new(); + public static string? ZipFilePath { get; private set; } + public static string? LogFilePath { get; private set; } + public static void SetLogFilePath(string? logFilePath) + { + lock(lockObj) + { + (LogFilePath, ZipFilePath) + = File.Exists(logFilePath) && Path.GetDirectoryName(logFilePath) is string logDir + ? (logFilePath, Path.Combine(logDir, $"{Path.GetFileNameWithoutExtension(logFilePath)}_AdditionalFiles.zip")) + : (null, null); + } + } + + private static bool TrySaveLogFile(ref string filename, byte[] fileData, CompressionLevel compression) + { + try + { + lock (lockObj) + { + if (string.IsNullOrEmpty(ZipFilePath)) + return false; + + using var archive = new ZipArchive(File.Open(ZipFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite), ZipArchiveMode.Update, false, Encoding.UTF8); + filename = GetUniqueEntryName(archive, filename); + + var entry = archive.CreateEntry(filename, compression); + using var entryStream = entry.Open(); + entryStream.Write(fileData); + } + + return true; + } + catch + { + return false; + } + } + + private static string GetUniqueEntryName(ZipArchive archive, string filename) + { + var entryFileName = filename; + for (int i = 1; archive.Entries.Any(e => e.Name == entryFileName); i++) + { + entryFileName = $"{Path.GetFileNameWithoutExtension(filename)}_({i++}){Path.GetExtension(filename)}"; + } + return entryFileName; + } + + public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, [NotNullWhen(true)] out LogEventPropertyValue? result) + { + if (value?.GetType().GetProperties() is PropertyInfo[] properties && properties.Length >= 2 + && properties.FirstOrDefault(p => p.Name.Equals("filename", StringComparison.InvariantCultureIgnoreCase)) is PropertyInfo filenameProperty && filenameProperty.PropertyType == typeof(string) + && properties.FirstOrDefault(p => p.Name.Equals("fileData", StringComparison.InvariantCultureIgnoreCase)) is PropertyInfo fileDataProperty && fileDataProperty.PropertyType == typeof(byte[])) + { + var filename = filenameProperty.GetValue(value) as string; + var fileData = fileDataProperty.GetValue(value) as byte[]; + + if (filename != null && fileData != null && fileData.Length > 0) + { + var compressionProperty = properties.FirstOrDefault(f => f.PropertyType == typeof(CompressionLevel)); + var compression = compressionProperty?.GetValue(value) is CompressionLevel c ? c : CompressionLevel.Fastest; + + result + = TrySaveLogFile(ref filename, fileData, compression) + ? propertyValueFactory.CreatePropertyValue($"Log file '{filename}' saved in {ZipFilePath}") + : propertyValueFactory.CreatePropertyValue($"Log file '{filename}' could not be saved in {ZipFilePath ?? ""}. File Contents = {Convert.ToBase64String(fileData)}"); + + return true; + } + } + + result = null; + return false; + } +} diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.Important.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.Important.cs index 7a18b190..29c235be 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.Important.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.Important.cs @@ -11,7 +11,13 @@ namespace LibationWinForms.Dialogs { public partial class SettingsDialog { - private void logsBtn_Click(object sender, EventArgs e) => Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName); + private void logsBtn_Click(object sender, EventArgs e) + { + if (File.Exists(LogFileFilter.LogFilePath)) + Go.To.File(LogFileFilter.LogFilePath); + else + Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName); + } private void Load_Important(Configuration config) { diff --git a/Source/Upgrading dotnet version.txt b/Source/Upgrading dotnet version.txt index a81879cf..e47b43ae 100644 --- a/Source/Upgrading dotnet version.txt +++ b/Source/Upgrading dotnet version.txt @@ -6,4 +6,3 @@ For reasons I cannot figure out, upgrading the major .net version breaks Libatio * https://github.com/Mbucari/AAXClean.Codecs * https://github.com/Mbucari/AAXClean -* https://github.com/Mbucari/serilog-sinks-zipfile