diff --git a/CHANGELOG.md b/CHANGELOG.md index f1670b19c7..0ed63cceff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ For experimental package changes, see the [experimental CHANGELOG](experimental/ ### :rocket: (Enhancement) +* feat(resource): create sync resource with some attributes that resolve asynchronously [#3460](https://github.com/open-telemetry/opentelemetry-js/pull/3460) @samimusallam * feat (api-logs): separate Events API into its own package [3550](https://github.com/open-telemetry/opentelemetry-js/pull/3550) @martinkuba * feat(sdk-metrics): apply binary search in histogram recording [#3539](https://github.com/open-telemetry/opentelemetry-js/pull/3539) @legendecas * perf(propagator-jaeger): improve deserializeSpanContext performance [#3541](https://github.com/open-telemetry/opentelemetry-js/pull/3541) @doochik diff --git a/README.md b/README.md index 93d49a049b..fb766e55ce 100644 --- a/README.md +++ b/README.md @@ -84,9 +84,7 @@ const sdk = new opentelemetry.NodeSDK({ // initialize the SDK and register with the OpenTelemetry API // this enables the API to record telemetry -sdk.start() - .then(() => console.log('Tracing initialized')) - .catch((error) => console.log('Error initializing tracing', error)); +sdk.start(); // gracefully shut down the SDK on process exit process.on('SIGTERM', () => { @@ -277,6 +275,11 @@ These instrumentations are hosted at { + async detect(config?: ResourceDetectionConfig): Promise { const isBrowser = typeof navigator !== 'undefined'; if (!isBrowser) { return Resource.empty(); diff --git a/experimental/packages/opentelemetry-browser-detector/test/BrowserDetector.test.ts b/experimental/packages/opentelemetry-browser-detector/test/BrowserDetector.test.ts index 7eb3928161..727007aa55 100644 --- a/experimental/packages/opentelemetry-browser-detector/test/BrowserDetector.test.ts +++ b/experimental/packages/opentelemetry-browser-detector/test/BrowserDetector.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import * as sinon from 'sinon'; -import { Resource } from '@opentelemetry/resources'; +import { IResource } from '@opentelemetry/resources'; import { browserDetector } from '../src/BrowserDetector'; import { describeBrowser, assertResource, assertEmptyResource } from './util'; @@ -47,7 +47,7 @@ describeBrowser('browserDetector()', () => { }, }); - const resource: Resource = await browserDetector.detect(); + const resource: IResource = await browserDetector.detect(); assertResource(resource, { platform: 'platform', brands: ['Chromium 106', 'Google Chrome 106', 'Not;A=Brand 99'], @@ -63,7 +63,7 @@ describeBrowser('browserDetector()', () => { userAgentData: undefined, }); - const resource: Resource = await browserDetector.detect(); + const resource: IResource = await browserDetector.detect(); assertResource(resource, { language: 'en-US', user_agent: 'dddd', @@ -74,7 +74,7 @@ describeBrowser('browserDetector()', () => { sinon.stub(globalThis, 'navigator').value({ userAgent: '', }); - const resource: Resource = await browserDetector.detect(); + const resource: IResource = await browserDetector.detect(); assertEmptyResource(resource); }); }); diff --git a/experimental/packages/opentelemetry-browser-detector/test/util.ts b/experimental/packages/opentelemetry-browser-detector/test/util.ts index b74fd89ced..3318f48910 100644 --- a/experimental/packages/opentelemetry-browser-detector/test/util.ts +++ b/experimental/packages/opentelemetry-browser-detector/test/util.ts @@ -16,7 +16,7 @@ import { Suite } from 'mocha'; import * as assert from 'assert'; import { BROWSER_ATTRIBUTES } from '../src/types'; -import { Resource } from '@opentelemetry/resources'; +import { IResource } from '@opentelemetry/resources'; export function describeBrowser(title: string, fn: (this: Suite) => void) { title = `Browser: ${title}`; @@ -27,7 +27,7 @@ export function describeBrowser(title: string, fn: (this: Suite) => void) { } export const assertResource = ( - resource: Resource, + resource: IResource, validations: { platform?: string; brands?: string[]; @@ -74,6 +74,6 @@ export const assertResource = ( * * @param resource the Resource to validate */ -export const assertEmptyResource = (resource: Resource) => { +export const assertEmptyResource = (resource: IResource) => { assert.strictEqual(Object.keys(resource.attributes).length, 0); }; diff --git a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts index 2d9808bf35..75b29bb61b 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts @@ -29,7 +29,7 @@ import { Histogram, } from '@opentelemetry/sdk-metrics'; import { hrTimeToMilliseconds } from '@opentelemetry/core'; -import { Resource } from '@opentelemetry/resources'; +import { IResource } from '@opentelemetry/resources'; type PrometheusDataTypeLiteral = | 'counter' @@ -340,7 +340,7 @@ export class PrometheusSerializer { return results; } - protected _serializeResource(resource: Resource): string { + protected _serializeResource(resource: IResource): string { const name = 'target_info'; const help = `# HELP ${name} Target metadata`; const type = `# TYPE ${name} gauge`; diff --git a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts index caad9b85e2..fa14043cd3 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts @@ -21,8 +21,10 @@ import { } from '@opentelemetry/instrumentation'; import { Detector, - detectResources, + DetectorSync, + detectResourcesSync, envDetector, + IResource, processDetector, Resource, ResourceDetectionConfig, @@ -63,8 +65,8 @@ export class NodeSDK { private _meterProviderConfig?: MeterProviderConfig; private _instrumentations: InstrumentationOption[]; - private _resource: Resource; - private _resourceDetectors: Detector[]; + private _resource: IResource; + private _resourceDetectors: Array; private _autoDetectResources: boolean; @@ -183,7 +185,7 @@ export class NodeSDK { } /** Detect resource attributes */ - public async detectResources(): Promise { + public detectResources(): void { if (this._disabled) { return; } @@ -192,18 +194,18 @@ export class NodeSDK { detectors: this._resourceDetectors, }; - this.addResource(await detectResources(internalConfig)); + this.addResource(detectResourcesSync(internalConfig)); } /** Manually add a resource */ - public addResource(resource: Resource): void { + public addResource(resource: IResource): void { this._resource = this._resource.merge(resource); } /** * Once the SDK has been configured, call this method to construct SDK components and register them with the OpenTelemetry API. */ - public async start(): Promise { + public start(): void { if (this._disabled) { return; } @@ -213,7 +215,7 @@ export class NodeSDK { }); if (this._autoDetectResources) { - await this.detectResources(); + this.detectResources(); } this._resource = diff --git a/experimental/packages/opentelemetry-sdk-node/src/types.ts b/experimental/packages/opentelemetry-sdk-node/src/types.ts index 4c816516dc..49dfae8f7c 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/types.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/types.ts @@ -17,7 +17,7 @@ import type { ContextManager, SpanAttributes } from '@opentelemetry/api'; import { TextMapPropagator } from '@opentelemetry/api'; import { InstrumentationOption } from '@opentelemetry/instrumentation'; -import { Detector, Resource } from '@opentelemetry/resources'; +import { Detector, DetectorSync, Resource } from '@opentelemetry/resources'; import { MetricReader, View } from '@opentelemetry/sdk-metrics'; import { Sampler, @@ -35,7 +35,7 @@ export interface NodeSDKConfiguration { views: View[]; instrumentations: InstrumentationOption[]; resource: Resource; - resourceDetectors: Detector[]; + resourceDetectors: Array; sampler: Sampler; serviceName?: string; spanProcessor: SpanProcessor; diff --git a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts index 2f7957a8ec..60b493435b 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts @@ -87,7 +87,7 @@ describe('Node SDK', () => { autoDetectResources: false, }); - await sdk.start(); + sdk.start(); assert.strictEqual( context['_getContextManager'](), @@ -114,7 +114,7 @@ describe('Node SDK', () => { autoDetectResources: false, }); - await sdk.start(); + sdk.start(); assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider)); @@ -139,7 +139,7 @@ describe('Node SDK', () => { autoDetectResources: false, }); - await sdk.start(); + sdk.start(); assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider)); @@ -171,7 +171,7 @@ describe('Node SDK', () => { autoDetectResources: false, }); - await sdk.start(); + sdk.start(); assert.strictEqual( context['_getContextManager'](), @@ -237,7 +237,7 @@ describe('Node SDK', () => { autoDetectResources: false, }); - await sdk.start(); + sdk.start(); assert.strictEqual( context['_getContextManager'](), @@ -391,8 +391,9 @@ describe('Node SDK', () => { envDetector, ], }); - await sdk.detectResources(); + sdk.detectResources(); const resource = sdk['_resource']; + await resource.waitForAsyncAttributes?.(); assert.strictEqual(resource.attributes['customAttr'], 'someValue'); @@ -420,8 +421,9 @@ describe('Node SDK', () => { ], }); - await sdk.detectResources(); + sdk.detectResources(); const resource = sdk['_resource']; + await resource.waitForAsyncAttributes?.(); assertServiceResource(resource, { instanceId: '627cc493', @@ -460,6 +462,7 @@ describe('Node SDK', () => { // This test depends on the env detector to be functioning as intended const mockedLoggerMethod = Sinon.fake(); const mockedVerboseLoggerMethod = Sinon.fake(); + diag.setLogger( { debug: mockedLoggerMethod, @@ -468,7 +471,8 @@ describe('Node SDK', () => { DiagLogLevel.VERBOSE ); - await sdk.detectResources(); + sdk.detectResources(); + await sdk['_resource'].waitForAsyncAttributes?.(); // Test that the Env Detector successfully found its resource and populated it with the right values. assert.ok( @@ -478,7 +482,7 @@ describe('Node SDK', () => { assert.ok( callArgsMatches( mockedVerboseLoggerMethod, - /{\s+'service\.instance\.id':\s+'627cc493',\s+'service\.name':\s+'my-service',\s+'service\.namespace':\s+'default',\s+'service\.version':\s+'0\.0\.1'\s+}\s*/ + /{\s+"service\.instance\.id":\s+"627cc493",\s+"service\.name":\s+"my-service",\s+"service\.namespace":\s+"default",\s+"service\.version":\s+"0.0.1"\s+}\s*/gm ) ); }); @@ -500,7 +504,7 @@ describe('Node SDK', () => { DiagLogLevel.DEBUG ); - await sdk.detectResources(); + sdk.detectResources(); assert.ok( callArgsContains( @@ -519,7 +523,7 @@ describe('Node SDK', () => { serviceName: 'config-set-name', }); - await sdk.start(); + sdk.start(); const resource = sdk['_resource']; assertServiceResource(resource, { @@ -531,8 +535,9 @@ describe('Node SDK', () => { process.env.OTEL_SERVICE_NAME = 'env-set-name'; const sdk = new NodeSDK(); - await sdk.start(); + sdk.start(); const resource = sdk['_resource']; + await resource.waitForAsyncAttributes?.(); assertServiceResource(resource, { name: 'env-set-name', @@ -546,8 +551,9 @@ describe('Node SDK', () => { serviceName: 'config-set-name', }); - await sdk.start(); + sdk.start(); const resource = sdk['_resource']; + await resource.waitForAsyncAttributes?.(); assertServiceResource(resource, { name: 'config-set-name', @@ -560,8 +566,9 @@ describe('Node SDK', () => { 'service.name=resource-env-set-name'; const sdk = new NodeSDK(); - await sdk.start(); + sdk.start(); const resource = sdk['_resource']; + await resource.waitForAsyncAttributes?.(); assertServiceResource(resource, { name: 'resource-env-set-name', @@ -576,8 +583,9 @@ describe('Node SDK', () => { serviceName: 'config-set-name', }); - await sdk.start(); + sdk.start(); const resource = sdk['_resource']; + await resource.waitForAsyncAttributes?.(); assertServiceResource(resource, { name: 'config-set-name', @@ -650,8 +658,9 @@ describe('Node SDK', () => { envDetector, ], }); - await sdk.detectResources(); + sdk.detectResources(); const resource = sdk['_resource']; + await resource.waitForAsyncAttributes?.(); assert.deepStrictEqual(resource, Resource.empty()); }); @@ -676,7 +685,7 @@ describe('setup exporter from env', () => { }); it('use default exporter TracerProviderWithEnvExporters when user does not provide span processor or trace exporter to sdk config', async () => { const sdk = new NodeSDK(); - await sdk.start(); + sdk.start(); const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; @@ -689,7 +698,7 @@ describe('setup exporter from env', () => { const sdk = new NodeSDK({ traceExporter, }); - await sdk.start(); + sdk.start(); const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; @@ -706,7 +715,7 @@ describe('setup exporter from env', () => { const sdk = new NodeSDK({ spanProcessor, }); - await sdk.start(); + sdk.start(); const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; @@ -723,7 +732,7 @@ describe('setup exporter from env', () => { const sdk = new NodeSDK({ traceExporter, }); - await sdk.start(); + sdk.start(); const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; @@ -739,7 +748,7 @@ describe('setup exporter from env', () => { env.OTEL_TRACES_EXPORTER = 'otlp'; env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'grpc'; const sdk = new NodeSDK(); - await sdk.start(); + sdk.start(); const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; @@ -752,7 +761,7 @@ describe('setup exporter from env', () => { it('use noop span processor when user sets env exporter to none', async () => { env.OTEL_TRACES_EXPORTER = 'none'; const sdk = new NodeSDK(); - await sdk.start(); + sdk.start(); const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; @@ -765,7 +774,7 @@ describe('setup exporter from env', () => { it('log warning that sdk will not be initalized when exporter is set to none', async () => { env.OTEL_TRACES_EXPORTER = 'none'; const sdk = new NodeSDK(); - await sdk.start(); + sdk.start(); assert.strictEqual( stubLoggerError.args[0][0], @@ -786,7 +795,7 @@ describe('setup exporter from env', () => { it('use default otlp exporter when empty value is provided for exporter via env', async () => { env.OTEL_TRACES_EXPORTER = ''; const sdk = new NodeSDK(); - await sdk.start(); + sdk.start(); const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; @@ -799,7 +808,7 @@ describe('setup exporter from env', () => { it('use only default exporter when none value is provided with other exporters', async () => { env.OTEL_TRACES_EXPORTER = 'otlp,zipkin,none'; const sdk = new NodeSDK(); - await sdk.start(); + sdk.start(); const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; @@ -812,7 +821,7 @@ describe('setup exporter from env', () => { it('log warning that only default exporter will be used since exporter list contains none with other exports ', async () => { env.OTEL_TRACES_EXPORTER = 'otlp,zipkin,none'; const sdk = new NodeSDK(); - await sdk.start(); + sdk.start(); assert.strictEqual( stubLoggerError.args[0][0], @@ -823,7 +832,7 @@ describe('setup exporter from env', () => { it('should warn that provided exporter value is unrecognized and not able to be set up', async () => { env.OTEL_TRACES_EXPORTER = 'invalid'; const sdk = new NodeSDK(); - await sdk.start(); + sdk.start(); assert.strictEqual( stubLoggerError.args[0][0], @@ -841,7 +850,7 @@ describe('setup exporter from env', () => { env.OTEL_TRACES_EXPORTER = 'zipkin, otlp, jaeger'; env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'grpc'; const sdk = new NodeSDK(); - await sdk.start(); + sdk.start(); const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; @@ -857,7 +866,7 @@ describe('setup exporter from env', () => { it('use the console exporter', async () => { env.OTEL_TRACES_EXPORTER = 'console, otlp'; const sdk = new NodeSDK(); - await sdk.start(); + sdk.start(); const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; diff --git a/experimental/packages/opentelemetry-sdk-node/test/util/resource-assertions.ts b/experimental/packages/opentelemetry-sdk-node/test/util/resource-assertions.ts index cd6b272433..bcbdceadc0 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/util/resource-assertions.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/util/resource-assertions.ts @@ -16,7 +16,7 @@ import { SDK_INFO } from '@opentelemetry/core'; import * as assert from 'assert'; -import { Resource } from '@opentelemetry/resources'; +import { IResource, Resource } from '@opentelemetry/resources'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; /** @@ -229,7 +229,7 @@ export const assertTelemetrySDKResource = ( * @param validations validations for the resource attributes */ export const assertServiceResource = ( - resource: Resource, + resource: IResource, validations: { name: string; instanceId?: string; diff --git a/experimental/packages/otlp-transformer/src/trace/index.ts b/experimental/packages/otlp-transformer/src/trace/index.ts index 8d20181221..ad06612a11 100644 --- a/experimental/packages/otlp-transformer/src/trace/index.ts +++ b/experimental/packages/otlp-transformer/src/trace/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { Resource } from '@opentelemetry/resources'; +import type { IResource } from '@opentelemetry/resources'; import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import { toAttributes } from '../common/internal'; import { sdkSpanToOtlpSpan } from './internal'; @@ -33,7 +33,7 @@ export function createExportTraceServiceRequest( } function createResourceMap(readableSpans: ReadableSpan[]) { - const resourceMap: Map> = new Map(); + const resourceMap: Map> = new Map(); for (const record of readableSpans) { let ilmMap = resourceMap.get(record.resource); diff --git a/packages/opentelemetry-exporter-zipkin/src/transform.ts b/packages/opentelemetry-exporter-zipkin/src/transform.ts index 9ffd48ecce..c3c1887616 100644 --- a/packages/opentelemetry-exporter-zipkin/src/transform.ts +++ b/packages/opentelemetry-exporter-zipkin/src/transform.ts @@ -18,7 +18,7 @@ import * as api from '@opentelemetry/api'; import { ReadableSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'; import { hrTimeToMicroseconds } from '@opentelemetry/core'; import * as zipkinTypes from './types'; -import { Resource } from '@opentelemetry/resources'; +import { IResource } from '@opentelemetry/resources'; const ZIPKIN_SPAN_KIND_MAPPING = { [api.SpanKind.CLIENT]: zipkinTypes.SpanKind.CLIENT, @@ -72,7 +72,7 @@ export function _toZipkinTags( status: api.SpanStatus, statusCodeTagName: string, statusErrorTagName: string, - resource: Resource + resource: IResource ): zipkinTypes.Tags { const tags: { [key: string]: string } = {}; for (const key of Object.keys(attributes)) { diff --git a/packages/opentelemetry-resources/package.json b/packages/opentelemetry-resources/package.json index 59c1054cb2..35dca15ecd 100644 --- a/packages/opentelemetry-resources/package.json +++ b/packages/opentelemetry-resources/package.json @@ -62,6 +62,7 @@ }, "devDependencies": { "@opentelemetry/api": ">=1.0.0 <1.5.0", + "@opentelemetry/resources_1.9.0": "npm:@opentelemetry/resources@1.9.0", "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", diff --git a/packages/opentelemetry-resources/src/IResource.ts b/packages/opentelemetry-resources/src/IResource.ts new file mode 100644 index 0000000000..b53a0e0244 --- /dev/null +++ b/packages/opentelemetry-resources/src/IResource.ts @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ResourceAttributes } from './types'; + +/** + * An interface that represents a resource. A Resource describes the entity for which signals (metrics or trace) are + * collected. + * + */ +export interface IResource { + /** + * Check if async attributes have resolved. This is useful to avoid awaiting + * waitForAsyncAttributes (which will introduce asynchronous behavior) when not necessary. + * + * @returns true if the resource "attributes" property is not yet settled to its final value + */ + asyncAttributesPending?: boolean; + + /** + * @returns the Resource's attributes. + */ + readonly attributes: ResourceAttributes; + + /** + * Returns a promise that will never be rejected. Resolves when all async attributes have finished being added to + * this Resource's attributes. This is useful in exporters to block until resource detection + * has finished. + */ + waitForAsyncAttributes?(): Promise; + + /** + * Returns a new, merged {@link Resource} by merging the current Resource + * with the other Resource. In case of a collision, other Resource takes + * precedence. + * + * @param other the Resource that will be merged with this. + * @returns the newly merged Resource. + */ + merge(other: IResource | null): IResource; +} diff --git a/packages/opentelemetry-resources/src/Resource.ts b/packages/opentelemetry-resources/src/Resource.ts index 0dc2072a2f..2ef82468de 100644 --- a/packages/opentelemetry-resources/src/Resource.ts +++ b/packages/opentelemetry-resources/src/Resource.ts @@ -14,29 +14,41 @@ * limitations under the License. */ +import { diag } from '@opentelemetry/api'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { SDK_INFO } from '@opentelemetry/core'; import { ResourceAttributes } from './types'; import { defaultServiceName } from './platform'; +import { IResource } from './IResource'; /** * A Resource describes the entity for which a signals (metrics or trace) are * collected. */ -export class Resource { +export class Resource implements IResource { static readonly EMPTY = new Resource({}); + private _syncAttributes: ResourceAttributes; + private _asyncAttributesPromise: Promise | undefined; + + /** + * Check if async attributes have resolved. This is useful to avoid awaiting + * waitForAsyncAttributes (which will introduce asynchronous behavior) when not necessary. + * + * @returns true if the resource "attributes" property is not yet settled to its final value + */ + public asyncAttributesPending: boolean; /** * Returns an empty Resource */ - static empty(): Resource { + static empty(): IResource { return Resource.EMPTY; } /** * Returns a Resource that identifies the SDK in use. */ - static default(): Resource { + static default(): IResource { return new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: defaultServiceName(), [SemanticResourceAttributes.TELEMETRY_SDK_LANGUAGE]: @@ -54,8 +66,45 @@ export class Resource { * information about the entity as numbers, strings or booleans * TODO: Consider to add check/validation on attributes. */ - readonly attributes: ResourceAttributes - ) {} + private _attributes: ResourceAttributes, + asyncAttributesPromise?: Promise + ) { + this.asyncAttributesPending = asyncAttributesPromise != null; + this._syncAttributes = _attributes; + this._asyncAttributesPromise = asyncAttributesPromise?.then( + asyncAttributes => { + this._attributes = Object.assign({}, this._attributes, asyncAttributes); + this.asyncAttributesPending = false; + return asyncAttributes; + }, + err => { + diag.debug("a resource's async attributes promise rejected: %s", err); + this.asyncAttributesPending = false; + return {}; + } + ); + } + + get attributes(): ResourceAttributes { + if (this.asyncAttributesPending) { + diag.error( + 'Accessing resource attributes before async attributes settled' + ); + } + + return this._attributes; + } + + /** + * Returns a promise that will never be rejected. Resolves when all async attributes have finished being added to + * this Resource's attributes. This is useful in exporters to block until resource detection + * has finished. + */ + async waitForAsyncAttributes(): Promise { + if (this.asyncAttributesPending) { + await this._asyncAttributesPromise; + } + } /** * Returns a new, merged {@link Resource} by merging the current Resource @@ -65,15 +114,36 @@ export class Resource { * @param other the Resource that will be merged with this. * @returns the newly merged Resource. */ - merge(other: Resource | null): Resource { - if (!other || !Object.keys(other.attributes).length) return this; + merge(other: IResource | null): IResource { + if (!other) return this; - // SpanAttributes from resource overwrite attributes from other resource. - const mergedAttributes = Object.assign( - {}, - this.attributes, - other.attributes - ); - return new Resource(mergedAttributes); + // SpanAttributes from other resource overwrite attributes from this resource. + const mergedSyncAttributes = { + ...this._syncAttributes, + //Support for old resource implementation where _syncAttributes is not defined + ...((other as Resource)._syncAttributes ?? other.attributes), + }; + + if ( + !this._asyncAttributesPromise && + !(other as Resource)._asyncAttributesPromise + ) { + return new Resource(mergedSyncAttributes); + } + + const mergedAttributesPromise = Promise.all([ + this._asyncAttributesPromise, + (other as Resource)._asyncAttributesPromise, + ]).then(([thisAsyncAttributes, otherAsyncAttributes]) => { + return { + ...this._syncAttributes, + ...thisAsyncAttributes, + //Support for old resource implementation where _syncAttributes is not defined + ...((other as Resource)._syncAttributes ?? other.attributes), + ...otherAsyncAttributes, + }; + }); + + return new Resource(mergedSyncAttributes, mergedAttributesPromise); } } diff --git a/packages/opentelemetry-resources/src/config.ts b/packages/opentelemetry-resources/src/config.ts index 915aad0764..239d596e6d 100644 --- a/packages/opentelemetry-resources/src/config.ts +++ b/packages/opentelemetry-resources/src/config.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import type { Detector } from './types'; +import type { Detector, DetectorSync } from './types'; /** * ResourceDetectionConfig provides an interface for configuring resource auto-detection. */ export interface ResourceDetectionConfig { - detectors?: Array; + detectors?: Array; } diff --git a/packages/opentelemetry-resources/src/detect-resources.ts b/packages/opentelemetry-resources/src/detect-resources.ts new file mode 100644 index 0000000000..be6943f81d --- /dev/null +++ b/packages/opentelemetry-resources/src/detect-resources.ts @@ -0,0 +1,126 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Resource } from './Resource'; +import { ResourceDetectionConfig } from './config'; +import { diag } from '@opentelemetry/api'; +import { isPromiseLike } from './utils'; +import { Detector, DetectorSync } from './types'; +import { IResource } from './IResource'; + +/** + * Runs all resource detectors and returns the results merged into a single Resource. Promise + * does not resolve until all the underlying detectors have resolved, unlike + * detectResourcesSync. + * + * @deprecated use detectResourceSync() instead. + * @param config Configuration for resource detection + */ +export const detectResources = async ( + config: ResourceDetectionConfig = {} +): Promise => { + const resources: IResource[] = await Promise.all( + (config.detectors || []).map(async d => { + try { + const resource = await d.detect(config); + diag.debug(`${d.constructor.name} found resource.`, resource); + return resource; + } catch (e) { + diag.debug(`${d.constructor.name} failed: ${e.message}`); + return Resource.empty(); + } + }) + ); + + // Future check if verbose logging is enabled issue #1903 + logResources(resources); + + return resources.reduce( + (acc, resource) => acc.merge(resource), + Resource.empty() + ); +}; + +/** + * Runs all resource detectors synchronously, merging their results. In case of attribute collision later resources will take precedence. + * + * @param config Configuration for resource detection + */ +export const detectResourcesSync = ( + config: ResourceDetectionConfig = {} +): IResource => { + const resources: IResource[] = (config.detectors ?? []).map( + (d: Detector | DetectorSync) => { + try { + const resourceOrPromise = d.detect(config); + let resource: IResource; + if (isPromiseLike(resourceOrPromise)) { + const createPromise = async () => { + const resolvedResource = await resourceOrPromise; + return resolvedResource.attributes; + }; + resource = new Resource({}, createPromise()); + } else { + resource = resourceOrPromise as IResource; + } + + if (resource.waitForAsyncAttributes) { + void resource + .waitForAsyncAttributes() + .then(() => + diag.debug(`${d.constructor.name} found resource.`, resource) + ); + } else { + diag.debug(`${d.constructor.name} found resource.`, resource); + } + + return resource; + } catch (e) { + diag.error(`${d.constructor.name} failed: ${e.message}`); + return Resource.empty(); + } + } + ); + + const mergedResources = resources.reduce( + (acc, resource) => acc.merge(resource), + Resource.empty() + ); + + if (mergedResources.waitForAsyncAttributes) { + void mergedResources.waitForAsyncAttributes().then(() => { + // Future check if verbose logging is enabled issue #1903 + logResources(resources); + }); + } + + return mergedResources; +}; + +/** + * Writes debug information about the detected resources to the logger defined in the resource detection config, if one is provided. + * + * @param resources The array of {@link Resource} that should be logged. Empty entries will be ignored. + */ +const logResources = (resources: Array) => { + resources.forEach(resource => { + // Print only populated resources + if (Object.keys(resource.attributes).length > 0) { + const resourceDebugString = JSON.stringify(resource.attributes, null, 4); + diag.verbose(resourceDebugString); + } + }); +}; diff --git a/packages/opentelemetry-resources/src/detectors/BrowserDetector.ts b/packages/opentelemetry-resources/src/detectors/BrowserDetector.ts index b9271d7e56..e50cd9b50b 100644 --- a/packages/opentelemetry-resources/src/detectors/BrowserDetector.ts +++ b/packages/opentelemetry-resources/src/detectors/BrowserDetector.ts @@ -14,50 +14,19 @@ * limitations under the License. */ -import { diag } from '@opentelemetry/api'; -import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; -import { Detector, Resource, ResourceDetectionConfig } from '..'; -import { ResourceAttributes } from '../types'; +import { + browserDetectorSync, + Detector, + IResource, + ResourceDetectionConfig, +} from '..'; /** * BrowserDetector will be used to detect the resources related to browser. */ class BrowserDetector implements Detector { - async detect(config?: ResourceDetectionConfig): Promise { - const isBrowser = typeof navigator !== 'undefined'; - if (!isBrowser) { - return Resource.empty(); - } - const browserResource: ResourceAttributes = { - [SemanticResourceAttributes.PROCESS_RUNTIME_NAME]: 'browser', - [SemanticResourceAttributes.PROCESS_RUNTIME_DESCRIPTION]: 'Web Browser', - [SemanticResourceAttributes.PROCESS_RUNTIME_VERSION]: navigator.userAgent, - }; - return this._getResourceAttributes(browserResource, config); - } - /** - * Validates process resource attribute map from process variables - * - * @param browserResource The un-sanitized resource attributes from process as key/value pairs. - * @param config: Config - * @returns The sanitized resource attributes. - */ - private _getResourceAttributes( - browserResource: ResourceAttributes, - _config?: ResourceDetectionConfig - ) { - if ( - browserResource[SemanticResourceAttributes.PROCESS_RUNTIME_VERSION] === '' - ) { - diag.debug( - 'BrowserDetector failed: Unable to find required browser resources. ' - ); - return Resource.empty(); - } else { - return new Resource({ - ...browserResource, - }); - } + detect(config?: ResourceDetectionConfig): Promise { + return Promise.resolve(browserDetectorSync.detect(config)); } } diff --git a/packages/opentelemetry-resources/src/detectors/BrowserDetectorSync.ts b/packages/opentelemetry-resources/src/detectors/BrowserDetectorSync.ts new file mode 100644 index 0000000000..b58fea94c8 --- /dev/null +++ b/packages/opentelemetry-resources/src/detectors/BrowserDetectorSync.ts @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; +import { DetectorSync, IResource, Resource, ResourceDetectionConfig } from '..'; +import { ResourceAttributes } from '../types'; +import { diag } from '@opentelemetry/api'; + +/** + * BrowserDetectorSync will be used to detect the resources related to browser. + */ +class BrowserDetectorSync implements DetectorSync { + detect(config?: ResourceDetectionConfig): IResource { + const isBrowser = typeof navigator !== 'undefined'; + if (!isBrowser) { + return Resource.empty(); + } + const browserResource: ResourceAttributes = { + [SemanticResourceAttributes.PROCESS_RUNTIME_NAME]: 'browser', + [SemanticResourceAttributes.PROCESS_RUNTIME_DESCRIPTION]: 'Web Browser', + [SemanticResourceAttributes.PROCESS_RUNTIME_VERSION]: navigator.userAgent, + }; + return this._getResourceAttributes(browserResource, config); + } + /** + * Validates process resource attribute map from process variables + * + * @param browserResource The un-sanitized resource attributes from process as key/value pairs. + * @param config: Config + * @returns The sanitized resource attributes. + */ + private _getResourceAttributes( + browserResource: ResourceAttributes, + _config?: ResourceDetectionConfig + ) { + if ( + browserResource[SemanticResourceAttributes.PROCESS_RUNTIME_VERSION] === '' + ) { + diag.debug( + 'BrowserDetector failed: Unable to find required browser resources. ' + ); + return Resource.empty(); + } else { + return new Resource({ + ...browserResource, + }); + } + } +} + +export const browserDetectorSync = new BrowserDetectorSync(); diff --git a/packages/opentelemetry-resources/src/detectors/EnvDetector.ts b/packages/opentelemetry-resources/src/detectors/EnvDetector.ts index 0f5cd65209..3467a8123f 100644 --- a/packages/opentelemetry-resources/src/detectors/EnvDetector.ts +++ b/packages/opentelemetry-resources/src/detectors/EnvDetector.ts @@ -14,37 +14,16 @@ * limitations under the License. */ -import { diag } from '@opentelemetry/api'; -import { getEnv } from '@opentelemetry/core'; -import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; -import { Resource } from '../Resource'; -import { Detector, ResourceAttributes } from '../types'; +import { Detector } from '../types'; import { ResourceDetectionConfig } from '../config'; +import { IResource } from '../IResource'; +import { envDetectorSync } from './EnvDetectorSync'; /** * EnvDetector can be used to detect the presence of and create a Resource * from the OTEL_RESOURCE_ATTRIBUTES environment variable. */ class EnvDetector implements Detector { - // Type, attribute keys, and attribute values should not exceed 256 characters. - private readonly _MAX_LENGTH = 255; - - // OTEL_RESOURCE_ATTRIBUTES is a comma-separated list of attributes. - private readonly _COMMA_SEPARATOR = ','; - - // OTEL_RESOURCE_ATTRIBUTES contains key value pair separated by '='. - private readonly _LABEL_KEY_VALUE_SPLITTER = '='; - - private readonly _ERROR_MESSAGE_INVALID_CHARS = - 'should be a ASCII string with a length greater than 0 and not exceed ' + - this._MAX_LENGTH + - ' characters.'; - - private readonly _ERROR_MESSAGE_INVALID_VALUE = - 'should be a ASCII string with a length not exceed ' + - this._MAX_LENGTH + - ' characters.'; - /** * Returns a {@link Resource} populated with attributes from the * OTEL_RESOURCE_ATTRIBUTES environment variable. Note this is an async @@ -52,107 +31,8 @@ class EnvDetector implements Detector { * * @param config The resource detection config */ - async detect(_config?: ResourceDetectionConfig): Promise { - const attributes: ResourceAttributes = {}; - const env = getEnv(); - - const rawAttributes = env.OTEL_RESOURCE_ATTRIBUTES; - const serviceName = env.OTEL_SERVICE_NAME; - - if (rawAttributes) { - try { - const parsedAttributes = this._parseResourceAttributes(rawAttributes); - Object.assign(attributes, parsedAttributes); - } catch (e) { - diag.debug(`EnvDetector failed: ${e.message}`); - } - } - - if (serviceName) { - attributes[SemanticResourceAttributes.SERVICE_NAME] = serviceName; - } - - return new Resource(attributes); - } - - /** - * Creates an attribute map from the OTEL_RESOURCE_ATTRIBUTES environment - * variable. - * - * OTEL_RESOURCE_ATTRIBUTES: A comma-separated list of attributes describing - * the source in more detail, e.g. “key1=val1,key2=val2”. Domain names and - * paths are accepted as attribute keys. Values may be quoted or unquoted in - * general. If a value contains whitespaces, =, or " characters, it must - * always be quoted. - * - * @param rawEnvAttributes The resource attributes as a comma-seperated list - * of key/value pairs. - * @returns The sanitized resource attributes. - */ - private _parseResourceAttributes( - rawEnvAttributes?: string - ): ResourceAttributes { - if (!rawEnvAttributes) return {}; - - const attributes: ResourceAttributes = {}; - const rawAttributes: string[] = rawEnvAttributes.split( - this._COMMA_SEPARATOR, - -1 - ); - for (const rawAttribute of rawAttributes) { - const keyValuePair: string[] = rawAttribute.split( - this._LABEL_KEY_VALUE_SPLITTER, - -1 - ); - if (keyValuePair.length !== 2) { - continue; - } - let [key, value] = keyValuePair; - // Leading and trailing whitespaces are trimmed. - key = key.trim(); - value = value.trim().split(/^"|"$/).join(''); - if (!this._isValidAndNotEmpty(key)) { - throw new Error(`Attribute key ${this._ERROR_MESSAGE_INVALID_CHARS}`); - } - if (!this._isValid(value)) { - throw new Error(`Attribute value ${this._ERROR_MESSAGE_INVALID_VALUE}`); - } - attributes[key] = decodeURIComponent(value); - } - return attributes; - } - - /** - * Determines whether the given String is a valid printable ASCII string with - * a length not exceed _MAX_LENGTH characters. - * - * @param str The String to be validated. - * @returns Whether the String is valid. - */ - private _isValid(name: string): boolean { - return name.length <= this._MAX_LENGTH && this._isBaggageOctetString(name); - } - - // https://www.w3.org/TR/baggage/#definition - private _isBaggageOctetString(str: string): boolean { - for (let i = 0; i < str.length; i++) { - const ch = str.charCodeAt(i); - if (ch < 0x21 || ch === 0x2c || ch === 0x3b || ch === 0x5c || ch > 0x7e) { - return false; - } - } - return true; - } - - /** - * Determines whether the given String is a valid printable ASCII string with - * a length greater than 0 and not exceed _MAX_LENGTH characters. - * - * @param str The String to be validated. - * @returns Whether the String is valid and not empty. - */ - private _isValidAndNotEmpty(str: string): boolean { - return str.length > 0 && this._isValid(str); + detect(config?: ResourceDetectionConfig): Promise { + return Promise.resolve(envDetectorSync.detect(config)); } } diff --git a/packages/opentelemetry-resources/src/detectors/EnvDetectorSync.ts b/packages/opentelemetry-resources/src/detectors/EnvDetectorSync.ts new file mode 100644 index 0000000000..1230657e19 --- /dev/null +++ b/packages/opentelemetry-resources/src/detectors/EnvDetectorSync.ts @@ -0,0 +1,160 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { diag } from '@opentelemetry/api'; +import { getEnv } from '@opentelemetry/core'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; +import { Resource } from '../Resource'; +import { DetectorSync, ResourceAttributes } from '../types'; +import { ResourceDetectionConfig } from '../config'; +import { IResource } from '../IResource'; + +/** + * EnvDetectorSync can be used to detect the presence of and create a Resource + * from the OTEL_RESOURCE_ATTRIBUTES environment variable. + */ +class EnvDetectorSync implements DetectorSync { + // Type, attribute keys, and attribute values should not exceed 256 characters. + private readonly _MAX_LENGTH = 255; + + // OTEL_RESOURCE_ATTRIBUTES is a comma-separated list of attributes. + private readonly _COMMA_SEPARATOR = ','; + + // OTEL_RESOURCE_ATTRIBUTES contains key value pair separated by '='. + private readonly _LABEL_KEY_VALUE_SPLITTER = '='; + + private readonly _ERROR_MESSAGE_INVALID_CHARS = + 'should be a ASCII string with a length greater than 0 and not exceed ' + + this._MAX_LENGTH + + ' characters.'; + + private readonly _ERROR_MESSAGE_INVALID_VALUE = + 'should be a ASCII string with a length not exceed ' + + this._MAX_LENGTH + + ' characters.'; + + /** + * Returns a {@link Resource} populated with attributes from the + * OTEL_RESOURCE_ATTRIBUTES environment variable. Note this is an async + * function to conform to the Detector interface. + * + * @param config The resource detection config + */ + detect(_config?: ResourceDetectionConfig): IResource { + const attributes: ResourceAttributes = {}; + const env = getEnv(); + + const rawAttributes = env.OTEL_RESOURCE_ATTRIBUTES; + const serviceName = env.OTEL_SERVICE_NAME; + + if (rawAttributes) { + try { + const parsedAttributes = this._parseResourceAttributes(rawAttributes); + Object.assign(attributes, parsedAttributes); + } catch (e) { + diag.debug(`EnvDetector failed: ${e.message}`); + } + } + + if (serviceName) { + attributes[SemanticResourceAttributes.SERVICE_NAME] = serviceName; + } + + return new Resource(attributes); + } + + /** + * Creates an attribute map from the OTEL_RESOURCE_ATTRIBUTES environment + * variable. + * + * OTEL_RESOURCE_ATTRIBUTES: A comma-separated list of attributes describing + * the source in more detail, e.g. “key1=val1,key2=val2”. Domain names and + * paths are accepted as attribute keys. Values may be quoted or unquoted in + * general. If a value contains whitespaces, =, or " characters, it must + * always be quoted. + * + * @param rawEnvAttributes The resource attributes as a comma-seperated list + * of key/value pairs. + * @returns The sanitized resource attributes. + */ + private _parseResourceAttributes( + rawEnvAttributes?: string + ): ResourceAttributes { + if (!rawEnvAttributes) return {}; + + const attributes: ResourceAttributes = {}; + const rawAttributes: string[] = rawEnvAttributes.split( + this._COMMA_SEPARATOR, + -1 + ); + for (const rawAttribute of rawAttributes) { + const keyValuePair: string[] = rawAttribute.split( + this._LABEL_KEY_VALUE_SPLITTER, + -1 + ); + if (keyValuePair.length !== 2) { + continue; + } + let [key, value] = keyValuePair; + // Leading and trailing whitespaces are trimmed. + key = key.trim(); + value = value.trim().split(/^"|"$/).join(''); + if (!this._isValidAndNotEmpty(key)) { + throw new Error(`Attribute key ${this._ERROR_MESSAGE_INVALID_CHARS}`); + } + if (!this._isValid(value)) { + throw new Error(`Attribute value ${this._ERROR_MESSAGE_INVALID_VALUE}`); + } + attributes[key] = decodeURIComponent(value); + } + return attributes; + } + + /** + * Determines whether the given String is a valid printable ASCII string with + * a length not exceed _MAX_LENGTH characters. + * + * @param str The String to be validated. + * @returns Whether the String is valid. + */ + private _isValid(name: string): boolean { + return name.length <= this._MAX_LENGTH && this._isBaggageOctetString(name); + } + + // https://www.w3.org/TR/baggage/#definition + private _isBaggageOctetString(str: string): boolean { + for (let i = 0; i < str.length; i++) { + const ch = str.charCodeAt(i); + if (ch < 0x21 || ch === 0x2c || ch === 0x3b || ch === 0x5c || ch > 0x7e) { + return false; + } + } + return true; + } + + /** + * Determines whether the given String is a valid printable ASCII string with + * a length greater than 0 and not exceed _MAX_LENGTH characters. + * + * @param str The String to be validated. + * @returns Whether the String is valid and not empty. + */ + private _isValidAndNotEmpty(str: string): boolean { + return str.length > 0 && this._isValid(str); + } +} + +export const envDetectorSync = new EnvDetectorSync(); diff --git a/packages/opentelemetry-resources/src/detectors/NoopDetector.ts b/packages/opentelemetry-resources/src/detectors/NoopDetector.ts index 61cf954bf7..463d8c629f 100644 --- a/packages/opentelemetry-resources/src/detectors/NoopDetector.ts +++ b/packages/opentelemetry-resources/src/detectors/NoopDetector.ts @@ -14,12 +14,13 @@ * limitations under the License. */ -import { Resource } from '../Resource'; import { Detector } from '../types'; +import { IResource } from '../IResource'; +import { noopDetectorSync } from './NoopDetectorSync'; export class NoopDetector implements Detector { - async detect(): Promise { - return new Resource({}); + detect(): Promise { + return Promise.resolve(noopDetectorSync.detect()); } } diff --git a/packages/opentelemetry-resources/src/detectors/NoopDetectorSync.ts b/packages/opentelemetry-resources/src/detectors/NoopDetectorSync.ts new file mode 100644 index 0000000000..a52473b9c3 --- /dev/null +++ b/packages/opentelemetry-resources/src/detectors/NoopDetectorSync.ts @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Resource } from '../Resource'; +import { DetectorSync } from '../types'; +import { IResource } from '../IResource'; + +export class NoopDetectorSync implements DetectorSync { + detect(): IResource { + return new Resource({}); + } +} + +export const noopDetectorSync = new NoopDetectorSync(); diff --git a/packages/opentelemetry-resources/src/detectors/ProcessDetector.ts b/packages/opentelemetry-resources/src/detectors/ProcessDetector.ts index 304a67f358..b1165f2c5b 100644 --- a/packages/opentelemetry-resources/src/detectors/ProcessDetector.ts +++ b/packages/opentelemetry-resources/src/detectors/ProcessDetector.ts @@ -14,64 +14,18 @@ * limitations under the License. */ -import { diag } from '@opentelemetry/api'; -import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; -import { Resource } from '../Resource'; -import { Detector, ResourceAttributes } from '../types'; +import { Detector } from '../types'; import { ResourceDetectionConfig } from '../config'; +import { IResource } from '../IResource'; +import { processDetectorSync } from './ProcessDetectorSync'; /** * ProcessDetector will be used to detect the resources related current process running * and being instrumented from the NodeJS Process module. */ class ProcessDetector implements Detector { - async detect(config?: ResourceDetectionConfig): Promise { - // Skip if not in Node.js environment. - if (typeof process !== 'object') { - return Resource.empty(); - } - const processResource: ResourceAttributes = { - [SemanticResourceAttributes.PROCESS_PID]: process.pid, - [SemanticResourceAttributes.PROCESS_EXECUTABLE_NAME]: process.title || '', - [SemanticResourceAttributes.PROCESS_COMMAND]: process.argv[1] || '', - [SemanticResourceAttributes.PROCESS_COMMAND_LINE]: - process.argv.join(' ') || '', - [SemanticResourceAttributes.PROCESS_RUNTIME_VERSION]: - process.versions.node, - [SemanticResourceAttributes.PROCESS_RUNTIME_NAME]: 'nodejs', - [SemanticResourceAttributes.PROCESS_RUNTIME_DESCRIPTION]: 'Node.js', - }; - return this._getResourceAttributes(processResource, config); - } - /** - * Validates process resource attribute map from process varaibls - * - * @param processResource The unsantized resource attributes from process as key/value pairs. - * @param config: Config - * @returns The sanitized resource attributes. - */ - private _getResourceAttributes( - processResource: ResourceAttributes, - _config?: ResourceDetectionConfig - ) { - if ( - processResource[SemanticResourceAttributes.PROCESS_EXECUTABLE_NAME] === - '' || - processResource[SemanticResourceAttributes.PROCESS_EXECUTABLE_PATH] === - '' || - processResource[SemanticResourceAttributes.PROCESS_COMMAND] === '' || - processResource[SemanticResourceAttributes.PROCESS_COMMAND_LINE] === '' || - processResource[SemanticResourceAttributes.PROCESS_RUNTIME_VERSION] === '' - ) { - diag.debug( - 'ProcessDetector failed: Unable to find required process resources. ' - ); - return Resource.empty(); - } else { - return new Resource({ - ...processResource, - }); - } + detect(config?: ResourceDetectionConfig): Promise { + return Promise.resolve(processDetectorSync.detect(config)); } } diff --git a/packages/opentelemetry-resources/src/detectors/ProcessDetectorSync.ts b/packages/opentelemetry-resources/src/detectors/ProcessDetectorSync.ts new file mode 100644 index 0000000000..d63b856e3e --- /dev/null +++ b/packages/opentelemetry-resources/src/detectors/ProcessDetectorSync.ts @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { diag } from '@opentelemetry/api'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; +import { Resource } from '../Resource'; +import { DetectorSync, ResourceAttributes } from '../types'; +import { ResourceDetectionConfig } from '../config'; +import { IResource } from '../IResource'; + +/** + * ProcessDetectorSync will be used to detect the resources related current process running + * and being instrumented from the NodeJS Process module. + */ +class ProcessDetectorSync implements DetectorSync { + detect(config?: ResourceDetectionConfig): IResource { + // Skip if not in Node.js environment. + if (typeof process !== 'object') { + return Resource.empty(); + } + const processResource: ResourceAttributes = { + [SemanticResourceAttributes.PROCESS_PID]: process.pid, + [SemanticResourceAttributes.PROCESS_EXECUTABLE_NAME]: process.title || '', + [SemanticResourceAttributes.PROCESS_COMMAND]: process.argv[1] || '', + [SemanticResourceAttributes.PROCESS_COMMAND_LINE]: + process.argv.join(' ') || '', + [SemanticResourceAttributes.PROCESS_RUNTIME_VERSION]: + process.versions.node, + [SemanticResourceAttributes.PROCESS_RUNTIME_NAME]: 'nodejs', + [SemanticResourceAttributes.PROCESS_RUNTIME_DESCRIPTION]: 'Node.js', + }; + return this._getResourceAttributes(processResource, config); + } + /** + * Validates process resource attribute map from process variables + * + * @param processResource The unsantized resource attributes from process as key/value pairs. + * @param config: Config + * @returns The sanitized resource attributes. + */ + private _getResourceAttributes( + processResource: ResourceAttributes, + _config?: ResourceDetectionConfig + ) { + if ( + processResource[SemanticResourceAttributes.PROCESS_EXECUTABLE_NAME] === + '' || + processResource[SemanticResourceAttributes.PROCESS_EXECUTABLE_PATH] === + '' || + processResource[SemanticResourceAttributes.PROCESS_COMMAND] === '' || + processResource[SemanticResourceAttributes.PROCESS_COMMAND_LINE] === '' || + processResource[SemanticResourceAttributes.PROCESS_RUNTIME_VERSION] === '' + ) { + diag.debug( + 'ProcessDetector failed: Unable to find required process resources. ' + ); + return Resource.empty(); + } else { + return new Resource({ + ...processResource, + }); + } + } +} + +export const processDetectorSync = new ProcessDetectorSync(); diff --git a/packages/opentelemetry-resources/src/detectors/index.ts b/packages/opentelemetry-resources/src/detectors/index.ts index 2a16563da5..5ed2b3f868 100644 --- a/packages/opentelemetry-resources/src/detectors/index.ts +++ b/packages/opentelemetry-resources/src/detectors/index.ts @@ -17,3 +17,6 @@ export * from './BrowserDetector'; export * from './EnvDetector'; export * from './ProcessDetector'; +export * from './BrowserDetectorSync'; +export * from './EnvDetectorSync'; +export * from './ProcessDetectorSync'; diff --git a/packages/opentelemetry-resources/src/index.ts b/packages/opentelemetry-resources/src/index.ts index 4cff4181e3..3f66901fbb 100644 --- a/packages/opentelemetry-resources/src/index.ts +++ b/packages/opentelemetry-resources/src/index.ts @@ -15,7 +15,9 @@ */ export * from './Resource'; +export * from './IResource'; export * from './platform'; export * from './types'; export * from './config'; export * from './detectors'; +export * from './detect-resources'; diff --git a/packages/opentelemetry-resources/src/platform/browser/HostDetectorSync.ts b/packages/opentelemetry-resources/src/platform/browser/HostDetectorSync.ts new file mode 100644 index 0000000000..1962f53028 --- /dev/null +++ b/packages/opentelemetry-resources/src/platform/browser/HostDetectorSync.ts @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { noopDetectorSync } from '../../detectors/NoopDetectorSync'; + +export const hostDetectorSync = noopDetectorSync; diff --git a/packages/opentelemetry-resources/src/platform/browser/OSDetectorSync.ts b/packages/opentelemetry-resources/src/platform/browser/OSDetectorSync.ts new file mode 100644 index 0000000000..416fc3b4ee --- /dev/null +++ b/packages/opentelemetry-resources/src/platform/browser/OSDetectorSync.ts @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { noopDetectorSync } from '../../detectors/NoopDetectorSync'; + +export const osDetectorSync = noopDetectorSync; diff --git a/packages/opentelemetry-resources/src/platform/browser/detect-resources.ts b/packages/opentelemetry-resources/src/platform/browser/detect-resources.ts deleted file mode 100644 index 457965fadf..0000000000 --- a/packages/opentelemetry-resources/src/platform/browser/detect-resources.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Resource } from '../../Resource'; -import { ResourceDetectionConfig } from '../../config'; -import { diag } from '@opentelemetry/api'; - -/** - * Runs all resource detectors and returns the results merged into a single - * Resource. - * - * @param config Configuration for resource detection - */ -export const detectResources = async ( - config: ResourceDetectionConfig = {} -): Promise => { - const internalConfig: ResourceDetectionConfig = Object.assign(config); - - const resources: Resource[] = await Promise.all( - (internalConfig.detectors || []).map(async d => { - try { - const resource = await d.detect(internalConfig); - diag.debug(`${d.constructor.name} found resource.`, resource); - return resource; - } catch (e) { - diag.debug(`${d.constructor.name} failed: ${e.message}`); - return Resource.empty(); - } - }) - ); - - return resources.reduce( - (acc, resource) => acc.merge(resource), - Resource.empty() - ); -}; diff --git a/packages/opentelemetry-resources/src/platform/browser/index.ts b/packages/opentelemetry-resources/src/platform/browser/index.ts index 6f3af40a45..b18be97c9c 100644 --- a/packages/opentelemetry-resources/src/platform/browser/index.ts +++ b/packages/opentelemetry-resources/src/platform/browser/index.ts @@ -15,6 +15,7 @@ */ export * from './default-service-name'; -export * from './detect-resources'; export * from './HostDetector'; export * from './OSDetector'; +export * from './HostDetectorSync'; +export * from './OSDetectorSync'; diff --git a/packages/opentelemetry-resources/src/platform/node/HostDetector.ts b/packages/opentelemetry-resources/src/platform/node/HostDetector.ts index c27766bb92..daf0b8e045 100644 --- a/packages/opentelemetry-resources/src/platform/node/HostDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/HostDetector.ts @@ -14,38 +14,18 @@ * limitations under the License. */ -import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; -import { Resource } from '../../Resource'; -import { Detector, ResourceAttributes } from '../../types'; +import { Detector } from '../../types'; import { ResourceDetectionConfig } from '../../config'; -import { arch, hostname } from 'os'; +import { IResource } from '../../IResource'; +import { hostDetectorSync } from './HostDetectorSync'; /** * HostDetector detects the resources related to the host current process is * running on. Currently only non-cloud-based attributes are included. */ class HostDetector implements Detector { - async detect(_config?: ResourceDetectionConfig): Promise { - const attributes: ResourceAttributes = { - [SemanticResourceAttributes.HOST_NAME]: hostname(), - [SemanticResourceAttributes.HOST_ARCH]: this._normalizeArch(arch()), - }; - return new Resource(attributes); - } - - private _normalizeArch(nodeArchString: string): string { - // Maps from https://nodejs.org/api/os.html#osarch to arch values in spec: - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/host.md - switch (nodeArchString) { - case 'arm': - return 'arm32'; - case 'ppc': - return 'ppc32'; - case 'x64': - return 'amd64'; - default: - return nodeArchString; - } + detect(_config?: ResourceDetectionConfig): Promise { + return Promise.resolve(hostDetectorSync.detect(_config)); } } diff --git a/packages/opentelemetry-resources/src/platform/node/HostDetectorSync.ts b/packages/opentelemetry-resources/src/platform/node/HostDetectorSync.ts new file mode 100644 index 0000000000..85bd717e54 --- /dev/null +++ b/packages/opentelemetry-resources/src/platform/node/HostDetectorSync.ts @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; +import { Resource } from '../../Resource'; +import { DetectorSync, ResourceAttributes } from '../../types'; +import { ResourceDetectionConfig } from '../../config'; +import { arch, hostname } from 'os'; +import { normalizeArch } from './utils'; + +/** + * HostDetectorSync detects the resources related to the host current process is + * running on. Currently only non-cloud-based attributes are included. + */ +class HostDetectorSync implements DetectorSync { + detect(_config?: ResourceDetectionConfig): Resource { + const attributes: ResourceAttributes = { + [SemanticResourceAttributes.HOST_NAME]: hostname(), + [SemanticResourceAttributes.HOST_ARCH]: normalizeArch(arch()), + }; + return new Resource(attributes); + } +} + +export const hostDetectorSync = new HostDetectorSync(); diff --git a/packages/opentelemetry-resources/src/platform/node/OSDetector.ts b/packages/opentelemetry-resources/src/platform/node/OSDetector.ts index befa9fb1f6..7ee528d497 100644 --- a/packages/opentelemetry-resources/src/platform/node/OSDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/OSDetector.ts @@ -14,36 +14,18 @@ * limitations under the License. */ -import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; -import { Resource } from '../../Resource'; -import { Detector, ResourceAttributes } from '../../types'; +import { Detector } from '../../types'; import { ResourceDetectionConfig } from '../../config'; -import { platform, release } from 'os'; +import { IResource } from '../../IResource'; +import { osDetectorSync } from './OSDetectorSync'; /** * OSDetector detects the resources related to the operating system (OS) on * which the process represented by this resource is running. */ class OSDetector implements Detector { - async detect(_config?: ResourceDetectionConfig): Promise { - const attributes: ResourceAttributes = { - [SemanticResourceAttributes.OS_TYPE]: this._normalizeType(platform()), - [SemanticResourceAttributes.OS_VERSION]: release(), - }; - return new Resource(attributes); - } - - private _normalizeType(nodePlatform: string): string { - // Maps from https://nodejs.org/api/os.html#osplatform to arch values in spec: - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/os.md - switch (nodePlatform) { - case 'sunos': - return 'solaris'; - case 'win32': - return 'windows'; - default: - return nodePlatform; - } + detect(_config?: ResourceDetectionConfig): Promise { + return Promise.resolve(osDetectorSync.detect(_config)); } } diff --git a/packages/opentelemetry-resources/src/platform/node/OSDetectorSync.ts b/packages/opentelemetry-resources/src/platform/node/OSDetectorSync.ts new file mode 100644 index 0000000000..9cb6a0385d --- /dev/null +++ b/packages/opentelemetry-resources/src/platform/node/OSDetectorSync.ts @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; +import { Resource } from '../../Resource'; +import { DetectorSync, ResourceAttributes } from '../../types'; +import { ResourceDetectionConfig } from '../../config'; +import { platform, release } from 'os'; +import { normalizeType } from './utils'; + +/** + * OSDetectorSync detects the resources related to the operating system (OS) on + * which the process represented by this resource is running. + */ +class OSDetectorSync implements DetectorSync { + detect(_config?: ResourceDetectionConfig): Resource { + const attributes: ResourceAttributes = { + [SemanticResourceAttributes.OS_TYPE]: normalizeType(platform()), + [SemanticResourceAttributes.OS_VERSION]: release(), + }; + return new Resource(attributes); + } +} + +export const osDetectorSync = new OSDetectorSync(); diff --git a/packages/opentelemetry-resources/src/platform/node/detect-resources.ts b/packages/opentelemetry-resources/src/platform/node/detect-resources.ts deleted file mode 100644 index abe1584dc7..0000000000 --- a/packages/opentelemetry-resources/src/platform/node/detect-resources.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Resource } from '../../Resource'; -import { ResourceDetectionConfig } from '../../config'; -import { diag } from '@opentelemetry/api'; -import * as util from 'util'; - -/** - * Runs all resource detectors and returns the results merged into a single - * Resource. - * - * @param config Configuration for resource detection - */ -export const detectResources = async ( - config: ResourceDetectionConfig = {} -): Promise => { - const internalConfig: ResourceDetectionConfig = Object.assign(config); - - const resources: Resource[] = await Promise.all( - (internalConfig.detectors || []).map(async d => { - try { - const resource = await d.detect(internalConfig); - diag.debug(`${d.constructor.name} found resource.`, resource); - return resource; - } catch (e) { - diag.debug(`${d.constructor.name} failed: ${e.message}`); - return Resource.empty(); - } - }) - ); - - // Future check if verbose logging is enabled issue #1903 - logResources(resources); - - return resources.reduce( - (acc, resource) => acc.merge(resource), - Resource.empty() - ); -}; - -/** - * Writes debug information about the detected resources to the logger defined in the resource detection config, if one is provided. - * - * @param resources The array of {@link Resource} that should be logged. Empty entries will be ignored. - */ -const logResources = (resources: Array) => { - resources.forEach(resource => { - // Print only populated resources - if (Object.keys(resource.attributes).length > 0) { - const resourceDebugString = util.inspect(resource.attributes, { - depth: 2, - breakLength: Infinity, - sorted: true, - compact: false, - }); - diag.verbose(resourceDebugString); - } - }); -}; diff --git a/packages/opentelemetry-resources/src/platform/node/index.ts b/packages/opentelemetry-resources/src/platform/node/index.ts index 6f3af40a45..b18be97c9c 100644 --- a/packages/opentelemetry-resources/src/platform/node/index.ts +++ b/packages/opentelemetry-resources/src/platform/node/index.ts @@ -15,6 +15,7 @@ */ export * from './default-service-name'; -export * from './detect-resources'; export * from './HostDetector'; export * from './OSDetector'; +export * from './HostDetectorSync'; +export * from './OSDetectorSync'; diff --git a/packages/opentelemetry-resources/src/platform/node/utils.ts b/packages/opentelemetry-resources/src/platform/node/utils.ts new file mode 100644 index 0000000000..52f01eaa00 --- /dev/null +++ b/packages/opentelemetry-resources/src/platform/node/utils.ts @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export const normalizeArch = (nodeArchString: string): string => { + // Maps from https://nodejs.org/api/os.html#osarch to arch values in spec: + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/host.md + switch (nodeArchString) { + case 'arm': + return 'arm32'; + case 'ppc': + return 'ppc32'; + case 'x64': + return 'amd64'; + default: + return nodeArchString; + } +}; + +export const normalizeType = (nodePlatform: string): string => { + // Maps from https://nodejs.org/api/os.html#osplatform to arch values in spec: + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/os.md + switch (nodePlatform) { + case 'sunos': + return 'solaris'; + case 'win32': + return 'windows'; + default: + return nodePlatform; + } +}; diff --git a/packages/opentelemetry-resources/src/types.ts b/packages/opentelemetry-resources/src/types.ts index 717f71381d..d20c09faa2 100644 --- a/packages/opentelemetry-resources/src/types.ts +++ b/packages/opentelemetry-resources/src/types.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { Resource } from './Resource'; import { ResourceDetectionConfig } from './config'; import { SpanAttributes } from '@opentelemetry/api'; +import { IResource } from './IResource'; /** * Interface for Resource attributes. @@ -26,9 +26,16 @@ import { SpanAttributes } from '@opentelemetry/api'; export type ResourceAttributes = SpanAttributes; /** - * Interface for a Resource Detector. In order to detect resources in parallel - * a detector returns a Promise containing a Resource. + * @deprecated please use {@link DetectorSync} */ export interface Detector { - detect(config?: ResourceDetectionConfig): Promise; + detect(config?: ResourceDetectionConfig): Promise; +} + +/** + * Interface for a synchronous Resource Detector. In order to detect attributes asynchronously, a detector + * can pass a Promise as the second parameter to the Resource constructor. + */ +export interface DetectorSync { + detect(config?: ResourceDetectionConfig): IResource; } diff --git a/packages/opentelemetry-resources/src/utils.ts b/packages/opentelemetry-resources/src/utils.ts new file mode 100644 index 0000000000..73d81040b3 --- /dev/null +++ b/packages/opentelemetry-resources/src/utils.ts @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const isPromiseLike = (val: any): val is PromiseLike => { + return ( + val !== null && typeof val === 'object' && typeof val.then === 'function' + ); +}; diff --git a/packages/opentelemetry-resources/test/Resource.test.ts b/packages/opentelemetry-resources/test/Resource.test.ts index 7d4bcae248..df9430af75 100644 --- a/packages/opentelemetry-resources/test/Resource.test.ts +++ b/packages/opentelemetry-resources/test/Resource.test.ts @@ -14,11 +14,14 @@ * limitations under the License. */ +import * as sinon from 'sinon'; import * as assert from 'assert'; import { SDK_INFO } from '@opentelemetry/core'; -import { Resource } from '../src'; +import { Resource, ResourceAttributes } from '../src'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { describeBrowser, describeNode } from './util'; +import { diag } from '@opentelemetry/api'; +import { Resource as Resource190 } from '@opentelemetry/resources_1.9.0'; describe('Resource', () => { const resource1 = new Resource({ @@ -90,6 +93,24 @@ describe('Resource', () => { assert.strictEqual(resource.attributes['custom.boolean'], true); }); + it('should log when accessing attributes before async attributes promise has settled', () => { + const debugStub = sinon.spy(diag, 'error'); + const resource = new Resource( + {}, + new Promise(resolve => { + setTimeout(resolve, 1); + }) + ); + + resource.attributes; + + assert.ok( + debugStub.calledWithMatch( + 'Accessing resource attributes before async attributes settled' + ) + ); + }); + describe('.empty()', () => { it('should return an empty resource (except required service name)', () => { const resource = Resource.empty(); @@ -99,6 +120,160 @@ describe('Resource', () => { it('should return the same empty resource', () => { assert.strictEqual(Resource.empty(), Resource.empty()); }); + + it('should return false for asyncAttributesPending immediately', () => { + assert.ok(!Resource.empty().asyncAttributesPending); + }); + }); + + describe('asynchronous attributes', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should return false for asyncAttributesPending if no promise provided', () => { + assert.ok(!new Resource({ foo: 'bar' }).asyncAttributesPending); + assert.ok(!Resource.empty().asyncAttributesPending); + assert.ok(!Resource.default().asyncAttributesPending); + }); + + it('should return false for asyncAttributesPending once promise settles', async () => { + const clock = sinon.useFakeTimers(); + const resourceResolve = new Resource( + {}, + new Promise(resolve => { + setTimeout(resolve, 1); + }) + ); + const resourceReject = new Resource( + {}, + new Promise((_, reject) => { + setTimeout(reject, 1); + }) + ); + + for (const resource of [resourceResolve, resourceReject]) { + assert.ok(resource.asyncAttributesPending); + await clock.nextAsync(); + await resource.waitForAsyncAttributes(); + assert.ok(!resource.asyncAttributesPending); + } + }); + + it('should merge async attributes into sync attributes once resolved', async () => { + //async attributes that resolve after 1 ms + const asyncAttributes = new Promise(resolve => { + setTimeout( + () => resolve({ async: 'fromasync', shared: 'fromasync' }), + 1 + ); + }); + + const resource = new Resource( + { sync: 'fromsync', shared: 'fromsync' }, + asyncAttributes + ); + + await resource.waitForAsyncAttributes(); + assert.deepStrictEqual(resource.attributes, { + sync: 'fromsync', + // async takes precedence + shared: 'fromasync', + async: 'fromasync', + }); + }); + + it('should merge async attributes when both resources have promises', async () => { + const resource1 = new Resource( + {}, + Promise.resolve({ promise1: 'promise1val', shared: 'promise1val' }) + ); + const resource2 = new Resource( + {}, + Promise.resolve({ promise2: 'promise2val', shared: 'promise2val' }) + ); + // this one rejects + const resource3 = new Resource({}, Promise.reject(new Error('reject'))); + const resource4 = new Resource( + {}, + Promise.resolve({ promise4: 'promise4val', shared: 'promise4val' }) + ); + + const merged = resource1 + .merge(resource2) + .merge(resource3) + .merge(resource4); + + await merged.waitForAsyncAttributes?.(); + + assert.deepStrictEqual(merged.attributes, { + promise1: 'promise1val', + promise2: 'promise2val', + promise4: 'promise4val', + shared: 'promise4val', + }); + }); + + it('should merge async attributes correctly when resource1 fulfils after resource2', async () => { + const resource1 = new Resource( + {}, + Promise.resolve({ promise1: 'promise1val', shared: 'promise1val' }) + ); + + const resource2 = new Resource({ + promise2: 'promise2val', + shared: 'promise2val', + }); + + const merged = resource1.merge(resource2); + + await merged.waitForAsyncAttributes?.(); + + assert.deepStrictEqual(merged.attributes, { + promise1: 'promise1val', + promise2: 'promise2val', + shared: 'promise2val', + }); + }); + + it('should merge async attributes correctly when resource2 fulfils after resource1', async () => { + const resource1 = new Resource( + { shared: 'promise1val' }, + Promise.resolve({ promise1: 'promise1val' }) + ); + + //async attributes that resolve after 1 ms + const asyncAttributes = new Promise(resolve => { + setTimeout( + () => resolve({ promise2: 'promise2val', shared: 'promise2val' }), + 1 + ); + }); + const resource2 = new Resource({}, asyncAttributes); + + const merged = resource1.merge(resource2); + + await merged.waitForAsyncAttributes?.(); + + assert.deepStrictEqual(merged.attributes, { + promise1: 'promise1val', + promise2: 'promise2val', + shared: 'promise2val', + }); + }); + + it('should log when promise rejects', async () => { + const debugStub = sinon.spy(diag, 'debug'); + + const resource = new Resource({}, Promise.reject(new Error('rejected'))); + await resource.waitForAsyncAttributes(); + + assert.ok( + debugStub.calledWithMatch( + "a resource's async attributes promise rejected" + ) + ); + }); }); describeNode('.default()', () => { @@ -144,4 +319,40 @@ describe('Resource', () => { ); }); }); + + describe('compatibility', () => { + it('should merge resource with old implementation', () => { + const resource = Resource.EMPTY; + const oldResource = new Resource190({ fromold: 'fromold' }); + + //TODO: find a solution for ts-ignore + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const mergedResource = resource.merge(oldResource); + + assert.strictEqual(mergedResource.attributes['fromold'], 'fromold'); + }); + + it('should merge resource containing async attributes with old implementation', async () => { + const resource = new Resource( + {}, + Promise.resolve({ fromnew: 'fromnew' }) + ); + + const oldResource = new Resource190({ fromold: 'fromold' }); + + //TODO: find a solution for ts-ignore + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const mergedResource = resource.merge(oldResource); + + assert.strictEqual(mergedResource.attributes['fromold'], 'fromold'); + + await mergedResource.waitForAsyncAttributes?.(); + + assert.strictEqual(mergedResource.attributes['fromnew'], 'fromnew'); + }); + }); }); diff --git a/packages/opentelemetry-resources/test/detect-resources.test.ts b/packages/opentelemetry-resources/test/detect-resources.test.ts new file mode 100644 index 0000000000..0db97057db --- /dev/null +++ b/packages/opentelemetry-resources/test/detect-resources.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { diag } from '@opentelemetry/api'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { Resource, Detector, detectResourcesSync, DetectorSync } from '../src'; +import { describeNode } from './util'; + +describe('detectResourcesSync', () => { + afterEach(() => { + sinon.restore(); + }); + + it('handles resource detectors which return Promise', async () => { + const detector: Detector = { + async detect() { + return new Resource({ sync: 'fromsync' }); + }, + }; + const resource = detectResourcesSync({ + detectors: [detector], + }); + + await resource.waitForAsyncAttributes?.(); + assert.deepStrictEqual(resource.attributes, { + sync: 'fromsync', + }); + }); + + it('handles resource detectors which return Resource with a promise inside', async () => { + const detector: DetectorSync = { + detect() { + return new Resource( + { sync: 'fromsync' }, + Promise.resolve({ async: 'fromasync' }) + ); + }, + }; + const resource = detectResourcesSync({ + detectors: [detector], + }); + + // before waiting, it should already have the sync resources + assert.deepStrictEqual(resource.attributes, { sync: 'fromsync' }); + await resource.waitForAsyncAttributes?.(); + assert.deepStrictEqual(resource.attributes, { + sync: 'fromsync', + async: 'fromasync', + }); + }); + + describeNode('logging', () => { + it("logs when a detector's async attributes promise rejects", async () => { + const debugStub = sinon.spy(diag, 'debug'); + + // use a class so it has a name + class DetectorRejects implements DetectorSync { + detect() { + return new Resource( + { sync: 'fromsync' }, + Promise.reject(new Error('reject')) + ); + } + } + class DetectorOk implements DetectorSync { + detect() { + return new Resource( + { sync: 'fromsync' }, + Promise.resolve({ async: 'fromasync' }) + ); + } + } + + const resource = detectResourcesSync({ + detectors: [new DetectorRejects(), new DetectorOk()], + }); + + await resource.waitForAsyncAttributes?.(); + + assert.ok( + debugStub.calledWithMatch( + "a resource's async attributes promise rejected" + ) + ); + }); + }); +}); diff --git a/packages/opentelemetry-resources/test/detectors/browser/BrowserDetector.test.ts b/packages/opentelemetry-resources/test/detectors/browser/BrowserDetector.test.ts index ee6a92ccce..95c1b95b44 100644 --- a/packages/opentelemetry-resources/test/detectors/browser/BrowserDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/browser/BrowserDetector.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import * as sinon from 'sinon'; -import { Resource } from '../../../src'; +import { IResource } from '../../../src'; import { browserDetector } from '../../../src/detectors/BrowserDetector'; import { describeBrowser } from '../../util'; import { @@ -32,7 +32,7 @@ describeBrowser('browserDetector()', () => { userAgent: 'dddd', }); - const resource: Resource = await browserDetector.detect(); + const resource: IResource = await browserDetector.detect(); assertResource(resource, { version: 'dddd', runtimeDescription: 'Web Browser', @@ -43,7 +43,7 @@ describeBrowser('browserDetector()', () => { sinon.stub(globalThis, 'navigator').value({ userAgent: '', }); - const resource: Resource = await browserDetector.detect(); + const resource: IResource = await browserDetector.detect(); assertEmptyResource(resource); }); }); diff --git a/packages/opentelemetry-resources/test/detectors/browser/EnvDetector.test.ts b/packages/opentelemetry-resources/test/detectors/browser/EnvDetector.test.ts index 0e8894ef93..8901595773 100644 --- a/packages/opentelemetry-resources/test/detectors/browser/EnvDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/browser/EnvDetector.test.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import { RAW_ENVIRONMENT } from '@opentelemetry/core'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; -import { envDetector, Resource } from '../../../src'; +import { envDetector, IResource } from '../../../src'; import { assertEmptyResource, assertWebEngineResource, @@ -39,7 +39,7 @@ describeBrowser('envDetector() on web browser', () => { }); it('should return resource information from environment variable', async () => { - const resource: Resource = await envDetector.detect(); + const resource: IResource = await envDetector.detect(); assertWebEngineResource(resource, { [SemanticResourceAttributes.WEBENGINE_NAME]: 'chromium', [SemanticResourceAttributes.WEBENGINE_VERSION]: '99', @@ -66,7 +66,7 @@ describeBrowser('envDetector() on web browser', () => { }); it('should return empty resource', async () => { - const resource: Resource = await envDetector.detect(); + const resource: IResource = await envDetector.detect(); assertEmptyResource(resource); }); }); @@ -75,14 +75,14 @@ describeBrowser('envDetector() on web browser', () => { describe('with empty env', () => { it('should return empty resource', async () => { - const resource: Resource = await envDetector.detect(); + const resource: IResource = await envDetector.detect(); assertEmptyResource(resource); }); }); describe('with empty env', () => { it('should return empty resource', async () => { - const resource: Resource = await envDetector.detect(); + const resource: IResource = await envDetector.detect(); assertEmptyResource(resource); }); }); diff --git a/packages/opentelemetry-resources/test/detectors/browser/HostDetector.test.ts b/packages/opentelemetry-resources/test/detectors/browser/HostDetector.test.ts index a1299541d9..6f1df6cd23 100644 --- a/packages/opentelemetry-resources/test/detectors/browser/HostDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/browser/HostDetector.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import * as sinon from 'sinon'; -import { hostDetector, Resource } from '../../../src'; +import { hostDetector, IResource } from '../../../src'; import { assertEmptyResource } from '../../util/resource-assertions'; import { describeBrowser } from '../../util'; @@ -24,7 +24,7 @@ describeBrowser('hostDetector() on web browser', () => { }); it('should return empty resource', async () => { - const resource: Resource = await hostDetector.detect(); + const resource: IResource = await hostDetector.detect(); assertEmptyResource(resource); }); }); diff --git a/packages/opentelemetry-resources/test/detectors/browser/OSDetector.test.ts b/packages/opentelemetry-resources/test/detectors/browser/OSDetector.test.ts index 991aa05271..bb69f73954 100644 --- a/packages/opentelemetry-resources/test/detectors/browser/OSDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/browser/OSDetector.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import * as sinon from 'sinon'; -import { osDetector, Resource } from '../../../src'; +import { osDetector, IResource } from '../../../src'; import { assertEmptyResource } from '../../util/resource-assertions'; import { describeBrowser } from '../../util'; @@ -24,7 +24,7 @@ describeBrowser('osDetector() on web browser', () => { }); it('should return empty resource', async () => { - const resource: Resource = await osDetector.detect(); + const resource: IResource = await osDetector.detect(); assertEmptyResource(resource); }); }); diff --git a/packages/opentelemetry-resources/test/detectors/browser/ProcessDetector.test.ts b/packages/opentelemetry-resources/test/detectors/browser/ProcessDetector.test.ts index ae4c824ce0..7c404e67ed 100644 --- a/packages/opentelemetry-resources/test/detectors/browser/ProcessDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/browser/ProcessDetector.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import * as sinon from 'sinon'; -import { processDetector, Resource } from '../../../src'; +import { processDetector, IResource } from '../../../src'; import { assertEmptyResource } from '../../util/resource-assertions'; import { describeBrowser } from '../../util'; @@ -24,7 +24,7 @@ describeBrowser('processDetector() on web browser', () => { }); it('should return empty resource', async () => { - const resource: Resource = await processDetector.detect(); + const resource: IResource = await processDetector.detect(); assertEmptyResource(resource); }); }); diff --git a/packages/opentelemetry-resources/test/detectors/node/BrowserDetector.test.ts b/packages/opentelemetry-resources/test/detectors/node/BrowserDetector.test.ts index 73873b1e1a..e8fff65750 100644 --- a/packages/opentelemetry-resources/test/detectors/node/BrowserDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/node/BrowserDetector.test.ts @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Resource } from '../../../src'; +import { IResource } from '../../../src'; import { browserDetector } from '../../../src/detectors/BrowserDetector'; import { describeNode } from '../../util'; import { assertEmptyResource } from '../../util/resource-assertions'; describeNode('browserDetector()', () => { it('should return empty resources if window.document is missing', async () => { - const resource: Resource = await browserDetector.detect(); + const resource: IResource = await browserDetector.detect(); assertEmptyResource(resource); }); }); diff --git a/packages/opentelemetry-resources/test/detectors/node/EnvDetector.test.ts b/packages/opentelemetry-resources/test/detectors/node/EnvDetector.test.ts index 1397978377..b4ccdd1ad3 100644 --- a/packages/opentelemetry-resources/test/detectors/node/EnvDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/node/EnvDetector.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { envDetector, Resource } from '../../../src'; +import { envDetector, IResource } from '../../../src'; import { assertK8sResource, assertEmptyResource, @@ -33,7 +33,7 @@ describeNode('envDetector() on Node.js', () => { }); it('should return resource information from environment variable', async () => { - const resource: Resource = await envDetector.detect(); + const resource: IResource = await envDetector.detect(); assertK8sResource(resource, { podName: 'pod-xyz-123', clusterName: 'c1', @@ -57,7 +57,7 @@ describeNode('envDetector() on Node.js', () => { }); it('should return empty resource', async () => { - const resource: Resource = await envDetector.detect(); + const resource: IResource = await envDetector.detect(); assertEmptyResource(resource); }); }); @@ -66,7 +66,7 @@ describeNode('envDetector() on Node.js', () => { describe('with empty env', () => { it('should return empty resource', async () => { - const resource: Resource = await envDetector.detect(); + const resource: IResource = await envDetector.detect(); assertEmptyResource(resource); }); }); diff --git a/packages/opentelemetry-resources/test/detectors/node/HostDetector.test.ts b/packages/opentelemetry-resources/test/detectors/node/HostDetector.test.ts index 3c809d500d..287b98b10b 100644 --- a/packages/opentelemetry-resources/test/detectors/node/HostDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/node/HostDetector.test.ts @@ -18,7 +18,7 @@ import * as sinon from 'sinon'; import * as assert from 'assert'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { describeNode } from '../../util'; -import { hostDetector, Resource } from '../../../src'; +import { hostDetector, IResource } from '../../../src'; describeNode('hostDetector() on Node.js', () => { afterEach(() => { @@ -31,7 +31,7 @@ describeNode('hostDetector() on Node.js', () => { sinon.stub(os, 'arch').returns('x64'); sinon.stub(os, 'hostname').returns('opentelemetry-test'); - const resource: Resource = await hostDetector.detect(); + const resource: IResource = await hostDetector.detect(); assert.strictEqual( resource.attributes[SemanticResourceAttributes.HOST_NAME], @@ -48,7 +48,7 @@ describeNode('hostDetector() on Node.js', () => { sinon.stub(os, 'arch').returns('some-unknown-arch'); - const resource: Resource = await hostDetector.detect(); + const resource: IResource = await hostDetector.detect(); assert.strictEqual( resource.attributes[SemanticResourceAttributes.HOST_ARCH], diff --git a/packages/opentelemetry-resources/test/detectors/node/OSDetector.test.ts b/packages/opentelemetry-resources/test/detectors/node/OSDetector.test.ts index dd3ec3e24b..58b1989ec3 100644 --- a/packages/opentelemetry-resources/test/detectors/node/OSDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/node/OSDetector.test.ts @@ -18,7 +18,7 @@ import * as sinon from 'sinon'; import * as assert from 'assert'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { describeNode } from '../../util'; -import { osDetector, Resource } from '../../../src'; +import { osDetector, IResource } from '../../../src'; describeNode('osDetector() on Node.js', () => { afterEach(() => { @@ -31,7 +31,7 @@ describeNode('osDetector() on Node.js', () => { sinon.stub(os, 'platform').returns('win32'); sinon.stub(os, 'release').returns('2.2.1(0.289/5/3)'); - const resource: Resource = await osDetector.detect(); + const resource: IResource = await osDetector.detect(); assert.strictEqual( resource.attributes[SemanticResourceAttributes.OS_TYPE], @@ -48,7 +48,7 @@ describeNode('osDetector() on Node.js', () => { sinon.stub(os, 'platform').returns('some-unknown-platform'); - const resource: Resource = await osDetector.detect(); + const resource: IResource = await osDetector.detect(); assert.strictEqual( resource.attributes[SemanticResourceAttributes.OS_TYPE], diff --git a/packages/opentelemetry-resources/test/detectors/node/ProcessDetector.test.ts b/packages/opentelemetry-resources/test/detectors/node/ProcessDetector.test.ts index dc1a3473a4..6f5793b6ae 100644 --- a/packages/opentelemetry-resources/test/detectors/node/ProcessDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/node/ProcessDetector.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import * as sinon from 'sinon'; -import { processDetector, Resource } from '../../../src'; +import { processDetector, IResource } from '../../../src'; import { assertResource, assertEmptyResource, @@ -34,7 +34,7 @@ describeNode('processDetector() on Node.js', () => { .value(['/tmp/node', '/home/ot/test.js', 'arg1', 'arg2']); sinon.stub(process, 'versions').value({ node: '1.4.1' }); - const resource: Resource = await processDetector.detect(); + const resource: IResource = await processDetector.detect(); assertResource(resource, { pid: 1234, name: 'otProcess', @@ -49,7 +49,7 @@ describeNode('processDetector() on Node.js', () => { sinon.stub(process, 'pid').value(1234); sinon.stub(process, 'title').value(undefined); sinon.stub(process, 'argv').value([]); - const resource: Resource = await processDetector.detect(); + const resource: IResource = await processDetector.detect(); assertEmptyResource(resource); }); }); diff --git a/packages/opentelemetry-resources/test/util/resource-assertions.ts b/packages/opentelemetry-resources/test/util/resource-assertions.ts index 70ab2c8c33..fdf541305e 100644 --- a/packages/opentelemetry-resources/test/util/resource-assertions.ts +++ b/packages/opentelemetry-resources/test/util/resource-assertions.ts @@ -16,7 +16,7 @@ import { SDK_INFO } from '@opentelemetry/core'; import * as assert from 'assert'; -import { Resource } from '../../src/Resource'; +import { IResource } from '../../src/IResource'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; /** @@ -26,7 +26,7 @@ import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions' * @param validations validations for the resource attributes */ export const assertCloudResource = ( - resource: Resource, + resource: IResource, validations: { provider?: string; accountId?: string; @@ -64,7 +64,7 @@ export const assertCloudResource = ( * @param validations validations for the resource attributes */ export const assertContainerResource = ( - resource: Resource, + resource: IResource, validations: { name?: string; id?: string; @@ -102,7 +102,7 @@ export const assertContainerResource = ( * @param validations validations for the resource attributes */ export const assertHostResource = ( - resource: Resource, + resource: IResource, validations: { hostName?: string; id?: string; @@ -153,7 +153,7 @@ export const assertHostResource = ( * @param validations validations for the resource attributes */ export const assertK8sResource = ( - resource: Resource, + resource: IResource, validations: { clusterName?: string; namespaceName?: string; @@ -191,7 +191,7 @@ export const assertK8sResource = ( * @param validations validations for the resource attributes */ export const assertTelemetrySDKResource = ( - resource: Resource, + resource: IResource, validations: { name?: string; language?: string; @@ -229,7 +229,7 @@ export const assertTelemetrySDKResource = ( * @param validations validations for the resource attributes */ export const assertServiceResource = ( - resource: Resource, + resource: IResource, validations: { name: string; instanceId: string; @@ -264,7 +264,7 @@ export const assertServiceResource = ( * @param validations validations for the resource attributes */ export const assertResource = ( - resource: Resource, + resource: IResource, validations: { pid?: number; name?: string; @@ -320,7 +320,7 @@ export const assertResource = ( }; export const assertWebEngineResource = ( - resource: Resource, + resource: IResource, validations: { name?: string; version?: string; @@ -352,11 +352,11 @@ export const assertWebEngineResource = ( * * @param resource the Resource to validate */ -export const assertEmptyResource = (resource: Resource) => { +export const assertEmptyResource = (resource: IResource) => { assert.strictEqual(Object.keys(resource.attributes).length, 0); }; -const assertHasOneLabel = (prefix: string, resource: Resource): void => { +const assertHasOneLabel = (prefix: string, resource: IResource): void => { const hasOne = Object.entries(SemanticResourceAttributes).find( ([key, value]) => { return ( diff --git a/packages/opentelemetry-sdk-trace-base/package.json b/packages/opentelemetry-sdk-trace-base/package.json index b0f0967ead..a76944c137 100644 --- a/packages/opentelemetry-sdk-trace-base/package.json +++ b/packages/opentelemetry-sdk-trace-base/package.json @@ -65,6 +65,7 @@ }, "devDependencies": { "@opentelemetry/api": ">=1.0.0 <1.5.0", + "@opentelemetry/resources_1.9.0": "npm:@opentelemetry/resources@1.9.0", "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", diff --git a/packages/opentelemetry-sdk-trace-base/src/BasicTracerProvider.ts b/packages/opentelemetry-sdk-trace-base/src/BasicTracerProvider.ts index 179b6b1d3e..c06f5cdcb6 100644 --- a/packages/opentelemetry-sdk-trace-base/src/BasicTracerProvider.ts +++ b/packages/opentelemetry-sdk-trace-base/src/BasicTracerProvider.ts @@ -29,7 +29,7 @@ import { getEnv, merge, } from '@opentelemetry/core'; -import { Resource } from '@opentelemetry/resources'; +import { IResource, Resource } from '@opentelemetry/resources'; import { SpanProcessor, Tracer } from '.'; import { loadDefaultConfig } from './config'; import { MultiSpanProcessor } from './MultiSpanProcessor'; @@ -71,7 +71,7 @@ export class BasicTracerProvider implements TracerProvider { private readonly _tracers: Map = new Map(); activeSpanProcessor: SpanProcessor; - readonly resource: Resource; + readonly resource: IResource; constructor(config: TracerConfig = {}) { const mergedConfig = merge( diff --git a/packages/opentelemetry-sdk-trace-base/src/Span.ts b/packages/opentelemetry-sdk-trace-base/src/Span.ts index 7677e6f5ce..fb2b717d48 100644 --- a/packages/opentelemetry-sdk-trace-base/src/Span.ts +++ b/packages/opentelemetry-sdk-trace-base/src/Span.ts @@ -42,7 +42,7 @@ import { otperformance, sanitizeAttributes, } from '@opentelemetry/core'; -import { Resource } from '@opentelemetry/resources'; +import { IResource } from '@opentelemetry/resources'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { ExceptionEventName } from './enums'; import { ReadableSpan } from './export/ReadableSpan'; @@ -64,7 +64,7 @@ export class Span implements APISpan, ReadableSpan { readonly links: Link[] = []; readonly events: TimedEvent[] = []; readonly startTime: HrTime; - readonly resource: Resource; + readonly resource: IResource; readonly instrumentationLibrary: InstrumentationLibrary; name: string; status: SpanStatus = { diff --git a/packages/opentelemetry-sdk-trace-base/src/Tracer.ts b/packages/opentelemetry-sdk-trace-base/src/Tracer.ts index 75443cea88..b77a9427ec 100644 --- a/packages/opentelemetry-sdk-trace-base/src/Tracer.ts +++ b/packages/opentelemetry-sdk-trace-base/src/Tracer.ts @@ -20,7 +20,7 @@ import { sanitizeAttributes, isTracingSuppressed, } from '@opentelemetry/core'; -import { Resource } from '@opentelemetry/resources'; +import { IResource } from '@opentelemetry/resources'; import { BasicTracerProvider } from './BasicTracerProvider'; import { Span } from './Span'; import { GeneralLimits, SpanLimits, TracerConfig } from './types'; @@ -38,7 +38,7 @@ export class Tracer implements api.Tracer { private readonly _generalLimits: GeneralLimits; private readonly _spanLimits: SpanLimits; private readonly _idGenerator: IdGenerator; - readonly resource: Resource; + readonly resource: IResource; readonly instrumentationLibrary: InstrumentationLibrary; /** diff --git a/packages/opentelemetry-sdk-trace-base/src/export/BatchSpanProcessorBase.ts b/packages/opentelemetry-sdk-trace-base/src/export/BatchSpanProcessorBase.ts index 1a0968642d..2f14b77c74 100644 --- a/packages/opentelemetry-sdk-trace-base/src/export/BatchSpanProcessorBase.ts +++ b/packages/opentelemetry-sdk-trace-base/src/export/BatchSpanProcessorBase.ts @@ -160,10 +160,11 @@ export abstract class BatchSpanProcessorBase context.with(suppressTracing(context.active()), () => { // Reset the finished spans buffer here because the next invocations of the _flush method // could pass the same finished spans to the exporter if the buffer is cleared - // outside of the execution of this callback. - this._exporter.export( - this._finishedSpans.splice(0, this._maxExportBatchSize), - result => { + // outside the execution of this callback. + const spans = this._finishedSpans.splice(0, this._maxExportBatchSize); + + const doExport = () => + this._exporter.export(spans, result => { clearTimeout(timer); if (result.code === ExportResultCode.SUCCESS) { resolve(); @@ -173,8 +174,24 @@ export abstract class BatchSpanProcessorBase new Error('BatchSpanProcessor: span export failed') ); } - } - ); + }); + const pendingResources = spans + .map(span => span.resource) + .filter(resource => resource.asyncAttributesPending); + + // Avoid scheduling a promise to make the behavior more predictable and easier to test + if (pendingResources.length === 0) { + doExport(); + } else { + Promise.all( + pendingResources.map(resource => + resource.waitForAsyncAttributes?.() + ) + ).then(doExport, err => { + globalErrorHandler(err); + reject(err); + }); + } }); }); } diff --git a/packages/opentelemetry-sdk-trace-base/src/export/ReadableSpan.ts b/packages/opentelemetry-sdk-trace-base/src/export/ReadableSpan.ts index 8552134a56..aa19891099 100644 --- a/packages/opentelemetry-sdk-trace-base/src/export/ReadableSpan.ts +++ b/packages/opentelemetry-sdk-trace-base/src/export/ReadableSpan.ts @@ -22,7 +22,7 @@ import { Link, SpanContext, } from '@opentelemetry/api'; -import { Resource } from '@opentelemetry/resources'; +import { IResource } from '@opentelemetry/resources'; import { InstrumentationLibrary } from '@opentelemetry/core'; import { TimedEvent } from '../TimedEvent'; @@ -39,6 +39,6 @@ export interface ReadableSpan { readonly events: TimedEvent[]; readonly duration: HrTime; readonly ended: boolean; - readonly resource: Resource; + readonly resource: IResource; readonly instrumentationLibrary: InstrumentationLibrary; } diff --git a/packages/opentelemetry-sdk-trace-base/src/export/SimpleSpanProcessor.ts b/packages/opentelemetry-sdk-trace-base/src/export/SimpleSpanProcessor.ts index 5e6064d03e..673e631c8d 100644 --- a/packages/opentelemetry-sdk-trace-base/src/export/SimpleSpanProcessor.ts +++ b/packages/opentelemetry-sdk-trace-base/src/export/SimpleSpanProcessor.ts @@ -26,6 +26,7 @@ import { Span } from '../Span'; import { SpanProcessor } from '../SpanProcessor'; import { ReadableSpan } from './ReadableSpan'; import { SpanExporter } from './SpanExporter'; +import { Resource } from '@opentelemetry/resources'; /** * An implementation of the {@link SpanProcessor} that converts the {@link Span} @@ -35,17 +36,18 @@ import { SpanExporter } from './SpanExporter'; */ export class SimpleSpanProcessor implements SpanProcessor { private _shutdownOnce: BindOnceFuture; + private _unresolvedExports: Set>; constructor(private readonly _exporter: SpanExporter) { this._shutdownOnce = new BindOnceFuture(this._shutdown, this); + this._unresolvedExports = new Set>(); } - forceFlush(): Promise { - // do nothing as all spans are being exported without waiting - return Promise.resolve(); + async forceFlush(): Promise { + // await unresolved resources before resolving + await Promise.all(Array.from(this._unresolvedExports)); } - // does nothing. onStart(_span: Span, _parentContext: Context): void {} onEnd(span: ReadableSpan): void { @@ -57,21 +59,40 @@ export class SimpleSpanProcessor implements SpanProcessor { return; } - internal - ._export(this._exporter, [span]) - .then((result: ExportResult) => { - if (result.code !== ExportResultCode.SUCCESS) { - globalErrorHandler( - result.error ?? - new Error( - `SimpleSpanProcessor: span export failed (status ${result})` - ) - ); - } - }) - .catch(error => { - globalErrorHandler(error); - }); + const doExport = () => + internal + ._export(this._exporter, [span]) + .then((result: ExportResult) => { + if (result.code !== ExportResultCode.SUCCESS) { + globalErrorHandler( + result.error ?? + new Error( + `SimpleSpanProcessor: span export failed (status ${result})` + ) + ); + } + }) + .catch(error => { + globalErrorHandler(error); + }); + + // Avoid scheduling a promise to make the behavior more predictable and easier to test + if (span.resource.asyncAttributesPending) { + const exportPromise = (span.resource as Resource) + .waitForAsyncAttributes() + .then( + () => { + this._unresolvedExports.delete(exportPromise); + return doExport(); + }, + err => globalErrorHandler(err) + ); + + // store the unresolved exports + this._unresolvedExports.add(exportPromise); + } else { + void doExport(); + } } shutdown(): Promise { diff --git a/packages/opentelemetry-sdk-trace-base/src/types.ts b/packages/opentelemetry-sdk-trace-base/src/types.ts index 54eaaf97d9..6854f0315a 100644 --- a/packages/opentelemetry-sdk-trace-base/src/types.ts +++ b/packages/opentelemetry-sdk-trace-base/src/types.ts @@ -15,7 +15,7 @@ */ import { ContextManager, TextMapPropagator } from '@opentelemetry/api'; -import { Resource } from '@opentelemetry/resources'; +import { IResource } from '@opentelemetry/resources'; import { IdGenerator } from './IdGenerator'; import { Sampler } from './Sampler'; @@ -35,7 +35,7 @@ export interface TracerConfig { spanLimits?: SpanLimits; /** Resource associated with trace telemetry */ - resource?: Resource; + resource?: IResource; /** * Generator of trace and span IDs diff --git a/packages/opentelemetry-sdk-trace-base/test/common/export/BatchSpanProcessorBase.test.ts b/packages/opentelemetry-sdk-trace-base/test/common/export/BatchSpanProcessorBase.test.ts index 9149e6cbf8..fd9574537b 100644 --- a/packages/opentelemetry-sdk-trace-base/test/common/export/BatchSpanProcessorBase.test.ts +++ b/packages/opentelemetry-sdk-trace-base/test/common/export/BatchSpanProcessorBase.test.ts @@ -34,6 +34,7 @@ import { TestRecordOnlySampler } from './TestRecordOnlySampler'; import { TestTracingSpanExporter } from './TestTracingSpanExporter'; import { TestStackContextManager } from './TestStackContextManager'; import { BatchSpanProcessorBase } from '../../../src/export/BatchSpanProcessorBase'; +import { Resource, ResourceAttributes } from '@opentelemetry/resources'; function createSampledSpan(spanName: string): Span { const tracer = new BasicTracerProvider({ @@ -390,6 +391,27 @@ describe('BatchSpanProcessorBase', () => { done(); }); }); + + it('should wait for pending resource on flush', async () => { + const tracer = new BasicTracerProvider({ + resource: new Resource( + {}, + new Promise(resolve => { + setTimeout(() => resolve({ async: 'fromasync' }), 1); + }) + ), + }).getTracer('default'); + + const span = tracer.startSpan('test') as Span; + span.end(); + + processor.onStart(span, ROOT_CONTEXT); + processor.onEnd(span); + + await processor.forceFlush(); + + assert.strictEqual(exporter.getFinishedSpans().length, 1); + }); }); describe('flushing spans with exporter triggering instrumentation', () => { diff --git a/packages/opentelemetry-sdk-trace-base/test/common/export/SimpleSpanProcessor.test.ts b/packages/opentelemetry-sdk-trace-base/test/common/export/SimpleSpanProcessor.test.ts index fda60f500e..88490c527b 100644 --- a/packages/opentelemetry-sdk-trace-base/test/common/export/SimpleSpanProcessor.test.ts +++ b/packages/opentelemetry-sdk-trace-base/test/common/export/SimpleSpanProcessor.test.ts @@ -36,6 +36,9 @@ import { } from '../../../src'; import { TestStackContextManager } from './TestStackContextManager'; import { TestTracingSpanExporter } from './TestTracingSpanExporter'; +import { Resource, ResourceAttributes } from '@opentelemetry/resources'; +import { Resource as Resource190 } from '@opentelemetry/resources_1.9.0'; +import { TestExporterWithDelay } from './TestExporterWithDelay'; describe('SimpleSpanProcessor', () => { let provider: BasicTracerProvider; @@ -149,6 +152,83 @@ describe('SimpleSpanProcessor', () => { }); describe('force flush', () => { + it('should await unresolved resources', async () => { + const processor = new SimpleSpanProcessor(exporter); + const providerWithAsyncResource = new BasicTracerProvider({ + resource: new Resource( + {}, + new Promise(resolve => { + setTimeout(() => resolve({ async: 'fromasync' }), 1); + }) + ), + }); + const spanContext: SpanContext = { + traceId: 'a3cda95b652f4a1592b449d5929fda1b', + spanId: '5e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + const span = new Span( + providerWithAsyncResource.getTracer('default'), + ROOT_CONTEXT, + 'span-name', + spanContext, + SpanKind.CLIENT + ); + processor.onStart(span, ROOT_CONTEXT); + assert.strictEqual(exporter.getFinishedSpans().length, 0); + + processor.onEnd(span); + assert.strictEqual(exporter.getFinishedSpans().length, 0); + + await processor.forceFlush(); + + const exportedSpans = exporter.getFinishedSpans(); + + assert.strictEqual(exportedSpans.length, 1); + assert.strictEqual( + exportedSpans[0].resource.attributes['async'], + 'fromasync' + ); + }); + + it('should await doExport() and delete from _unresolvedExports', async () => { + const testExporterWithDelay = new TestExporterWithDelay(); + const processor = new SimpleSpanProcessor(testExporterWithDelay); + + const providerWithAsyncResource = new BasicTracerProvider({ + resource: new Resource( + {}, + new Promise(resolve => { + setTimeout(() => resolve({ async: 'fromasync' }), 1); + }) + ), + }); + const spanContext: SpanContext = { + traceId: 'a3cda95b652f4a1592b449d5929fda1b', + spanId: '5e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + const span = new Span( + providerWithAsyncResource.getTracer('default'), + ROOT_CONTEXT, + 'span-name', + spanContext, + SpanKind.CLIENT + ); + processor.onStart(span, ROOT_CONTEXT); + processor.onEnd(span); + + assert.strictEqual(processor['_unresolvedExports'].size, 1); + + await processor.forceFlush(); + + assert.strictEqual(processor['_unresolvedExports'].size, 0); + + const exportedSpans = testExporterWithDelay.getFinishedSpans(); + + assert.strictEqual(exportedSpans.length, 1); + }); + describe('when flushing complete', () => { it('should call an async callback', done => { const processor = new SimpleSpanProcessor(exporter); @@ -203,4 +283,36 @@ describe('SimpleSpanProcessor', () => { assert.equal(exporterCreatedSpans.length, 0); }); }); + + describe('compatibility', () => { + it('should export when using old resource implementation', async () => { + const processor = new SimpleSpanProcessor(exporter); + const providerWithAsyncResource = new BasicTracerProvider({ + resource: new Resource190({ fromold: 'fromold' }), + }); + const spanContext: SpanContext = { + traceId: 'a3cda95b652f4a1592b449d5929fda1b', + spanId: '5e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + const span = new Span( + providerWithAsyncResource.getTracer('default'), + ROOT_CONTEXT, + 'span-name', + spanContext, + SpanKind.CLIENT + ); + processor.onStart(span, ROOT_CONTEXT); + assert.strictEqual(exporter.getFinishedSpans().length, 0); + processor.onEnd(span); + + const exportedSpans = exporter.getFinishedSpans(); + + assert.strictEqual(exportedSpans.length, 1); + assert.strictEqual( + exportedSpans[0].resource.attributes['fromold'], + 'fromold' + ); + }); + }); }); diff --git a/packages/opentelemetry-sdk-trace-base/test/common/export/TestExporterWithDelay.ts b/packages/opentelemetry-sdk-trace-base/test/common/export/TestExporterWithDelay.ts new file mode 100644 index 0000000000..d10dab5271 --- /dev/null +++ b/packages/opentelemetry-sdk-trace-base/test/common/export/TestExporterWithDelay.ts @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ExportResult } from '@opentelemetry/core'; +import { InMemorySpanExporter, ReadableSpan } from '../../../src'; + +/** + * A test-only exporter that delays during export to mimic a real exporter. + */ +export class TestExporterWithDelay extends InMemorySpanExporter { + private _exporterCreatedSpans: ReadableSpan[] = []; + + constructor() { + super(); + } + + override export( + spans: ReadableSpan[], + resultCallback: (result: ExportResult) => void + ): void { + super.export(spans, () => setTimeout(resultCallback, 1)); + } + + override shutdown(): Promise { + return super.shutdown().then(() => { + this._exporterCreatedSpans = []; + }); + } + + override reset() { + super.reset(); + this._exporterCreatedSpans = []; + } + + getExporterCreatedSpans(): ReadableSpan[] { + return this._exporterCreatedSpans; + } +} diff --git a/packages/sdk-metrics/src/MeterProvider.ts b/packages/sdk-metrics/src/MeterProvider.ts index 72f0945b9f..f10cf42b9b 100644 --- a/packages/sdk-metrics/src/MeterProvider.ts +++ b/packages/sdk-metrics/src/MeterProvider.ts @@ -21,7 +21,7 @@ import { MeterOptions, createNoopMeter, } from '@opentelemetry/api'; -import { Resource } from '@opentelemetry/resources'; +import { IResource, Resource } from '@opentelemetry/resources'; import { MetricReader } from './export/MetricReader'; import { MeterProviderSharedState } from './state/MeterProviderSharedState'; import { MetricCollector } from './state/MetricCollector'; @@ -33,7 +33,7 @@ import { View } from './view/View'; */ export interface MeterProviderOptions { /** Resource associated with metric telemetry */ - resource?: Resource; + resource?: IResource; views?: View[]; } diff --git a/packages/sdk-metrics/src/export/MetricData.ts b/packages/sdk-metrics/src/export/MetricData.ts index e7adfa0357..931be0c05b 100644 --- a/packages/sdk-metrics/src/export/MetricData.ts +++ b/packages/sdk-metrics/src/export/MetricData.ts @@ -16,7 +16,7 @@ import { HrTime, MetricAttributes } from '@opentelemetry/api'; import { InstrumentationScope } from '@opentelemetry/core'; -import { Resource } from '@opentelemetry/resources'; +import { IResource } from '@opentelemetry/resources'; import { InstrumentDescriptor } from '../InstrumentDescriptor'; import { AggregationTemporality } from './AggregationTemporality'; import { Histogram } from '../aggregator/types'; @@ -67,7 +67,7 @@ export interface ScopeMetrics { } export interface ResourceMetrics { - resource: Resource; + resource: IResource; scopeMetrics: ScopeMetrics[]; } diff --git a/packages/sdk-metrics/src/export/PeriodicExportingMetricReader.ts b/packages/sdk-metrics/src/export/PeriodicExportingMetricReader.ts index c156e883ad..2371ecb67f 100644 --- a/packages/sdk-metrics/src/export/PeriodicExportingMetricReader.ts +++ b/packages/sdk-metrics/src/export/PeriodicExportingMetricReader.ts @@ -24,6 +24,7 @@ import { import { MetricReader } from './MetricReader'; import { PushMetricExporter } from './MetricExporter'; import { callWithTimeout, TimeoutError } from '../utils'; +import { diag } from '@opentelemetry/api'; export type PeriodicExportingMetricReaderOptions = { /** @@ -117,11 +118,24 @@ export class PeriodicExportingMetricReader extends MetricReader { ); } - const result = await internal._export(this._exporter, resourceMetrics); - if (result.code !== ExportResultCode.SUCCESS) { - throw new Error( - `PeriodicExportingMetricReader: metrics export failed (error ${result.error})` - ); + const doExport = async () => { + const result = await internal._export(this._exporter, resourceMetrics); + if (result.code !== ExportResultCode.SUCCESS) { + throw new Error( + `PeriodicExportingMetricReader: metrics export failed (error ${result.error})` + ); + } + }; + + // Avoid scheduling a promise to make the behavior more predictable and easier to test + if (resourceMetrics.resource.asyncAttributesPending) { + resourceMetrics.resource + .waitForAsyncAttributes?.() + .then(doExport, err => + diag.debug('Error while resolving async portion of resource: ', err) + ); + } else { + await doExport(); } } diff --git a/packages/sdk-metrics/src/state/MeterProviderSharedState.ts b/packages/sdk-metrics/src/state/MeterProviderSharedState.ts index a63f53d51d..fa7903b20e 100644 --- a/packages/sdk-metrics/src/state/MeterProviderSharedState.ts +++ b/packages/sdk-metrics/src/state/MeterProviderSharedState.ts @@ -15,7 +15,7 @@ */ import { InstrumentationScope } from '@opentelemetry/core'; -import { Resource } from '@opentelemetry/resources'; +import { IResource } from '@opentelemetry/resources'; import { Aggregation, InstrumentType } from '..'; import { instrumentationScopeId } from '../utils'; import { ViewRegistry } from '../view/ViewRegistry'; @@ -32,7 +32,7 @@ export class MeterProviderSharedState { meterSharedStates: Map = new Map(); - constructor(public resource: Resource) {} + constructor(public resource: IResource) {} getMeterSharedState(instrumentationScope: InstrumentationScope) { const id = instrumentationScopeId(instrumentationScope);