diff --git a/dotnet/samples/GettingStarted/AgentOpenTelemetry/Program.cs b/dotnet/samples/GettingStarted/AgentOpenTelemetry/Program.cs
index d5c64ae416..0d9ac511ee 100644
--- a/dotnet/samples/GettingStarted/AgentOpenTelemetry/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentOpenTelemetry/Program.cs
@@ -103,7 +103,7 @@ using var instrumentedChatClient = new AzureOpenAIClient(new Uri(endpoint), new
.AsIChatClient() // Converts a native OpenAI SDK ChatClient into a Microsoft.Extensions.AI.IChatClient
.AsBuilder()
.UseFunctionInvocation()
- .UseOpenTelemetry(loggerFactory: loggerFactory, sourceName: SourceName, (cfg) => cfg.EnableSensitiveData = true)
+ .UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true)
.Build();
appLogger.LogInformation("Creating Agent with OpenTelemetry instrumentation");
@@ -112,7 +112,7 @@ using var agent = new ChatClientAgent(instrumentedChatClient,
name: "OpenTelemetryDemoAgent",
instructions: "You are a helpful assistant that provides concise and informative responses.",
tools: [AIFunctionFactory.Create(GetWeatherAsync)])
- .WithOpenTelemetry(loggerFactory, SourceName); // Enable telemetry on the agent
+ .WithOpenTelemetry(SourceName); // Enable telemetry on the agent
var thread = agent.GetNewThread();
diff --git a/dotnet/src/Microsoft.Agents.AI/AgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI/AgentExtensions.cs
index a47ab1ce4a..d1850738a1 100644
--- a/dotnet/src/Microsoft.Agents.AI/AgentExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI/AgentExtensions.cs
@@ -4,7 +4,6 @@ using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
-using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
@@ -18,15 +17,20 @@ public static class AgentExtensions
/// Wraps the agent with OpenTelemetry instrumentation.
///
/// The agent to wrap.
- /// The to use for emitting events.
/// An optional source name that will be used on the telemetry data.
/// When indicates whether potentially sensitive information should be included in telemetry. Default is
/// An that wraps the original agent with telemetry.
- public static OpenTelemetryAgent WithOpenTelemetry(this AIAgent agent, ILoggerFactory? loggerFactory = null, string? sourceName = null, bool? enableSensitiveData = null) =>
- new(agent, loggerFactory?.CreateLogger(typeof(OpenTelemetryAgent)), sourceName)
+ public static OpenTelemetryAgent WithOpenTelemetry(this AIAgent agent, string? sourceName = null, bool? enableSensitiveData = null)
+ {
+ OpenTelemetryAgent otel = new(agent, sourceName);
+
+ if (enableSensitiveData is not null)
{
- EnableSensitiveData = enableSensitiveData ?? false
- };
+ otel.EnableSensitiveData = enableSensitiveData.Value;
+ }
+
+ return otel;
+ }
///
/// Creates a that will invoke the provided Agent.
diff --git a/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs b/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs
index eda230675c..6960327286 100644
--- a/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs
@@ -64,7 +64,6 @@ internal sealed class FunctionInvocationDelegatingAgent : DelegatingAIAgent
Arguments = arguments,
Function = this.InnerFunction,
CallContent = new(string.Empty, this.InnerFunction.Name, new Dictionary(arguments)),
- Iteration = 0, // Indicate this function was not invoked by a FICC and has no iteration flow.
};
return await next(innerAgent, context, CoreLogicAsync, cancellationToken).ConfigureAwait(false);
diff --git a/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj b/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj
index 765874a4b5..28b62cf1d4 100644
--- a/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj
+++ b/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj
@@ -1,4 +1,4 @@
-
+
$(ProjectsTargetFrameworks)
@@ -11,6 +11,7 @@
true
true
true
+ true
diff --git a/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAIAgentBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAIAgentBuilderExtensions.cs
index 59b652ffe6..8cf0dbfcd6 100644
--- a/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAIAgentBuilderExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAIAgentBuilderExtensions.cs
@@ -1,9 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
-using Microsoft.Extensions.AI;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
@@ -19,20 +16,16 @@ public static class OpenTelemetryAIAgentBuilderExtensions
/// The specification is still experimental and subject to change; as such, the telemetry output by this agent is also subject to change.
///
/// The .
- /// An optional to use to create a logger for logging events.
/// An optional source name that will be used on the telemetry data.
/// An optional callback that can be used to configure the instance.
/// The .
public static AIAgentBuilder UseOpenTelemetry(
this AIAgentBuilder builder,
- ILoggerFactory? loggerFactory = null,
string? sourceName = null,
Action? configure = null) =>
Throw.IfNull(builder).Use((innerAgent, services) =>
{
- loggerFactory ??= services.GetService();
-
- var agent = new OpenTelemetryAgent(innerAgent, loggerFactory?.CreateLogger(typeof(OpenTelemetryAgent)), sourceName);
+ var agent = new OpenTelemetryAgent(innerAgent, sourceName);
configure?.Invoke(agent);
return agent;
diff --git a/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs b/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs
index 1fc4659aa8..ba8e35a46c 100644
--- a/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs
@@ -3,97 +3,49 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Diagnostics.Metrics;
-using System.Linq;
using System.Runtime.CompilerServices;
-using System.Text.Json;
-using System.Text.Json.Nodes;
-using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Logging.Abstractions;
-using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
-///
-/// Represents a delegating agent that implements OpenTelemetry instrumentation for agent operations.
-///
+/// Represents a delegating that implements the OpenTelemetry Semantic Conventions for Generative AI systems.
///
-/// This class provides telemetry instrumentation for agent operations including activities, metrics, and logging.
-/// The telemetry output follows OpenTelemetry semantic conventions in and is subject to change as the conventions evolve.
+/// This class provides an implementation of the Semantic Conventions for Generative AI systems, defined at .
+/// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change.
///
-public sealed partial class OpenTelemetryAgent : DelegatingAIAgent, IDisposable
+public sealed class OpenTelemetryAgent : DelegatingAIAgent, IDisposable
{
- private const LogLevel EventLogLevel = LogLevel.Information;
- private JsonSerializerOptions _jsonSerializerOptions;
- private readonly OpenTelemetryChatClient? _openTelemetryChatClient;
- private readonly string? _system;
- private readonly ActivitySource _activitySource;
- private readonly Meter _meter;
- private readonly Histogram _operationDurationHistogram;
- private readonly Histogram _tokenUsageHistogram;
- private readonly ILogger _logger;
+ // IMPLEMENTATION NOTE: The OpenTelemetryChatClient from Microsoft.Extensions.AI provides a full and up-to-date
+ // implementationof the OpenTelemetry Semantic Conventions for Generative AI systems, specifically for the client
+ // metrics and the chat span. But the chat span is almost identical to the invoke_agent span, just with invoke_agent
+ // have a different value for the operation name and a few additional tags. To avoid needing to reimplement the
+ // convention, then, and keep it up-to-date as the convention evolves, for now this implementation just delegates
+ // to OpenTelemetryChatClient for the actual telemetry work. For RunAsync and RunStreamingAsync, it delegates to the
+ // inner agent not directly but rather via OpenTelemetryChatClient, which wraps a ForwardingChatClient that in turn
+ // calls back into the inner agent.
- ///
- /// Initializes a new instance of the class.
- ///
- /// The underlying agent to wrap with telemetry.
- /// The to use for emitting events.
+ /// The providing the bulk of the telemetry.
+ private readonly OpenTelemetryChatClient _otelClient;
+ /// The provider name extracted from .
+ private readonly string? _providerName;
+
+ /// Initializes a new instance of the class.
+ /// The underlying .
/// An optional source name that will be used on the telemetry data.
- public OpenTelemetryAgent(AIAgent innerAgent, ILogger? logger = null, string? sourceName = null)
- : base(innerAgent)
+ /// is .
+ public OpenTelemetryAgent(AIAgent innerAgent, string? sourceName = null) : base(innerAgent)
{
- string name = string.IsNullOrEmpty(sourceName) ? OpenTelemetryConsts.DefaultSourceName : sourceName!;
- this._activitySource = new(name);
- this._meter = new(name);
- this._logger = logger ?? NullLogger.Instance;
- this._system = this.GetService()?.ProviderName ?? OpenTelemetryConsts.GenAI.SystemNameValues.MicrosoftAgentsAI;
+ this._providerName = innerAgent.GetService()?.ProviderName;
- // Attempt to get the open telemetry chat client if the inner agent is a ChatClientAgent.
- this._openTelemetryChatClient = (this.InnerAgent as ChatClientAgent)?.ChatClient.GetService();
-
- // Inherit by default the EnableSensitiveData setting from the TelemetryChatClient if available.
- this.EnableSensitiveData = this._openTelemetryChatClient?.EnableSensitiveData ?? false;
-
- this._operationDurationHistogram = this._meter.CreateHistogram(
- OpenTelemetryConsts.GenAI.Client.OperationDuration.Name,
- OpenTelemetryConsts.SecondsUnit,
- OpenTelemetryConsts.GenAI.Client.OperationDuration.Description
-#if NET9_0_OR_GREATER
- , advice: new() { HistogramBucketBoundaries = OpenTelemetryConsts.GenAI.Client.OperationDuration.ExplicitBucketBoundaries }
-#endif
- );
-
- this._tokenUsageHistogram = this._meter.CreateHistogram(
- OpenTelemetryConsts.GenAI.Client.TokenUsage.Name,
- OpenTelemetryConsts.TokensUnit,
- OpenTelemetryConsts.GenAI.Client.TokenUsage.Description
-#if NET9_0_OR_GREATER
- , advice: new() { HistogramBucketBoundaries = OpenTelemetryConsts.GenAI.Client.TokenUsage.ExplicitBucketBoundaries }
-#endif
- );
-
- this._jsonSerializerOptions = AIJsonUtilities.DefaultOptions;
+ this._otelClient = new OpenTelemetryChatClient(
+ new ForwardingChatClient(this),
+ sourceName: string.IsNullOrEmpty(sourceName) ? OpenTelemetryConsts.DefaultSourceName : sourceName!);
}
- /// Gets or sets JSON serialization options to use when formatting chat data into telemetry strings.
- public JsonSerializerOptions JsonSerializerOptions
- {
- get => this._jsonSerializerOptions;
- set => this._jsonSerializerOptions = Throw.IfNull(value);
- }
-
- ///
- /// Disposes the telemetry resources.
- ///
- public void Dispose()
- {
- this._activitySource.Dispose();
- this._meter.Dispose();
- }
+ ///
+ public void Dispose() => this._otelClient.Dispose();
///
/// Gets or sets a value indicating whether potentially sensitive information should be included in telemetry.
@@ -101,434 +53,176 @@ public sealed partial class OpenTelemetryAgent : DelegatingAIAgent, IDisposable
///
/// if potentially sensitive information should be included in telemetry;
/// if telemetry shouldn't include raw inputs and outputs.
- /// The default value is .
+ /// The default value is , unless the OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT
+ /// environment variable is set to "true" (case-insensitive).
///
///
/// By default, telemetry includes metadata, such as token counts, but not raw inputs
/// and outputs, such as message content, function call arguments, and function call results.
+ /// The default value can be overridden by setting the OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT
+ /// environment variable to "true". Explicitly setting this property will override the environment variable.
///
- public bool EnableSensitiveData { get; set; }
-
- ///
- public override object? GetService(Type serviceType, object? serviceKey = null)
+ public bool EnableSensitiveData
{
- // Handle ActivitySource requests directly - always return our own ActivitySource
- if (serviceType == typeof(ActivitySource))
- {
- return this._activitySource;
- }
-
- // For other service types, use the base delegation logic
- return base.GetService(serviceType, serviceKey);
+ get => this._otelClient.EnableSensitiveData;
+ set => this._otelClient.EnableSensitiveData = value;
}
///
public override async Task RunAsync(
- IEnumerable messages,
- AgentThread? thread = null,
- AgentRunOptions? options = null,
- CancellationToken cancellationToken = default)
+ IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
- var inputMessages = Throw.IfNull(messages) as IReadOnlyCollection ?? messages.ToList();
+ ChatOptions co = new ForwardedOptions(options, thread, Activity.Current);
- using Activity? activity = this.CreateAndConfigureActivity(OpenTelemetryConsts.GenAI.Operation.NameValues.InvokeAgent, thread);
- Stopwatch? stopwatch = this._operationDurationHistogram.Enabled ? Stopwatch.StartNew() : null;
+ var response = await this._otelClient.GetResponseAsync(messages, co, cancellationToken).ConfigureAwait(false);
- this.LogChatMessages(inputMessages);
-
- AgentRunResponse? response = null;
- Exception? error = null;
- try
- {
- response = await base.RunAsync(inputMessages, thread, options, cancellationToken).ConfigureAwait(false);
- return response;
- }
- catch (Exception ex)
- {
- error = ex;
- throw;
- }
- finally
- {
- this.TraceResponse(activity, response, error, stopwatch);
- }
+ return (AgentRunResponse)response.RawRepresentation!;
}
///
public override async IAsyncEnumerable RunStreamingAsync(
- IEnumerable messages,
- AgentThread? thread = null,
- AgentRunOptions? options = null,
- [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- var inputMessages = Throw.IfNull(messages) as IReadOnlyCollection ?? messages.ToList();
+ ChatOptions co = new ForwardedOptions(options, thread, Activity.Current);
- using Activity? activity = this.CreateAndConfigureActivity(OpenTelemetryConsts.GenAI.Operation.NameValues.InvokeAgent, thread);
- Stopwatch? stopwatch = this._operationDurationHistogram.Enabled ? Stopwatch.StartNew() : null;
-
- IAsyncEnumerable updates;
- try
+ await foreach (var update in this._otelClient.GetStreamingResponseAsync(messages, co, cancellationToken).ConfigureAwait(false))
{
- updates = base.RunStreamingAsync(inputMessages, thread, options, cancellationToken);
- }
- catch (Exception ex)
- {
- this.TraceResponse(activity, response: null, ex, stopwatch);
- throw;
- }
-
- var responseEnumerator = updates.GetAsyncEnumerator(cancellationToken);
- List trackedUpdates = [];
- Exception? error = null;
-
- try
- {
- while (true)
- {
- AgentRunResponseUpdate update;
- try
- {
- if (!await responseEnumerator.MoveNextAsync().ConfigureAwait(false))
- {
- break;
- }
- update = responseEnumerator.Current;
- }
- catch (Exception ex)
- {
- error = ex;
- throw;
- }
-
- trackedUpdates.Add(update);
- yield return update;
- Activity.Current = activity; // workaround for https://github.com/dotnet/runtime/issues/47802
- }
- }
- finally
- {
- this.TraceResponse(activity, trackedUpdates.ToAgentRunResponse(), error, stopwatch);
- await responseEnumerator.DisposeAsync().ConfigureAwait(false);
+ yield return (AgentRunResponseUpdate)update.RawRepresentation!;
}
}
- ///
- /// Creates an activity for an agent request, or returns null if not enabled.
- ///
- private Activity? CreateAndConfigureActivity(string operationName, AgentThread? thread)
+ /// Augments the current activity created by the with agent-specific information.
+ /// The that was current prior to the 's invocation.
+ private void UpdateCurrentActivity(Activity? previousActivity)
{
- // Get the GenAI system name for telemetry
- var chatClientAgent = this.InnerAgent as ChatClientAgent;
- Activity? activity = null;
- if (this._activitySource.HasListeners())
+ // If there isn't a current activity to augment, or it's the same one that was current when the agent was invoked (meaning
+ // the OpenTelemetryChatClient didn't create one), then there's nothing to do.
+ if (Activity.Current is not { } activity ||
+ ReferenceEquals(activity, previousActivity))
{
- string activityName = string.IsNullOrWhiteSpace(this.Name) ? operationName : $"{operationName} {this.Name}";
- activity = this._activitySource.StartActivity(activityName, ActivityKind.Client);
+ return;
+ }
- if (activity is not null)
+ // Override information set by OpenTelemetryChatClient to make it specific to invoke_agent.
+
+ activity.DisplayName = $"invoke_agent {this.DisplayName}";
+
+ if (!string.IsNullOrWhiteSpace(this._providerName))
+ {
+ _ = activity.SetTag(OpenTelemetryConsts.GenAI.Provider.Name, this._providerName);
+ }
+
+ // Further augment the activity with agent-specific tags.
+
+ _ = activity.SetTag(OpenTelemetryConsts.GenAI.Agent.Id, this.Id);
+
+ if (this.Name is { } name && !string.IsNullOrWhiteSpace(name))
+ {
+ _ = activity.SetTag(OpenTelemetryConsts.GenAI.Agent.Name, this.Name);
+ }
+
+ if (this.Description is { } description && !string.IsNullOrWhiteSpace(description))
+ {
+ _ = activity.SetTag(OpenTelemetryConsts.GenAI.Agent.Description, description);
+ }
+ }
+
+ /// State passed from this instance into the inner agent, circumventing the intermediate .
+ private sealed class ForwardedOptions : ChatOptions
+ {
+ public ForwardedOptions(AgentRunOptions? options, AgentThread? thread, Activity? currentActivity)
+ {
+ this.Options = options;
+ this.Thread = thread;
+ this.CurrentActivity = currentActivity;
+
+ if (options is ChatClientAgentRunOptions { ChatOptions: { } chatClientOptions })
{
- _ = activity
- // Required attributes per OpenTelemetry semantic conventions
- .AddTag(OpenTelemetryConsts.GenAI.Operation.Name, operationName)
- .AddTag(OpenTelemetryConsts.GenAI.SystemName, this._system)
- // Agent-specific attributes
- .AddTag(OpenTelemetryConsts.GenAI.Agent.Id, this.Id);
-
- // Add agent name if available (following gen_ai.agent.name convention - conditionally required when available)
- if (!string.IsNullOrWhiteSpace(this.Name))
- {
- _ = activity.AddTag(OpenTelemetryConsts.GenAI.Agent.Name, this.Name);
- }
-
- // Add description if available (following gen_ai.agent.description convention)
- if (!string.IsNullOrWhiteSpace(this.Description))
- {
- _ = activity.AddTag(OpenTelemetryConsts.GenAI.Agent.Description, this.Description);
- }
-
- // Add conversation ID if thread is available (following gen_ai.conversation.id convention)
- var metadata = thread?.GetService();
- if (!string.IsNullOrWhiteSpace(metadata?.ConversationId))
- {
- _ = activity.AddTag(OpenTelemetryConsts.GenAI.Conversation.Id, metadata.ConversationId);
- }
-
- // Add instructions if available (for ChatClientAgent)
- if (!string.IsNullOrWhiteSpace(chatClientAgent?.Instructions))
- {
- _ = activity.AddTag(OpenTelemetryConsts.GenAI.Request.Instructions, chatClientAgent.Instructions);
- }
+ // Keep this faux copy ctor in sync with public properties on ChatOptions.
+ this.AdditionalProperties = chatClientOptions.AdditionalProperties;
+ this.AllowMultipleToolCalls = chatClientOptions.AllowMultipleToolCalls;
+ this.ConversationId = chatClientOptions.ConversationId;
+ this.FrequencyPenalty = chatClientOptions.FrequencyPenalty;
+ this.Instructions = chatClientOptions.Instructions;
+ this.MaxOutputTokens = chatClientOptions.MaxOutputTokens;
+ this.ModelId = chatClientOptions.ModelId;
+ this.PresencePenalty = chatClientOptions.PresencePenalty;
+ this.RawRepresentationFactory = chatClientOptions.RawRepresentationFactory;
+ this.ResponseFormat = chatClientOptions.ResponseFormat;
+ this.Seed = chatClientOptions.Seed;
+ this.StopSequences = chatClientOptions.StopSequences;
+ this.Temperature = chatClientOptions.Temperature;
+ this.Tools = chatClientOptions.Tools;
+ this.ToolMode = chatClientOptions.ToolMode;
+ this.TopK = chatClientOptions.TopK;
+ this.TopP = chatClientOptions.TopP;
}
}
- return activity;
+ public AgentRunOptions? Options { get; }
+
+ public AgentThread? Thread { get; }
+
+ public Activity? CurrentActivity { get; }
}
- ///
- /// Adds a tag to the tag list if the value is not null or whitespace.
- ///
- private static void AddIfNotWhiteSpace(ref TagList tags, string key, string? value)
+ /// The stub used to delegate from the into the inner .
+ ///
+ private sealed class ForwardingChatClient(OpenTelemetryAgent parentAgent) : IChatClient
{
- if (!string.IsNullOrWhiteSpace(value))
+ public async Task GetResponseAsync(
+ IEnumerable messages, ChatOptions? options, CancellationToken cancellationToken)
{
- tags.Add(key, value);
- }
- }
+ ForwardedOptions? fo = options as ForwardedOptions;
- ///
- /// Adds agent response information to the activity and records metrics.
- ///
- private void TraceResponse(
- Activity? activity,
- AgentRunResponse? response,
- Exception? error,
- Stopwatch? stopwatch)
- {
- // Record operation duration metric
- if (this._operationDurationHistogram.Enabled && stopwatch is not null)
- {
- TagList tags = new()
+ // Update the current activity to reflect the agent invocation.
+ parentAgent.UpdateCurrentActivity(fo?.CurrentActivity);
+
+ // Invoke the inner agent.
+ var response = await parentAgent.InnerAgent.RunAsync(messages, fo?.Thread, fo?.Options, cancellationToken).ConfigureAwait(false);
+
+ // Wrap the response in a ChatResponse so we can pass it back through OpenTelemetryChatClient.
+ return new ChatResponse
{
- { OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Operation.NameValues.InvokeAgent }
+ AdditionalProperties = response.AdditionalProperties,
+ CreatedAt = response.CreatedAt,
+ Messages = response.Messages,
+ RawRepresentation = response,
+ ResponseId = response.ResponseId,
+ Usage = response.Usage,
};
-
- AddIfNotWhiteSpace(ref tags, OpenTelemetryConsts.GenAI.Agent.Name, this.DisplayName);
-
- if (error is not null)
- {
- tags.Add(OpenTelemetryConsts.Error.Type, error.GetType().FullName);
- }
-
- this._operationDurationHistogram.Record(stopwatch.Elapsed.TotalSeconds, tags);
}
- // Record token usage metrics
- if (this._tokenUsageHistogram.Enabled && response?.Usage is { } usage)
+ public async IAsyncEnumerable GetStreamingResponseAsync(
+ IEnumerable messages, ChatOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken)
{
- if (usage.InputTokenCount is long inputTokens)
+ ForwardedOptions? fo = options as ForwardedOptions;
+
+ // Update the current activity to reflect the agent invocation.
+ parentAgent.UpdateCurrentActivity(fo?.CurrentActivity);
+
+ // Invoke the inner agent.
+ await foreach (var update in parentAgent.InnerAgent.RunStreamingAsync(messages, fo?.Thread, fo?.Options, cancellationToken).ConfigureAwait(false))
{
- TagList tags = new()
+ // Wrap the response updates in ChatResponseUpdates so we can pass them back through OpenTelemetryChatClient.
+ yield return new ChatResponseUpdate
{
- { OpenTelemetryConsts.GenAI.Token.Type, "input" }
+ AdditionalProperties = update.AdditionalProperties,
+ AuthorName = update.AuthorName,
+ Contents = update.Contents,
+ CreatedAt = update.CreatedAt,
+ MessageId = update.MessageId,
+ RawRepresentation = update,
+ ResponseId = update.ResponseId,
+ Role = update.Role,
};
-
- AddIfNotWhiteSpace(ref tags, OpenTelemetryConsts.GenAI.Agent.Name, this.Name);
-
- this._tokenUsageHistogram.Record((int)inputTokens, tags);
- }
-
- if (usage.OutputTokenCount is long outputTokens)
- {
- TagList tags = new()
- {
- { OpenTelemetryConsts.GenAI.Token.Type, "output" }
- };
-
- AddIfNotWhiteSpace(ref tags, OpenTelemetryConsts.GenAI.Agent.Name, this.Name);
-
- this._tokenUsageHistogram.Record((int)outputTokens, tags);
}
}
- // Add activity tags
- if (activity is not null)
- {
- if (error is not null)
- {
- _ = activity
- .AddTag(OpenTelemetryConsts.Error.Type, error.GetType().FullName)
- .SetStatus(ActivityStatusCode.Error, error.Message);
- }
+ public object? GetService(Type serviceType, object? serviceKey = null) =>
+ // Delegate any inquiries made by the OpenTelemetryChatClient back to the parent agent.
+ parentAgent.GetService(serviceType, serviceKey);
- if (response is not null)
- {
- if (!string.IsNullOrWhiteSpace(response.ResponseId))
- {
- _ = activity.AddTag(OpenTelemetryConsts.GenAI.Response.Id, response.ResponseId);
- }
-
- if (response.Usage?.InputTokenCount is long inputTokens)
- {
- _ = activity.AddTag(OpenTelemetryConsts.GenAI.Usage.InputTokens, (int)inputTokens);
- }
-
- if (response.Usage?.OutputTokenCount is long outputTokens)
- {
- _ = activity.AddTag(OpenTelemetryConsts.GenAI.Usage.OutputTokens, (int)outputTokens);
- }
- }
- }
-
- // Log the agent response for choice events
- if (response is not null)
- {
- this.LogAgentResponse(response);
- }
+ public void Dispose() { }
}
-
- private void LogChatMessages(IEnumerable messages)
- {
- if (this._openTelemetryChatClient is not null)
- {
- // To avoid duplication of telemetry data the logging will be skipped if the agent is a ChatClientAgent and
- // its innerChatClient already has telemetry enabled,
- return;
- }
-
- if (!this._logger.IsEnabled(EventLogLevel))
- {
- return;
- }
-
- foreach (ChatMessage message in messages)
- {
- if (message.Role == ChatRole.Assistant)
- {
- this.Log(new EventId(1, OpenTelemetryConsts.GenAI.Assistant.Message),
- JsonSerializer.Serialize(this.CreateAssistantEvent(message.Contents), OtelContext.Default.AssistantEvent));
- }
- else if (message.Role == ChatRole.Tool)
- {
- foreach (FunctionResultContent frc in message.Contents.OfType())
- {
- this.Log(new EventId(1, OpenTelemetryConsts.GenAI.Tool.Message),
- JsonSerializer.Serialize(new ToolEvent()
- {
- Id = frc.CallId,
- Content = this.EnableSensitiveData && frc.Result is object result ?
- JsonSerializer.SerializeToNode(result, this._jsonSerializerOptions.GetTypeInfo(result.GetType())) :
- null,
- }, OtelContext.Default.ToolEvent));
- }
- }
- else
- {
- this.Log(new EventId(1, message.Role == ChatRole.System ? OpenTelemetryConsts.GenAI.System.Message : OpenTelemetryConsts.GenAI.User.Message),
- JsonSerializer.Serialize(new SystemOrUserEvent()
- {
- Role = message.Role != ChatRole.System && message.Role != ChatRole.User && !string.IsNullOrWhiteSpace(message.Role.Value) ? message.Role.Value : null,
- Content = this.GetMessageContent(message.Contents),
- }, OtelContext.Default.SystemOrUserEvent));
- }
- }
- }
-
- private void LogAgentResponse(AgentRunResponse response)
- {
- if (this._openTelemetryChatClient is not null)
- {
- // To avoid duplication of telemetry data the logging will be skipped if the agent is a ChatClientAgent and
- // its innerChatClient already has telemetry enabled
- return;
- }
-
- if (!this._logger.IsEnabled(EventLogLevel))
- {
- return;
- }
-
- EventId id = new(1, OpenTelemetryConsts.GenAI.Choice);
- this.Log(id, JsonSerializer.Serialize(new ChoiceEvent()
- {
- FinishReason = (response.RawRepresentation as ChatResponse)?.FinishReason?.Value ?? string.Empty,
- Index = 0,
- Message = this.CreateAssistantEvent(response.Messages is { Count: 1 } ? response.Messages[0].Contents : response.Messages.SelectMany(m => m.Contents)),
- }, OtelContext.Default.ChoiceEvent));
- }
-
- private void Log(EventId id, string eventBodyJson)
- {
- // This is not the idiomatic way to log, but it's necessary for now in order to structure
- // the data in a way that the OpenTelemetry collector can work with it. The event body
- // can be very large and should not be logged as an attribute.
-
- KeyValuePair[] tags =
- [
- new(OpenTelemetryConsts.Event.Name, id.Name),
- new(OpenTelemetryConsts.GenAI.SystemName, this._system),
- ];
-
- this._logger.Log(EventLogLevel, id, tags, null, (_, __) => eventBodyJson);
- }
-
- private AssistantEvent CreateAssistantEvent(IEnumerable contents)
- {
- var toolCalls = contents.OfType().Select(fc => new ToolCall
- {
- Id = fc.CallId,
- Function = new()
- {
- Name = fc.Name,
- Arguments = this.EnableSensitiveData ?
- JsonSerializer.SerializeToNode(fc.Arguments, this._jsonSerializerOptions.GetTypeInfo(typeof(IDictionary))) :
- null,
- },
- }).ToArray();
-
- return new()
- {
- Content = this.GetMessageContent(contents),
- ToolCalls = toolCalls.Length > 0 ? toolCalls : null,
- };
- }
-
- private string? GetMessageContent(IEnumerable contents)
- {
- if (this.EnableSensitiveData)
- {
- string content = string.Concat(contents.OfType());
- if (content.Length > 0)
- {
- return content;
- }
- }
-
- return null;
- }
-
- private sealed partial class SystemOrUserEvent
- {
- public string? Role { get; set; }
- public string? Content { get; set; }
- }
-
- private sealed class AssistantEvent
- {
- public string? Content { get; set; }
- public ToolCall[]? ToolCalls { get; set; }
- }
-
- private sealed partial class ToolEvent
- {
- public string? Id { get; set; }
- public JsonNode? Content { get; set; }
- }
-
- private sealed partial class ChoiceEvent
- {
- public string? FinishReason { get; set; }
- public int Index { get; set; }
- public AssistantEvent? Message { get; set; }
- }
-
- private sealed partial class ToolCall
- {
- public string? Id { get; set; }
- public string? Type { get; set; } = "function";
- public ToolCallFunction? Function { get; set; }
- }
-
- private sealed partial class ToolCallFunction
- {
- public string? Name { get; set; }
- public JsonNode? Arguments { get; set; }
- }
-
- [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
- [JsonSerializable(typeof(SystemOrUserEvent))]
- [JsonSerializable(typeof(AssistantEvent))]
- [JsonSerializable(typeof(ToolEvent))]
- [JsonSerializable(typeof(ChoiceEvent))]
- [JsonSerializable(typeof(object))]
- private sealed partial class OtelContext : JsonSerializerContext;
}
diff --git a/dotnet/src/Microsoft.Agents.AI/OpenTelemetryConsts.cs b/dotnet/src/Microsoft.Agents.AI/OpenTelemetryConsts.cs
index 152ab793f0..ac84dc00da 100644
--- a/dotnet/src/Microsoft.Agents.AI/OpenTelemetryConsts.cs
+++ b/dotnet/src/Microsoft.Agents.AI/OpenTelemetryConsts.cs
@@ -7,33 +7,9 @@ internal static class OpenTelemetryConsts
{
public const string DefaultSourceName = "Experimental.Microsoft.Agents.AI";
- public const string SecondsUnit = "s";
- public const string TokensUnit = "token";
-
- public static class Event
- {
- public const string Name = "event.name";
- }
-
- public static class Error
- {
- public const string Type = "error.type";
- }
-
public static class GenAI
{
- public const string Choice = "gen_ai.choice";
-
- public const string SystemName = "gen_ai.system";
-
- public static class SystemNameValues
- {
- public const string MicrosoftAgentsAI = "microsoft.agents.ai";
- }
-
- public const string Chat = "chat";
- public const string Embeddings = "embeddings";
- public const string ExecuteTool = "execute_tool";
+ public const string InvokeAgent = "invoke_agent";
public static class Agent
{
@@ -42,113 +18,9 @@ internal static class OpenTelemetryConsts
public const string Description = "gen_ai.agent.description";
}
- public static class Assistant
+ public static class Provider
{
- public const string Message = "gen_ai.assistant.message";
+ public const string Name = "gen_ai.provider.name";
}
-
- public static class Client
- {
- public static class OperationDuration
- {
- public const string Description = "Measures the duration of a GenAI operation";
- public const string Name = "gen_ai.client.operation.duration";
- public static readonly double[] ExplicitBucketBoundaries = [0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.56, 5.12, 10.24, 20.48, 40.96, 81.92];
- }
-
- public static class TokenUsage
- {
- public const string Description = "Measures number of input and output tokens used";
- public const string Name = "gen_ai.client.token.usage";
- public static readonly int[] ExplicitBucketBoundaries = [1, 4, 16, 64, 256, 1_024, 4_096, 16_384, 65_536, 262_144, 1_048_576, 4_194_304, 16_777_216, 67_108_864];
- }
- }
-
- public static class Conversation
- {
- public const string Id = "gen_ai.conversation.id";
- }
-
- public static class Operation
- {
- public const string Name = "gen_ai.operation.name";
-
- public static class NameValues
- {
- public const string InvokeAgent = "invoke_agent";
- }
- }
-
- public static class Output
- {
- public const string Type = "gen_ai.output.type";
- }
-
- public static class Request
- {
- public const string EmbeddingDimensions = "gen_ai.request.embedding.dimensions";
- public const string FrequencyPenalty = "gen_ai.request.frequency_penalty";
- public const string Model = "gen_ai.request.model";
- public const string MaxTokens = "gen_ai.request.max_tokens";
- public const string PresencePenalty = "gen_ai.request.presence_penalty";
- public const string Seed = "gen_ai.request.seed";
- public const string StopSequences = "gen_ai.request.stop_sequences";
- public const string Temperature = "gen_ai.request.temperature";
- public const string TopK = "gen_ai.request.top_k";
- public const string TopP = "gen_ai.request.top_p";
-
- // Not available in OTEL : Potential proposals
- public const string Instructions = "gen_ai.request.instructions";
-
- public static string PerProvider(string providerName, string parameterName) => $"gen_ai.{providerName}.request.{parameterName}";
- }
-
- public static class Response
- {
- public const string FinishReasons = "gen_ai.response.finish_reasons";
- public const string Id = "gen_ai.response.id";
- public const string Model = "gen_ai.response.model";
-
- public static string PerProvider(string providerName, string parameterName) => $"gen_ai.{providerName}.response.{parameterName}";
- }
-
- public static class System
- {
- public const string Message = "gen_ai.system.message";
- }
-
- public static class Token
- {
- public const string Type = "gen_ai.token.type";
- }
-
- public static class Tool
- {
- public const string Name = "gen_ai.tool.name";
- public const string Description = "gen_ai.tool.description";
- public const string Message = "gen_ai.tool.message";
-
- public static class Call
- {
- public const string Id = "gen_ai.tool.call.id";
- }
- }
-
- public static class Usage
- {
- public const string InputTokens = "gen_ai.usage.input_tokens";
- public const string OutputTokens = "gen_ai.usage.output_tokens";
- }
-
- public static class User
- {
- public const string Message = "gen_ai.user.message";
- }
- }
-
- public static class Server
- {
- public const string Address = "server.address";
- public const string Port = "server.port";
}
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/OpenTelemetryAIAgentBuilderExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/OpenTelemetryAIAgentBuilderExtensionsTests.cs
index 9181d81f63..5d895f9553 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/OpenTelemetryAIAgentBuilderExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/OpenTelemetryAIAgentBuilderExtensionsTests.cs
@@ -39,24 +39,6 @@ public class OpenTelemetryAIAgentBuilderExtensionsTests
Assert.IsType(result);
}
- ///
- /// Verify that UseOpenTelemetry with logger factory works correctly.
- ///
- [Fact]
- public void UseOpenTelemetry_WithLoggerFactory_UsesProvidedLoggerFactory()
- {
- // Arrange
- var mockAgent = new Mock();
- using var loggerFactory = LoggerFactory.Create(builder => { });
- var builder = new AIAgentBuilder(mockAgent.Object);
-
- // Act
- var result = builder.UseOpenTelemetry(loggerFactory).Build();
-
- // Assert
- Assert.IsType(result);
- }
-
///
/// Verify that UseOpenTelemetry with source name works correctly.
///
@@ -131,7 +113,6 @@ public class OpenTelemetryAIAgentBuilderExtensionsTests
// Act
var result = builder.UseOpenTelemetry(
- loggerFactory: loggerFactory,
sourceName: SourceName,
configure: agent =>
{
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/OpenTelemetryAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/OpenTelemetryAgentTests.cs
index 04165186c2..d8d5c56b0a 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/OpenTelemetryAgentTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/OpenTelemetryAgentTests.cs
@@ -5,2722 +5,595 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
-using System.Text.Json;
+using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
-using Microsoft.Extensions.Logging;
-using Moq;
-using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
+#pragma warning disable CA1861 // Avoid constant arrays as arguments
+#pragma warning disable RCS1186 // Use Regex instance instead of static method
+
namespace Microsoft.Agents.AI.UnitTests;
public class OpenTelemetryAgentTests
{
- [Theory]
- [InlineData(false)]
- [InlineData(true)]
- public async Task RunAsync_ExpectedTelemetryData_CollectedAsync(bool withError)
+ [Fact]
+ public void Ctor_InvalidArgs_Throws()
{
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var activities = new List();
- using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
- .AddSource(sourceName)
- .AddInMemoryExporter(activities)
- .Build();
+ Assert.Throws(() => new OpenTelemetryAgent(null!));
+ }
- var mockLogger = new Mock();
- var mockAgent = CreateMockAgent(withError);
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object, mockLogger.Object, sourceName);
+ [Fact]
+ public void Ctor_NullSourceName_Valid()
+ {
+ using var agent = new OpenTelemetryAgent(new TestAIAgent(), null);
+ Assert.NotNull(agent);
+ }
- var messages = new List
+ [Fact]
+ public void Properties_DelegateToInnerAgent()
+ {
+ TestAIAgent innerAgent = new()
{
- new(ChatRole.User, "What's the weather like?")
+ NameFunc = () => "TestAgent",
+ DescriptionFunc = () => "This is a test agent.",
};
- var thread = new Mock().Object;
+ using var agent = new OpenTelemetryAgent(innerAgent, "MySource");
- // Act & Assert
- if (withError)
+ Assert.Equal("TestAgent", agent.Name);
+ Assert.Equal("This is a test agent.", agent.Description);
+ Assert.Equal(innerAgent.Id, agent.Id);
+ Assert.Equal(innerAgent.DisplayName, agent.DisplayName);
+ }
+
+ [Fact]
+ public void EnableSensitiveData_Roundtrips()
+ {
+ using var agent = new OpenTelemetryAgent(new TestAIAgent(), "MySource");
+ for (int i = 0; i < 2; i++)
{
- var exception = await Assert.ThrowsAsync(
- () => telemetryAgent.RunAsync(messages, thread));
- Assert.Equal("Test error", exception.Message);
+ Assert.False(agent.EnableSensitiveData);
+ agent.EnableSensitiveData = true;
+ Assert.True(agent.EnableSensitiveData);
+ agent.EnableSensitiveData = false;
}
- else
- {
- var response = await telemetryAgent.RunAsync(messages, thread);
- Assert.NotNull(response);
- Assert.Equal("Test response", response.Messages.First().Text);
- }
-
- // Verify activity was created
- var activity = Assert.Single(activities);
- Assert.NotNull(activity.Id);
- Assert.NotEmpty(activity.Id);
- Assert.Equal($"{OpenTelemetryConsts.GenAI.Operation.NameValues.InvokeAgent} TestAgent", activity.DisplayName);
- Assert.Equal(ActivityKind.Client, activity.Kind);
-
- // Verify activity tags
- Assert.Equal(OpenTelemetryConsts.GenAI.Operation.NameValues.InvokeAgent, activity.GetTagItem(OpenTelemetryConsts.GenAI.Operation.Name));
- Assert.Equal(OpenTelemetryConsts.GenAI.SystemNameValues.MicrosoftAgentsAI, activity.GetTagItem(OpenTelemetryConsts.GenAI.SystemName));
- Assert.Equal("test-agent-id", activity.GetTagItem(OpenTelemetryConsts.GenAI.Agent.Id));
- Assert.Equal("TestAgent", activity.GetTagItem(OpenTelemetryConsts.GenAI.Agent.Name));
- Assert.Equal("Test Description", activity.GetTagItem(OpenTelemetryConsts.GenAI.Agent.Description));
-
- if (withError)
- {
- Assert.Equal("System.InvalidOperationException", activity.GetTagItem(OpenTelemetryConsts.Error.Type));
- Assert.Equal(ActivityStatusCode.Error, activity.Status);
- Assert.Equal("Test error", activity.StatusDescription);
- }
- else
- {
- Assert.Equal("test-response-id", activity.GetTagItem(OpenTelemetryConsts.GenAI.Response.Id));
- Assert.Equal(10, activity.GetTagItem(OpenTelemetryConsts.GenAI.Usage.InputTokens));
- Assert.Equal(20, activity.GetTagItem(OpenTelemetryConsts.GenAI.Usage.OutputTokens));
- }
-
- Assert.True(activity.Duration.TotalMilliseconds > 0);
}
[Theory]
- [InlineData(false)]
- [InlineData(true)]
- public async Task RunStreamingAsync_ExpectedTelemetryData_CollectedAsync(bool withError)
+ [InlineData(false, false)]
+ [InlineData(false, true)]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ public async Task WithoutChatOptions_ExpectedInformationLogged_Async(bool enableSensitiveData, bool streaming)
{
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
+ var sourceName = Guid.NewGuid().ToString();
var activities = new List();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
.AddInMemoryExporter(activities)
.Build();
- var mockAgent = CreateMockStreamingAgent(withError);
- var mockLogger = new Mock();
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object, mockLogger.Object, sourceName);
-
- var messages = new List
+ var innerAgent = new TestAIAgent
{
- new(ChatRole.User, "Tell me a story")
- };
+ NameFunc = () => "TestAgent",
+ DescriptionFunc = () => "This is a test agent.",
- var thread = new Mock().Object;
-
- // Act & Assert
- if (withError)
- {
- var exception = await Assert.ThrowsAsync(async () =>
+ RunAsyncFunc = async (messages, thread, options, cancellationToken) =>
{
- await foreach (var update in telemetryAgent.RunStreamingAsync(messages, thread))
+ await Task.Yield();
+ return new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "The blue whale, I think."))
{
- // Should not reach here
- }
- });
- Assert.Equal("Streaming error", exception.Message);
- }
- else
+ ResponseId = "id123",
+ Usage = new UsageDetails
+ {
+ InputTokenCount = 10,
+ OutputTokenCount = 20,
+ TotalTokenCount = 42,
+ },
+ AdditionalProperties = new()
+ {
+ ["system_fingerprint"] = "abcdefgh",
+ ["AndSomethingElse"] = "value2",
+ },
+ };
+ },
+
+ RunStreamingAsyncFunc = CallbackAsync,
+
+ GetServiceFunc = (serviceType, serviceKey) =>
+ serviceType == typeof(AIAgentMetadata) ? new AIAgentMetadata("TestAgentProviderFromAIAgentMetadata") :
+ serviceType == typeof(ChatClientMetadata) ? new ChatClientMetadata("TestAgentProviderFromChatClientMetadata", new Uri("http://localhost:12345/something"), "amazingmodel") :
+ null,
+ };
+
+ async static IAsyncEnumerable CallbackAsync(
+ IEnumerable messages, AgentThread? thread, AgentRunOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken)
{
- var updates = new List();
- await foreach (var update in telemetryAgent.RunStreamingAsync(messages, thread))
+ await Task.Yield();
+
+ foreach (string text in new[] { "The ", "blue ", "whale,", " ", "", "I", " think." })
{
- updates.Add(update);
+ await Task.Yield();
+ yield return new AgentRunResponseUpdate(ChatRole.Assistant, text)
+ {
+ ResponseId = "id123",
+ };
}
- Assert.NotEmpty(updates);
- }
- // Verify activity was created
- var activity = Assert.Single(activities);
- Assert.NotNull(activity.Id);
- Assert.NotEmpty(activity.Id);
- Assert.Equal($"{OpenTelemetryConsts.GenAI.Operation.NameValues.InvokeAgent} TestAgent", activity.DisplayName);
- Assert.Equal(ActivityKind.Client, activity.Kind);
-
- // Verify activity tags
- Assert.Equal(OpenTelemetryConsts.GenAI.Operation.NameValues.InvokeAgent, activity.GetTagItem(OpenTelemetryConsts.GenAI.Operation.Name));
- Assert.Equal(OpenTelemetryConsts.GenAI.SystemNameValues.MicrosoftAgentsAI, activity.GetTagItem(OpenTelemetryConsts.GenAI.SystemName));
- Assert.Equal("test-agent-id", activity.GetTagItem(OpenTelemetryConsts.GenAI.Agent.Id));
- Assert.Equal("TestAgent", activity.GetTagItem(OpenTelemetryConsts.GenAI.Agent.Name));
- Assert.Equal("Test Description", activity.GetTagItem(OpenTelemetryConsts.GenAI.Agent.Description));
-
- if (withError)
- {
- Assert.Equal("System.InvalidOperationException", activity.GetTagItem(OpenTelemetryConsts.Error.Type));
- Assert.Equal(ActivityStatusCode.Error, activity.Status);
- Assert.Equal("Streaming error", activity.StatusDescription);
- }
- else
- {
- Assert.Equal("stream-response-id", activity.GetTagItem(OpenTelemetryConsts.GenAI.Response.Id));
- Assert.Equal(15, activity.GetTagItem(OpenTelemetryConsts.GenAI.Usage.InputTokens));
- Assert.Equal(25, activity.GetTagItem(OpenTelemetryConsts.GenAI.Usage.OutputTokens));
- }
-
- Assert.True(activity.Duration.TotalMilliseconds > 0);
- }
-
- [Fact]
- public async Task RunAsync_WithChatClientAgent_IncludesInstructionsAsync()
- {
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var activities = new List();
- using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
- .AddSource(sourceName)
- .AddInMemoryExporter(activities)
- .Build();
-
- var mockChatClient = new Mock();
- mockChatClient.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
- .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Response")));
-
- var chatClientAgent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
- {
- Id = "chat-agent-id",
- Name = "ChatAgent",
- Instructions = "You are a helpful assistant."
- });
-
- using var telemetryAgent = new OpenTelemetryAgent(chatClientAgent, sourceName: sourceName);
-
- var messages = new List
- {
- new(ChatRole.User, "Hello")
- };
-
- // Act
- await telemetryAgent.RunAsync(messages);
-
- // Assert
- var activity = Assert.Single(activities);
- Assert.Equal("You are a helpful assistant.", activity.GetTagItem(OpenTelemetryConsts.GenAI.Request.Instructions));
- // Should use default system when ChatClientMetadata is not available
- Assert.Equal(OpenTelemetryConsts.GenAI.SystemNameValues.MicrosoftAgentsAI, activity.GetTagItem(OpenTelemetryConsts.GenAI.SystemName));
- }
-
- [Fact]
- public async Task RunAsync_WithChatClientAgent_WithMetadata_UsesProviderNameAsync()
- {
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var activities = new List();
- using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
- .AddSource(sourceName)
- .AddInMemoryExporter(activities)
- .Build();
-
- var mockChatClient = new Mock();
- mockChatClient.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
- .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Response")));
-
- // Setup ChatClientMetadata to return a specific provider name
- var metadata = new ChatClientMetadata("openai");
- mockChatClient.Setup(c => c.GetService(typeof(ChatClientMetadata), null))
- .Returns(metadata);
-
- var chatClientAgent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
- {
- Id = "chat-agent-id",
- Name = "ChatAgent",
- Instructions = "You are a helpful assistant."
- });
-
- using var telemetryAgent = new OpenTelemetryAgent(chatClientAgent, sourceName: sourceName);
-
- var messages = new List
- {
- new(ChatRole.User, "Hello")
- };
-
- // Act
- await telemetryAgent.RunAsync(messages);
-
- // Assert
- var activity = Assert.Single(activities);
- Assert.Equal("You are a helpful assistant.", activity.GetTagItem(OpenTelemetryConsts.GenAI.Request.Instructions));
- // Should use the provider name from ChatClientMetadata
- Assert.Equal("openai", activity.GetTagItem(OpenTelemetryConsts.GenAI.SystemName));
- }
-
- [Fact]
- public async Task RunAsync_WithNonChatClientAgent_UsesDefaultSystemAsync()
- {
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var activities = new List();
- using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
- .AddSource(sourceName)
- .AddInMemoryExporter(activities)
- .Build();
-
- var mockAgent = CreateMockAgent(false);
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object, sourceName: sourceName);
-
- var messages = new List
- {
- new(ChatRole.User, "Hello")
- };
-
- // Act
- await telemetryAgent.RunAsync(messages);
-
- // Assert
- var activity = Assert.Single(activities);
- // Should use default system when agent is not a ChatClientAgent
- Assert.Equal(OpenTelemetryConsts.GenAI.SystemNameValues.MicrosoftAgentsAI, activity.GetTagItem(OpenTelemetryConsts.GenAI.SystemName));
- }
-
- [Theory]
- [InlineData("azure")]
- [InlineData("openai")]
- [InlineData("custom-provider")]
- public async Task RunAsync_WithChatClientAgent_WithDifferentProviders_UsesCorrectSystemAsync(string providerName)
- {
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var activities = new List();
- using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
- .AddSource(sourceName)
- .AddInMemoryExporter(activities)
- .Build();
-
- var mockChatClient = new Mock();
- mockChatClient.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
- .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Response")));
-
- // Setup ChatClientMetadata to return the specified provider name
- var metadata = new ChatClientMetadata(providerName);
- mockChatClient.Setup(c => c.GetService(typeof(ChatClientMetadata), null))
- .Returns(metadata);
-
- var chatClientAgent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
- {
- Id = "chat-agent-id",
- Name = "ChatAgent"
- });
-
- using var telemetryAgent = new OpenTelemetryAgent(chatClientAgent, sourceName: sourceName);
-
- var messages = new List
- {
- new(ChatRole.User, "Hello")
- };
-
- // Act
- await telemetryAgent.RunAsync(messages);
-
- // Assert
- var activity = Assert.Single(activities);
- // Should use the provider name from ChatClientMetadata
- Assert.Equal(providerName, activity.GetTagItem(OpenTelemetryConsts.GenAI.SystemName));
- }
-
- [Fact]
- public async Task RunStreamingAsync_WithChatClientAgent_WithMetadata_UsesProviderNameAsync()
- {
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var activities = new List();
- using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
- .AddSource(sourceName)
- .AddInMemoryExporter(activities)
- .Build();
-
- var mockChatClient = new Mock();
- ChatResponseUpdate[] returnUpdates =
- [
- new ChatResponseUpdate(role: ChatRole.Assistant, content: "Stream response")
- ];
- mockChatClient.Setup(c => c.GetStreamingResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
- .Returns(returnUpdates.ToAsyncEnumerable());
-
- // Setup ChatClientMetadata to return a specific provider name
- var metadata = new ChatClientMetadata("azure");
- mockChatClient.Setup(c => c.GetService(typeof(ChatClientMetadata), null))
- .Returns(metadata);
-
- var chatClientAgent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
- {
- Id = "chat-agent-id",
- Name = "ChatAgent",
- Instructions = "You are a helpful assistant."
- });
-
- using var telemetryAgent = new OpenTelemetryAgent(chatClientAgent, sourceName: sourceName);
-
- var messages = new List
- {
- new(ChatRole.User, "Hello")
- };
-
- // Act
- await foreach (var update in telemetryAgent.RunStreamingAsync(messages))
- {
- // Consume the stream
- }
-
- // Assert
- var activity = Assert.Single(activities);
- Assert.Equal("You are a helpful assistant.", activity.GetTagItem(OpenTelemetryConsts.GenAI.Request.Instructions));
- // Should use the provider name from ChatClientMetadata
- Assert.Equal("azure", activity.GetTagItem(OpenTelemetryConsts.GenAI.SystemName));
- }
-
- [Fact]
- public async Task RunAsync_WithThreadId_IncludesThreadIdAsync()
- {
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var activities = new List();
- using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
- .AddSource(sourceName)
- .AddInMemoryExporter(activities)
- .Build();
-
- var mockAgent = CreateMockAgent(false);
- var mockThread = new Mock();
- mockThread.Setup(t => t.GetService(typeof(AgentThreadMetadata), null))
- .Returns(new AgentThreadMetadata("thread-123"));
-
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object, sourceName: sourceName);
-
- var messages = new List
- {
- new(ChatRole.User, "Hello")
- };
-
- // Act
- await telemetryAgent.RunAsync(messages, mockThread.Object);
-
- // Assert
- var activity = Assert.Single(activities);
- Assert.Equal("thread-123", activity.GetTagItem(OpenTelemetryConsts.GenAI.Conversation.Id));
- }
-
- [Fact]
- public void WithOpenTelemetry_ExtensionMethod_CreatesOpenTelemetryAgent()
- {
- // Arrange
- var mockAgent = new Mock();
- mockAgent.Setup(a => a.Id).Returns("test-id");
- mockAgent.Setup(a => a.Name).Returns("TestAgent");
-
- // Act
- using var telemetryAgent = mockAgent.Object.WithOpenTelemetry();
-
- // Assert
- Assert.IsType(telemetryAgent);
- Assert.Equal("test-id", telemetryAgent.Id);
- Assert.Equal("TestAgent", telemetryAgent.Name);
- Assert.False(telemetryAgent.EnableSensitiveData); // Default should be false
- }
-
- [Theory]
- [InlineData(true)]
- [InlineData(false)]
- [InlineData(null)]
- public void WithOpenTelemetry_ExtensionMethodWithEnableSensitiveData_SetsPropertyCorrectly(bool? enableSensitiveData)
- {
- // Arrange
- var mockAgent = new Mock();
- mockAgent.Setup(a => a.Id).Returns("test-id");
- mockAgent.Setup(a => a.Name).Returns("TestAgent");
-
- // Act
- using var telemetryAgent = mockAgent.Object.WithOpenTelemetry(enableSensitiveData: enableSensitiveData);
-
- // Assert
- Assert.IsType(telemetryAgent);
- Assert.Equal("test-id", telemetryAgent.Id);
- Assert.Equal("TestAgent", telemetryAgent.Name);
- Assert.Equal(enableSensitiveData ?? false, telemetryAgent.EnableSensitiveData);
- }
-
- [Fact]
- public void WithOpenTelemetry_ExtensionMethodWithAllParameters_CreatesOpenTelemetryAgentWithCorrectSettings()
- {
- // Arrange
- var mockAgent = new Mock();
- mockAgent.Setup(a => a.Id).Returns("test-id");
- mockAgent.Setup(a => a.Name).Returns("TestAgent");
- var mockLoggerFactory = new Mock();
- var mockLogger = new Mock();
- mockLoggerFactory.Setup(f => f.CreateLogger(It.IsAny()))
- .Returns(mockLogger.Object);
- const string SourceName = "custom-source";
- const bool EnableSensitiveData = true;
-
- // Act
- using var telemetryAgent = mockAgent.Object.WithOpenTelemetry(
- loggerFactory: mockLoggerFactory.Object,
- sourceName: SourceName,
- enableSensitiveData: EnableSensitiveData);
-
- // Assert
- Assert.IsType(telemetryAgent);
- Assert.Equal("test-id", telemetryAgent.Id);
- Assert.Equal("TestAgent", telemetryAgent.Name);
- Assert.True(telemetryAgent.EnableSensitiveData);
- }
-
- [Theory]
- [InlineData(true)]
- [InlineData(false)]
- public async Task WithOpenTelemetry_EnableSensitiveDataParameter_SetsPropertyCorrectlyAsync(bool enableSensitiveData)
- {
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var activities = new List();
- using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
- .AddSource(sourceName)
- .AddInMemoryExporter(activities)
- .Build();
-
- var mockLoggerFactory = new Mock();
- var mockLogger = new Mock();
- mockLoggerFactory.Setup(f => f.CreateLogger(It.IsAny()))
- .Returns(mockLogger.Object);
-
- var mockAgent = CreateMockAgent(false);
-
- // Use the extension method with enableSensitiveData parameter
- using var telemetryAgent = mockAgent.Object.WithOpenTelemetry(
- loggerFactory: mockLoggerFactory.Object,
- sourceName: sourceName,
- enableSensitiveData: enableSensitiveData);
-
- var messages = new List
- {
- new(ChatRole.User, "What's the weather like?"),
- new(ChatRole.Assistant, [
- new TextContent("I'll check the weather for you."),
- new FunctionCallContent("get_weather", "call_123", new Dictionary { ["location"] = "Seattle" })
- ]),
- new(ChatRole.Tool, [
- new FunctionResultContent("call_123", "Sunny, 75°F")
- ])
- };
-
- // Act
- await telemetryAgent.RunAsync(messages);
-
- // Assert
- var activity = Assert.Single(activities);
- Assert.NotNull(activity);
-
- // Verify that EnableSensitiveData property is set correctly
- Assert.Equal(enableSensitiveData, telemetryAgent.EnableSensitiveData);
-
- // Verify that the telemetry agent was created successfully
- Assert.IsType(telemetryAgent);
- }
-
- [Fact]
- public void WithOpenTelemetry_EnableSensitiveDataParameterOverridesDefault_WhenSpecified()
- {
- // Arrange
- var mockAgent = new Mock();
- mockAgent.Setup(a => a.Id).Returns("test-id");
- mockAgent.Setup(a => a.Name).Returns("TestAgent");
-
- // Act & Assert - Test that explicit true overrides default false
- using var telemetryAgentTrue = mockAgent.Object.WithOpenTelemetry(enableSensitiveData: true);
- Assert.True(telemetryAgentTrue.EnableSensitiveData);
-
- // Act & Assert - Test that explicit false is respected
- using var telemetryAgentFalse = mockAgent.Object.WithOpenTelemetry(enableSensitiveData: false);
- Assert.False(telemetryAgentFalse.EnableSensitiveData);
-
- // Act & Assert - Test that null uses default (false)
- using var telemetryAgentDefault = mockAgent.Object.WithOpenTelemetry(enableSensitiveData: null);
- Assert.False(telemetryAgentDefault.EnableSensitiveData);
-
- // Act & Assert - Test that omitting parameter uses default (false)
- using var telemetryAgentOmitted = mockAgent.Object.WithOpenTelemetry();
- Assert.False(telemetryAgentOmitted.EnableSensitiveData);
- }
-
- #region ILogger Tests
-
- ///
- /// Verify that OpenTelemetryAgent constructor accepts ILogger parameter and uses it.
- ///
- [Fact]
- public void Constructor_WithILogger_AcceptsLoggerParameter()
- {
- // Arrange
- var mockAgent = new Mock();
- mockAgent.Setup(a => a.Id).Returns("test-id");
- mockAgent.Setup(a => a.Name).Returns("TestAgent");
- var mockLogger = new Mock();
- const string SourceName = "test-source";
-
- // Act
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object, mockLogger.Object, SourceName);
-
- // Assert
- Assert.Equal("test-id", telemetryAgent.Id);
- Assert.Equal("TestAgent", telemetryAgent.Name);
- }
-
- ///
- /// Verify that OpenTelemetryAgent constructor works with null ILogger parameter.
- ///
- [Fact]
- public void Constructor_WithNullILogger_UsesNullLogger()
- {
- // Arrange
- var mockAgent = new Mock();
- mockAgent.Setup(a => a.Id).Returns("test-id");
- mockAgent.Setup(a => a.Name).Returns("TestAgent");
- const string SourceName = "test-source";
-
- // Act
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object, logger: null, SourceName);
-
- // Assert
- Assert.Equal("test-id", telemetryAgent.Id);
- Assert.Equal("TestAgent", telemetryAgent.Name);
- }
-
- ///
- /// Verify that OpenTelemetryAgent uses the provided ILogger for logging events during RunAsync.
- ///
- [Fact]
- public async Task RunAsync_WithILogger_LogsEventsCorrectlyAsync()
- {
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var mockLogger = new Mock();
-
- // Setup the logger to return true for IsEnabled to ensure logging occurs
- mockLogger.Setup(x => x.IsEnabled(LogLevel.Information)).Returns(true);
-
- var mockAgent = CreateMockAgent(false);
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object, mockLogger.Object, sourceName);
-
- var messages = new List
- {
- new(ChatRole.User, "Test message")
- };
-
- // Act
- await telemetryAgent.RunAsync(messages);
-
- // Assert
- // Verify that the logger was called for logging events
- mockLogger.Verify(
- x => x.Log(
- LogLevel.Information,
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny>()),
- Times.AtLeastOnce);
- }
-
- ///
- /// Verify that OpenTelemetryAgent extension method accepts ILogger parameter.
- ///
- [Fact]
- public void WithOpenTelemetry_ExtensionMethodWithILogger_CreatesOpenTelemetryAgentWithLogger()
- {
- // Arrange
- var mockAgent = new Mock();
- mockAgent.Setup(a => a.Id).Returns("test-id");
- mockAgent.Setup(a => a.Name).Returns("TestAgent");
- var mockLoggerFactory = new Mock();
- var mockLogger = new Mock();
- mockLoggerFactory.Setup(f => f.CreateLogger(It.IsAny()))
- .Returns(mockLogger.Object);
- const string SourceName = "test-source";
-
- // Act
- using var telemetryAgent = mockAgent.Object.WithOpenTelemetry(mockLoggerFactory.Object, SourceName);
-
- // Assert
- Assert.IsType(telemetryAgent);
- Assert.Equal("test-id", telemetryAgent.Id);
- Assert.Equal("TestAgent", telemetryAgent.Name);
- }
-
- #endregion
-
- #region OpenTelemetry Logging Deduplication Tests
-
- ///
- /// Verify that when OpenTelemetryAgent wraps a ChatClientAgent with OpenTelemetry-enabled ChatClient,
- /// logs are not duplicated.
- ///
- [Fact]
- public async Task RunAsync_WithOpenTelemetryChatClientAgent_DoesNotDuplicateLogsAsync()
- {
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var activities = new List();
- using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
- .AddSource(sourceName)
- .AddInMemoryExporter(activities)
- .Build();
-
- var mockLogger = new Mock();
-
- // Setup the logger to return true for IsEnabled to ensure logging occurs
- mockLogger.Setup(x => x.IsEnabled(LogLevel.Information)).Returns(true);
-
- var mockChatClient = new Mock();
-
- // Setup ChatClient to return a response
- mockChatClient.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
- .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Response")));
-
- // Setup ChatClientMetadata
- var metadata = new ChatClientMetadata("openai");
- mockChatClient.Setup(c => c.GetService(typeof(ChatClientMetadata), null))
- .Returns(metadata);
-
- // Create a real OpenTelemetryChatClient to simulate OpenTelemetry-enabled ChatClient
- var openTelemetryChatClient = new OpenTelemetryChatClient(mockChatClient.Object, sourceName: sourceName);
- mockChatClient.Setup(c => c.GetService(typeof(OpenTelemetryChatClient), null)).Returns(openTelemetryChatClient);
-
- var chatClientAgent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
- {
- Id = "chat-agent-id",
- Name = "ChatAgent",
- Instructions = "You are a helpful assistant."
- });
-
- using var telemetryAgent = new OpenTelemetryAgent(chatClientAgent, mockLogger.Object, sourceName);
-
- var messages = new List
- {
- new(ChatRole.User, "Hello")
- };
-
- // Act
- await telemetryAgent.RunAsync(messages);
-
- // Assert
- // Verify that the logger was NOT called because OpenTelemetryChatClient is present (deduplication)
- mockLogger.Verify(
- x => x.Log(
- LogLevel.Information,
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny>()),
- Times.Never);
-
- // Verify that activities were created (indicating telemetry is working)
- Assert.NotEmpty(activities);
-
- // Cleanup
- openTelemetryChatClient.Dispose();
- }
-
- ///
- /// Verify that OpenTelemetryAgent works correctly when wrapping a ChatClientAgent
- /// without OpenTelemetry-enabled ChatClient.
- ///
- [Fact]
- public async Task RunAsync_WithRegularChatClientAgent_LogsCorrectlyAsync()
- {
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var activities = new List();
- using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
- .AddSource(sourceName)
- .AddInMemoryExporter(activities)
- .Build();
-
- var mockLogger = new Mock();
-
- // Setup the logger to return true for IsEnabled to ensure logging occurs
- mockLogger.Setup(x => x.IsEnabled(LogLevel.Information)).Returns(true);
-
- var mockChatClient = new Mock();
-
- // Setup ChatClient to return a response
- mockChatClient.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
- .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Response")));
-
- // Setup ChatClientMetadata
- var metadata = new ChatClientMetadata("openai");
- mockChatClient.Setup(c => c.GetService(typeof(ChatClientMetadata), null))
- .Returns(metadata);
-
- // No OpenTelemetryChatClient setup - simulating regular ChatClient
- mockChatClient.Setup(c => c.GetService(typeof(OpenTelemetryChatClient), null)).Returns((OpenTelemetryChatClient?)null);
-
- var chatClientAgent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
- {
- Id = "chat-agent-id",
- Name = "ChatAgent",
- Instructions = "You are a helpful assistant."
- });
-
- using var telemetryAgent = new OpenTelemetryAgent(chatClientAgent, mockLogger.Object, sourceName);
-
- var messages = new List
- {
- new(ChatRole.User, "Hello")
- };
-
- // Act
- await telemetryAgent.RunAsync(messages);
-
- // Assert
- // Verify that the logger was called
- mockLogger.Verify(
- x => x.Log(
- LogLevel.Information,
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny>()),
- Times.AtLeastOnce);
-
- // Verify that activities were created
- Assert.NotEmpty(activities);
- }
-
- ///
- /// Verify that EnableSensitiveData setting is inherited from OpenTelemetryChatClient when available.
- ///
- [Fact]
- public void Constructor_WithOpenTelemetryChatClient_InheritsEnableSensitiveDataSetting()
- {
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var mockChatClient = new Mock();
-
- // Setup ChatClientMetadata
- var metadata = new ChatClientMetadata("openai");
- mockChatClient.Setup(c => c.GetService(typeof(ChatClientMetadata), null))
- .Returns(metadata);
-
- // Create a real OpenTelemetryChatClient with EnableSensitiveData = true
- var openTelemetryChatClient = new OpenTelemetryChatClient(mockChatClient.Object, sourceName: sourceName)
- {
- EnableSensitiveData = true
- };
- mockChatClient.Setup(c => c.GetService(typeof(OpenTelemetryChatClient), null)).Returns(openTelemetryChatClient);
-
- var chatClientAgent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
- {
- Id = "chat-agent-id",
- Name = "ChatAgent"
- });
-
- // Act
- using var telemetryAgent = new OpenTelemetryAgent(chatClientAgent, sourceName: sourceName);
-
- // Assert
- Assert.True(telemetryAgent.EnableSensitiveData);
-
- // Cleanup
- openTelemetryChatClient.Dispose();
- }
-
- #endregion
-
- #region GetService Method Tests
-
- ///
- /// Verify that GetService returns ActivitySource when requested.
- ///
- [Fact]
- public void GetService_RequestingActivitySource_ReturnsActivitySource()
- {
- // Arrange
- var mockAgent = new Mock();
- mockAgent.Setup(a => a.Id).Returns("test-id");
- const string SourceName = "test-source";
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object, sourceName: SourceName);
-
- // Act
- var result = telemetryAgent.GetService(typeof(ActivitySource));
-
- // Assert
- Assert.NotNull(result);
- Assert.IsType(result);
- var activitySource = (ActivitySource)result;
- Assert.Equal(SourceName, activitySource.Name);
- }
-
- ///
- /// Verify that GetService delegates to inner agent for unknown service types.
- ///
- [Fact]
- public void GetService_RequestingUnknownServiceType_DelegatesToInnerAgent()
- {
- // Arrange
- var mockAgent = new Mock();
- var customService = new object();
- mockAgent.Setup(a => a.GetService(typeof(string), null))
- .Returns(customService);
-
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object);
-
- // Act
- var result = telemetryAgent.GetService(typeof(string));
-
- // Assert
- Assert.Same(customService, result);
- mockAgent.Verify(a => a.GetService(typeof(string), null), Times.Once);
- }
-
- ///
- /// Verify that GetService returns null for unknown service types when inner agent returns null.
- ///
- [Fact]
- public void GetService_RequestingUnknownServiceTypeWithNullFromInnerAgent_ReturnsNull()
- {
- // Arrange
- var mockAgent = new Mock();
- mockAgent.Setup(a => a.GetService(typeof(string), null))
- .Returns((object?)null);
-
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object);
-
- // Act
- var result = telemetryAgent.GetService(typeof(string));
-
- // Assert
- Assert.Null(result);
- mockAgent.Verify(a => a.GetService(typeof(string), null), Times.Once);
- }
-
- ///
- /// Verify that GetService with serviceKey parameter delegates correctly to inner agent.
- ///
- [Fact]
- public void GetService_WithServiceKey_DelegatesToInnerAgent()
- {
- // Arrange
- var mockAgent = new Mock();
- var customService = new object();
- const string ServiceKey = "test-key";
- mockAgent.Setup(a => a.GetService(typeof(string), ServiceKey))
- .Returns(customService);
-
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object);
-
- // Act
- var result = telemetryAgent.GetService(typeof(string), ServiceKey);
-
- // Assert
- Assert.Same(customService, result);
- mockAgent.Verify(a => a.GetService(typeof(string), ServiceKey), Times.Once);
- }
-
- ///
- /// Verify that GetService returns ActivitySource even when inner agent has the same service type.
- ///
- [Fact]
- public void GetService_RequestingActivitySourceWithInnerAgentHavingSameType_ReturnsOpenTelemetryActivitySource()
- {
- // Arrange
- var mockAgent = new Mock();
- var innerActivitySource = new ActivitySource("inner-source");
- mockAgent.Setup(a => a.GetService(typeof(ActivitySource), null))
- .Returns(innerActivitySource);
-
- const string SourceName = "telemetry-source";
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object, sourceName: SourceName);
-
- // Act
- var result = telemetryAgent.GetService(typeof(ActivitySource));
-
- // Assert
- Assert.NotNull(result);
- Assert.IsType(result);
- var activitySource = (ActivitySource)result;
- Assert.Equal(SourceName, activitySource.Name);
- Assert.NotSame(innerActivitySource, result); // Should return OpenTelemetryAgent's ActivitySource, not inner agent's
-
- // Cleanup
- innerActivitySource.Dispose();
- }
-
- ///
- /// Verify that GetService can retrieve AIAgentMetadata from inner agent.
- ///
- [Fact]
- public void GetService_RequestingAIAgentMetadata_DelegatesToInnerAgent()
- {
- // Arrange
- var mockAgent = new Mock();
- var agentMetadata = new AIAgentMetadata("test-provider");
- mockAgent.Setup(a => a.GetService(typeof(AIAgentMetadata), null))
- .Returns(agentMetadata);
-
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object);
-
- // Act
- var result = telemetryAgent.GetService(typeof(AIAgentMetadata));
-
- // Assert
- Assert.Same(agentMetadata, result);
- mockAgent.Verify(a => a.GetService(typeof(AIAgentMetadata), null), Times.AtLeastOnce);
- }
-
- ///
- /// Verify that GetService calls base.GetService() first and returns the agent itself when requesting OpenTelemetryAgent type.
- ///
- [Fact]
- public void GetService_RequestingOpenTelemetryAgentType_ReturnsBaseImplementation()
- {
- // Arrange
- var mockAgent = new Mock();
- mockAgent.Setup(a => a.Id).Returns("test-id");
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object);
-
- // Act
- var result = telemetryAgent.GetService(typeof(OpenTelemetryAgent));
-
- // Assert
- Assert.NotNull(result);
- Assert.Same(telemetryAgent, result);
- // Verify that the inner agent's GetService was not called for this type since base.GetService() handled it
- mockAgent.Verify(a => a.GetService(typeof(OpenTelemetryAgent), null), Times.Never);
- }
-
- ///
- /// Verify that GetService calls base.GetService() first and returns the agent itself when requesting AIAgent type.
- ///
- [Fact]
- public void GetService_RequestingAIAgentType_ReturnsBaseImplementation()
- {
- // Arrange
- var mockAgent = new Mock();
- mockAgent.Setup(a => a.Id).Returns("test-id");
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object);
-
- // Act
- var result = telemetryAgent.GetService(typeof(AIAgent));
-
- // Assert
- Assert.NotNull(result);
- Assert.Same(telemetryAgent, result);
- // Verify that the inner agent's GetService was not called for this type since base.GetService() handled it
- mockAgent.Verify(a => a.GetService(typeof(AIAgent), null), Times.Never);
- }
-
- ///
- /// Verify that GetService calls base.GetService() first but continues to derived logic when base returns null.
- /// For ActivitySource, it returns the agent's own ActivitySource regardless of service key.
- ///
- [Fact]
- public void GetService_RequestingActivitySourceWithServiceKey_ReturnsOwnActivitySource()
- {
- // Arrange
- var mockAgent = new Mock();
- mockAgent.Setup(a => a.Id).Returns("test-id");
- const string SourceName = "test-source";
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object, sourceName: SourceName);
-
- // Act - Request ActivitySource with a service key (base.GetService will return null due to serviceKey)
- var result = telemetryAgent.GetService(typeof(ActivitySource), "some-key");
-
- // Assert
- Assert.NotNull(result);
- Assert.IsType(result);
- var activitySource = (ActivitySource)result;
- Assert.Equal(SourceName, activitySource.Name);
- // Verify that the inner agent's GetService was NOT called because ActivitySource is handled by the telemetry agent itself
- mockAgent.Verify(a => a.GetService(typeof(ActivitySource), "some-key"), Times.Never);
- }
-
- ///
- /// Verify that GetService calls base.GetService() first but continues to inner agent when base returns null and it's not ActivitySource.
- ///
- [Fact]
- public void GetService_RequestingUnknownServiceWithServiceKey_CallsInnerAgent()
- {
- // Arrange
- var mockAgent = new Mock();
- mockAgent.Setup(a => a.Id).Returns("test-id");
- mockAgent.Setup(a => a.GetService(typeof(string), "some-key")).Returns("test-result");
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object);
-
- // Act - Request string with a service key (base.GetService will return null due to serviceKey)
- var result = telemetryAgent.GetService(typeof(string), "some-key");
-
- // Assert
- Assert.NotNull(result);
- Assert.Equal("test-result", result);
- // Verify that the inner agent's GetService was called after base.GetService() returned null
- mockAgent.Verify(a => a.GetService(typeof(string), "some-key"), Times.Once);
- }
-
- ///
- /// Verify that OpenTelemetryAgent delegates AIAgentMetadata requests to inner agent.
- ///
- [Fact]
- public void GetService_RequestingAIAgentMetadata_DelegatesToInnerAgentWithChatClientAgent()
- {
- // Arrange
- var mockChatClient = new Mock();
- var chatClientMetadata = new ChatClientMetadata("test-provider");
- mockChatClient.Setup(c => c.GetService(typeof(ChatClientMetadata), null))
- .Returns(chatClientMetadata);
-
- var chatClientAgent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
- {
- Instructions = "Test instructions"
- });
-
- using var telemetryAgent = new OpenTelemetryAgent(chatClientAgent);
-
- // Act
- var result = telemetryAgent.GetService(typeof(AIAgentMetadata));
-
- // Assert
- Assert.NotNull(result);
- Assert.IsType(result);
- var agentMetadata = (AIAgentMetadata)result;
- Assert.Equal("test-provider", agentMetadata.ProviderName);
- }
-
- ///
- /// Verify that when OpenTelemetryAgent wraps a ChatClientAgent, the AIAgentMetadata.ProviderName
- /// from ChatClientMetadata is correctly reflected in OpenTelemetry activities.
- ///
- [Theory]
- [InlineData("openai")]
- [InlineData("azure")]
- [InlineData("anthropic")]
- [InlineData("custom-provider")]
- public async Task RunAsync_WithChatClientAgent_ProviderNameReflectedInTelemetryAsync(string providerName)
- {
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var activities = new List();
- using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
- .AddSource(sourceName)
- .AddInMemoryExporter(activities)
- .Build();
-
- var mockChatClient = new Mock();
- var chatClientMetadata = new ChatClientMetadata(providerName);
- mockChatClient.Setup(c => c.GetService(typeof(ChatClientMetadata), null))
- .Returns(chatClientMetadata);
- mockChatClient.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
- .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Response")));
-
- var chatClientAgent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
- {
- Instructions = "Test instructions"
- });
-
- using var telemetryAgent = new OpenTelemetryAgent(chatClientAgent, sourceName: sourceName);
-
- var messages = new List
- {
- new(ChatRole.User, "Test message")
- };
-
- // Act
- await telemetryAgent.RunAsync(messages);
-
- // Assert
- var activity = Assert.Single(activities);
-
- // Verify that the provider name from ChatClientMetadata appears in telemetry
- Assert.Equal(providerName, activity.GetTagItem(OpenTelemetryConsts.GenAI.SystemName));
-
- // Verify that GetService returns the same provider name
- var agentMetadata = telemetryAgent.GetService(typeof(AIAgentMetadata)) as AIAgentMetadata;
- Assert.NotNull(agentMetadata);
- Assert.Equal(providerName, agentMetadata.ProviderName);
- }
-
- ///
- /// Verify that when OpenTelemetryAgent wraps a ChatClientAgent with null ChatClientMetadata,
- /// the system defaults to "Microsoft.Extensions.AI" in telemetry.
- ///
- [Fact]
- public async Task RunAsync_WithChatClientAgent_NullMetadata_DefaultsToMEAIAsync()
- {
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var activities = new List();
- using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
- .AddSource(sourceName)
- .AddInMemoryExporter(activities)
- .Build();
-
- var mockChatClient = new Mock();
- // Setup ChatClient to return null metadata
- mockChatClient.Setup(c => c.GetService(typeof(ChatClientMetadata), null))
- .Returns((ChatClientMetadata?)null);
- mockChatClient.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
- .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Response")));
-
- var chatClientAgent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
- {
- Instructions = "Test instructions"
- });
-
- using var telemetryAgent = new OpenTelemetryAgent(chatClientAgent, sourceName: sourceName);
-
- var messages = new List
- {
- new(ChatRole.User, "Test message")
- };
-
- // Act
- await telemetryAgent.RunAsync(messages);
-
- // Assert
- var activity = Assert.Single(activities);
-
- // Verify that the system defaults to "microsoft.agents.ai" when no metadata is available
- Assert.Equal("microsoft.agents.ai", activity.GetTagItem(OpenTelemetryConsts.GenAI.SystemName));
-
- // Verify that GetService returns null provider name
- var agentMetadata = telemetryAgent.GetService(typeof(AIAgentMetadata)) as AIAgentMetadata;
- Assert.NotNull(agentMetadata);
- Assert.Null(agentMetadata.ProviderName);
- }
-
- ///
- /// Verify that when OpenTelemetryAgent wraps a non-ChatClientAgent with custom metadata,
- /// the custom provider name is correctly reflected in telemetry.
- ///
- [Fact]
- public async Task RunAsync_WithCustomAgent_CustomMetadata_ReflectedInTelemetryAsync()
- {
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var activities = new List();
- using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
- .AddSource(sourceName)
- .AddInMemoryExporter(activities)
- .Build();
-
- const string CustomProviderName = "custom-ai-provider";
- var mockAgent = new Mock();
- var customMetadata = new AIAgentMetadata(CustomProviderName);
-
- // Setup mock agent to return custom metadata
- mockAgent.Setup(a => a.GetService(typeof(AIAgentMetadata), null))
- .Returns(customMetadata);
- mockAgent.Setup(a => a.RunAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
- .ReturnsAsync(new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Custom response")));
-
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object, sourceName: sourceName);
-
- var messages = new List
- {
- new(ChatRole.User, "Test message")
- };
-
- // Act
- await telemetryAgent.RunAsync(messages);
-
- // Assert
- var activity = Assert.Single(activities);
-
- // Verify that the custom provider name appears in telemetry
- Assert.Equal(CustomProviderName, activity.GetTagItem(OpenTelemetryConsts.GenAI.SystemName));
-
- // Verify that GetService returns the same custom provider name
- var agentMetadata = telemetryAgent.GetService(typeof(AIAgentMetadata)) as AIAgentMetadata;
- Assert.NotNull(agentMetadata);
- Assert.Equal(CustomProviderName, agentMetadata.ProviderName);
- }
-
- ///
- /// Verify that when OpenTelemetryAgent wraps a non-ChatClientAgent with no metadata,
- /// the system defaults to "Microsoft.Extensions.AI" in telemetry.
- ///
- [Fact]
- public async Task RunAsync_WithCustomAgent_NoMetadata_DefaultsToMEAIAsync()
- {
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var activities = new List();
- using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
- .AddSource(sourceName)
- .AddInMemoryExporter(activities)
- .Build();
-
- var mockAgent = new Mock();
-
- // Setup mock agent to return null metadata
- mockAgent.Setup(a => a.GetService(typeof(AIAgentMetadata), null))
- .Returns((AIAgentMetadata?)null);
- mockAgent.Setup(a => a.RunAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
- .ReturnsAsync(new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Response")));
-
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object, sourceName: sourceName);
-
- var messages = new List
- {
- new(ChatRole.User, "Test message")
- };
-
- // Act
- await telemetryAgent.RunAsync(messages);
-
- // Assert
- var activity = Assert.Single(activities);
-
- // Verify that the system defaults to "microsoft.agents.ai" when no metadata is available
- Assert.Equal("microsoft.agents.ai", activity.GetTagItem(OpenTelemetryConsts.GenAI.SystemName));
-
- // Verify that GetService returns null for metadata
- var agentMetadata = telemetryAgent.GetService(typeof(AIAgentMetadata)) as AIAgentMetadata;
- Assert.Null(agentMetadata);
- }
-
- ///
- /// Verify that streaming operations also correctly reflect provider name in telemetry.
- ///
- [Theory]
- [InlineData("openai")]
- [InlineData("azure")]
- [InlineData("anthropic")]
- public async Task RunStreamingAsync_WithChatClientAgent_ProviderNameReflectedInTelemetryAsync(string providerName)
- {
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var activities = new List();
- using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
- .AddSource(sourceName)
- .AddInMemoryExporter(activities)
- .Build();
-
- var mockChatClient = new Mock();
- var chatClientMetadata = new ChatClientMetadata(providerName);
- mockChatClient.Setup(c => c.GetService(typeof(ChatClientMetadata), null))
- .Returns(chatClientMetadata);
-
- ChatResponseUpdate[] returnUpdates =
- [
- new ChatResponseUpdate(role: ChatRole.Assistant, content: "Hello"),
- new ChatResponseUpdate(role: ChatRole.Assistant, content: " World"),
- ];
-
- mockChatClient.Setup(c => c.GetStreamingResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
- .Returns(returnUpdates.ToAsyncEnumerable());
-
- var chatClientAgent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
- {
- Instructions = "Test instructions"
- });
-
- using var telemetryAgent = new OpenTelemetryAgent(chatClientAgent, sourceName: sourceName);
-
- var messages = new List
- {
- new(ChatRole.User, "Test message")
- };
-
- // Act
- await foreach (var update in telemetryAgent.RunStreamingAsync(messages))
- {
- // Process updates
- }
-
- // Assert
- var activity = Assert.Single(activities);
-
- // Verify that the provider name from ChatClientMetadata appears in telemetry
- Assert.Equal(providerName, activity.GetTagItem(OpenTelemetryConsts.GenAI.SystemName));
-
- // Verify that GetService returns the same provider name
- var agentMetadata = telemetryAgent.GetService(typeof(AIAgentMetadata)) as AIAgentMetadata;
- Assert.NotNull(agentMetadata);
- Assert.Equal(providerName, agentMetadata.ProviderName);
- }
-
- ///
- /// Verify that provider name consistency is maintained across multiple RunAsync calls.
- ///
- [Fact]
- public async Task RunAsync_MultipleCallsWithSameAgent_ConsistentProviderNameAsync()
- {
- // Arrange
- var sourceName = Guid.NewGuid().ToString("N");
- var activities = new List();
- using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
- .AddSource(sourceName)
- .AddInMemoryExporter(activities)
- .Build();
-
- const string ProviderName = "consistent-provider";
- var mockChatClient = new Mock();
- var chatClientMetadata = new ChatClientMetadata(ProviderName);
- mockChatClient.Setup(c => c.GetService(typeof(ChatClientMetadata), null))
- .Returns(chatClientMetadata);
- mockChatClient.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
- .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Response")));
-
- var chatClientAgent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
- {
- Instructions = "Test instructions"
- });
-
- using var telemetryAgent = new OpenTelemetryAgent(chatClientAgent, sourceName: sourceName);
-
- var messages = new List
- {
- new(ChatRole.User, "Test message")
- };
-
- // Act - Make multiple calls
- await telemetryAgent.RunAsync(messages);
- await telemetryAgent.RunAsync(messages);
- await telemetryAgent.RunAsync(messages);
-
- // Assert
- Assert.Equal(3, activities.Count);
-
- // Verify that all activities have the same provider name
- foreach (var activity in activities)
- {
- Assert.Equal(ProviderName, activity.GetTagItem(OpenTelemetryConsts.GenAI.SystemName));
- }
-
- // Verify that GetService consistently returns the same provider name
- var agentMetadata1 = telemetryAgent.GetService(typeof(AIAgentMetadata)) as AIAgentMetadata;
- var agentMetadata2 = telemetryAgent.GetService(typeof(AIAgentMetadata)) as AIAgentMetadata;
- Assert.NotNull(agentMetadata1);
- Assert.NotNull(agentMetadata2);
- Assert.Equal(ProviderName, agentMetadata1.ProviderName);
- Assert.Equal(ProviderName, agentMetadata2.ProviderName);
- Assert.Same(agentMetadata1, agentMetadata2); // Should be cached
- }
-
- #endregion
-
- [Fact]
- public async Task RunAsync_NoListeners_NoActivitiesCreatedAsync()
- {
- // Arrange - No tracer provider, so no listeners
- var mockAgent = CreateMockAgent(false);
- using var telemetryAgent = new OpenTelemetryAgent(mockAgent.Object, sourceName: "test-source");
-
- var messages = new List
- {
- new(ChatRole.User, "Hello")
- };
-
- // Act
- await telemetryAgent.RunAsync(messages);
-
- // Assert - Should complete without creating activities
- mockAgent.Verify(a => a.RunAsync(messages, null, null, It.IsAny()), Times.Once);
- }
-
- private static Mock CreateMockAgent(bool throwError)
- {
- var mockAgent = new Mock();
- mockAgent.Setup(a => a.Id).Returns("test-agent-id");
- mockAgent.Setup(a => a.Name).Returns("TestAgent");
- mockAgent.Setup(a => a.Description).Returns("Test Description");
-
- if (throwError)
- {
- mockAgent.Setup(a => a.RunAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
- .ThrowsAsync(new InvalidOperationException("Test error"));
- }
- else
- {
- var response = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Test response"))
+ yield return new AgentRunResponseUpdate
{
- ResponseId = "test-response-id",
- Usage = new UsageDetails
+ Contents = [new UsageContent(new()
{
InputTokenCount = 10,
- OutputTokenCount = 20
- }
+ OutputTokenCount = 20,
+ TotalTokenCount = 42,
+ })],
+ AdditionalProperties = new()
+ {
+ ["system_fingerprint"] = "abcdefgh",
+ ["AndSomethingElse"] = "value2",
+ },
};
-
- mockAgent.Setup(a => a.RunAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
- .ReturnsAsync(response);
}
- return mockAgent;
- }
+ using var agent = new OpenTelemetryAgent(innerAgent, sourceName) { EnableSensitiveData = enableSensitiveData };
- private static Mock CreateMockStreamingAgent(bool throwError)
- {
- var mockAgent = new Mock();
- mockAgent.Setup(a => a.Id).Returns("test-agent-id");
- mockAgent.Setup(a => a.Name).Returns("TestAgent");
- mockAgent.Setup(a => a.Description).Returns("Test Description");
+ List messages =
+ [
+ new(ChatRole.System, "You are a close friend."),
+ new(ChatRole.User, "Hey!"),
+ new(ChatRole.Assistant, [new FunctionCallContent("12345", "GetPersonName")]),
+ new(ChatRole.Tool, [new FunctionResultContent("12345", "John")]),
+ new(ChatRole.Assistant, "Hey John, what's up?"),
+ new(ChatRole.User, "What's the biggest animal?")
+ ];
- if (throwError)
+ if (streaming)
{
- mockAgent.Setup(a => a.RunStreamingAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(ThrowingAsyncEnumerableAsync());
+ await foreach (var update in agent.RunStreamingAsync(messages))
+ {
+ await Task.Yield();
+ }
}
else
{
- mockAgent.Setup(a => a.RunStreamingAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(CreateStreamingResponseAsync());
+ await agent.RunAsync(messages);
}
- return mockAgent;
+ var activity = Assert.Single(activities);
- static async IAsyncEnumerable ThrowingAsyncEnumerableAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
+ Assert.NotNull(activity.Id);
+ Assert.NotEmpty(activity.Id);
+
+ Assert.Equal("localhost", activity.GetTagItem("server.address"));
+ Assert.Equal(12345, (int)activity.GetTagItem("server.port")!);
+
+ Assert.Equal("invoke_agent TestAgent", activity.DisplayName);
+ Assert.Equal("TestAgentProviderFromAIAgentMetadata", activity.GetTagItem("gen_ai.provider.name"));
+ Assert.Equal(innerAgent.Name, activity.GetTagItem("gen_ai.agent.name"));
+ Assert.Equal(innerAgent.Id, activity.GetTagItem("gen_ai.agent.id"));
+ Assert.Equal(innerAgent.Description, activity.GetTagItem("gen_ai.agent.description"));
+
+ Assert.Equal("amazingmodel", activity.GetTagItem("gen_ai.request.model"));
+
+ Assert.Equal("id123", activity.GetTagItem("gen_ai.response.id"));
+ Assert.Equal(10, activity.GetTagItem("gen_ai.usage.input_tokens"));
+ Assert.Equal(20, activity.GetTagItem("gen_ai.usage.output_tokens"));
+ Assert.Equal(enableSensitiveData ? "abcdefgh" : null, activity.GetTagItem("system_fingerprint"));
+ Assert.Equal(enableSensitiveData ? "value2" : null, activity.GetTagItem("AndSomethingElse"));
+
+ Assert.True(activity.Duration.TotalMilliseconds > 0);
+
+ var tags = activity.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
+ if (enableSensitiveData)
{
- await Task.Yield();
- if (Environment.ProcessorCount > 0) // always true
- {
- throw new InvalidOperationException("Streaming error");
- }
+ Assert.Equal(ReplaceWhitespace("""
+ [
+ {
+ "role": "system",
+ "parts": [
+ {
+ "type": "text",
+ "content": "You are a close friend."
+ }
+ ]
+ },
+ {
+ "role": "user",
+ "parts": [
+ {
+ "type": "text",
+ "content": "Hey!"
+ }
+ ]
+ },
+ {
+ "role": "assistant",
+ "parts": [
+ {
+ "type": "tool_call",
+ "id": "12345",
+ "name": "GetPersonName"
+ }
+ ]
+ },
+ {
+ "role": "tool",
+ "parts": [
+ {
+ "type": "tool_call_response",
+ "id": "12345",
+ "response": "John"
+ }
+ ]
+ },
+ {
+ "role": "assistant",
+ "parts": [
+ {
+ "type": "text",
+ "content": "Hey John, what's up?"
+ }
+ ]
+ },
+ {
+ "role": "user",
+ "parts": [
+ {
+ "type": "text",
+ "content": "What's the biggest animal?"
+ }
+ ]
+ }
+ ]
+ """), ReplaceWhitespace(tags["gen_ai.input.messages"]));
- yield break;
+ Assert.Equal(ReplaceWhitespace("""
+ [
+ {
+ "role": "assistant",
+ "parts": [
+ {
+ "type": "text",
+ "content": "The blue whale, I think."
+ }
+ ]
+ }
+ ]
+ """), ReplaceWhitespace(tags["gen_ai.output.messages"]));
+ }
+ else
+ {
+ Assert.False(tags.ContainsKey("gen_ai.input.messages"));
+ Assert.False(tags.ContainsKey("gen_ai.output.messages"));
}
- static async IAsyncEnumerable CreateStreamingResponseAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
+ Assert.False(tags.ContainsKey("gen_ai.system_instructions"));
+ Assert.False(tags.ContainsKey("gen_ai.tool.definitions"));
+ }
+
+ public static IEnumerable