Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Diagnostics;
using OpenTelemetry;
using OpenTelemetry.Logs;

using static Azure.Monitor.OpenTelemetry.Exporter.Internals.GenAI.MainAgentAttributeConstants;

namespace Azure.Monitor.OpenTelemetry.Exporter.Internals.GenAI
{
/// <summary>
/// A log processor that copies main-agent attributes from the current span
/// onto emitted log records so that logs are attributed to the user-facing agent.
/// </summary>
internal sealed class MainAgentAttributionLogProcessor : BaseProcessor<LogRecord>
{
private static readonly string[] s_mainAgentAttributes = new[]
{
MainAgentName,
MainAgentId,
MainAgentVersion,
MainAgentConversationId,
};

public override void OnEnd(LogRecord logRecord)
{
var activity = Activity.Current;
if (activity == null)
{
return;
}

// Quick check: skip if current span has no main agent attributes.
if (activity.GetTagItem(MainAgentName) == null &&
activity.GetTagItem(MainAgentId) == null)
{
return;
}

// Collect values into a stack-friendly fixed-size buffer to avoid
// unnecessary List allocations on every log record.
Comment thread
rajkumar-rangaraj marked this conversation as resolved.
Outdated
var values = new KeyValuePair<string, object?>[s_mainAgentAttributes.Length];
int count = 0;

foreach (var attributeKey in s_mainAgentAttributes)
{
var value = activity.GetTagItem(attributeKey);
if (value != null)
{
values[count++] = new KeyValuePair<string, object?>(attributeKey, value);
}
}

if (count == 0)
{
return;
}

var existingAttributes = logRecord.Attributes;
var merged = new List<KeyValuePair<string, object?>>(
(existingAttributes?.Count ?? 0) + count);

if (existingAttributes != null)
{
merged.AddRange(existingAttributes);
}

for (int i = 0; i < count; i++)
{
merged.Add(values[i]);
}

logRecord.Attributes = merged;
}
}
}
Loading
Loading