diff --git a/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md b/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md index 816d10823f..95d9c0e3fe 100644 --- a/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md @@ -8,6 +8,10 @@ * TraceExporter bug fix to not export non-recorded Activities. [352](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/352) +* Add support for the native `Activity` properties `Status` and +`StatusDescription`. +[359](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/359) + ## 1.2.6 [2022-Apr-21] * Set GenevaMetricExporter temporality preference back to Delta. @@ -15,9 +19,9 @@ ## 1.2.5 [2022-Apr-20] Broken -Note: This release was broken due to the GenevaMetricExporter -using a TemporalityPreference of Cumulative instead of Delta, it has been -unlisted from NuGet. +Note: This release was broken due to the GenevaMetricExporter using a +TemporalityPreference of Cumulative instead of Delta, it has been unlisted from +NuGet. [303](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/303) is the PR that introduced this bug to GenevaMetricExporterExtensions.cs @@ -26,20 +30,17 @@ is the PR that introduced this bug to GenevaMetricExporterExtensions.cs ## 1.2.4 [2022-Apr-20] Broken -This is the first release of the `OpenTelemetry.Exporter.Geneva` -project. -Note: This release was broken due to using OpenTelemetry 1.2.0-rc5. -Therefore, it has been unlisted on NuGet. +This is the first release of the `OpenTelemetry.Exporter.Geneva` project. Note: +This release was broken due to using OpenTelemetry 1.2.0-rc5. Therefore, it has +been unlisted on NuGet. -* LogExporter modified to stop calling `ToString()` -on `LogRecord.State` to obtain Log body. It now -obtains body from `LogRecord.FormattedMessage` -or special casing "{OriginalFormat}" only. +* LogExporter modified to stop calling `ToString()` on `LogRecord.State` to +obtain Log body. It now obtains body from `LogRecord.FormattedMessage` or +special casing "{OriginalFormat}" only. [295](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/295) -* Fixed a bug which causes LogExporter to not -serialize if the `LogRecord.State` had a -single KeyValuePair. +* Fixed a bug which causes LogExporter to not serialize if the `LogRecord.State` +had a single KeyValuePair. [295](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/295) * Update OTel SDK version to `1.2.0-rc5`. diff --git a/src/OpenTelemetry.Exporter.Geneva/GenevaTraceExporter.cs b/src/OpenTelemetry.Exporter.Geneva/GenevaTraceExporter.cs index 1c776d79d8..bbe5501481 100644 --- a/src/OpenTelemetry.Exporter.Geneva/GenevaTraceExporter.cs +++ b/src/OpenTelemetry.Exporter.Geneva/GenevaTraceExporter.cs @@ -106,6 +106,7 @@ public GenevaTraceExporter(GenevaExporterOptions options) } dedicatedFields["otel.status_code"] = true; + dedicatedFields["otel.status_description"] = true; this.m_dedicatedFields = dedicatedFields; } @@ -333,6 +334,9 @@ internal int SerializeActivity(Activity activity) // Iteration #1 - Get those fields which become dedicated column // i.e all PartB fields and opt-in part c fields. bool hasEnvProperties = false; + bool isStatusSuccess = true; + string statusDescription = string.Empty; + foreach (var entry in activity.TagObjects) { // TODO: check name collision @@ -344,11 +348,16 @@ internal int SerializeActivity(Activity activity) { if (string.Equals(entry.Value.ToString(), "ERROR", StringComparison.Ordinal)) { - MessagePackSerializer.SerializeBool(buffer, idxSuccessPatch, false); + isStatusSuccess = false; } continue; } + else if (string.Equals(entry.Key, "otel.status_description", StringComparison.Ordinal)) + { + statusDescription = entry.Value.ToString(); + continue; + } else if (this.m_customFields == null || this.m_customFields.ContainsKey(entry.Key)) { // TODO: the above null check can be optimized and avoided inside foreach. @@ -391,6 +400,34 @@ internal int SerializeActivity(Activity activity) cntFields += 1; MessagePackSerializer.WriteUInt16(buffer, idxMapSizeEnvPropertiesPatch, envPropertiesCount); } + + if (activity.Status != ActivityStatusCode.Unset) + { + if (activity.Status == ActivityStatusCode.Error) + { + MessagePackSerializer.SerializeBool(buffer, idxSuccessPatch, false); + } + + if (!string.IsNullOrEmpty(activity.StatusDescription)) + { + cursor = MessagePackSerializer.SerializeAsciiString(buffer, cursor, "statusMessage"); + cursor = MessagePackSerializer.SerializeUnicodeString(buffer, cursor, activity.StatusDescription); + cntFields += 1; + } + } + else + { + if (!isStatusSuccess) + { + MessagePackSerializer.SerializeBool(buffer, idxSuccessPatch, false); + if (!string.IsNullOrEmpty(statusDescription)) + { + cursor = MessagePackSerializer.SerializeAsciiString(buffer, cursor, "statusMessage"); + cursor = MessagePackSerializer.SerializeUnicodeString(buffer, cursor, statusDescription); + cntFields += 1; + } + } + } #endregion MessagePackSerializer.WriteUInt16(buffer, this.m_idxMapSizePatch, cntFields); @@ -436,8 +473,6 @@ internal int SerializeActivity(Activity activity) ["messaging.system"] = "messagingSystem", ["messaging.destination"] = "messagingDestination", ["messaging.url"] = "messagingUrl", - - ["otel.status_description"] = "statusMessage", }; private bool isDisposed; diff --git a/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaTraceExporterTests.cs b/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaTraceExporterTests.cs index 8474dc2b21..96d81a9d3e 100644 --- a/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaTraceExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaTraceExporterTests.cs @@ -205,6 +205,7 @@ public void GenevaTraceExporter_Serialization_Success(bool hasTableNameMapping, // Set the ActivitySourceName to the unique value of the test method name to avoid interference with // the ActivitySource used by other unit tests. var sourceName = GetTestMethodName(); + Action> customChecksForActivity = null; using var listener = new ActivityListener(); listener.ShouldListenTo = (activitySource) => activitySource.Name == sourceName; @@ -213,7 +214,7 @@ public void GenevaTraceExporter_Serialization_Success(bool hasTableNameMapping, { _ = exporter.SerializeActivity(activity); object fluentdData = MessagePack.MessagePackSerializer.Deserialize(m_buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Instance); - this.AssertFluentdForwardModeForActivity(exporterOptions, fluentdData, activity, CS40_PART_B_MAPPING, dedicatedFields); + this.AssertFluentdForwardModeForActivity(exporterOptions, fluentdData, activity, CS40_PART_B_MAPPING, dedicatedFields, customChecksForActivity); invocationCount++; }; ActivitySource.AddActivityListener(listener); @@ -246,11 +247,28 @@ public void GenevaTraceExporter_Serialization_Success(bool hasTableNameMapping, activity?.SetTag("clientRequestId", "58a37988-2c05-427a-891f-5e0e1266fcc5"); activity?.SetTag("foo", 1); activity?.SetTag("bar", 2); - activity?.SetStatus(Status.Error); + activity?.SetStatus(Status.Error.WithDescription("Error description from OTel API")); } } - Assert.Equal(2, invocationCount); + using (var activity = source.StartActivity("TestActivityForSetStatusAPI")) + { + activity?.SetStatus(ActivityStatusCode.Error, "Error description from .NET API"); + } + + // If the activity Status is set using both the OTel API and the .NET API, the `Status` and `StatusDescription` set by + // the .NET API is chosen + using (var activity = source.StartActivity("PreferStatusFromDotnetAPI")) + { + activity?.SetStatus(Status.Error.WithDescription("Error description from OTel API")); + activity?.SetStatus(ActivityStatusCode.Error, "Error description from .NET API"); + customChecksForActivity = mapping => + { + Assert.Equal("Error description from .NET API", mapping["statusMessage"]); + }; + } + + Assert.Equal(4, invocationCount); } finally { @@ -397,7 +415,7 @@ private static string GetTestMethodName([CallerMemberName] string callingMethodN return callingMethodName; } - private void AssertFluentdForwardModeForActivity(GenevaExporterOptions exporterOptions, object fluentdData, Activity activity, IReadOnlyDictionary CS40_PART_B_MAPPING, IReadOnlyDictionary dedicatedFields) + private void AssertFluentdForwardModeForActivity(GenevaExporterOptions exporterOptions, object fluentdData, Activity activity, IReadOnlyDictionary CS40_PART_B_MAPPING, IReadOnlyDictionary dedicatedFields, Action> customChecksForActivity) { /* Fluentd Forward Mode: [ @@ -461,7 +479,22 @@ private void AssertFluentdForwardModeForActivity(GenevaExporterOptions exporterO Assert.Equal(activity.StartTimeUtc, mapping["startTime"]); var activityStatusCode = activity.GetStatus().StatusCode; - Assert.Equal(activityStatusCode == StatusCode.Error ? false : true, mapping["success"]); + + if (activity.Status == ActivityStatusCode.Error) + { + Assert.False((bool)mapping["success"]); + Assert.Equal(activity.StatusDescription, mapping["statusMessage"]); + } + else if (activityStatusCode == StatusCode.Error) + { + Assert.False((bool)mapping["success"]); + var activityStatusDesc = activity.GetStatus().Description; + Assert.Equal(activityStatusDesc, mapping["statusMessage"]); + } + else + { + Assert.True((bool)mapping["success"]); + } // Part B Span optional fields and Part C fields if (activity.ParentSpanId != default) @@ -509,6 +542,11 @@ private void AssertFluentdForwardModeForActivity(GenevaExporterOptions exporterO // Status code check is already done when we check for "success" key in the mapping continue; } + else if (string.Equals(tag.Key, "otel.status_description", StringComparison.Ordinal)) + { + // Status description check is already done when we check for "statusMessage" key in the mapping + continue; + } else { // If CustomFields are proivded, dedicatedFields will be populated @@ -526,6 +564,8 @@ private void AssertFluentdForwardModeForActivity(GenevaExporterOptions exporterO // Epilouge Assert.Equal("DateTime", timeFormat["TimeFormat"]); + + customChecksForActivity?.Invoke(mapping); } } }