Skip to content
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4ad3ff5
feat: add OpenTelemetry instrumentation + Agent365 baggage support
rido-min May 4, 2026
1e8f696
Refactor telemetry naming; add OpenTelemetry & OpenAI
rido-min May 4, 2026
b1612cb
configuring token manager
rido-min May 5, 2026
3a483e7
Update to .NET 10, OpenTelemetry 1.0.2, improve Agent365
rido-min May 7, 2026
37ab50c
Add InvokeAgentScope for Agent365 telemetry integration
rido-min May 7, 2026
6318b74
temp otel debugging
rido-min May 8, 2026
f246f1a
Refactor bot logic into ObservabilityBotApp class
rido-min May 11, 2026
ff6c4f5
wip
rido-min May 11, 2026
e603538
Merge branch 'main' into feature/observability-otel-baggage
rido-min May 13, 2026
4228e91
fix after merge
rido-min May 13, 2026
54f6f90
wip
rido-min May 14, 2026
f46d188
Merge branch 'main' into feature/observability-otel-baggage
rido-min May 15, 2026
452375b
Remove calendarTools/teams clients and local OpenTelemetry ref
rido-min May 15, 2026
4f188fd
fix: set Error status on router span exceptions; preserve TenantId in…
rido-min May 15, 2026
a0cb054
docs: align Observability-Design.md with actual code
rido-min May 15, 2026
17646ad
feat(apps): add router-level handler metrics to Microsoft.Teams.Apps …
rido-min May 15, 2026
7f9bc09
Merge branch 'main' into feature/observability-otel-baggage
rido-min May 15, 2026
174130c
fix: address PR review comments for observability instrumentation
rido-min May 16, 2026
7a06dd2
fix: address remaining PR review comments
rido-min May 16, 2026
41a5e93
fix: revert unrelated PABot dependency changes
Copilot May 16, 2026
6bc1e37
Replace runtime version lookup with compile-time constant
rido-min May 21, 2026
00062de
Merge branch 'feature/observability-otel-baggage' of https://github.c…
rido-min May 21, 2026
47db33a
Merge remote-tracking branch 'origin/main' into feature/observability…
rido-min May 21, 2026
6d38558
fix: resolve TenantId hiding after merge with main
rido-min May 21, 2026
098ab0e
code clean up
rido-min May 21, 2026
0d8987d
Merge branch 'main' into feature/observability-otel-baggage
rido-min May 21, 2026
9ed8e8d
fix: address PR review comments from latest Copilot review
rido-min May 21, 2026
0683fa4
Merge branch 'main' into feature/observability-otel-baggage
rido-min May 22, 2026
12ceab7
Merge branch 'main' into feature/observability-otel-baggage
rido-min May 27, 2026
8b9a1b5
Update dependencies and refactor Agent365 token resolver
rido-min May 27, 2026
3b2b090
Add launchSettings template with env vars for ElCanario
rido-min May 28, 2026
1a9458c
Merge branch 'main' into feature/observability-otel-baggage
rido-min May 29, 2026
dc5e5c4
Refactor ObservabilityBotApp constructor and citation loop
rido-min May 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/core.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<Project Path="samples/MeetingsBot/MeetingsBot.csproj" />
<Project Path="samples/MessageExtensionBot/MessageExtensionBot.csproj" />
<Project Path="samples/OAuthFlowBot/OAuthFlowBot.csproj" />
<Project Path="samples/ObservabilityBot/ObservabilityBot.csproj" />
<Project Path="samples/PABot/PABot.csproj" Id="ef8f29ef-fe59-4edf-8a50-6e7ab6699a45" />
<Project Path="samples/Quoting/Quoting.csproj" />
<Project Path="samples/SsoBot/SsoBot.csproj" />
Expand Down Expand Up @@ -49,6 +50,7 @@
<Project Path="test/Microsoft.Teams.Apps.BotBuilder.UnitTests/Microsoft.Teams.Apps.BotBuilder.UnitTests.csproj" Id="17f5e8eb-ccb0-40e6-9630-a9d936d6ccde" />
<Project Path="test/Microsoft.Teams.Apps.UnitTests/Microsoft.Teams.Apps.UnitTests.csproj" Id="16f5e8eb-ccb0-40e6-9630-a9d936d6ccde" />
<Project Path="test/Microsoft.Teams.Core.UnitTests/Microsoft.Teams.Core.UnitTests.csproj" Id="8e6790ed-0352-45f6-b3dc-0c4c3bcab038" />

</Folder>
Comment on lines 52 to 55
<Project Path="src/Microsoft.Teams.Apps.BotBuilder/Microsoft.Teams.Apps.BotBuilder.csproj" />
<Project Path="src/Microsoft.Teams.Apps/Microsoft.Teams.Apps.csproj" Id="0e993a63-8a1a-4cdf-8a29-cc8c59bd6c30" />
Expand Down
412 changes: 412 additions & 0 deletions core/docs/Observability-Design.md

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions core/samples/ObservabilityBot/ObservabilityBot.csproj
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.2" />
<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.5.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.5.1" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.1.0" />
<PackageReference Include="ModelContextProtocol" Version="1.2.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>
182 changes: 182 additions & 0 deletions core/samples/ObservabilityBot/ObservabilityBotApp.cs
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;

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 [];
Comment on lines +132 to +148
})
.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);
}
}
97 changes: 97 additions & 0 deletions core/samples/ObservabilityBot/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// 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;


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.Exporter.UseS2SEndpoint = true;
o.Agent365.Exporter.TokenResolver = async (agentId, tenantId) =>
{
var provider = rootProvider!.GetRequiredService<IAuthorizationHeaderProvider>();
var options = new AuthorizationHeaderProviderOptions { AcquireTokenOptions = new() { AuthenticationOptionsName = "AzureAd", Tenant = tenantId } };
options.WithAgentIdentity(agentId);
var token = await provider.CreateAuthorizationHeaderForAppAsync(
"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();
Loading
Loading