From a184beb6021f09c099bfa261ba506fa8e090ec12 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:06:41 -0700 Subject: [PATCH 1/2] Fix A365 review issues: wire processors into distro, fix types, improve tests - Wire A365SpanProcessor (baggage enrichment) and PerRequestSpanProcessor into distro.ts when A365 is enabled, replacing SimpleSpanProcessor - Fix PerRequestSpanProcessor.onStart to accept Span instead of ReadableSpan per the OTel SpanProcessor interface contract - Remove 'implements Disposable' from OpenTelemetryScope and BaggageScope (ES2022 lib does not include esnext.disposable) - Widen isParentSpanRef parameter type from ParentContext to unknown - Add trim() to parseEnvBoolean for whitespace-resilient env var parsing - Remove unwired A365_OBSERVABILITY_LOG_LEVEL from docs and config - Add attribute assertions to A365SpanProcessor tests - Add OutputScope unit tests (creation, tenantId validation, message recording, overwrite, raw dict, user details) --- MIGRATION_A365.md | 1 - README.md | 1 - src/a365/configuration/A365Configuration.ts | 3 +- src/a365/context.ts | 6 +- src/a365/middleware/BaggageBuilder.ts | 2 +- .../processors/PerRequestSpanProcessor.ts | 21 +++- src/a365/scopes/OpenTelemetryScope.ts | 2 +- src/distro/distro.ts | 18 +++- .../unit/a365/a365SpanProcessor.test.ts | 64 +++++-------- test/internal/unit/a365/scopes.test.ts | 95 +++++++++++++++++++ 10 files changed, 154 insertions(+), 59 deletions(-) diff --git a/MIGRATION_A365.md b/MIGRATION_A365.md index 4fdd421c..795d7913 100644 --- a/MIGRATION_A365.md +++ b/MIGRATION_A365.md @@ -140,7 +140,6 @@ Environment variable names are **unchanged** from Agent365-nodejs: | `A365_OBSERVABILITY_SCOPES_OVERRIDE` | Space-separated list of OAuth scopes | | `A365_OBSERVABILITY_DOMAIN_OVERRIDE` | Override service domain | | `CLUSTER_CATEGORY` | Cluster category (`prod`, `dev`, `test`, etc.) | -| `A365_OBSERVABILITY_LOG_LEVEL` | SDK log level (`none`, `info`, `warn`, `error`) | | `A365_PER_REQUEST_MAX_TRACES` | Max buffered traces (default: `1000`) | | `A365_PER_REQUEST_MAX_SPANS_PER_TRACE` | Max spans per trace (default: `5000`) | | `A365_PER_REQUEST_MAX_CONCURRENT_EXPORTS` | Max concurrent exports (default: `20`) | diff --git a/README.md b/README.md index 1a0d4a90..9d059595 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,6 @@ A365 options can also be set via environment variables (highest precedence): | `A365_OBSERVABILITY_SCOPES_OVERRIDE` | Space-separated list of OAuth scopes | | `A365_OBSERVABILITY_DOMAIN_OVERRIDE` | Override service domain | | `CLUSTER_CATEGORY` | Override cluster category | -| `A365_OBSERVABILITY_LOG_LEVEL` | SDK log level (`none`, `info`, `warn`, `error`) | ### Example diff --git a/src/a365/configuration/A365Configuration.ts b/src/a365/configuration/A365Configuration.ts index e7e33a90..f29c50d0 100644 --- a/src/a365/configuration/A365Configuration.ts +++ b/src/a365/configuration/A365Configuration.ts @@ -17,7 +17,7 @@ import { JsonConfig } from "../../shared/jsonConfig.js"; */ function parseEnvBoolean(envValue: string | undefined): boolean { if (!envValue) return false; - return ["true", "1", "yes", "on"].includes(envValue.toLowerCase()); + return ["true", "1", "yes", "on"].includes(envValue.trim().toLowerCase()); } /** @@ -30,7 +30,6 @@ export const A365_ENV_VARS = { AUTH_SCOPES: "A365_OBSERVABILITY_SCOPES_OVERRIDE", DOMAIN: "A365_OBSERVABILITY_DOMAIN_OVERRIDE", CLUSTER_CATEGORY: "CLUSTER_CATEGORY", - LOG_LEVEL: "A365_OBSERVABILITY_LOG_LEVEL", } as const; const DEFAULT_AUTH_SCOPE = "https://api.powerplatform.com/.default"; diff --git a/src/a365/context.ts b/src/a365/context.ts index 303f0ed0..59f3ddcc 100644 --- a/src/a365/context.ts +++ b/src/a365/context.ts @@ -16,7 +16,7 @@ import { context, trace, propagation } from "@opentelemetry/api"; import type { Context, SpanContext } from "@opentelemetry/api"; import { TraceFlags } from "@opentelemetry/api"; -import type { ParentSpanRef, ParentContext } from "./contracts.js"; +import type { ParentSpanRef } from "./contracts.js"; import { Logger } from "../shared/logging/index.js"; // --------------------------------------------------------------------------- @@ -36,10 +36,10 @@ function isValidSpanId(spanId: string): boolean { // --------------------------------------------------------------------------- /** Type guard to distinguish a ParentSpanRef from an OTel Context. */ -export function isParentSpanRef(value: ParentContext): value is ParentSpanRef { +export function isParentSpanRef(value: unknown): value is ParentSpanRef { if (typeof value !== "object" || value === null) return false; - const maybeCtx = value as Context; + const maybeCtx = value as Record; if ( typeof maybeCtx.getValue === "function" && typeof maybeCtx.setValue === "function" && diff --git a/src/a365/middleware/BaggageBuilder.ts b/src/a365/middleware/BaggageBuilder.ts index 72a6b5f5..dcc2d166 100644 --- a/src/a365/middleware/BaggageBuilder.ts +++ b/src/a365/middleware/BaggageBuilder.ts @@ -240,7 +240,7 @@ export class BaggageBuilder { * Manages the lifecycle of baggage values, setting them in the OTel context * and restoring the previous context when the scope ends. */ -export class BaggageScope implements Disposable { +export class BaggageScope { /** @internal Exposed for testing. */ readonly contextWithBaggage: Context; diff --git a/src/a365/processors/PerRequestSpanProcessor.ts b/src/a365/processors/PerRequestSpanProcessor.ts index 075ec6ed..2a7e30df 100644 --- a/src/a365/processors/PerRequestSpanProcessor.ts +++ b/src/a365/processors/PerRequestSpanProcessor.ts @@ -9,8 +9,8 @@ * Adapted from microsoft/Agent365-nodejs agents-a365-observability/src/tracing/PerRequestSpanProcessor.ts */ -import { context } from "@opentelemetry/api"; -import type { Context } from "@opentelemetry/api"; +import { context, trace } from "@opentelemetry/api"; +import type { Context, Span } from "@opentelemetry/api"; import type { ReadableSpan, SpanProcessor, SpanExporter } from "@opentelemetry/sdk-trace-base"; import { Logger } from "../../shared/logging/index.js"; @@ -20,6 +20,16 @@ function isRootSpan(span: ReadableSpan): boolean { return !span.parentSpanContext; } +/** + * Check whether this span is a root span by examining the parent context. + * In onStart, the SDK passes a live Span (not ReadableSpan), so we check + * whether the parent context already contains a span. + */ +function isRootSpanFromContext(ctx: Context): boolean { + const parentSpan = trace.getSpan(ctx); + return !parentSpan; +} + type TraceBuffer = { spans: ReadableSpan[]; openCount: number; @@ -103,7 +113,7 @@ export class PerRequestSpanProcessor implements SpanProcessor { ); } - onStart(span: ReadableSpan, ctx: Context): void { + onStart(span: Span, ctx: Context): void { const traceId = span.spanContext().traceId; let buf = this.traces.get(traceId); if (!buf) { @@ -131,13 +141,14 @@ export class PerRequestSpanProcessor implements SpanProcessor { } buf.openCount += 1; + const root = isRootSpanFromContext(ctx); logger.info( `[PerRequestSpanProcessor] Span start name=${span.name} traceId=${traceId} spanId=${span.spanContext().spanId}` + - ` root=${isRootSpan(span)} openCount=${buf.openCount}`, + ` root=${root} openCount=${buf.openCount}`, ); // Capture a context to export under. - if (isRootSpan(span)) { + if (root) { buf.rootCtx = ctx; } else { buf.rootCtx ??= ctx; diff --git a/src/a365/scopes/OpenTelemetryScope.ts b/src/a365/scopes/OpenTelemetryScope.ts index 2b151538..b516b6e2 100644 --- a/src/a365/scopes/OpenTelemetryScope.ts +++ b/src/a365/scopes/OpenTelemetryScope.ts @@ -30,7 +30,7 @@ import { Logger } from "../../shared/logging/index.js"; * * Subclasses: `InvokeAgentScope`, `ExecuteToolScope`, `InferenceScope`, `OutputScope`. */ -export abstract class OpenTelemetryScope implements Disposable { +export abstract class OpenTelemetryScope { private static readonly tracer = trace.getTracer(OpenTelemetryConstants.SOURCE_NAME); protected readonly span: Span; diff --git a/src/distro/distro.ts b/src/distro/distro.ts index 6350d812..4c57e80e 100644 --- a/src/distro/distro.ts +++ b/src/distro/distro.ts @@ -9,7 +9,6 @@ import type { MetricReader, ViewOptions } from "@opentelemetry/sdk-metrics"; import { type SpanProcessor, BatchSpanProcessor, - SimpleSpanProcessor, } from "@opentelemetry/sdk-trace-base"; import type { LogRecordProcessor } from "@opentelemetry/sdk-logs"; @@ -22,7 +21,12 @@ import { patchOpenTelemetryInstrumentationEnable } from "../utils/opentelemetryI import { parseResourceDetectorsFromEnvVar } from "../utils/common.js"; import { setupAzureMonitorComponents } from "../azureMonitorSetup.js"; import { isOtlpEnabled, createOtlpComponents } from "../otlp/index.js"; -import { A365Configuration, Agent365Exporter } from "../a365/index.js"; +import { + A365Configuration, + Agent365Exporter, + A365SpanProcessor, + PerRequestSpanProcessor, +} from "../a365/index.js"; import type { MicrosoftOpenTelemetryOptions } from "../types.js"; import { MICROSOFT_OPENTELEMETRY_VERSION } from "../types.js"; @@ -111,10 +115,14 @@ export function useMicrosoftOpenTelemetry(options?: MicrosoftOpenTelemetryOption domainOverride: a365Config.domainOverride, tokenResolver: a365Config.tokenResolver, }); - const a365Processor = a365Config.perRequestExport - ? new SimpleSpanProcessor(a365Exporter) + // A365SpanProcessor copies baggage (tenant, agent, session, etc.) to span attributes + spanProcessors.push(new A365SpanProcessor()); + // PerRequestSpanProcessor buffers spans per trace and exports on root completion + // with the request's auth token; BatchSpanProcessor for standard batch export + const a365ExportProcessor = a365Config.perRequestExport + ? new PerRequestSpanProcessor(a365Exporter) : new BatchSpanProcessor(a365Exporter); - spanProcessors.push(a365Processor); + spanProcessors.push(a365ExportProcessor); } const views: ViewOptions[] = metricHandler.getViews().concat(customViews); diff --git a/test/internal/unit/a365/a365SpanProcessor.test.ts b/test/internal/unit/a365/a365SpanProcessor.test.ts index a99c95a2..b8fc739b 100644 --- a/test/internal/unit/a365/a365SpanProcessor.test.ts +++ b/test/internal/unit/a365/a365SpanProcessor.test.ts @@ -3,7 +3,6 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { context, propagation, SpanKind } from "@opentelemetry/api"; -import type { Span } from "@opentelemetry/api"; import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base"; import { @@ -43,16 +42,13 @@ describe("A365SpanProcessor", () => { const ctx = propagation.setBaggage(context.active(), baggage); const tracer = provider.getTracer("test"); - let testSpan: Span | undefined; - - context.with(ctx, () => { - testSpan = tracer.startSpan("test-span", { kind: SpanKind.CLIENT }); - if (testSpan) { - testSpan.end(); - } - }); + const testSpan = tracer.startSpan("test-span", { kind: SpanKind.CLIENT }, ctx); + testSpan.end(); - expect(testSpan).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const attrs = (testSpan as any)._attributes ?? (testSpan as any).attributes ?? {}; + expect(attrs[OpenTelemetryConstants.TENANT_ID_KEY]).toBe("tenant-123"); + expect(attrs[OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]).toBe("agent-789"); }); it("should copy sessionId from baggage to span", () => { @@ -105,18 +101,13 @@ describe("A365SpanProcessor", () => { const ctx = propagation.setBaggage(context.active(), baggage); const tracer = provider.getTracer("test"); - let testSpan: Span | undefined; - - context.with(ctx, () => { - testSpan = tracer.startSpan("invoke_agent test", { - kind: SpanKind.CLIENT, - }); - if (testSpan) { - testSpan.end(); - } - }); + const testSpan = tracer.startSpan("invoke_agent test", { kind: SpanKind.CLIENT }, ctx); + testSpan.end(); - expect(testSpan).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const attrs = (testSpan as any)._attributes ?? (testSpan as any).attributes ?? {}; + expect(attrs[OpenTelemetryConstants.TENANT_ID_KEY]).toBe("tenant-123"); + expect(attrs[OpenTelemetryConstants.USER_ID_KEY]).toBe("caller-456"); }); it("should not overwrite existing span attributes", () => { @@ -128,21 +119,18 @@ describe("A365SpanProcessor", () => { const ctx = propagation.setBaggage(context.active(), baggage); const tracer = provider.getTracer("test"); - let testSpan: Span | undefined; - - context.with(ctx, () => { - testSpan = tracer.startSpan("test-span", { + const testSpan = tracer.startSpan( + "test-span", + { kind: SpanKind.CLIENT, attributes: { [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-existing", }, - }); - if (testSpan) { - testSpan.end(); - } - }); + }, + ctx, + ); + testSpan.end(); - expect(testSpan).toBeDefined(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const attrs = (testSpan as any)._attributes ?? (testSpan as any).attributes ?? {}; expect(attrs[OpenTelemetryConstants.TENANT_ID_KEY]).toBe("tenant-existing"); @@ -155,16 +143,12 @@ describe("A365SpanProcessor", () => { const ctx = propagation.setBaggage(context.active(), baggage); const tracer = provider.getTracer("test"); - let testSpan: Span | undefined; - - context.with(ctx, () => { - testSpan = tracer.startSpan("test-span", { kind: SpanKind.CLIENT }); - if (testSpan) { - testSpan.end(); - } - }); + const testSpan = tracer.startSpan("test-span", { kind: SpanKind.CLIENT }, ctx); + testSpan.end(); - expect(testSpan).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const attrs = (testSpan as any)._attributes ?? (testSpan as any).attributes ?? {}; + expect(attrs[OpenTelemetryConstants.TENANT_ID_KEY]).toBeUndefined(); }); it("should set telemetry SDK attributes", () => { diff --git a/test/internal/unit/a365/scopes.test.ts b/test/internal/unit/a365/scopes.test.ts index 852c6e00..99bcf81f 100644 --- a/test/internal/unit/a365/scopes.test.ts +++ b/test/internal/unit/a365/scopes.test.ts @@ -15,6 +15,7 @@ import { ExecuteToolScope, InvokeAgentScope, InferenceScope, + OutputScope, OpenTelemetryScope, OpenTelemetryConstants, } from "../../../../src/a365/index.js"; @@ -25,6 +26,7 @@ import type { InferenceDetails, UserDetails, InputMessages, + OutputResponse, } from "../../../../src/a365/index.js"; import { InferenceOperationType, @@ -1148,6 +1150,99 @@ describe("Request content and message serialization (span attributes)", () => { }); }); + describe("OutputScope", () => { + it("should create scope with agent and request details", () => { + const spy = vi.spyOn(OpenTelemetryScope.prototype as any, "setTagMaybe"); + const response: OutputResponse = { + messages: "Hello from agent", + }; + const scope = OutputScope.start(testRequest, response, testAgentDetails); + + expect(scope).toBeInstanceOf(OutputScope); + const calls = spy.mock.calls.map((args) => ({ key: args[0], val: args[1] })); + expect(calls).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, + val: testRequest.conversationId, + }), + expect.objectContaining({ + key: OpenTelemetryConstants.CHANNEL_NAME_KEY, + val: testRequest.channel?.name, + }), + ]), + ); + scope.dispose(); + spy.mockRestore(); + }); + + it("should require tenantId on agentDetails", () => { + const response: OutputResponse = { messages: "test" }; + expect(() => + OutputScope.start(testRequest, response, { agentId: "a1" }), + ).toThrow("tenantId is required"); + }); + + it("should record output messages from response", () => { + const response: OutputResponse = { messages: "Hello user" }; + const scope = OutputScope.start(testRequest, response, testAgentDetails); + scope.dispose(); + + const attributes = getLastSpan().attributes; + const parsed = JSON.parse( + attributes[OpenTelemetryConstants.GEN_AI_OUTPUT_MESSAGES_KEY] as string, + ); + expect(parsed.version).toBe(A365_MESSAGE_SCHEMA_VERSION); + expect(parsed.messages).toHaveLength(1); + expect(parsed.messages[0].role).toBe("assistant"); + expect(parsed.messages[0].parts[0].content).toBe("Hello user"); + }); + + it("should allow overwriting output messages via recordOutputMessages", () => { + const response: OutputResponse = { messages: "initial" }; + const scope = OutputScope.start(testRequest, response, testAgentDetails); + scope.recordOutputMessages("updated output"); + scope.dispose(); + + const attributes = getLastSpan().attributes; + const parsed = JSON.parse( + attributes[OpenTelemetryConstants.GEN_AI_OUTPUT_MESSAGES_KEY] as string, + ); + expect(parsed.messages[0].parts[0].content).toBe("updated output"); + }); + + it("should handle raw dict as output messages", () => { + const response: OutputResponse = { + messages: { result: "tool output", score: 0.95 } as any, + }; + const scope = OutputScope.start(testRequest, response, testAgentDetails); + scope.dispose(); + + const attributes = getLastSpan().attributes; + const raw = attributes[OpenTelemetryConstants.GEN_AI_OUTPUT_MESSAGES_KEY] as string; + const parsed = JSON.parse(raw); + expect(parsed.result).toBe("tool output"); + expect(parsed.score).toBe(0.95); + }); + + it("should include user details when provided", () => { + const spy = vi.spyOn(OpenTelemetryScope.prototype as any, "setTagMaybe"); + const user: UserDetails = { userId: "user-1", userEmail: "u@test.com" }; + const response: OutputResponse = { messages: "test" }; + const scope = OutputScope.start(testRequest, response, testAgentDetails, user); + + const calls = spy.mock.calls.map((args) => ({ key: args[0], val: args[1] })); + expect(calls).toEqual( + expect.arrayContaining([ + expect.objectContaining({ key: OpenTelemetryConstants.USER_ID_KEY, val: "user-1" }), + expect.objectContaining({ key: OpenTelemetryConstants.USER_EMAIL_KEY, val: "u@test.com" }), + ]), + ); + scope.dispose(); + spy.mockRestore(); + }); + }); + describe("ExecuteToolScope – tool args and response serialization", () => { it("should serialize object arguments to span attribute", () => { const objArgs = { query: "GDPR", maxResults: 5 }; From 3507004a867c336a1dfd62dcf6fc5624573309f9 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:38:48 -0700 Subject: [PATCH 2/2] Update --- A365_IMPLEMENTATION_PLAN.md | 388 ------------------ PLANNING.md | 79 ++++ .../processors/PerRequestSpanProcessor.ts | 2 +- src/distro/distro.ts | 9 +- .../unit/a365/a365SpanProcessor.test.ts | 45 +- test/internal/unit/a365/scopes.test.ts | 11 +- 6 files changed, 120 insertions(+), 414 deletions(-) delete mode 100644 A365_IMPLEMENTATION_PLAN.md diff --git a/A365_IMPLEMENTATION_PLAN.md b/A365_IMPLEMENTATION_PLAN.md deleted file mode 100644 index 599c27e1..00000000 --- a/A365_IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,388 +0,0 @@ -# A365 Integration Implementation Plan - -## Overview - -This document details the plan for migrating the A365 observability runtime from `microsoft/Agent365-nodejs` into the Microsoft OpenTelemetry JavaScript distro (`@microsoft/opentelemetry`). Per PLANNING.md Phase 6 (A365 Convergence), A365 code is **migrated as source** — not consumed as an npm dependency — under a clearly defined internal module boundary (`_a365/`). - -This plan covers both **Phase 5 (GenAI Instrumentations)** and **Phase 6 (A365 Convergence)** from PLANNING.md. The GenAI instrumentations (OpenAI, OpenAI Agents SDK, LangChain) are Phase 5 deliverables but are sourced from A365's existing implementations, so they are coordinated here. The core A365 infrastructure (exporter, processors, scopes, hosting) is Phase 6. - -Phases 1–4 are now substantially complete: -- ~~**Phase 1 (Package Foundation):** Published as `@microsoft/opentelemetry`, with ESLint, Vitest, and CI configured.~~ -- **Phase 2 (Configuration Surface):** `MicrosoftOpenTelemetryOptions` is defined in `src/distro/types.ts` with a backend-scoped pattern (`azureMonitor?: AzureMonitorOpenTelemetryOptions`). The A365 scope (`a365?: A365Options`) follows this established pattern. Per PLANNING.md, A365-scoped configuration includes: A365 exporter endpoint, baggage extensions, and A365-specific span processors. -- **Phase 3 (Azure Monitor Migration):** Azure Monitor distro code has been fully migrated in-repo under `src/` (traces, metrics, logs, browser SDK loader, statsbeat, etc.). -- **Phase 4 (Core OTel Setup):** `useMicrosoftOpenTelemetry()` in `src/distro/distro.ts` orchestrates NodeSDK creation with Azure Monitor handlers. The entry point has explicit `// TODO` placeholders for OTLP and A365 integration. - -This plan builds on the established architecture. ---- - -## Source Packages (Agent365-nodejs) - -The following packages from [`microsoft/Agent365-nodejs/packages/`](https://github.com/microsoft/Agent365-nodejs/tree/main/packages) are in scope for migration: - -| Package | Purpose | Key Files | -|---------|---------|-----------| -| `agents-a365-observability` | Core observability SDK — exporter, span processors, scopes, context propagation, configuration, constants/contracts | `ObservabilityBuilder.ts`, `ObservabilityManager.ts`, `configuration/`, `tracing/`, `internal/`, `utils/` | -| `agents-a365-observability-hosting` | Hosting integration — middleware, token caching, baggage propagation | `middleware/`, `caching/`, `utils/` | -| `agents-a365-observability-extensions-openai` | OpenAI Agents SDK instrumentation | `OpenAIAgentsTraceInstrumentor.ts`, `OpenAIAgentsTraceProcessor.ts`, `Constants.ts`, `Utils.ts`, `configuration/` | -| `agents-a365-observability-extensions-langchain` | LangChain instrumentation via callback handler patching | `LangChainTraceInstrumentor.ts`, `tracer.ts`, `Utils.ts` | -| `agents-a365-runtime` | Runtime configuration utilities (partial — only what observability depends on) | `environment-utils.ts`, `configuration/`, `ClusterCategory` | - ---- - -## Target Module Structure - -All A365 code lives under `src/_a365/` with clear internal module boundaries. GenAI instrumentations (Phase 5) live in separate top-level internal modules (`_openai/`, `_langchain/`) per PLANNING.md convention, but are sourced from A365's existing implementations and coordinated here. The setup orchestrator follows the established pattern of `src/azureMonitorSetup.ts` (a top-level setup helper called from `src/distro/distro.ts`): - -``` -src/ -├── index.ts # Re-exports from distro; backward-compat useAzureMonitor() -├── distro/ -│ ├── distro.ts # useMicrosoftOpenTelemetry() entry point — calls a365Setup -│ ├── types.ts # MicrosoftOpenTelemetryOptions (a365 scope here) -│ └── index.ts # Barrel export for distro -├── azureMonitorSetup.ts # Azure Monitor setup helper (existing) -├── a365Setup.ts # A365 setup orchestrator (wires _a365/ into providers) -│ -├── _a365/ # Internal A365 module boundary (Phase 6) -│ ├── index.ts # Internal barrel export -│ │ -│ ├── configuration/ -│ │ ├── A365Configuration.ts # From ObservabilityConfiguration + RuntimeConfiguration -│ │ └── A365ConfigurationOptions.ts -│ │ -│ ├── exporter/ -│ │ ├── Agent365Exporter.ts # Custom SpanExporter → A365 service -│ │ └── Agent365ExporterOptions.ts -│ │ -│ ├── processors/ -│ │ ├── PerRequestSpanProcessor.ts # Buffers spans per trace, exports on root completion -│ │ ├── SpanProcessor.ts # Generic span processing -│ │ └── BaggageSpanProcessor.ts # Copies baggage items to span attributes -│ │ -│ ├── scopes/ -│ │ ├── OpenTelemetryScope.ts # Base span scope (Disposable) -│ │ ├── InvokeAgentScope.ts # Agent invocation spans -│ │ ├── ExecuteToolScope.ts # Tool execution spans -│ │ ├── InferenceScope.ts # LLM inference spans -│ │ └── OutputScope.ts # Output message spans -│ │ -│ ├── context/ -│ │ ├── tokenContext.ts # runWithExportToken / getExportToken -│ │ ├── parentSpanContext.ts # ParentSpanRef, manual parent linking -│ │ └── traceContextPropagation.ts # W3C traceparent inject/extract -│ │ -│ ├── contracts/ -│ │ ├── messages.ts # ChatMessage, InputMessages, OutputMessages -│ │ ├── details.ts # AgentDetails, UserDetails, SpanDetails -│ │ └── types.ts # MessageRole, MessagePart union types -│ │ -│ ├── constants.ts # OpenTelemetryConstants (semantic attribute keys) -│ │ -│ ├── hosting/ -│ │ ├── BaggageMiddleware.ts -│ │ ├── OutputLoggingMiddleware.ts -│ │ ├── ObservabilityHostingManager.ts -│ │ └── AgenticTokenCache.ts -│ │ -│ └── utils/ -│ ├── baggageBuilder.ts -│ ├── scopeUtils.ts -│ └── logger.ts -│ -├── _openai/ # OpenAI GenAI instrumentations (Phase 5 — sourced from A365) -│ ├── OpenAIAgentsTraceInstrumentor.ts -│ ├── OpenAIAgentsTraceProcessor.ts -│ ├── Constants.ts -│ ├── Utils.ts -│ └── configuration/ -│ └── OpenAIObservabilityConfiguration.ts -│ -└── _langchain/ # LangChain GenAI instrumentations (Phase 5 — sourced from A365) - ├── LangChainTraceInstrumentor.ts - ├── tracer.ts - └── Utils.ts -``` - ---- - -## Implementation Tasks - -### Task 1: Migrate A365 Configuration Layer - -**Source:** `agents-a365-observability/src/configuration/` + `agents-a365-runtime/src/configuration/` - -**Work:** -- Create `_a365/configuration/A365Configuration.ts` — merge `ObservabilityConfiguration` and the runtime config it extends -- Extract only the config properties needed by observability (drop runtime-only concerns like authorization service, Power Platform API discovery) -- Define `A365ConfigurationOptions` interface for the distro's public `a365` config scope -- Map environment variables to the distro convention: - -| A365 Original Env Var | Distro Env Var | -|----------------------|----------------| -| `ENABLE_A365_OBSERVABILITY_EXPORTER` | `MICROSOFT_OTEL_A365_EXPORTER_ENABLED` | -| `ENABLE_A365_OBSERVABILITY_PER_REQUEST_EXPORT` | `MICROSOFT_OTEL_A365_PER_REQUEST_EXPORT` | -| `A365_OBSERVABILITY_SCOPES_OVERRIDE` | `MICROSOFT_OTEL_A365_AUTH_SCOPES` | -| `A365_OBSERVABILITY_DOMAIN_OVERRIDE` | `MICROSOFT_OTEL_A365_DOMAIN` | -| `A365_OBSERVABILITY_LOG_LEVEL` | `MICROSOFT_OTEL_A365_LOG_LEVEL` | -| `AGENT_CLUSTER_CATEGORY` | `MICROSOFT_OTEL_A365_CLUSTER_CATEGORY` | - -- Integrate into the distro's `src/distro/types.ts` as the `a365` scope (following the established `azureMonitor` scoping pattern): - -```typescript -// In src/distro/types.ts -export interface MicrosoftOpenTelemetryOptions { - // ── Global options (already defined) ── - resource?: Resource; - samplingRatio?: number; - tracesPerSecond?: number; - instrumentationOptions?: InstrumentationOptions; - logRecordProcessors?: LogRecordProcessor[]; - spanProcessors?: SpanProcessor[]; - metricReaders?: MetricReader[]; - views?: ViewOptions[]; - - // ── Backend-scoped options ── - azureMonitor?: AzureMonitorOpenTelemetryOptions; // (already defined) - a365?: A365Options; // NEW -} - -export interface A365Options { - enabled?: boolean; - tokenResolver?: (agentId: string, tenantId: string) => string | Promise; - clusterCategory?: ClusterCategory; - domainOverride?: string; - authScopes?: string[]; - perRequestExport?: boolean; - baggage?: { - propagationEnabled?: boolean; // Enable baggage propagation middleware - enrichSpans?: boolean; // Copy baggage items to span attributes - }; - hosting?: { - enabled?: boolean; // Enable hosting middleware (requires @microsoft/agents-hosting) - }; -} -``` - -Instrumentation options for GenAI frameworks (OpenAI Agents, LangChain) are configured at the distro level via `instrumentationOptions` in `MicrosoftOpenTelemetryOptions`, not under `a365`. - -**Depends on:** Phase 2 configuration surface (now complete — `MicrosoftOpenTelemetryOptions` with backend scoping is established in `src/distro/types.ts`) - ---- - -### Task 2: Migrate Agent365 Exporter - -**Source:** `agents-a365-observability/src/tracing/exporter/` - -**Work:** -- Copy `Agent365Exporter.ts` and `Agent365ExporterOptions.ts` into `_a365/exporter/` -- The exporter implements `SpanExporter` from `@opentelemetry/sdk-trace-base` -- Key behavior to preserve: - - Span partitioning by `(tenantId, agentId)` tuples - - OTLP-like JSON payload construction (`resourceSpans → scopeSpans → spans`) - - Dual endpoint styles: `/observability/...` and `/observabilityService/...` (S2S) - - Token resolution: batch export via `tokenResolver()`, per-request via context `getExportToken()` - - Automatic retry with exponential backoff - - HTTP request timeout support -- **Adaptation:** Replace dependency on `agents-a365-runtime` config with the distro's `A365Configuration` -- **Adaptation:** Replace internal logger with distro's logging approach (`Logger` from `src/shared/logging/`) -- Add new dependency: native `fetch` (Node 18+) or `undici` for HTTP — check what the exporter currently uses - -**Depends on:** Task 1 - ---- - -### Task 3: Migrate Span Processors - -**Source:** `agents-a365-observability/src/tracing/` - -**Work:** -- Copy `PerRequestSpanProcessor.ts` → `_a365/processors/PerRequestSpanProcessor.ts` - - Buffers spans per traceId in `Map` - - Waits for root span to end + all children to complete - - Grace period timeout for stuck spans - - Concurrent export limiting - - Stores OpenTelemetry Context for token retrieval at export time -- Copy `SpanProcessor.ts` → `_a365/processors/SpanProcessor.ts` -- Copy baggage builder → `_a365/processors/BaggageSpanProcessor.ts` - - Extracts OTel baggage from context, copies to span attributes -- **Adaptation:** These implement `SpanProcessor` from `@opentelemetry/sdk-trace-base` — wire them into the distro's provider setup via `src/a365Setup.ts` -- The processor chain order must be preserved: - 1. BaggageSpanProcessor (enriches spans with baggage attributes) - 2. SpanProcessor (generic processing) - 3. PerRequestSpanProcessor or BatchSpanProcessor (export) - -**Depends on:** Task 2 - ---- - -### Task 4: Migrate Scopes & Contracts - -**Source:** `agents-a365-observability/src/tracing/scopes/` + `agents-a365-observability/src/tracing/contracts.ts` + `agents-a365-observability/src/tracing/constants.ts` - -**Work:** -- Copy scope hierarchy into `_a365/scopes/`: - - `OpenTelemetryScope` — base class with `Disposable`/`Symbol.dispose`, wraps OTel spans - - `InvokeAgentScope` — agent invocation - - `ExecuteToolScope` — tool execution - - `InferenceScope` — LLM inference - - `OutputScope` — output recording -- Copy contracts into `_a365/contracts/`: - - `MessageRole` enum: system, user, assistant, tool - - `MessagePart` union: TextPart, ToolCallRequestPart, ToolCallResponsePart, ReasoningPart, BlobPart, FilePart, UriPart, etc. - - `ChatMessage`, `InputMessages`, `OutputMessages` - - `AgentDetails`, `UserDetails`, `SpanDetails` -- Copy `OpenTelemetryConstants` into `_a365/constants.ts`: - - Gen AI semantic convention attribute keys (`gen_ai.agent.id`, `gen_ai.agent.name`, etc.) - - Microsoft-specific attributes (`microsoft.a365.caller.agent.*`, `microsoft.tenant.id`) - - Tool/inference/message attributes -- **Decision needed:** Scopes are the primary public API surface for A365 consumers building agent frameworks — decide whether to re-export from the distro's top-level `index.ts` or keep internal - -**Depends on:** Task 1 - ---- - -### Task 5: Migrate Context Propagation - -**Source:** `agents-a365-observability/src/tracing/context/` - -**Work:** -- Copy `token-context.ts` → `_a365/context/tokenContext.ts` - - `runWithExportToken(token, callback)` — stores token in OTel context - - `getExportToken()` — retrieves token from active context - - `updateExportToken(token)` — updates token -- Copy `parent-span-context.ts` → `_a365/context/parentSpanContext.ts` - - `ParentSpanRef` type (traceId, spanId) - - `runWithParentSpanRef()` / `createContextWithParentSpanRef()` -- Copy `trace-context-propagation.ts` → `_a365/context/traceContextPropagation.ts` - - W3C traceparent inject/extract to/from HTTP headers -- **Note:** These use `@opentelemetry/api` context APIs — no adaptation needed beyond import paths - -**Depends on:** None (standalone utilities) - ---- - -### Task 6: Migrate OpenAI Agents Instrumentation - -**Source:** `agents-a365-observability-extensions-openai/src/` - -**Work:** -- Per PLANNING.md Phase 5, GenAI instrumentations go into clearly marked internal modules -- Copy into `src/_openai/` (separate from `_a365/` per PLANNING.md convention): - - `OpenAIAgentsTraceInstrumentor.ts` — extends `InstrumentationBase`, targets `@openai/agents >= 0.1.5` - - `OpenAIAgentsTraceProcessor.ts` — implements OpenAI's `TracingProcessor` interface - - `Constants.ts` — OpenAI-specific constants - - `Utils.ts` — OpenAI utilities - - `configuration/OpenAIObservabilityConfiguration.ts` -- Key behavior: - - `enable()` → gets tracer, creates processor, calls OpenAI's `setTraceProcessors([processor])` - - Config options: `suppressInvokeAgentInput`, `isContentRecordingEnabled` - - Maps OpenAI agent events to A365 scope types (InvokeAgent, ExecuteTool, Inference, Output) -- **Adaptation:** Replace `@microsoft/agents-a365-observability` imports with `../_a365/` imports -- **Adaptation:** Replace `@microsoft/agents-a365-runtime` imports with distro config -- **Dependency:** `@openai/agents` as optional peer dependency -- Wire into distro config: `instrumentationOptions.openaiAgents` - -**Depends on:** Task 4 (scopes/contracts) - ---- - -### Task 7: Migrate LangChain Instrumentation - -**Source:** `agents-a365-observability-extensions-langchain/src/` - -**Work:** -- Copy into `src/_langchain/` (separate from `_a365/` per PLANNING.md convention): - - `LangChainTraceInstrumentor.ts` — singleton instrumentor - - `tracer.ts` — `LangChainTracer` implementing LangChain callback handler - - `Utils.ts` — LangChain utilities -- Key behavior: - - Patches `@langchain/core/callbacks/manager.CallbackManager._configureSync()` - - Injects `LangChainTracer` into callback handlers - - Listens to `on_chain_start`, `on_chain_end`, `on_tool_start`, etc. - - Creates/manages spans for chain invocations and tool calls - - Config: `isContentRecordingEnabled` -- **Adaptation:** Replace `@microsoft/agents-a365-observability` imports with `../_a365/` imports -- **Dependency:** `@langchain/core` as optional peer dependency -- Wire into distro config: `instrumentationOptions.langchain` - -**Depends on:** Task 4 (scopes/contracts) - ---- - -### Task 8: Migrate Hosting Integration - -**Source:** `agents-a365-observability-hosting/src/` - -**Work:** -- Copy into `_a365/hosting/`: - - `AgenticTokenCache.ts` — TTL-based token caching for A365 service authentication - - `BaggageMiddleware.ts` — propagates baggage from request headers to span context - - `OutputLoggingMiddleware.ts` — records output, injects parent span info into response headers - - `ObservabilityHostingManager.ts` — lifecycle management in hosting environments - - `BaggageBuilderUtils.ts`, `ScopeUtils.ts`, `TurnContextUtils.ts` -- **Adaptation:** Replace `@microsoft/agents-hosting` dependency: - - Evaluate whether hosting middleware should be directly included or behind a feature flag - - The hosting package depends on `@microsoft/agents-hosting` (Microsoft Agent Framework) — this is a tight coupling - - **Option A:** Keep hosting code but make `@microsoft/agents-hosting` an optional peer dependency - - **Option B:** Defer hosting integration until the distro has a hosting story -- **Recommendation:** Option A — migrate the code, gate behind `a365.hosting.enabled` config flag, declare `@microsoft/agents-hosting` as optional peer dependency - -**Depends on:** Tasks 1, 3, 5 - ---- - -### Task 10: Testing - -**Work:** -- Unit tests for each migrated module (maintain test parity with Agent365-nodejs): - - `_a365/exporter/` — mock HTTP, verify payload format, retry behavior - - `_a365/processors/` — verify buffering, grace period, concurrent export limits - - `_a365/scopes/` — verify span attributes, lifecycle, disposable pattern - - `_a365/context/` — verify context propagation, token storage/retrieval - - `_a365/configuration/` — verify env var parsing, defaults, validation - - `_openai/` — mock `@openai/agents`, verify trace processor registration - - `_langchain/` — mock `@langchain/core`, verify callback patching - - `a365Setup.ts` — integration test for full A365 setup flow -- Use Vitest (confirmed — already configured in `vitest.config.ts` with tests under `test/`) -- Tests for missing optional peer dependencies (graceful failures when `@openai/agents`, `@langchain/core`, or `@microsoft/agents-hosting` not installed) -- Tests for disabled A365 scenario (no processors registered, no exporter created) -- **Telemetry pipeline validation** (per PLANNING.md Phase 6): Validate that existing A365 telemetry pipelines continue to work under the new distro setup with the in-repo code: - - Verify span attribute fidelity: spans produced by the migrated code must have identical attributes, naming, and structure to those produced by the standalone `Agent365-nodejs` packages - - Verify exporter payload compatibility: the JSON payloads sent to the A365 service endpoint must be wire-compatible - - Verify processor chain ordering produces the same enrichment results - - Verify context propagation (token context, parent span ref, W3C traceparent) works identically - - Smoke test: run a representative agent workload through the distro and compare telemetry output with the same workload through the standalone A365 SDK - -**Depends on:** Tasks 1–9 - ---- - -## Public API Surface - -### Exported from `@microsoft/opentelemetry` (Top-Level) - -These A365 types should be re-exported from the distro's `src/distro/index.ts` barrel (which is then re-exported by `src/index.ts`) for consumers building agent frameworks: - -```typescript -// Scopes — primary API for creating agent observability spans -export { OpenTelemetryScope } from './_a365/scopes/OpenTelemetryScope'; -export { InvokeAgentScope } from './_a365/scopes/InvokeAgentScope'; -export { ExecuteToolScope } from './_a365/scopes/ExecuteToolScope'; -export { InferenceScope } from './_a365/scopes/InferenceScope'; -export { OutputScope } from './_a365/scopes/OutputScope'; - -// Contracts — message and detail types -export type { ChatMessage, InputMessages, OutputMessages } from './_a365/contracts/messages'; -export type { AgentDetails, UserDetails, SpanDetails } from './_a365/contracts/details'; -export { MessageRole } from './_a365/contracts/types'; - -// Context propagation utilities -export { runWithExportToken, getExportToken } from './_a365/context/tokenContext'; -export { injectContextToHeaders, extractContextFromHeaders } from './_a365/context/traceContextPropagation'; - -// Configuration types -export type { A365Options } from './distro/types'; - -// Constants -export { OpenTelemetryConstants } from './_a365/constants'; -``` diff --git a/PLANNING.md b/PLANNING.md index a8cf4172..66e24cc5 100644 --- a/PLANNING.md +++ b/PLANNING.md @@ -179,3 +179,82 @@ Provide runnable sample apps covering the main scenarios: - Map external instrumentation span attributes and naming to OpenTelemetry GenAI semantic conventions - Provide adapters or processors that normalize non-standard telemetry without taking a direct dependency on external instrumentation packages - Document which external instrumentations are supported for normalization and any known gaps + +## TODO: A365 Gap Analysis (agent365-nodejs parity) + +Comparison of our in-repo A365 code against the full `agent365-nodejs` SDK. Items marked ✅ are implemented; ❌ items need to be built. + +### agents-a365-observability (core) — Gaps + +| File | Description | Status | +|------|-------------|--------| +| `PerRequestSpanProcessorConfiguration.ts` | Dedicated config for per-request processor | ❌ Missing | +| `PerRequestSpanProcessorConfigurationOptions.ts` | Options interface for above | ❌ Missing | +| `PerRequestProcessorInternalOverrides.ts` | Internal overrides for per-request processor | ❌ Missing | +| `trace-context-propagation.ts` | Distributed tracing context propagation | ❌ Missing | +| `ObservabilityBuilder.ts` | Builder pattern for observability setup | ❌ Missing | +| `ObservabilityManager.ts` | Lifecycle manager for observability | ❌ Missing | +| `utils/logging.ts` | ILogger event helpers | ❌ Missing | +| `util.ts` (root tracing) | May differ from our `exporter/utils.ts` / `processors/util.ts` | ⚠️ Review | + +### agents-a365-observability-hosting (entire package missing) + +| File | Description | Status | +|------|-------------|--------| +| `middleware/ObservabilityHostingManager.ts` | Hosting lifecycle manager | ❌ Missing | +| `middleware/BaggageMiddleware.ts` | HTTP baggage middleware | ❌ Missing | +| `middleware/OutputLoggingMiddleware.ts` | Output logging middleware | ❌ Missing | +| `utils/BaggageBuilderUtils.ts` | Baggage builder utilities | ❌ Missing | +| `utils/ScopeUtils.ts` | Scope utility helpers | ❌ Missing | +| `utils/TurnContextUtils.ts` | Turn context utilities | ❌ Missing | +| `caching/AgenticTokenCache.ts` | Agentic token cache | ❌ Missing | + +### agents-a365-runtime (entire package missing) + +| File | Description | Status | +|------|-------------|--------| +| `environment-utils.ts` | Environment utilities | ❌ Missing | +| `power-platform-api-discovery.ts` | Power Platform API discovery | ❌ Missing | +| `agentic-authorization-service.ts` | Agentic authorization service | ❌ Missing | +| `operation-error.ts` | Operation error types | ❌ Missing | +| `operation-result.ts` | Operation result types | ❌ Missing | +| `utility.ts` | General utilities | ❌ Missing | +| `configuration/` | Config provider | ❌ Missing | + +### agents-a365-tooling (entire package missing) + +| File | Description | Status | +|------|-------------|--------| +| `McpToolServerConfigurationService.ts` | MCP tool server configuration | ❌ Missing | +| `contracts.ts` | Tooling contracts | ❌ Missing | +| `Utility.ts` | Tooling utilities | ❌ Missing | +| `configuration/` | Tooling configuration | ❌ Missing | +| `models/` | Tooling models | ❌ Missing | + +### agents-a365-notifications (entire package missing) + +| File | Description | Status | +|------|-------------|--------| +| `agent-notification.ts` | Agent notification handler | ❌ Missing | +| `constants.ts` | Notification constants | ❌ Missing | +| `extensions/` | Notification extensions | ❌ Missing | +| `models/` | Notification models | ❌ Missing | + +### Extension packages (all missing) + +| Package | Status | +|---------|--------| +| `agents-a365-observability-extensions-langchain` | ❌ Missing | +| `agents-a365-observability-extensions-openai` | ❌ Missing | +| `agents-a365-tooling-extensions-claude` | ❌ Missing | +| `agents-a365-tooling-extensions-langchain` | ❌ Missing | +| `agents-a365-tooling-extensions-openai` | ❌ Missing | + +### Priority Order + +1. **Core observability gaps** — `trace-context-propagation`, `PerRequestSpanProcessorConfiguration*`, `ObservabilityBuilder/Manager`, `utils/logging` +2. **Hosting middleware** — `ObservabilityHostingManager`, baggage/output middleware, scope utils +3. **Runtime** — env utils, API discovery, auth service +4. **Tooling** — MCP tool server config +5. **Notifications** — agent notifications +6. **Extension packages** — langchain/openai/claude integrations diff --git a/src/a365/processors/PerRequestSpanProcessor.ts b/src/a365/processors/PerRequestSpanProcessor.ts index 2a7e30df..aa6332ca 100644 --- a/src/a365/processors/PerRequestSpanProcessor.ts +++ b/src/a365/processors/PerRequestSpanProcessor.ts @@ -143,7 +143,7 @@ export class PerRequestSpanProcessor implements SpanProcessor { const root = isRootSpanFromContext(ctx); logger.info( - `[PerRequestSpanProcessor] Span start name=${span.name} traceId=${traceId} spanId=${span.spanContext().spanId}` + + `[PerRequestSpanProcessor] Span start traceId=${traceId} spanId=${span.spanContext().spanId}` + ` root=${root} openCount=${buf.openCount}`, ); diff --git a/src/distro/distro.ts b/src/distro/distro.ts index 4c57e80e..7a111356 100644 --- a/src/distro/distro.ts +++ b/src/distro/distro.ts @@ -6,10 +6,7 @@ import { logs } from "@opentelemetry/api-logs"; import type { NodeSDKConfiguration } from "@opentelemetry/sdk-node"; import { NodeSDK } from "@opentelemetry/sdk-node"; import type { MetricReader, ViewOptions } from "@opentelemetry/sdk-metrics"; -import { - type SpanProcessor, - BatchSpanProcessor, -} from "@opentelemetry/sdk-trace-base"; +import { type SpanProcessor, BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"; import type { LogRecordProcessor } from "@opentelemetry/sdk-logs"; import { InternalConfig } from "../shared/config.js"; @@ -116,7 +113,9 @@ export function useMicrosoftOpenTelemetry(options?: MicrosoftOpenTelemetryOption tokenResolver: a365Config.tokenResolver, }); // A365SpanProcessor copies baggage (tenant, agent, session, etc.) to span attributes - spanProcessors.push(new A365SpanProcessor()); + if (a365Config.baggage.enrichSpans) { + spanProcessors.push(new A365SpanProcessor()); + } // PerRequestSpanProcessor buffers spans per trace and exports on root completion // with the request's auth token; BatchSpanProcessor for standard batch export const a365ExportProcessor = a365Config.perRequestExport diff --git a/test/internal/unit/a365/a365SpanProcessor.test.ts b/test/internal/unit/a365/a365SpanProcessor.test.ts index b8fc739b..fd0892ac 100644 --- a/test/internal/unit/a365/a365SpanProcessor.test.ts +++ b/test/internal/unit/a365/a365SpanProcessor.test.ts @@ -3,7 +3,11 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { context, propagation, SpanKind } from "@opentelemetry/api"; -import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base"; +import { + BasicTracerProvider, + InMemorySpanExporter, + SimpleSpanProcessor, +} from "@opentelemetry/sdk-trace-base"; import { A365SpanProcessor, @@ -15,11 +19,13 @@ import { describe("A365SpanProcessor", () => { let provider: BasicTracerProvider; let processor: A365SpanProcessor; + let memoryExporter: InMemorySpanExporter; beforeEach(() => { processor = new A365SpanProcessor(); + memoryExporter = new InMemorySpanExporter(); provider = new BasicTracerProvider({ - spanProcessors: [processor], + spanProcessors: [processor, new SimpleSpanProcessor(memoryExporter)], }); }); @@ -45,8 +51,9 @@ describe("A365SpanProcessor", () => { const testSpan = tracer.startSpan("test-span", { kind: SpanKind.CLIENT }, ctx); testSpan.end(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const attrs = (testSpan as any)._attributes ?? (testSpan as any).attributes ?? {}; + const spans = memoryExporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + const attrs = spans[0].attributes; expect(attrs[OpenTelemetryConstants.TENANT_ID_KEY]).toBe("tenant-123"); expect(attrs[OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]).toBe("agent-789"); }); @@ -62,8 +69,9 @@ describe("A365SpanProcessor", () => { const testSpan = tracer.startSpan("test-span", { kind: SpanKind.CLIENT }, ctx); testSpan.end(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const attrs = (testSpan as any)._attributes ?? (testSpan as any).attributes ?? {}; + const spans = memoryExporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + const attrs = spans[0].attributes; expect(attrs[OpenTelemetryConstants.SESSION_ID_KEY]).toBe("session-abc"); }); @@ -78,8 +86,9 @@ describe("A365SpanProcessor", () => { const testSpan = tracer.startSpan("test-span", { kind: SpanKind.CLIENT }, ctx); testSpan.end(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const attrs = (testSpan as any)._attributes ?? (testSpan as any).attributes ?? {}; + const spans = memoryExporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + const attrs = spans[0].attributes; expect(attrs[OpenTelemetryConstants.SESSION_DESCRIPTION_KEY]).toBe( "Test session description", ); @@ -104,8 +113,9 @@ describe("A365SpanProcessor", () => { const testSpan = tracer.startSpan("invoke_agent test", { kind: SpanKind.CLIENT }, ctx); testSpan.end(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const attrs = (testSpan as any)._attributes ?? (testSpan as any).attributes ?? {}; + const spans = memoryExporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + const attrs = spans[0].attributes; expect(attrs[OpenTelemetryConstants.TENANT_ID_KEY]).toBe("tenant-123"); expect(attrs[OpenTelemetryConstants.USER_ID_KEY]).toBe("caller-456"); }); @@ -131,8 +141,9 @@ describe("A365SpanProcessor", () => { ); testSpan.end(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const attrs = (testSpan as any)._attributes ?? (testSpan as any).attributes ?? {}; + const spans = memoryExporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + const attrs = spans[0].attributes; expect(attrs[OpenTelemetryConstants.TENANT_ID_KEY]).toBe("tenant-existing"); }); @@ -146,8 +157,9 @@ describe("A365SpanProcessor", () => { const testSpan = tracer.startSpan("test-span", { kind: SpanKind.CLIENT }, ctx); testSpan.end(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const attrs = (testSpan as any)._attributes ?? (testSpan as any).attributes ?? {}; + const spans = memoryExporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + const attrs = spans[0].attributes; expect(attrs[OpenTelemetryConstants.TENANT_ID_KEY]).toBeUndefined(); }); @@ -158,8 +170,9 @@ describe("A365SpanProcessor", () => { const testSpan = tracer.startSpan("test-span", { kind: SpanKind.CLIENT }, ctx); testSpan.end(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const attrs = (testSpan as any)._attributes ?? (testSpan as any).attributes ?? {}; + const spans = memoryExporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + const attrs = spans[0].attributes; expect(attrs[OpenTelemetryConstants.TELEMETRY_SDK_NAME_KEY]).toBe( OpenTelemetryConstants.TELEMETRY_SDK_NAME_VALUE, ); diff --git a/test/internal/unit/a365/scopes.test.ts b/test/internal/unit/a365/scopes.test.ts index 99bcf81f..fdab48f3 100644 --- a/test/internal/unit/a365/scopes.test.ts +++ b/test/internal/unit/a365/scopes.test.ts @@ -1178,9 +1178,9 @@ describe("Request content and message serialization (span attributes)", () => { it("should require tenantId on agentDetails", () => { const response: OutputResponse = { messages: "test" }; - expect(() => - OutputScope.start(testRequest, response, { agentId: "a1" }), - ).toThrow("tenantId is required"); + expect(() => OutputScope.start(testRequest, response, { agentId: "a1" })).toThrow( + "tenantId is required", + ); }); it("should record output messages from response", () => { @@ -1235,7 +1235,10 @@ describe("Request content and message serialization (span attributes)", () => { expect(calls).toEqual( expect.arrayContaining([ expect.objectContaining({ key: OpenTelemetryConstants.USER_ID_KEY, val: "user-1" }), - expect.objectContaining({ key: OpenTelemetryConstants.USER_EMAIL_KEY, val: "u@test.com" }), + expect.objectContaining({ + key: OpenTelemetryConstants.USER_EMAIL_KEY, + val: "u@test.com", + }), ]), ); scope.dispose();