New setting: dynamically change log level without app restart

This commit is contained in:
Robert McRackan 2021-07-21 13:38:22 -04:00
parent b6fe3ae009
commit 35fe3ae786
7 changed files with 205 additions and 102 deletions

View File

@ -4,8 +4,12 @@ using System.ComponentModel;
using System.IO;
using System.Linq;
using Dinah.Core;
using Dinah.Core.Logging;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serilog;
using Serilog.Events;
namespace FileManager
{
@ -34,28 +38,6 @@ namespace FileManager
#region persistent configuration settings/values
#region // properties to test reflection
/*
// field should NOT be populated
public string TestField;
// int should NOT be populated
public int TestInt { get; set; }
// read-only should NOT be populated
public string TestGet { get; } // get only: should NOT get auto-populated
// set-only should NOT be populated
public string TestSet { private get; set; }
// get and set: SHOULD be auto-populated
public string TestGetSet { get; set; }
*/
#endregion
// settings will be persisted when all are true
// - property (not field)
// - string
// - public getter
// - public setter
// note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app
// default setting and directory creation occur in class responsible for files.
@ -65,8 +47,8 @@ namespace FileManager
private PersistentDictionary persistentDictionary;
public object GetObject(string propertyName) => persistentDictionary.GetObject(propertyName);
public void SetObject(string propertyName, object newValue) => persistentDictionary.Set(propertyName, newValue);
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue) => persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue);
public void SetObject(string propertyName, object newValue) => persistentDictionary.SetNonString(propertyName, newValue);
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false) => persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging);
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
@ -87,7 +69,7 @@ namespace FileManager
public string Books
{
get => persistentDictionary.GetString(nameof(Books));
set => persistentDictionary.Set(nameof(Books), value);
set => persistentDictionary.SetString(nameof(Books), value);
}
// temp/working dir(s) should be outside of dropbox
@ -95,21 +77,21 @@ namespace FileManager
public string InProgress
{
get => persistentDictionary.GetString(nameof(InProgress));
set => persistentDictionary.Set(nameof(InProgress), value);
set => persistentDictionary.SetString(nameof(InProgress), value);
}
[Description("Allow Libation for fix up audiobook metadata?")]
public bool AllowLibationFixup
{
get => persistentDictionary.Get<bool>(nameof(AllowLibationFixup));
set => persistentDictionary.Set(nameof(AllowLibationFixup), value);
get => persistentDictionary.GetNonString<bool>(nameof(AllowLibationFixup));
set => persistentDictionary.SetNonString(nameof(AllowLibationFixup), value);
}
[Description("Decrypt to lossy format?")]
public bool DecryptToLossy
{
get => persistentDictionary.Get<bool>(nameof(DecryptToLossy));
set => persistentDictionary.Set(nameof(DecryptToLossy), value);
get => persistentDictionary.GetNonString<bool>(nameof(DecryptToLossy));
set => persistentDictionary.SetNonString(nameof(DecryptToLossy), value);
}
#endregion
@ -170,6 +152,53 @@ namespace FileManager
}
#endregion
#region logging
private IConfigurationRoot configuration;
public void ConfigureLogging()
{
configuration = new ConfigurationBuilder()
.AddJsonFile(SettingsFilePath, optional: false, reloadOnChange: true)
.Build();
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
}
[Description("The importance of a log event")]
public LogEventLevel LogLevel
{
get
{
try
{
var logLevelStr = persistentDictionary.GetStringFromJsonPath("Serilog", "MinimumLevel");
var logLevelEnum = Enum<LogEventLevel>.Parse(logLevelStr);
return logLevelEnum;
}
catch
{
return LogEventLevel.Information;
}
}
set
{
persistentDictionary.SetWithJsonPath("Serilog", "MinimumLevel", value.ToString());
configuration.Reload();
Log.Logger.Information("Updated LogLevel MinimumLevel. {@DebugInfo}", new
{
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
LogLevel_Debug_Enabled = Log.Logger.IsDebugEnabled(),
LogLevel_Information_Enabled = Log.Logger.IsInformationEnabled(),
LogLevel_Warning_Enabled = Log.Logger.IsWarningEnabled(),
LogLevel_Error_Enabled = Log.Logger.IsErrorEnabled(),
LogLevel_Fatal_Enabled = Log.Logger.IsFatalEnabled()
});
}
}
#endregion
#region singleton stuff
public static Configuration Instance { get; } = new Configuration();
private Configuration() { }
@ -191,7 +220,12 @@ namespace FileManager
// must write here before SettingsFilePath in next step reads cache
libationFilesPathCache = getLiberationFilesSettingFromJson();
// load json values into memory. create settings if not exists
// Config init in Program.ensureSerilogConfig() only happens when serilog setting is first created (prob on 1st run).
// This Set() enforces current LibationFiles every time we restart Libation or redirect LibationFiles
var logPath = Path.Combine(LibationFiles, "Log.log");
SetWithJsonPath("Serilog.WriteTo[1].Args", "path", logPath, true);
configuration?.Reload();
persistentDictionary = new PersistentDictionary(SettingsFilePath);
return libationFilesPathCache;
@ -259,18 +293,19 @@ namespace FileManager
if (startingContents == endingContents)
return true;
try
{
Serilog.Log.Logger.Information("Libation files changed {@DebugInfo}", new { APPSETTINGS_JSON, LIBATION_FILES_KEY, directory });
}
catch { }
// now it's set in the file again but no settings have moved yet
File.WriteAllText(APPSETTINGS_JSON, endingContents);
try
{
Log.Logger.Information("Libation files changed {@DebugInfo}", new { APPSETTINGS_JSON, LIBATION_FILES_KEY, directory });
}
catch { }
//// attempting this will try to change the settings file which has not yet been moved
//// after this is fixed, can remove it from Program.configureLogging()
// var logPath = Path.Combine(LibationFiles, "Log.log");
// SetWithJsonPath("Serilog.WriteTo[1].Args", "path", logPath);
// SetWithJsonPath("Serilog.WriteTo[1].Args", "path", logPath, true);
return true;
}

