diff --git a/packages/opentelemetry-resources/src/config.ts b/packages/opentelemetry-resources/src/config.ts new file mode 100644 index 0000000000..8eb9007eb6 --- /dev/null +++ b/packages/opentelemetry-resources/src/config.ts @@ -0,0 +1,35 @@ +/* + * 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 { Logger } from '@opentelemetry/api'; + +/** + * ResourceDetectionConfig provides an interface for configuring resource auto-detection. + */ +export interface ResourceDetectionConfig { + /** Optional Logger. */ + logger?: Logger; +} + +/** + * ResourceDetectionConfigWithLogger provides an interface for interacting with + * {@link ResourceDetectionConfig} instances that must have a logger defined. + */ +export interface ResourceDetectionConfigWithLogger + extends ResourceDetectionConfig { + /** Required Logger */ + logger: Logger; +} diff --git a/packages/opentelemetry-resources/src/platform/node/detect-resources.ts b/packages/opentelemetry-resources/src/platform/node/detect-resources.ts index d226ed4437..ca7add14b2 100644 --- a/packages/opentelemetry-resources/src/platform/node/detect-resources.ts +++ b/packages/opentelemetry-resources/src/platform/node/detect-resources.ts @@ -17,25 +17,72 @@ import { Resource } from '../../Resource'; import { envDetector, awsEc2Detector, gcpDetector } from './detectors'; import { Detector } from '../../types'; +import { + ResourceDetectionConfig, + ResourceDetectionConfigWithLogger, +} from '../../config'; +import { Logger } from '@opentelemetry/api'; +import * as util from 'util'; +import { NoopLogger } from '@opentelemetry/core'; const DETECTORS: Array = [envDetector, awsEc2Detector, gcpDetector]; /** * Runs all resource detectors and returns the results merged into a single * Resource. + * + * @param config Configuration for resource detection */ -export const detectResources = async (): Promise => { +export const detectResources = async ( + config: ResourceDetectionConfig = {} +): Promise => { + const internalConfig: ResourceDetectionConfigWithLogger = Object.assign( + { + logger: new NoopLogger(), + }, + config + ); + const resources: Array = await Promise.all( DETECTORS.map(d => { try { - return d.detect(); + return d.detect(internalConfig); } catch { return Resource.empty(); } }) ); + // Log Resources only if there is a user-provided logger + if (config.logger) { + logResources(config.logger, resources); + } return resources.reduce( (acc, resource) => acc.merge(resource), Resource.createTelemetrySDKResource() ); }; + +/** + * Writes debug information about the detected resources to the logger defined in the resource detection config, if one is provided. + * + * @param logger The {@link Logger} to write the debug information to. + * @param resources The array of {@link Resource} that should be logged. Empty entried will be ignored. + */ +const logResources = (logger: Logger, resources: Array) => { + resources.forEach((resource, index) => { + // Print only populated resources + if (Object.keys(resource.labels).length > 0) { + const resourceDebugString = util.inspect(resource.labels, { + depth: 2, + breakLength: Infinity, + sorted: true, + compact: false, + }); + const detectorName = DETECTORS[index].constructor + ? DETECTORS[index].constructor.name + : 'Unknown detector'; + logger.debug(`${detectorName} found resource.`); + logger.debug(resourceDebugString); + } + }); +}; diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts index e58c214db0..76fd11527a 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts @@ -18,6 +18,7 @@ import * as http from 'http'; import { Resource } from '../../../Resource'; import { CLOUD_RESOURCE, HOST_RESOURCE } from '../../../constants'; import { Detector } from '../../../types'; +import { ResourceDetectionConfigWithLogger } from '../../../config'; /** * The AwsEc2Detector can be used to detect if a process is running in AWS EC2 @@ -38,8 +39,10 @@ class AwsEc2Detector implements Detector { * populated with instance metadata as labels. Returns a promise containing an * empty {@link Resource} if the connection or parsing of the identity * document fails. + * + * @param config The resource detection config with a required logger */ - async detect(): Promise { + async detect(config: ResourceDetectionConfigWithLogger): Promise { try { const { accountId, @@ -56,7 +59,8 @@ class AwsEc2Detector implements Detector { [HOST_RESOURCE.ID]: instanceId, [HOST_RESOURCE.TYPE]: instanceType, }); - } catch { + } catch (e) { + config.logger.debug(`AwsEc2Detector failed: ${e.message}`); return Resource.empty(); } } diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts index 58ba567c36..718c566f12 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts @@ -16,6 +16,7 @@ import { Resource } from '../../../Resource'; import { Detector, ResourceLabels } from '../../../types'; +import { ResourceDetectionConfigWithLogger } from '../../../config'; /** * EnvDetector can be used to detect the presence of and create a Resource @@ -45,16 +46,24 @@ class EnvDetector implements Detector { * Returns a {@link Resource} populated with labels from the * OTEL_RESOURCE_LABELS environment variable. Note this is an async function * to conform to the Detector interface. + * + * @param config The resource detection config with a required logger */ - async detect(): Promise { + async detect(config: ResourceDetectionConfigWithLogger): Promise { try { const labelString = process.env.OTEL_RESOURCE_LABELS; - if (!labelString) return Resource.empty(); + if (!labelString) { + config.logger.debug( + 'EnvDetector failed: Environment variable "OTEL_RESOURCE_LABELS" is missing.' + ); + return Resource.empty(); + } const labels = this._parseResourceLabels( process.env.OTEL_RESOURCE_LABELS ); return new Resource(labels); - } catch { + } catch (e) { + config.logger.debug(`EnvDetector failed: ${e.message}`); return Resource.empty(); } } diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts index 25518ed963..a93173d1a1 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts @@ -24,6 +24,7 @@ import { K8S_RESOURCE, CONTAINER_RESOURCE, } from '../../../constants'; +import { ResourceDetectionConfigWithLogger } from '../../../config'; /** * The GcpDetector can be used to detect if a process is running in the Google @@ -31,8 +32,19 @@ import { * the instance. Returns an empty Resource if detection fails. */ class GcpDetector implements Detector { - async detect(): Promise { - if (!(await gcpMetadata.isAvailable())) return Resource.empty(); + /** + * Attempts to connect and obtain instance configuration data from the GCP metadata service. + * If the connection is succesful it returns a promise containing a {@link Resource} + * populated with instance metadata as labels. Returns a promise containing an + * empty {@link Resource} if the connection or parsing of the metadata fails. + * + * @param config The resource detection config with a required logger + */ + async detect(config: ResourceDetectionConfigWithLogger): Promise { + if (!(await gcpMetadata.isAvailable())) { + config.logger.debug('GcpDetector failed: GCP Metadata unavailable.'); + return Resource.empty(); + } const [projectId, instanceId, zoneId, clusterName] = await Promise.all([ this._getProjectId(), diff --git a/packages/opentelemetry-resources/src/types.ts b/packages/opentelemetry-resources/src/types.ts index e1aa0d45b0..c33562312f 100644 --- a/packages/opentelemetry-resources/src/types.ts +++ b/packages/opentelemetry-resources/src/types.ts @@ -15,6 +15,7 @@ */ import { Resource } from './Resource'; +import { ResourceDetectionConfigWithLogger } from './config'; /** Interface for Resource labels */ export interface ResourceLabels { @@ -26,5 +27,5 @@ export interface ResourceLabels { * a detector returns a Promise containing a Resource. */ export interface Detector { - detect(): Promise; + detect(config: ResourceDetectionConfigWithLogger): Promise; } diff --git a/packages/opentelemetry-resources/test/detect-resources.test.ts b/packages/opentelemetry-resources/test/detect-resources.test.ts index ca609fb3c4..b607efa0d6 100644 --- a/packages/opentelemetry-resources/test/detect-resources.test.ts +++ b/packages/opentelemetry-resources/test/detect-resources.test.ts @@ -16,6 +16,7 @@ import * as nock from 'nock'; import * as sinon from 'sinon'; +import * as assert from 'assert'; import { URL } from 'url'; import { Resource, detectResources } from '../src'; import { awsEc2Detector } from '../src/platform/node/detectors'; @@ -162,4 +163,113 @@ describe('detectResources', async () => { stub.restore(); }); }); + + describe('with a debug logger', () => { + // Local functions to test if a mocked method is ever called with a specific argument or regex matching for an argument. + // Needed because of race condition with parallel detectors. + const callArgsContains = ( + mockedFunction: sinon.SinonSpy, + arg: any + ): boolean => { + return mockedFunction.getCalls().some(call => { + return call.args.some(callarg => arg === callarg); + }); + }; + const callArgsMatches = ( + mockedFunction: sinon.SinonSpy, + regex: RegExp + ): boolean => { + return mockedFunction.getCalls().some(call => { + return regex.test(call.args.toString()); + }); + }; + + it('prints detected resources and debug messages to the logger', async () => { + // This test depends on the env detector to be functioning as intended + const mockedLoggerMethod = sinon.fake(); + await detectResources({ + logger: { + debug: mockedLoggerMethod, + info: sinon.fake(), + warn: sinon.fake(), + error: sinon.fake(), + }, + }); + + // Test for AWS and GCP Detector failure + assert.ok( + callArgsContains( + mockedLoggerMethod, + 'GcpDetector failed: GCP Metadata unavailable.' + ) + ); + assert.ok( + callArgsContains( + mockedLoggerMethod, + 'AwsEc2Detector failed: Nock: Disallowed net connect for "169.254.169.254:80/latest/dynamic/instance-identity/document"' + ) + ); + // Test that the Env Detector successfully found its resource and populated it with the right values. + assert.ok( + callArgsContains(mockedLoggerMethod, 'EnvDetector found resource.') + ); + // Regex formatting accounts for whitespace variations in util.inspect output over different node versions + assert.ok( + callArgsMatches( + mockedLoggerMethod, + /{\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*/ + ) + ); + }); + + describe('with missing environemnt variable', () => { + beforeEach(() => { + delete process.env.OTEL_RESOURCE_LABELS; + }); + + it('prints correct error messages when EnvDetector has no env variable', async () => { + const mockedLoggerMethod = sinon.fake(); + await detectResources({ + logger: { + debug: mockedLoggerMethod, + info: sinon.fake(), + warn: sinon.fake(), + error: sinon.fake(), + }, + }); + + assert.ok( + callArgsContains( + mockedLoggerMethod, + 'EnvDetector failed: Environment variable "OTEL_RESOURCE_LABELS" is missing.' + ) + ); + }); + }); + + describe('with a faulty environment variable', () => { + beforeEach(() => { + process.env.OTEL_RESOURCE_LABELS = 'bad=~label'; + }); + + it('prints correct error messages when EnvDetector has an invalid variable', async () => { + const mockedLoggerMethod = sinon.fake(); + await detectResources({ + logger: { + debug: mockedLoggerMethod, + info: sinon.fake(), + warn: sinon.fake(), + error: sinon.fake(), + }, + }); + + assert.ok( + callArgsContains( + mockedLoggerMethod, + 'EnvDetector failed: Label value should be a ASCII string with a length not exceed 255 characters.' + ) + ); + }); + }); + }); }); diff --git a/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts b/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts index b60a6cc7f6..9b9e6eb035 100644 --- a/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts @@ -24,6 +24,7 @@ import { assertHostResource, assertEmptyResource, } from '../util/resource-assertions'; +import { NoopLogger } from '@opentelemetry/core'; const { origin: AWS_HOST, pathname: AWS_PATH } = new URL( awsEc2Detector.AWS_INSTANCE_IDENTITY_DOCUMENT_URI @@ -52,7 +53,9 @@ describe('awsEc2Detector', () => { const scope = nock(AWS_HOST) .get(AWS_PATH) .reply(200, () => mockedAwsResponse); - const resource: Resource = await awsEc2Detector.detect(); + const resource: Resource = await awsEc2Detector.detect({ + logger: new NoopLogger(), + }); scope.done(); assert.ok(resource); @@ -74,7 +77,9 @@ describe('awsEc2Detector', () => { const scope = nock(AWS_HOST).get(AWS_PATH).replyWithError({ code: 'ENOTFOUND', }); - const resource: Resource = await awsEc2Detector.detect(); + const resource: Resource = await awsEc2Detector.detect({ + logger: new NoopLogger(), + }); scope.done(); assert.ok(resource); diff --git a/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts b/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts index 20a63e64d5..894de6e870 100644 --- a/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts @@ -21,6 +21,7 @@ import { assertEmptyResource, } from '../util/resource-assertions'; import { K8S_RESOURCE } from '../../src'; +import { NoopLogger } from '@opentelemetry/core'; describe('envDetector()', () => { describe('with valid env', () => { @@ -34,7 +35,9 @@ describe('envDetector()', () => { }); it('should return resource information from environment variable', async () => { - const resource: Resource = await envDetector.detect(); + const resource: Resource = await envDetector.detect({ + logger: new NoopLogger(), + }); assertK8sResource(resource, { [K8S_RESOURCE.POD_NAME]: 'pod-xyz-123', [K8S_RESOURCE.CLUSTER_NAME]: 'c1', @@ -45,7 +48,9 @@ describe('envDetector()', () => { describe('with empty env', () => { it('should return empty resource', async () => { - const resource: Resource = await envDetector.detect(); + const resource: Resource = await envDetector.detect({ + logger: new NoopLogger(), + }); assertEmptyResource(resource); }); }); diff --git a/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts b/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts index 75adb78980..c476feb5a7 100644 --- a/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts @@ -32,6 +32,7 @@ import { assertContainerResource, assertEmptyResource, } from '../util/resource-assertions'; +import { NoopLogger } from '@opentelemetry/core'; const HEADERS = { [HEADER_NAME.toLowerCase()]: HEADER_VALUE, @@ -80,7 +81,9 @@ describe('gcpDetector', () => { const secondaryScope = nock(SECONDARY_HOST_ADDRESS) .get(INSTANCE_PATH) .reply(200, {}, HEADERS); - const resource: Resource = await gcpDetector.detect(); + const resource: Resource = await gcpDetector.detect({ + logger: new NoopLogger(), + }); secondaryScope.done(); scope.done(); @@ -111,7 +114,7 @@ describe('gcpDetector', () => { const secondaryScope = nock(SECONDARY_HOST_ADDRESS) .get(INSTANCE_PATH) .reply(200, {}, HEADERS); - const resource = await gcpDetector.detect(); + const resource = await gcpDetector.detect({ logger: new NoopLogger() }); secondaryScope.done(); scope.done(); @@ -143,7 +146,7 @@ describe('gcpDetector', () => { const secondaryScope = nock(SECONDARY_HOST_ADDRESS) .get(INSTANCE_PATH) .reply(200, {}, HEADERS); - const resource = await gcpDetector.detect(); + const resource = await gcpDetector.detect({ logger: new NoopLogger() }); secondaryScope.done(); scope.done(); @@ -155,7 +158,7 @@ describe('gcpDetector', () => { }); it('returns empty resource if not detected', async () => { - const resource = await gcpDetector.detect(); + const resource = await gcpDetector.detect({ logger: new NoopLogger() }); assertEmptyResource(resource); }); });