From 73bcdd191b4717a94ee0b9564cd7805f8733e679 Mon Sep 17 00:00:00 2001 From: Peng Fan Date: Thu, 30 Apr 2026 14:46:00 -0700 Subject: [PATCH 1/4] Exclude hostDetector from default OTel resource detectors The NodeSDK default resource detectors auto-attach host.name and host.arch to every span, leaking machine details into exported telemetry. Explicitly list only envDetector, processDetector, and serviceInstanceIdDetector to exclude hostDetector. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/agents-a365-observability/src/ObservabilityBuilder.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/agents-a365-observability/src/ObservabilityBuilder.ts b/packages/agents-a365-observability/src/ObservabilityBuilder.ts index d55f2fcf..7099538e 100644 --- a/packages/agents-a365-observability/src/ObservabilityBuilder.ts +++ b/packages/agents-a365-observability/src/ObservabilityBuilder.ts @@ -9,7 +9,7 @@ import { Agent365Exporter } from './tracing/exporter/Agent365Exporter'; import type { TokenResolver } from './tracing/exporter/Agent365ExporterOptions'; import { Agent365ExporterOptions } from './tracing/exporter/Agent365ExporterOptions'; import { PerRequestSpanProcessor } from './tracing/PerRequestSpanProcessor'; -import { resourceFromAttributes } from '@opentelemetry/resources'; +import { resourceFromAttributes, envDetector, processDetector, serviceInstanceIdDetector } from '@opentelemetry/resources'; import { ATTR_SERVICE_NAME, ATTR_SERVICE_NAMESPACE } from '@opentelemetry/semantic-conventions'; import { trace } from '@opentelemetry/api'; import { ClusterCategory, IConfigurationProvider } from '@microsoft/agents-a365-runtime'; @@ -261,6 +261,7 @@ export class ObservabilityBuilder { // Create & configure the NodeSDK manually so we can inject processors + resource. this.sdk = new NodeSDK({ resource: this.createResource(), + resourceDetectors: [envDetector, processDetector, serviceInstanceIdDetector], spanProcessors: [ spanProcessor, exportProcessor, From be976de09e921dc5fd037d2203ccc24b95f1e415 Mon Sep 17 00:00:00 2001 From: Peng Fan Date: Thu, 30 Apr 2026 15:58:59 -0700 Subject: [PATCH 2/4] Exclude hostDetector and serviceInstanceIdDetector from OTel resource detectors Prevents host.name, host.arch, and service.instance.id from leaking into exported telemetry. Keeps envDetector and processDetector. Adds unit test that verifies actual resource attributes on exported spans. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/ObservabilityBuilder.ts | 4 +- .../core/resource-detectors.test.ts | 81 +++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 tests/observability/core/resource-detectors.test.ts diff --git a/packages/agents-a365-observability/src/ObservabilityBuilder.ts b/packages/agents-a365-observability/src/ObservabilityBuilder.ts index 7099538e..d09484d2 100644 --- a/packages/agents-a365-observability/src/ObservabilityBuilder.ts +++ b/packages/agents-a365-observability/src/ObservabilityBuilder.ts @@ -9,7 +9,7 @@ import { Agent365Exporter } from './tracing/exporter/Agent365Exporter'; import type { TokenResolver } from './tracing/exporter/Agent365ExporterOptions'; import { Agent365ExporterOptions } from './tracing/exporter/Agent365ExporterOptions'; import { PerRequestSpanProcessor } from './tracing/PerRequestSpanProcessor'; -import { resourceFromAttributes, envDetector, processDetector, serviceInstanceIdDetector } from '@opentelemetry/resources'; +import { resourceFromAttributes, envDetector, processDetector } from '@opentelemetry/resources'; import { ATTR_SERVICE_NAME, ATTR_SERVICE_NAMESPACE } from '@opentelemetry/semantic-conventions'; import { trace } from '@opentelemetry/api'; import { ClusterCategory, IConfigurationProvider } from '@microsoft/agents-a365-runtime'; @@ -261,7 +261,7 @@ export class ObservabilityBuilder { // Create & configure the NodeSDK manually so we can inject processors + resource. this.sdk = new NodeSDK({ resource: this.createResource(), - resourceDetectors: [envDetector, processDetector, serviceInstanceIdDetector], + resourceDetectors: [envDetector, processDetector], spanProcessors: [ spanProcessor, exportProcessor, diff --git a/tests/observability/core/resource-detectors.test.ts b/tests/observability/core/resource-detectors.test.ts new file mode 100644 index 00000000..96176676 --- /dev/null +++ b/tests/observability/core/resource-detectors.test.ts @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { trace } from '@opentelemetry/api'; +import { ObservabilityBuilder } from '@microsoft/agents-a365-observability/src/ObservabilityBuilder'; + +describe('ObservabilityBuilder resource detectors', () => { + it('should not include host.name or host.arch in exported spans', async () => { + // Force ObservabilityBuilder to take the NodeSDK code path + jest.spyOn(trace, 'getTracerProvider').mockReturnValue({} as any); + + const builder = new ObservabilityBuilder() + .withService('resource-detector-test', '1.0.0'); + + builder.build(); + builder.start(); + + // Restore spy so we can read the real provider set by NodeSDK + jest.restoreAllMocks(); + + // Give async resource detectors time to resolve + await new Promise(resolve => setTimeout(resolve, 1000)); + + // The NodeSDK sets a ProxyTracerProvider as global. + const globalProvider = trace.getTracerProvider() as any; + + // Walk the internal chain to find the resource + let resource: any; + const candidates = [ + globalProvider?.resource, + globalProvider?.getDelegate?.()?.resource, + globalProvider?._delegate?.resource, + globalProvider?._delegate?.getDelegate?.()?.resource, + ]; + + for (const candidate of candidates) { + if (candidate?.attributes) { + resource = candidate; + break; + } + } + + // Fallback: access from the builder's internal sdk field + if (!resource) { + const sdk = (builder as any).sdk; + if (sdk) { + const internalCandidates = [ + sdk?._tracerProvider?.resource, + sdk?._resource, + sdk?.resource, + ]; + for (const candidate of internalCandidates) { + if (candidate?.attributes) { + resource = candidate; + break; + } + } + } + } + + expect(resource).toBeDefined(); + expect(resource.attributes).toBeDefined(); + + // host.name and host.arch should NOT be present (hostDetector excluded) + expect(resource.attributes['host.name']).toBeUndefined(); + expect(resource.attributes['host.arch']).toBeUndefined(); + + // service.instance.id should NOT be present (serviceInstanceIdDetector excluded) + expect(resource.attributes['service.instance.id']).toBeUndefined(); + + // process.pid SHOULD be present (processDetector kept) + expect(resource.attributes['process.pid']).toBeDefined(); + expect(resource.attributes['process.pid']).toBe(process.pid); + + // service.name SHOULD be present (explicitly set) + expect(resource.attributes['service.name']).toContain('resource-detector-test'); + + // Clean up + await builder.shutdown(); + }); +}); From 2d32a1adfe153ceb20d88089a1935ae76553b867 Mon Sep 17 00:00:00 2001 From: Peng Fan Date: Thu, 30 Apr 2026 16:13:59 -0700 Subject: [PATCH 3/4] Move resource detectors test to observabilityManager.test.ts Co-Authored-By: Claude Opus 4.6 (1M context) --- .../core/observabilityBuilder-options.test.ts | 1 + .../core/observabilityManager.test.ts | 78 ++++++++++++++++++ .../core/resource-detectors.test.ts | 81 ------------------- 3 files changed, 79 insertions(+), 81 deletions(-) delete mode 100644 tests/observability/core/resource-detectors.test.ts diff --git a/tests/observability/core/observabilityBuilder-options.test.ts b/tests/observability/core/observabilityBuilder-options.test.ts index ebfd2654..a4f4c98d 100644 --- a/tests/observability/core/observabilityBuilder-options.test.ts +++ b/tests/observability/core/observabilityBuilder-options.test.ts @@ -78,6 +78,7 @@ describe('ObservabilityBuilder exporterOptions merging', () => { }); }); + describe('ObservabilityBuilder serviceNamespace', () => { let createResourceSpy: jest.SpyInstance; diff --git a/tests/observability/core/observabilityManager.test.ts b/tests/observability/core/observabilityManager.test.ts index 5eb0d850..1c1a9b41 100644 --- a/tests/observability/core/observabilityManager.test.ts +++ b/tests/observability/core/observabilityManager.test.ts @@ -1,5 +1,7 @@ import { describe, it, expect, afterEach } from '@jest/globals'; import { ObservabilityManager, Builder } from '@microsoft/agents-a365-observability'; +import { trace } from '@opentelemetry/api'; +import { ObservabilityBuilder } from '@microsoft/agents-a365-observability/src/ObservabilityBuilder'; describe('Agent 365 SDK', () => { afterEach(async () => { @@ -54,4 +56,80 @@ describe('Agent 365 SDK', () => { }); }); + + describe('Resource detectors', () => { + it('should not include host.name or host.arch in exported spans', async () => { + // Force ObservabilityBuilder to take the NodeSDK code path + jest.spyOn(trace, 'getTracerProvider').mockReturnValue({} as any); + + const builder = new ObservabilityBuilder() + .withService('resource-detector-test', '1.0.0'); + + builder.build(); + builder.start(); + + // Restore spy so we can read the real provider set by NodeSDK + jest.restoreAllMocks(); + + // Give async resource detectors time to resolve + await new Promise(resolve => setTimeout(resolve, 1000)); + + // The NodeSDK sets a ProxyTracerProvider as global. + const globalProvider = trace.getTracerProvider() as any; + + // Walk the internal chain to find the resource + let resource: any; + const candidates = [ + globalProvider?.resource, + globalProvider?.getDelegate?.()?.resource, + globalProvider?._delegate?.resource, + globalProvider?._delegate?.getDelegate?.()?.resource, + ]; + + for (const candidate of candidates) { + if (candidate?.attributes) { + resource = candidate; + break; + } + } + + // Fallback: access from the builder's internal sdk field + if (!resource) { + const sdk = (builder as any).sdk; + if (sdk) { + const internalCandidates = [ + sdk?._tracerProvider?.resource, + sdk?._resource, + sdk?.resource, + ]; + for (const candidate of internalCandidates) { + if (candidate?.attributes) { + resource = candidate; + break; + } + } + } + } + + expect(resource).toBeDefined(); + expect(resource.attributes).toBeDefined(); + + // host.name and host.arch should NOT be present (hostDetector excluded) + expect(resource.attributes['host.name']).toBeUndefined(); + expect(resource.attributes['host.arch']).toBeUndefined(); + + // service.instance.id should NOT be present (serviceInstanceIdDetector excluded) + expect(resource.attributes['service.instance.id']).toBeUndefined(); + + // process.pid SHOULD be present (processDetector kept) + expect(resource.attributes['process.pid']).toBeDefined(); + expect(resource.attributes['process.pid']).toBe(process.pid); + + // service.name SHOULD be present (explicitly set) + expect(resource.attributes['service.name']).toContain('resource-detector-test'); + + // Clean up + await builder.shutdown(); + }); + }); }); diff --git a/tests/observability/core/resource-detectors.test.ts b/tests/observability/core/resource-detectors.test.ts deleted file mode 100644 index 96176676..00000000 --- a/tests/observability/core/resource-detectors.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { trace } from '@opentelemetry/api'; -import { ObservabilityBuilder } from '@microsoft/agents-a365-observability/src/ObservabilityBuilder'; - -describe('ObservabilityBuilder resource detectors', () => { - it('should not include host.name or host.arch in exported spans', async () => { - // Force ObservabilityBuilder to take the NodeSDK code path - jest.spyOn(trace, 'getTracerProvider').mockReturnValue({} as any); - - const builder = new ObservabilityBuilder() - .withService('resource-detector-test', '1.0.0'); - - builder.build(); - builder.start(); - - // Restore spy so we can read the real provider set by NodeSDK - jest.restoreAllMocks(); - - // Give async resource detectors time to resolve - await new Promise(resolve => setTimeout(resolve, 1000)); - - // The NodeSDK sets a ProxyTracerProvider as global. - const globalProvider = trace.getTracerProvider() as any; - - // Walk the internal chain to find the resource - let resource: any; - const candidates = [ - globalProvider?.resource, - globalProvider?.getDelegate?.()?.resource, - globalProvider?._delegate?.resource, - globalProvider?._delegate?.getDelegate?.()?.resource, - ]; - - for (const candidate of candidates) { - if (candidate?.attributes) { - resource = candidate; - break; - } - } - - // Fallback: access from the builder's internal sdk field - if (!resource) { - const sdk = (builder as any).sdk; - if (sdk) { - const internalCandidates = [ - sdk?._tracerProvider?.resource, - sdk?._resource, - sdk?.resource, - ]; - for (const candidate of internalCandidates) { - if (candidate?.attributes) { - resource = candidate; - break; - } - } - } - } - - expect(resource).toBeDefined(); - expect(resource.attributes).toBeDefined(); - - // host.name and host.arch should NOT be present (hostDetector excluded) - expect(resource.attributes['host.name']).toBeUndefined(); - expect(resource.attributes['host.arch']).toBeUndefined(); - - // service.instance.id should NOT be present (serviceInstanceIdDetector excluded) - expect(resource.attributes['service.instance.id']).toBeUndefined(); - - // process.pid SHOULD be present (processDetector kept) - expect(resource.attributes['process.pid']).toBeDefined(); - expect(resource.attributes['process.pid']).toBe(process.pid); - - // service.name SHOULD be present (explicitly set) - expect(resource.attributes['service.name']).toContain('resource-detector-test'); - - // Clean up - await builder.shutdown(); - }); -}); From 152b11cc5ca376ce53f0baeb7d2810559779ebd5 Mon Sep 17 00:00:00 2001 From: PengF <126631706+fpfp100@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:27:34 -0700 Subject: [PATCH 4/4] Update tests/observability/core/observabilityManager.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../core/observabilityManager.test.ts | 108 ++++++++++-------- 1 file changed, 58 insertions(+), 50 deletions(-) diff --git a/tests/observability/core/observabilityManager.test.ts b/tests/observability/core/observabilityManager.test.ts index 1c1a9b41..f5d1bb81 100644 --- a/tests/observability/core/observabilityManager.test.ts +++ b/tests/observability/core/observabilityManager.test.ts @@ -60,76 +60,84 @@ describe('Agent 365 SDK', () => { describe('Resource detectors', () => { it('should not include host.name or host.arch in exported spans', async () => { // Force ObservabilityBuilder to take the NodeSDK code path - jest.spyOn(trace, 'getTracerProvider').mockReturnValue({} as any); + const getTracerProviderSpy = jest + .spyOn(trace, 'getTracerProvider') + .mockReturnValue({} as any); const builder = new ObservabilityBuilder() .withService('resource-detector-test', '1.0.0'); - builder.build(); - builder.start(); + try { + builder.build(); + builder.start(); - // Restore spy so we can read the real provider set by NodeSDK - jest.restoreAllMocks(); + // Restore only this spy so we can read the real provider set by NodeSDK + getTracerProviderSpy.mockRestore(); - // Give async resource detectors time to resolve - await new Promise(resolve => setTimeout(resolve, 1000)); + // Give async resource detectors time to resolve + await new Promise(resolve => setTimeout(resolve, 1000)); - // The NodeSDK sets a ProxyTracerProvider as global. - const globalProvider = trace.getTracerProvider() as any; + // The NodeSDK sets a ProxyTracerProvider as global. + const globalProvider = trace.getTracerProvider() as any; - // Walk the internal chain to find the resource - let resource: any; - const candidates = [ - globalProvider?.resource, - globalProvider?.getDelegate?.()?.resource, - globalProvider?._delegate?.resource, - globalProvider?._delegate?.getDelegate?.()?.resource, - ]; + // Walk the internal chain to find the resource + let resource: any; + const candidates = [ + globalProvider?.resource, + globalProvider?.getDelegate?.()?.resource, + globalProvider?._delegate?.resource, + globalProvider?._delegate?.getDelegate?.()?.resource, + ]; - for (const candidate of candidates) { - if (candidate?.attributes) { - resource = candidate; - break; + for (const candidate of candidates) { + if (candidate?.attributes) { + resource = candidate; + break; + } } - } - // Fallback: access from the builder's internal sdk field - if (!resource) { - const sdk = (builder as any).sdk; - if (sdk) { - const internalCandidates = [ - sdk?._tracerProvider?.resource, - sdk?._resource, - sdk?.resource, - ]; - for (const candidate of internalCandidates) { - if (candidate?.attributes) { - resource = candidate; - break; + // Fallback: access from the builder's internal sdk field + if (!resource) { + const sdk = (builder as any).sdk; + if (sdk) { + const internalCandidates = [ + sdk?._tracerProvider?.resource, + sdk?._resource, + sdk?.resource, + ]; + for (const candidate of internalCandidates) { + if (candidate?.attributes) { + resource = candidate; + break; + } } } } - } - expect(resource).toBeDefined(); - expect(resource.attributes).toBeDefined(); + expect(resource).toBeDefined(); + expect(resource.attributes).toBeDefined(); - // host.name and host.arch should NOT be present (hostDetector excluded) - expect(resource.attributes['host.name']).toBeUndefined(); - expect(resource.attributes['host.arch']).toBeUndefined(); + // host.name and host.arch should NOT be present (hostDetector excluded) + expect(resource.attributes['host.name']).toBeUndefined(); + expect(resource.attributes['host.arch']).toBeUndefined(); - // service.instance.id should NOT be present (serviceInstanceIdDetector excluded) - expect(resource.attributes['service.instance.id']).toBeUndefined(); + // service.instance.id should NOT be present (serviceInstanceIdDetector excluded) + expect(resource.attributes['service.instance.id']).toBeUndefined(); - // process.pid SHOULD be present (processDetector kept) - expect(resource.attributes['process.pid']).toBeDefined(); - expect(resource.attributes['process.pid']).toBe(process.pid); + // process.pid SHOULD be present (processDetector kept) + expect(resource.attributes['process.pid']).toBeDefined(); + expect(resource.attributes['process.pid']).toBe(process.pid); - // service.name SHOULD be present (explicitly set) - expect(resource.attributes['service.name']).toContain('resource-detector-test'); + // service.name SHOULD be present (explicitly set) + expect(resource.attributes['service.name']).toContain('resource-detector-test'); + } finally { + if (getTracerProviderSpy.mockRestore) { + getTracerProviderSpy.mockRestore(); + } - // Clean up - await builder.shutdown(); + // Clean up + await builder.shutdown(); + } }); }); });