-
Notifications
You must be signed in to change notification settings - Fork 16
feat: OpenTelemetry instrumentation + Agent365 baggage support #475
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+2,421
−137
Merged
Changes from 30 commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
4ad3ff5
feat: add OpenTelemetry instrumentation + Agent365 baggage support
rido-min 1e8f696
Refactor telemetry naming; add OpenTelemetry & OpenAI
rido-min b1612cb
configuring token manager
rido-min 3a483e7
Update to .NET 10, OpenTelemetry 1.0.2, improve Agent365
rido-min 37ab50c
Add InvokeAgentScope for Agent365 telemetry integration
rido-min 6318b74
temp otel debugging
rido-min f246f1a
Refactor bot logic into ObservabilityBotApp class
rido-min ff6c4f5
wip
rido-min e603538
Merge branch 'main' into feature/observability-otel-baggage
rido-min 4228e91
fix after merge
rido-min 54f6f90
wip
rido-min f46d188
Merge branch 'main' into feature/observability-otel-baggage
rido-min 452375b
Remove calendarTools/teams clients and local OpenTelemetry ref
rido-min 4f188fd
fix: set Error status on router span exceptions; preserve TenantId in…
rido-min a0cb054
docs: align Observability-Design.md with actual code
rido-min 17646ad
feat(apps): add router-level handler metrics to Microsoft.Teams.Apps …
rido-min 7f9bc09
Merge branch 'main' into feature/observability-otel-baggage
rido-min 174130c
fix: address PR review comments for observability instrumentation
rido-min 7a06dd2
fix: address remaining PR review comments
rido-min 41a5e93
fix: revert unrelated PABot dependency changes
Copilot 6bc1e37
Replace runtime version lookup with compile-time constant
rido-min 00062de
Merge branch 'feature/observability-otel-baggage' of https://github.c…
rido-min 47db33a
Merge remote-tracking branch 'origin/main' into feature/observability…
rido-min 6d38558
fix: resolve TenantId hiding after merge with main
rido-min 098ab0e
code clean up
rido-min 0d8987d
Merge branch 'main' into feature/observability-otel-baggage
rido-min 9ed8e8d
fix: address PR review comments from latest Copilot review
rido-min 0683fa4
Merge branch 'main' into feature/observability-otel-baggage
rido-min 12ceab7
Merge branch 'main' into feature/observability-otel-baggage
rido-min 8b9a1b5
Update dependencies and refactor Agent365 token resolver
rido-min 3b2b090
Add launchSettings template with env vars for ElCanario
rido-min 1a9458c
Merge branch 'main' into feature/observability-otel-baggage
rido-min dc5e5c4
Refactor ObservabilityBotApp constructor and citation loop
rido-min c6ff508
Simplify the observability and baggage-handling infrastructure (#544)
MehakBindra 279dac3
Merge branch 'main' into feature/observability-otel-baggage
rido-min 2c4af92
Update async calls for cancellation and minor refactoring
rido-min 2f76767
Add early return for existing TeamsConversation instances
rido-min 2935bb5
Simplify Agent365 baggage to Apps-only TeamsBaggageBuilder
rido-min 3382c03
Refactor clients, logging, and cleanup unused usings
rido-min File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.OpenTelemetry" Version="1.0.3" /> | ||
| <PackageReference Include="OpenTelemetry.Api" Version="1.15.3" /> | ||
| <PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.15.3" /> | ||
| <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" /> | ||
| <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" /> | ||
| <PackageReference Include="Microsoft.Extensions.AI" Version="10.6.0" /> | ||
| <PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.6.0" /> | ||
| <PackageReference Include="Azure.AI.OpenAI" Version="2.1.0" /> | ||
| <PackageReference Include="ModelContextProtocol" Version="1.3.0" /> | ||
| <PackageReference Include="Azure.Monitor.OpenTelemetry.AspNetCore" Version="1.5.0" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\src\Microsoft.Teams.Apps\Microsoft.Teams.Apps.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Collections.Concurrent; | ||
| using System.Text.Json; | ||
| using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; | ||
| using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes; | ||
| using Microsoft.Extensions.AI; | ||
| using Microsoft.Teams.Apps; | ||
| using Microsoft.Teams.Apps.Api.Clients; | ||
| using Microsoft.Teams.Apps.Handlers; | ||
| using Microsoft.Teams.Apps.Schema; | ||
| using Microsoft.Teams.Apps.Schema.Entities; | ||
| using Microsoft.Teams.Core; | ||
| using Microsoft.Teams.Core.Hosting; | ||
|
|
||
|
rido-min marked this conversation as resolved.
|
||
| namespace ObservabilityBot; | ||
|
|
||
| public class ObservabilityBotApp : TeamsBotApplication | ||
| { | ||
| private readonly IChatClient _chatClient; | ||
| private readonly ChatOptions _chatOptions; | ||
| private readonly ConcurrentDictionary<string, List<ChatMessage>> _chatHistories = new(); | ||
| private readonly string _deploymentName; | ||
|
|
||
| public ObservabilityBotApp( | ||
| ConversationClient conversationClient, | ||
| UserTokenClient userTokenClient, | ||
| ApiClient teamsApiClient, | ||
| IHttpContextAccessor httpContextAccessor, | ||
| ILogger<ObservabilityBotApp> logger, | ||
| IChatClient chatClient, | ||
| ChatOptions chatOptions, | ||
| BotApplicationOptions? options = null, | ||
| TeamsBotApplicationOptions? teamsOptions = null) | ||
| : base(conversationClient, userTokenClient, teamsApiClient, httpContextAccessor, logger, options, teamsOptions) | ||
| { | ||
| _chatClient = chatClient; | ||
| _chatOptions = chatOptions; | ||
| _deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "unknown"; | ||
|
|
||
| this.OnMessage(HandleMessageAsync); | ||
| } | ||
|
|
||
| private async Task HandleMessageAsync(Context<MessageActivity> context, CancellationToken ct) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(context.Activity); | ||
| ArgumentNullException.ThrowIfNull(context.Activity.Conversation); | ||
| ArgumentNullException.ThrowIfNull(context.Activity.Conversation.Id); | ||
|
|
||
| await context.Typing(string.Empty, ct); | ||
|
|
||
| var conversationId = context.Activity.Conversation.Id; | ||
| var history = _chatHistories.GetOrAdd(conversationId, _ => []); | ||
|
|
||
| lock (history) | ||
| { | ||
| history.Add(new ChatMessage(ChatRole.User, context.Activity.Text)); | ||
| } | ||
|
|
||
| // Build Agent365 scope contracts from the turn context. | ||
| var recipient = context.Activity.Recipient; | ||
| var agentDetails = new AgentDetails( | ||
| agentId: recipient?.AgenticAppId ?? recipient?.Id, | ||
| agentName: recipient?.Name, | ||
| agenticUserId: recipient?.AgenticUserId, | ||
| agentBlueprintId: recipient?.AgenticAppBlueprintId, | ||
| tenantId: recipient?.TenantId); | ||
|
|
||
| var request = new Request( | ||
| content: context.Activity.Text, | ||
| conversationId: conversationId, | ||
| channel: new Channel(context.Activity.ChannelId)); | ||
|
|
||
| // === InferenceScope: wraps the LLM + tool-call loop === | ||
| var inferenceDetails = new InferenceCallDetails( | ||
| InferenceOperationType.Chat, | ||
| model: _deploymentName, | ||
| providerName: "AzureOpenAI"); | ||
|
|
||
| List<ChatMessage> snapshot; | ||
| lock (history) { snapshot = [.. history]; } | ||
|
|
||
| ChatResponse chatResponse; | ||
| using (var inferenceScope = InferenceScope.Start(request, inferenceDetails, agentDetails)) | ||
| { | ||
| chatResponse = await _chatClient.GetResponseAsync(snapshot, _chatOptions, ct); | ||
|
|
||
| if (chatResponse.Usage is { } usage) | ||
| { | ||
| if (usage.InputTokenCount is { } inputTokens) | ||
| inferenceScope.RecordInputTokens((int)inputTokens); | ||
| if (usage.OutputTokenCount is { } outputTokens) | ||
| inferenceScope.RecordOutputTokens((int)outputTokens); | ||
| } | ||
|
|
||
| var finishReason = chatResponse.FinishReason?.Value ?? "stop"; | ||
| inferenceScope.RecordFinishReasons([finishReason]); | ||
| } | ||
|
|
||
| lock (history) | ||
| { | ||
| history.AddRange(chatResponse.Messages); | ||
| } | ||
|
|
||
| // === ExecuteToolScope: record each tool invocation === | ||
| var toolCalls = chatResponse.Messages | ||
| .SelectMany(m => m.Contents.OfType<FunctionCallContent>()) | ||
| .GroupBy(fc => fc.CallId ?? fc.Name ?? "") | ||
| .ToDictionary(g => g.Key, g => g.First()); | ||
|
|
||
| foreach (var funcResult in chatResponse.Messages | ||
| .SelectMany(m => m.Contents.OfType<FunctionResultContent>())) | ||
| { | ||
| toolCalls.TryGetValue(funcResult.CallId ?? "", out var matchingCall); | ||
|
|
||
| var toolDetails = new ToolCallDetails( | ||
| toolName: matchingCall?.Name ?? "unknown", | ||
| arguments: matchingCall?.Arguments is { } args ? JsonSerializer.Serialize(args) : null, | ||
| toolCallId: funcResult.CallId); | ||
|
|
||
| using var toolScope = ExecuteToolScope.Start(request, toolDetails, agentDetails); | ||
| if (funcResult.Result is not null) | ||
| { | ||
| toolScope.RecordResponse(funcResult.Result.ToString()!); | ||
| } | ||
| } | ||
|
|
||
| // Extract citations from tool results. | ||
| var citations = chatResponse.Messages | ||
| .SelectMany(m => m.Contents.OfType<FunctionResultContent>()) | ||
| .Where(frc => frc.Result is not null) | ||
| .SelectMany(frc => | ||
| { | ||
| try | ||
| { | ||
| var json = JsonSerializer.Deserialize<JsonElement>(frc.Result!.ToString()!); | ||
| if (json.TryGetProperty("structuredContent", out var sc) && | ||
| sc.TryGetProperty("results", out var results)) | ||
| { | ||
| return results.EnumerateArray() | ||
| .Where(r => r.TryGetProperty("contentUrl", out _)) | ||
| .Select(r => ( | ||
| Title: r.GetProperty("title").GetString() ?? "", | ||
| Url: r.GetProperty("contentUrl").GetString() ?? "", | ||
| Content: r.TryGetProperty("content", out var c) ? c.GetString() ?? "" : "" | ||
| )); | ||
| } | ||
| } | ||
| catch (JsonException) { } | ||
| return []; | ||
|
rido-min marked this conversation as resolved.
|
||
| }) | ||
| .DistinctBy(c => c.Url) | ||
| .Take(5).ToList(); | ||
|
|
||
| var responseText = chatResponse.Text; | ||
|
|
||
| for (int i = 0; i < citations.Count; i++) | ||
| { | ||
| responseText += $"[{i + 1}] "; | ||
| } | ||
|
|
||
| // === OutputScope: record the agent's reply === | ||
| using (OutputScope.Start(request, new Response([responseText]), agentDetails)) | ||
| { | ||
| } | ||
|
|
||
| var builder = TeamsActivity.CreateBuilder() | ||
| .WithText(responseText, TextFormats.Markdown) | ||
| .AddMention(context.Activity?.From!) | ||
| .AddAIGenerated(); | ||
|
|
||
| for (int i = 0; i < citations.Count; i++) | ||
| { | ||
| var citation = citations[i]; | ||
| var abstract_ = citation.Content.Length > 160 ? citation.Content[..157] + "..." : citation.Content; | ||
| builder.AddCitation(i + 1, new CitationAppearance() { Name = citation.Title, Url = new Uri(citation.Url), Abstract = abstract_, Icon = CitationIcon.Text }); | ||
| } | ||
|
|
||
| await context.Send(builder.Build(), ct); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using Azure.AI.OpenAI; | ||
| using Microsoft.Extensions.AI; | ||
| using Microsoft.Identity.Abstractions; | ||
| using Microsoft.Identity.Web; | ||
| using Microsoft.OpenTelemetry; | ||
| using Microsoft.Teams.Apps; | ||
| using Microsoft.Teams.Apps.Diagnostics; | ||
| using Microsoft.Teams.Core.Diagnostics; | ||
| using ModelContextProtocol.Client; | ||
| using ObservabilityBot; | ||
| using OpenTelemetry; | ||
| using OpenTelemetry.Resources; | ||
|
rido-min marked this conversation as resolved.
|
||
|
|
||
|
|
||
| string[] activitySources = [CoreTelemetryNames.ActivitySourceName, TeamsBotApplicationTelemetry.ActivitySourceName, "Experimental.Microsoft.Extensions.AI", "ModelContextProtocol"]; | ||
| string[] meterNames = [CoreTelemetryNames.MeterName, TeamsBotApplicationTelemetry.MeterName, "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"]; | ||
|
|
||
| WebApplicationBuilder builder = WebApplication.CreateBuilder(args); | ||
| IServiceProvider? rootProvider = null; | ||
| builder.Services.AddTeamsBotApplication<ObservabilityBotApp>(); | ||
|
|
||
| builder.Services.AddOpenTelemetry() | ||
| .ConfigureResource(r => r | ||
| .AddService(serviceName: "ObservabilityBot", serviceVersion: "0.0.1") | ||
| .AddAttributes(new Dictionary<string, object> | ||
| { | ||
| ["deployment.environment"] = builder.Environment.EnvironmentName, | ||
| ["service.namespace"] = "Microsoft.Teams" | ||
| })) | ||
| .UseMicrosoftOpenTelemetry(o => { | ||
| o.Exporters = ExportTarget.Otlp | ExportTarget.Agent365 | ExportTarget.AzureMonitor; | ||
| o.Instrumentation.EnableHttpClientInstrumentation = true; | ||
| o.Instrumentation.EnableAspNetCoreInstrumentation = true; | ||
|
|
||
| o.Agent365.ContextualTokenResolver = async trctx => | ||
| { | ||
| var provider = rootProvider!.GetRequiredService<IAuthorizationHeaderProvider>(); | ||
| var options = new AuthorizationHeaderProviderOptions { AcquireTokenOptions = new() { AuthenticationOptionsName = "AzureAd", Tenant = trctx.TenantId } }; | ||
| ArgumentNullException.ThrowIfNull(trctx.Identity.AgenticUserId); | ||
| options.WithAgentUserIdentity(trctx.Identity.AgentId, new Guid(trctx.Identity.AgenticUserId)); | ||
| var token = await provider.CreateAuthorizationHeaderAsync( | ||
| ["api://9b975845-388f-4429-889e-eab1ef63949c/.default"], options); | ||
| return token.Substring("Bearer".Length).Trim(); | ||
| }; | ||
| }) | ||
| .WithTracing(t => t.AddSource(activitySources)) | ||
| .WithMetrics(m => m.AddMeter(meterNames)); | ||
|
|
||
| builder.Logging.AddOpenTelemetry(o => o.IncludeFormattedMessage = true); | ||
|
|
||
| // Register MCP clients | ||
| builder.Services.AddKeyedSingleton("msdocs", (sp, key) => | ||
| McpClient.CreateAsync( | ||
| new HttpClientTransport(new() | ||
| { | ||
| Endpoint = new Uri("https://learn.microsoft.com/api/mcp"), | ||
| TransportMode = HttpTransportMode.AutoDetect, | ||
| Name = "msdocs" | ||
| }))); | ||
|
|
||
| // Register IChatClient | ||
| var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidDataException("AZURE_OPENAI_ENDPOINT not found"); | ||
| var azoai_key = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY") ?? throw new InvalidDataException("AZURE_OPENAI_KEY not found"); | ||
| var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? throw new InvalidDataException("AZURE_OPENAI_DEPLOYMENT not found"); | ||
|
|
||
| builder.Services.AddSingleton<IChatClient>(sp => | ||
| new ChatClientBuilder( | ||
| new AzureOpenAIClient(new Uri(endpoint), new System.ClientModel.ApiKeyCredential(azoai_key)) | ||
| .GetChatClient(deploymentName) | ||
| .AsIChatClient()) | ||
| .UseFunctionInvocation() | ||
| .UseOpenTelemetry(sourceName: "Experimental.Microsoft.Extensions.AI") | ||
| .UseLogging(sp.GetRequiredService<ILoggerFactory>()) | ||
| .Build()); | ||
|
|
||
| builder.Services.AddSingleton<ChatOptions>(sp => | ||
| { | ||
| var msdocsClient = sp.GetRequiredKeyedService<Task<McpClient>>("msdocs").GetAwaiter().GetResult(); | ||
| var msdocsTools = msdocsClient.ListToolsAsync().GetAwaiter().GetResult(); | ||
|
|
||
| return new ChatOptions | ||
| { | ||
| AllowMultipleToolCalls = true, | ||
| Instructions = "Use the following tools to answer the user's question. If you don't know the answer, use the 'Search Microsoft Docs' tool to find relevant information. Use calendar tools for scheduling-related queries.", | ||
| Tools = [..msdocsTools] | ||
| }; | ||
| }); | ||
|
|
||
| WebApplication app = builder.Build(); | ||
| rootProvider = app.Services; | ||
| app.MapGet("/", () => "ObservabilityBot is running. Telemetry source: " + CoreTelemetryNames.ActivitySourceName); | ||
|
|
||
| app.UseTeamsBotApplication<ObservabilityBotApp>(); | ||
|
|
||
| app.Run(); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.