Skip to content

Commit

Permalink
OTel: Fix duplicated attributes and suppression (#40027)
Browse files Browse the repository at this point in the history
* Fix duplicated attributes and suppression
  • Loading branch information
lmolkova authored Nov 15, 2023
1 parent dd563c7 commit 027b7a7
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 34 deletions.
24 changes: 12 additions & 12 deletions sdk/core/Azure.Core/samples/Diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,18 +164,18 @@ If your are using Azure SDK libraries in ASP.NET Core application consider using

## Distributed tracing

Azure SDKs are instrumented for distributed tracing using OpenTelemetry or ApplicationInsights SDK.
Azure client libraries are instrumented for distributed tracing using OpenTelemetry or ApplicationInsights SDK.

Distributed tracing relies on `ActivitySource` and `Activity` primitives defined in .NET. Check out [Adding distributed tracing instrumentation](https://learn.microsoft.com/dotnet/core/diagnostics/distributed-tracing-instrumentation-walkthroughs) guide for more details.

Azure SDKs produce the following kinds of activities:
Azure client libraries produce the following kinds of activities:

- *HTTP calls*: every HTTP call originating from Azure SDKs
- *HTTP calls*: every HTTP call originating from an Azure SDK
- *client method calls*: for example, `BlobClient.DownloadTo` or `SecretClient.StartDeleteSecret`.
- *messaging events*: Event Hubs and Service Bus message creation is traced and correlated with its sending, receiving, and processing.

Prior to November 2023, OpenTelemetry support was experimental for all Azure SDKs (see [Enabling experimental tracing features](#enabling-experimental-tracing-features) for the details).
Most of Azure SDKs released in or after November 2023 have OpenTelemetry support enabled by default. Tracing support in messaging libraries (`Azure.Messaging.ServiceBus` and `Azure.Messaging.EventHubs`) remains experimental.
Prior to November 2023, OpenTelemetry support was experimental for all Azure client libraries (see [Enabling experimental tracing features](#enabling-experimental-tracing-features) for the details).
Most of Azure client libraries released in or after November 2023 have OpenTelemetry support enabled by default. Tracing support in messaging libraries (`Azure.Messaging.ServiceBus` and `Azure.Messaging.EventHubs`) remains experimental.

More detailed distributed tracing convention can be found at [Azure SDK semantic conventions](https://github.com/Azure/azure-sdk/blob/main/docs/tracing/distributed-tracing-conventions.md).

Expand All @@ -195,7 +195,7 @@ builder.Services.AddOpenTelemetry()
.AddOtlpExporter())
```

_Note: check out [Enable experimental tracing features](#enabling-experimental-tracing-features) in case you're using one of the messaging SDKs._
_Note: check out [Enable experimental tracing features](#enabling-experimental-tracing-features) in case you're using one of the messaging libraries._

By calling into `AddSource("Azure.*")` we're telling OpenTelemetry SDK to listen to all sources which name starts with `Azure.`.

Expand All @@ -210,7 +210,7 @@ _Note: if you use Azure Monitor Distro for ASP.NET Core, you may skip this secti

Azure SDK traces all HTTP calls using `Azure.Core.Http` source. If you enable it along with generic HTTP client instrumentation such as `OpenTelemetry.Instrumentation.Http`, it would lead to double-collection of HTTP calls originating from Azure client libraries.

Unlike generic HTTP activities, Azure SDK HTTP activities include Azure-specific attributes such as request identifiers usually passed to and from Azure serices in `x-ms-client-request-id`, `x-ms-request-id` or similar request and respose headers. This data may be important when correlating client and server telemetry or creating support tickets.
Unlike generic HTTP activities, Azure SDK HTTP activities include Azure-specific attributes such as request identifiers usually passed to and from Azure services in `x-ms-client-request-id`, `x-ms-request-id` or similar request and response headers. This data may be important when correlating client and server telemetry or creating support tickets.

To avoid double-collection you may either
- enrich generic HTTP client activities with Azure request identifiers and disable Azure SDK HTTP activities.
Expand Down Expand Up @@ -250,7 +250,7 @@ _Note: request identifiers are also stamped on HTTP logs emitted by `Azure.Core`

#### Filtering out duplicated HTTP client activities

Another approach would be to filter out generic HTTP client activities with the following approach:
Another approach would be to filter out generic HTTP client activities:

```csharp
.WithTracing(tracerProviderBuilder => tracerProviderBuilder
Expand All @@ -267,7 +267,7 @@ _Note: filtering does not prevent new activity from being created and new [`trac

## HTTP metrics

Azure SDKs do not collect HTTP-level metrics. Please use metrics coming from .NET platform and/or generic HTTP client instrumentation.
Azure client libraries do not collect HTTP-level metrics. Please use metrics coming from .NET platform and/or generic HTTP client instrumentation.

## Azure Monitor

Expand All @@ -279,7 +279,7 @@ If your application uses ApplicationInsights SDK (classic), automatic collection

### Sample

To see an example of distributed tracing in action, take a look at our [sample app](https://github.com/Azure/azure-sdk-for-net/blob/main/samples/linecounter/README.md) that combines several Azure SDKs.
To see an example of distributed tracing in action, take a look at our [sample app](https://github.com/Azure/azure-sdk-for-net/blob/main/samples/linecounter/README.md) that combines several Azure client libraries.

### Enabling experimental tracing features

Expand Down Expand Up @@ -335,9 +335,9 @@ var secretClient = new SecretClient(new Uri("https://example.com"), new DefaultA
secretClient.GetSecret("<secret-name>");
```

## Setting x-ms-client-request-id value sent with requests
## Setting `x-ms-client-request-id` value sent with requests

By default x-ms-client-request-id header gets a unique value per client method call. If you would like to use a specific value for a set of requests use the `HttpPipeline.CreateClientRequestIdScope` method.
By default `x-ms-client-request-id` header gets a unique value per client method call. If you would like to use a specific value for a set of requests use the `HttpPipeline.CreateClientRequestIdScope` method.

```C# Snippet:ClientRequestId
var secretClient = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential());
Expand Down
13 changes: 3 additions & 10 deletions sdk/core/Azure.Core/src/Pipeline/Internal/RequestActivityPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,20 +122,13 @@ private async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory<HttpPip

if (message.Response.IsError)
{
if (isActivitySourceEnabled)
{
scope.AddAttribute("error.type", statusCodeStr);
}
else
{
scope.AddAttribute("otel.status_code", "ERROR");
}
scope.Failed(statusCodeStr);
}
else if (!isActivitySourceEnabled)

if (!isActivitySourceEnabled)
{
// Set the status to UNSET so the AppInsights doesn't try to infer it from the status code
scope.AddAttribute("otel.status_code", "UNSET");
scope.AddAttribute("otel.status_code", message.Response.IsError ? "ERROR" : "UNSET");
}
}

Expand Down
9 changes: 6 additions & 3 deletions sdk/core/Azure.Core/src/Shared/DiagnosticScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ public void AddLink(string traceparent, string? tracestate, IDictionary<string,
public void Start()
{
Activity? started = _activityAdapter?.Start();
started?.SetCustomProperty(AzureSdkScopeLabel, AzureSdkScopeValue);
if (_suppressNestedClientActivities)
{
started?.SetCustomProperty(AzureSdkScopeLabel, AzureSdkScopeValue);
}
}

public void SetDisplayName(string displayName)
Expand Down Expand Up @@ -261,7 +264,7 @@ public void AddTag(string name, object value)
_tagCollection?.Add(new KeyValuePair<string, object>(name, value!));
#else
_tagCollection ??= new ActivityTagsCollection();
_tagCollection.Add(name, value!);
_tagCollection[name] = value!;
#endif
}
else
Expand Down Expand Up @@ -550,7 +553,7 @@ private void AddObjectTag(string name, object value)
#else
if (_activitySource?.HasListeners() == true)
{
_currentActivity?.AddTag(name, value);
_currentActivity?.SetTag(name, value);
}
else
{
Expand Down
46 changes: 37 additions & 9 deletions sdk/core/Azure.Core/tests/ClientDiagnosticsTests.Net50.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,34 @@ public void StableStartsActivityShortNameSourceActivity()
CollectionAssert.Contains(activity.Tags, new KeyValuePair<string, string>(DiagnosticScope.OpenTelemetrySchemaAttribute, DiagnosticScope.OpenTelemetrySchemaVersion));
}

[Test]
[NonParallelizable]
public void DuplicateTagsAreUpdatedActivitySource()
{
using var activityListener = new TestActivitySourceListener("Azure.Clients.ActivityName");
DiagnosticScopeFactory clientDiagnostics = new DiagnosticScopeFactory("Azure.Clients", "Microsoft.Azure.Core.Cool.Tests", true, false, true);

DiagnosticScope scope = clientDiagnostics.CreateScope("ActivityName");
scope.AddAttribute("key1", "value1");
scope.AddAttribute("key1", "value2");

scope.Start();
scope.AddAttribute("key2", "value1");
scope.AddAttribute("key2", "value2");

scope.Dispose();

Assert.AreEqual(1, activityListener.Activities.Count);
var activity = activityListener.Activities.Dequeue();

var key1 = activity.TagObjects.Where(kvp => kvp.Key == "key1").ToList();
var key2 = activity.TagObjects.Where(kvp => kvp.Key == "key2").ToList();
Assert.AreEqual(1, key1.Count);
Assert.AreEqual(1, key2.Count);
Assert.AreEqual("value2", key1.Single().Value);
Assert.AreEqual("value2", key2.Single().Value);
}

[TestCase(null)]
[TestCase(false)]
[TestCase(true)]
Expand Down Expand Up @@ -176,7 +204,7 @@ public void NestedClientActivitiesSuppressed(int kind, bool expectSuppression)
using DiagnosticScope scope = clientDiagnostics.CreateScope("ClientName.ActivityName", (ActivityKind)kind);
scope.Start();

DiagnosticScope nestedScope = clientDiagnostics.CreateScope("ClientName.NestedActivityName", (ActivityKind)kind);
DiagnosticScope nestedScope = clientDiagnostics.CreateScope("ClientName.NestedActivityName", ActivityKind.Client);
nestedScope.Start();
if (expectSuppression)
{
Expand All @@ -199,7 +227,7 @@ public void NestedClientActivitiesSuppressed(int kind, bool expectSuppression)
[TestCase(ActivityKind.Producer, false)]
[TestCase(ActivityKind.Consumer, false)]
[NonParallelizable]
public void NestedClientActivitiesSuppressionActivitySource(int kind, bool expectSuppression)
public void NestedInternalActivitiesSuppressionActivitySource(int kind, bool expectSuppression)
{
using var activityListener = new TestActivitySourceListener("Azure.Clients.ClientName");

Expand All @@ -208,7 +236,7 @@ public void NestedClientActivitiesSuppressionActivitySource(int kind, bool expec
using DiagnosticScope scope = clientDiagnostics.CreateScope("ClientName.ActivityName", (ActivityKind)kind);
scope.Start();

DiagnosticScope nestedScope = clientDiagnostics.CreateScope("ClientName.NestedActivityName", (ActivityKind)kind);
DiagnosticScope nestedScope = clientDiagnostics.CreateScope("ClientName.NestedActivityName", ActivityKind.Internal);
nestedScope.Start();
if (expectSuppression)
{
Expand All @@ -224,12 +252,12 @@ public void NestedClientActivitiesSuppressionActivitySource(int kind, bool expec
Assert.AreEqual("ClientName.ActivityName", Activity.Current.OperationName);
}

[TestCase(true, true, true)]
[TestCase(true, false, false)]
[TestCase(false, true, true)]
[TestCase(false, false, false)]
[TestCase(true, true)]
[TestCase(true, false)]
[TestCase(false, true)]
[TestCase(false, false)]
[NonParallelizable]
public void NestedActivitiesSuppressionConfiguration(bool suppressOuter, bool suppressNested, bool expectSuppression)
public void NestedActivitiesSuppressionConfiguration(bool suppressOuter, bool suppressNested)
{
using var activityListener = new TestActivitySourceListener("Azure.Clients.ClientName");
DiagnosticScopeFactory clientDiagnostics = new DiagnosticScopeFactory("Azure.Clients", "Microsoft.Azure.Core.Cool.Tests", true, suppressOuter, true);
Expand All @@ -241,7 +269,7 @@ public void NestedActivitiesSuppressionConfiguration(bool suppressOuter, bool su
DiagnosticScope nestedScope = clientDiagnostics2.CreateScope("ClientName.NestedActivityName");
nestedScope.Start();

if (expectSuppression)
if (suppressOuter && suppressNested)
{
Assert.IsFalse(nestedScope.IsEnabled);
Assert.AreEqual("ClientName.ActivityName", Activity.Current.OperationName);
Expand Down

0 comments on commit 027b7a7

Please sign in to comment.