Update sample to also include function calling telemetry (#577)

This commit is contained in:
Roger Barreto
2025-09-01 18:19:00 +01:00
committed by GitHub
Unverified
parent cf0e779638
commit 19210fc5c9
2 changed files with 58 additions and 44 deletions
+46 -42
View File
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using Azure.AI.OpenAI;
@@ -8,13 +9,14 @@ using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenAI;
using OpenTelemetry;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
#region Setup Telemetry
const string SourceName = "OpenTelemetryAspire.ConsoleApp";
const string ServiceName = "AgentOpenTelemetry";
@@ -24,7 +26,7 @@ AppContext.SetSwitch("Microsoft.Extensions.AI.Agents.EnableTelemetry", true);
// Configure OpenTelemetry for Aspire dashboard
var otlpEndpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT") ?? "http://localhost:4318";
// Create a resource to identify this service (like Python example)
// Create a resource to identify this service
var resource = ResourceBuilder.CreateDefault()
.AddService(ServiceName, serviceVersion: "1.0.0")
.AddAttributes(new Dictionary<string, object>
@@ -40,23 +42,17 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource(SourceName) // Our custom activity source
.AddSource("Microsoft.Extensions.AI.Agents") // Agent Framework telemetry
.AddHttpClientInstrumentation() // Capture HTTP calls to OpenAI
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri(otlpEndpoint);
})
.AddOtlpExporter(options => { options.Endpoint = new Uri(otlpEndpoint); })
.Build();
// Setup metrics with resource and instrument name filtering (like Python example)
// Setup metrics with resource and instrument name filtering
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"))
.AddMeter(SourceName) // Our custom meter
.AddMeter("Microsoft.Extensions.AI.Agents") // Agent Framework metrics
.AddHttpClientInstrumentation() // HTTP client metrics
.AddRuntimeInstrumentation() // .NET runtime metrics
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri(otlpEndpoint);
})
.AddOtlpExporter(options => { options.Endpoint = new Uri(otlpEndpoint); })
.Build();
// Setup structured logging with OpenTelemetry
@@ -74,16 +70,19 @@ serviceCollection.AddLogging(loggingBuilder => loggingBuilder
options.IncludeFormattedMessage = true;
}));
var serviceProvider = serviceCollection.BuildServiceProvider();
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
using var activitySource = new ActivitySource(SourceName);
using var meter = new Meter(SourceName);
// Create custom metrics (similar to Python example)
// Create custom metrics
var interactionCounter = meter.CreateCounter<int>("agent_interactions_total", description: "Total number of agent interactions");
var responseTimeHistogram = meter.CreateHistogram<double>("agent_response_time_seconds", description: "Agent response time in seconds");
#endregion
var serviceProvider = serviceCollection.BuildServiceProvider();
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var appLogger = loggerFactory.CreateLogger<Program>();
Console.WriteLine("""
=== OpenTelemetry Aspire Demo ===
This demo shows OpenTelemetry integration with the Agent Framework.
@@ -91,34 +90,39 @@ Console.WriteLine("""
Type your message and press Enter. Type 'exit' or empty message to quit.
""");
// Log application startup
logger.LogInformation("OpenTelemetry Aspire Demo application started");
logger.LogInformation("OTLP endpoint configured: {OtlpEndpoint}", otlpEndpoint);
logger.LogDebug("Service name: {ServiceName}, Source name: {SourceName}", ServiceName, SourceName);
// Create the chat client
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT environment variable is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
logger.LogInformation("Initializing Azure OpenAI client with endpoint: {Endpoint}", endpoint);
logger.LogDebug("Using deployment: {DeploymentName}", deploymentName);
// Log application startup
appLogger.LogInformation("OpenTelemetry Aspire Demo application started");
// Create a logger specifically for the agent
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
[Description("Get the weather for a given location.")]
static async Task<string> GetWeather([Description("The location to get the weather for.")] string location)
{
await Task.Delay(2000);
return $"The weather in {location} is cloudy with a high of 15°C.";
}
// Create the agent with OpenTelemetry instrumentation
logger.LogInformation("Creating Agent with OpenTelemetry instrumentation");
// To ensure chat client's function calling is captured in the open telemetry, the chat client needs to have UseOpenTelemetry after UseFunctionInvocation
using var instrumentedChatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
.GetChatClient(deploymentName)
.AsIChatClient() // Converts a native OpenAI SDK ChatClient into a Microsoft.Extensions.AI.IChatClient
.AsBuilder()
.UseFunctionInvocation()
.UseOpenTelemetry(loggerFactory: loggerFactory, sourceName: SourceName, (cfg) => { cfg.EnableSensitiveData = true; })
.Build();
using var agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(
appLogger.LogInformation("Creating Agent with OpenTelemetry instrumentation");
// Create the agent with the instrumented chat client
using var agent = new ChatClientAgent(instrumentedChatClient,
name: "OpenTelemetryDemoAgent",
instructions: "You are a helpful assistant that provides concise and informative responses.")
.WithOpenTelemetry(loggerFactory, SourceName);
instructions: "You are a helpful assistant that provides concise and informative responses.",
tools: [AIFunctionFactory.Create(GetWeather)])
.WithOpenTelemetry(loggerFactory, SourceName); // Enable telemetry on the agent
var thread = agent.GetNewThread();
logger.LogInformation("Agent created successfully with ID: {AgentId}", agent.Id);
appLogger.LogInformation("Agent created successfully with ID: {AgentId}", agent.Id);
// Create a parent span for the entire agent session
using var sessionActivity = activitySource.StartActivity("Agent Session");
@@ -127,8 +131,8 @@ sessionActivity?.SetTag("agent.name", "OpenTelemetryDemoAgent");
sessionActivity?.SetTag("session.id", sessionId);
sessionActivity?.SetTag("session.start_time", DateTimeOffset.UtcNow.ToString("O"));
logger.LogInformation("Starting agent session with ID: {SessionId}", sessionId);
using (logger.BeginScope(new Dictionary<string, object> { ["SessionId"] = sessionId, ["AgentName"] = "OpenTelemetryDemoAgent" }))
appLogger.LogInformation("Starting agent session with ID: {SessionId}", sessionId);
using (appLogger.BeginScope(new Dictionary<string, object> { ["SessionId"] = sessionId, ["AgentName"] = "OpenTelemetryDemoAgent" }))
{
var interactionCount = 0;
@@ -139,12 +143,12 @@ using (logger.BeginScope(new Dictionary<string, object> { ["SessionId"] = sessio
if (string.IsNullOrWhiteSpace(userInput) || userInput.Equals("exit", StringComparison.OrdinalIgnoreCase))
{
logger.LogInformation("User requested to exit the session");
appLogger.LogInformation("User requested to exit the session");
break;
}
interactionCount++;
logger.LogInformation("Processing user interaction #{InteractionNumber}: {UserInput}", interactionCount, userInput);
appLogger.LogInformation("Processing user interaction #{InteractionNumber}: {UserInput}", interactionCount, userInput);
// Create a child span for each individual interaction
using var activity = activitySource.StartActivity("Agent Interaction");
@@ -156,7 +160,7 @@ using (logger.BeginScope(new Dictionary<string, object> { ["SessionId"] = sessio
try
{
logger.LogDebug("Starting agent execution for interaction #{InteractionNumber}", interactionCount);
appLogger.LogDebug("Starting agent execution for interaction #{InteractionNumber}", interactionCount);
Console.Write("Agent: ");
// Run the agent (this will create its own internal telemetry spans)
@@ -177,7 +181,7 @@ using (logger.BeginScope(new Dictionary<string, object> { ["SessionId"] = sessio
activity?.SetTag("response.success", true);
logger.LogInformation("Agent interaction #{InteractionNumber} completed successfully in {ResponseTime:F2} seconds",
appLogger.LogInformation("Agent interaction #{InteractionNumber} completed successfully in {ResponseTime:F2} seconds",
interactionCount, responseTime);
}
catch (Exception ex)
@@ -197,7 +201,7 @@ using (logger.BeginScope(new Dictionary<string, object> { ["SessionId"] = sessio
activity?.SetTag("error.message", ex.Message);
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
logger.LogError(ex, "Agent interaction #{InteractionNumber} failed after {ResponseTime:F2} seconds: {ErrorMessage}",
appLogger.LogError(ex, "Agent interaction #{InteractionNumber} failed after {ResponseTime:F2} seconds: {ErrorMessage}",
interactionCount, responseTime, ex.Message);
}
}
@@ -206,7 +210,7 @@ using (logger.BeginScope(new Dictionary<string, object> { ["SessionId"] = sessio
sessionActivity?.SetTag("session.total_interactions", interactionCount);
sessionActivity?.SetTag("session.end_time", DateTimeOffset.UtcNow.ToString("O"));
logger.LogInformation("Agent session completed. Total interactions: {TotalInteractions}", interactionCount);
appLogger.LogInformation("Agent session completed. Total interactions: {TotalInteractions}", interactionCount);
} // End of logging scope
logger.LogInformation("OpenTelemetry Aspire Demo application shutting down");
appLogger.LogInformation("OpenTelemetry Aspire Demo application shutting down");
@@ -18,7 +18,7 @@ internal static class ChatClientExtensions
chatBuilder.UseAgentInvocation();
}
if (chatClient.GetService<NewFunctionInvokingChatClient>() is null)
if (chatClient.GetService<FunctionInvokingChatClient>() is null && chatClient.GetService<NewFunctionInvokingChatClient>() is null)
{
_ = chatBuilder.Use((IChatClient innerClient, IServiceProvider services) =>
{
@@ -33,7 +33,17 @@ internal static class ChatClientExtensions
if (options?.ChatOptions?.Tools is { Count: > 0 })
{
// When tools are provided in the constructor, set the tools for the whole lifecycle of the chat client
agentChatClient.GetService<NewFunctionInvokingChatClient>()!.AdditionalTools = options.ChatOptions.Tools;
var newFunctionService = agentChatClient.GetService<NewFunctionInvokingChatClient>();
var oldFunctionService = agentChatClient.GetService<FunctionInvokingChatClient>();
if (newFunctionService is not null)
{
newFunctionService.AdditionalTools = options.ChatOptions.Tools;
}
else
{
oldFunctionService!.AdditionalTools = options.ChatOptions.Tools;
}
}
return agentChatClient;