-
Notifications
You must be signed in to change notification settings - Fork 5
Add OutputScope, BaggageMiddleware, OutputLoggingMiddleware, and ObservabilityHostingManager #210
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
Changes from all commits
cf01843
ccf4db5
003cb1f
bfec932
edd0274
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| import { TurnContext, Middleware } from '@microsoft/agents-hosting'; | ||
| import { ActivityTypes, ActivityEventNames } from '@microsoft/agents-activity'; | ||
| import { BaggageBuilder } from '@microsoft/agents-a365-observability'; | ||
| import { | ||
| getExecutionTypePair, | ||
| getCallerBaggagePairs, | ||
| getTargetAgentBaggagePairs, | ||
| getTenantIdPair, | ||
| getSourceMetadataBaggagePairs, | ||
| getConversationIdAndItemLinkPairs, | ||
| } from '../utils/TurnContextUtils'; | ||
|
|
||
| /** | ||
| * Middleware that propagates OpenTelemetry baggage context derived from TurnContext. | ||
| * Async replies (ContinueConversation) are passed through without baggage setup. | ||
| */ | ||
| export class BaggageMiddleware implements Middleware { | ||
|
|
||
| async onTurn(context: TurnContext, next: () => Promise<void>): Promise<void> { | ||
| const isAsyncReply = | ||
| context.activity?.type === ActivityTypes.Event && | ||
| context.activity?.name === ActivityEventNames.ContinueConversation; | ||
|
|
||
| if (isAsyncReply) { | ||
| await next(); | ||
| return; | ||
| } | ||
|
|
||
| const baggageScope = new BaggageBuilder() | ||
| .setPairs(getCallerBaggagePairs(context)) | ||
| .setPairs(getTargetAgentBaggagePairs(context)) | ||
| .setPairs(getTenantIdPair(context)) | ||
| .setPairs(getSourceMetadataBaggagePairs(context)) | ||
| .setPairs(getConversationIdAndItemLinkPairs(context)) | ||
| .setPairs(getExecutionTypePair(context)) | ||
| .build(); | ||
|
|
||
| await baggageScope.run(async () => { | ||
| await next(); | ||
| }); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| import { Middleware } from '@microsoft/agents-hosting'; | ||
| import { logger } from '@microsoft/agents-a365-observability'; | ||
| import { BaggageMiddleware } from './BaggageMiddleware'; | ||
| import { OutputLoggingMiddleware } from './OutputLoggingMiddleware'; | ||
|
|
||
| /** | ||
| * Configuration options for the hosting observability layer. | ||
| */ | ||
| export interface ObservabilityHostingOptions { | ||
| /** Enable baggage propagation middleware. Defaults to true. */ | ||
| enableBaggage?: boolean; | ||
|
|
||
| /** Enable output logging middleware for tracing outgoing messages. Defaults to false. */ | ||
| enableOutputLogging?: boolean; | ||
| } | ||
|
|
||
| /** | ||
| * Manager for configuring hosting-layer observability middleware. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const manager = new ObservabilityHostingManager(); | ||
| * manager.configure(adapter, { enableOutputLogging: true }); | ||
| * ``` | ||
| */ | ||
| export class ObservabilityHostingManager { | ||
| private _configured = false; | ||
|
|
||
| /** | ||
| * Registers observability middleware on the adapter. | ||
| * Subsequent calls are ignored. | ||
| */ | ||
| configure( | ||
| adapter: { use(...middlewares: Array<Middleware>): void }, | ||
| options: ObservabilityHostingOptions | ||
| ): void { | ||
| if (this._configured) { | ||
| logger.warn('[ObservabilityHostingManager] Already configured. Subsequent configure() calls are ignored.'); | ||
| return; | ||
| } | ||
|
|
||
| const enableBaggage = options.enableBaggage !== false; | ||
| const enableOutputLogging = options.enableOutputLogging === true; | ||
|
|
||
| if (enableBaggage) { | ||
| adapter.use(new BaggageMiddleware()); | ||
| logger.info('[ObservabilityHostingManager] BaggageMiddleware registered.'); | ||
| } | ||
| if (enableOutputLogging) { | ||
| adapter.use(new OutputLoggingMiddleware()); | ||
| logger.info('[ObservabilityHostingManager] OutputLoggingMiddleware registered.'); | ||
| } | ||
|
|
||
| logger.info(`[ObservabilityHostingManager] Configured. Baggage: ${enableBaggage}, OutputLogging: ${enableOutputLogging}.`); | ||
| this._configured = true; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| import { TurnContext, Middleware, SendActivitiesHandler } from '@microsoft/agents-hosting'; | ||
| import { | ||
| OutputScope, | ||
| AgentDetails, | ||
| TenantDetails, | ||
| CallerDetails, | ||
| ParentSpanRef, | ||
| logger, | ||
| } from '@microsoft/agents-a365-observability'; | ||
| import { ScopeUtils } from '../utils/ScopeUtils'; | ||
|
|
||
| /** | ||
| * TurnState key for the parent span reference. | ||
| * Set this in `turnState` to link OutputScope spans as children of an InvokeAgentScope. | ||
| */ | ||
| export const A365_PARENT_SPAN_KEY = 'A365ParentSpanId'; | ||
|
||
|
|
||
| /** | ||
| * Middleware that creates {@link OutputScope} spans for outgoing messages. | ||
| * Links to a parent span when {@link A365_PARENT_SPAN_KEY} is set in turnState. | ||
| * | ||
| * **Privacy note:** Outgoing message content is captured verbatim | ||
| * as span attributes and exported to the configured telemetry backend. | ||
| */ | ||
| export class OutputLoggingMiddleware implements Middleware { | ||
|
|
||
| async onTurn(context: TurnContext, next: () => Promise<void>): Promise<void> { | ||
| const agentDetails = ScopeUtils.deriveAgentDetails(context); | ||
| const tenantDetails = ScopeUtils.deriveTenantDetails(context); | ||
|
|
||
| if (!agentDetails || !tenantDetails) { | ||
| await next(); | ||
| return; | ||
| } | ||
|
|
||
| const callerDetails = ScopeUtils.deriveCallerDetails(context); | ||
| const conversationId = ScopeUtils.deriveConversationId(context); | ||
| const sourceMetadata = ScopeUtils.deriveSourceMetadataObject(context); | ||
|
|
||
| context.onSendActivities( | ||
| this._createSendHandler(context, agentDetails, tenantDetails, callerDetails, conversationId, sourceMetadata) | ||
| ); | ||
|
|
||
| await next(); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a send handler that wraps outgoing messages in OutputScope spans. | ||
| * Reads parent span ref lazily so the agent handler can set it during `next()`. | ||
| */ | ||
| private _createSendHandler( | ||
| turnContext: TurnContext, | ||
| agentDetails: AgentDetails, | ||
| tenantDetails: TenantDetails, | ||
| callerDetails?: CallerDetails, | ||
| conversationId?: string, | ||
| sourceMetadata?: { name?: string; description?: string }, | ||
| ): SendActivitiesHandler { | ||
| return async (_ctx, activities, sendNext) => { | ||
| const messages = activities | ||
| .filter((a) => a.type === 'message' && a.text) | ||
| .map((a) => a.text!); | ||
|
|
||
| if (messages.length === 0) { | ||
| return await sendNext(); | ||
| } | ||
|
|
||
| const parentSpanRef: ParentSpanRef | undefined = turnContext.turnState.get(A365_PARENT_SPAN_KEY); | ||
| if (!parentSpanRef) { | ||
nikhilNava marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| logger.warn( | ||
fpfp100 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| `[OutputLoggingMiddleware] No parent span ref in turnState under '${A365_PARENT_SPAN_KEY}'. OutputScope will not be linked to a parent.` | ||
| ); | ||
| } | ||
|
|
||
| const outputScope = OutputScope.start( | ||
| { messages }, | ||
| agentDetails, | ||
| tenantDetails, | ||
| callerDetails, | ||
| conversationId, | ||
| sourceMetadata, | ||
| undefined, | ||
| parentSpanRef, | ||
| ); | ||
| try { | ||
| return await sendNext(); | ||
| } catch (error) { | ||
| outputScope.recordError( | ||
| error instanceof Error ? error : new Error(typeof error === 'string' ? error : JSON.stringify(error)) | ||
| ); | ||
| throw error; | ||
| } finally { | ||
| outputScope.dispose(); | ||
| } | ||
| }; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.