-
Notifications
You must be signed in to change notification settings - Fork 777
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
EnrichmentScope & EnrichingActivityProcessor #969
Changes from all commits
73cee9b
66e1724
d52bec5
35a1a9c
b4d6532
1b16067
320c946
f33057c
651b4c7
04a86dd
e682a6a
6b88c9f
7eeaf58
7f76856
793a98c
96f9d30
60448bd
102c6b1
d319e56
24ef002
239e212
caf1dbb
6dbe93e
3463097
7c116de
505b87d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// <copyright file="Program.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.Net.Http; | ||
using System.Threading.Tasks; | ||
using OpenTelemetry; | ||
using OpenTelemetry.Trace; | ||
|
||
public class Program | ||
{ | ||
private static readonly HttpClient HttpClient = new HttpClient(); | ||
|
||
public static async Task Main() | ||
{ | ||
using var tracerProvider = Sdk.CreateTracerProviderBuilder() | ||
.AddHttpClientInstrumentation() | ||
.AddProcessor(new EnrichingActivityProcessor()) | ||
.AddConsoleExporter() | ||
.Build(); | ||
|
||
using (EnrichmentScope.Begin(a => | ||
{ | ||
a.AddTag("mycompany.user_id", 1234); | ||
a.AddTag("mycompany.customer_id", 5678); | ||
|
||
HttpRequestMessage request = (HttpRequestMessage)a.GetCustomProperty("HttpHandler.Request"); | ||
if (request != null) | ||
{ | ||
a.AddTag("http.user_agent", request.Headers.UserAgent.ToString()); | ||
} | ||
|
||
HttpResponseMessage response = (HttpResponseMessage)a.GetCustomProperty("HttpHandler.Response"); | ||
if (response != null) | ||
{ | ||
a.AddTag("http.content_type", response.Content.Headers.ContentType.ToString()); | ||
} | ||
})) | ||
{ | ||
using var request = new HttpRequestMessage | ||
{ | ||
Method = HttpMethod.Get, | ||
RequestUri = new Uri("https://www.opentelemetry.io/"), | ||
}; | ||
|
||
request.Headers.UserAgent.TryParseAdd("mycompany/mylibrary"); | ||
|
||
using var response = await HttpClient.SendAsync(request).ConfigureAwait(false); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
# EnrichingActivityProcessor | ||
|
||
Sometimes the built-in instrumentation doesn't add enough data or you want to | ||
augment the data it provides with contextual information your application or | ||
library has available. For these cases OpenTelemetry .NET provides an | ||
`EnrichingActivityProcessor` and `EnrichmentScope` API. | ||
|
||
## Basic Usage | ||
|
||
### Add EnrichingActivityProcessor | ||
|
||
To enrich your `Activity` objects first add the `EnrichingActivityProcessor` to | ||
your `TracerProvider`: | ||
|
||
```csharp | ||
using var tracerProvider = Sdk.CreateTracerProviderBuilder() | ||
.AddHttpClientInstrumentation() | ||
.AddProcessor(new EnrichingActivityProcessor()) | ||
.AddConsoleExporter() | ||
.Build(); | ||
``` | ||
|
||
**Note:** Order is important. Make sure your `EnrichingActivityProcessor` is | ||
added before your `Exporter` and any other `ActivityProcessor`s you are using. | ||
|
||
### Use `EnrichmentScope.Begin` | ||
|
||
Use `EnrichmentScope.Begin` to wrap the call you want to instrument: | ||
|
||
```csharp | ||
using (EnrichmentScope.Begin(a => | ||
{ | ||
a.AddTag("mycompany.user_id", 1234); | ||
a.AddTag("mycompany.customer_id", 5678); | ||
|
||
HttpRequestMessage request = (HttpRequestMessage)a.GetCustomProperty("HttpHandler.Request"); | ||
if (request != null) | ||
{ | ||
a.AddTag("http.user_agent", request.Headers.UserAgent.ToString()); | ||
} | ||
|
||
HttpResponseMessage response = (HttpResponseMessage)a.GetCustomProperty("HttpHandler.Response"); | ||
if (response != null) | ||
{ | ||
a.AddTag("http.content_type", response.Content.Headers.ContentType.ToString()); | ||
} | ||
})) | ||
{ | ||
using var request = new HttpRequestMessage | ||
{ | ||
Method = HttpMethod.Get, | ||
RequestUri = new Uri("https://www.opentelemetry.io/"), | ||
}; | ||
|
||
request.Headers.UserAgent.TryParseAdd("mycompany/mylibrary"); | ||
|
||
using var response = await HttpClient.SendAsync(request).ConfigureAwait(false); | ||
} | ||
``` | ||
|
||
In that example the `Activity` created for the call to `www.opentelemetry.io` | ||
will be decorated with `mycompany.user_id` & `mycompany.customer_id` tags | ||
(simulating contextual data) and `http.user_agent` & `http.content_type` tags | ||
which are taken directly from the raw HTTP objects. | ||
|
||
## Advanced Usage | ||
|
||
### EnrichmentScopeTarget | ||
|
||
`EnrichmentScope.Begin` supports a "target" argument which can be used to alter | ||
the enrichment behavior via the `EnrichmentScopeTarget` enumeration: | ||
|
||
```csharp | ||
using EnrichmentScope.Begin( | ||
target: EnrichmentScopeTarget.FirstChild, | ||
enrichmentAction: a => a.AddTag("mycompany.user_id", 1234)); | ||
|
||
using EnrichmentScope.Begin( | ||
target: EnrichmentScopeTarget.AllChildren, | ||
enrichmentAction: a => a.AddTag("mycompany.user_id", 1234)); | ||
``` | ||
|
||
* **FirstChild**: The first child `Activity` created under the scope will be | ||
enriched and then the scope will automatically be closed. | ||
* **AllChildren**: All child `Activity` objects created under the scope will be | ||
enriched until the scope is closed. | ||
|
||
The default behavior is `EnrichmentScopeTarget.FirstChild`. | ||
|
||
### Nesting Scopes | ||
|
||
Enrichment scopes may be nested. | ||
|
||
```csharp | ||
using EnrichmentScope.Begin( | ||
target: EnrichmentScopeTarget.AllChildren, | ||
enrichmentAction: a => a.AddTag("mycompany.customer_id", 5678)) | ||
{ | ||
Comment on lines
+95
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool, I like what is demonstrated here! |
||
using EnrichmentScope.Begin( | ||
target: EnrichmentScopeTarget.FirstChild, | ||
enrichmentAction: a => a.AddTag("mycompany.user_id", 1234)) | ||
{ | ||
using var response1 = await HttpClient.GetAsync("https://www.opentelemetry.io/").ConfigureAwait(false); | ||
} | ||
|
||
using EnrichmentScope.Begin( | ||
target: EnrichmentScopeTarget.FirstChild, | ||
enrichmentAction: a => a.AddTag("mycompany.user_id", 1818)) | ||
{ | ||
using var response2 = await HttpClient.GetAsync("https://www.cncf.io/").ConfigureAwait(false); | ||
} | ||
} | ||
``` | ||
|
||
In that example the request to `www.opentelemetry.io` will be enriched with the | ||
`mycompany.user_id=1234` and `mycompany.customer_id=5678` tags (in that order) | ||
and the request to `www.cncf.com` will be enriched with the | ||
`mycompany.user_id=1818` and `mycompany.customer_id=5678` tags (in that order). |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
<ItemGroup> | ||
<!--- | ||
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="$(OpenTelemetryExporterConsolePkgVer)" /> | ||
--> | ||
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Console\OpenTelemetry.Exporter.Console.csproj" /> | ||
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.Http\OpenTelemetry.Instrumentation.Http.csproj" /> | ||
</ItemGroup> | ||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -90,6 +90,7 @@ public override void OnStartActivity(Activity activity, object payload) | |
|
||
activity.SetKind(ActivityKind.Client); | ||
activity.DisplayName = HttpTagHelper.GetOperationNameForHttpMethod(request.Method); | ||
activity.SetCustomProperty("HttpHandler.Request", request); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does this relate to the enrichment stuff? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the demo I wanted to enrich by some context stuff and by adding userAgent from request & contentType from response. Surprised to notice that the raw objects were not available (they are for .NET Framework). So this is making the raw objects available to anyone downstream that might be interested. |
||
|
||
this.activitySource.Start(activity); | ||
|
||
|
@@ -135,7 +136,8 @@ public override void OnStopActivity(Activity activity, object payload) | |
|
||
if (this.stopResponseFetcher.Fetch(payload) is HttpResponseMessage response) | ||
{ | ||
// response could be null for DNS issues, timeouts, etc... | ||
activity.SetCustomProperty("HttpHandler.Response", response); | ||
|
||
activity.SetTag(SemanticConventions.AttributeHttpStatusCode, (int)response.StatusCode); | ||
|
||
activity.SetStatus( | ||
|
@@ -158,6 +160,8 @@ public override void OnException(Activity activity, object payload) | |
return; | ||
} | ||
|
||
activity.SetCustomProperty("HttpHandler.Exception", exc); | ||
|
||
if (exc is HttpRequestException) | ||
{ | ||
if (exc.InnerException is SocketException exception) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
// <copyright file="EnrichingActivityProcessor.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.Internal; | ||
|
||
namespace OpenTelemetry.Trace | ||
{ | ||
/// <summary> | ||
/// An <see cref="ActivityProcessor"/> implementation that enriches <see cref="Activity"/> objects using <see cref="EnrichmentScope"/>s. | ||
/// </summary> | ||
public class EnrichingActivityProcessor : ActivityProcessor | ||
{ | ||
/// <inheritdoc/> | ||
public override void OnEnd(Activity activity) | ||
{ | ||
EnrichmentScope scope = EnrichmentScope.Current; | ||
while (scope != null) | ||
{ | ||
try | ||
{ | ||
scope.EnrichmentAction?.Invoke(activity); | ||
} | ||
catch (Exception ex) | ||
{ | ||
OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(EnrichingActivityProcessor), ex); | ||
} | ||
|
||
var nextParent = scope.Parent; | ||
|
||
if (scope.EnrichmentTarget == EnrichmentScopeTarget.FirstChild) | ||
{ | ||
scope.Dispose(); | ||
} | ||
|
||
scope = nextParent; | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not suggesting any action for this PR, but food for thought...
This note makes me wonder if we should consider codifying processor/exporter ordering in an effort to make this less prone to messing up. I don't have a clear suggestion at the moment, but maybe a combination of "type" and "priority".
Like if we know it's an "exporter type" versus "processor type", can the builder be smart enough to make sure to place it at the end?
Furthermore, maybe not all processors are equal. Some may be "high priority" and should therefore be injected at the beginning of the pipeline. This would in turn require some logic to deal with ties.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@reyang Tagging you just in case you didn't see this. Some food for thought as we improve/refactor the build-up. The ordering problem is similar to ASP.NET Core Middleware. Not much provided there, other than guidance that order is critical.
Interesting ideas about distinguishing "exporting processors" versus "enriching processors" versus other types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reminded me of TSR and IRP.