diff --git a/packages/agents-a365-observability/src/ObservabilityBuilder.ts b/packages/agents-a365-observability/src/ObservabilityBuilder.ts index d55f2fcf..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 } 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,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], spanProcessors: [ spanProcessor, exportProcessor, 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..f5d1bb81 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,88 @@ 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 + const getTracerProviderSpy = jest + .spyOn(trace, 'getTracerProvider') + .mockReturnValue({} as any); + + const builder = new ObservabilityBuilder() + .withService('resource-detector-test', '1.0.0'); + + try { + builder.build(); + builder.start(); + + // 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)); + + // 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'); + } finally { + if (getTracerProviderSpy.mockRestore) { + getTracerProviderSpy.mockRestore(); + } + + // Clean up + await builder.shutdown(); + } + }); + }); });