diff --git a/src/Observability/Runtime/Etw/EtwEventSource.cs b/src/Observability/Runtime/Etw/EtwEventSource.cs index 7eaca46e..b2921a35 100644 --- a/src/Observability/Runtime/Etw/EtwEventSource.cs +++ b/src/Observability/Runtime/Etw/EtwEventSource.cs @@ -6,93 +6,67 @@ 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 (with 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 (with + /// ) 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(EventSourceSettings.ThrowOnEventWriteErrors); 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 - /// . + /// + /// When , the underlying will be created without + /// . By default, throw on errors is enabled. /// /// - /// 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 suppressThrowOnEventWriteErrors = 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 = suppressThrowOnEventWriteErrors + ? new EtwEventSource() + : new EtwEventSource(EventSourceSettings.ThrowOnEventWriteErrors); } /// - /// 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() { - 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 +75,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..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 Configure_WithTrue_BeforeLogAccess_CreatesInstanceWithThrowOnErrors() + public void Initialize_Default_CreatesInstanceWithThrowOnErrors() { // Arrange & Act - EtwEventSource.Configure(throwOnEventWriteErrors: true); + EtwEventSource.Initialize(); 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_WithSuppressTrue_CreatesInstanceWithoutThrowOnErrors() { // Arrange & Act - EtwEventSource.Configure(throwOnEventWriteErrors: false); + EtwEventSource.Initialize(suppressThrowOnEventWriteErrors: true); var log = EtwEventSource.Log; // Assert @@ -44,27 +44,40 @@ public void Configure_WithFalse_BeforeLogAccess_CreatesInstanceWithoutThrowOnErr } [TestMethod] - public void Log_WithoutConfigure_CreatesInstanceWithoutThrowOnErrors() + 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] - 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()); - 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(); + + // Act & Assert + var ex = Assert.ThrowsException(() => + EtwEventSource.Initialize()); + + StringAssert.Contains(ex.Message, "already been initialized"); + } } }