diff --git a/packages/agents-a365-observability/src/ObservabilityBuilder.ts b/packages/agents-a365-observability/src/ObservabilityBuilder.ts index 08d23cc..1c1604f 100644 --- a/packages/agents-a365-observability/src/ObservabilityBuilder.ts +++ b/packages/agents-a365-observability/src/ObservabilityBuilder.ts @@ -91,24 +91,33 @@ export class ObservabilityBuilder { } private createBatchProcessor(): BatchSpanProcessor { - if (!isAgent365ExporterEnabled()) { - return new BatchSpanProcessor(new ConsoleSpanExporter()); + // To send telemetry to Agent365 service, BOTH conditions must be met: + // 1. ENABLE_A365_OBSERVABILITY_EXPORTER=true must be explicitly set + // 2. A tokenResolver must be provided + const isExporterEnabled = isAgent365ExporterEnabled(); + const hasTokenResolver = this.options.tokenResolver || this.options.exporterOptions?.tokenResolver; + + // Use Agent365Exporter only if both exporter is enabled AND token resolver is available + if (isExporterEnabled && hasTokenResolver) { + const opts = new Agent365ExporterOptions(); + if (this.options.exporterOptions) { + Object.assign(opts, this.options.exporterOptions); + } + opts.clusterCategory = this.options.clusterCategory || opts.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 + }); } - - const opts = new Agent365ExporterOptions(); - if (this.options.exporterOptions) { - Object.assign(opts, this.options.exporterOptions); - } - opts.clusterCategory = this.options.clusterCategory || opts.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 - }); + + // Default: use console exporter (for local development and when service export is not configured) + return new BatchSpanProcessor(new ConsoleSpanExporter()); } private createResource() { diff --git a/packages/agents-a365-observability/src/tracing/exporter/Agent365ExporterOptions.ts b/packages/agents-a365-observability/src/tracing/exporter/Agent365ExporterOptions.ts index 547b4e4..9b5db4c 100644 --- a/packages/agents-a365-observability/src/tracing/exporter/Agent365ExporterOptions.ts +++ b/packages/agents-a365-observability/src/tracing/exporter/Agent365ExporterOptions.ts @@ -29,8 +29,8 @@ export class Agent365ExporterOptions { /** Environment / cluster category (e.g. "preprod", "prod"). */ public clusterCategory: ClusterCategory | string = 'prod'; - /** Optional delegate to resolve auth token used by exporter */ - public tokenResolver?: TokenResolver; // Optional if ENABLE_A365_OBSERVABILITY_EXPORTER is false + /** Optional delegate to resolve auth token used by exporter. Required to send telemetry to Agent365 service; if not provided, telemetry is logged to console. */ + public tokenResolver?: TokenResolver; /** Maximum span queue size before new spans are dropped. */ public maxQueueSize: number = 2048; diff --git a/packages/agents-a365-observability/src/tracing/exporter/utils.ts b/packages/agents-a365-observability/src/tracing/exporter/utils.ts index 1a63dfc..40c0fcc 100644 --- a/packages/agents-a365-observability/src/tracing/exporter/utils.ts +++ b/packages/agents-a365-observability/src/tracing/exporter/utils.ts @@ -6,6 +6,7 @@ import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import { SpanKind, SpanStatusCode } from '@opentelemetry/api'; import { OpenTelemetryConstants } from '../constants'; import logger from '../../utils/logging'; +import { isAgent365ExporterEnabled as isExporterEnabled } from '../util'; /** * Convert trace ID to hex string format @@ -110,12 +111,16 @@ export function partitionByIdentity( /** * Check if Agent 365 exporter is enabled via environment variable + * Requires explicit enabling by setting to 'true', '1', 'yes', or 'on' + * This wrapper adds logging for debugging purposes */ export function isAgent365ExporterEnabled(): boolean { - const a365Env = process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER]?.toLowerCase() || ''; - const validValues = ['true', '1', 'yes', 'on']; - const enabled: boolean = validValues.includes(a365Env); - logger.info(`[Agent365Exporter] Agent 365 exporter enabled: ${enabled}`); + const enabled = isExporterEnabled(); + const envVar = process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER]; + const message = envVar + ? `[Agent365Exporter] Agent 365 exporter enabled: ${enabled} (env var: ${envVar})` + : '[Agent365Exporter] Agent 365 exporter enabled: false (not set, requires explicit enabling)'; + logger.info(message); return enabled; } diff --git a/packages/agents-a365-observability/src/tracing/util.ts b/packages/agents-a365-observability/src/tracing/util.ts index e8568fe..afe02ce 100644 --- a/packages/agents-a365-observability/src/tracing/util.ts +++ b/packages/agents-a365-observability/src/tracing/util.ts @@ -5,8 +5,24 @@ import { OpenTelemetryConstants } from './constants'; import { ClusterCategory } from '@microsoft/agents-a365-runtime'; + +/** + * Helper function to check if a value is explicitly disabled + */ +const isExplicitlyDisabled = (value: string | undefined): boolean => { + if (!value) return false; + const lowerValue = value.toLowerCase(); + return ( + lowerValue === 'false' || + lowerValue === '0' || + lowerValue === 'no' || + lowerValue === 'off' + ); +}; + /** * Check if exporter is enabled via environment variables + * Requires explicit enabling by setting to 'true', '1', 'yes', or 'on' */ export const isAgent365ExporterEnabled: () => boolean = (): boolean => { const enableA365Exporter = process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER]?.toLowerCase(); @@ -21,17 +37,24 @@ export const isAgent365ExporterEnabled: () => boolean = (): boolean => { /** * Gets the enable telemetry configuration value + * Enabled by default, can be disabled by setting to 'false', '0', 'no', or 'off' */ export const isAgent365TelemetryEnabled: () => boolean = (): boolean => { - const enableObservability = process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY]?.toLowerCase(); - const enableA365 = process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY]?.toLowerCase(); + const enableObservability = process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY]; + const enableA365 = process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY]; - return ( - enableObservability === 'true' || - enableObservability === '1' || - enableA365 === 'true' || - enableA365 === '1' - ); + // If neither is set, default to enabled (true) + if (!enableObservability && !enableA365) { + return true; + } + + // If both are set, both must not be disabled + // If only one is set, it must not be disabled + return enableObservability && enableA365 + ? !isExplicitlyDisabled(enableObservability) && !isExplicitlyDisabled(enableA365) + : enableObservability + ? !isExplicitlyDisabled(enableObservability) + : !isExplicitlyDisabled(enableA365); }; /** diff --git a/tests/observability/core/observabilityBuilder-options.test.ts b/tests/observability/core/observabilityBuilder-options.test.ts index 1da0564..6b1e69b 100644 --- a/tests/observability/core/observabilityBuilder-options.test.ts +++ b/tests/observability/core/observabilityBuilder-options.test.ts @@ -24,17 +24,20 @@ jest.mock('@microsoft/agents-a365-observability/src/tracing/exporter/Agent365Exp describe('ObservabilityBuilder exporterOptions merging', () => { beforeEach(() => { - // Ensure exporter is enabled so BatchSpanProcessor is created with Agent365Exporter - process.env.ENABLE_A365_OBSERVABILITY_EXPORTER = 'true'; + // Clean up any captured options from previous tests delete (global as any).__capturedExporterOptions; delete (global as any).__capturedExporterOptionsCallCount; }); afterEach(() => { + // Clean up environment variable after each test delete process.env.ENABLE_A365_OBSERVABILITY_EXPORTER; }); it('applies provided exporterOptions and allows builder overrides to take precedence', () => { + // Enable Agent365 exporter to test the exporter options + process.env.ENABLE_A365_OBSERVABILITY_EXPORTER = 'true'; + const builder = new ObservabilityBuilder() .withExporterOptions({ maxQueueSize: 10, @@ -65,13 +68,42 @@ describe('ObservabilityBuilder exporterOptions merging', () => { }); it('defaults to prod clusterCategory when none provided', () => { + // Enable Agent365 exporter to test the exporter options + process.env.ENABLE_A365_OBSERVABILITY_EXPORTER = 'true'; + const builder = new ObservabilityBuilder() - .withExporterOptions({ maxQueueSize: 15 }); // no cluster category passed + .withExporterOptions({ maxQueueSize: 15 }) // no cluster category passed + .withTokenResolver(() => 'test-token'); // Add token resolver so Agent365Exporter is used builder.build(); const captured: any = (global as any).__capturedExporterOptions; expect(captured.clusterCategory).toBe('prod'); expect(captured.maxQueueSize).toBe(15); expect(captured.scheduledDelayMilliseconds).toBe(5000); // default value - }); + }); + + it('uses ConsoleSpanExporter when no tokenResolver is provided', () => { + const builder = new ObservabilityBuilder() + .withExporterOptions({ maxQueueSize: 15 }); // no token resolver + + const built = builder.build(); + expect(built).toBe(true); + + // Since no tokenResolver was provided, Agent365Exporter should NOT be created + const captured: any = (global as any).__capturedExporterOptions; + expect(captured).toBeUndefined(); + }); + + it('uses ConsoleSpanExporter when ENABLE_A365_OBSERVABILITY_EXPORTER is not set', () => { + // Even with tokenResolver, if env var is not set, should use ConsoleSpanExporter + const builder = new ObservabilityBuilder() + .withTokenResolver(() => 'test-token'); + + const built = builder.build(); + expect(built).toBe(true); + + // Since ENABLE_A365_OBSERVABILITY_EXPORTER is not set, Agent365Exporter should NOT be created + const captured: any = (global as any).__capturedExporterOptions; + expect(captured).toBeUndefined(); + }); }); diff --git a/tests/observability/core/util.test.ts b/tests/observability/core/util.test.ts new file mode 100644 index 0000000..c4521b1 --- /dev/null +++ b/tests/observability/core/util.test.ts @@ -0,0 +1,128 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// ------------------------------------------------------------------------------ + +import { isAgent365TelemetryEnabled, isAgent365ExporterEnabled } from '@microsoft/agents-a365-observability/src/tracing/util'; +import { OpenTelemetryConstants } from '@microsoft/agents-a365-observability/src/tracing/constants'; + +describe('Observability Utility Functions', () => { + describe('isAgent365TelemetryEnabled', () => { + beforeEach(() => { + // Clear all relevant environment variables before each test + delete process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY]; + delete process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY]; + }); + + it('should return true by default when no environment variables are set', () => { + expect(isAgent365TelemetryEnabled()).toBe(true); + }); + + it('should return false when ENABLE_OBSERVABILITY is explicitly set to false', () => { + process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY] = 'false'; + expect(isAgent365TelemetryEnabled()).toBe(false); + }); + + it('should return false when ENABLE_OBSERVABILITY is set to 0', () => { + process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY] = '0'; + expect(isAgent365TelemetryEnabled()).toBe(false); + }); + + it('should return false when ENABLE_OBSERVABILITY is set to no', () => { + process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY] = 'no'; + expect(isAgent365TelemetryEnabled()).toBe(false); + }); + + it('should return false when ENABLE_OBSERVABILITY is set to off', () => { + process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY] = 'off'; + expect(isAgent365TelemetryEnabled()).toBe(false); + }); + + it('should return true when ENABLE_OBSERVABILITY is set to true', () => { + process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY] = 'true'; + expect(isAgent365TelemetryEnabled()).toBe(true); + }); + + it('should return true when ENABLE_OBSERVABILITY is set to 1', () => { + process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY] = '1'; + expect(isAgent365TelemetryEnabled()).toBe(true); + }); + + it('should return false when ENABLE_A365_OBSERVABILITY is explicitly set to false', () => { + process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY] = 'false'; + expect(isAgent365TelemetryEnabled()).toBe(false); + }); + + it('should return true when ENABLE_A365_OBSERVABILITY is set to true', () => { + process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY] = 'true'; + expect(isAgent365TelemetryEnabled()).toBe(true); + }); + + it('should return false when both are explicitly disabled', () => { + process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY] = 'false'; + process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY] = 'false'; + expect(isAgent365TelemetryEnabled()).toBe(false); + }); + + it('should return true when ENABLE_OBSERVABILITY is enabled and ENABLE_A365_OBSERVABILITY is not set', () => { + process.env[OpenTelemetryConstants.ENABLE_OBSERVABILITY] = 'true'; + expect(isAgent365TelemetryEnabled()).toBe(true); + }); + + it('should return true when ENABLE_A365_OBSERVABILITY is enabled and ENABLE_OBSERVABILITY is not set', () => { + process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY] = 'true'; + expect(isAgent365TelemetryEnabled()).toBe(true); + }); + }); + + describe('isAgent365ExporterEnabled', () => { + beforeEach(() => { + // Clear environment variable before each test + delete process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER]; + }); + + it('should return false by default when environment variable is not set', () => { + expect(isAgent365ExporterEnabled()).toBe(false); + }); + + it('should return false when explicitly set to false', () => { + process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER] = 'false'; + expect(isAgent365ExporterEnabled()).toBe(false); + }); + + it('should return false when set to 0', () => { + process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER] = '0'; + expect(isAgent365ExporterEnabled()).toBe(false); + }); + + it('should return false when set to no', () => { + process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER] = 'no'; + expect(isAgent365ExporterEnabled()).toBe(false); + }); + + it('should return false when set to off', () => { + process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER] = 'off'; + expect(isAgent365ExporterEnabled()).toBe(false); + }); + + it('should return true when set to true', () => { + process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER] = 'true'; + expect(isAgent365ExporterEnabled()).toBe(true); + }); + + it('should return true when set to 1', () => { + process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER] = '1'; + expect(isAgent365ExporterEnabled()).toBe(true); + }); + + it('should return true when set to yes', () => { + process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER] = 'yes'; + expect(isAgent365ExporterEnabled()).toBe(true); + }); + + it('should return true when set to on', () => { + process.env[OpenTelemetryConstants.ENABLE_A365_OBSERVABILITY_EXPORTER] = 'on'; + expect(isAgent365ExporterEnabled()).toBe(true); + }); + }); +});