Skip to content
111 changes: 84 additions & 27 deletions samples/dotnet/otel/AgentOtelExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Agents.Core.Telemetry;
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Http;

namespace Otel
{
Expand All @@ -20,14 +22,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 thread
MattB-msft marked this conversation as resolved.
.AddAttributes(new[]
{
Expand All @@ -38,8 +39,7 @@ public static TBuilder ConfigureOtelProviders<TBuilder>(this TBuilder builder) w
.AddSource(
"Microsoft.AspNetCore",
"System.Net.Http",
AgentsTelemetry.SourceName,
AgentTelemetry.ServiceName
AgentsTelemetry.SourceName
)
Comment thread
MattB-msft marked this conversation as resolved.
.SetSampler(new AlwaysOnSampler())
.AddAspNetCoreInstrumentation(tracing =>
Expand All @@ -50,10 +50,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 +68,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");
Comment thread
MattB-msft marked this conversation as resolved.
};
})
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddMeter(AgentsTelemetry.SourceName, AgentTelemetry.ServiceName)
.AddMeter(AgentsTelemetry.SourceName)
Comment thread
MattB-msft marked this conversation as resolved.
.AddOtlpExporter());

builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
Expand All @@ -110,8 +97,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)
Comment thread
MattB-msft marked this conversation as resolved.
{

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