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