From 5b158b2fbd4749295afd5c4fa971799a9fb04e40 Mon Sep 17 00:00:00 2001 From: Deepratna Awale Date: Fri, 6 Nov 2026 10:24:56 +0000 Subject: [PATCH 1/2] Add a log level setting for the log file and console The file log was fixed at Debug and the console at Trace, with no way for users to reduce how much gets written (LykosAI/StabilityMatrix#1790). Add a Log Level option under Settings > System Settings. It sets the minimum level of the catch-all file and console rules, is applied when settings load and whenever it changes, and leaves the per-logger filters (System.*, Microsoft.*, etc.) untouched. The default stays at Debug so existing behaviour is unchanged. --- StabilityMatrix.Avalonia/App.axaml.cs | 8 ++ .../Languages/Resources.Designer.cs | 18 ++++ .../Languages/Resources.resx | 6 ++ .../Logging/AppLogLevelApplier.cs | 73 ++++++++++++++++ .../Settings/MainSettingsViewModel.cs | 12 +++ .../Views/Settings/MainSettingsPage.axaml | 7 ++ .../Models/Settings/AppLogLevel.cs | 16 ++++ .../Models/Settings/Settings.cs | 5 ++ .../Avalonia/AppLogLevelApplierTests.cs | 83 +++++++++++++++++++ 9 files changed, 228 insertions(+) create mode 100644 StabilityMatrix.Avalonia/Logging/AppLogLevelApplier.cs create mode 100644 StabilityMatrix.Core/Models/Settings/AppLogLevel.cs create mode 100644 StabilityMatrix.Tests/Avalonia/AppLogLevelApplierTests.cs diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 3910cc874..ad87be718 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -413,6 +413,14 @@ private void ConfigureServiceProvider() var settingsManager = Services.GetRequiredService(); + // Apply the user's log level once settings load, and whenever it changes + settingsManager.Loaded += (_, _) => + AppLogLevelApplier.ApplyToActiveConfiguration(settingsManager.Settings.LogLevel); + settingsManager.RegisterPropertyChangedHandler( + settings => settings.LogLevel, + AppLogLevelApplier.ApplyToActiveConfiguration + ); + if (Program.Args.DataDirectoryOverride is not null) { var normalizedDataDirPath = Path.GetFullPath(Program.Args.DataDirectoryOverride); diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index 44bca5b52..b0d5d3b22 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -2643,6 +2643,24 @@ public static string Label_LoginRequiredMessage { } } + /// + /// Looks up a localized string similar to Log Level. + /// + public static string Label_LogLevel { + get { + return ResourceManager.GetString("Label_LogLevel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Minimum level of messages written to the log files and console. Lower levels include more detail but produce larger logs. + /// + public static string Label_LogLevel_Description { + get { + return ResourceManager.GetString("Label_LogLevel_Description", resourceCulture); + } + } + /// /// Looks up a localized string similar to Logs. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index ede9d4b94..e88e5837d 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -1883,4 +1883,10 @@ Account tokens will not be viewable after saving, please make a note of them if Could not load this page. Check your connection and try again. + + Log Level + + + Minimum level of messages written to the log files and console. Lower levels include more detail but produce larger logs. + diff --git a/StabilityMatrix.Avalonia/Logging/AppLogLevelApplier.cs b/StabilityMatrix.Avalonia/Logging/AppLogLevelApplier.cs new file mode 100644 index 000000000..f70ee9cca --- /dev/null +++ b/StabilityMatrix.Avalonia/Logging/AppLogLevelApplier.cs @@ -0,0 +1,73 @@ +using NLog; +using NLog.Config; +using NLog.Targets; +using NLog.Targets.Wrappers; +using StabilityMatrix.Core.Models.Settings; + +namespace StabilityMatrix.Avalonia.Logging; + +/// +/// Applies the user's to the app's file and console log rules at runtime. +/// +public static class AppLogLevelApplier +{ + public static LogLevel ToNLogLevel(this AppLogLevel level) => + level switch + { + AppLogLevel.Trace => LogLevel.Trace, + AppLogLevel.Debug => LogLevel.Debug, + AppLogLevel.Info => LogLevel.Info, + AppLogLevel.Warn => LogLevel.Warn, + AppLogLevel.Error => LogLevel.Error, + _ => LogLevel.Debug, + }; + + /// + /// Sets the minimum level of the catch-all rules writing to the log file or console in + /// . Rules for specific loggers (such as the filters that + /// limit System.* and Microsoft.* to warnings) are left unchanged. + /// + /// The number of rules updated. + public static int Apply(LoggingConfiguration? configuration, AppLogLevel level) + { + if (configuration is null) + return 0; + + var minLevel = level.ToNLogLevel(); + var updated = 0; + + foreach (var rule in configuration.LoggingRules) + { + if (rule.LoggerNamePattern != "*") + continue; + + if (!rule.Targets.Select(Unwrap).Any(t => t is FileTarget or RichConsoleTarget)) + continue; + + rule.SetLoggingLevels(minLevel, LogLevel.Fatal); + updated++; + } + + return updated; + } + + /// + /// Applies to the active NLog configuration and refreshes existing loggers. + /// + public static void ApplyToActiveConfiguration(AppLogLevel level) + { + if (Apply(LogManager.Configuration, level) > 0) + { + LogManager.ReconfigExistingLoggers(); + } + } + + private static Target Unwrap(Target target) + { + while (target is WrapperTargetBase { WrappedTarget: { } inner }) + { + target = inner; + } + return target; + } +} diff --git a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs index efb77e2e8..b4cd5e10c 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs @@ -126,6 +126,11 @@ public partial class MainSettingsViewModel : PageViewModelBase public IReadOnlyList HolidayModes { get; } = Enum.GetValues().ToList(); + public IReadOnlyList LogLevels { get; } = Enum.GetValues().ToList(); + + [ObservableProperty] + private AppLogLevel selectedLogLevel; + [ObservableProperty] private float selectedAnimationScale; @@ -312,6 +317,13 @@ ICivitBaseModelTypeService baseModelTypeService true ); + settingsManager.RelayPropertyFor( + this, + vm => vm.SelectedLogLevel, + settings => settings.LogLevel, + true + ); + settingsManager.RelayPropertyFor( this, vm => vm.AutoLoadCivitModels, diff --git a/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml b/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml index 0bb34f7aa..bdac71c45 100644 --- a/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml @@ -662,6 +662,13 @@ + + + + + + + +/// Minimum level of messages written to the app log file and console. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum AppLogLevel +{ + Trace, + Debug, + Info, + Warn, + Error, +} diff --git a/StabilityMatrix.Core/Models/Settings/Settings.cs b/StabilityMatrix.Core/Models/Settings/Settings.cs index 527af435e..1367b8afa 100644 --- a/StabilityMatrix.Core/Models/Settings/Settings.cs +++ b/StabilityMatrix.Core/Models/Settings/Settings.cs @@ -238,6 +238,11 @@ public bool IsEnvironmentVariableUserOverridden(string key) [Obsolete("Use OutputsPageResizeFactor instead")] public Size OutputsImageSize { get; set; } = new(300, 300); public HolidayMode HolidayModeSetting { get; set; } = HolidayMode.Automatic; + + /// + /// Minimum level of messages written to the log file and console + /// + public AppLogLevel LogLevel { get; set; } = AppLogLevel.Debug; public bool IsWorkflowInfiniteScrollEnabled { get; set; } = true; public bool IsOutputsTreeViewEnabled { get; set; } = true; public CheckpointSortMode CheckpointSortMode { get; set; } = CheckpointSortMode.SharedFolderType; diff --git a/StabilityMatrix.Tests/Avalonia/AppLogLevelApplierTests.cs b/StabilityMatrix.Tests/Avalonia/AppLogLevelApplierTests.cs new file mode 100644 index 000000000..9aa20bb21 --- /dev/null +++ b/StabilityMatrix.Tests/Avalonia/AppLogLevelApplierTests.cs @@ -0,0 +1,83 @@ +using NLog; +using NLog.Config; +using NLog.Targets; +using NLog.Targets.Wrappers; +using StabilityMatrix.Avalonia.Logging; +using StabilityMatrix.Core.Models.Settings; + +namespace StabilityMatrix.Tests.Avalonia; + +[TestClass] +public class AppLogLevelApplierTests +{ + private static (LoggingConfiguration Config, MemoryTarget Memory) CreateConfig() + { + var memory = new MemoryTarget("memory") { Layout = "${level}|${logger}|${message}" }; + var factory = new LogFactory(); + var config = new LoggingConfiguration(factory); + + // Mirrors App.ConfigureLogging: a filter for a specific logger, then catch-all file and console rules + config.AddRule( + new LoggingRule("System.*", LogLevel.Trace, LogLevel.Info, new NullTarget()) { Final = true } + ); + config.AddRule( + LogLevel.Debug, + LogLevel.Fatal, + new AsyncTargetWrapper( + new FileTarget("logfile") { FileName = Path.Combine(Path.GetTempPath(), "unused.log") } + ) + ); + config.AddRule( + LogLevel.Trace, + LogLevel.Fatal, + new AsyncTargetWrapper(new RichConsoleTarget("console")) + ); + config.AddRule(LogLevel.Trace, LogLevel.Fatal, memory); + + return (config, memory); + } + + [DataTestMethod] + [DataRow(AppLogLevel.Trace)] + [DataRow(AppLogLevel.Debug)] + [DataRow(AppLogLevel.Info)] + [DataRow(AppLogLevel.Warn)] + [DataRow(AppLogLevel.Error)] + public void Apply_SetsFileAndConsoleRules(AppLogLevel level) + { + var (config, _) = CreateConfig(); + + var updated = AppLogLevelApplier.Apply(config, level); + + Assert.AreEqual(2, updated); + var expected = level.ToNLogLevel(); + foreach (var rule in config.LoggingRules.Skip(1).Take(2)) + { + foreach (var l in LogLevel.AllLoggingLevels) + { + Assert.AreEqual(l >= expected, rule.IsLoggingEnabledForLevel(l), $"{l} with setting {level}"); + } + } + } + + [TestMethod] + public void Apply_LeavesOtherRulesUnchanged() + { + var (config, _) = CreateConfig(); + + AppLogLevelApplier.Apply(config, AppLogLevel.Error); + + var systemRule = config.LoggingRules[0]; + Assert.IsTrue(systemRule.IsLoggingEnabledForLevel(LogLevel.Trace)); + Assert.IsTrue(systemRule.IsLoggingEnabledForLevel(LogLevel.Info)); + + var memoryRule = config.LoggingRules[3]; + Assert.IsTrue(memoryRule.IsLoggingEnabledForLevel(LogLevel.Trace)); + } + + [TestMethod] + public void Apply_NullConfiguration_ReturnsZero() + { + Assert.AreEqual(0, AppLogLevelApplier.Apply(null, AppLogLevel.Info)); + } +} From afa2449c1aa9f8216a563e4369704c51167bde64 Mon Sep 17 00:00:00 2001 From: Deepratna Awale Date: Fri, 6 Nov 2026 10:39:42 +0000 Subject: [PATCH 2/2] Default the log level to Info Use Info as the default for new and existing settings files, and start the file and console rules at the same level so messages logged before settings load match the default. --- StabilityMatrix.Avalonia/App.axaml.cs | 6 ++++-- StabilityMatrix.Core/Models/Settings/Settings.cs | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index ad87be718..7a39d4a96 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -1332,12 +1332,14 @@ private static LoggingConfiguration ConfigureLogging() // Console logging var consoleTarget = new RichConsoleTarget("console") { Theme = RichNLogTheme.Code2 }; - builder.ForLogger().FilterMinLevel(NLog.LogLevel.Trace).WriteTo(consoleTarget).WithAsync(); + // Minimum levels for the console and file are replaced by the user's setting once settings load + var defaultMinLevel = Settings.DefaultLogLevel.ToNLogLevel(); + builder.ForLogger().FilterMinLevel(defaultMinLevel).WriteTo(consoleTarget).WithAsync(); // File logging // Use Compat.AppDataHome so logs follow --home-dir overrides var fileTarget = LogFileTarget.Create(Compat.LogsDir); - builder.ForLogger().FilterMinLevel(NLog.LogLevel.Debug).WriteTo(fileTarget).WithAsync(); + builder.ForLogger().FilterMinLevel(defaultMinLevel).WriteTo(fileTarget).WithAsync(); #if SM_LOG_WINDOW // LogViewer target when debug mode diff --git a/StabilityMatrix.Core/Models/Settings/Settings.cs b/StabilityMatrix.Core/Models/Settings/Settings.cs index 1367b8afa..f84f45fa2 100644 --- a/StabilityMatrix.Core/Models/Settings/Settings.cs +++ b/StabilityMatrix.Core/Models/Settings/Settings.cs @@ -242,7 +242,12 @@ public bool IsEnvironmentVariableUserOverridden(string key) /// /// Minimum level of messages written to the log file and console /// - public AppLogLevel LogLevel { get; set; } = AppLogLevel.Debug; + public AppLogLevel LogLevel { get; set; } = DefaultLogLevel; + + /// + /// Log level used until settings are loaded, and for settings files without a + /// + public const AppLogLevel DefaultLogLevel = AppLogLevel.Info; public bool IsWorkflowInfiniteScrollEnabled { get; set; } = true; public bool IsOutputsTreeViewEnabled { get; set; } = true; public CheckpointSortMode CheckpointSortMode { get; set; } = CheckpointSortMode.SharedFolderType;