Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions StabilityMatrix.Avalonia/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,14 @@ private void ConfigureServiceProvider()

var settingsManager = Services.GetRequiredService<ISettingsManager>();

// 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);
Expand Down Expand Up @@ -1324,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
Expand Down
18 changes: 18 additions & 0 deletions StabilityMatrix.Avalonia/Languages/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions StabilityMatrix.Avalonia/Languages/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1883,4 +1883,10 @@ Account tokens will not be viewable after saving, please make a note of them if
<data name="Error_DocumentationPageLoadFailed" xml:space="preserve">
<value>Could not load this page. Check your connection and try again.</value>
</data>
<data name="Label_LogLevel" xml:space="preserve">
<value>Log Level</value>
</data>
<data name="Label_LogLevel_Description" xml:space="preserve">
<value>Minimum level of messages written to the log files and console. Lower levels include more detail but produce larger logs.</value>
</data>
</root>
73 changes: 73 additions & 0 deletions StabilityMatrix.Avalonia/Logging/AppLogLevelApplier.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Applies the user's <see cref="AppLogLevel"/> to the app's file and console log rules at runtime.
/// </summary>
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,
};

/// <summary>
/// Sets the minimum level of the catch-all rules writing to the log file or console in
/// <paramref name="configuration"/>. Rules for specific loggers (such as the filters that
/// limit System.* and Microsoft.* to warnings) are left unchanged.
/// </summary>
/// <returns>The number of rules updated.</returns>
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;
}

/// <summary>
/// Applies <paramref name="level"/> to the active NLog configuration and refreshes existing loggers.
/// </summary>
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ public partial class MainSettingsViewModel : PageViewModelBase

public IReadOnlyList<HolidayMode> HolidayModes { get; } = Enum.GetValues<HolidayMode>().ToList();

public IReadOnlyList<AppLogLevel> LogLevels { get; } = Enum.GetValues<AppLogLevel>().ToList();

[ObservableProperty]
private AppLogLevel selectedLogLevel;

[ObservableProperty]
private float selectedAnimationScale;

Expand Down Expand Up @@ -312,6 +317,13 @@ ICivitBaseModelTypeService baseModelTypeService
true
);

settingsManager.RelayPropertyFor(
this,
vm => vm.SelectedLogLevel,
settings => settings.LogLevel,
true
);

settingsManager.RelayPropertyFor(
this,
vm => vm.AutoLoadCivitModels,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,13 @@
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>

<!-- Log Level -->
<ui:SettingsExpanderItem Content="{x:Static lang:Resources.Label_LogLevel}" Description="{x:Static lang:Resources.Label_LogLevel_Description}">
<ui:SettingsExpanderItem.Footer>
<ui:FAComboBox ItemsSource="{Binding LogLevels}" SelectedItem="{Binding SelectedLogLevel}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>

<!-- Windows Long Paths -->
<ui:SettingsExpanderItem
Content="{x:Static lang:Resources.Label_EnableLongPaths}"
Expand Down
16 changes: 16 additions & 0 deletions StabilityMatrix.Core/Models/Settings/AppLogLevel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;

namespace StabilityMatrix.Core.Models.Settings;

/// <summary>
/// Minimum level of messages written to the app log file and console.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<AppLogLevel>))]
public enum AppLogLevel
{
Trace,
Debug,
Info,
Warn,
Error,
}
10 changes: 10 additions & 0 deletions StabilityMatrix.Core/Models/Settings/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,16 @@ public bool IsEnvironmentVariableUserOverridden(string key)
[Obsolete("Use OutputsPageResizeFactor instead")]
public Size OutputsImageSize { get; set; } = new(300, 300);
public HolidayMode HolidayModeSetting { get; set; } = HolidayMode.Automatic;

/// <summary>
/// Minimum level of messages written to the log file and console
/// </summary>
public AppLogLevel LogLevel { get; set; } = DefaultLogLevel;

/// <summary>
/// Log level used until settings are loaded, and for settings files without a <see cref="LogLevel"/>
/// </summary>
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;
Expand Down
83 changes: 83 additions & 0 deletions StabilityMatrix.Tests/Avalonia/AppLogLevelApplierTests.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading