diff --git a/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md b/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md index 4aad8f8be7..fce0e4bacc 100644 --- a/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +* Fixed race condition in `MsgPackTraceExporter` where concurrent threads + calling `SerializeActivity` would corrupt the `prepopulatedFields` dictionary, + leading to "Bad forward protocol format" errors. + ([#3881](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3881)) + ## 1.15.0 Released 2026-Jan-21 diff --git a/src/OpenTelemetry.Exporter.Geneva/Internal/MsgPack/MsgPackTraceExporter.cs b/src/OpenTelemetry.Exporter.Geneva/Internal/MsgPack/MsgPackTraceExporter.cs index c7d310454b..6fcb2d976b 100644 --- a/src/OpenTelemetry.Exporter.Geneva/Internal/MsgPack/MsgPackTraceExporter.cs +++ b/src/OpenTelemetry.Exporter.Geneva/Internal/MsgPack/MsgPackTraceExporter.cs @@ -4,6 +4,7 @@ #if NET using System.Collections.Frozen; #endif +using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; using System.Runtime.InteropServices; @@ -86,7 +87,7 @@ internal sealed class MsgPackTraceExporter : MsgPackExporter, IDisposable // this stores the prepopulated fields until CreateFraming can consume them into the prologue. // after CreateFraming is called, this dictionary is set to null, so don't use it after that. - private Dictionary prepopulatedFields; + private ConcurrentDictionary prepopulatedFields; private bool isDisposed; @@ -152,12 +153,12 @@ public MsgPackTraceExporter(GenevaExporterOptions options, Func resour this.userProvidedPrepopulatedFields = options.PrepopulatedFields != null && options.PrepopulatedFields.Count > 0; - this.prepopulatedFields = new Dictionary(0, StringComparer.Ordinal); + this.prepopulatedFields = new ConcurrentDictionary(StringComparer.Ordinal); if (options.PrepopulatedFields != null) { foreach (var entry in options.PrepopulatedFields) { - this.prepopulatedFields.Add(entry.Key, entry.Value); + this.prepopulatedFields[entry.Key] = entry.Value; } } @@ -347,15 +348,6 @@ internal void CreateFraming() var resourceAttributes = this.resourceProvider().Attributes; - if (this.resourceFieldNames != null) - { - // if ResourceFieldNames is set, we use resource attributes rather than PrepopulatedFields - this.prepopulatedFields = new Dictionary(0, StringComparer.Ordinal); - } - - // this is guaranteed to not be null because it's set in the constructor - Guard.ThrowIfNull(this.prepopulatedFields); - foreach (var resourceAttribute in resourceAttributes) { var key = resourceAttribute.Key; @@ -437,14 +429,17 @@ internal void CreateFraming() cursor = AddPartAField(buffer, cursor, entry.Key, entry.Value); } - this.bufferPrologue = new byte[cursor - 0]; - System.Buffer.BlockCopy(buffer, 0, this.bufferPrologue, 0, cursor - 0); + var bufferPrologue = new byte[cursor - 0]; + System.Buffer.BlockCopy(buffer, 0, bufferPrologue, 0, cursor - 0); // Now generate the epilogue cursor = MessagePackSerializer.Serialize(buffer, 0, new Dictionary { { "TimeFormat", "DateTime" } }); - this.bufferEpilogue = new byte[cursor - 0]; - System.Buffer.BlockCopy(buffer, 0, this.bufferEpilogue, 0, cursor - 0); + var bufferEpilogue = new byte[cursor - 0]; + System.Buffer.BlockCopy(buffer, 0, bufferEpilogue, 0, cursor - 0); + + this.bufferPrologue = bufferPrologue; + this.bufferEpilogue = bufferEpilogue; } internal ArraySegment SerializeActivity(Activity activity)