-
Notifications
You must be signed in to change notification settings - Fork 8
Add middleware to baggage and output spans #209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
abeca5f
Initial plan
Copilot b047dc8
Add BaggageTurnMiddleware and OutputLoggingMiddleware with tests
Copilot 342c8c2
Address code review: wrap OutputScope in try/finally, fix nullable ha…
Copilot 68e10ac
Merge branch 'main' into copilot/implement-baggage-output-middleware
nikhilNava 8eca2d2
Add UseObservabilityMiddleware extension for IChannelAdapter with tes…
Copilot 216bb9b
Revert ObservabilityMiddlewareExtensions: remove extension, tests, an…
Copilot f66cb82
Update src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
nikhilNava 96b2f2f
Update src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
nikhilNava 1b0b29d
implement PR agent suggestion
nikhilc-microsoft File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
46 changes: 46 additions & 0 deletions
46
src/Observability/Hosting/Middleware/BaggageTurnMiddleware.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,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 | ||
| { | ||
| /// <summary> | ||
| /// Bot Framework middleware that propagates OpenTelemetry baggage context | ||
| /// derived from <see cref="ITurnContext"/>. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Async replies (ContinueConversation events) are passed through without | ||
| /// baggage setup because their context is established by the originating turn. | ||
| /// </remarks> | ||
| public sealed class BaggageTurnMiddleware : IMiddleware | ||
| { | ||
| /// <inheritdoc/> | ||
| 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); | ||
| } | ||
| } | ||
| } | ||
| } |
206 changes: 206 additions & 0 deletions
206
src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| // 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 | ||
| { | ||
| /// <summary> | ||
| /// Bot Framework middleware that creates <see cref="OutputScope"/> spans | ||
| /// for outgoing messages. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// Links to a parent span when <see cref="A365ParentSpanKey"/> is set in | ||
| /// <see cref="ITurnContext.StackState"/>. | ||
| /// </para> | ||
| /// <para> | ||
| /// <b>Privacy note:</b> Outgoing message content is captured verbatim as | ||
| /// span attributes and exported to the configured telemetry backend. | ||
| /// </para> | ||
| /// </remarks> | ||
| public sealed class OutputLoggingMiddleware : IMiddleware | ||
| { | ||
| /// <summary> | ||
| /// The <see cref="ITurnContext.StackState"/> key used to store the parent | ||
| /// span reference. Set this value to a W3C traceparent string | ||
| /// (e.g. <c>"00-{trace_id}-{span_id}-{trace_flags}"</c>) to link | ||
| /// <see cref="OutputScope"/> spans as children of an | ||
| /// <see cref="InvokeAgentScope"/>. | ||
| /// </summary> | ||
| public const string A365ParentSpanKey = "A365ParentSpanId"; | ||
|
|
||
| /// <inheritdoc/> | ||
| 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<string>(); | ||
| foreach (var a in activities) | ||
| { | ||
| if (string.Equals(a.Type, ActivityTypes.Message, StringComparison.OrdinalIgnoreCase) | ||
| && !string.IsNullOrEmpty(a.Text)) | ||
| { | ||
| messages.Add(a.Text); | ||
| } | ||
| } | ||
nikhilNava marked this conversation as resolved.
Dismissed
Show dismissed
Hide dismissed
|
||
|
|
||
| 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.TryGetValue(A365ParentSpanKey, out var parentSpanValue) && parentSpanValue != null) | ||
| { | ||
| parentId = parentSpanValue.ToString(); | ||
| } | ||
|
|
||
| var outputScope = OutputScope.Start( | ||
nikhilNava marked this conversation as resolved.
Dismissed
Show dismissed
Hide dismissed
|
||
| agentDetails: agentDetails, | ||
| tenantDetails: tenantDetails, | ||
| response: new Response(messages), | ||
| conversationId: conversationId, | ||
| sourceMetadata: sourceMetadata, | ||
| callerDetails: callerDetails, | ||
| parentId: parentId); | ||
|
|
||
| try | ||
| { | ||
| outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiExecutionTypeKey, executionType); | ||
| return await nextSend().ConfigureAwait(false); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| outputScope.RecordError(ex); | ||
| throw; | ||
| } | ||
| finally | ||
| { | ||
| outputScope.Dispose(); | ||
| } | ||
| }; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.