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
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}`);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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
Expand Down
46 changes: 6 additions & 40 deletions packages/agents-a365-observability/src/tracing/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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';
}
}
20 changes: 15 additions & 5 deletions tests/observability/core/agent365-exporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
},
Expand All @@ -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;
Expand Down Expand Up @@ -184,7 +190,11 @@ describe('Agent365Exporter', () => {
const headersArg = fetchCalls[0][1].headers as Record<string, string>;

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}`);
});

Expand Down