Skip to content
Open
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **OutputLoggingMiddleware**: Middleware that creates OutputScope spans for outgoing messages with lazy parent span linking via `A365_PARENT_SPAN_KEY`.
- **ObservabilityHostingManager**: Manager for configuring hosting-layer observability middleware with `ObservabilityHostingOptions`.

### Added
- **Agent365ExporterOptions**: New `httpRequestTimeoutMilliseconds` option (default 30s) controls the per-HTTP-request timeout for backend calls. This is distinct from `exporterTimeoutMilliseconds` which controls the overall BatchSpanProcessor export deadline.

### Fixed
- **Agent365ExporterOptions**: `exporterTimeoutMilliseconds` default increased from 30s to 90s to allow sufficient time for retries across multiple identity groups within a single export cycle.

Comment on lines +27 to +29
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changelog says exporterTimeoutMilliseconds default increased to 60s, but Agent365ExporterOptions currently defaults exporterTimeoutMilliseconds to 90000ms (90s). Please correct this entry to match the actual default (or adjust the default to match the changelog) to avoid publishing inaccurate release notes.

Copilot uses AI. Check for mistakes.
### Changed
- **ObservabilityHostingManager**: `enableBaggage` option now defaults to `false` (was `true`). Callers must explicitly set `enableBaggage: true` to register the BaggageMiddleware.
- `ScopeUtils.deriveAgentDetails` now resolves `agentId` via `activity.getAgenticInstanceId()` for embodied (agentic) agents only. For non-embodied agents, `agentId` is `undefined` since the token's app ID cannot reliably be attributed to the agent.
- `ScopeUtils.deriveAgentDetails` now resolves `agentBlueprintId` from the JWT `xms_par_app_azp` claim via `RuntimeUtility.getAgentIdFromToken()` instead of reading `recipient.agenticAppBlueprintId`.
- `ScopeUtils.deriveAgentDetails` now resolves `agentUPN` via `activity.getAgenticUser()` instead of `recipient.agenticUserId`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { OutputLoggingMiddleware } from './OutputLoggingMiddleware';
* Configuration options for the hosting observability layer.
*/
export interface ObservabilityHostingOptions {
/** Enable baggage propagation middleware. Defaults to true. */
/** Enable baggage propagation middleware. Defaults to false. */
enableBaggage?: boolean;

/** Enable output logging middleware for tracing outgoing messages. Defaults to false. */
Expand Down Expand Up @@ -42,7 +42,7 @@ export class ObservabilityHostingManager {
return;
}

const enableBaggage = options.enableBaggage !== false;
const enableBaggage = options.enableBaggage === true;
const enableOutputLogging = options.enableOutputLogging === true;

if (enableBaggage) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import logger, { formatError } from '../../utils/logging';
import { Agent365ExporterOptions } from './Agent365ExporterOptions';
import { ExporterEventNames } from './ExporterEventNames';

const DEFAULT_HTTP_TIMEOUT_SECONDS = 30000; // 30 seconds in ms
const DEFAULT_MAX_RETRIES = 3;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If max retries = 3
exporterTimeoutMilliseconds = MaxRetries * httpRequestTimeoutMilliseconds

so your default exporterTimeoutMilliseconds should be 18 sec?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch. update it to 90 second.


interface OTLPExportRequest {
Expand Down Expand Up @@ -247,7 +246,7 @@ export class Agent365Exporter implements SpanExporter {
method: 'POST',
headers,
body,
signal: AbortSignal.timeout(DEFAULT_HTTP_TIMEOUT_SECONDS)
signal: AbortSignal.timeout(this.options.httpRequestTimeoutMilliseconds)
});
Comment on lines 246 to 250
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New option httpRequestTimeoutMilliseconds changes the fetch() call by wiring a timeout AbortSignal, but there’s no unit test asserting that a custom timeout value is honored (e.g., by spying on AbortSignal.timeout or inspecting the fetch options). Adding a targeted test would help prevent regressions where this option is accidentally ignored.

Copilot uses AI. Check for mistakes.

correlationId = response?.headers?.get('x-ms-correlation-id') || response?.headers?.get('x-correlation-id') || 'unknown';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export type TokenResolver = (agentId: string, tenantId: string) => string | null
* @property {boolean} [useS2SEndpoint] When true, exporter will POST to the S2S path (/observabilityService/tenants/{tenantId}/agents/{agentId}/traces).
* @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} exporterTimeoutMilliseconds Maximum time (ms) the BatchSpanProcessor waits for the entire export() call to complete before giving up. Covers partitioning, token resolution, and all HTTP retries.
* @property {number} httpRequestTimeoutMilliseconds Timeout (ms) for each individual HTTP request to the observability backend. Applies per fetch() call inside the retry loop; each retry gets a fresh timeout.
* @property {number} maxExportBatchSize Maximum number of spans per export batch.
*/
export class Agent365ExporterOptions {
Expand All @@ -43,8 +44,11 @@ export class Agent365ExporterOptions {
/** Delay (ms) between automatic batch flush attempts. */
public scheduledDelayMilliseconds: number = 5000;

/** Per-export timeout in milliseconds. */
public exporterTimeoutMilliseconds: number = 30000;
/** Maximum time (ms) the BatchSpanProcessor waits for the entire export() call to complete. */
public exporterTimeoutMilliseconds: number = 90000;

/** Timeout (ms) for each individual HTTP request to the observability backend. Each retry attempt gets a fresh timeout. */
public httpRequestTimeoutMilliseconds: number = 30000;
Comment on lines +47 to +51
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exporterTimeoutMilliseconds is set to 90000ms (90s), but the PR description and CHANGELOG entry describe this default as 60s. Please align the implementation and docs (either change the default to 60000ms or update the written documentation to 90s) so consumers aren’t misled about the effective export deadline.

Copilot uses AI. Check for mistakes.

/** Maximum number of spans per export batch. */
public maxExportBatchSize: number = 512;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,39 @@ function mockAdapter() {
}

describe('ObservabilityHostingManager', () => {
it('registers BaggageMiddleware by default', () => {
it('does not register BaggageMiddleware by default', () => {
const adapter = mockAdapter();
new ObservabilityHostingManager().configure(adapter, {});
expect(adapter.registered).toHaveLength(0);
});

it('registers BaggageMiddleware when enableBaggage is true', () => {
const adapter = mockAdapter();
new ObservabilityHostingManager().configure(adapter, { enableBaggage: true });
expect(adapter.registered).toHaveLength(1);
expect(adapter.registered[0]).toBeInstanceOf(BaggageMiddleware);
});

it('registers both middleware when enableOutputLogging is true', () => {
it('registers both middleware when enableBaggage and enableOutputLogging are true', () => {
const adapter = mockAdapter();
new ObservabilityHostingManager().configure(adapter, { enableOutputLogging: true });
new ObservabilityHostingManager().configure(adapter, { enableBaggage: true, enableOutputLogging: true });
expect(adapter.registered).toHaveLength(2);
expect(adapter.registered[0]).toBeInstanceOf(BaggageMiddleware);
expect(adapter.registered[1]).toBeInstanceOf(OutputLoggingMiddleware);
});

it('skips BaggageMiddleware when enableBaggage is false', () => {
it('registers only OutputLoggingMiddleware when enableOutputLogging is true and enableBaggage is omitted', () => {
const adapter = mockAdapter();
new ObservabilityHostingManager().configure(adapter, { enableBaggage: false, enableOutputLogging: true });
new ObservabilityHostingManager().configure(adapter, { enableOutputLogging: true });
expect(adapter.registered).toHaveLength(1);
expect(adapter.registered[0]).toBeInstanceOf(OutputLoggingMiddleware);
});

it('subsequent configure calls on same instance are no-ops', () => {
const adapter = mockAdapter();
const manager = new ObservabilityHostingManager();
manager.configure(adapter, { enableOutputLogging: true });
manager.configure(adapter, { enableOutputLogging: true });
manager.configure(adapter, { enableBaggage: true, enableOutputLogging: true });
manager.configure(adapter, { enableBaggage: true, enableOutputLogging: true });
expect(adapter.registered).toHaveLength(2);
});
});