diff --git a/packages/agents-a365-observability/src/ObservabilityBuilder.ts b/packages/agents-a365-observability/src/ObservabilityBuilder.ts index 08d23cc..65db2b0 100644 --- a/packages/agents-a365-observability/src/ObservabilityBuilder.ts +++ b/packages/agents-a365-observability/src/ObservabilityBuilder.ts @@ -5,7 +5,7 @@ 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 { isAgent365ExporterEnabled } from './tracing/exporter/utils'; import { Agent365Exporter } from './tracing/exporter/Agent365Exporter'; import type { TokenResolver } from './tracing/exporter/Agent365ExporterOptions'; import { Agent365ExporterOptions } from './tracing/exporter/Agent365ExporterOptions'; diff --git a/packages/agents-a365-observability/src/tracing/exporter/Agent365Exporter.ts b/packages/agents-a365-observability/src/tracing/exporter/Agent365Exporter.ts index 2b49ccb..dd49cab 100644 --- a/packages/agents-a365-observability/src/tracing/exporter/Agent365Exporter.ts +++ b/packages/agents-a365-observability/src/tracing/exporter/Agent365Exporter.ts @@ -7,10 +7,19 @@ 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 { + partitionByIdentity, + parseIdentityKey, + hexTraceId, + hexSpanId, + kindName, + statusName, + useCustomDomainForObservability, + resolveAgent365Endpoint, + getAgent365ObservabilityDomainOverride +} from './utils'; import logger, { formatError } from '../../utils/logging'; import { Agent365ExporterOptions } from './Agent365ExporterOptions'; -import { useCustomDomainForObservability, resolveAgent365Endpoint } from '../util'; const DEFAULT_HTTP_TIMEOUT_SECONDS = 30000; // 30 seconds in ms const DEFAULT_MAX_RETRIES = 3; @@ -144,25 +153,26 @@ export class Agent365Exporter implements SpanExporter { const payload = this.buildExportRequest(spans); const body = JSON.stringify(payload); - const usingCustomServiceEndpoint = useCustomDomainForObservability(); - // Select endpoint path based on S2S flag - const endpointPath = + const endpointRelativePath = this.options.useS2SEndpoint ? `/maven/agent365/service/agents/${agentId}/traces` : `/maven/agent365/agents/${agentId}/traces`; let url: string; - if (usingCustomServiceEndpoint) { + const domainOverride = getAgent365ObservabilityDomainOverride(); + if (domainOverride) { + url = `${domainOverride}${endpointRelativePath}?api-version=1`; + } else if (usingCustomServiceEndpoint) { const base = resolveAgent365Endpoint(this.options.clusterCategory as ClusterCategory); - url = `${base}${endpointPath}?api-version=1`; + 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}${endpointPath}?api-version=1`; + url = `https://${endpoint}${endpointRelativePath}?api-version=1`; logger.info(`[Agent365Exporter] Resolved endpoint: ${url}`); } diff --git a/packages/agents-a365-observability/src/tracing/exporter/utils.ts b/packages/agents-a365-observability/src/tracing/exporter/utils.ts index 1a63dfc..c341d73 100644 --- a/packages/agents-a365-observability/src/tracing/exporter/utils.ts +++ b/packages/agents-a365-observability/src/tracing/exporter/utils.ts @@ -4,6 +4,7 @@ import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import { SpanKind, SpanStatusCode } from '@opentelemetry/api'; +import { ClusterCategory } from '@microsoft/agents-a365-runtime'; import { OpenTelemetryConstants } from '../constants'; import logger from '../../utils/logging'; @@ -119,6 +120,47 @@ export function isAgent365ExporterEnabled(): boolean { 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. + */ +export function useCustomDomainForObservability(): boolean { + const value = process.env.A365_OBSERVABILITY_USE_CUSTOM_DOMAIN?.toLowerCase() || ''; + const validValues = ['true', '1', 'yes', 'on']; + const enabled = validValues.includes(value); + 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. + */ +export function resolveAgent365Endpoint(clusterCategory: ClusterCategory): string { + switch (clusterCategory) { + case 'prod': + default: + return 'https://agent365.svc.cloud.microsoft'; + } +} + +/** + * Get Agent365 Observability domain override. + * Internal development and test clusters can override this by setting the + * `A365_OBSERVABILITY_DOMAIN_OVERRIDE` environment variable. When set to a + * non-empty value, that value is used as the base URI regardless of cluster category. Otherwise, null is returned. + */ +export function getAgent365ObservabilityDomainOverride(): string | null { + const override = process.env.A365_OBSERVABILITY_DOMAIN_OVERRIDE; + + if (override && override.trim().length > 0) { + // Normalize to avoid double slashes when concatenating paths + return override.trim().replace(/\/+$/, ''); + } + return null; +} + /** * Parse identity key back to tenant and agent IDs diff --git a/packages/agents-a365-observability/src/tracing/util.ts b/packages/agents-a365-observability/src/tracing/util.ts index 7ad10c1..63e2638 100644 --- a/packages/agents-a365-observability/src/tracing/util.ts +++ b/packages/agents-a365-observability/src/tracing/util.ts @@ -4,9 +4,13 @@ // ------------------------------------------------------------------------------ import { OpenTelemetryConstants } from './constants'; -import { ClusterCategory } from '@microsoft/agents-a365-runtime'; + /** - * Check if exporter is enabled via environment variables + * Check if exporter is enabled via environment variables. + * + * NOTE: Exporter-specific helpers have been moved to + * tracing/exporter/utils.ts. This file remains only for any + * non-exporter tracing utilities that may be added in the future. */ export const isAgent365ExporterEnabled: () => boolean = (): boolean => { const enableA365Exporter = process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER]?.toLowerCase(); @@ -18,41 +22,3 @@ export const isAgent365ExporterEnabled: () => boolean = (): boolean => { enableA365Exporter === 'on' ); }; - -/** - * 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. - */ -export const useCustomDomainForObservability = (): boolean => { - const value = process.env.A365_OBSERVABILITY_USE_CUSTOM_DOMAIN?.toLowerCase(); - return ( - value === 'true' || - value === '1' || - value === 'yes' || - value === 'on' - ); -}; - -/** - * Resolve the Agent365 service endpoint base URI for a given cluster category. - * - * By default this returns the production Agent365 endpoint. Internal development - * and test clusters can override this by setting the - * `A365_OBSERVABILITY_DOMAIN_OVERRIDE` environment variable. When set to a - * non-empty value, that value is used as the base URI regardless of cluster category. - */ -export function resolveAgent365Endpoint(clusterCategory: ClusterCategory): string { - const override = process.env.A365_OBSERVABILITY_DOMAIN_OVERRIDE; - - if (override && override.trim().length > 0) { - // Normalize to avoid double slashes when concatenating paths - return override.trim().replace(/\/+$/, ''); - } - - switch (clusterCategory) { - case 'prod': - default: - return 'https://agent365.svc.cloud.microsoft'; - } -} diff --git a/tests/observability/core/agent365-exporter.test.ts b/tests/observability/core/agent365-exporter.test.ts index b48186c..befcab5 100644 --- a/tests/observability/core/agent365-exporter.test.ts +++ b/tests/observability/core/agent365-exporter.test.ts @@ -134,7 +134,7 @@ describe('Agent365Exporter', () => { it.each([ { - description: 'set to non-empty value', + description: 'set to non-empty value and A365_OBSERVABILITY_USE_CUSTOM_DOMAIN is true', override: 'https://custom-observability.internal', expectedBaseUrl: 'https://custom-observability.internal' }, @@ -152,10 +152,16 @@ describe('Agent365Exporter', () => { description: 'unset (undefined)', override: undefined, expectedBaseUrl: 'https://agent365.svc.cloud.microsoft' - } - ])('uses correct domain when A365_OBSERVABILITY_DOMAIN_OVERRIDE is $description', async ({ override, expectedBaseUrl }) => { + }, + { + description: 'set to non-empty value and A365_OBSERVABILITY_USE_CUSTOM_DOMAIN is false', + override: 'https://custom-observability.internal', + expectedBaseUrl: 'https://custom-observability.internal', + notUseCustomDomain: true + }, + ])('uses correct domain when A365_OBSERVABILITY_DOMAIN_OVERRIDE is $description', async ({ override, expectedBaseUrl, notUseCustomDomain }) => { mockFetchSequence([200]); - process.env.A365_OBSERVABILITY_USE_CUSTOM_DOMAIN = 'true'; + process.env.A365_OBSERVABILITY_USE_CUSTOM_DOMAIN = notUseCustomDomain ? 'false' : 'true'; if (override !== undefined) { process.env.A365_OBSERVABILITY_DOMAIN_OVERRIDE = override as string; @@ -184,7 +190,11 @@ describe('Agent365Exporter', () => { const headersArg = fetchCalls[0][1].headers as Record; expect(urlArg).toBe(`${expectedBaseUrl}/maven/agent365/agents/${agentId}/traces?api-version=1`); - expect(headersArg['x-ms-tenant-id']).toBe(tenantId); + if (!notUseCustomDomain) { + expect(headersArg['x-ms-tenant-id']).toBe(tenantId); + } else { + expect(headersArg['x-ms-tenant-id']).toBeUndefined(); + } expect(headersArg['authorization']).toBe(`Bearer ${token}`); });