From abeca5f7a925b865fac834d1969677d246c47150 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 14:58:36 +0000
Subject: [PATCH 1/8] Initial plan
From b047dc8e32eb8b4d43b4ed4287135f1c0e7dea59 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 15:12:35 +0000
Subject: [PATCH 2/8] Add BaggageTurnMiddleware and OutputLoggingMiddleware
with tests
Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com>
---
.../Middleware/BaggageTurnMiddleware.cs | 46 ++++
.../Middleware/OutputLoggingMiddleware.cs | 219 ++++++++++++++++++
...ts.A365.Observability.Hosting.Tests.csproj | 1 +
.../Middleware/BaggageTurnMiddlewareTests.cs | 140 +++++++++++
.../OutputLoggingMiddlewareTests.cs | 146 ++++++++++++
5 files changed, 552 insertions(+)
create mode 100644 src/Observability/Hosting/Middleware/BaggageTurnMiddleware.cs
create mode 100644 src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
create mode 100644 src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/BaggageTurnMiddlewareTests.cs
create mode 100644 src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/OutputLoggingMiddlewareTests.cs
diff --git a/src/Observability/Hosting/Middleware/BaggageTurnMiddleware.cs b/src/Observability/Hosting/Middleware/BaggageTurnMiddleware.cs
new file mode 100644
index 00000000..9d5111a9
--- /dev/null
+++ b/src/Observability/Hosting/Middleware/BaggageTurnMiddleware.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.A365.Observability.Hosting.Extensions;
+using Microsoft.Agents.A365.Observability.Runtime.Common;
+using Microsoft.Agents.Builder;
+using Microsoft.Agents.Core.Models;
+
+namespace Microsoft.Agents.A365.Observability.Hosting.Middleware
+{
+ ///
+ /// Bot Framework middleware that propagates OpenTelemetry baggage context
+ /// derived from .
+ ///
+ ///
+ /// Async replies (ContinueConversation events) are passed through without
+ /// baggage setup because their context is established by the originating turn.
+ ///
+ public sealed class BaggageTurnMiddleware : IMiddleware
+ {
+ ///
+ public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default)
+ {
+ var activity = turnContext.Activity;
+ bool isAsyncReply = activity != null
+ && activity.Type == ActivityTypes.Event
+ && activity.Name == ActivityEventNames.ContinueConversation;
+
+ if (isAsyncReply)
+ {
+ await next(cancellationToken).ConfigureAwait(false);
+ return;
+ }
+
+ var builder = new BaggageBuilder();
+ builder.FromTurnContext(turnContext);
+
+ using (builder.Build())
+ {
+ await next(cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs b/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
new file mode 100644
index 00000000..7a7244bc
--- /dev/null
+++ b/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
@@ -0,0 +1,219 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.A365.Observability.Hosting.Extensions;
+using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts;
+using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes;
+using Microsoft.Agents.Builder;
+using Microsoft.Agents.Core.Models;
+
+namespace Microsoft.Agents.A365.Observability.Hosting.Middleware
+{
+ ///
+ /// Bot Framework middleware that creates spans
+ /// for outgoing messages.
+ ///
+ ///
+ ///
+ /// Links to a parent span when is set in
+ /// .
+ ///
+ ///
+ /// Privacy note: Outgoing message content is captured verbatim as
+ /// span attributes and exported to the configured telemetry backend.
+ ///
+ ///
+ public sealed class OutputLoggingMiddleware : IMiddleware
+ {
+ ///
+ /// The key used to store the parent
+ /// span reference. Set this value to a W3C traceparent string
+ /// (e.g. "00-{trace_id}-{span_id}-{trace_flags}") to link
+ /// spans as children of an
+ /// .
+ ///
+ public const string A365ParentSpanKey = "A365ParentSpanId";
+
+ ///
+ public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default)
+ {
+ var agentDetails = DeriveAgentDetails(turnContext);
+ var tenantDetails = DeriveTenantDetails(turnContext);
+
+ if (agentDetails == null || tenantDetails == null)
+ {
+ await next(cancellationToken).ConfigureAwait(false);
+ return;
+ }
+
+ var callerDetails = DeriveCallerDetails(turnContext);
+ var conversationId = turnContext.Activity?.Conversation?.Id;
+ var sourceMetadata = DeriveSourceMetadata(turnContext);
+ var executionType = DeriveExecutionType(turnContext);
+
+ turnContext.OnSendActivities(CreateSendHandler(
+ turnContext,
+ agentDetails,
+ tenantDetails,
+ callerDetails,
+ conversationId,
+ sourceMetadata,
+ executionType));
+
+ await next(cancellationToken).ConfigureAwait(false);
+ }
+
+ private static AgentDetails? DeriveAgentDetails(ITurnContext turnContext)
+ {
+ var recipient = turnContext.Activity?.Recipient;
+ if (recipient == null)
+ {
+ return null;
+ }
+
+ // Gate on the recipient having an agentic identity
+ var agentId = recipient.AgenticAppId ?? recipient.Id;
+ if (string.IsNullOrEmpty(agentId))
+ {
+ return null;
+ }
+
+ return new AgentDetails(
+ agentId: agentId,
+ agentName: recipient.Name,
+ agentAUID: recipient.AadObjectId,
+ agentUPN: recipient.AgenticUserId,
+ agentDescription: recipient.Role,
+ tenantId: recipient.TenantId);
+ }
+
+ private static TenantDetails? DeriveTenantDetails(ITurnContext turnContext)
+ {
+ var tenantId = turnContext.Activity?.Recipient?.TenantId;
+ if (string.IsNullOrWhiteSpace(tenantId) || !Guid.TryParse(tenantId, out var tenantGuid))
+ {
+ return null;
+ }
+
+ return new TenantDetails(tenantGuid);
+ }
+
+ private static CallerDetails? DeriveCallerDetails(ITurnContext turnContext)
+ {
+ var from = turnContext.Activity?.From;
+ if (from == null)
+ {
+ return null;
+ }
+
+ return new CallerDetails(
+ callerId: from.Id ?? string.Empty,
+ callerName: from.Name ?? string.Empty,
+ callerUpn: from.AgenticUserId ?? string.Empty,
+ tenantId: from.TenantId);
+ }
+
+ private static SourceMetadata? DeriveSourceMetadata(ITurnContext turnContext)
+ {
+ var channelId = turnContext.Activity?.ChannelId;
+ if (channelId == null)
+ {
+ return null;
+ }
+
+ return new SourceMetadata(
+ name: channelId.Channel,
+ description: channelId.SubChannel);
+ }
+
+ private static string? DeriveExecutionType(ITurnContext turnContext)
+ {
+ var pairs = turnContext.GetExecutionTypePair();
+ foreach (var pair in pairs)
+ {
+ if (pair.Key == OpenTelemetryConstants.GenAiExecutionTypeKey)
+ {
+ return pair.Value?.ToString();
+ }
+ }
+
+ return null;
+ }
+
+ private static SendActivitiesHandler CreateSendHandler(
+ ITurnContext turnContext,
+ AgentDetails agentDetails,
+ TenantDetails tenantDetails,
+ CallerDetails? callerDetails,
+ string? conversationId,
+ SourceMetadata? sourceMetadata,
+ string? executionType)
+ {
+ return async (ctx, activities, nextSend) =>
+ {
+ var messages = new List();
+ foreach (var a in activities)
+ {
+ if (string.Equals(a.Type, ActivityTypes.Message, StringComparison.OrdinalIgnoreCase)
+ && !string.IsNullOrEmpty(a.Text))
+ {
+ messages.Add(a.Text);
+ }
+ }
+
+ if (messages.Count == 0)
+ {
+ return await nextSend().ConfigureAwait(false);
+ }
+
+ // Read parent span lazily so the agent handler can set it during logic()
+ string? parentId = null;
+ if (turnContext.StackState.ContainsKey(A365ParentSpanKey))
+ {
+ parentId = turnContext.StackState[A365ParentSpanKey]?.ToString();
+ }
+
+ var outputScope = OutputScope.Start(
+ agentDetails: agentDetails,
+ tenantDetails: tenantDetails,
+ response: new Response(messages),
+ parentId: parentId);
+
+ outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiConversationIdKey, conversationId);
+ outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiExecutionTypeKey, executionType);
+
+ if (sourceMetadata != null)
+ {
+ outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiChannelNameKey, sourceMetadata.Name);
+ outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiChannelLinkKey, sourceMetadata.Description);
+ }
+
+ if (callerDetails != null)
+ {
+ outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiCallerIdKey, callerDetails.CallerId);
+ outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiCallerUpnKey, callerDetails.CallerUpn);
+ outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiCallerNameKey, callerDetails.CallerName);
+ outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiCallerTenantIdKey, callerDetails.TenantId);
+ }
+
+ try
+ {
+ return await nextSend().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ outputScope.RecordError(ex);
+ throw;
+ }
+ finally
+ {
+ outputScope.Dispose();
+ }
+ };
+ }
+ }
+}
diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Microsoft.Agents.A365.Observability.Hosting.Tests.csproj b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Microsoft.Agents.A365.Observability.Hosting.Tests.csproj
index 4c414d33..a8168905 100644
--- a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Microsoft.Agents.A365.Observability.Hosting.Tests.csproj
+++ b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Microsoft.Agents.A365.Observability.Hosting.Tests.csproj
@@ -8,6 +8,7 @@
+
diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/BaggageTurnMiddlewareTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/BaggageTurnMiddlewareTests.cs
new file mode 100644
index 00000000..e2f63aca
--- /dev/null
+++ b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/BaggageTurnMiddlewareTests.cs
@@ -0,0 +1,140 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using FluentAssertions;
+using Microsoft.Agents.A365.Observability.Hosting.Middleware;
+using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes;
+using Microsoft.Agents.Builder;
+using Microsoft.Agents.Core.Models;
+using Moq;
+using OpenTelemetry;
+
+namespace Microsoft.Agents.A365.Observability.Hosting.Tests.Middleware;
+
+[TestClass]
+public class BaggageTurnMiddlewareTests
+{
+ [TestMethod]
+ public async Task OnTurnAsync_SetsOpenTelemetryBaggage()
+ {
+ // Arrange
+ var middleware = new BaggageTurnMiddleware();
+ var turnContext = CreateTurnContext();
+
+ string? capturedTenantId = null;
+ string? capturedCallerId = null;
+
+ NextDelegate next = (ct) =>
+ {
+ capturedTenantId = Baggage.Current.GetBaggage(OpenTelemetryConstants.TenantIdKey);
+ capturedCallerId = Baggage.Current.GetBaggage(OpenTelemetryConstants.GenAiCallerIdKey);
+ return Task.CompletedTask;
+ };
+
+ // Act
+ await middleware.OnTurnAsync(turnContext, next);
+
+ // Assert
+ capturedTenantId.Should().Be("tenant-123");
+ capturedCallerId.Should().Be("caller-id");
+ }
+
+ [TestMethod]
+ public async Task OnTurnAsync_SkipsBaggageForContinueConversation()
+ {
+ // Arrange
+ var middleware = new BaggageTurnMiddleware();
+ var turnContext = CreateTurnContext(
+ activityType: ActivityTypes.Event,
+ activityName: ActivityEventNames.ContinueConversation);
+
+ bool logicCalled = false;
+ string? capturedCallerId = null;
+
+ NextDelegate next = (ct) =>
+ {
+ logicCalled = true;
+ capturedCallerId = Baggage.Current.GetBaggage(OpenTelemetryConstants.GenAiCallerIdKey);
+ return Task.CompletedTask;
+ };
+
+ // Act
+ await middleware.OnTurnAsync(turnContext, next);
+
+ // Assert
+ logicCalled.Should().BeTrue();
+ // Baggage should NOT be set because the middleware skipped it
+ capturedCallerId.Should().BeNull();
+ }
+
+ [TestMethod]
+ public async Task OnTurnAsync_CallsNextDelegate()
+ {
+ // Arrange
+ var middleware = new BaggageTurnMiddleware();
+ var turnContext = CreateTurnContext();
+
+ bool nextCalled = false;
+ NextDelegate next = (ct) =>
+ {
+ nextCalled = true;
+ return Task.CompletedTask;
+ };
+
+ // Act
+ await middleware.OnTurnAsync(turnContext, next);
+
+ // Assert
+ nextCalled.Should().BeTrue();
+ }
+
+ [TestMethod]
+ public async Task OnTurnAsync_RestoresBaggageAfterNext()
+ {
+ // Arrange
+ var middleware = new BaggageTurnMiddleware();
+ var turnContext = CreateTurnContext();
+
+ string? baggageBeforeMiddleware = Baggage.Current.GetBaggage(OpenTelemetryConstants.TenantIdKey);
+
+ NextDelegate next = (ct) => Task.CompletedTask;
+
+ // Act
+ await middleware.OnTurnAsync(turnContext, next);
+
+ // Assert – the baggage scope should be disposed after OnTurnAsync returns
+ string? baggageAfterMiddleware = Baggage.Current.GetBaggage(OpenTelemetryConstants.TenantIdKey);
+ baggageAfterMiddleware.Should().Be(baggageBeforeMiddleware);
+ }
+
+ private static ITurnContext CreateTurnContext(
+ string activityType = "message",
+ string? activityName = null)
+ {
+ var mockActivity = new Mock();
+ mockActivity.Setup(a => a.Type).Returns(activityType);
+ mockActivity.Setup(a => a.Name).Returns(activityName!);
+ mockActivity.Setup(a => a.Text).Returns("Hello");
+ mockActivity.Setup(a => a.From).Returns(new ChannelAccount
+ {
+ Id = "caller-id",
+ Name = "Caller",
+ AadObjectId = "caller-aad",
+ });
+ mockActivity.Setup(a => a.Recipient).Returns(new ChannelAccount
+ {
+ Id = "agent-id",
+ Name = "Agent",
+ TenantId = "tenant-123",
+ Role = "user",
+ });
+ mockActivity.Setup(a => a.Conversation).Returns(new ConversationAccount { Id = "conv-id" });
+ mockActivity.Setup(a => a.ServiceUrl).Returns("https://example.com");
+ mockActivity.Setup(a => a.ChannelId).Returns(new ChannelId("test-channel"));
+
+ var mockTurnContext = new Mock();
+ mockTurnContext.Setup(tc => tc.Activity).Returns(mockActivity.Object);
+
+ return mockTurnContext.Object;
+ }
+}
diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/OutputLoggingMiddlewareTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/OutputLoggingMiddlewareTests.cs
new file mode 100644
index 00000000..e714917c
--- /dev/null
+++ b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/OutputLoggingMiddlewareTests.cs
@@ -0,0 +1,146 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using FluentAssertions;
+using Microsoft.Agents.A365.Observability.Hosting.Middleware;
+using Microsoft.Agents.Builder;
+using Microsoft.Agents.Core.Models;
+using Moq;
+
+namespace Microsoft.Agents.A365.Observability.Hosting.Tests.Middleware;
+
+[TestClass]
+public class OutputLoggingMiddlewareTests
+{
+ [TestMethod]
+ public async Task OnTurnAsync_CallsNextDelegate()
+ {
+ // Arrange
+ var middleware = new OutputLoggingMiddleware();
+ var turnContext = CreateTurnContext();
+
+ bool nextCalled = false;
+ NextDelegate next = (ct) =>
+ {
+ nextCalled = true;
+ return Task.CompletedTask;
+ };
+
+ // Act
+ await middleware.OnTurnAsync(turnContext, next);
+
+ // Assert
+ nextCalled.Should().BeTrue();
+ }
+
+ [TestMethod]
+ public async Task OnTurnAsync_RegistersSendHandler_WhenRecipientHasDetails()
+ {
+ // Arrange
+ var middleware = new OutputLoggingMiddleware();
+ var mockTurnContext = new Mock();
+ SetupTurnContext(mockTurnContext);
+
+ NextDelegate next = (ct) => Task.CompletedTask;
+
+ // Act
+ await middleware.OnTurnAsync(mockTurnContext.Object, next);
+
+ // Assert
+ mockTurnContext.Verify(tc => tc.OnSendActivities(It.IsAny()), Times.Once);
+ }
+
+ [TestMethod]
+ public async Task OnTurnAsync_PassesThrough_WhenRecipientIsNull()
+ {
+ // Arrange
+ var middleware = new OutputLoggingMiddleware();
+ var mockActivity = new Mock();
+ mockActivity.Setup(a => a.Recipient).Returns((ChannelAccount)null!);
+ mockActivity.Setup(a => a.Type).Returns("message");
+
+ var mockTurnContext = new Mock();
+ mockTurnContext.Setup(tc => tc.Activity).Returns(mockActivity.Object);
+
+ bool nextCalled = false;
+ NextDelegate next = (ct) =>
+ {
+ nextCalled = true;
+ return Task.CompletedTask;
+ };
+
+ // Act
+ await middleware.OnTurnAsync(mockTurnContext.Object, next);
+
+ // Assert
+ nextCalled.Should().BeTrue();
+ mockTurnContext.Verify(tc => tc.OnSendActivities(It.IsAny()), Times.Never);
+ }
+
+ [TestMethod]
+ public async Task OnTurnAsync_PassesThrough_WhenTenantIdIsMissing()
+ {
+ // Arrange
+ var middleware = new OutputLoggingMiddleware();
+ var mockActivity = new Mock();
+ mockActivity.Setup(a => a.Type).Returns("message");
+ mockActivity.Setup(a => a.Recipient).Returns(new ChannelAccount
+ {
+ Id = "agent-id",
+ Name = "Agent",
+ // No TenantId set
+ });
+
+ var mockTurnContext = new Mock();
+ mockTurnContext.Setup(tc => tc.Activity).Returns(mockActivity.Object);
+
+ bool nextCalled = false;
+ NextDelegate next = (ct) =>
+ {
+ nextCalled = true;
+ return Task.CompletedTask;
+ };
+
+ // Act
+ await middleware.OnTurnAsync(mockTurnContext.Object, next);
+
+ // Assert
+ nextCalled.Should().BeTrue();
+ mockTurnContext.Verify(tc => tc.OnSendActivities(It.IsAny()), Times.Never);
+ }
+
+ private static ITurnContext CreateTurnContext()
+ {
+ var mockTurnContext = new Mock();
+ SetupTurnContext(mockTurnContext);
+ return mockTurnContext.Object;
+ }
+
+ private static void SetupTurnContext(Mock mockTurnContext)
+ {
+ var mockActivity = new Mock();
+ mockActivity.Setup(a => a.Type).Returns("message");
+ mockActivity.Setup(a => a.Text).Returns("Hello");
+ mockActivity.Setup(a => a.From).Returns(new ChannelAccount
+ {
+ Id = "caller-id",
+ Name = "Caller",
+ AadObjectId = "caller-aad",
+ });
+ mockActivity.Setup(a => a.Recipient).Returns(new ChannelAccount
+ {
+ Id = "agent-id",
+ Name = "Agent",
+ TenantId = "badf1f56-284d-4dc5-ac59-0dd53900e743",
+ Role = "agenticAppInstance",
+ });
+ mockActivity.Setup(a => a.Conversation).Returns(new ConversationAccount { Id = "conv-id" });
+ mockActivity.Setup(a => a.ServiceUrl).Returns("https://example.com");
+ mockActivity.Setup(a => a.ChannelId).Returns(new ChannelId("test-channel"));
+
+ mockTurnContext.Setup(tc => tc.Activity).Returns(mockActivity.Object);
+ mockTurnContext.Setup(tc => tc.StackState).Returns(new TurnContextStateCollection());
+ mockTurnContext.Setup(tc => tc.OnSendActivities(It.IsAny()))
+ .Returns(mockTurnContext.Object);
+ }
+}
From 342c8c27d2dca1514d3348b874eca686f0a69f25 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 15:13:59 +0000
Subject: [PATCH 3/8] Address code review: wrap OutputScope in try/finally, fix
nullable handling in test
Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com>
---
.../Middleware/OutputLoggingMiddleware.cs | 32 +++++++++----------
.../Middleware/BaggageTurnMiddlewareTests.cs | 5 ++-
2 files changed, 20 insertions(+), 17 deletions(-)
diff --git a/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs b/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
index 7a7244bc..bf495d94 100644
--- a/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
+++ b/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
@@ -183,25 +183,25 @@ private static SendActivitiesHandler CreateSendHandler(
response: new Response(messages),
parentId: parentId);
- outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiConversationIdKey, conversationId);
- outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiExecutionTypeKey, executionType);
-
- if (sourceMetadata != null)
+ try
{
- outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiChannelNameKey, sourceMetadata.Name);
- outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiChannelLinkKey, sourceMetadata.Description);
- }
+ outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiConversationIdKey, conversationId);
+ outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiExecutionTypeKey, executionType);
- if (callerDetails != null)
- {
- outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiCallerIdKey, callerDetails.CallerId);
- outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiCallerUpnKey, callerDetails.CallerUpn);
- outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiCallerNameKey, callerDetails.CallerName);
- outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiCallerTenantIdKey, callerDetails.TenantId);
- }
+ if (sourceMetadata != null)
+ {
+ outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiChannelNameKey, sourceMetadata.Name);
+ outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiChannelLinkKey, sourceMetadata.Description);
+ }
+
+ if (callerDetails != null)
+ {
+ outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiCallerIdKey, callerDetails.CallerId);
+ outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiCallerUpnKey, callerDetails.CallerUpn);
+ outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiCallerNameKey, callerDetails.CallerName);
+ outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiCallerTenantIdKey, callerDetails.TenantId);
+ }
- try
- {
return await nextSend().ConfigureAwait(false);
}
catch (Exception ex)
diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/BaggageTurnMiddlewareTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/BaggageTurnMiddlewareTests.cs
index e2f63aca..538ea909 100644
--- a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/BaggageTurnMiddlewareTests.cs
+++ b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/BaggageTurnMiddlewareTests.cs
@@ -113,7 +113,10 @@ private static ITurnContext CreateTurnContext(
{
var mockActivity = new Mock();
mockActivity.Setup(a => a.Type).Returns(activityType);
- mockActivity.Setup(a => a.Name).Returns(activityName!);
+ if (activityName != null)
+ {
+ mockActivity.Setup(a => a.Name).Returns(activityName);
+ }
mockActivity.Setup(a => a.Text).Returns("Hello");
mockActivity.Setup(a => a.From).Returns(new ChannelAccount
{
From 8eca2d2ccddf69ae452964bd431def260bcc3586 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 16:32:53 +0000
Subject: [PATCH 4/8] Add UseObservabilityMiddleware extension for
IChannelAdapter with tests and docs
Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com>
---
.../ObservabilityMiddlewareExtensions.cs | 71 ++++++++++++++
src/Observability/Hosting/docs/design.md | 90 +++++++++++++----
.../ObservabilityMiddlewareExtensionsTests.cs | 98 +++++++++++++++++++
3 files changed, 241 insertions(+), 18 deletions(-)
create mode 100644 src/Observability/Hosting/Middleware/ObservabilityMiddlewareExtensions.cs
create mode 100644 src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/ObservabilityMiddlewareExtensionsTests.cs
diff --git a/src/Observability/Hosting/Middleware/ObservabilityMiddlewareExtensions.cs b/src/Observability/Hosting/Middleware/ObservabilityMiddlewareExtensions.cs
new file mode 100644
index 00000000..69685723
--- /dev/null
+++ b/src/Observability/Hosting/Middleware/ObservabilityMiddlewareExtensions.cs
@@ -0,0 +1,71 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using Microsoft.Agents.A365.Observability.Hosting.Middleware;
+using Microsoft.Agents.Builder;
+
+namespace Microsoft.Agents.A365.Observability.Hosting
+{
+ ///
+ /// Extension methods for registering observability middleware on an .
+ ///
+ public static class ObservabilityMiddlewareExtensions
+ {
+ ///
+ /// Adds the observability middleware to the adapter pipeline.
+ ///
+ /// The channel adapter to add middleware to.
+ ///
+ /// When true (the default), registers
+ /// which propagates OpenTelemetry baggage context from the .
+ ///
+ ///
+ /// When true (the default), registers
+ /// which creates OutputScope spans for outgoing messages.
+ ///
+ /// The adapter, for method chaining.
+ ///
+ ///
+ /// Baggage middleware should be registered early in the pipeline so that
+ /// downstream middleware and handlers run inside the baggage context.
+ ///
+ ///
+ /// Output logging middleware captures outgoing message content verbatim
+ /// as span attributes. Ensure your telemetry backend is appropriate for the
+ /// data sensitivity of your agent before enabling this in production.
+ ///
+ ///
+ ///
+ /// // In Program.cs, after building the app:
+ /// var app = builder.Build();
+ /// var adapter = app.Services.GetRequiredService<IChannelAdapter>();
+ /// adapter.UseObservabilityMiddleware();
+ ///
+ ///
+ ///
+ /// is null.
+ public static IChannelAdapter UseObservabilityMiddleware(
+ this IChannelAdapter adapter,
+ bool enableBaggage = true,
+ bool enableOutputLogging = true)
+ {
+ if (adapter == null)
+ {
+ throw new ArgumentNullException(nameof(adapter));
+ }
+
+ if (enableBaggage)
+ {
+ adapter.Use(new BaggageTurnMiddleware());
+ }
+
+ if (enableOutputLogging)
+ {
+ adapter.Use(new OutputLoggingMiddleware());
+ }
+
+ return adapter;
+ }
+ }
+}
diff --git a/src/Observability/Hosting/docs/design.md b/src/Observability/Hosting/docs/design.md
index 30114619..73573f88 100644
--- a/src/Observability/Hosting/docs/design.md
+++ b/src/Observability/Hosting/docs/design.md
@@ -9,20 +9,23 @@ The `Microsoft.Agents.A365.Observability.Hosting` package provides ASP.NET Core
```
Microsoft.Agents.A365.Observability.Hosting
├── Middleware/
-│ └── ObservabilityBaggageMiddleware # Per-request baggage context
+│ ├── BaggageTurnMiddleware # Bot Framework IMiddleware – baggage from TurnContext
+│ ├── OutputLoggingMiddleware # Bot Framework IMiddleware – OutputScope for outgoing messages
+│ ├── ObservabilityMiddlewareExtensions # IChannelAdapter.UseObservabilityMiddleware()
+│ └── ObservabilityBaggageMiddleware # ASP.NET Core middleware – per-request baggage
├── Caching/
-│ ├── IExporterTokenCache # Token cache interface
-│ ├── AgenticTokenCache # Agentic token caching
-│ ├── ServiceTokenCache # Service token caching
-│ └── AgenticTokenStruct # Token data structure
+│ ├── IExporterTokenCache # Token cache interface
+│ ├── AgenticTokenCache # Agentic token caching
+│ ├── ServiceTokenCache # Service token caching
+│ └── AgenticTokenStruct # Token data structure
├── Extensions/
-│ ├── BaggageBuilderExtensions # Baggage context helpers
-│ ├── InvokeAgentScopeExtensions # Scope enrichment from TurnContext
-│ ├── TurnContextExtensions # TurnContext telemetry extraction
-│ ├── ObservabilityBuilderExtensions # Builder extensions
+│ ├── BaggageBuilderExtensions # Baggage context helpers
+│ ├── InvokeAgentScopeExtensions # Scope enrichment from TurnContext
+│ ├── TurnContextExtensions # TurnContext telemetry extraction
+│ ├── ObservabilityBuilderExtensions # Builder extensions
│ └── ObservabilityServiceCollectionExtensions # DI setup
└── Internal/
- └── AttributeKeys # Internal attribute key constants
+ └── AttributeKeys # Internal attribute key constants
```
## Key Components
@@ -55,6 +58,47 @@ app.UseObservabilityRequestContext(ctx =>
2. Sets baggage context using `BaggageBuilder.SetRequestContext()`
3. Context is automatically disposed after request completes
+### Bot Framework Turn-Level Middleware
+
+The package also provides two `IMiddleware` implementations that run inside the Bot Framework adapter pipeline (per-turn), and a convenience extension to register them.
+
+#### BaggageTurnMiddleware
+
+**Source**: [BaggageTurnMiddleware.cs](../Middleware/BaggageTurnMiddleware.cs)
+
+Propagates OpenTelemetry baggage context derived from `ITurnContext`. Skips `ContinueConversation` events (async replies) because their context is established by the originating turn.
+
+#### OutputLoggingMiddleware
+
+**Source**: [OutputLoggingMiddleware.cs](../Middleware/OutputLoggingMiddleware.cs)
+
+Creates `OutputScope` spans for outgoing messages. Links to a parent span when `OutputLoggingMiddleware.A365ParentSpanKey` is set in `ITurnContext.StackState`.
+
+> **Privacy note:** Outgoing message content is captured verbatim as span attributes.
+
+#### UseObservabilityMiddleware (convenience extension)
+
+**Source**: [ObservabilityMiddlewareExtensions.cs](../Middleware/ObservabilityMiddlewareExtensions.cs)
+
+Registers both middlewares on an `IChannelAdapter` in a single call:
+
+```csharp
+// In Program.cs — after building the app
+var app = builder.Build();
+var adapter = app.Services.GetRequiredService();
+adapter.UseObservabilityMiddleware();
+```
+
+You can selectively enable/disable each middleware:
+
+```csharp
+// Baggage only (no output logging)
+adapter.UseObservabilityMiddleware(enableOutputLogging: false);
+
+// Output logging only (no baggage)
+adapter.UseObservabilityMiddleware(enableBaggage: false);
+```
+
### InvokeAgentScopeExtensions
**Source**: [InvokeAgentScopeExtensions.cs](../Extensions/InvokeAgentScopeExtensions.cs)
@@ -269,7 +313,10 @@ public static InvokeAgentScope SetCallerTags(
```
src/Observability/Hosting/
├── Middleware/
-│ └── ObservabilityBaggageMiddleware.cs # Per-request baggage
+│ ├── BaggageTurnMiddleware.cs # Bot Framework turn-level baggage
+│ ├── OutputLoggingMiddleware.cs # Bot Framework turn-level output spans
+│ ├── ObservabilityMiddlewareExtensions.cs # IChannelAdapter.UseObservabilityMiddleware()
+│ └── ObservabilityBaggageMiddleware.cs # ASP.NET Core per-request baggage
├── Caching/
│ ├── IExporterTokenCache.cs # Token cache interface
│ ├── AgenticTokenCache.cs # Agentic token caching
@@ -303,21 +350,28 @@ src/Observability/Hosting/
// Program.cs
var builder = WebApplication.CreateBuilder(args);
-// Add observability services
-builder.Services.AddAgent365Observability(builder.Configuration);
+// 1. Configure observability tracing
+builder.Services.AddAgenticTracingExporter();
+builder.AddA365Tracing(config =>
+{
+ config.WithSemanticKernel(); // if using SK
+});
+
+// 2. Register your agent
+builder.AddAgent();
var app = builder.Build();
-// Add baggage middleware early in pipeline
+// 3. Register turn-level observability middleware on the adapter
+var adapter = app.Services.GetRequiredService();
+adapter.UseObservabilityMiddleware();
+
+// 4. (Optional) Add ASP.NET Core per-request baggage middleware
app.UseObservabilityRequestContext(ctx =>
{
- // Extract from JWT claims
var tenantId = ctx.User?.FindFirst("tid")?.Value
?? ctx.User?.FindFirst("tenant_id")?.Value;
-
- // Extract from custom header
var agentId = ctx.Request.Headers["X-Agent-Id"].FirstOrDefault();
-
return (tenantId, agentId);
});
diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/ObservabilityMiddlewareExtensionsTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/ObservabilityMiddlewareExtensionsTests.cs
new file mode 100644
index 00000000..0bee9690
--- /dev/null
+++ b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/ObservabilityMiddlewareExtensionsTests.cs
@@ -0,0 +1,98 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using FluentAssertions;
+using Microsoft.Agents.A365.Observability.Hosting.Middleware;
+using Microsoft.Agents.Builder;
+using Moq;
+
+namespace Microsoft.Agents.A365.Observability.Hosting.Tests.Middleware;
+
+[TestClass]
+public class ObservabilityMiddlewareExtensionsTests
+{
+ [TestMethod]
+ public void UseObservabilityMiddleware_RegistersBothMiddlewares_ByDefault()
+ {
+ // Arrange
+ var mockAdapter = new Mock();
+ mockAdapter.Setup(a => a.Use(It.IsAny())).Returns(mockAdapter.Object);
+
+ // Act
+ mockAdapter.Object.UseObservabilityMiddleware();
+
+ // Assert
+ mockAdapter.Verify(a => a.Use(It.IsAny()), Times.Once);
+ mockAdapter.Verify(a => a.Use(It.IsAny()), Times.Once);
+ }
+
+ [TestMethod]
+ public void UseObservabilityMiddleware_RegistersBaggageOnly_WhenOutputLoggingDisabled()
+ {
+ // Arrange
+ var mockAdapter = new Mock();
+ mockAdapter.Setup(a => a.Use(It.IsAny())).Returns(mockAdapter.Object);
+
+ // Act
+ mockAdapter.Object.UseObservabilityMiddleware(enableOutputLogging: false);
+
+ // Assert
+ mockAdapter.Verify(a => a.Use(It.IsAny()), Times.Once);
+ mockAdapter.Verify(a => a.Use(It.IsAny()), Times.Never);
+ }
+
+ [TestMethod]
+ public void UseObservabilityMiddleware_RegistersOutputLoggingOnly_WhenBaggageDisabled()
+ {
+ // Arrange
+ var mockAdapter = new Mock();
+ mockAdapter.Setup(a => a.Use(It.IsAny())).Returns(mockAdapter.Object);
+
+ // Act
+ mockAdapter.Object.UseObservabilityMiddleware(enableBaggage: false);
+
+ // Assert
+ mockAdapter.Verify(a => a.Use(It.IsAny()), Times.Never);
+ mockAdapter.Verify(a => a.Use(It.IsAny()), Times.Once);
+ }
+
+ [TestMethod]
+ public void UseObservabilityMiddleware_RegistersNothing_WhenBothDisabled()
+ {
+ // Arrange
+ var mockAdapter = new Mock();
+ mockAdapter.Setup(a => a.Use(It.IsAny())).Returns(mockAdapter.Object);
+
+ // Act
+ mockAdapter.Object.UseObservabilityMiddleware(enableBaggage: false, enableOutputLogging: false);
+
+ // Assert
+ mockAdapter.Verify(a => a.Use(It.IsAny()), Times.Never);
+ }
+
+ [TestMethod]
+ public void UseObservabilityMiddleware_ThrowsOnNullAdapter()
+ {
+ // Arrange
+ IChannelAdapter adapter = null!;
+
+ // Act & Assert
+ var act = () => adapter.UseObservabilityMiddleware();
+ act.Should().Throw()
+ .WithParameterName("adapter");
+ }
+
+ [TestMethod]
+ public void UseObservabilityMiddleware_ReturnsAdapter_ForChaining()
+ {
+ // Arrange
+ var mockAdapter = new Mock();
+ mockAdapter.Setup(a => a.Use(It.IsAny())).Returns(mockAdapter.Object);
+
+ // Act
+ var result = mockAdapter.Object.UseObservabilityMiddleware();
+
+ // Assert
+ result.Should().BeSameAs(mockAdapter.Object);
+ }
+}
From 216bb9b83cd9b9dd5f2e914e959186657aeddb94 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 17:33:00 +0000
Subject: [PATCH 5/8] Revert ObservabilityMiddlewareExtensions: remove
extension, tests, and restore design.md
Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com>
---
.../ObservabilityMiddlewareExtensions.cs | 71 --------------
src/Observability/Hosting/docs/design.md | 90 ++++-------------
.../ObservabilityMiddlewareExtensionsTests.cs | 98 -------------------
3 files changed, 18 insertions(+), 241 deletions(-)
delete mode 100644 src/Observability/Hosting/Middleware/ObservabilityMiddlewareExtensions.cs
delete mode 100644 src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/ObservabilityMiddlewareExtensionsTests.cs
diff --git a/src/Observability/Hosting/Middleware/ObservabilityMiddlewareExtensions.cs b/src/Observability/Hosting/Middleware/ObservabilityMiddlewareExtensions.cs
deleted file mode 100644
index 69685723..00000000
--- a/src/Observability/Hosting/Middleware/ObservabilityMiddlewareExtensions.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using System;
-using Microsoft.Agents.A365.Observability.Hosting.Middleware;
-using Microsoft.Agents.Builder;
-
-namespace Microsoft.Agents.A365.Observability.Hosting
-{
- ///
- /// Extension methods for registering observability middleware on an .
- ///
- public static class ObservabilityMiddlewareExtensions
- {
- ///
- /// Adds the observability middleware to the adapter pipeline.
- ///
- /// The channel adapter to add middleware to.
- ///
- /// When true (the default), registers
- /// which propagates OpenTelemetry baggage context from the .
- ///
- ///
- /// When true (the default), registers
- /// which creates OutputScope spans for outgoing messages.
- ///
- /// The adapter, for method chaining.
- ///
- ///
- /// Baggage middleware should be registered early in the pipeline so that
- /// downstream middleware and handlers run inside the baggage context.
- ///
- ///
- /// Output logging middleware captures outgoing message content verbatim
- /// as span attributes. Ensure your telemetry backend is appropriate for the
- /// data sensitivity of your agent before enabling this in production.
- ///
- ///
- ///
- /// // In Program.cs, after building the app:
- /// var app = builder.Build();
- /// var adapter = app.Services.GetRequiredService<IChannelAdapter>();
- /// adapter.UseObservabilityMiddleware();
- ///
- ///
- ///
- /// is null.
- public static IChannelAdapter UseObservabilityMiddleware(
- this IChannelAdapter adapter,
- bool enableBaggage = true,
- bool enableOutputLogging = true)
- {
- if (adapter == null)
- {
- throw new ArgumentNullException(nameof(adapter));
- }
-
- if (enableBaggage)
- {
- adapter.Use(new BaggageTurnMiddleware());
- }
-
- if (enableOutputLogging)
- {
- adapter.Use(new OutputLoggingMiddleware());
- }
-
- return adapter;
- }
- }
-}
diff --git a/src/Observability/Hosting/docs/design.md b/src/Observability/Hosting/docs/design.md
index 73573f88..30114619 100644
--- a/src/Observability/Hosting/docs/design.md
+++ b/src/Observability/Hosting/docs/design.md
@@ -9,23 +9,20 @@ The `Microsoft.Agents.A365.Observability.Hosting` package provides ASP.NET Core
```
Microsoft.Agents.A365.Observability.Hosting
├── Middleware/
-│ ├── BaggageTurnMiddleware # Bot Framework IMiddleware – baggage from TurnContext
-│ ├── OutputLoggingMiddleware # Bot Framework IMiddleware – OutputScope for outgoing messages
-│ ├── ObservabilityMiddlewareExtensions # IChannelAdapter.UseObservabilityMiddleware()
-│ └── ObservabilityBaggageMiddleware # ASP.NET Core middleware – per-request baggage
+│ └── ObservabilityBaggageMiddleware # Per-request baggage context
├── Caching/
-│ ├── IExporterTokenCache # Token cache interface
-│ ├── AgenticTokenCache # Agentic token caching
-│ ├── ServiceTokenCache # Service token caching
-│ └── AgenticTokenStruct # Token data structure
+│ ├── IExporterTokenCache # Token cache interface
+│ ├── AgenticTokenCache # Agentic token caching
+│ ├── ServiceTokenCache # Service token caching
+│ └── AgenticTokenStruct # Token data structure
├── Extensions/
-│ ├── BaggageBuilderExtensions # Baggage context helpers
-│ ├── InvokeAgentScopeExtensions # Scope enrichment from TurnContext
-│ ├── TurnContextExtensions # TurnContext telemetry extraction
-│ ├── ObservabilityBuilderExtensions # Builder extensions
+│ ├── BaggageBuilderExtensions # Baggage context helpers
+│ ├── InvokeAgentScopeExtensions # Scope enrichment from TurnContext
+│ ├── TurnContextExtensions # TurnContext telemetry extraction
+│ ├── ObservabilityBuilderExtensions # Builder extensions
│ └── ObservabilityServiceCollectionExtensions # DI setup
└── Internal/
- └── AttributeKeys # Internal attribute key constants
+ └── AttributeKeys # Internal attribute key constants
```
## Key Components
@@ -58,47 +55,6 @@ app.UseObservabilityRequestContext(ctx =>
2. Sets baggage context using `BaggageBuilder.SetRequestContext()`
3. Context is automatically disposed after request completes
-### Bot Framework Turn-Level Middleware
-
-The package also provides two `IMiddleware` implementations that run inside the Bot Framework adapter pipeline (per-turn), and a convenience extension to register them.
-
-#### BaggageTurnMiddleware
-
-**Source**: [BaggageTurnMiddleware.cs](../Middleware/BaggageTurnMiddleware.cs)
-
-Propagates OpenTelemetry baggage context derived from `ITurnContext`. Skips `ContinueConversation` events (async replies) because their context is established by the originating turn.
-
-#### OutputLoggingMiddleware
-
-**Source**: [OutputLoggingMiddleware.cs](../Middleware/OutputLoggingMiddleware.cs)
-
-Creates `OutputScope` spans for outgoing messages. Links to a parent span when `OutputLoggingMiddleware.A365ParentSpanKey` is set in `ITurnContext.StackState`.
-
-> **Privacy note:** Outgoing message content is captured verbatim as span attributes.
-
-#### UseObservabilityMiddleware (convenience extension)
-
-**Source**: [ObservabilityMiddlewareExtensions.cs](../Middleware/ObservabilityMiddlewareExtensions.cs)
-
-Registers both middlewares on an `IChannelAdapter` in a single call:
-
-```csharp
-// In Program.cs — after building the app
-var app = builder.Build();
-var adapter = app.Services.GetRequiredService();
-adapter.UseObservabilityMiddleware();
-```
-
-You can selectively enable/disable each middleware:
-
-```csharp
-// Baggage only (no output logging)
-adapter.UseObservabilityMiddleware(enableOutputLogging: false);
-
-// Output logging only (no baggage)
-adapter.UseObservabilityMiddleware(enableBaggage: false);
-```
-
### InvokeAgentScopeExtensions
**Source**: [InvokeAgentScopeExtensions.cs](../Extensions/InvokeAgentScopeExtensions.cs)
@@ -313,10 +269,7 @@ public static InvokeAgentScope SetCallerTags(
```
src/Observability/Hosting/
├── Middleware/
-│ ├── BaggageTurnMiddleware.cs # Bot Framework turn-level baggage
-│ ├── OutputLoggingMiddleware.cs # Bot Framework turn-level output spans
-│ ├── ObservabilityMiddlewareExtensions.cs # IChannelAdapter.UseObservabilityMiddleware()
-│ └── ObservabilityBaggageMiddleware.cs # ASP.NET Core per-request baggage
+│ └── ObservabilityBaggageMiddleware.cs # Per-request baggage
├── Caching/
│ ├── IExporterTokenCache.cs # Token cache interface
│ ├── AgenticTokenCache.cs # Agentic token caching
@@ -350,28 +303,21 @@ src/Observability/Hosting/
// Program.cs
var builder = WebApplication.CreateBuilder(args);
-// 1. Configure observability tracing
-builder.Services.AddAgenticTracingExporter();
-builder.AddA365Tracing(config =>
-{
- config.WithSemanticKernel(); // if using SK
-});
-
-// 2. Register your agent
-builder.AddAgent();
+// Add observability services
+builder.Services.AddAgent365Observability(builder.Configuration);
var app = builder.Build();
-// 3. Register turn-level observability middleware on the adapter
-var adapter = app.Services.GetRequiredService();
-adapter.UseObservabilityMiddleware();
-
-// 4. (Optional) Add ASP.NET Core per-request baggage middleware
+// Add baggage middleware early in pipeline
app.UseObservabilityRequestContext(ctx =>
{
+ // Extract from JWT claims
var tenantId = ctx.User?.FindFirst("tid")?.Value
?? ctx.User?.FindFirst("tenant_id")?.Value;
+
+ // Extract from custom header
var agentId = ctx.Request.Headers["X-Agent-Id"].FirstOrDefault();
+
return (tenantId, agentId);
});
diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/ObservabilityMiddlewareExtensionsTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/ObservabilityMiddlewareExtensionsTests.cs
deleted file mode 100644
index 0bee9690..00000000
--- a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/ObservabilityMiddlewareExtensionsTests.cs
+++ /dev/null
@@ -1,98 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using FluentAssertions;
-using Microsoft.Agents.A365.Observability.Hosting.Middleware;
-using Microsoft.Agents.Builder;
-using Moq;
-
-namespace Microsoft.Agents.A365.Observability.Hosting.Tests.Middleware;
-
-[TestClass]
-public class ObservabilityMiddlewareExtensionsTests
-{
- [TestMethod]
- public void UseObservabilityMiddleware_RegistersBothMiddlewares_ByDefault()
- {
- // Arrange
- var mockAdapter = new Mock();
- mockAdapter.Setup(a => a.Use(It.IsAny())).Returns(mockAdapter.Object);
-
- // Act
- mockAdapter.Object.UseObservabilityMiddleware();
-
- // Assert
- mockAdapter.Verify(a => a.Use(It.IsAny()), Times.Once);
- mockAdapter.Verify(a => a.Use(It.IsAny()), Times.Once);
- }
-
- [TestMethod]
- public void UseObservabilityMiddleware_RegistersBaggageOnly_WhenOutputLoggingDisabled()
- {
- // Arrange
- var mockAdapter = new Mock();
- mockAdapter.Setup(a => a.Use(It.IsAny())).Returns(mockAdapter.Object);
-
- // Act
- mockAdapter.Object.UseObservabilityMiddleware(enableOutputLogging: false);
-
- // Assert
- mockAdapter.Verify(a => a.Use(It.IsAny()), Times.Once);
- mockAdapter.Verify(a => a.Use(It.IsAny()), Times.Never);
- }
-
- [TestMethod]
- public void UseObservabilityMiddleware_RegistersOutputLoggingOnly_WhenBaggageDisabled()
- {
- // Arrange
- var mockAdapter = new Mock();
- mockAdapter.Setup(a => a.Use(It.IsAny())).Returns(mockAdapter.Object);
-
- // Act
- mockAdapter.Object.UseObservabilityMiddleware(enableBaggage: false);
-
- // Assert
- mockAdapter.Verify(a => a.Use(It.IsAny()), Times.Never);
- mockAdapter.Verify(a => a.Use(It.IsAny()), Times.Once);
- }
-
- [TestMethod]
- public void UseObservabilityMiddleware_RegistersNothing_WhenBothDisabled()
- {
- // Arrange
- var mockAdapter = new Mock();
- mockAdapter.Setup(a => a.Use(It.IsAny())).Returns(mockAdapter.Object);
-
- // Act
- mockAdapter.Object.UseObservabilityMiddleware(enableBaggage: false, enableOutputLogging: false);
-
- // Assert
- mockAdapter.Verify(a => a.Use(It.IsAny()), Times.Never);
- }
-
- [TestMethod]
- public void UseObservabilityMiddleware_ThrowsOnNullAdapter()
- {
- // Arrange
- IChannelAdapter adapter = null!;
-
- // Act & Assert
- var act = () => adapter.UseObservabilityMiddleware();
- act.Should().Throw()
- .WithParameterName("adapter");
- }
-
- [TestMethod]
- public void UseObservabilityMiddleware_ReturnsAdapter_ForChaining()
- {
- // Arrange
- var mockAdapter = new Mock();
- mockAdapter.Setup(a => a.Use(It.IsAny())).Returns(mockAdapter.Object);
-
- // Act
- var result = mockAdapter.Object.UseObservabilityMiddleware();
-
- // Assert
- result.Should().BeSameAs(mockAdapter.Object);
- }
-}
From f66cb82e5f9409ac3ad82d8de0b8fe596c460223 Mon Sep 17 00:00:00 2001
From: Nikhil Navakiran
Date: Thu, 5 Mar 2026 00:07:44 +0530
Subject: [PATCH 6/8] Update
src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../Hosting/Middleware/OutputLoggingMiddleware.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs b/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
index bf495d94..78545668 100644
--- a/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
+++ b/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
@@ -172,9 +172,9 @@ private static SendActivitiesHandler CreateSendHandler(
// Read parent span lazily so the agent handler can set it during logic()
string? parentId = null;
- if (turnContext.StackState.ContainsKey(A365ParentSpanKey))
+ if (turnContext.StackState.TryGetValue(A365ParentSpanKey, out var parentSpanValue) && parentSpanValue is not null)
{
- parentId = turnContext.StackState[A365ParentSpanKey]?.ToString();
+ parentId = parentSpanValue.ToString();
}
var outputScope = OutputScope.Start(
From 96b2f2f203d968281f6987a494574261c2ed7aef Mon Sep 17 00:00:00 2001
From: Nikhil Navakiran
Date: Thu, 5 Mar 2026 00:08:48 +0530
Subject: [PATCH 7/8] Update
src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../Middleware/OutputLoggingMiddleware.cs | 19 +++----------------
1 file changed, 3 insertions(+), 16 deletions(-)
diff --git a/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs b/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
index 78545668..a123e9ab 100644
--- a/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
+++ b/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
@@ -181,27 +181,14 @@ private static SendActivitiesHandler CreateSendHandler(
agentDetails: agentDetails,
tenantDetails: tenantDetails,
response: new Response(messages),
+ conversationId: conversationId,
+ sourceMetadata: sourceMetadata,
+ callerDetails: callerDetails,
parentId: parentId);
try
{
- outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiConversationIdKey, conversationId);
outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiExecutionTypeKey, executionType);
-
- if (sourceMetadata != null)
- {
- outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiChannelNameKey, sourceMetadata.Name);
- outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiChannelLinkKey, sourceMetadata.Description);
- }
-
- if (callerDetails != null)
- {
- outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiCallerIdKey, callerDetails.CallerId);
- outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiCallerUpnKey, callerDetails.CallerUpn);
- outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiCallerNameKey, callerDetails.CallerName);
- outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiCallerTenantIdKey, callerDetails.TenantId);
- }
-
return await nextSend().ConfigureAwait(false);
}
catch (Exception ex)
From 1b0b29d71fe534448e7c2cb06cf9f7608f819f23 Mon Sep 17 00:00:00 2001
From: "Nikhil Chitlur Navakiran (from Dev Box)"
Date: Thu, 5 Mar 2026 00:15:31 +0530
Subject: [PATCH 8/8] implement PR agent suggestion
---
src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs b/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
index a123e9ab..f60756d3 100644
--- a/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
+++ b/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
@@ -172,7 +172,7 @@ private static SendActivitiesHandler CreateSendHandler(
// Read parent span lazily so the agent handler can set it during logic()
string? parentId = null;
- if (turnContext.StackState.TryGetValue(A365ParentSpanKey, out var parentSpanValue) && parentSpanValue is not null)
+ if (turnContext.StackState.TryGetValue(A365ParentSpanKey, out var parentSpanValue) && parentSpanValue != null)
{
parentId = parentSpanValue.ToString();
}