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);
+ }
+}