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
1 change: 0 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,6 @@ The keyword "Kairo" is legacy and should not appear in any code. Flag and remove
| `A365_OBSERVABILITY_SCOPES_OVERRIDE` | Override observability auth scopes | Space-separated scope strings |
| `ENABLE_A365_OBSERVABILITY_EXPORTER` | Enable Agent365 exporter | `true`, `false` (default) |
| `ENABLE_A365_OBSERVABILITY_PER_REQUEST_EXPORT` | Enable per-request export mode | `true`, `false` (default) |
| `A365_OBSERVABILITY_USE_CUSTOM_DOMAIN` | Use custom domain for export | `true`, `false` (default) |
| `A365_OBSERVABILITY_DOMAIN_OVERRIDE` | Custom domain URL override | URL string |
| `A365_OBSERVABILITY_LOG_LEVEL` | Internal logging level | `none` (default), `error`, `warn`, `info`, `debug` |
| `A365_PER_REQUEST_MAX_TRACES` | Max buffered traces per request (`PerRequestSpanProcessorConfiguration`) | Number (default: 1000) |
Expand Down
13 changes: 0 additions & 13 deletions docs/prd-configuration-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ The Agent365 SDK currently relies on environment variables for all configuration
|---------|--------------|---------|------|---------|
| `observabilityAuthenticationScopes` | `A365_OBSERVABILITY_SCOPES_OVERRIDE` | `['https://api.powerplatform.com/.default']` | `string[]` | ObservabilityConfiguration |
| `isObservabilityExporterEnabled` | `ENABLE_A365_OBSERVABILITY_EXPORTER` | `false` | `boolean` | ObservabilityConfiguration |
| `useCustomDomainForObservability` | `A365_OBSERVABILITY_USE_CUSTOM_DOMAIN` | `false` | `boolean` | ObservabilityConfiguration |
| `observabilityDomainOverride` | `A365_OBSERVABILITY_DOMAIN_OVERRIDE` | `null` | `string \| null` | ObservabilityConfiguration |
| `observabilityLogLevel` | `A365_OBSERVABILITY_LOG_LEVEL` | `'none'` | `string` | ObservabilityConfiguration |

Expand Down Expand Up @@ -392,7 +391,6 @@ export type ObservabilityConfigurationOptions = RuntimeConfigurationOptions & {
*/
observabilityAuthenticationScopes?: () => string[];
isObservabilityExporterEnabled?: () => boolean;
useCustomDomainForObservability?: () => boolean;
observabilityDomainOverride?: () => string | null;
observabilityLogLevel?: () => string;
};
Expand Down Expand Up @@ -449,15 +447,6 @@ export class ObservabilityConfiguration extends RuntimeConfiguration {
return ['true', '1', 'yes', 'on'].includes(value);
}

get useCustomDomainForObservability(): boolean {
const result = this.observabilityOverrides.useCustomDomainForObservability?.();
if (result !== undefined) {
return result;
}
const value = process.env.A365_OBSERVABILITY_USE_CUSTOM_DOMAIN?.toLowerCase() ?? '';
return ['true', '1', 'yes', 'on'].includes(value);
}

