Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
388 changes: 0 additions & 388 deletions A365_IMPLEMENTATION_PLAN.md

This file was deleted.

1 change: 0 additions & 1 deletion MIGRATION_A365.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`) |
Expand Down
79 changes: 79 additions & 0 deletions PLANNING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 1 addition & 2 deletions src/a365/configuration/A365Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

/**
Expand All @@ -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";
Expand Down
6 changes: 3 additions & 3 deletions src/a365/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

// ---------------------------------------------------------------------------
Expand All @@ -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<string, unknown>;
if (
typeof maybeCtx.getValue === "function" &&
typeof maybeCtx.setValue === "function" &&
Expand Down
2 changes: 1 addition & 1 deletion src/a365/middleware/BaggageBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
23 changes: 17 additions & 6 deletions src/a365/processors/PerRequestSpanProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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}`,
`[PerRequestSpanProcessor] Span start traceId=${traceId} spanId=${span.spanContext().spanId}` +
` root=${root} openCount=${buf.openCount}`,
);

// Capture a context to export under.
if (isRootSpan(span)) {
if (root) {
buf.rootCtx = ctx;
} else {
buf.rootCtx ??= ctx;
Expand Down
2 changes: 1 addition & 1 deletion src/a365/scopes/OpenTelemetryScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
25 changes: 16 additions & 9 deletions src/distro/distro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +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,
SimpleSpanProcessor,
} 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";
Expand All @@ -22,7 +18,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";

Expand Down Expand Up @@ -111,10 +112,16 @@ 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
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
? new PerRequestSpanProcessor(a365Exporter)
: new BatchSpanProcessor(a365Exporter);
spanProcessors.push(a365Processor);
spanProcessors.push(a365ExportProcessor);
Comment thread
hectorhdzg marked this conversation as resolved.
}

const views: ViewOptions[] = metricHandler.getViews().concat(customViews);
Expand Down
Loading
Loading