Repository: serilog/serilog

Issue:
Allow developers to react to changes in the MinimumLevel of a LoggingLevelSwitch
**Is your feature request related to a problem? Please describe.**
There's currently no easy way of detecting when the `MinimumLevel` of a `LoggingLevelSwitch` changes, and the recommended workaround is monitoring its value on a timer, which works but is cumbersome.

**Describe the solution you'd like**

There has been requests to make the `MinimumLevel` virtual so it can be overridden, or change it to an interface for custom implementations, (e.g. #1436, #1417, #1232,  and #682), and none of these were implemented because it would considerably affect performance of reads on the `MinimumLevel`.

These requests seem to share the same end goal: Run custom code **when the `MinimumLevel` changes**.

Given that _writes_ to `MinimumLevel` shouldn't happen nearly as often as _reads_, it seems that executing custom code immediately after the value of `MinimumLevel` is changed wouldn't be a big concern in terms of performance - unless I'm missing something (?).

With that in mind, I'd like to propose the introduction of a couple of methods in the `LoggingLevelSwitch` class that allow developers to enable/disable running custom code when the `MinimumLevel` changes.

It could be something along these lines:

```diff
public class LoggingLevelSwitch
{
+   Action<LogEventLevel> _notify;
    // ... (omitted for brevity)

    public LogEventLevel MinimumLevel
    {
        get { return _minimumLevel; }
        set
        {
            _minimumLevel = value;
+           var n = _notify;
+           n?.Invoke(value);
        }
    }
+
+   public void EnableNotifications(Action<LogEventLevel> notify)
+   {
+      _notify = notify ?? throw new ArgumentNullException(nameof(notify));
+   }
+
+   public void DisableNotifications()
+   {
+       _notify = null;
+   }
}
```

**Describe alternatives you've considered**
Monitor instances of `LoggingLevelSwitch` on a timer or background thread.

**Additional context**

If executing custom code in the setter of `MinimumLevel` turns out not to be a bad idea, we could also consider:

- Checking if the new value being assigned is different than the current value and only run the action if that's the case
- Provide previous and actual `LogEventLevel` rather than only the actual
- Allow more than one `Action` to be executed rather than a single one w/ last one wins

I'd be happy to send a PR once a design is agreed.


Hints:
That's an awesome idea! :+1: .... disappointed now that I didn't come up with it :sweat_smile: 

Scenario-wise, having support for multiple listeners, dynamically added, would make it easier for code to interact with a `LoggingLevelSwitch` that it doesn't "own".

 We could follow `ChangeToken`'s registration style, and do:

```csharp
IDisposable OnChange(Action<LogEventLevel> listener);
```

where disposing the result would deregister the listener?

Synchronization is interesting; a lock would need to cover both adding/removing listeners, and changing the level, so that listeners always "know" the correct level (interleavings of changing the level and adding listeners shouldn't result in listeners being stale/missing a change).

For that to work,

 * Either `OnChange` would need to immediately call the callback with the current value, when added, or
 * We'd need an `ILevelChangeListener` with `OnRegistered(level)` and `OnChanged(level)`

Last thought ... calling callbacks under a lock is good to avoid; we probably could, here, but not sure it's worth the substantial effort involved. "The callback will be invoked under a lock" might be worth noting in the XDOC for OnChange?
Any reason not to make it a straight Event (or an Observable as you've all but suggested above). [Some notes on Observable vs Events](https://stackoverflow.com/questions/24572366/should-iobservable-be-preferred-over-events-when-exposing-notifications-in-a-lib)
Need to give it some thought - probably no reason; events might be a bit simpler of those two. I think explicit callbacks are nice to work with in a lot of cases, and are used as much as the other two, but could just be me, there :-)
I'd love to dig into possibilities here further, but I think it's stalled so I'll close it now as stale. Feel free to jump in if you're considering picking this back up :-)

Patch:
diff --git a/src/Serilog/Core/LoggingLevelSwitch.cs b/src/Serilog/Core/LoggingLevelSwitch.cs
index 08c5db432..847bdc293 100644
--- a/src/Serilog/Core/LoggingLevelSwitch.cs
+++ b/src/Serilog/Core/LoggingLevelSwitch.cs
@@ -20,6 +20,7 @@ namespace Serilog.Core;
 public class LoggingLevelSwitch
 {
     volatile LogEventLevel _minimumLevel;
+    readonly object _levelUpdateLock = new();
 
     /// <summary>
     /// Create a <see cref="LoggingLevelSwitch"/> at the initial
@@ -31,6 +32,12 @@ public LoggingLevelSwitch(LogEventLevel initialMinimumLevel = LogEventLevel.Info
         _minimumLevel = initialMinimumLevel;
     }
 
+    /// <summary>
+    /// The event arises when <see cref="MinimumLevel"/> changed. Note that the event is raised
+    /// under a lock so be careful within event handler to not fall into deadlock.
+    /// </summary>
+    public event EventHandler<LoggingLevelSwitchChangedEventArgs>? MinimumLevelChanged;
+
     /// <summary>
     /// The current minimum level, below which no events
     /// should be generated.
@@ -40,6 +47,17 @@ public LoggingLevelSwitch(LogEventLevel initialMinimumLevel = LogEventLevel.Info
     public LogEventLevel MinimumLevel
     {
         get { return _minimumLevel; }
-        set { _minimumLevel = value; }
+        set
+        {
+            lock (_levelUpdateLock)
+            {
+                var old = _minimumLevel;
+                if (old != value)
+                {
+                    _minimumLevel = value;
+                    MinimumLevelChanged?.Invoke(this, new LoggingLevelSwitchChangedEventArgs(old, value));
+                }
+            }
+        }
     }
 }
diff --git a/src/Serilog/Core/LoggingLevelSwitchChangedEventArgs.cs b/src/Serilog/Core/LoggingLevelSwitchChangedEventArgs.cs
new file mode 100644
index 000000000..b76a79de5
--- /dev/null
+++ b/src/Serilog/Core/LoggingLevelSwitchChangedEventArgs.cs
@@ -0,0 +1,42 @@
+// Copyright 2013-2015 Serilog Contributors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace Serilog.Core;
+
+/// <summary>
+/// Event arguments for <see cref="LoggingLevelSwitch.MinimumLevelChanged"/> event.
+/// </summary>
+public class LoggingLevelSwitchChangedEventArgs : EventArgs
+{
+    /// <summary>
+    /// Creates an instance of <see cref="LoggingLevelSwitchChangedEventArgs"/> specifying old and new levels.
+    /// </summary>
+    /// <param name="oldLevel">Old level.</param>
+    /// <param name="newLevel">New level.</param>
+    public LoggingLevelSwitchChangedEventArgs(LogEventLevel oldLevel, LogEventLevel newLevel)
+    {
+        OldLevel = oldLevel;
+        NewLevel = newLevel;
+    }
+
+    /// <summary>
+    /// Old level.
+    /// </summary>
+    public LogEventLevel OldLevel { get; }
+
+    /// <summary>
+    /// New level.
+    /// </summary>
+    public LogEventLevel NewLevel { get; }
+}


Test patch:
diff --git a/test/Serilog.ApprovalTests/Serilog.approved.txt b/test/Serilog.ApprovalTests/Serilog.approved.txt
index ecb422542..fece82d1f 100644
--- a/test/Serilog.ApprovalTests/Serilog.approved.txt
+++ b/test/Serilog.ApprovalTests/Serilog.approved.txt
@@ -280,6 +280,13 @@ namespace Serilog.Core
     {
         public LoggingLevelSwitch(Serilog.Events.LogEventLevel initialMinimumLevel = 2) { }
         public Serilog.Events.LogEventLevel MinimumLevel { get; set; }
+        public event System.EventHandler<Serilog.Core.LoggingLevelSwitchChangedEventArgs>? MinimumLevelChanged;
+    }
+    public class LoggingLevelSwitchChangedEventArgs : System.EventArgs
+    {
+        public LoggingLevelSwitchChangedEventArgs(Serilog.Events.LogEventLevel oldLevel, Serilog.Events.LogEventLevel newLevel) { }
+        public Serilog.Events.LogEventLevel NewLevel { get; }
+        public Serilog.Events.LogEventLevel OldLevel { get; }
     }
     [System.AttributeUsage(System.AttributeTargets.Constructor | System.AttributeTargets.Method)]
     public sealed class MessageTemplateFormatMethodAttribute : System.Attribute
diff --git a/test/Serilog.Tests/Core/LoggerTests.cs b/test/Serilog.Tests/Core/LoggerTests.cs
index 90b19b2a3..799139766 100644
--- a/test/Serilog.Tests/Core/LoggerTests.cs
+++ b/test/Serilog.Tests/Core/LoggerTests.cs
@@ -137,7 +137,7 @@ public void TheNoneLoggerIsAConstant()
     [Fact]
     public void TheNoneLoggerIsSingleton()
     {
-        lock (new object())
+        lock (this)
         {
             Log.CloseAndFlush();
             Assert.Same(Log.Logger, Logger.None);
diff --git a/test/Serilog.Tests/Core/LoggingLevekSwitchTests.cs b/test/Serilog.Tests/Core/LoggingLevekSwitchTests.cs
new file mode 100644
index 000000000..0540fc378
--- /dev/null
+++ b/test/Serilog.Tests/Core/LoggingLevekSwitchTests.cs
@@ -0,0 +1,22 @@
+namespace Serilog.Tests.Core;
+
+public class LoggingLevekSwitchTests
+{
+    [Fact]
+    public void MinimumLevelChanged_Should_Work()
+    {
+        var sw = new LoggingLevelSwitch();
+        bool changed = false;
+        sw.MinimumLevelChanged += (o, e) =>
+        {
+            Assert.Equal(sw, o);
+            Assert.Equal(Information, e.OldLevel);
+            Assert.Equal(Fatal, e.NewLevel);
+            changed = true;
+        };
+        Assert.Equal(Information, sw.MinimumLevel);
+        sw.MinimumLevel = Fatal;
+        Assert.Equal(Fatal, sw.MinimumLevel);
+        Assert.True(changed);
+    }
+}