diff --git a/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md b/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md index b17bff0405..9a6ae58355 100644 --- a/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md @@ -5,6 +5,11 @@ * Updated OpenTelemetry core component version(s) to `1.17.0`. ([#4773](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4773)) +* Updated ETW manifest and payload in `EtwDataTransport` + with synthetic payload so that the runtime-generated .NET + ETW manifest matches the actual payload. + ([#4729](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4729) + ## 1.16.0 Released 2026-Jul-09 diff --git a/src/OpenTelemetry.Exporter.Geneva/Internal/Transports/EtwDataTransport.cs b/src/OpenTelemetry.Exporter.Geneva/Internal/Transports/EtwDataTransport.cs index 8e4dc39034..63963d95c9 100644 --- a/src/OpenTelemetry.Exporter.Geneva/Internal/Transports/EtwDataTransport.cs +++ b/src/OpenTelemetry.Exporter.Geneva/Internal/Transports/EtwDataTransport.cs @@ -20,7 +20,7 @@ public EtwDataTransport(string providerName) public void Send(byte[] data, int size) { - this.eventSource.SendEvent((int)EtwEventSource.EtwEventId.TraceEvent, data, size); + this.eventSource.SendEvent(data, size); } public bool IsEnabled() @@ -54,25 +54,44 @@ public enum EtwEventId #pragma warning disable CA1822 // Mark members as static + /// + /// Dummy _data_ field is present so that when is called, the event has at least one + /// parameter with and single data object, it matches the runtime metadata + /// in ETW manifest. + /// + /// + /// Dummy placeholder for reflection-based ETW manifest generation. + /// In the ETW manifest for .NET, byte[] payload is always prepended with a synthetic length + /// field. + /// [Event((int)EtwEventId.TraceEvent, Version = 1, Level = EventLevel.Informational)] - public void InformationalEvent() + public void InformationalEvent(byte[] data) { } - #pragma warning restore CA1822 // Mark members as static + /// + /// Writes given raw data to ETW buffer. + /// Two fields are written to conform to .NET notation: length and data. + /// A separate length field isn't explicitly needed, since raw ETW payload already comes with buffer length + /// data, but the convention for .NET ETW libraries requires that field. + /// + /// Buffer with data to be sent. + /// How many bytes of that buffer to send. [NonEvent] #if NET [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "WriteEventCore is safe when eventData object is a primitive type, which it is in this case.")] #endif - public unsafe void SendEvent(int eventId, byte[] data, int size) + public unsafe void SendEvent(byte[] data, int size) { - var dataDesc = stackalloc EventData[1]; + var dataDesc = stackalloc EventData[2]; fixed (byte* ptr = data) { - dataDesc[0].DataPointer = (IntPtr)ptr; - dataDesc[0].Size = size; - this.WriteEventCore(eventId, 1, dataDesc); + dataDesc[0].DataPointer = (IntPtr)(&size); + dataDesc[0].Size = 4; + dataDesc[1].DataPointer = (IntPtr)ptr; + dataDesc[1].Size = size; + this.WriteEventCore((int)EtwEventId.TraceEvent, 2, dataDesc); } } } diff --git a/test/OpenTelemetry.Exporter.Geneva.Tests/EtwCollection.cs b/test/OpenTelemetry.Exporter.Geneva.Tests/EtwCollection.cs new file mode 100644 index 0000000000..5d1e4d463a --- /dev/null +++ b/test/OpenTelemetry.Exporter.Geneva.Tests/EtwCollection.cs @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Exporter.Geneva.Tests; + +/// +/// Collection definition for ETW tests. +/// +/// Since ETW is a system-global (not even process-global) resource, these tests shouldn't be run in parallel. +/// +[CollectionDefinition(Name, DisableParallelization = true)] +#pragma warning disable CA1711 // Identifiers should not have incorrect suffix +public sealed class EtwCollection +#pragma warning restore CA1711 // Identifiers should not have incorrect suffix +{ + public const string Name = "ETW collection"; +} diff --git a/test/OpenTelemetry.Exporter.Geneva.Tests/Internal/Transports/EtwDataTransportTests.cs b/test/OpenTelemetry.Exporter.Geneva.Tests/Internal/Transports/EtwDataTransportTests.cs new file mode 100644 index 0000000000..ded0e27382 --- /dev/null +++ b/test/OpenTelemetry.Exporter.Geneva.Tests/Internal/Transports/EtwDataTransportTests.cs @@ -0,0 +1,69 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.Tracing; +using OpenTelemetry.Exporter.Geneva.Transports; + +namespace OpenTelemetry.Exporter.Geneva.Tests.Internal.Transports; + +[Collection(EtwCollection.Name)] +public class EtwDataTransportTests +{ + [Fact] + public void TestEtwMessageRoundtrip() + { + var randomProviderName = "x" + Guid.NewGuid().ToString("N"); + using var listener = new TestEventSourceListener(randomProviderName); + using var transport = new EtwDataTransport(randomProviderName); + byte[] payload = [1, 2, 3, 4]; + + transport.Send(payload, payload.Length); + + Assert.Single(listener.Events); + var theEvent = listener.Events[0]; + Assert.NotNull(theEvent.Payload); + Assert.Single(theEvent.Payload); + + var rawPayload = theEvent.Payload[0] as byte[]; + Assert.NotNull(rawPayload); + Assert.Equal(payload, rawPayload); + } + + /// + /// Test event source listener. + /// + private sealed class TestEventSourceListener : EventListener + { + /// + /// Event source name to automatically attach to. + /// + private readonly string eventSourceName; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the event source to attach to. + public TestEventSourceListener(string eventSourceName) + { + this.eventSourceName = eventSourceName; + } + + /// + /// Gets events emitted by ETW. + /// + public List Events { get; } = new(); + + /// + protected override void OnEventSourceCreated(EventSource eventSource) + { + base.OnEventSourceCreated(eventSource); + if (string.Equals(eventSource.Name, this.eventSourceName, StringComparison.Ordinal)) + { + this.EnableEvents(eventSource, EventLevel.LogAlways); + } + } + + /// + protected override void OnEventWritten(EventWrittenEventArgs eventData) => this.Events.Add(eventData); + } +}