Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 83 additions & 27 deletions samples/dotnet/otel/AgentOtelExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Agents.Core.Telemetry;
using System.Net.Http.Headers;

namespace Otel
{
Expand All @@ -20,14 +21,13 @@ namespace Otel
// To learn more about using the local aspire desktop, see https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/standalone?tabs=bash
public static class AgentOtelExtension
{

public static TBuilder ConfigureOtelProviders<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{

builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService(
serviceName: AgentTelemetry.ServiceName,
serviceVersion: AgentTelemetry.ServiceVersion
serviceName: AgentsTelemetry.SourceName,
serviceVersion: AgentsTelemetry.SourceVersion
)
Comment on lines 27 to 31
.AddAttributes(new[]
{
Expand All @@ -38,8 +38,7 @@ public static TBuilder ConfigureOtelProviders<TBuilder>(this TBuilder builder) w
.AddSource(
"Microsoft.AspNetCore",
"System.Net.Http",
AgentsTelemetry.SourceName,
AgentTelemetry.ServiceName
AgentsTelemetry.SourceName
)
Comment on lines 38 to 42
.SetSampler(new AlwaysOnSampler())
.AddAspNetCoreInstrumentation(tracing =>
Expand All @@ -50,10 +49,13 @@ public static TBuilder ConfigureOtelProviders<TBuilder>(this TBuilder builder) w
{
activity.SetTag("http.request.body.size", request.ContentLength);
activity.SetTag("user_agent", request.Headers.UserAgent);
ExtractHeadersForOTEL(activity, request.Headers, "http.request.headers");

};
tracing.EnrichWithHttpResponse = (activity, response) =>
{
activity.SetTag("http.response.body.size", response.ContentLength);
ExtractHeadersForOTEL(activity, response.Headers, "http.response.headers");
};
})
.AddHttpClientInstrumentation(o =>
Expand All @@ -65,38 +67,22 @@ public static TBuilder ConfigureOtelProviders<TBuilder>(this TBuilder builder) w
activity.SetTag("http.request.method", request.Method);
activity.SetTag("http.request.host", request.RequestUri?.Host);
activity.SetTag("http.request.useragent", request.Headers?.UserAgent);
ExtractHeadersForOTEL(activity, request.Headers, "http.request.headers");
};
o.EnrichWithHttpResponseMessage = (activity, response) =>
{
activity.SetTag("http.response.status_code", (int)response.StatusCode);
// Convert response.Content.Headers to a string array: "HeaderName=val1,val2"
var headerList = response.Content?.Headers?
.Where(h => h.Key != "Authorization")
.Select(h => $"{h.Key}={string.Join(",", h.Value)}")
.ToArray();

if (headerList is { Length: > 0 })
{
// Set as an array tag (preferred for OTEL exporters supporting array-of-primitive attributes)
activity.SetTag("http.response.headers", headerList);

// (Optional) Also emit individual header tags (comment out if too high-cardinality)
// foreach (var h in response.Content.Headers)
// {
// activity.SetTag($"http.response.header.{h.Key.ToLowerInvariant()}", string.Join(",", h.Value));
// }
}

ExtractHeadersForOTEL(activity, response.Headers, "http.response.headers");
};
})
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddMeter(AgentsTelemetry.SourceName, AgentTelemetry.ServiceName)
.AddMeter(AgentsTelemetry.SourceName)
.AddOtlpExporter());

builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
Expand All @@ -110,8 +96,78 @@ public static TBuilder ConfigureOtelProviders<TBuilder>(this TBuilder builder) w
// builder.Services.AddOpenTelemetry()
// .UseAzureMonitor();
//}

return builder;
}

/// <summary>
/// Extracts HTTP headers from the request or response and adds them as tags to the OpenTelemetry activity.
/// </summary>
/// <param name="activity">The OpenTelemetry activity to which the header tags will be added.</param>
/// <param name="request">The HTTP content headers to extract. If null, no tags will be added.</param>
/// <param name="tagName">The name of the tag to use when adding headers to the activity (e.g., "http.request.headers" or "http.response.headers").</param>
/// <remarks>
/// This method filters out the "Authorization" header for security reasons and formats the remaining headers
/// as "HeaderName=value1,value2" strings. The headers are then added to the activity as an array tag,
/// which is compatible with OpenTelemetry exporters that support array-of-primitive attributes.
/// </remarks>
private static void ExtractHeadersForOTEL(System.Diagnostics.Activity activity, System.Net.Http.Headers.HttpHeaders? request, string tagName)
{

if (request == null)
{
return;
}

var headerList = request.Where(h => !string.Equals(h.Key, "Authorization", StringComparison.OrdinalIgnoreCase))
.Select(h => $"{h.Key}={string.Join(",", h.Value)}")
.ToArray();

if (headerList is { Length: > 0 })
{
// Set as an array tag (preferred for OTEL exporters supporting array-of-primitive attributes)
activity.SetTag(tagName, headerList);

// (Optional) Also emit individual header tags (comment out if too high-cardinality)
// foreach (var h in response.Content.Headers)
// {
// activity.SetTag($"http.response.header.{h.Key.ToLowerInvariant()}", string.Join(",", h.Value));
// }
}
}

/// <summary>
/// Extracts HTTP headers from the request or response and adds them as tags to the OpenTelemetry activity.
/// </summary>
/// <param name="activity">The OpenTelemetry activity to which the header tags will be added.</param>
/// <param name="request">The HTTP content headers to extract. If null, no tags will be added.</param>
/// <param name="tagName">The name of the tag to use when adding headers to the activity (e.g., "http.request.headers" or "http.response.headers").</param>
/// <remarks>
/// This method filters out the "Authorization" header for security reasons and formats the remaining headers
/// as "HeaderName=value1,value2" strings. The headers are then added to the activity as an array tag,
/// which is compatible with OpenTelemetry exporters that support array-of-primitive attributes.
/// </remarks>
private static void ExtractHeadersForOTEL(System.Diagnostics.Activity activity, IHeaderDictionary? request, string tagName)
{

if (request == null)
{
return;
}
var headerList = request.Where(h => !string.Equals(h.Key, "Authorization", StringComparison.OrdinalIgnoreCase))
.Select(h => $"{h.Key}={string.Join(",", h.Value)}")
.ToArray();

if (headerList is { Length: > 0 })
{
// Set as an array tag (preferred for OTEL exporters supporting array-of-primitive attributes)
activity.SetTag(tagName, headerList);

// (Optional) Also emit individual header tags (comment out if too high-cardinality)
// foreach (var h in response.Content.Headers)
// {
// activity.SetTag($"http.response.header.{h.Key.ToLowerInvariant()}", string.Join(",", h.Value));
// }
}
}
}
}
Loading