From d45060b434e469ec5c1a6cd7a3d06faa2eac4a9b Mon Sep 17 00:00:00 2001 From: Rajkumar Rangaraj Date: Mon, 28 Oct 2024 10:08:30 -0700 Subject: [PATCH] [otlp] OTLP Exporter Custom serializer - Spans (#5928) Co-authored-by: Mikel Blanchard --- .../Serializer/ProtobufOtlpTagWriter.cs | 1 + .../Serializer/ProtobufOtlpTraceSerializer.cs | 335 ++++++++++++++++++ .../OtlpTraceExporterTests.cs | 91 +++-- 3 files changed, 396 insertions(+), 31 deletions(-) create mode 100644 src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTraceSerializer.cs diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTagWriter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTagWriter.cs index 65fef4fe110..2cc0fb4fb81 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTagWriter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTagWriter.cs @@ -111,6 +111,7 @@ public override OtlpTagWriterArrayState BeginWriteArray() public override void WriteNullValue(ref OtlpTagWriterArrayState state) { + state.WritePosition = ProtobufSerializer.WriteTagAndLength(state.Buffer, state.WritePosition, 0, ProtobufOtlpFieldNumberConstants.ArrayValue_Value, ProtobufWireType.LEN); } public override void WriteIntegralValue(ref OtlpTagWriterArrayState state, long value) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTraceSerializer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTraceSerializer.cs new file mode 100644 index 00000000000..813b691e67f --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTraceSerializer.cs @@ -0,0 +1,335 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; + +internal static class ProtobufOtlpTraceSerializer +{ + private const int ReserveSizeForLength = 4; + private const string UnsetStatusCodeTagValue = "UNSET"; + private const string OkStatusCodeTagValue = "OK"; + private const string ErrorStatusCodeTagValue = "ERROR"; + private const int TraceIdSize = 16; + private const int SpanIdSize = 8; + + internal static int WriteSpan(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, Activity activity) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpFieldNumberConstants.ScopeSpans_Span, ProtobufWireType.LEN); + int spanLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + writePosition = ProtobufSerializer.WriteTagAndLength(buffer, writePosition, TraceIdSize, ProtobufOtlpFieldNumberConstants.Span_Trace_Id, ProtobufWireType.LEN); + writePosition = WriteTraceId(buffer, writePosition, activity.TraceId); + + writePosition = ProtobufSerializer.WriteTagAndLength(buffer, writePosition, SpanIdSize, ProtobufOtlpFieldNumberConstants.Span_Span_Id, ProtobufWireType.LEN); + writePosition = WriteSpanId(buffer, writePosition, activity.SpanId); + + if (activity.TraceStateString != null) + { + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpFieldNumberConstants.Span_Trace_State, activity.TraceStateString); + } + + if (activity.ParentSpanId != default) + { + writePosition = ProtobufSerializer.WriteTagAndLength(buffer, writePosition, SpanIdSize, ProtobufOtlpFieldNumberConstants.Span_Parent_Span_Id, ProtobufWireType.LEN); + writePosition = WriteSpanId(buffer, writePosition, activity.ParentSpanId); + } + + writePosition = WriteTraceFlags(buffer, writePosition, activity.ActivityTraceFlags, activity.HasRemoteParent, ProtobufOtlpFieldNumberConstants.Span_Flags); + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpFieldNumberConstants.Span_Name, activity.DisplayName); + writePosition = ProtobufSerializer.WriteEnumWithTag(buffer, writePosition, ProtobufOtlpFieldNumberConstants.Span_Kind, (int)activity.Kind + 1); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpFieldNumberConstants.Span_Start_Time_Unix_Nano, (ulong)activity.StartTimeUtc.ToUnixTimeNanoseconds()); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpFieldNumberConstants.Span_End_Time_Unix_Nano, (ulong)(activity.StartTimeUtc.ToUnixTimeNanoseconds() + activity.Duration.ToNanoseconds())); + + (writePosition, StatusCode? statusCode, string? statusMessage) = WriteActivityTags(buffer, writePosition, sdkLimitOptions, activity); + writePosition = WriteSpanEvents(buffer, writePosition, sdkLimitOptions, activity); + writePosition = WriteSpanLinks(buffer, writePosition, sdkLimitOptions, activity); + writePosition = WriteSpanStatus(buffer, writePosition, activity, statusCode, statusMessage); + ProtobufSerializer.WriteReservedLength(buffer, spanLengthPosition, writePosition - (spanLengthPosition + ReserveSizeForLength)); + + return writePosition; + } + + internal static int WriteTraceId(byte[] buffer, int position, ActivityTraceId activityTraceId) + { + var traceBytes = new Span(buffer, position, TraceIdSize); + activityTraceId.CopyTo(traceBytes); + return position + TraceIdSize; + } + + internal static int WriteSpanId(byte[] buffer, int position, ActivitySpanId activitySpanId) + { + var spanIdBytes = new Span(buffer, position, SpanIdSize); + activitySpanId.CopyTo(spanIdBytes); + return position + SpanIdSize; + } + + internal static int WriteTraceFlags(byte[] buffer, int position, ActivityTraceFlags activityTraceFlags, bool hasRemoteParent, int fieldNumber) + { + uint spanFlags = (uint)activityTraceFlags & (byte)0x000000FF; + + spanFlags |= 0x00000100; + if (hasRemoteParent) + { + spanFlags |= 0x00000200; + } + + position = ProtobufSerializer.WriteFixed32WithTag(buffer, position, fieldNumber, spanFlags); + + return position; + } + + internal static (int Position, StatusCode? StatusCode, string? StatusMessage) WriteActivityTags(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, Activity activity) + { + StatusCode? statusCode = null; + string? statusMessage = null; + int maxAttributeCount = sdkLimitOptions.SpanAttributeCountLimit ?? int.MaxValue; + int maxAttributeValueLength = sdkLimitOptions.AttributeValueLengthLimit ?? int.MaxValue; + int attributeCount = 0; + int droppedAttributeCount = 0; + + ProtobufOtlpTagWriter.OtlpTagWriterState otlpTagWriterState = new ProtobufOtlpTagWriter.OtlpTagWriterState + { + Buffer = buffer, + WritePosition = writePosition, + }; + + foreach (ref readonly var tag in activity.EnumerateTagObjects()) + { + switch (tag.Key) + { + case "otel.status_code": + + statusCode = tag.Value switch + { + /* + * Note: Order here does matter for perf. Unset is + * first because assumption is most spans will be + * Unset, then Error. Ok is not set by the SDK. + */ + not null when UnsetStatusCodeTagValue.Equals(tag.Value as string, StringComparison.OrdinalIgnoreCase) => StatusCode.Unset, + not null when ErrorStatusCodeTagValue.Equals(tag.Value as string, StringComparison.OrdinalIgnoreCase) => StatusCode.Error, + not null when OkStatusCodeTagValue.Equals(tag.Value as string, StringComparison.OrdinalIgnoreCase) => StatusCode.Ok, + _ => null, + }; + continue; + case "otel.status_description": + statusMessage = tag.Value as string; + continue; + } + + if (attributeCount < maxAttributeCount) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpFieldNumberConstants.Span_Attributes, ProtobufWireType.LEN); + int spanAttributesLengthPosition = otlpTagWriterState.WritePosition; + otlpTagWriterState.WritePosition += ReserveSizeForLength; + + ProtobufOtlpTagWriter.Instance.TryWriteTag(ref otlpTagWriterState, tag.Key, tag.Value, maxAttributeValueLength); + + ProtobufSerializer.WriteReservedLength(buffer, spanAttributesLengthPosition, otlpTagWriterState.WritePosition - (spanAttributesLengthPosition + 4)); + attributeCount++; + } + else + { + droppedAttributeCount++; + } + } + + if (droppedAttributeCount > 0) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(buffer, otlpTagWriterState.WritePosition, ProtobufOtlpFieldNumberConstants.Span_Dropped_Attributes_Count, ProtobufWireType.VARINT); + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteVarInt32(buffer, otlpTagWriterState.WritePosition, (uint)droppedAttributeCount); + } + + return (otlpTagWriterState.WritePosition, statusCode, statusMessage); + } + + internal static int WriteSpanEvents(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, Activity activity) + { + int maxEventCountLimit = sdkLimitOptions.SpanEventCountLimit ?? int.MaxValue; + int eventCount = 0; + int droppedEventCount = 0; + foreach (ref readonly var evnt in activity.EnumerateEvents()) + { + if (eventCount < maxEventCountLimit) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpFieldNumberConstants.Span_Events, ProtobufWireType.LEN); + int spanEventsLengthPosition = writePosition; + writePosition += ReserveSizeForLength; // Reserve 4 bytes for length + + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpFieldNumberConstants.Event_Name, evnt.Name); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpFieldNumberConstants.Event_Time_Unix_Nano, (ulong)evnt.Timestamp.ToUnixTimeNanoseconds()); + writePosition = WriteEventAttributes(ref buffer, writePosition, sdkLimitOptions, evnt); + + ProtobufSerializer.WriteReservedLength(buffer, spanEventsLengthPosition, writePosition - (spanEventsLengthPosition + ReserveSizeForLength)); + eventCount++; + } + else + { + droppedEventCount++; + } + } + + if (droppedEventCount > 0) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpFieldNumberConstants.Span_Dropped_Events_Count, ProtobufWireType.VARINT); + writePosition = ProtobufSerializer.WriteVarInt32(buffer, writePosition, (uint)droppedEventCount); + } + + return writePosition; + } + + internal static int WriteEventAttributes(ref byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, ActivityEvent evnt) + { + int maxAttributeCount = sdkLimitOptions.SpanEventAttributeCountLimit ?? int.MaxValue; + int maxAttributeValueLength = sdkLimitOptions.AttributeValueLengthLimit ?? int.MaxValue; + int attributeCount = 0; + int droppedAttributeCount = 0; + + ProtobufOtlpTagWriter.OtlpTagWriterState otlpTagWriterState = new ProtobufOtlpTagWriter.OtlpTagWriterState + { + Buffer = buffer, + WritePosition = writePosition, + }; + + foreach (ref readonly var tag in evnt.EnumerateTagObjects()) + { + if (attributeCount < maxAttributeCount) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpFieldNumberConstants.Event_Attributes, ProtobufWireType.LEN); + int eventAttributesLengthPosition = otlpTagWriterState.WritePosition; + otlpTagWriterState.WritePosition += ReserveSizeForLength; + ProtobufOtlpTagWriter.Instance.TryWriteTag(ref otlpTagWriterState, tag.Key, tag.Value, maxAttributeValueLength); + ProtobufSerializer.WriteReservedLength(buffer, eventAttributesLengthPosition, otlpTagWriterState.WritePosition - (eventAttributesLengthPosition + ReserveSizeForLength)); + attributeCount++; + } + else + { + droppedAttributeCount++; + } + } + + if (droppedAttributeCount > 0) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(buffer, otlpTagWriterState.WritePosition, ProtobufOtlpFieldNumberConstants.Event_Dropped_Attributes_Count, ProtobufWireType.VARINT); + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteVarInt32(buffer, otlpTagWriterState.WritePosition, (uint)droppedAttributeCount); + } + + return otlpTagWriterState.WritePosition; + } + + internal static int WriteSpanLinks(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, Activity activity) + { + int maxLinksCount = sdkLimitOptions.SpanLinkCountLimit ?? int.MaxValue; + int linkCount = 0; + int droppedLinkCount = 0; + + foreach (ref readonly var link in activity.EnumerateLinks()) + { + if (linkCount < maxLinksCount) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpFieldNumberConstants.Span_Links, ProtobufWireType.LEN); + int spanLinksLengthPosition = writePosition; + writePosition += ReserveSizeForLength; // Reserve 4 bytes for length + + writePosition = ProtobufSerializer.WriteTagAndLength(buffer, writePosition, TraceIdSize, ProtobufOtlpFieldNumberConstants.Link_Trace_Id, ProtobufWireType.LEN); + writePosition = WriteTraceId(buffer, writePosition, link.Context.TraceId); + writePosition = ProtobufSerializer.WriteTagAndLength(buffer, writePosition, SpanIdSize, ProtobufOtlpFieldNumberConstants.Link_Span_Id, ProtobufWireType.LEN); + writePosition = WriteSpanId(buffer, writePosition, link.Context.SpanId); + if (link.Context.TraceState != null) + { + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpFieldNumberConstants.Span_Trace_State, link.Context.TraceState); + } + + writePosition = WriteLinkAttributes(buffer, writePosition, sdkLimitOptions, link); + writePosition = WriteTraceFlags(buffer, writePosition, link.Context.TraceFlags, link.Context.IsRemote, ProtobufOtlpFieldNumberConstants.Link_Flags); + + ProtobufSerializer.WriteReservedLength(buffer, spanLinksLengthPosition, writePosition - (spanLinksLengthPosition + ReserveSizeForLength)); + linkCount++; + } + else + { + droppedLinkCount++; + } + } + + if (droppedLinkCount > 0) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpFieldNumberConstants.Span_Dropped_Links_Count, ProtobufWireType.VARINT); + writePosition = ProtobufSerializer.WriteVarInt32(buffer, writePosition, (uint)droppedLinkCount); + } + + return writePosition; + } + + internal static int WriteLinkAttributes(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, ActivityLink link) + { + int maxAttributeCount = sdkLimitOptions.SpanLinkAttributeCountLimit ?? int.MaxValue; + int maxAttributeValueLength = sdkLimitOptions.AttributeValueLengthLimit ?? int.MaxValue; + int attributeCount = 0; + int droppedAttributeCount = 0; + + ProtobufOtlpTagWriter.OtlpTagWriterState otlpTagWriterState = new ProtobufOtlpTagWriter.OtlpTagWriterState + { + Buffer = buffer, + WritePosition = writePosition, + }; + + foreach (ref readonly var tag in link.EnumerateTagObjects()) + { + if (attributeCount < maxAttributeCount) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpFieldNumberConstants.Link_Attributes, ProtobufWireType.LEN); + int linkAttributesLengthPosition = otlpTagWriterState.WritePosition; + otlpTagWriterState.WritePosition += ReserveSizeForLength; + ProtobufOtlpTagWriter.Instance.TryWriteTag(ref otlpTagWriterState, tag.Key, tag.Value, maxAttributeValueLength); + ProtobufSerializer.WriteReservedLength(buffer, linkAttributesLengthPosition, otlpTagWriterState.WritePosition - (linkAttributesLengthPosition + ReserveSizeForLength)); + attributeCount++; + } + else + { + droppedAttributeCount++; + } + } + + if (droppedAttributeCount > 0) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(buffer, otlpTagWriterState.WritePosition, ProtobufOtlpFieldNumberConstants.Link_Dropped_Attributes_Count, ProtobufWireType.VARINT); + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteVarInt32(buffer, otlpTagWriterState.WritePosition, (uint)droppedAttributeCount); + } + + return otlpTagWriterState.WritePosition; + } + + internal static int WriteSpanStatus(byte[] buffer, int position, Activity activity, StatusCode? statusCode, string? statusMessage) + { + if (activity.Status == ActivityStatusCode.Unset && statusCode == null) + { + return position; + } + + var useActivity = activity.Status != ActivityStatusCode.Unset; + var isError = useActivity ? activity.Status == ActivityStatusCode.Error : statusCode == StatusCode.Error; + var description = useActivity ? activity.StatusDescription : statusMessage; + + if (isError && description != null) + { + var descriptionSpan = description.AsSpan(); + var numberOfUtf8CharsInString = ProtobufSerializer.GetNumberOfUtf8CharsInString(descriptionSpan); + position = ProtobufSerializer.WriteTagAndLength(buffer, position, numberOfUtf8CharsInString + 4, ProtobufOtlpFieldNumberConstants.Span_Status, ProtobufWireType.LEN); + position = ProtobufSerializer.WriteStringWithTag(buffer, position, ProtobufOtlpFieldNumberConstants.Status_Message, numberOfUtf8CharsInString, descriptionSpan); + } + else + { + position = ProtobufSerializer.WriteTagAndLength(buffer, position, 2, ProtobufOtlpFieldNumberConstants.Span_Status, ProtobufWireType.LEN); + } + + var finalStatusCode = useActivity ? (int)activity.Status : (statusCode != null && statusCode != StatusCode.Unset) ? (int)statusCode! : (int)StatusCode.Unset; + position = ProtobufSerializer.WriteEnumWithTag(buffer, position, ProtobufOtlpFieldNumberConstants.Status_Code, finalStatusCode); + + return position; + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs index bb76069214a..97d62b75345 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs @@ -5,6 +5,7 @@ using Google.Protobuf.Collections; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; @@ -370,8 +371,10 @@ void RunTest(SdkLimitOptions sdkOptions, Batch batch) } } - [Fact] - public void SpanLimitsTest() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SpanLimitsTest(bool useCustomSerializer) { var sdkOptions = new SdkLimitOptions() { @@ -406,7 +409,7 @@ public void SpanLimitsTest() activity.AddEvent(event1); activity.AddEvent(event2); - var otlpSpan = activity.ToOtlpSpan(sdkOptions); + var otlpSpan = useCustomSerializer ? ToOtlpSpan(sdkOptions, activity) : activity.ToOtlpSpan(sdkOptions); Assert.NotNull(otlpSpan); Assert.Equal(3, otlpSpan.Attributes.Count); @@ -432,8 +435,10 @@ public void SpanLimitsTest() Assert.Equal(new object().ToString()!.Substring(0, 4), otlpSpan.Links[0].Attributes[2].Value.StringValue); } - [Fact] - public void ToOtlpSpanTest() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ToOtlpSpanTest(bool useCustomSerializer) { using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); @@ -475,7 +480,7 @@ public void ToOtlpSpanTest() rootActivity.TraceId.CopyTo(traceIdSpan); var traceId = traceIdSpan.ToArray(); - var otlpSpan = rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); + var otlpSpan = useCustomSerializer ? ToOtlpSpan(DefaultSdkLimitOptions, rootActivity) : rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); Assert.NotNull(otlpSpan); Assert.Equal("root", otlpSpan.Name); @@ -511,7 +516,7 @@ public void ToOtlpSpanTest() rootActivity.Context.SpanId.CopyTo(parentIdSpan); var parentId = parentIdSpan.ToArray(); - otlpSpan = childActivity.ToOtlpSpan(DefaultSdkLimitOptions); + otlpSpan = useCustomSerializer ? ToOtlpSpan(DefaultSdkLimitOptions, childActivity) : childActivity.ToOtlpSpan(DefaultSdkLimitOptions); Assert.NotNull(otlpSpan); Assert.Equal("child", otlpSpan.Name); @@ -546,8 +551,10 @@ public void ToOtlpSpanTest() Assert.False(flags.HasFlag(OtlpTrace.SpanFlags.ContextIsRemoteMask)); } - [Fact] - public void ToOtlpSpanActivitiesWithNullArrayTest() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ToOtlpSpanActivitiesWithNullArrayTest(bool useCustomSerializer) { using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); @@ -557,7 +564,7 @@ public void ToOtlpSpanActivitiesWithNullArrayTest() var stringArr = new string?[] { "test", string.Empty, null }; rootActivity.SetTag("stringArray", stringArr); - var otlpSpan = rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); + var otlpSpan = useCustomSerializer ? ToOtlpSpan(DefaultSdkLimitOptions, rootActivity) : rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); Assert.NotNull(otlpSpan); @@ -570,17 +577,20 @@ public void ToOtlpSpanActivitiesWithNullArrayTest() } [Theory] - [InlineData(ActivityStatusCode.Unset, "Description will be ignored if status is Unset.")] - [InlineData(ActivityStatusCode.Ok, "Description will be ignored if status is Okay.")] - [InlineData(ActivityStatusCode.Error, "Description will be kept if status is Error.")] - public void ToOtlpSpanNativeActivityStatusTest(ActivityStatusCode expectedStatusCode, string statusDescription) + [InlineData(ActivityStatusCode.Unset, "Description will be ignored if status is Unset.", true)] + [InlineData(ActivityStatusCode.Ok, "Description will be ignored if status is Okay.", true)] + [InlineData(ActivityStatusCode.Error, "Description will be kept if status is Error.", true)] + [InlineData(ActivityStatusCode.Unset, "Description will be ignored if status is Unset.", false)] + [InlineData(ActivityStatusCode.Ok, "Description will be ignored if status is Okay.", false)] + [InlineData(ActivityStatusCode.Error, "Description will be kept if status is Error.", false)] + public void ToOtlpSpanNativeActivityStatusTest(ActivityStatusCode expectedStatusCode, string statusDescription, bool useCustomSerializer) { using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); using var activity = activitySource.StartActivity("Name"); Assert.NotNull(activity); activity.SetStatus(expectedStatusCode, statusDescription); - var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); + var otlpSpan = useCustomSerializer ? ToOtlpSpan(DefaultSdkLimitOptions, activity) : activity.ToOtlpSpan(DefaultSdkLimitOptions); Assert.NotNull(otlpSpan); if (expectedStatusCode == ActivityStatusCode.Unset) { @@ -690,9 +700,11 @@ public void ToOtlpSpanActivityStatusTakesPrecedenceOverStatusTagsWhenActivitySta } [Theory] - [InlineData(true)] - [InlineData(false)] - public void ToOtlpSpanTraceStateTest(bool traceStateWasSet) + [InlineData(true, true)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(false, false)] + public void ToOtlpSpanTraceStateTest(bool traceStateWasSet, bool useCustomSerializer) { using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); using var activity = activitySource.StartActivity("Name"); @@ -703,7 +715,7 @@ public void ToOtlpSpanTraceStateTest(bool traceStateWasSet) activity.TraceStateString = tracestate; } - var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); + var otlpSpan = useCustomSerializer ? ToOtlpSpan(DefaultSdkLimitOptions, activity) : activity.ToOtlpSpan(DefaultSdkLimitOptions); Assert.NotNull(otlpSpan); if (traceStateWasSet) @@ -892,11 +904,15 @@ public void NamedOptionsMutateSeparateInstancesTest() } [Theory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - public void SpanFlagsTest(bool isRecorded, bool isRemote) + [InlineData(true, true, true)] + [InlineData(true, false, true)] + [InlineData(false, true, true)] + [InlineData(false, false, true)] + [InlineData(true, true, false)] + [InlineData(true, false, false)] + [InlineData(false, true, false)] + [InlineData(false, false, false)] + public void SpanFlagsTest(bool isRecorded, bool isRemote, bool useCustomSerializer) { using var activitySource = new ActivitySource(nameof(this.SpanFlagsTest)); @@ -909,7 +925,7 @@ public void SpanFlagsTest(bool isRecorded, bool isRemote) using var rootActivity = activitySource.StartActivity("root", ActivityKind.Server, ctx); Assert.NotNull(rootActivity); - var otlpSpan = rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); + var otlpSpan = useCustomSerializer ? ToOtlpSpan(DefaultSdkLimitOptions, rootActivity) : rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); Assert.NotNull(otlpSpan); var flags = (OtlpTrace.SpanFlags)otlpSpan.Flags; @@ -938,11 +954,15 @@ public void SpanFlagsTest(bool isRecorded, bool isRemote) } [Theory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - public void SpanLinkFlagsTest(bool isRecorded, bool isRemote) + [InlineData(true, true, true)] + [InlineData(true, false, true)] + [InlineData(false, true, true)] + [InlineData(false, false, true)] + [InlineData(true, true, false)] + [InlineData(true, false, false)] + [InlineData(false, true, false)] + [InlineData(false, false, false)] + public void SpanLinkFlagsTest(bool isRecorded, bool isRemote, bool useCustomSerializer) { using var activitySource = new ActivitySource(nameof(this.SpanLinkFlagsTest)); @@ -960,7 +980,7 @@ public void SpanLinkFlagsTest(bool isRecorded, bool isRemote) using var rootActivity = activitySource.StartActivity("root", ActivityKind.Server, default(ActivityContext), links: links); Assert.NotNull(rootActivity); - var otlpSpan = rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); + var otlpSpan = useCustomSerializer ? ToOtlpSpan(DefaultSdkLimitOptions, rootActivity) : rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); Assert.NotNull(otlpSpan); var spanLink = Assert.Single(otlpSpan.Links); @@ -990,6 +1010,15 @@ public void SpanLinkFlagsTest(bool isRecorded, bool isRemote) } } + private static OtlpTrace.Span? ToOtlpSpan(SdkLimitOptions sdkOptions, Activity activity) + { + var buffer = new byte[4096]; + var writePosition = ProtobufOtlpTraceSerializer.WriteSpan(buffer, 0, sdkOptions, activity); + using var stream = new MemoryStream(buffer, 0, writePosition); + var scopeSpans = OtlpTrace.ScopeSpans.Parser.ParseFrom(stream); + return scopeSpans.Spans.FirstOrDefault(); + } + private void ArrayValueAsserts(RepeatedField values) { var expectedStringArray = new string?[] { "1234", "1234", string.Empty, null };