get observabilityDomainOverride(): string | null {
const result = this.observabilityOverrides.observabilityDomainOverride?.();
if (result !== undefined) {
Expand Down Expand Up @@ -666,7 +655,6 @@ packages/agents-a365-observability-extensions-openai/src/
2. **`tests/observability/tracing/exporter-utils.test.ts`** (NEW)
- `isAgent365ExporterEnabled()` with all boolean variants
- `isPerRequestExportEnabled()` with all boolean variants
- `useCustomDomainForObservability()` edge cases
- `getAgent365ObservabilityDomainOverride()` edge cases
- `resolveAgent365Endpoint()` for all cluster categories

Expand Down Expand Up @@ -1173,7 +1161,6 @@ These methods remain available for backward compatibility but should not be used
| `A365_OBSERVABILITY_SCOPES_OVERRIDE` | string (space-sep) | prod scope | observability |
| `ENABLE_A365_OBSERVABILITY_EXPORTER` | boolean | `false` | observability |
| `ENABLE_A365_OBSERVABILITY_PER_REQUEST_EXPORT` | boolean | `false` | observability (PerRequestSpanProcessorConfiguration) |
| `A365_OBSERVABILITY_USE_CUSTOM_DOMAIN` | boolean | `false` | observability |
| `A365_OBSERVABILITY_DOMAIN_OVERRIDE` | string | `null` | observability |
| `A365_OBSERVABILITY_LOG_LEVEL` | string | `'none'` | observability |
| `A365_PER_REQUEST_MAX_TRACES` | number | `1000` | observability (PerRequestSpanProcessorConfiguration) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,5 @@ export class OpenAIObservabilityConfiguration extends ObservabilityConfiguration
}

// Inherited: clusterCategory, isDevelopmentEnvironment, observabilityAuthenticationScopes,
// isObservabilityExporterEnabled, useCustomDomainForObservability,
// observabilityDomainOverride, observabilityLogLevel
// isObservabilityExporterEnabled, observabilityDomainOverride, observabilityLogLevel
}
2 changes: 0 additions & 2 deletions packages/agents-a365-observability/docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,6 @@ const customConfig = new ObservabilityConfiguration({
|----------|--------------|---------|-------------|
| `observabilityAuthenticationScopes` | `A365_OBSERVABILITY_SCOPES_OVERRIDE` | `['https://api.powerplatform.com/.default']` | OAuth scopes for observability auth |
| `isObservabilityExporterEnabled` | `ENABLE_A365_OBSERVABILITY_EXPORTER` | `false` | Enable Agent365 exporter |
| `useCustomDomainForObservability` | `A365_OBSERVABILITY_USE_CUSTOM_DOMAIN` | `false` | Use custom domain for export |
| `observabilityDomainOverride` | `A365_OBSERVABILITY_DOMAIN_OVERRIDE` | `null` | Custom domain URL override |
| `observabilityLogLevel` | `A365_OBSERVABILITY_LOG_LEVEL` | `none` | Internal logging level |
| `clusterCategory` | `CLUSTER_CATEGORY` | `prod` | (Inherited) Environment cluster |
Expand Down Expand Up @@ -482,7 +481,6 @@ console.log(config.perRequestMaxTraces); // Max buffered traces (default: 1000)
| `ENABLE_A365_OBSERVABILITY_EXPORTER` | Enable/disable Agent365 exporter | `false` |
| `A365_OBSERVABILITY_SCOPES_OVERRIDE` | Override auth scopes (space-separated) | Production scope |
| `ENABLE_A365_OBSERVABILITY_PER_REQUEST_EXPORT` | Enable per-request export mode | `false` |
| `A365_OBSERVABILITY_USE_CUSTOM_DOMAIN` | Use custom domain for export | `false` |
| `A365_OBSERVABILITY_DOMAIN_OVERRIDE` | Custom domain URL | - |
| `A365_OBSERVABILITY_LOG_LEVEL` | Internal log level | `none` |
| `A365_PER_REQUEST_MAX_TRACES` | Max buffered traces (`PerRequestSpanProcessorConfiguration`) | `1000` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,6 @@ export class ObservabilityConfiguration extends RuntimeConfiguration {
return RuntimeConfiguration.parseEnvBoolean(process.env.ENABLE_A365_OBSERVABILITY_EXPORTER);
}

get useCustomDomainForObservability(): boolean {
const result = this.observabilityOverrides.useCustomDomainForObservability?.();
if (result !== undefined) return result;
return RuntimeConfiguration.parseEnvBoolean(process.env.A365_OBSERVABILITY_USE_CUSTOM_DOMAIN);
}

get observabilityDomainOverride(): string | null {
const result = this.observabilityOverrides.observabilityDomainOverride?.();
if (result !== undefined) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,8 @@ export type ObservabilityConfigurationOptions = RuntimeConfigurationOptions & {
*/
isObservabilityExporterEnabled?: () => boolean;

/**
* Override to enable/disable custom domain for observability endpoints.
* When enabled, uses `observabilityDomainOverride` instead of the default endpoint.
*
* @returns `true` to use custom domain, `false` to use default.
* @envvar A365_OBSERVABILITY_USE_CUSTOM_DOMAIN - 'true', '1', 'yes', 'on' to enable.
* @default false
*/
useCustomDomainForObservability?: () => boolean;

/**
* Override for the custom observability domain/endpoint.
* Only used when `useCustomDomainForObservability` is true.
* Trailing slashes are automatically removed.
*
* @returns Custom domain URL string, or `null` for no override.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { ExportResult, ExportResultCode } from '@opentelemetry/core';
import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';

import { PowerPlatformApiDiscovery, ClusterCategory, IConfigurationProvider } from '@microsoft/agents-a365-runtime';
import { ClusterCategory, IConfigurationProvider } from '@microsoft/agents-a365-runtime';
import type { ObservabilityConfiguration } from '../../configuration';
import {
partitionByIdentity,
Expand All @@ -15,7 +15,6 @@ import {
hexSpanId,
kindName,
statusName,
useCustomDomainForObservability,
resolveAgent365Endpoint,
getAgent365ObservabilityDomainOverride,
isPerRequestExportEnabled
Expand Down Expand Up @@ -82,8 +81,8 @@ interface OTLPStatus {
* Observability span exporter for Agent365:
* - Partitions spans by (tenantId, agentId)
* - Builds OTLP-like JSON: resourceSpans -> scopeSpans -> spans
* - POSTs per group to https://{endpoint}/maven/agent365/agents/{agentId}/traces?api-version=1
* or, when useS2SEndpoint is true, https://{endpoint}/maven/agent365/service/agents/{agentId}/traces?api-version=1
* - POSTs per group to https://{endpoint}/observability/tenants/{tenantId}/agents/{agentId}/traces?api-version=1
* or, when useS2SEndpoint is true, https://{endpoint}/observabilityService/tenants/{tenantId}/agents/{agentId}/traces?api-version=1
* - Adds Bearer token via token_resolver(agentId, tenantId)
*/
export class Agent365Exporter implements SpanExporter {
Expand Down Expand Up @@ -170,27 +169,20 @@ export class Agent365Exporter implements SpanExporter {

const payload = this.buildExportRequest(spans);
const body = JSON.stringify(payload);
const usingCustomServiceEndpoint = useCustomDomainForObservability(this.configProvider);
// Select endpoint path based on S2S flag
// Select endpoint path based on S2S flag (includes tenantId in path)
const endpointRelativePath =
this.options.useS2SEndpoint
? `/maven/agent365/service/agents/${agentId}/traces`
: `/maven/agent365/agents/${agentId}/traces`;
? `/observabilityService/tenants/${tenantId}/agents/${agentId}/traces`
: `/observability/tenants/${tenantId}/agents/${agentId}/traces`;

let url: string;
const domainOverride = getAgent365ObservabilityDomainOverride(this.configProvider);
if (domainOverride) {
url = `${domainOverride}${endpointRelativePath}?api-version=1`;
} else if (usingCustomServiceEndpoint) {
} else {
const base = resolveAgent365Endpoint(this.options.clusterCategory as ClusterCategory);
url = `${base}${endpointRelativePath}?api-version=1`;
logger.info(`[Agent365Exporter] Using custom domain endpoint: ${url}`);
} else {
// Default behavior: discover PPAPI gateway endpoint per-tenant
const discovery = new PowerPlatformApiDiscovery(this.options.clusterCategory as ClusterCategory);
const endpoint = discovery.getTenantIslandClusterEndpoint(tenantId);
url = `https://${endpoint}${endpointRelativePath}?api-version=1`;
logger.info(`[Agent365Exporter] Resolved endpoint: ${url}`);
logger.info(`[Agent365Exporter] Using default endpoint: ${url}`);
}

const headers: Record<string, string> = {
Expand Down Expand Up @@ -229,10 +221,8 @@ export class Agent365Exporter implements SpanExporter {
return;
}

// Add tenant id to headers when using custom domain
if (usingCustomServiceEndpoint) {
headers['x-ms-tenant-id'] = tenantId;
}
// Always include tenant id header
headers['x-ms-tenant-id'] = tenantId;

// Basic retry loop
const { ok, correlationId } = await this.postWithRetries(url, body, headers);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type TokenResolver = (agentId: string, tenantId: string) => string | null
* @property {ClusterCategory | string} clusterCategory Environment / cluster category (e.g. ClusterCategory.preprod, ClusterCategory.prod, default to ClusterCategory.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).
* @property {boolean} [useS2SEndpoint] When true, exporter will POST to the S2S path (/maven/agent365/service/agents/{agentId}/traces).
* @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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,21 +156,6 @@ export function isPerRequestExportEnabled(
return enabled;
}

/**
* Single toggle to use custom domain for observability export.
* When true exporter will send traces to custom Agent365 service endpoint
* and include x-ms-tenant-id in headers.
* @param configProvider Optional configuration provider. Defaults to defaultObservabilityConfigurationProvider if not specified.
*/
export function useCustomDomainForObservability(
configProvider?: IConfigurationProvider<ObservabilityConfiguration>
): boolean {
const provider = configProvider ?? defaultObservabilityConfigurationProvider;
const enabled = provider.getConfiguration().useCustomDomainForObservability;
logger.info(`[Agent365Exporter] Use custom domain for observability: ${enabled}`);
return enabled;
}

/**
* Resolve the Agent365 service endpoint base URI for a given cluster category.
* When an explicit override is not configured, this determines the default base URI.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,6 @@ describe('OpenAIObservabilityConfiguration', () => {
expect(config.observabilityAuthenticationScopes).toEqual(['custom-scope/.default']);
});

it('should inherit useCustomDomainForObservability from override', () => {
const config = new OpenAIObservabilityConfiguration({
useCustomDomainForObservability: () => true
});
expect(config.useCustomDomainForObservability).toBe(true);
});

it('should inherit observabilityDomainOverride from override', () => {
const config = new OpenAIObservabilityConfiguration({
observabilityDomainOverride: () => 'https://custom.domain'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,48 +131,6 @@ describe('ObservabilityConfiguration', () => {
});
});

describe('useCustomDomainForObservability', () => {
it('should use override function when provided', () => {
const config = new ObservabilityConfiguration({
useCustomDomainForObservability: () => true
});
expect(config.useCustomDomainForObservability).toBe(true);
});

it.each([
['true', true],
['1', true],
['yes', true],
['on', true],
['false', false],
['0', false],
['', false]
])('should return %s when env var is "%s"', (envValue, expected) => {
process.env.A365_OBSERVABILITY_USE_CUSTOM_DOMAIN = envValue;
const config = new ObservabilityConfiguration({});
expect(config.useCustomDomainForObservability).toBe(expected);
});

it('should return false when env var is not set', () => {
delete process.env.A365_OBSERVABILITY_USE_CUSTOM_DOMAIN;
const config = new ObservabilityConfiguration({});
expect(config.useCustomDomainForObservability).toBe(false);
});

it('should call override function on each access (dynamic resolution)', () => {
let callCount = 0;
const config = new ObservabilityConfiguration({
useCustomDomainForObservability: () => {
callCount++;
return true;
}
});
void config.useCustomDomainForObservability;
void config.useCustomDomainForObservability;
expect(callCount).toBe(2);
});
});

describe('observabilityDomainOverride', () => {
it('should use override function when provided', () => {
const config = new ObservabilityConfiguration({
Expand Down
Loading