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
88 changes: 31 additions & 57 deletions src/Observability/Runtime/Etw/EtwEventSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,93 +6,67 @@
namespace Microsoft.Agents.A365.Observability.Runtime.Etw
{
/// <summary>
/// ETW Event Source for Observability
/// ETW Event Source for Observability.
/// Call <see cref="Initialize"/> once at startup to configure the singleton,
/// then access it via <see cref="Log"/>. If <see cref="Log"/> is accessed
/// before <see cref="Initialize"/>, a default instance (with throw on errors) is created.
/// </summary>
[EventSource(Name = "A365-O11y-EventSource")]
public class EtwEventSource : EventSource
{
private static readonly object _lock = new object();
private static bool _throwOnEventWriteErrors = false;
private static Lazy<EtwEventSource> _lazy =
new Lazy<EtwEventSource>(CreateInstance);

private static EtwEventSource CreateInstance() =>
_throwOnEventWriteErrors
? new EtwEventSource(EventSourceSettings.ThrowOnEventWriteErrors)
: new EtwEventSource();
private static EtwEventSource? _instance;

/// <summary>
/// Singleton instance of the EtwEventSource.
/// Gets the singleton instance. Creates a default instance (with
/// <see cref="EventSourceSettings.ThrowOnEventWriteErrors"/>) if
/// <see cref="Initialize"/> has not been called.
/// </summary>
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) { }

/// <summary>
/// Configures the singleton before it is first used.
/// Must be called before accessing <see cref="Log"/>.
/// Initializes the singleton with the specified settings.
/// Must be called before the first access of <see cref="Log"/>.
/// </summary>
/// <param name="throwOnEventWriteErrors">
/// When <see langword="true"/>, the underlying <see cref="EventSource"/> will be created with
/// <see cref="EventSourceSettings.ThrowOnEventWriteErrors"/>.
/// <param name="suppressThrowOnEventWriteErrors">
/// When <see langword="true"/>, the underlying <see cref="EventSource"/> will be created without
/// <see cref="EventSourceSettings.ThrowOnEventWriteErrors"/>. By default, throw on errors is enabled.
/// </param>
/// <exception cref="InvalidOperationException">
/// Thrown if the singleton has already been created (i.e. <see cref="Log"/> has been accessed).
/// Thrown if the singleton has already been created.
/// </exception>
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.");
}
Comment on lines +43 to 47
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initialize is also not synchronized with Log/other Initialize calls. Two threads can both observe _instance == null and race to create different EtwEventSource instances, or Log can create the default instance while another thread is in Initialize, causing surprising InvalidOperationExceptions. Add synchronization (lock/Interlocked) so initialization and first access are atomic and deterministic.

Copilot uses AI. Check for mistakes.

_instance = suppressThrowOnEventWriteErrors
? new EtwEventSource()
: new EtwEventSource(EventSourceSettings.ThrowOnEventWriteErrors);
}

/// <summary>
/// Resets the singleton so tests can exercise <see cref="Configure"/> on a fresh instance.
/// Resets the singleton so tests can exercise <see cref="Initialize"/> on a fresh instance.
/// </summary>
internal static void ResetForTesting()
{
lock (_lock)
{
if (_lazy.IsValueCreated)
{
_lazy.Value.Dispose();
}

_throwOnEventWriteErrors = false;
_lazy = new Lazy<EtwEventSource>(CreateInstance);
}
_instance?.Dispose();
_instance = null;
}

/// <summary>
/// Handler for stopping a span.
/// Writes an ETW event with the necessary information from the span.
/// </summary>
[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);
Expand All @@ -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.
/// </summary>
[Event(2000,
Level = EventLevel.Informational,
[Event(2000,
Level = EventLevel.Informational,
Message = "{0}")]
public void LogJson(string message) =>
WriteEvent(2000, message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,49 +22,62 @@ 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
Assert.IsTrue(log.Settings.HasFlag(EventSourceSettings.ThrowOnEventWriteErrors));
}

[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
Assert.IsFalse(log.Settings.HasFlag(EventSourceSettings.ThrowOnEventWriteErrors));
}

[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<InvalidOperationException>(() =>
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<InvalidOperationException>(() =>
EtwEventSource.Initialize());

StringAssert.Contains(ex.Message, "already been initialized");
}
}
}
Loading