diff --git a/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md b/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md index bb63d18174..b9ab34fac2 100644 --- a/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +* Support for serializing `ActivityEvent`s. ## 1.14.0 diff --git a/src/OpenTelemetry.Exporter.Geneva/Internal/MsgPack/MsgPackTraceExporter.cs b/src/OpenTelemetry.Exporter.Geneva/Internal/MsgPack/MsgPackTraceExporter.cs index d607758eb4..035d0020b1 100644 --- a/src/OpenTelemetry.Exporter.Geneva/Internal/MsgPack/MsgPackTraceExporter.cs +++ b/src/OpenTelemetry.Exporter.Geneva/Internal/MsgPack/MsgPackTraceExporter.cs @@ -473,6 +473,55 @@ internal ArraySegment SerializeActivity(Activity activity) cntFields += 1; } + var eventEnumerator = activity.EnumerateEvents(); + if (eventEnumerator.MoveNext()) + { + cursor = MessagePackSerializer.SerializeAsciiString(buffer, cursor, "events"); + cursor = MessagePackSerializer.WriteArrayHeader(buffer, cursor, ushort.MaxValue); + var idxEventPatch = cursor - 2; + + ushort cntEvent = 0; + do + { + ref readonly var evt = ref eventEnumerator.Current; + + var eventTagEnumerator = evt.EnumerateTagObjects(); + var hasTags = eventTagEnumerator.MoveNext(); + var eventFieldCount = hasTags ? 3 : 2; + + cursor = MessagePackSerializer.WriteMapHeader(buffer, cursor, (byte)eventFieldCount); + cursor = MessagePackSerializer.SerializeAsciiString(buffer, cursor, "name"); + cursor = MessagePackSerializer.SerializeUnicodeString(buffer, cursor, evt.Name); + cursor = MessagePackSerializer.SerializeAsciiString(buffer, cursor, "time"); + cursor = MessagePackSerializer.SerializeUtcDateTime(buffer, cursor, evt.Timestamp.UtcDateTime); + + if (hasTags) + { + cursor = MessagePackSerializer.SerializeAsciiString(buffer, cursor, "properties"); + cursor = MessagePackSerializer.WriteMapHeader(buffer, cursor, ushort.MaxValue); + var idxEventAttributesPatch = cursor - 2; + + ushort cntEventAttributes = 0; + do + { + ref readonly var tag = ref eventTagEnumerator.Current; + cursor = MessagePackSerializer.SerializeUnicodeString(buffer, cursor, tag.Key); + cursor = MessagePackSerializer.Serialize(buffer, cursor, tag.Value); + cntEventAttributes += 1; + } + while (eventTagEnumerator.MoveNext()); + + MessagePackSerializer.WriteUInt16(buffer, idxEventAttributesPatch, cntEventAttributes); + } + + cntEvent += 1; + } + while (eventEnumerator.MoveNext()); + + MessagePackSerializer.WriteUInt16(buffer, idxEventPatch, cntEvent); + cntFields += 1; + } + // TODO: The current approach is to iterate twice over TagObjects so that all // env_properties can be added the very end. This avoids speculating the size // and preallocating a separate buffer for it. diff --git a/src/OpenTelemetry.Exporter.Geneva/Internal/Tld/TldTraceExporter.cs b/src/OpenTelemetry.Exporter.Geneva/Internal/Tld/TldTraceExporter.cs index 437d6be0d7..a46baa2443 100644 --- a/src/OpenTelemetry.Exporter.Geneva/Internal/Tld/TldTraceExporter.cs +++ b/src/OpenTelemetry.Exporter.Geneva/Internal/Tld/TldTraceExporter.cs @@ -227,6 +227,28 @@ internal EventBuilder SerializeActivity(Activity activity) partBFieldsCount++; } + var eventEnumerator = activity.EnumerateEvents(); + if (eventEnumerator.MoveNext()) + { + var keyValuePairsForEvents = KeyValuePairs.Value ??= []; + keyValuePairsForEvents.Clear(); + + do + { + ref readonly var evt = ref eventEnumerator.Current; + + keyValuePairsForEvents.Add(new("name", evt.Name)); + keyValuePairsForEvents.Add(new("time", evt.Timestamp.UtcDateTime)); + keyValuePairsForEvents.Add(new("properties", evt.Tags)); + } + while (eventEnumerator.MoveNext()); + + var serializedEventsStringAsBytes = JsonSerializer.SerializeKeyValuePairsListAsBytes(keyValuePairsForEvents, out var count); + eb.AddCountedAnsiString("events", serializedEventsStringAsBytes, 0, count); + + partBFieldsCount++; + } + byte hasEnvProperties = 0; byte isStatusSuccess = 1; var statusDescription = string.Empty; diff --git a/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaTraceExporterTests.cs b/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaTraceExporterTests.cs index 96dfebe1c1..ba64d16930 100644 --- a/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaTraceExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaTraceExporterTests.cs @@ -170,6 +170,10 @@ public void GenevaTraceExporter_Success_Windows() var link = new ActivityLink(new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded)); using (var activity = source.StartActivity("Foo", ActivityKind.Internal, null, null, [link])) { + activity?.AddEvent(new ActivityEvent("TestEvent", DateTimeOffset.UtcNow, new ActivityTagsCollection + { + { "eventKey", "eventValue" }, + })); } using (var activity = source.StartActivity("Bar")) @@ -305,6 +309,14 @@ public void GenevaTraceExporter_Serialization_Success(bool hasTableNameMapping, activity?.SetTag("clientRequestId", "58a37988-2c05-427a-891f-5e0e1266fcc5"); activity?.SetTag("foo", 1); activity?.SetTag("bar", 2); + + activity?.AddEvent(new ActivityEvent("TestEvent1", DateTimeOffset.UtcNow, new ActivityTagsCollection + { + { "eventFoo", 1 }, + { "eventBar", "Hello, World!" }, + })); + activity?.AddEvent(new ActivityEvent("TestEvent2")); + #pragma warning disable CS0618 // Type or member is obsolete activity?.SetStatus(Status.Error.WithDescription("Error description from OTel API")); #pragma warning restore CS0618 // Type or member is obsolete @@ -340,7 +352,7 @@ public void GenevaTraceExporter_Serialization_Success(bool hasTableNameMapping, this.AssertMappingEntry(userFieldsLocation, "resourceAndPrepopulated", "comes from prepopulated"); } - // Linked spans are checked in CheckSpanForActivity, so no need to do a custom check here + // Linked spans and events are checked in CheckSpanForActivity, so no need to do a custom check here }); } @@ -1002,6 +1014,41 @@ private void CheckSpanForActivity(GenevaExporterOptions exporterOptions, object } #endregion + #region Assert Activity Events + if (activity.Events.Any()) + { + Assert.Contains(mapping, m => (m.Key as string) == "events"); + var mappingEvents = mapping["events"] as IEnumerable; + using var activityEventsEnumerator = activity.Events.GetEnumerator(); + using var mappingEventsEnumerator = mappingEvents.GetEnumerator(); + while (activityEventsEnumerator.MoveNext() && mappingEventsEnumerator.MoveNext()) + { + var activityEvent = activityEventsEnumerator.Current; + var mappingEvent = mappingEventsEnumerator.Current as Dictionary; + + Assert.Equal(activityEvent.Name, mappingEvent["name"]); + Assert.Contains("time", mappingEvent.Keys); + + if (activityEvent.Tags.Any()) + { + Assert.Contains("properties", mappingEvent.Keys); + var mappingEventProperties = mappingEvent["properties"] as Dictionary; + Assert.NotNull(mappingEventProperties); + foreach (var tag in activityEvent.Tags) + { + Assert.Contains(mappingEventProperties, kvp => (kvp.Key as string) == tag.Key); + } + } + } + + Assert.Equal(activityEventsEnumerator.MoveNext(), mappingEventsEnumerator.MoveNext()); + } + else + { + Assert.DoesNotContain(mapping, m => (m.Key as string) == "events"); + } + #endregion + #region Assert Activity Tags if (exporterOptions.CustomFields == null) {