View File

@ -5,6 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
<PackageReference Include="Octokit" Version="0.50.0" />
<PackageReference Include="Polly" Version="7.2.2" />
</ItemGroup>

View File

@ -47,7 +47,7 @@ namespace FileManager
return stringCache[propertyName];
}
public T Get<T>(string propertyName)
public T GetNonString<T>(string propertyName)
{
var obj = GetObject(propertyName);
if (obj is null) return default;
@ -68,10 +68,32 @@ namespace FileManager
return objectCache[propertyName];
}
public string GetStringFromJsonPath(string jsonPath, string propertyName) => GetStringFromJsonPath($"{jsonPath}.{propertyName}");
public string GetStringFromJsonPath(string jsonPath)
{
if (!stringCache.ContainsKey(jsonPath))
{
try
{
var jObject = readFile();
var token = jObject.SelectToken(jsonPath);
if (token is null)
return null;
stringCache[jsonPath] = (string)token;
}
catch
{
return null;
}
}
return stringCache[jsonPath];
}
public bool Exists(string propertyName) => readFile().ContainsKey(propertyName);
private object locker { get; } = new object();
public void Set(string propertyName, string newValue)
public void SetString(string propertyName, string newValue)
{
// only do this check in string cache, NOT object cache
if (stringCache.ContainsKey(propertyName) && stringCache[propertyName] == newValue)
@ -83,7 +105,7 @@ namespace FileManager
writeFile(propertyName, newValue);
}
public void Set(string propertyName, object newValue)
public void SetNonString(string propertyName, object newValue)
{
// set cache
objectCache[propertyName] = newValue;
@ -97,19 +119,6 @@ namespace FileManager
if (IsReadOnly)
return;
try
{
var str = newValue?.ToString();
var formattedValue
= str is null ? "[null]"
: string.IsNullOrEmpty(str) ? "[empty]"
: string.IsNullOrWhiteSpace(str) ? $"[whitespace. Length={str.Length}]"
: str.Length > 100 ? $"[Length={str.Length}] {str[0..50]}...{str[^50..^0]}"
: str;
Serilog.Log.Logger.Information($"Config changed. {propertyName}={formattedValue}");
}
catch { }
// write new setting to file
lock (locker)
{
@ -119,31 +128,67 @@ namespace FileManager
jObject[propertyName] = newValue;
var endContents = JsonConvert.SerializeObject(jObject, Formatting.Indented);
if (startContents != endContents)
if (startContents == endContents)
return;
File.WriteAllText(Filepath, endContents);
}
try
{
var str = formatValueForLog(newValue?.ToString());
Serilog.Log.Logger.Information("Config changed. {@DebugInfo}", new { propertyName, newValue = str });
}
catch { }
}
// special case: no caching. no logging
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue)
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false)
{
if (IsReadOnly)
return;
var path = $"{jsonPath}.{propertyName}";
{
// only do this check in string cache, NOT object cache
if (stringCache.ContainsKey(path) && stringCache[path] == newValue)
return;
// set cache
stringCache[path] = newValue;
}
lock (locker)
{
var jObject = readFile();
var token = jObject.SelectToken(jsonPath);
var oldValue = (string)token[propertyName];
var oldValue = token.Value<string>(propertyName);
if (oldValue == newValue)
return;
if (oldValue != newValue)
{
token[propertyName] = newValue;
File.WriteAllText(Filepath, JsonConvert.SerializeObject(jObject, Formatting.Indented));
}
if (!suppressLogging)
{
try
{
var str = formatValueForLog(newValue?.ToString());
Serilog.Log.Logger.Information("Config changed. {@DebugInfo}", new { jsonPath, propertyName, newValue = str });
}
catch { }
}
}
private static string formatValueForLog(string value)
=> value is null ? "[null]"
: string.IsNullOrEmpty(value) ? "[empty]"
: string.IsNullOrWhiteSpace(value) ? $"[whitespace. Length={value.Length}]"
: value.Length > 100 ? $"[Length={value.Length}] {value[0..50]}...{value[^50..^0]}"
: value;
private JObject readFile()
{
var settingsJsonContents = File.ReadAllText(Filepath);

View File

@ -13,7 +13,7 @@
<!-- <PublishSingleFile>true</PublishSingleFile> -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Version>5.2.3.1</Version>
<Version>5.3.0.1</Version>
</PropertyGroup>
<ItemGroup>

View File

@ -10,7 +10,6 @@ using FileManager;
using InternalUtilities;
using LibationWinForms;
using LibationWinForms.Dialogs;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serilog;
@ -285,27 +284,16 @@ namespace LibationLauncher
}
// to restore original: Console.SetOut(origOut);
private static System.IO.TextWriter origOut { get; } = Console.Out;
private static TextWriter origOut { get; } = Console.Out;
private static void configureLogging(Configuration config)
{
{
// always override path here.
// init in ensureSerilogConfig() only happens when serilog setting is first created (prob on 1st run).
// the override here uses current libation files every time we restart libation
var logPath = Path.Combine(config.LibationFiles, "Log.log");
config.SetWithJsonPath("Serilog.WriteTo[1].Args", "path", logPath);
}
config.ConfigureLogging();
var configuration = new ConfigurationBuilder()
.AddJsonFile(config.SettingsFilePath, optional: false, reloadOnChange: true)
.Build();
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
// Fwd Console to serilog. Serilog also write to Console (should probably change this) so it might be asking for trouble.
// First SerilogTextWriter needs to be more robust and tested. Esp the Write() methods
// Fwd Console to serilog.
// Serilog also write to Console (should probably change this) so it might be asking for trouble.
// SerilogTextWriter needs to be more robust and tested. Esp the Write() methods.
// Empirical testing so far has shown no issues.
Console.SetOut(new MultiTextWriter(origOut, new SerilogTextWriter()));
// .Here() captures debug info via System.Runtime.CompilerServices attributes. Warning: expensive

View File

@ -33,13 +33,15 @@
this.saveBtn = new System.Windows.Forms.Button();
this.cancelBtn = new System.Windows.Forms.Button();
this.advancedSettingsGb = new System.Windows.Forms.GroupBox();
this.logsBtn = new System.Windows.Forms.Button();
this.convertLossyRb = new System.Windows.Forms.RadioButton();
this.convertLosslessRb = new System.Windows.Forms.RadioButton();
this.inProgressSelectControl = new LibationWinForms.Dialogs.DirectorySelectControl();
this.allowLibationFixupCbox = new System.Windows.Forms.CheckBox();
this.booksSelectControl = new LibationWinForms.Dialogs.DirectoryOrCustomSelectControl();
this.booksGb = new System.Windows.Forms.GroupBox();
this.logsBtn = new System.Windows.Forms.Button();
this.loggingLevelLbl = new System.Windows.Forms.Label();
this.loggingLevelCb = new System.Windows.Forms.ComboBox();
this.advancedSettingsGb.SuspendLayout();
this.booksGb.SuspendLayout();
this.SuspendLayout();
@ -67,7 +69,7 @@
// saveBtn
//
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.saveBtn.Location = new System.Drawing.Point(714, 380);
this.saveBtn.Location = new System.Drawing.Point(714, 409);
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.saveBtn.Name = "saveBtn";
this.saveBtn.Size = new System.Drawing.Size(88, 27);
@ -80,7 +82,7 @@
//
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancelBtn.Location = new System.Drawing.Point(832, 380);
this.cancelBtn.Location = new System.Drawing.Point(832, 409);
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
@ -93,21 +95,30 @@
//
this.advancedSettingsGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.advancedSettingsGb.Controls.Add(this.logsBtn);
this.advancedSettingsGb.Controls.Add(this.convertLossyRb);
this.advancedSettingsGb.Controls.Add(this.convertLosslessRb);
this.advancedSettingsGb.Controls.Add(this.inProgressSelectControl);
this.advancedSettingsGb.Controls.Add(this.allowLibationFixupCbox);
this.advancedSettingsGb.Controls.Add(this.inProgressDescLbl);
this.advancedSettingsGb.Location = new System.Drawing.Point(12, 141);
this.advancedSettingsGb.Location = new System.Drawing.Point(12, 170);
this.advancedSettingsGb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.advancedSettingsGb.Name = "advancedSettingsGb";
this.advancedSettingsGb.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.advancedSettingsGb.Size = new System.Drawing.Size(908, 226);
this.advancedSettingsGb.TabIndex = 3;
this.advancedSettingsGb.TabIndex = 5;
this.advancedSettingsGb.TabStop = false;
this.advancedSettingsGb.Text = "Advanced settings for control freaks";
//
// logsBtn
//
this.logsBtn.Location = new System.Drawing.Point(262, 141);
this.logsBtn.Name = "logsBtn";
this.logsBtn.Size = new System.Drawing.Size(132, 23);
this.logsBtn.TabIndex = 4;
this.logsBtn.Text = "Open log folder";
this.logsBtn.UseVisualStyleBackColor = true;
this.logsBtn.Click += new System.EventHandler(this.logsBtn_Click);
//
// convertLossyRb
//
this.convertLossyRb.AutoSize = true;
@ -170,19 +181,27 @@
this.booksGb.Location = new System.Drawing.Point(12, 12);
this.booksGb.Name = "booksGb";
this.booksGb.Size = new System.Drawing.Size(908, 123);
this.booksGb.TabIndex = 6;
this.booksGb.TabIndex = 1;
this.booksGb.TabStop = false;
this.booksGb.Text = "Books location";
//
// logsBtn
// loggingLevelLbl
//
this.logsBtn.Location = new System.Drawing.Point(826, 18);
this.logsBtn.Name = "logsBtn";
this.logsBtn.Size = new System.Drawing.Size(75, 64);
this.logsBtn.TabIndex = 3;
this.logsBtn.Text = "Open log\r\nfiles folder";
this.logsBtn.UseVisualStyleBackColor = true;
this.logsBtn.Click += new System.EventHandler(this.logsBtn_Click);
this.loggingLevelLbl.AutoSize = true;
this.loggingLevelLbl.Location = new System.Drawing.Point(12, 144);
this.loggingLevelLbl.Name = "loggingLevelLbl";
this.loggingLevelLbl.Size = new System.Drawing.Size(78, 15);
this.loggingLevelLbl.TabIndex = 2;
this.loggingLevelLbl.Text = "Logging level";
//
// loggingLevelCb
//
this.loggingLevelCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.loggingLevelCb.FormattingEnabled = true;
this.loggingLevelCb.Location = new System.Drawing.Point(96, 141);
this.loggingLevelCb.Name = "loggingLevelCb";
this.loggingLevelCb.Size = new System.Drawing.Size(129, 23);
this.loggingLevelCb.TabIndex = 3;
//
// SettingsDialog
//
@ -190,7 +209,10 @@
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(933, 421);
this.ClientSize = new System.Drawing.Size(933, 450);
this.Controls.Add(this.logsBtn);
this.Controls.Add(this.loggingLevelCb);
this.Controls.Add(this.loggingLevelLbl);
this.Controls.Add(this.booksGb);
this.Controls.Add(this.advancedSettingsGb);
this.Controls.Add(this.cancelBtn);
@ -206,6 +228,7 @@
this.booksGb.ResumeLayout(false);
this.booksGb.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
@ -222,5 +245,7 @@
private System.Windows.Forms.RadioButton convertLosslessRb;
private System.Windows.Forms.GroupBox booksGb;
private System.Windows.Forms.Button logsBtn;
private System.Windows.Forms.Label loggingLevelLbl;
private System.Windows.Forms.ComboBox loggingLevelCb;
}
}

