From d029454a280c0807d211aa96a83726693c84f835 Mon Sep 17 00:00:00 2001 From: Ron Beglaubter Date: Fri, 27 Mar 2026 10:40:43 +0300 Subject: [PATCH 1/2] Simplify EtwEventSource singleton for public consumption (#215 follow-up) Replace Lazy/lock pattern with simple null-coalescing singleton. Rename Configure() to Initialize(), make ResetForTesting() public so NuGet consumers can use it in their tests. --- .../Runtime/Etw/EtwEventSource.cs | 83 +++++++------------ .../Etw/EtwEventSourceConfigureTests.cs | 29 +++++-- 2 files changed, 49 insertions(+), 63 deletions(-) diff --git a/src/Observability/Runtime/Etw/EtwEventSource.cs b/src/Observability/Runtime/Etw/EtwEventSource.cs index 7eaca46e..8f37f0fe 100644 --- a/src/Observability/Runtime/Etw/EtwEventSource.cs +++ b/src/Observability/Runtime/Etw/EtwEventSource.cs @@ -6,93 +6,66 @@ namespace Microsoft.Agents.A365.Observability.Runtime.Etw { /// - /// ETW Event Source for Observability + /// ETW Event Source for Observability. + /// Call once at startup to configure the singleton, + /// then access it via . If is accessed + /// before , a default instance (no throw on errors) is created. /// [EventSource(Name = "A365-O11y-EventSource")] public class EtwEventSource : EventSource { - private static readonly object _lock = new object(); - private static bool _throwOnEventWriteErrors = false; - private static Lazy _lazy = - new Lazy(CreateInstance); - - private static EtwEventSource CreateInstance() => - _throwOnEventWriteErrors - ? new EtwEventSource(EventSourceSettings.ThrowOnEventWriteErrors) - : new EtwEventSource(); + private static EtwEventSource? _instance; /// - /// Singleton instance of the EtwEventSource. + /// Gets the singleton instance. Creates a default instance if + /// has not been called. /// - public static EtwEventSource Log - { - get - { - if (_lazy.IsValueCreated) - { - return _lazy.Value; - } - - lock (_lock) - { - return _lazy.Value; - } - } - } + public static EtwEventSource Log => _instance ??= new EtwEventSource(); private EtwEventSource() : base() { } private EtwEventSource(EventSourceSettings settings) : base(settings) { } /// - /// Configures the singleton before it is first used. - /// Must be called before accessing . + /// Initializes the singleton with the specified settings. + /// Must be called before the first access of . /// /// /// When , the underlying will be created with /// . /// /// - /// Thrown if the singleton has already been created (i.e. has been accessed). + /// Thrown if the singleton has already been created. /// - public static void Configure(bool throwOnEventWriteErrors) + public static void Initialize(bool throwOnEventWriteErrors = false) { - lock (_lock) + if (_instance != null) { - if (_lazy.IsValueCreated) - { - throw new InvalidOperationException( - "EtwEventSource has already been created. Configure() must be called before the first access of Log."); - } - - _throwOnEventWriteErrors = throwOnEventWriteErrors; + throw new InvalidOperationException( + "EtwEventSource has already been initialized. Initialize() must be called before the first access of Log."); } + + _instance = throwOnEventWriteErrors + ? new EtwEventSource(EventSourceSettings.ThrowOnEventWriteErrors) + : new EtwEventSource(); } /// - /// Resets the singleton so tests can exercise on a fresh instance. + /// Resets the singleton so tests can exercise on a fresh instance. /// - internal static void ResetForTesting() + public static void ResetForTesting() { - lock (_lock) - { - if (_lazy.IsValueCreated) - { - _lazy.Value.Dispose(); - } - - _throwOnEventWriteErrors = false; - _lazy = new Lazy(CreateInstance); - } + _instance?.Dispose(); + _instance = null; } /// /// Handler for stopping a span. /// Writes an ETW event with the necessary information from the span. /// - [Event(1000, - Level = EventLevel.Informational, - Opcode = EventOpcode.Stop, + [Event(1000, + Level = EventLevel.Informational, + Opcode = EventOpcode.Stop, Message = "A365 Otel span: Name={0} Id={1} Body={4}")] public void SpanStop(string name, string spanId, string traceId, string parentSpanId, string content) => WriteEvent(1000, name, spanId, traceId, parentSpanId, content); @@ -101,8 +74,8 @@ public void SpanStop(string name, string spanId, string traceId, string parentSp /// Handler for logging JSON messages. /// Writes an ETW event with the provided JSON message. /// - [Event(2000, - Level = EventLevel.Informational, + [Event(2000, + Level = EventLevel.Informational, Message = "{0}")] public void LogJson(string message) => WriteEvent(2000, message); diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Etw/EtwEventSourceConfigureTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Etw/EtwEventSourceConfigureTests.cs index 355426f6..8c86f41d 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Etw/EtwEventSourceConfigureTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Etw/EtwEventSourceConfigureTests.cs @@ -22,10 +22,10 @@ public void Cleanup() } [TestMethod] - public void Configure_WithTrue_BeforeLogAccess_CreatesInstanceWithThrowOnErrors() + public void Initialize_WithTrue_CreatesInstanceWithThrowOnErrors() { // Arrange & Act - EtwEventSource.Configure(throwOnEventWriteErrors: true); + EtwEventSource.Initialize(throwOnEventWriteErrors: true); var log = EtwEventSource.Log; // Assert @@ -33,10 +33,10 @@ public void Configure_WithTrue_BeforeLogAccess_CreatesInstanceWithThrowOnErrors( } [TestMethod] - public void Configure_WithFalse_BeforeLogAccess_CreatesInstanceWithoutThrowOnErrors() + public void Initialize_WithFalse_CreatesInstanceWithoutThrowOnErrors() { // Arrange & Act - EtwEventSource.Configure(throwOnEventWriteErrors: false); + EtwEventSource.Initialize(throwOnEventWriteErrors: false); var log = EtwEventSource.Log; // Assert @@ -44,7 +44,7 @@ public void Configure_WithFalse_BeforeLogAccess_CreatesInstanceWithoutThrowOnErr } [TestMethod] - public void Log_WithoutConfigure_CreatesInstanceWithoutThrowOnErrors() + public void Log_WithoutInitialize_CreatesDefaultInstance() { // Act var log = EtwEventSource.Log; @@ -54,17 +54,30 @@ public void Log_WithoutConfigure_CreatesInstanceWithoutThrowOnErrors() } [TestMethod] - public void Configure_AfterLogAccess_ThrowsInvalidOperationException() + public void Initialize_AfterLogAccess_ThrowsInvalidOperationException() { // Arrange - force singleton creation _ = EtwEventSource.Log; // Act & Assert var ex = Assert.ThrowsException(() => - EtwEventSource.Configure(throwOnEventWriteErrors: true)); + EtwEventSource.Initialize(throwOnEventWriteErrors: true)); - StringAssert.Contains(ex.Message, "Configure()"); + StringAssert.Contains(ex.Message, "Initialize()"); StringAssert.Contains(ex.Message, "before the first access"); } + + [TestMethod] + public void Initialize_CalledTwice_ThrowsInvalidOperationException() + { + // Arrange + EtwEventSource.Initialize(throwOnEventWriteErrors: false); + + // Act & Assert + var ex = Assert.ThrowsException(() => + EtwEventSource.Initialize(throwOnEventWriteErrors: true)); + + StringAssert.Contains(ex.Message, "already been initialized"); + } } } From 6df8683fb68e72bc05546d1bc7d26bffbff1d7a9 Mon Sep 17 00:00:00 2001 From: Ron Beglaubter Date: Sat, 28 Mar 2026 02:20:11 +0300 Subject: [PATCH 2/2] Make ThrowOnEventWriteErrors the default and ResetForTesting internal Flip Initialize() so throw-on-errors is on by default; consumers pass suppressThrowOnEventWriteErrors: true to opt out. ResetForTesting() reverted to internal visibility. --- .../Runtime/Etw/EtwEventSource.cs | 23 ++++++++++--------- .../Etw/EtwEventSourceConfigureTests.cs | 18 +++++++-------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/Observability/Runtime/Etw/EtwEventSource.cs b/src/Observability/Runtime/Etw/EtwEventSource.cs index 8f37f0fe..b2921a35 100644 --- a/src/Observability/Runtime/Etw/EtwEventSource.cs +++ b/src/Observability/Runtime/Etw/EtwEventSource.cs @@ -9,7 +9,7 @@ namespace Microsoft.Agents.A365.Observability.Runtime.Etw /// ETW Event Source for Observability. /// Call once at startup to configure the singleton, /// then access it via . If is accessed - /// before , a default instance (no throw on errors) is created. + /// before , a default instance (with throw on errors) is created. /// [EventSource(Name = "A365-O11y-EventSource")] public class EtwEventSource : EventSource @@ -17,10 +17,11 @@ public class EtwEventSource : EventSource private static EtwEventSource? _instance; /// - /// Gets the singleton instance. Creates a default instance if + /// Gets the singleton instance. Creates a default instance (with + /// ) if /// has not been called. /// - public static EtwEventSource Log => _instance ??= new EtwEventSource(); + public static EtwEventSource Log => _instance ??= new EtwEventSource(EventSourceSettings.ThrowOnEventWriteErrors); private EtwEventSource() : base() { } @@ -30,14 +31,14 @@ private EtwEventSource(EventSourceSettings settings) : base(settings) { } /// Initializes the singleton with the specified settings. /// Must be called before the first access of . /// - /// - /// When , the underlying will be created with - /// . + /// + /// When , the underlying will be created without + /// . By default, throw on errors is enabled. /// /// /// Thrown if the singleton has already been created. /// - public static void Initialize(bool throwOnEventWriteErrors = false) + public static void Initialize(bool suppressThrowOnEventWriteErrors = false) { if (_instance != null) { @@ -45,15 +46,15 @@ public static void Initialize(bool throwOnEventWriteErrors = false) "EtwEventSource has already been initialized. Initialize() must be called before the first access of Log."); } - _instance = throwOnEventWriteErrors - ? new EtwEventSource(EventSourceSettings.ThrowOnEventWriteErrors) - : new EtwEventSource(); + _instance = suppressThrowOnEventWriteErrors + ? new EtwEventSource() + : new EtwEventSource(EventSourceSettings.ThrowOnEventWriteErrors); } /// /// Resets the singleton so tests can exercise on a fresh instance. /// - public static void ResetForTesting() + internal static void ResetForTesting() { _instance?.Dispose(); _instance = null; diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Etw/EtwEventSourceConfigureTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Etw/EtwEventSourceConfigureTests.cs index 8c86f41d..a39dd584 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Etw/EtwEventSourceConfigureTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Etw/EtwEventSourceConfigureTests.cs @@ -22,10 +22,10 @@ public void Cleanup() } [TestMethod] - public void Initialize_WithTrue_CreatesInstanceWithThrowOnErrors() + public void Initialize_Default_CreatesInstanceWithThrowOnErrors() { // Arrange & Act - EtwEventSource.Initialize(throwOnEventWriteErrors: true); + EtwEventSource.Initialize(); var log = EtwEventSource.Log; // Assert @@ -33,10 +33,10 @@ public void Initialize_WithTrue_CreatesInstanceWithThrowOnErrors() } [TestMethod] - public void Initialize_WithFalse_CreatesInstanceWithoutThrowOnErrors() + public void Initialize_WithSuppressTrue_CreatesInstanceWithoutThrowOnErrors() { // Arrange & Act - EtwEventSource.Initialize(throwOnEventWriteErrors: false); + EtwEventSource.Initialize(suppressThrowOnEventWriteErrors: true); var log = EtwEventSource.Log; // Assert @@ -44,13 +44,13 @@ public void Initialize_WithFalse_CreatesInstanceWithoutThrowOnErrors() } [TestMethod] - public void Log_WithoutInitialize_CreatesDefaultInstance() + public void Log_WithoutInitialize_CreatesDefaultInstanceWithThrowOnErrors() { // Act var log = EtwEventSource.Log; // Assert - Assert.IsFalse(log.Settings.HasFlag(EventSourceSettings.ThrowOnEventWriteErrors)); + Assert.IsTrue(log.Settings.HasFlag(EventSourceSettings.ThrowOnEventWriteErrors)); } [TestMethod] @@ -61,7 +61,7 @@ public void Initialize_AfterLogAccess_ThrowsInvalidOperationException() // Act & Assert var ex = Assert.ThrowsException(() => - EtwEventSource.Initialize(throwOnEventWriteErrors: true)); + EtwEventSource.Initialize()); StringAssert.Contains(ex.Message, "Initialize()"); StringAssert.Contains(ex.Message, "before the first access"); @@ -71,11 +71,11 @@ public void Initialize_AfterLogAccess_ThrowsInvalidOperationException() public void Initialize_CalledTwice_ThrowsInvalidOperationException() { // Arrange - EtwEventSource.Initialize(throwOnEventWriteErrors: false); + EtwEventSource.Initialize(); // Act & Assert var ex = Assert.ThrowsException(() => - EtwEventSource.Initialize(throwOnEventWriteErrors: true)); + EtwEventSource.Initialize()); StringAssert.Contains(ex.Message, "already been initialized"); }