Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
996b5e7
use Agent365ExporterOptions for exporter and create AgenticTokenCache…
jsl517 Nov 10, 2025
9c50c72
tests
jsl517 Nov 11, 2025
51b4d86
copilot comment
jsl517 Nov 11, 2025
a0de073
configure batch span processor
jsl517 Nov 11, 2025
88605eb
copilot comment
jsl517 Nov 11, 2025
ca17b96
checkpoint to use cached context and authorization to exchange token.
jsl517 Nov 12, 2025
1b89510
only do refresh token in message handler, unit tests
jsl517 Nov 13, 2025
fa95349
sample update
jsl517 Nov 14, 2025
1c3940e
comments
jsl517 Nov 14, 2025
36c5966
comments
jsl517 Nov 14, 2025
3d174b3
observability always use prod
jsl517 Nov 14, 2025
9ca9f6a
Merge branch 'main' into users/pefan/exportoption
jsl517 Nov 14, 2025
7271de5
should be lowercase
jsl517 Nov 14, 2025
ea0cfc6
Revert "observability always use prod"
jsl517 Nov 15, 2025
5221582
Merge branch 'main' into users/pefan/exportoption
jsl517 Nov 15, 2025
64728ca
default to prod
jsl517 Nov 15, 2025
f1d9e61
expose Agent365ExporterOptions
jsl517 Nov 18, 2025
e07318b
expose Agent365ExporterOptions
jsl517 Nov 18, 2025
057aae7
move azure token cache to its own package
jsl517 Nov 19, 2025
20133be
readme update
jsl517 Nov 19, 2025
d322096
Merge branch 'main' into users/pefan/exportoption
jsl517 Nov 19, 2025
6058c84
fix package.json error
jsl517 Nov 19, 2025
1149e09
fix logging
jsl517 Nov 19, 2025
5e3d93b
lint,jest config
jsl517 Nov 19, 2025
55b5536
cleanup
jsl517 Nov 19, 2025
cf31254
comment
jsl517 Nov 19, 2025
142ec28
rename
jsl517 Nov 19, 2025
54ddd3e
lint
jsl517 Nov 19, 2025
5303929
comment
jsl517 Nov 19, 2025
0b11ffa
Merge branch 'main' into users/pefan/exportoption
fpfp100 Nov 19, 2025
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
31 changes: 18 additions & 13 deletions packages/agents-a365-observability/src/ObservabilityBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { NodeSDK } from '@opentelemetry/sdk-node';
import { ConsoleSpanExporter, BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { SpanProcessor } from './tracing/processors/SpanProcessor';
import { isAgent365ExporterEnabled } from './tracing/util';
import { Agent365Exporter, TokenResolver } from './tracing/exporter/Agent365Exporter';
import { Agent365Exporter } from './tracing/exporter/Agent365Exporter';
import type { TokenResolver } from './tracing/exporter/Agent365ExporterOptions';
import { Agent365ExporterOptions } from './tracing/exporter/Agent365ExporterOptions';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
import { trace } from '@opentelemetry/api';
Expand Down Expand Up @@ -67,18 +69,21 @@ export class ObservabilityBuilder {
return this;
}

private getTraceExporter() {
if (isAgent365ExporterEnabled()){
if (!this.options.tokenResolver) {
throw new Error('tokenResolver must be provided when Agent365 exporter is enabled');
}
return new Agent365Exporter(
this.options.tokenResolver,
this.options.clusterCategory || 'prod'
);
} else {
return new ConsoleSpanExporter();
private createBatchProcessor(): BatchSpanProcessor {
if (!isAgent365ExporterEnabled()) {
return new BatchSpanProcessor(new ConsoleSpanExporter());
}
const opts = new Agent365ExporterOptions();
Comment thread
fpfp100 marked this conversation as resolved.
opts.clusterCategory = this.options.clusterCategory || 'prod';
if (this.options.tokenResolver) {
opts.tokenResolver = this.options.tokenResolver;
}
return new BatchSpanProcessor(new Agent365Exporter(opts), {
maxQueueSize: opts.maxQueueSize,
scheduledDelayMillis: opts.scheduledDelayMilliseconds,
exportTimeoutMillis: opts.exporterTimeoutMilliseconds,
maxExportBatchSize: opts.maxExportBatchSize
});
}

private createResource() {
Expand All @@ -104,7 +109,7 @@ export class ObservabilityBuilder {
const spanProcessor = new SpanProcessor();

// 2. batch processor that actually ships spans out
const batchProcessor = new BatchSpanProcessor(this.getTraceExporter());
const batchProcessor = this.createBatchProcessor();

const globalProvider: any = trace.getTracerProvider();

Expand Down
1 change: 1 addition & 0 deletions packages/agents-a365-observability/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ export { OpenTelemetryScope } from './tracing/scopes/OpenTelemetryScope';
export { ExecuteToolScope } from './tracing/scopes/ExecuteToolScope';
export { InvokeAgentScope } from './tracing/scopes/InvokeAgentScope';
export { InferenceScope} from './tracing/scopes/InferenceScope';
export { AgenticTokenCacheInstance } from './utils/AgenticTokenCache';
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------------------------

import { ExportResult,ExportResultCode } from '@opentelemetry/core';
import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';

import { PowerPlatformApiDiscovery, ClusterCategory } from '@microsoft/agents-a365-runtime';
import { partitionByIdentity, parseIdentityKey, hexTraceId, hexSpanId, kindName, statusName } from './utils';
import logger, {formatError} from '../../utils/logging';

import logger, { formatError } from '../../utils/logging';
import { AgenticTokenCacheInstance } from '../../utils/AgenticTokenCache';
import { Agent365ExporterOptions } from './Agent365ExporterOptions';
const DEFAULT_HTTP_TIMEOUT_SECONDS = 30000; // 30 seconds in ms
const DEFAULT_MAX_RETRIES = 3;

Expand All @@ -14,7 +20,7 @@ interface OTLPExportRequest {

interface ResourceSpan {
resource: {
attributes: Record<string, any> | null;
attributes: Record<string, unknown> | null;
};
scopeSpans: ScopeSpan[];
}
Expand All @@ -35,7 +41,7 @@ interface OTLPSpan {
kind: string;
startTimeUnixNano: number;
endTimeUnixNano: number;
attributes: Record<string, any> | null;
attributes: Record<string, unknown> | null;
events?: OTLPEvent[] | null;
links?: OTLPLink[] | null;
status: OTLPStatus;
Expand All @@ -44,24 +50,20 @@ interface OTLPSpan {
interface OTLPEvent {
timeUnixNano: number;
name: string;
attributes?: Record<string, any> | null;
attributes?: Record<string, unknown> | null;
}

interface OTLPLink {
traceId: string;
spanId: string;
attributes?: Record<string, any> | null;
attributes?: Record<string, unknown> | null;
}

interface OTLPStatus {
code: string;
message?: string;
}

/**
* Token resolver function type - supports both sync and async implementations
*/
export type TokenResolver = (agentId: string, tenantId: string) => string | null | Promise<string | null>;

/**
* Observability span exporter for Agent365:
Expand All @@ -71,20 +73,25 @@ export type TokenResolver = (agentId: string, tenantId: string) => string | null
* - Adds Bearer token via token_resolver(agentId, tenantId)
*/
export class Agent365Exporter implements SpanExporter {
private readonly tokenResolver: TokenResolver;
private readonly clusterCategory: ClusterCategory;
private closed = false;
private readonly options: Agent365ExporterOptions;
Comment thread
fpfp100 marked this conversation as resolved.

constructor(
tokenResolver: TokenResolver,
clusterCategory: ClusterCategory = 'prod'
) {
if (!tokenResolver) {
logger.error('[Agent365Exporter] token_resolver is not provided');
throw new Error('token_resolver must be provided.');
/**
* Initialize exporter with a fully constructed options instance.
* If tokenResolver is missing, installs cache-backed resolver.
*/
constructor(options: Agent365ExporterOptions) {
if (!options) {
throw new Error('Agent365ExporterOptions must be provided (was null/undefined)');
}

if (!options.tokenResolver) {
options.tokenResolver = AgenticTokenCacheInstance.getObservabilityToken.bind(AgenticTokenCacheInstance);
logger.info('Agent365Exporter initialized with agentic resolver', `clusterCategory=${options.clusterCategory}`);
} else {
logger.info('Agent365Exporter initialized with custom tokenResolver', `clusterCategory=${options.clusterCategory}`);
}
this.tokenResolver = tokenResolver;
this.clusterCategory = clusterCategory;
this.options = options;
}

/**
Expand Down Expand Up @@ -140,7 +147,7 @@ export class Agent365Exporter implements SpanExporter {
const body = JSON.stringify(payload);

// Resolve endpoint + token
const discovery = new PowerPlatformApiDiscovery(this.clusterCategory);
const discovery = new PowerPlatformApiDiscovery(this.options.clusterCategory as ClusterCategory);
const endpoint = discovery.getTenantIslandClusterEndpoint(tenantId);
const url = `https://${endpoint}/maven/agent365/agents/${agentId}/traces?api-version=1`;
logger.info(`[Agent365Exporter] Resolved endpoint: ${endpoint}`);
Expand All @@ -149,7 +156,7 @@ export class Agent365Exporter implements SpanExporter {
'content-type': 'application/json'
};

const tokenResult = this.tokenResolver(agentId, tenantId);
const tokenResult = this.options.tokenResolver!(agentId, tenantId);
Comment thread
fpfp100 marked this conversation as resolved.
Outdated
Comment thread
fpfp100 marked this conversation as resolved.
Outdated
const token = tokenResult instanceof Promise ? await tokenResult : tokenResult;
if (token) {
headers['authorization'] = `Bearer ${token}`;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------------------------

import { ClusterCategory } from '@microsoft/agents-a365-runtime';
/**
* A function that resolves and returns an authentication token for the given agent and tenant.
* Implementations may perform synchronous lookup (e.g., in-memory cache) or asynchronous network calls.
* Return null if a token cannot be provided; exporter will log and proceed without an authorization header.
*/
export type TokenResolver = (agentId: string, tenantId: string) => string | null | Promise<string | null>;

/**
* Options controlling the behavior of the Agent365 OpenTelemetry span exporter.
*
* These values tune batching, timeouts, token acquisition and endpoint shape. All properties have sensible
* defaults so callers can usually construct without arguments and override selectively.
*
* @property {ClusterCategory | string} clusterCategory Environment / cluster category (e.g. "preprod", "prod").
* @property {TokenResolver} [tokenResolver] Optional delegate to obtain an auth token. If omitted the exporter will
* fall back to reading the cached token (AgenticTokenCacheInstance.getObservabilityToken).
Comment thread
fpfp100 marked this conversation as resolved.
* @property {boolean} useS2SEndpoint When true uses service-to-service path (/maven/agent365/service/agents/{agentId}/traces);
* when false uses the standard path (/maven/agent365/agents/{agentId}/traces).
Comment thread
fpfp100 marked this conversation as resolved.
Outdated
* @property {number} maxQueueSize Maximum span queue size before drops occur (passed to BatchSpanProcessor).
* @property {number} scheduledDelayMilliseconds Delay between automatic batch flush attempts.
* @property {number} exporterTimeoutMilliseconds Per-export timeout (abort if exceeded).
* @property {number} maxExportBatchSize Maximum number of spans per export batch.
*/
export class Agent365ExporterOptions {
/** Environment / cluster category (e.g. "preprod", "prod"). */
public clusterCategory: ClusterCategory | string = 'preprod';

/** Optional delegate to resolve auth token; falls back to AgenticTokenCache when absent. */
public tokenResolver?: TokenResolver;

/** Use service-to-service endpoint variant when true; standard endpoint when false. */
public useS2SEndpoint: boolean = false;
Comment thread
fpfp100 marked this conversation as resolved.
Outdated

/** Maximum span queue size before new spans are dropped. */
public maxQueueSize: number = 2048;

/** Delay (ms) between automatic batch flush attempts. */
public scheduledDelayMilliseconds: number = 5000;

/** Per-export timeout in milliseconds. */
public exporterTimeoutMilliseconds: number = 30000;

/** Maximum number of spans per export batch. */
public maxExportBatchSize: number = 512;
}
Loading
Loading