Add status to activity (#802)
* Extension method to support Status. * Added tests and status support for otlp. * Fix test * Added description to Status API * Resolving conflict * Resolve conflict * Avoid status object allocation. * Check for null * Resolving conflict * resolving conflict Co-authored-by: Cijo Thomas <cithomas@microsoft.com>
This commit is contained in:
parent
7960b2903a
commit
4eece94ec6
|
|
@ -0,0 +1,68 @@
|
|||
// <copyright file="ActivityExtensions.cs" company="OpenTelemetry Authors">
|
||||
// Copyright The OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// </copyright>
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenTelemetry.Trace
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods on Activity.
|
||||
/// </summary>
|
||||
public static class ActivityExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the status of activity execution.
|
||||
/// Activity class in .NET does not support 'Status'.
|
||||
/// This extension provides a workaround to store Status as special tags with key name of ot.status_code and ot.status_description.
|
||||
/// Read more about SetStatus here https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/api.md#set-status.
|
||||
/// </summary>
|
||||
/// <param name="activity">Activity instance.</param>
|
||||
/// <param name="status">Activity execution status.</param>
|
||||
public static void SetStatus(this Activity activity, Status status)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(activity));
|
||||
}
|
||||
|
||||
activity.AddTag(SpanAttributeConstants.StatusCodeKey, SpanHelper.GetCachedCanonicalCodeString(status.CanonicalCode));
|
||||
activity.AddTag(SpanAttributeConstants.StatusDescriptionKey, status.Description);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status of activity execution.
|
||||
/// Activity class in .NET does not support 'Status'.
|
||||
/// This extension provides a workaround to retrieve Status from special tags with key name ot.status_code and ot.status_description.
|
||||
/// </summary>
|
||||
/// <param name="activity">Activity instance.</param>
|
||||
/// <returns>Activity execution status.</returns>
|
||||
public static Status GetStatus(this Activity activity)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(activity));
|
||||
}
|
||||
|
||||
var statusCanonicalCode = activity.Tags.FirstOrDefault(k => k.Key == SpanAttributeConstants.StatusCodeKey).Value;
|
||||
var statusDescription = activity.Tags.FirstOrDefault(d => d.Key == SpanAttributeConstants.StatusDescriptionKey).Value;
|
||||
|
||||
var status = SpanHelper.ResolveCanonicalCodeToStatus(statusCanonicalCode);
|
||||
return status.IsValid ? status.WithDescription(statusDescription) : status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,8 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// </copyright>
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenTelemetry.Trace
|
||||
|
|
@ -22,6 +24,8 @@ namespace OpenTelemetry.Trace
|
|||
/// </summary>
|
||||
public static class SpanHelper
|
||||
{
|
||||
private static readonly Status DefaultStatus = default;
|
||||
|
||||
private static readonly Dictionary<StatusCanonicalCode, string> StatusCanonicalCodeToStringCache = new Dictionary<StatusCanonicalCode, string>()
|
||||
{
|
||||
[StatusCanonicalCode.Ok] = StatusCanonicalCode.Ok.ToString(),
|
||||
|
|
@ -125,5 +129,43 @@ namespace OpenTelemetry.Trace
|
|||
|
||||
return newStatus;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method that returns Status from <see cref="StatusCanonicalCode"/> to save on allocations.
|
||||
/// </summary>
|
||||
/// <param name="statusCanonicalCode"><see cref="StatusCanonicalCode"/>.</param>
|
||||
/// <returns>Resolved span <see cref="Status"/> for the Canonical status code.</returns>
|
||||
public static Status ResolveCanonicalCodeToStatus(string statusCanonicalCode)
|
||||
{
|
||||
bool success = Enum.TryParse(statusCanonicalCode, out StatusCanonicalCode canonicalCode);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
return DefaultStatus;
|
||||
}
|
||||
|
||||
var status = canonicalCode switch
|
||||
{
|
||||
StatusCanonicalCode.Cancelled => Status.Cancelled,
|
||||
StatusCanonicalCode.Unknown => Status.Unknown,
|
||||
StatusCanonicalCode.InvalidArgument => Status.InvalidArgument,
|
||||
StatusCanonicalCode.DeadlineExceeded => Status.DeadlineExceeded,
|
||||
StatusCanonicalCode.NotFound => Status.NotFound,
|
||||
StatusCanonicalCode.AlreadyExists => Status.AlreadyExists,
|
||||
StatusCanonicalCode.PermissionDenied => Status.PermissionDenied,
|
||||
StatusCanonicalCode.ResourceExhausted => Status.ResourceExhausted,
|
||||
StatusCanonicalCode.FailedPrecondition => Status.FailedPrecondition,
|
||||
StatusCanonicalCode.Aborted => Status.Aborted,
|
||||
StatusCanonicalCode.OutOfRange => Status.OutOfRange,
|
||||
StatusCanonicalCode.Unimplemented => Status.Unimplemented,
|
||||
StatusCanonicalCode.Internal => Status.Internal,
|
||||
StatusCanonicalCode.Unavailable => Status.Unavailable,
|
||||
StatusCanonicalCode.DataLoss => Status.DataLoss,
|
||||
StatusCanonicalCode.Unauthenticated => Status.Unauthenticated,
|
||||
_ => Status.Ok,
|
||||
};
|
||||
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation
|
|||
SpanId = ByteString.CopyFrom(spanIdBytes.ToArray()),
|
||||
ParentSpanId = parentSpanIdString,
|
||||
|
||||
// TODO: Status is still pending, need to pursue OTEL spec change.
|
||||
Status = ToOtlpStatus(activity.GetStatus()),
|
||||
|
||||
StartTimeUnixNano = (ulong)startTimeUnixNano,
|
||||
EndTimeUnixNano = (ulong)(startTimeUnixNano + activity.Duration.ToNanoseconds()),
|
||||
|
|
@ -115,7 +115,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation
|
|||
foreach (var kvp in activity.Tags)
|
||||
{
|
||||
var attribute = ToOtlpAttribute(kvp);
|
||||
if (attribute != null)
|
||||
if (attribute != null && attribute.Key != SpanAttributeConstants.StatusCodeKey && attribute.Key != SpanAttributeConstants.StatusDescriptionKey)
|
||||
{
|
||||
otlpSpan.Attributes.Add(attribute);
|
||||
}
|
||||
|
|
@ -129,6 +129,27 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation
|
|||
return otlpSpan;
|
||||
}
|
||||
|
||||
private static OtlpTrace.Status ToOtlpStatus(Status status)
|
||||
{
|
||||
if (!status.IsValid)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var otlpStatus = new Opentelemetry.Proto.Trace.V1.Status
|
||||
{
|
||||
// The numerical values of the two enumerations match, a simple cast is enough.
|
||||
Code = (OtlpTrace.Status.Types.StatusCode)status.CanonicalCode,
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(status.Description))
|
||||
{
|
||||
otlpStatus.Message = status.Description;
|
||||
}
|
||||
|
||||
return otlpStatus;
|
||||
}
|
||||
|
||||
private static Dictionary<Resource, Dictionary<ActivitySource, List<OtlpTrace.Span>>> GroupByResourceAndLibrary(
|
||||
IEnumerable<Activity> activityBatch)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,307 +1,307 @@
|
|||
// <copyright file="ActivityExtensionsTest.cs" company="OpenTelemetry Authors">
|
||||
// Copyright The OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// </copyright>
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Google.Protobuf.Collections;
|
||||
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
|
||||
using Opentelemetry.Proto.Common.V1;
|
||||
#if NET452
|
||||
using OpenTelemetry.Internal;
|
||||
#endif
|
||||
using OpenTelemetry.Trace;
|
||||
using OpenTelemetry.Trace.Configuration;
|
||||
using Xunit;
|
||||
using OtlpCommon = Opentelemetry.Proto.Common.V1;
|
||||
using OtlpTrace = Opentelemetry.Proto.Trace.V1;
|
||||
|
||||
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests
|
||||
{
|
||||
public class ActivityExtensionsTest
|
||||
{
|
||||
static ActivityExtensionsTest()
|
||||
{
|
||||
Activity.DefaultIdFormat = ActivityIdFormat.W3C;
|
||||
Activity.ForceDefaultIdFormat = true;
|
||||
|
||||
var listener = new ActivityListener
|
||||
{
|
||||
ShouldListenTo = _ => true,
|
||||
GetRequestedDataUsingParentId = (ref ActivityCreationOptions<string> options) => ActivityDataRequest.AllData,
|
||||
GetRequestedDataUsingContext = (ref ActivityCreationOptions<ActivityContext> options) => ActivityDataRequest.AllData,
|
||||
};
|
||||
|
||||
ActivitySource.AddActivityListener(listener);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToOtlpResourceSpansTest()
|
||||
{
|
||||
var evenTags = new[] { new KeyValuePair<string, string>("k0", "v0") };
|
||||
var oddTags = new[] { new KeyValuePair<string, string>("k1", "v1") };
|
||||
var sources = new[]
|
||||
{
|
||||
new ActivitySource("even", "2.4.6"),
|
||||
new ActivitySource("odd", "1.3.5"),
|
||||
};
|
||||
|
||||
var resource = new Resources.Resource(
|
||||
new List<KeyValuePair<string, object>>
|
||||
{
|
||||
new KeyValuePair<string, object>(Resources.Resource.ServiceNamespaceKey, "ns1"),
|
||||
});
|
||||
|
||||
// This following is done just to set Resource to Activity.
|
||||
using var openTelemetrySdk = OpenTelemetrySdk.EnableOpenTelemetry(b => b
|
||||
.AddActivitySource(sources[0].Name)
|
||||
.AddActivitySource(sources[1].Name)
|
||||
.SetResource(resource));
|
||||
|
||||
var activities = new List<Activity>();
|
||||
Activity activity = null;
|
||||
const int numOfSpans = 10;
|
||||
bool isEven;
|
||||
for (var i = 0; i < numOfSpans; i++)
|
||||
{
|
||||
isEven = i % 2 == 0;
|
||||
var source = sources[i % 2];
|
||||
var activityKind = isEven ? ActivityKind.Client : ActivityKind.Server;
|
||||
var activityTags = isEven ? evenTags : oddTags;
|
||||
|
||||
activity = source.StartActivity($"span-{i}", activityKind, activity?.Context ?? default, activityTags);
|
||||
activities.Add(activity);
|
||||
}
|
||||
|
||||
activities.Reverse();
|
||||
|
||||
var otlpResourceSpans = activities.ToOtlpResourceSpans();
|
||||
|
||||
Assert.Single(otlpResourceSpans);
|
||||
var oltpResource = otlpResourceSpans.First().Resource;
|
||||
Assert.Equal(resource.Attributes.First().Key, oltpResource.Attributes.First().Key);
|
||||
Assert.Equal(resource.Attributes.First().Value, oltpResource.Attributes.First().Value.StringValue);
|
||||
|
||||
foreach (var instrumentationLibrarySpans in otlpResourceSpans.First().InstrumentationLibrarySpans)
|
||||
{
|
||||
Assert.Equal(numOfSpans / 2, instrumentationLibrarySpans.Spans.Count);
|
||||
Assert.NotNull(instrumentationLibrarySpans.InstrumentationLibrary);
|
||||
|
||||
var expectedSpanNames = new List<string>();
|
||||
var start = instrumentationLibrarySpans.InstrumentationLibrary.Name == "even" ? 0 : 1;
|
||||
for (var i = start; i < numOfSpans; i += 2)
|
||||
{
|
||||
expectedSpanNames.Add($"span-{i}");
|
||||
}
|
||||
|
||||
var otlpSpans = instrumentationLibrarySpans.Spans;
|
||||
Assert.Equal(expectedSpanNames.Count, otlpSpans.Count);
|
||||
|
||||
var kv0 = new OtlpCommon.KeyValue { Key = "k0", Value = new AnyValue { StringValue = "v0" } };
|
||||
var kv1 = new OtlpCommon.KeyValue { Key = "k1", Value = new AnyValue { StringValue = "v1" } };
|
||||
|
||||
var expectedTag = instrumentationLibrarySpans.InstrumentationLibrary.Name == "even"
|
||||
? kv0
|
||||
: kv1;
|
||||
|
||||
foreach (var otlpSpan in otlpSpans)
|
||||
{
|
||||
Assert.Contains(otlpSpan.Name, expectedSpanNames);
|
||||
Assert.Contains(expectedTag, otlpSpan.Attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToOtlpSpanTest()
|
||||
{
|
||||
var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest));
|
||||
|
||||
using var rootActivity = activitySource.StartActivity("root", ActivityKind.Producer);
|
||||
|
||||
var attributes = new List<KeyValuePair<string, object>>
|
||||
{
|
||||
new KeyValuePair<string, object>("bool", true),
|
||||
new KeyValuePair<string, object>("long", 1L),
|
||||
new KeyValuePair<string, object>("string", "text"),
|
||||
new KeyValuePair<string, object>("double", 3.14),
|
||||
|
||||
// TODO: update if arrays of standard attribute types are supported
|
||||
new KeyValuePair<string, object>("unknown_attrib_type", new byte[] { 1 }),
|
||||
};
|
||||
|
||||
foreach (var kvp in attributes)
|
||||
{
|
||||
rootActivity.AddTag(kvp.Key, kvp.Value.ToString());
|
||||
}
|
||||
|
||||
var startTime = new DateTime(2020, 02, 20, 20, 20, 20, DateTimeKind.Utc);
|
||||
|
||||
DateTimeOffset dateTimeOffset;
|
||||
#if NET452
|
||||
dateTimeOffset = DateTimeOffsetExtensions.FromUnixTimeMilliseconds(0);
|
||||
#else
|
||||
dateTimeOffset = DateTimeOffset.FromUnixTimeMilliseconds(0);
|
||||
#endif
|
||||
|
||||
var expectedUnixTimeTicks = (ulong)(startTime.Ticks - dateTimeOffset.Ticks);
|
||||
var duration = TimeSpan.FromMilliseconds(1555);
|
||||
|
||||
rootActivity.SetStartTime(startTime);
|
||||
rootActivity.SetEndTime(startTime + duration);
|
||||
|
||||
Span<byte> traceIdSpan = stackalloc byte[16];
|
||||
rootActivity.TraceId.CopyTo(traceIdSpan);
|
||||
var traceId = traceIdSpan.ToArray();
|
||||
|
||||
var otlpSpan = rootActivity.ToOtlpSpan();
|
||||
|
||||
Assert.NotNull(otlpSpan);
|
||||
Assert.Equal("root", otlpSpan.Name);
|
||||
Assert.Equal(OtlpTrace.Span.Types.SpanKind.Producer, otlpSpan.Kind);
|
||||
Assert.Equal(traceId, otlpSpan.TraceId);
|
||||
Assert.Empty(otlpSpan.ParentSpanId);
|
||||
Assert.Null(otlpSpan.Status);
|
||||
Assert.Empty(otlpSpan.Events);
|
||||
Assert.Empty(otlpSpan.Links);
|
||||
AssertActivityTagsIntoOtlpAttributes(attributes, otlpSpan.Attributes);
|
||||
|
||||
var expectedStartTimeUnixNano = 100 * expectedUnixTimeTicks;
|
||||
Assert.Equal(expectedStartTimeUnixNano, otlpSpan.StartTimeUnixNano);
|
||||
var expectedEndTimeUnixNano = expectedStartTimeUnixNano + (duration.TotalMilliseconds * 1_000_000);
|
||||
Assert.Equal(expectedEndTimeUnixNano, otlpSpan.EndTimeUnixNano);
|
||||
|
||||
var childLinks = new List<ActivityLink> { new ActivityLink(rootActivity.Context, attributes) };
|
||||
var childActivity = activitySource.StartActivity(
|
||||
"child",
|
||||
ActivityKind.Client,
|
||||
rootActivity.Context,
|
||||
links: childLinks);
|
||||
|
||||
var childEvents = new List<ActivityEvent> { new ActivityEvent("e0"), new ActivityEvent("e1", attributes) };
|
||||
childActivity.AddEvent(childEvents[0]);
|
||||
childActivity.AddEvent(childEvents[1]);
|
||||
|
||||
Span<byte> parentIdSpan = stackalloc byte[8];
|
||||
rootActivity.Context.SpanId.CopyTo(parentIdSpan);
|
||||
var parentId = parentIdSpan.ToArray();
|
||||
|
||||
otlpSpan = childActivity.ToOtlpSpan();
|
||||
|
||||
Assert.NotNull(otlpSpan);
|
||||
Assert.Equal("child", otlpSpan.Name);
|
||||
Assert.Equal(OtlpTrace.Span.Types.SpanKind.Client, otlpSpan.Kind);
|
||||
Assert.Equal(traceId, otlpSpan.TraceId);
|
||||
Assert.Equal(parentId, otlpSpan.ParentSpanId);
|
||||
Assert.Empty(otlpSpan.Attributes);
|
||||
|
||||
Assert.Equal(childEvents.Count, otlpSpan.Events.Count);
|
||||
for (var i = 0; i < childEvents.Count; i++)
|
||||
{
|
||||
Assert.Equal(childEvents[i].Name, otlpSpan.Events[i].Name);
|
||||
AssertOtlpAttributes(childEvents[i].Attributes.ToList(), otlpSpan.Events[i].Attributes);
|
||||
}
|
||||
|
||||
childLinks.Reverse();
|
||||
Assert.Equal(childLinks.Count, otlpSpan.Links.Count);
|
||||
for (var i = 0; i < childLinks.Count; i++)
|
||||
{
|
||||
AssertOtlpAttributes(childLinks[i].Attributes.ToList(), otlpSpan.Links[i].Attributes);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseOpenTelemetryProtocolActivityExporterWithCustomActivityProcessor()
|
||||
{
|
||||
const string ActivitySourceName = "otlp.test";
|
||||
TestActivityProcessor testActivityProcessor = new TestActivityProcessor();
|
||||
|
||||
bool startCalled = false;
|
||||
bool endCalled = false;
|
||||
|
||||
testActivityProcessor.StartAction =
|
||||
(a) =>
|
||||
{
|
||||
startCalled = true;
|
||||
};
|
||||
|
||||
testActivityProcessor.EndAction =
|
||||
(a) =>
|
||||
{
|
||||
endCalled = true;
|
||||
};
|
||||
|
||||
var openTelemetrySdk = OpenTelemetrySdk.EnableOpenTelemetry(b => b
|
||||
.AddActivitySource(ActivitySourceName)
|
||||
.UseOtlpExporter(
|
||||
null, p => p.AddProcessor((next) => testActivityProcessor)));
|
||||
|
||||
var source = new ActivitySource(ActivitySourceName);
|
||||
var activity = source.StartActivity("Test Otlp Activity");
|
||||
activity?.Stop();
|
||||
|
||||
Assert.True(startCalled);
|
||||
Assert.True(endCalled);
|
||||
}
|
||||
|
||||
private static void AssertActivityTagsIntoOtlpAttributes(
|
||||
List<KeyValuePair<string, object>> expectedTags,
|
||||
RepeatedField<OtlpCommon.KeyValue> otlpAttributes)
|
||||
{
|
||||
Assert.Equal(expectedTags.Count, otlpAttributes.Count);
|
||||
for (var i = 0; i < expectedTags.Count; i++)
|
||||
{
|
||||
Assert.Equal(expectedTags[i].Key, otlpAttributes[i].Key);
|
||||
AssertOtlpAttributeValue(expectedTags[i].Value, otlpAttributes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertOtlpAttributes(
|
||||
List<KeyValuePair<string, object>> expectedAttributes,
|
||||
RepeatedField<OtlpCommon.KeyValue> otlpAttributes)
|
||||
{
|
||||
Assert.Equal(expectedAttributes.Count(), otlpAttributes.Count);
|
||||
for (int i = 0; i < otlpAttributes.Count; i++)
|
||||
{
|
||||
Assert.Equal(expectedAttributes[i].Key, otlpAttributes[i].Key);
|
||||
AssertOtlpAttributeValue(expectedAttributes[i].Value, otlpAttributes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertOtlpAttributeValue(object originalValue, OtlpCommon.KeyValue akv)
|
||||
{
|
||||
switch (originalValue)
|
||||
{
|
||||
case string s:
|
||||
Assert.Equal(s, akv.Value.StringValue);
|
||||
break;
|
||||
case bool b:
|
||||
Assert.Equal(b, akv.Value.BoolValue);
|
||||
break;
|
||||
case long l:
|
||||
Assert.Equal(l, akv.Value.IntValue);
|
||||
break;
|
||||
case double d:
|
||||
Assert.Equal(d, akv.Value.DoubleValue);
|
||||
break;
|
||||
default:
|
||||
Assert.Equal(originalValue.ToString(), akv.Value.StringValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// <copyright file="ActivityExtensionsTest.cs" company="OpenTelemetry Authors">
|
||||
// Copyright The OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// </copyright>
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Google.Protobuf.Collections;
|
||||
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
|
||||
using Opentelemetry.Proto.Common.V1;
|
||||
#if NET452
|
||||
using OpenTelemetry.Internal;
|
||||
#endif
|
||||
using OpenTelemetry.Trace;
|
||||
using OpenTelemetry.Trace.Configuration;
|
||||
using Xunit;
|
||||
using OtlpCommon = Opentelemetry.Proto.Common.V1;
|
||||
using OtlpTrace = Opentelemetry.Proto.Trace.V1;
|
||||
|
||||
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests
|
||||
{
|
||||
public class ActivityExtensionsTest
|
||||
{
|
||||
static ActivityExtensionsTest()
|
||||
{
|
||||
Activity.DefaultIdFormat = ActivityIdFormat.W3C;
|
||||
Activity.ForceDefaultIdFormat = true;
|
||||
|
||||
var listener = new ActivityListener
|
||||
{
|
||||
ShouldListenTo = _ => true,
|
||||
GetRequestedDataUsingParentId = (ref ActivityCreationOptions<string> options) => ActivityDataRequest.AllData,
|
||||
GetRequestedDataUsingContext = (ref ActivityCreationOptions<ActivityContext> options) => ActivityDataRequest.AllData,
|
||||
};
|
||||
|
||||
ActivitySource.AddActivityListener(listener);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToOtlpResourceSpansTest()
|
||||
{
|
||||
var evenTags = new[] { new KeyValuePair<string, string>("k0", "v0") };
|
||||
var oddTags = new[] { new KeyValuePair<string, string>("k1", "v1") };
|
||||
var sources = new[]
|
||||
{
|
||||
new ActivitySource("even", "2.4.6"),
|
||||
new ActivitySource("odd", "1.3.5"),
|
||||
};
|
||||
|
||||
var resource = new Resources.Resource(
|
||||
new List<KeyValuePair<string, object>>
|
||||
{
|
||||
new KeyValuePair<string, object>(Resources.Resource.ServiceNamespaceKey, "ns1"),
|
||||
});
|
||||
|
||||
// This following is done just to set Resource to Activity.
|
||||
using var openTelemetrySdk = OpenTelemetrySdk.EnableOpenTelemetry(b => b
|
||||
.AddActivitySource(sources[0].Name)
|
||||
.AddActivitySource(sources[1].Name)
|
||||
.SetResource(resource));
|
||||
|
||||
var activities = new List<Activity>();
|
||||
Activity activity = null;
|
||||
const int numOfSpans = 10;
|
||||
bool isEven;
|
||||
for (var i = 0; i < numOfSpans; i++)
|
||||
{
|
||||
isEven = i % 2 == 0;
|
||||
var source = sources[i % 2];
|
||||
var activityKind = isEven ? ActivityKind.Client : ActivityKind.Server;
|
||||
var activityTags = isEven ? evenTags : oddTags;
|
||||
|
||||
activity = source.StartActivity($"span-{i}", activityKind, activity?.Context ?? default, activityTags);
|
||||
activities.Add(activity);
|
||||
}
|
||||
|
||||
activities.Reverse();
|
||||
|
||||
var otlpResourceSpans = activities.ToOtlpResourceSpans();
|
||||
|
||||
Assert.Single(otlpResourceSpans);
|
||||
var oltpResource = otlpResourceSpans.First().Resource;
|
||||
Assert.Equal(resource.Attributes.First().Key, oltpResource.Attributes.First().Key);
|
||||
Assert.Equal(resource.Attributes.First().Value, oltpResource.Attributes.First().Value.StringValue);
|
||||
|
||||
foreach (var instrumentationLibrarySpans in otlpResourceSpans.First().InstrumentationLibrarySpans)
|
||||
{
|
||||
Assert.Equal(numOfSpans / 2, instrumentationLibrarySpans.Spans.Count);
|
||||
Assert.NotNull(instrumentationLibrarySpans.InstrumentationLibrary);
|
||||
|
||||
var expectedSpanNames = new List<string>();
|
||||
var start = instrumentationLibrarySpans.InstrumentationLibrary.Name == "even" ? 0 : 1;
|
||||
for (var i = start; i < numOfSpans; i += 2)
|
||||
{
|
||||
expectedSpanNames.Add($"span-{i}");
|
||||
}
|
||||
|
||||
var otlpSpans = instrumentationLibrarySpans.Spans;
|
||||
Assert.Equal(expectedSpanNames.Count, otlpSpans.Count);
|
||||
|
||||
var kv0 = new OtlpCommon.KeyValue { Key = "k0", Value = new AnyValue { StringValue = "v0" } };
|
||||
var kv1 = new OtlpCommon.KeyValue { Key = "k1", Value = new AnyValue { StringValue = "v1" } };
|
||||
|
||||
var expectedTag = instrumentationLibrarySpans.InstrumentationLibrary.Name == "even"
|
||||
? kv0
|
||||
: kv1;
|
||||
|
||||
foreach (var otlpSpan in otlpSpans)
|
||||
{
|
||||
Assert.Contains(otlpSpan.Name, expectedSpanNames);
|
||||
Assert.Contains(expectedTag, otlpSpan.Attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToOtlpSpanTest()
|
||||
{
|
||||
var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest));
|
||||
|
||||
using var rootActivity = activitySource.StartActivity("root", ActivityKind.Producer);
|
||||
|
||||
var attributes = new List<KeyValuePair<string, object>>
|
||||
{
|
||||
new KeyValuePair<string, object>("bool", true),
|
||||
new KeyValuePair<string, object>("long", 1L),
|
||||
new KeyValuePair<string, object>("string", "text"),
|
||||
new KeyValuePair<string, object>("double", 3.14),
|
||||
|
||||
// TODO: update if arrays of standard attribute types are supported
|
||||
new KeyValuePair<string, object>("unknown_attrib_type", new byte[] { 1 }),
|
||||
};
|
||||
|
||||
foreach (var kvp in attributes)
|
||||
{
|
||||
rootActivity.AddTag(kvp.Key, kvp.Value.ToString());
|
||||
}
|
||||
|
||||
var startTime = new DateTime(2020, 02, 20, 20, 20, 20, DateTimeKind.Utc);
|
||||
|
||||
DateTimeOffset dateTimeOffset;
|
||||
#if NET452
|
||||
dateTimeOffset = DateTimeOffsetExtensions.FromUnixTimeMilliseconds(0);
|
||||
#else
|
||||
dateTimeOffset = DateTimeOffset.FromUnixTimeMilliseconds(0);
|
||||
#endif
|
||||
|
||||
var expectedUnixTimeTicks = (ulong)(startTime.Ticks - dateTimeOffset.Ticks);
|
||||
var duration = TimeSpan.FromMilliseconds(1555);
|
||||
|
||||
rootActivity.SetStartTime(startTime);
|
||||
rootActivity.SetEndTime(startTime + duration);
|
||||
|
||||
Span<byte> traceIdSpan = stackalloc byte[16];
|
||||
rootActivity.TraceId.CopyTo(traceIdSpan);
|
||||
var traceId = traceIdSpan.ToArray();
|
||||
|
||||
var otlpSpan = rootActivity.ToOtlpSpan();
|
||||
|
||||
Assert.NotNull(otlpSpan);
|
||||
Assert.Equal("root", otlpSpan.Name);
|
||||
Assert.Equal(OtlpTrace.Span.Types.SpanKind.Producer, otlpSpan.Kind);
|
||||
Assert.Equal(traceId, otlpSpan.TraceId);
|
||||
Assert.Empty(otlpSpan.ParentSpanId);
|
||||
Assert.Null(otlpSpan.Status);
|
||||
Assert.Empty(otlpSpan.Events);
|
||||
Assert.Empty(otlpSpan.Links);
|
||||
AssertActivityTagsIntoOtlpAttributes(attributes, otlpSpan.Attributes);
|
||||
|
||||
var expectedStartTimeUnixNano = 100 * expectedUnixTimeTicks;
|
||||
Assert.Equal(expectedStartTimeUnixNano, otlpSpan.StartTimeUnixNano);
|
||||
var expectedEndTimeUnixNano = expectedStartTimeUnixNano + (duration.TotalMilliseconds * 1_000_000);
|
||||
Assert.Equal(expectedEndTimeUnixNano, otlpSpan.EndTimeUnixNano);
|
||||
|
||||
var childLinks = new List<ActivityLink> { new ActivityLink(rootActivity.Context, attributes) };
|
||||
var childActivity = activitySource.StartActivity(
|
||||
"child",
|
||||
ActivityKind.Client,
|
||||
rootActivity.Context,
|
||||
links: childLinks);
|
||||
|
||||
var childEvents = new List<ActivityEvent> { new ActivityEvent("e0"), new ActivityEvent("e1", attributes) };
|
||||
childActivity.AddEvent(childEvents[0]);
|
||||
childActivity.AddEvent(childEvents[1]);
|
||||
|
||||
Span<byte> parentIdSpan = stackalloc byte[8];
|
||||
rootActivity.Context.SpanId.CopyTo(parentIdSpan);
|
||||
var parentId = parentIdSpan.ToArray();
|
||||
|
||||
otlpSpan = childActivity.ToOtlpSpan();
|
||||
|
||||
Assert.NotNull(otlpSpan);
|
||||
Assert.Equal("child", otlpSpan.Name);
|
||||
Assert.Equal(OtlpTrace.Span.Types.SpanKind.Client, otlpSpan.Kind);
|
||||
Assert.Equal(traceId, otlpSpan.TraceId);
|
||||
Assert.Equal(parentId, otlpSpan.ParentSpanId);
|
||||
Assert.Empty(otlpSpan.Attributes);
|
||||
|
||||
Assert.Equal(childEvents.Count, otlpSpan.Events.Count);
|
||||
for (var i = 0; i < childEvents.Count; i++)
|
||||
{
|
||||
Assert.Equal(childEvents[i].Name, otlpSpan.Events[i].Name);
|
||||
AssertOtlpAttributes(childEvents[i].Attributes.ToList(), otlpSpan.Events[i].Attributes);
|
||||
}
|
||||
|
||||
childLinks.Reverse();
|
||||
Assert.Equal(childLinks.Count, otlpSpan.Links.Count);
|
||||
for (var i = 0; i < childLinks.Count; i++)
|
||||
{
|
||||
AssertOtlpAttributes(childLinks[i].Attributes.ToList(), otlpSpan.Links[i].Attributes);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseOpenTelemetryProtocolActivityExporterWithCustomActivityProcessor()
|
||||
{
|
||||
const string ActivitySourceName = "otlp.test";
|
||||
TestActivityProcessor testActivityProcessor = new TestActivityProcessor();
|
||||
|
||||
bool startCalled = false;
|
||||
bool endCalled = false;
|
||||
|
||||
testActivityProcessor.StartAction =
|
||||
(a) =>
|
||||
{
|
||||
startCalled = true;
|
||||
};
|
||||
|
||||
testActivityProcessor.EndAction =
|
||||
(a) =>
|
||||
{
|
||||
endCalled = true;
|
||||
};
|
||||
|
||||
var openTelemetrySdk = OpenTelemetrySdk.EnableOpenTelemetry(b => b
|
||||
.AddActivitySource(ActivitySourceName)
|
||||
.UseOtlpExporter(
|
||||
null, p => p.AddProcessor((next) => testActivityProcessor)));
|
||||
|
||||
var source = new ActivitySource(ActivitySourceName);
|
||||
var activity = source.StartActivity("Test Otlp Activity");
|
||||
activity?.Stop();
|
||||
|
||||
Assert.True(startCalled);
|
||||
Assert.True(endCalled);
|
||||
}
|
||||
|
||||
private static void AssertActivityTagsIntoOtlpAttributes(
|
||||
List<KeyValuePair<string, object>> expectedTags,
|
||||
RepeatedField<OtlpCommon.KeyValue> otlpAttributes)
|
||||
{
|
||||
Assert.Equal(expectedTags.Count, otlpAttributes.Count);
|
||||
for (var i = 0; i < expectedTags.Count; i++)
|
||||
{
|
||||
Assert.Equal(expectedTags[i].Key, otlpAttributes[i].Key);
|
||||
AssertOtlpAttributeValue(expectedTags[i].Value, otlpAttributes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertOtlpAttributes(
|
||||
List<KeyValuePair<string, object>> expectedAttributes,
|
||||
RepeatedField<OtlpCommon.KeyValue> otlpAttributes)
|
||||
{
|
||||
Assert.Equal(expectedAttributes.Count(), otlpAttributes.Count);
|
||||
for (int i = 0; i < otlpAttributes.Count; i++)
|
||||
{
|
||||
Assert.Equal(expectedAttributes[i].Key, otlpAttributes[i].Key);
|
||||
AssertOtlpAttributeValue(expectedAttributes[i].Value, otlpAttributes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertOtlpAttributeValue(object originalValue, OtlpCommon.KeyValue akv)
|
||||
{
|
||||
switch (originalValue)
|
||||
{
|
||||
case string s:
|
||||
Assert.Equal(s, akv.Value.StringValue);
|
||||
break;
|
||||
case bool b:
|
||||
Assert.Equal(b, akv.Value.BoolValue);
|
||||
break;
|
||||
case long l:
|
||||
Assert.Equal(l, akv.Value.IntValue);
|
||||
break;
|
||||
case double d:
|
||||
Assert.Equal(d, akv.Value.DoubleValue);
|
||||
break;
|
||||
default:
|
||||
Assert.Equal(originalValue.ToString(), akv.Value.StringValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
// <copyright file="ActivityExtensionsTest.cs" company="OpenTelemetry Authors">
|
||||
// Copyright The OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// </copyright>
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using OpenTelemetry.Trace.Configuration;
|
||||
using Xunit;
|
||||
|
||||
namespace OpenTelemetry.Trace.Test
|
||||
{
|
||||
public class ActivityExtensionsTest
|
||||
{
|
||||
private const string ActivitySourceName = "test.status";
|
||||
private const string ActivityName = "Test Activity";
|
||||
|
||||
[Fact]
|
||||
public void SetStatus()
|
||||
{
|
||||
using var openTelemetrySdk = OpenTelemetrySdk.EnableOpenTelemetry(b => b
|
||||
.AddActivitySource(ActivitySourceName));
|
||||
|
||||
using var source = new ActivitySource(ActivitySourceName);
|
||||
using var activity = source.StartActivity(ActivityName);
|
||||
activity.SetStatus(Status.Ok);
|
||||
activity?.Stop();
|
||||
|
||||
Assert.True(activity.GetStatus().IsOk);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetStatusWithDescription()
|
||||
{
|
||||
using var openTelemetrySdk = OpenTelemetrySdk.EnableOpenTelemetry(b => b
|
||||
.AddActivitySource(ActivitySourceName));
|
||||
|
||||
using var source = new ActivitySource(ActivitySourceName);
|
||||
using var activity = source.StartActivity(ActivityName);
|
||||
activity.SetStatus(Status.NotFound.WithDescription("Not Found"));
|
||||
activity?.Stop();
|
||||
|
||||
var status = activity.GetStatus();
|
||||
Assert.Equal(StatusCanonicalCode.NotFound, status.CanonicalCode);
|
||||
Assert.Equal("Not Found", status.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetCancelledStatus()
|
||||
{
|
||||
using var openTelemetrySdk = OpenTelemetrySdk.EnableOpenTelemetry(b => b
|
||||
.AddActivitySource(ActivitySourceName));
|
||||
|
||||
using var source = new ActivitySource(ActivitySourceName);
|
||||
using var activity = source.StartActivity(ActivityName);
|
||||
activity.SetStatus(Status.Cancelled);
|
||||
activity?.Stop();
|
||||
|
||||
Assert.True(activity.GetStatus().CanonicalCode.Equals(Status.Cancelled.CanonicalCode));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOrSetStatusThrowsExceptionOnNullActivity()
|
||||
{
|
||||
using var source = new ActivitySource(ActivitySourceName);
|
||||
using var activity = source.StartActivity(ActivityName);
|
||||
Assert.Throws<ArgumentNullException>(() => activity.SetStatus(Status.Ok));
|
||||
Assert.Throws<ArgumentNullException>(() => activity.GetStatus());
|
||||
activity?.Stop();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetStatusWithNoStatusInActivity()
|
||||
{
|
||||
using var openTelemetrySdk = OpenTelemetrySdk.EnableOpenTelemetry(b => b
|
||||
.AddActivitySource(ActivitySourceName));
|
||||
|
||||
using var source = new ActivitySource(ActivitySourceName);
|
||||
using var activity = source.StartActivity(ActivityName);
|
||||
activity?.Stop();
|
||||
|
||||
Assert.False(activity.GetStatus().IsValid);
|
||||
}
|
||||
|
||||
[Fact(Skip = "Activity does not support UpdateTag now. Enable once .NET Activity support SetTag method.")]
|
||||
public void LastSetStatusWins()
|
||||
{
|
||||
using var openTelemetrySdk = OpenTelemetrySdk.EnableOpenTelemetry(b => b
|
||||
.AddActivitySource(ActivitySourceName));
|
||||
|
||||
using var source = new ActivitySource(ActivitySourceName);
|
||||
using var activity = source.StartActivity(ActivityName);
|
||||
activity.SetStatus(Status.Cancelled);
|
||||
activity.SetStatus(Status.Ok);
|
||||
activity?.Stop();
|
||||
|
||||
Assert.True(activity.GetStatus().IsOk);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue