From 12cdffc2717e104e16ba8433448e48323cdaa81b Mon Sep 17 00:00:00 2001 From: David Luna Date: Fri, 12 Jul 2024 13:27:26 +0200 Subject: [PATCH 1/3] refactor(detector-aws): change implementation to DetectorSync interface for EC2 detector --- .../src/detectors/AwsEc2Detector.ts | 53 +++++++++++-------- .../test/detectors/AwsEc2Detector.test.ts | 51 +++++++----------- .../auto-instrumentations-node/src/utils.ts | 6 ++- 3 files changed, 56 insertions(+), 54 deletions(-) diff --git a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEc2Detector.ts b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEc2Detector.ts index 5afec499d2..78de0a1710 100644 --- a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEc2Detector.ts +++ b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEc2Detector.ts @@ -15,8 +15,10 @@ */ import { - Detector, + DetectorSync, + IResource, Resource, + ResourceAttributes, ResourceDetectionConfig, } from '@opentelemetry/resources'; import { @@ -38,7 +40,7 @@ import * as http from 'http'; * and return a {@link Resource} populated with metadata about the EC2 * instance. Returns an empty Resource if detection fails. */ -class AwsEc2Detector implements Detector { +class AwsEc2Detector implements DetectorSync { /** * See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html * for documentation about the AWS instance identity document @@ -53,31 +55,38 @@ class AwsEc2Detector implements Detector { readonly AWS_METADATA_TOKEN_HEADER = 'X-aws-ec2-metadata-token'; readonly MILLISECOND_TIME_OUT = 5000; + detect(_config?: ResourceDetectionConfig): IResource { + return new Resource({}, this._getAttributes()); + } + /** * Attempts to connect and obtain an AWS instance Identity document. If the - * connection is successful it returns a promise containing a {@link Resource} - * populated with instance metadata. Returns a promise containing an - * empty {@link Resource} if the connection or parsing of the identity + * connection is successful it returns a promise containing a {@link ResourceAttributes} + * object with instance metadata. Returns a promise containing an + * empty {@link ResourceAttributes} if the connection or parsing of the identity * document fails. - * - * @param config (unused) The resource detection config */ - async detect(_config?: ResourceDetectionConfig): Promise { - const token = await this._fetchToken(); - const { accountId, instanceId, instanceType, region, availabilityZone } = - await this._fetchIdentity(token); - const hostname = await this._fetchHost(token); + async _getAttributes(): Promise { + try { + const token = await this._fetchToken(); + const { accountId, instanceId, instanceType, region, availabilityZone } = + await this._fetchIdentity(token); + const hostname = await this._fetchHost(token); - return new Resource({ - [SEMRESATTRS_CLOUD_PROVIDER]: CLOUDPROVIDERVALUES_AWS, - [SEMRESATTRS_CLOUD_PLATFORM]: CLOUDPLATFORMVALUES_AWS_EC2, - [SEMRESATTRS_CLOUD_ACCOUNT_ID]: accountId, - [SEMRESATTRS_CLOUD_REGION]: region, - [SEMRESATTRS_CLOUD_AVAILABILITY_ZONE]: availabilityZone, - [SEMRESATTRS_HOST_ID]: instanceId, - [SEMRESATTRS_HOST_TYPE]: instanceType, - [SEMRESATTRS_HOST_NAME]: hostname, - }); + return { + [SEMRESATTRS_CLOUD_PROVIDER]: CLOUDPROVIDERVALUES_AWS, + [SEMRESATTRS_CLOUD_PLATFORM]: CLOUDPLATFORMVALUES_AWS_EC2, + [SEMRESATTRS_CLOUD_ACCOUNT_ID]: accountId, + [SEMRESATTRS_CLOUD_REGION]: region, + [SEMRESATTRS_CLOUD_AVAILABILITY_ZONE]: availabilityZone, + [SEMRESATTRS_HOST_ID]: instanceId, + [SEMRESATTRS_HOST_TYPE]: instanceType, + [SEMRESATTRS_HOST_NAME]: hostname, + }; + } catch { + return {}; + } + } private async _fetchToken(): Promise { diff --git a/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEc2Detector.test.ts b/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEc2Detector.test.ts index 501d019b59..9f2793f117 100644 --- a/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEc2Detector.test.ts +++ b/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEc2Detector.test.ts @@ -16,7 +16,7 @@ import * as nock from 'nock'; import * as assert from 'assert'; -import { Resource } from '@opentelemetry/resources'; + import { awsEc2Detector } from '../../src'; import { assertCloudResource, @@ -64,7 +64,8 @@ describe('awsEc2Detector', () => { .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) .reply(200, () => mockedHostResponse); - const resource: Resource = await awsEc2Detector.detect(); + const resource = awsEc2Detector.detect(); + await resource.waitForAsyncAttributes?.(); scope.done(); @@ -85,8 +86,7 @@ describe('awsEc2Detector', () => { }); describe('with unsuccessful request', () => { - it('should throw when receiving error response code', async () => { - const expectedError = new Error('Failed to load page, status code: 404'); + it('should return empty resource when receiving error response code', async () => { const scope = nock(AWS_HOST) .persist() .put(AWS_TOKEN_PATH) @@ -99,19 +99,16 @@ describe('awsEc2Detector', () => { .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) .reply(404, () => new Error()); - try { - await awsEc2Detector.detect(); - assert.ok(false, 'Expected to throw'); - } catch (err) { - assert.deepStrictEqual(err, expectedError); - } + const resource = awsEc2Detector.detect(); + await resource.waitForAsyncAttributes?.(); + + assert.deepStrictEqual(resource.attributes, {}); scope.done(); }); - it('should throw when timed out', function (done) { + it('should return empty resource when timed out', async function () { this.timeout(6000); - const expectedError = new Error('EC2 metadata api request timed out.'); const scope = nock(AWS_HOST) .put(AWS_TOKEN_PATH) .matchHeader(AWS_METADATA_TTL_HEADER, '60') @@ -124,21 +121,15 @@ describe('awsEc2Detector', () => { .delayConnection(5000) .reply(200, () => mockedHostResponse); - awsEc2Detector - .detect() - .then(() => { - assert.ok(false, 'Expected to throw'); - }) - .catch(err => { - assert.deepStrictEqual(err, expectedError); - }) - .finally(() => { - scope.done(); - done(); - }); + const resource = awsEc2Detector.detect(); + await resource.waitForAsyncAttributes?.(); + + assert.deepStrictEqual(resource.attributes, {}); + + scope.done(); }); - it('should throw when replied with an Error', async () => { + it('should return empty resource when replied with an Error', async () => { const expectedError = new Error('NOT FOUND'); const scope = nock(AWS_HOST) .put(AWS_TOKEN_PATH) @@ -148,12 +139,10 @@ describe('awsEc2Detector', () => { .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) .replyWithError(expectedError.message); - try { - await awsEc2Detector.detect(); - assert.ok(false, 'Expected to throw'); - } catch (err) { - assert.deepStrictEqual(err, expectedError); - } + const resource = awsEc2Detector.detect(); + await resource.waitForAsyncAttributes?.(); + + assert.deepStrictEqual(resource.attributes, {}); scope.done(); }); diff --git a/metapackages/auto-instrumentations-node/src/utils.ts b/metapackages/auto-instrumentations-node/src/utils.ts index c258dfc597..481dbb68ec 100644 --- a/metapackages/auto-instrumentations-node/src/utils.ts +++ b/metapackages/auto-instrumentations-node/src/utils.ts @@ -235,7 +235,11 @@ function getDisabledInstrumentationsFromEnv() { export function getResourceDetectorsFromEnv(): Array { const resourceDetectors = new Map< string, - Detector | DetectorSync | Detector[] | DetectorSync[] + | Detector + | DetectorSync + | Detector[] + | DetectorSync[] + | Array >([ [RESOURCE_DETECTOR_CONTAINER, containerDetector], [RESOURCE_DETECTOR_ENVIRONMENT, envDetectorSync], From 6b8880747110eeaafd4ea60c1aef83cda822080e Mon Sep 17 00:00:00 2001 From: David Luna Date: Fri, 12 Jul 2024 13:29:23 +0200 Subject: [PATCH 2/3] refactor(detector-aws): lint fix --- .../src/detectors/AwsEc2Detector.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEc2Detector.ts b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEc2Detector.ts index 78de0a1710..ab40a9b185 100644 --- a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEc2Detector.ts +++ b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEc2Detector.ts @@ -86,7 +86,6 @@ class AwsEc2Detector implements DetectorSync { } catch { return {}; } - } private async _fetchToken(): Promise { From eac609bbbcfba234b17196721292936a703d6b72 Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 1 Aug 2024 17:19:52 +0200 Subject: [PATCH 3/3] feat(detector-aws): add EC2 detector implementing DetectorSync interface --- .../src/detectors/AwsEc2Detector.ts | 153 +--------------- .../src/detectors/AwsEc2DetectorSync.ts | 173 ++++++++++++++++++ .../src/detectors/index.ts | 3 +- .../test/detectors/AwsEc2Detector.test.ts | 23 +-- .../test/detectors/AwsEc2DetectorSync.test.ts | 152 +++++++++++++++ 5 files changed, 348 insertions(+), 156 deletions(-) create mode 100644 detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEc2DetectorSync.ts create mode 100644 detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEc2DetectorSync.test.ts diff --git a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEc2Detector.ts b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEc2Detector.ts index ab40a9b185..3014fa5844 100644 --- a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEc2Detector.ts +++ b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEc2Detector.ts @@ -15,158 +15,23 @@ */ import { - DetectorSync, + Detector, IResource, - Resource, - ResourceAttributes, ResourceDetectionConfig, } from '@opentelemetry/resources'; -import { - SEMRESATTRS_CLOUD_PROVIDER, - SEMRESATTRS_CLOUD_PLATFORM, - SEMRESATTRS_CLOUD_REGION, - SEMRESATTRS_CLOUD_ACCOUNT_ID, - SEMRESATTRS_CLOUD_AVAILABILITY_ZONE, - SEMRESATTRS_HOST_ID, - SEMRESATTRS_HOST_TYPE, - SEMRESATTRS_HOST_NAME, - CLOUDPROVIDERVALUES_AWS, - CLOUDPLATFORMVALUES_AWS_EC2, -} from '@opentelemetry/semantic-conventions'; -import * as http from 'http'; + +import { awsEc2DetectorSync } from './AwsEc2DetectorSync'; /** * The AwsEc2Detector can be used to detect if a process is running in AWS EC2 * and return a {@link Resource} populated with metadata about the EC2 - * instance. Returns an empty Resource if detection fails. + * instance. + * + * @deprecated Use {@link AwsEc2DetectorSync} class instead. */ -class AwsEc2Detector implements DetectorSync { - /** - * See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html - * for documentation about the AWS instance identity document - * and standard of IMDSv2. - */ - readonly AWS_IDMS_ENDPOINT = '169.254.169.254'; - readonly AWS_INSTANCE_TOKEN_DOCUMENT_PATH = '/latest/api/token'; - readonly AWS_INSTANCE_IDENTITY_DOCUMENT_PATH = - '/latest/dynamic/instance-identity/document'; - readonly AWS_INSTANCE_HOST_DOCUMENT_PATH = '/latest/meta-data/hostname'; - readonly AWS_METADATA_TTL_HEADER = 'X-aws-ec2-metadata-token-ttl-seconds'; - readonly AWS_METADATA_TOKEN_HEADER = 'X-aws-ec2-metadata-token'; - readonly MILLISECOND_TIME_OUT = 5000; - - detect(_config?: ResourceDetectionConfig): IResource { - return new Resource({}, this._getAttributes()); - } - - /** - * Attempts to connect and obtain an AWS instance Identity document. If the - * connection is successful it returns a promise containing a {@link ResourceAttributes} - * object with instance metadata. Returns a promise containing an - * empty {@link ResourceAttributes} if the connection or parsing of the identity - * document fails. - */ - async _getAttributes(): Promise { - try { - const token = await this._fetchToken(); - const { accountId, instanceId, instanceType, region, availabilityZone } = - await this._fetchIdentity(token); - const hostname = await this._fetchHost(token); - - return { - [SEMRESATTRS_CLOUD_PROVIDER]: CLOUDPROVIDERVALUES_AWS, - [SEMRESATTRS_CLOUD_PLATFORM]: CLOUDPLATFORMVALUES_AWS_EC2, - [SEMRESATTRS_CLOUD_ACCOUNT_ID]: accountId, - [SEMRESATTRS_CLOUD_REGION]: region, - [SEMRESATTRS_CLOUD_AVAILABILITY_ZONE]: availabilityZone, - [SEMRESATTRS_HOST_ID]: instanceId, - [SEMRESATTRS_HOST_TYPE]: instanceType, - [SEMRESATTRS_HOST_NAME]: hostname, - }; - } catch { - return {}; - } - } - - private async _fetchToken(): Promise { - const options = { - host: this.AWS_IDMS_ENDPOINT, - path: this.AWS_INSTANCE_TOKEN_DOCUMENT_PATH, - method: 'PUT', - timeout: this.MILLISECOND_TIME_OUT, - headers: { - [this.AWS_METADATA_TTL_HEADER]: '60', - }, - }; - return await this._fetchString(options); - } - - private async _fetchIdentity(token: string): Promise { - const options = { - host: this.AWS_IDMS_ENDPOINT, - path: this.AWS_INSTANCE_IDENTITY_DOCUMENT_PATH, - method: 'GET', - timeout: this.MILLISECOND_TIME_OUT, - headers: { - [this.AWS_METADATA_TOKEN_HEADER]: token, - }, - }; - const identity = await this._fetchString(options); - return JSON.parse(identity); - } - - private async _fetchHost(token: string): Promise { - const options = { - host: this.AWS_IDMS_ENDPOINT, - path: this.AWS_INSTANCE_HOST_DOCUMENT_PATH, - method: 'GET', - timeout: this.MILLISECOND_TIME_OUT, - headers: { - [this.AWS_METADATA_TOKEN_HEADER]: token, - }, - }; - return await this._fetchString(options); - } - - /** - * Establishes an HTTP connection to AWS instance document url. - * If the application is running on an EC2 instance, we should be able - * to get back a valid JSON document. Parses that document and stores - * the identity properties in a local map. - */ - private async _fetchString(options: http.RequestOptions): Promise { - return new Promise((resolve, reject) => { - const timeoutId = setTimeout(() => { - req.abort(); - reject(new Error('EC2 metadata api request timed out.')); - }, this.MILLISECOND_TIME_OUT); - - const req = http.request(options, res => { - clearTimeout(timeoutId); - const { statusCode } = res; - res.setEncoding('utf8'); - let rawData = ''; - res.on('data', chunk => (rawData += chunk)); - res.on('end', () => { - if (statusCode && statusCode >= 200 && statusCode < 300) { - try { - resolve(rawData); - } catch (e) { - reject(e); - } - } else { - reject( - new Error('Failed to load page, status code: ' + statusCode) - ); - } - }); - }); - req.on('error', err => { - clearTimeout(timeoutId); - reject(err); - }); - req.end(); - }); +class AwsEc2Detector implements Detector { + detect(_config?: ResourceDetectionConfig): Promise { + return Promise.resolve(awsEc2DetectorSync.detect(_config)); } } diff --git a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEc2DetectorSync.ts b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEc2DetectorSync.ts new file mode 100644 index 0000000000..b553a7ecf8 --- /dev/null +++ b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEc2DetectorSync.ts @@ -0,0 +1,173 @@ +/* + * 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 { + DetectorSync, + IResource, + Resource, + ResourceAttributes, + ResourceDetectionConfig, +} from '@opentelemetry/resources'; +import { + SEMRESATTRS_CLOUD_PROVIDER, + SEMRESATTRS_CLOUD_PLATFORM, + SEMRESATTRS_CLOUD_REGION, + SEMRESATTRS_CLOUD_ACCOUNT_ID, + SEMRESATTRS_CLOUD_AVAILABILITY_ZONE, + SEMRESATTRS_HOST_ID, + SEMRESATTRS_HOST_TYPE, + SEMRESATTRS_HOST_NAME, + CLOUDPROVIDERVALUES_AWS, + CLOUDPLATFORMVALUES_AWS_EC2, +} from '@opentelemetry/semantic-conventions'; +import * as http from 'http'; + +/** + * The AwsEc2DetectorSync can be used to detect if a process is running in AWS EC2 + * and return a {@link Resource} populated with metadata about the EC2 + * instance. Returns an empty Resource if detection fails. + */ +class AwsEc2DetectorSync implements DetectorSync { + /** + * See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html + * for documentation about the AWS instance identity document + * and standard of IMDSv2. + */ + readonly AWS_IDMS_ENDPOINT = '169.254.169.254'; + readonly AWS_INSTANCE_TOKEN_DOCUMENT_PATH = '/latest/api/token'; + readonly AWS_INSTANCE_IDENTITY_DOCUMENT_PATH = + '/latest/dynamic/instance-identity/document'; + readonly AWS_INSTANCE_HOST_DOCUMENT_PATH = '/latest/meta-data/hostname'; + readonly AWS_METADATA_TTL_HEADER = 'X-aws-ec2-metadata-token-ttl-seconds'; + readonly AWS_METADATA_TOKEN_HEADER = 'X-aws-ec2-metadata-token'; + readonly MILLISECOND_TIME_OUT = 5000; + + detect(_config?: ResourceDetectionConfig): IResource { + return new Resource({}, this._getAttributes()); + } + + /** + * Attempts to connect and obtain an AWS instance Identity document. If the + * connection is successful it returns a promise containing a {@link ResourceAttributes} + * object with instance metadata. Returns a promise containing an + * empty {@link ResourceAttributes} if the connection or parsing of the identity + * document fails. + */ + async _getAttributes(): Promise { + try { + const token = await this._fetchToken(); + const { accountId, instanceId, instanceType, region, availabilityZone } = + await this._fetchIdentity(token); + const hostname = await this._fetchHost(token); + + return { + [SEMRESATTRS_CLOUD_PROVIDER]: CLOUDPROVIDERVALUES_AWS, + [SEMRESATTRS_CLOUD_PLATFORM]: CLOUDPLATFORMVALUES_AWS_EC2, + [SEMRESATTRS_CLOUD_ACCOUNT_ID]: accountId, + [SEMRESATTRS_CLOUD_REGION]: region, + [SEMRESATTRS_CLOUD_AVAILABILITY_ZONE]: availabilityZone, + [SEMRESATTRS_HOST_ID]: instanceId, + [SEMRESATTRS_HOST_TYPE]: instanceType, + [SEMRESATTRS_HOST_NAME]: hostname, + }; + } catch { + return {}; + } + } + + private async _fetchToken(): Promise { + const options = { + host: this.AWS_IDMS_ENDPOINT, + path: this.AWS_INSTANCE_TOKEN_DOCUMENT_PATH, + method: 'PUT', + timeout: this.MILLISECOND_TIME_OUT, + headers: { + [this.AWS_METADATA_TTL_HEADER]: '60', + }, + }; + return await this._fetchString(options); + } + + private async _fetchIdentity(token: string): Promise { + const options = { + host: this.AWS_IDMS_ENDPOINT, + path: this.AWS_INSTANCE_IDENTITY_DOCUMENT_PATH, + method: 'GET', + timeout: this.MILLISECOND_TIME_OUT, + headers: { + [this.AWS_METADATA_TOKEN_HEADER]: token, + }, + }; + const identity = await this._fetchString(options); + return JSON.parse(identity); + } + + private async _fetchHost(token: string): Promise { + const options = { + host: this.AWS_IDMS_ENDPOINT, + path: this.AWS_INSTANCE_HOST_DOCUMENT_PATH, + method: 'GET', + timeout: this.MILLISECOND_TIME_OUT, + headers: { + [this.AWS_METADATA_TOKEN_HEADER]: token, + }, + }; + return await this._fetchString(options); + } + + /** + * Establishes an HTTP connection to AWS instance document url. + * If the application is running on an EC2 instance, we should be able + * to get back a valid JSON document. Parses that document and stores + * the identity properties in a local map. + */ + private async _fetchString(options: http.RequestOptions): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + req.abort(); + reject(new Error('EC2 metadata api request timed out.')); + }, this.MILLISECOND_TIME_OUT); + + const req = http.request(options, res => { + clearTimeout(timeoutId); + const { statusCode } = res; + res.setEncoding('utf8'); + let rawData = ''; + res.on('data', chunk => (rawData += chunk)); + res.on('end', () => { + if (statusCode && statusCode >= 200 && statusCode < 300) { + try { + resolve(rawData); + } catch (e) { + reject(e); + } + } else { + reject( + new Error('Failed to load page, status code: ' + statusCode) + ); + } + }); + }); + req.on('error', err => { + clearTimeout(timeoutId); + reject(err); + }); + req.end(); + }); + } +} + +export const awsEc2DetectorSync = new AwsEc2DetectorSync(); diff --git a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/index.ts b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/index.ts index b475e4a208..a56a54aeb5 100644 --- a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/index.ts +++ b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/index.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -export * from './AwsEc2Detector'; +export { awsEc2Detector } from './AwsEc2Detector'; +export { awsEc2DetectorSync } from './AwsEc2DetectorSync'; export * from './AwsBeanstalkDetector'; export * from './AwsEcsDetector'; export * from './AwsEksDetector'; diff --git a/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEc2Detector.test.ts b/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEc2Detector.test.ts index 9f2793f117..c837f4c6ed 100644 --- a/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEc2Detector.test.ts +++ b/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEc2Detector.test.ts @@ -17,18 +17,19 @@ import * as nock from 'nock'; import * as assert from 'assert'; -import { awsEc2Detector } from '../../src'; +import { awsEc2Detector, awsEc2DetectorSync } from '../../src'; import { assertCloudResource, assertHostResource, } from '@opentelemetry/contrib-test-utils'; -const AWS_HOST = 'http://' + awsEc2Detector.AWS_IDMS_ENDPOINT; -const AWS_TOKEN_PATH = awsEc2Detector.AWS_INSTANCE_TOKEN_DOCUMENT_PATH; -const AWS_IDENTITY_PATH = awsEc2Detector.AWS_INSTANCE_IDENTITY_DOCUMENT_PATH; -const AWS_HOST_PATH = awsEc2Detector.AWS_INSTANCE_HOST_DOCUMENT_PATH; -const AWS_METADATA_TTL_HEADER = awsEc2Detector.AWS_METADATA_TTL_HEADER; -const AWS_METADATA_TOKEN_HEADER = awsEc2Detector.AWS_METADATA_TOKEN_HEADER; +const AWS_HOST = 'http://' + awsEc2DetectorSync.AWS_IDMS_ENDPOINT; +const AWS_TOKEN_PATH = awsEc2DetectorSync.AWS_INSTANCE_TOKEN_DOCUMENT_PATH; +const AWS_IDENTITY_PATH = + awsEc2DetectorSync.AWS_INSTANCE_IDENTITY_DOCUMENT_PATH; +const AWS_HOST_PATH = awsEc2DetectorSync.AWS_INSTANCE_HOST_DOCUMENT_PATH; +const AWS_METADATA_TTL_HEADER = awsEc2DetectorSync.AWS_METADATA_TTL_HEADER; +const AWS_METADATA_TOKEN_HEADER = awsEc2DetectorSync.AWS_METADATA_TOKEN_HEADER; const mockedTokenResponse = 'my-token'; const mockedIdentityResponse = { @@ -64,7 +65,7 @@ describe('awsEc2Detector', () => { .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) .reply(200, () => mockedHostResponse); - const resource = awsEc2Detector.detect(); + const resource = await awsEc2Detector.detect(); await resource.waitForAsyncAttributes?.(); scope.done(); @@ -99,7 +100,7 @@ describe('awsEc2Detector', () => { .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) .reply(404, () => new Error()); - const resource = awsEc2Detector.detect(); + const resource = await awsEc2Detector.detect(); await resource.waitForAsyncAttributes?.(); assert.deepStrictEqual(resource.attributes, {}); @@ -121,7 +122,7 @@ describe('awsEc2Detector', () => { .delayConnection(5000) .reply(200, () => mockedHostResponse); - const resource = awsEc2Detector.detect(); + const resource = await awsEc2Detector.detect(); await resource.waitForAsyncAttributes?.(); assert.deepStrictEqual(resource.attributes, {}); @@ -139,7 +140,7 @@ describe('awsEc2Detector', () => { .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) .replyWithError(expectedError.message); - const resource = awsEc2Detector.detect(); + const resource = await awsEc2Detector.detect(); await resource.waitForAsyncAttributes?.(); assert.deepStrictEqual(resource.attributes, {}); diff --git a/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEc2DetectorSync.test.ts b/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEc2DetectorSync.test.ts new file mode 100644 index 0000000000..4f309d5ec5 --- /dev/null +++ b/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEc2DetectorSync.test.ts @@ -0,0 +1,152 @@ +/* + * 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 * as nock from 'nock'; +import * as assert from 'assert'; + +import { + assertCloudResource, + assertHostResource, +} from '@opentelemetry/contrib-test-utils'; + +import { awsEc2DetectorSync } from '../../src'; + +const AWS_HOST = 'http://' + awsEc2DetectorSync.AWS_IDMS_ENDPOINT; +const AWS_TOKEN_PATH = awsEc2DetectorSync.AWS_INSTANCE_TOKEN_DOCUMENT_PATH; +const AWS_IDENTITY_PATH = + awsEc2DetectorSync.AWS_INSTANCE_IDENTITY_DOCUMENT_PATH; +const AWS_HOST_PATH = awsEc2DetectorSync.AWS_INSTANCE_HOST_DOCUMENT_PATH; +const AWS_METADATA_TTL_HEADER = awsEc2DetectorSync.AWS_METADATA_TTL_HEADER; +const AWS_METADATA_TOKEN_HEADER = awsEc2DetectorSync.AWS_METADATA_TOKEN_HEADER; + +const mockedTokenResponse = 'my-token'; +const mockedIdentityResponse = { + instanceId: 'my-instance-id', + instanceType: 'my-instance-type', + accountId: 'my-account-id', + region: 'my-region', + availabilityZone: 'my-zone', +}; +const mockedHostResponse = 'my-hostname'; + +describe('awsEc2DetectorSync', () => { + beforeEach(() => { + nock.disableNetConnect(); + nock.cleanAll(); + }); + + afterEach(() => { + nock.enableNetConnect(); + }); + + describe('with successful request', () => { + it('should return aws_ec2_instance resource', async () => { + const scope = nock(AWS_HOST) + .persist() + .put(AWS_TOKEN_PATH) + .matchHeader(AWS_METADATA_TTL_HEADER, '60') + .reply(200, () => mockedTokenResponse) + .get(AWS_IDENTITY_PATH) + .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) + .reply(200, () => mockedIdentityResponse) + .get(AWS_HOST_PATH) + .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) + .reply(200, () => mockedHostResponse); + + const resource = awsEc2DetectorSync.detect(); + await resource.waitForAsyncAttributes?.(); + + scope.done(); + + assert.ok(resource); + + assertCloudResource(resource, { + provider: 'aws', + accountId: 'my-account-id', + region: 'my-region', + zone: 'my-zone', + }); + assertHostResource(resource, { + id: 'my-instance-id', + hostType: 'my-instance-type', + name: 'my-hostname', + }); + }); + }); + + describe('with unsuccessful request', () => { + it('should return empty resource when receiving error response code', async () => { + const scope = nock(AWS_HOST) + .persist() + .put(AWS_TOKEN_PATH) + .matchHeader(AWS_METADATA_TTL_HEADER, '60') + .reply(200, () => mockedTokenResponse) + .get(AWS_IDENTITY_PATH) + .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) + .reply(200, () => mockedIdentityResponse) + .get(AWS_HOST_PATH) + .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) + .reply(404, () => new Error()); + + const resource = awsEc2DetectorSync.detect(); + await resource.waitForAsyncAttributes?.(); + + assert.deepStrictEqual(resource.attributes, {}); + + scope.done(); + }); + + it('should return empty resource when timed out', async function () { + this.timeout(6000); + const scope = nock(AWS_HOST) + .put(AWS_TOKEN_PATH) + .matchHeader(AWS_METADATA_TTL_HEADER, '60') + .reply(200, () => mockedTokenResponse) + .get(AWS_IDENTITY_PATH) + .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) + .reply(200, () => mockedIdentityResponse) + .get(AWS_HOST_PATH) + .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) + .delayConnection(5000) + .reply(200, () => mockedHostResponse); + + const resource = awsEc2DetectorSync.detect(); + await resource.waitForAsyncAttributes?.(); + + assert.deepStrictEqual(resource.attributes, {}); + + scope.done(); + }); + + it('should return empty resource when replied with an Error', async () => { + const expectedError = new Error('NOT FOUND'); + const scope = nock(AWS_HOST) + .put(AWS_TOKEN_PATH) + .matchHeader(AWS_METADATA_TTL_HEADER, '60') + .reply(200, () => mockedTokenResponse) + .get(AWS_IDENTITY_PATH) + .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) + .replyWithError(expectedError.message); + + const resource = awsEc2DetectorSync.detect(); + await resource.waitForAsyncAttributes?.(); + + assert.deepStrictEqual(resource.attributes, {}); + + scope.done(); + }); + }); +});