Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
<!-- Depending on monthly deliverables, we may switch between PackageReference or ProjectReference. Keeping both here to make the switch easier. -->

<!-- FOR PUBLIC RELEASES, MUST USE PackageReference. THIS REQUIRES A STAGGERED RELEASE IF SHIPPING A NEW EXPORTER. -->
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" />
<!-- <PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" /> -->

<!-- FOR LOCAL DEV, ProjectReference IS PREFERRED. -->
<!-- <ProjectReference Include="..\..\Azure.Monitor.OpenTelemetry.Exporter\src\Azure.Monitor.OpenTelemetry.Exporter.csproj" /> -->
<ProjectReference Include="..\..\Azure.Monitor.OpenTelemetry.Exporter\src\Azure.Monitor.OpenTelemetry.Exporter.csproj" />
</ItemGroup>
Comment thread
rajkumar-rangaraj marked this conversation as resolved.

<!-- Shared sources from Azure.Core -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ private class TracerProviderVariables
public bool foundAzureMonitorTraceExporter;
public bool foundLiveMetricsProcessor;
public bool foundStandardMetricsExtractionProcessor;
public bool foundMainAgentAttributionSpanProcessor;
}

public static void EvaluateTracerProvider(IServiceProvider serviceProvider, bool expectedLiveMetricsProcessor, bool expectedProfilingSessionTraceProcessor, bool hasInstrumentations)
Expand All @@ -298,6 +299,7 @@ public static void EvaluateTracerProvider(IServiceProvider serviceProvider, bool
Assert.True(variables.foundStandardMetricsExtractionProcessor);
Assert.True(variables.foundAzureMonitorTraceExporter);
Assert.Equal(expectedProfilingSessionTraceProcessor, variables.foundProfilingSessionTraceProcessor);
Assert.True(variables.foundMainAgentAttributionSpanProcessor);

// Validate Sampler
// The default TracesPerSecond is 5.0, so we expect RateLimitedSampler by default
Expand Down Expand Up @@ -379,45 +381,65 @@ public static void EvaluateLoggerProvider(IServiceProvider serviceProvider, bool
var processor = processorProperty.GetValue(loggerProvider);
Assert.NotNull(processor);

if (liveMetricsEnabled)
{
// When LiveMetrics is enabled, processor should be a CompositeProcessor
Assert.Contains("CompositeProcessor", processor.GetType().Name);
// Walk the processor chain to find expected processors.
bool foundMainAgentAttributionLogProcessor = false;
bool foundLiveMetricsLogProcessor = false;
bool foundAzureMonitorLogExporter = false;

// Get the first processor (LiveMetricsLogProcessor)
var headField = processor.GetType().GetField("Head", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(headField);
var firstNode = headField.GetValue(processor);
Assert.NotNull(firstNode);
WalkLogProcessorChain(processor, ref foundMainAgentAttributionLogProcessor, ref foundLiveMetricsLogProcessor, ref foundAzureMonitorLogExporter);

var valueField = firstNode.GetType().GetField("Value", BindingFlags.Public | BindingFlags.Instance);
var firstProcessor = valueField!.GetValue(firstNode);
Assert.NotNull(firstProcessor);
Assert.Contains(nameof(LiveMetrics.LiveMetricsLogProcessor), firstProcessor.GetType().Name);
Assert.True(foundMainAgentAttributionLogProcessor, "MainAgentAttributionLogProcessor not found");
Assert.True(foundAzureMonitorLogExporter, "AzureMonitorLogExporter not found");
Assert.Equal(liveMetricsEnabled, foundLiveMetricsLogProcessor);
}

// Get the second processor (BatchLogRecordExportProcessor & AzureMonitorLogExporter)
var nextProperty = firstNode.GetType().GetProperty("Next", BindingFlags.Public | BindingFlags.Instance);
var secondNode = nextProperty!.GetValue(firstNode);
Assert.NotNull(secondNode);
private static void WalkLogProcessorChain(object processor, ref bool foundMainAgent, ref bool foundLiveMetrics, ref bool foundExporter)
{
var processorType = processor.GetType();

var secondProcessor = valueField.GetValue(secondNode);
Assert.NotNull(secondProcessor);
if (processorType.Name.Contains("MainAgentAttributionLogProcessor"))
{
foundMainAgent = true;
return;
}
else if (processorType.Name.Contains(nameof(LiveMetrics.LiveMetricsLogProcessor)))
{
foundLiveMetrics = true;
return;
}
else if (processorType.Name.Contains("CompositeProcessor"))
{
// Walk the linked list inside the CompositeProcessor
var headField = processorType.GetField("Head", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(headField);
var currentNode = headField.GetValue(processor);

var exporterProperty = secondProcessor.GetType().GetProperty("Exporter", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(exporterProperty);
while (currentNode != null)
{
var valueField = currentNode.GetType().GetField("Value", BindingFlags.Public | BindingFlags.Instance);
var nextProperty = currentNode.GetType().GetProperty("Next", BindingFlags.Public | BindingFlags.Instance);

var exporter = exporterProperty.GetValue(secondProcessor);
Assert.NotNull(exporter);
Assert.Contains(nameof(AzureMonitorLogExporter), exporter.GetType().Name);
var childProcessor = valueField!.GetValue(currentNode);
if (childProcessor != null)
{
WalkLogProcessorChain(childProcessor, ref foundMainAgent, ref foundLiveMetrics, ref foundExporter);
}

currentNode = nextProperty!.GetValue(currentNode);
}

return;
}
else
{
var exporterProperty = processor.GetType().GetProperty("Exporter", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(exporterProperty);

// Check if this processor wraps an exporter
var exporterProperty = processorType.GetProperty("Exporter", BindingFlags.NonPublic | BindingFlags.Instance);
if (exporterProperty != null)
{
var exporter = exporterProperty.GetValue(processor);
Assert.NotNull(exporter);
Assert.Contains(nameof(AzureMonitorLogExporter), exporter.GetType().Name);
if (exporter != null && exporter.GetType().Name.Contains(nameof(AzureMonitorLogExporter)))
{
foundExporter = true;
}
}
}

Expand Down Expand Up @@ -446,6 +468,10 @@ private static void WalkTracerCompositeProcessor(object compositeProcessor, Trac
{
variables.foundProfilingSessionTraceProcessor = true;
}
else if (processorType.Name.Contains("MainAgentAttributionSpanProcessor"))
{
variables.foundMainAgentAttributionSpanProcessor = true;
}
else if (processorType.Name.Contains(nameof(LiveMetrics.LiveMetricsActivityProcessor)))
{
variables.foundLiveMetricsProcessor = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

### Features Added

- Added GenAI main agent attribution support. Automatically propagates `microsoft.gen_ai.main_agent.*` attributes from parent spans to child spans and log records, enabling end-to-end tracing of AI agent orchestration.
([#59368](https://github.com/Azure/azure-sdk-for-net/pull/59368))

### Breaking Changes

### Bugs Fixed
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# GenAI Main Agent Attribution

## Owner

<!-- TODO: Add owner -->

## Approvers

* Rajkumar Rangaraj
* Jackson Weber
* Radhika Gupta

## Status

Experimental

## Purpose

Specifies how Azure Monitor Distros (Python, .NET, Java, Node.js) detect the top-level ("main") agent in a GenAI multi-agent system and propagate main-agent context so that all emitted telemetry (traces, metrics, logs) is attributed to the user-facing agent rather than to internal implementation agents.

## Specification

### Span Attribution

The distro MUST register a SpanProcessor in the TracerProvider pipeline before any export processor (e.g., BatchSpanProcessor).

In [OnStart(span, parentContext)][1], the processor MUST:

1. Extract the Span from `parentContext` to obtain the parent span. If there is no parent span, return.

2. For each row in the table below, read the **primary source** attribute from the parent span. If it exists, copy it onto `span` as the target attribute. Otherwise, read the **fallback source** attribute from the parent span and, if it exists, copy it onto `span` as the target attribute.

| Target attribute (on `span`) | Primary source (on parent span) | Fallback source (on parent span) |
| :--- | :--- | :--- |
| `microsoft.gen_ai.main_agent.name` | `microsoft.gen_ai.main_agent.name` | `gen_ai.agent.name` |
| `microsoft.gen_ai.main_agent.id` | `microsoft.gen_ai.main_agent.id` | `gen_ai.agent.id` |
| `microsoft.gen_ai.main_agent.version` | `microsoft.gen_ai.main_agent.version` | `gen_ai.agent.version` |
| `microsoft.gen_ai.main_agent.conversation_id` | `microsoft.gen_ai.main_agent.conversation_id` | `gen_ai.conversation.id` |

In [OnEnd(span)][7], the processor MUST:

1. If `span` does not have `gen_ai.operation.name` = `invoke_agent`, return.

2. If `span` already has any `microsoft.gen_ai.main_agent.*` attribute, return.

3. For each row in the table below, read the source attribute from `span` and, if it exists, copy it onto `span` as the target attribute.

| Target attribute | Source attribute |
| :--- | :--- |
| `microsoft.gen_ai.main_agent.name` | `gen_ai.agent.name` |
| `microsoft.gen_ai.main_agent.id` | `gen_ai.agent.id` |
| `microsoft.gen_ai.main_agent.version` | `gen_ai.agent.version` |
| `microsoft.gen_ai.main_agent.conversation_id` | `gen_ai.conversation.id` |

### Log Attribution

The distro MUST register a LogRecordProcessor in the LoggerProvider pipeline before any export processor (e.g., BatchLogRecordProcessor).

In [OnEmit(logRecord, context)][2], the processor MUST:

1. Extract the Span from `context` to obtain the current span. If there is no current span, return.

2. If the current span has any `microsoft.gen_ai.main_agent.*` attributes, copy all of them onto the `logRecord`.

### Metric Attribution

The OTel Metrics SDK does not define a per-measurement processor hook ([issue][3], [PR][4]).

**Java**: The Java SDK includes an internal [`View.AttributesProcessor`][5] that accepts `(Attributes, Context)` and can modify measurement attributes. The distro could use this to extract the active span from `Context` and copy `microsoft.gen_ai.main_agent.*` attributes onto measurements.

**Python, .NET, Node.js**: I don't believe an equivalent internal hook exists.

[1]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#onstart "SpanProcessor OnStart"
[2]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/sdk.md#onemit "LogRecordProcessor OnEmit"
[3]: https://github.com/open-telemetry/opentelemetry-specification/issues/4298 "Support measurement processors in Metrics SDK"
[4]: https://github.com/open-telemetry/opentelemetry-specification/pull/4318 "Add MeasurementProcessor to Metrics SDK (closed)"
[5]: https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/AttributesProcessor.java "Java AttributesProcessor (internal API)"
[7]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#onend "SpanProcessor OnEnd"
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Azure.Core;
using Azure.Monitor.OpenTelemetry.Exporter.Internals;
using Azure.Monitor.OpenTelemetry.Exporter.Internals.Diagnostics;
using Azure.Monitor.OpenTelemetry.Exporter.Internals.GenAI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -94,6 +95,7 @@ public static TracerProviderBuilder AddAzureMonitorTraceExporter(

sp.EnsureNoUseAzureMonitorExporterRegistrations();

builder.AddProcessor(new MainAgentAttributionSpanProcessor());
builder.AddProcessor(new CompositeProcessor<Activity>(new BaseProcessor<Activity>[]
{
new StandardMetricsExtractionProcessor(new AzureMonitorMetricExporter(exporterOptions), exporterOptions),
Expand Down Expand Up @@ -208,6 +210,7 @@ public static OpenTelemetryLoggerOptions AddAzureMonitorLogExporter(
? new LogFilteringProcessor(exporter)
: new BatchLogRecordExportProcessor(exporter);

loggerOptions.AddProcessor(new MainAgentAttributionLogProcessor());
return loggerOptions.AddProcessor(processor);
}

Expand Down Expand Up @@ -277,9 +280,15 @@ public static LoggerProviderBuilder AddAzureMonitorLogExporter(

// TODO: Do we need provide an option to alter BatchExportLogRecordProcessorOptions?
var exporter = new AzureMonitorLogExporter(exporterOptions);
return exporterOptions.EnableTraceBasedLogsSampler
BaseProcessor<LogRecord> exportProcessor = exporterOptions.EnableTraceBasedLogsSampler
? new LogFilteringProcessor(exporter)
: new BatchLogRecordExportProcessor(exporter);

return new CompositeProcessor<LogRecord>(new BaseProcessor<LogRecord>[]
{
new MainAgentAttributionLogProcessor(),
exportProcessor
});
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading;
using System.Threading.Tasks;
using Azure.Monitor.OpenTelemetry.Exporter.Internals;
using Azure.Monitor.OpenTelemetry.Exporter.Internals.GenAI;
using Azure.Monitor.OpenTelemetry.LiveMetrics;
using Azure.Monitor.OpenTelemetry.LiveMetrics.Internals;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -66,6 +67,7 @@ private static void Initialize(IServiceProvider serviceProvider)
}

// TODO: Add Ai Sampler.
tracerProvider.AddProcessor(new MainAgentAttributionSpanProcessor());
tracerProvider.AddProcessor(new CompositeProcessor<Activity>(new BaseProcessor<Activity>[]
{
new StandardMetricsExtractionProcessor(new AzureMonitorMetricExporter(exporterOptions), exporterOptions),
Expand All @@ -87,6 +89,8 @@ private static void Initialize(IServiceProvider serviceProvider)
? new LogFilteringProcessor(exporter)
: new BatchLogRecordExportProcessor(exporter);

loggerProvider.AddProcessor(new MainAgentAttributionLogProcessor());

if (exporterOptions.EnableLiveMetrics)
{
var manager = serviceProvider!.GetRequiredService<LiveMetricsClientManager>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Azure.Monitor.OpenTelemetry.Exporter.Internals.GenAI
{
internal static class MainAgentAttributeConstants
{
// Target attributes (microsoft.gen_ai.main_agent.*)
internal const string MainAgentName = "microsoft.gen_ai.main_agent.name";
internal const string MainAgentId = "microsoft.gen_ai.main_agent.id";
internal const string MainAgentVersion = "microsoft.gen_ai.main_agent.version";
internal const string MainAgentConversationId = "microsoft.gen_ai.main_agent.conversation_id";

// Source / fallback attributes (gen_ai.agent.* / gen_ai.conversation.*)
internal const string GenAiAgentName = "gen_ai.agent.name";
internal const string GenAiAgentId = "gen_ai.agent.id";
internal const string GenAiAgentVersion = "gen_ai.agent.version";
internal const string GenAiConversationId = "gen_ai.conversation.id";

// Operation name
internal const string GenAiOperationName = "gen_ai.operation.name";
internal const string InvokeAgentOperationName = "invoke_agent";
}
}
Loading
Loading