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"
This commit is contained in:
Michael Bucari-Tovo 2025-07-21 21:43:26 -06:00
parent 40b4915b65
commit 7848366818
8 changed files with 154 additions and 25 deletions

View File

@ -7,10 +7,9 @@
<ItemGroup>
<PackageReference Include="Octokit" Version="14.0.0" />
<!-- Do not remove unused Serilog.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. -->
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.ZipFile" Version="3.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />

View File

@ -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<string>() 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<string>() != 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");
}

View File

@ -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);
}
}
}

View File

@ -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<Configuration.KnownDirectories> KnownDirectories { get; } = new()
{

View File

@ -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<LongPath>(lp => lp.Path)
.CreateLogger();
}
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Destructure.ByTransforming<LongPath>(lp => lp.Path)
.Destructure.With<LogFileFilter>()
.CreateLogger();
}
[Description("The importance of a log event")]
[Description("The importance of a log event")]
public LogEventLevel LogLevel
{
get

View File

@ -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;
/// <summary>
/// Hooks the file sink to set the log file path for the LogFileFilter.
/// </summary>
public class FileSinkHook : FileLifecycleHooks
{
public override Stream OnFileOpened(string path, Stream underlyingStream, Encoding encoding)
{
LogFileFilter.SetLogFilePath(path);
return base.OnFileOpened(path, underlyingStream, encoding);
}
}
/// <summary>
/// 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.
/// </summary>
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 ?? "<null_path>"}. File Contents = {Convert.ToBase64String(fileData)}");
return true;
}
}
result = null;
return false;
}
}

View File

@ -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)
{

View File

@ -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