View File

@ -18,6 +18,13 @@ namespace LibationWinForms.Dialogs
if (this.DesignMode)
return;
{
loggingLevelCb.Items.Clear();
foreach (var level in Enum<Serilog.Events.LogEventLevel>.GetValues())
loggingLevelCb.Items.Add(level);
loggingLevelCb.SelectedItem = config.LogLevel;
}
this.booksLocationDescLbl.Text = desc(nameof(config.Books));
this.inProgressDescLbl.Text = desc(nameof(config.InProgress));
@ -65,11 +72,6 @@ namespace LibationWinForms.Dialogs
private void saveBtn_Click(object sender, EventArgs e)
{
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
config.DecryptToLossy = convertLossyRb.Checked;
config.InProgress = inProgressSelectControl.SelectedDirectory;
var newBooks = booksSelectControl.SelectedDirectory;
if (string.IsNullOrWhiteSpace(newBooks))
@ -92,6 +94,13 @@ namespace LibationWinForms.Dialogs
config.Books = newBooks;
config.LogLevel = (Serilog.Events.LogEventLevel)loggingLevelCb.SelectedItem;
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
config.DecryptToLossy = convertLossyRb.Checked;
config.InProgress = inProgressSelectControl.SelectedDirectory;
this.DialogResult = DialogResult.OK;
this.Close();
}