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(); }