From 7d569da8001789d73ca33c46b57f2c0a2c3940a5 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Thu, 12 Mar 2020 19:27:08 -0700 Subject: [PATCH 01/20] feat: add resource detection from environment variable --- .../src/detectors/EnvDetector.ts | 144 ++++++++++++++++++ .../test/detectors/EnvDetector.test.ts | 52 +++++++ .../test/util/resource-assertions.ts | 9 ++ 3 files changed, 205 insertions(+) create mode 100644 packages/opentelemetry-resources/src/detectors/EnvDetector.ts create mode 100644 packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts diff --git a/packages/opentelemetry-resources/src/detectors/EnvDetector.ts b/packages/opentelemetry-resources/src/detectors/EnvDetector.ts new file mode 100644 index 00000000000..debdc40c947 --- /dev/null +++ b/packages/opentelemetry-resources/src/detectors/EnvDetector.ts @@ -0,0 +1,144 @@ +/*! + * Copyright 2020, 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'; + +/** + * EnvDetector can be used to detect the presence of and create a Resource + * from the OTEL_RESOURCE_LABELS environment variable. + */ +export class EnvDetector { + // Type, label keys, and label values should not exceed 256 characters. + private static readonly MAX_LENGTH = 255; + + // OTEL_RESOURCE_LABELS is a comma-separated list of labels. + private static readonly COMMA_SEPARATOR = ','; + + // OTEL_RESOURCE_LABELS contains key value pair separated by '='. + private static readonly LABEL_KEY_VALUE_SPLITTER = '='; + + private static readonly ERROR_MESSAGE_INVALID_CHARS = + 'should be a ASCII string with a length greater than 0 and not exceed ' + + EnvDetector.MAX_LENGTH + + ' characters.'; + + private static readonly ERROR_MESSAGE_INVALID_VALUE = + 'should be a ASCII string with a length not exceed ' + + EnvDetector.MAX_LENGTH + + ' characters.'; + + /** + * Returns a {@link Resource} populated with labels from the + * OTEL_RESOURCE_LABELS environment variable + */ + static detect(): Resource { + try { + const labelString = process.env.OTEL_RESOURCE_LABELS; + if (!labelString) return Resource.empty(); + const labels = EnvDetector.parseResourceLabels( + process.env.OTEL_RESOURCE_LABELS + ); + return new Resource(labels); + } catch { + return Resource.empty(); + } + } + + /** + * Creates a label map from the OC_RESOURCE_LABELS environment variable. + * + * OC_RESOURCE_LABELS: A comma-separated list of labels describing the + * source in more detail, e.g. “key1=val1,key2=val2”. Domain names and paths + * are accepted as label keys. Values may be quoted or unquoted in general. If + * a value contains whitespaces, =, or " characters, it must always be quoted. + * + * @param rawEnvLabels The resource labels as a comma-seperated list + * of key/value pairs. + * @returns The sanitized resource labels. + */ + private static parseResourceLabels( + rawEnvLabels?: string + ): { [key: string]: string } { + if (!rawEnvLabels) return {}; + + const labels: { [key: string]: string } = {}; + const rawLabels: string[] = rawEnvLabels.split( + EnvDetector.COMMA_SEPARATOR, + -1 + ); + for (const rawLabel of rawLabels) { + const keyValuePair: string[] = rawLabel.split( + EnvDetector.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 (!EnvDetector.isValidAndNotEmpty(key)) { + throw new Error(`Label key ${EnvDetector.ERROR_MESSAGE_INVALID_CHARS}`); + } + if (!EnvDetector.isValid(value)) { + throw new Error( + `Label value ${EnvDetector.ERROR_MESSAGE_INVALID_VALUE}` + ); + } + labels[key] = value; + } + return labels; + } + + /** + * 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 static isValid(name: string): boolean { + return ( + name.length <= EnvDetector.MAX_LENGTH && + EnvDetector.isPrintableString(name) + ); + } + + private static isPrintableString(str: string): boolean { + for (let i = 0; i < str.length; i++) { + const ch: string = str.charAt(i); + if (ch <= ' ' || ch >= '~') { + 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 static isValidAndNotEmpty(str: string): boolean { + return str.length > 0 && EnvDetector.isValid(str); + } +} diff --git a/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts b/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts new file mode 100644 index 00000000000..32c399dcb34 --- /dev/null +++ b/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts @@ -0,0 +1,52 @@ +/*! + * Copyright 2020, 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 '../../src/Resource'; +import { EnvDetector } from '../../src/detectors/EnvDetector'; +import { + assertK8sResource, + assertEmptyResource, +} from '../util/resource-assertions'; +import { K8S_RESOURCE } from '../../src'; + +describe('EnvDetector()', () => { + describe('with valid env', () => { + before(() => { + process.env.OTEL_RESOURCE_LABELS = + 'k8s.pod.name="pod-xyz-123",k8s.cluster.name="c1",k8s.namespace.name="default"'; + }); + + after(() => { + delete process.env.OTEL_RESOURCE_LABELS; + }); + + it('should return resource information from environment variable', () => { + const resource: Resource = EnvDetector.detect(); + assertK8sResource(resource, { + [K8S_RESOURCE.POD_NAME]: 'pod-xyz-123', + [K8S_RESOURCE.CLUSTER_NAME]: 'c1', + [K8S_RESOURCE.NAMESPACE_NAME]: 'default', + }); + }); + }); + + describe('with empty env', () => { + it('should return empty resource', () => { + const resource: Resource = EnvDetector.detect(); + assertEmptyResource(resource); + }); + }); +}); diff --git a/packages/opentelemetry-resources/test/util/resource-assertions.ts b/packages/opentelemetry-resources/test/util/resource-assertions.ts index 0401a8dd4f4..02332191a16 100644 --- a/packages/opentelemetry-resources/test/util/resource-assertions.ts +++ b/packages/opentelemetry-resources/test/util/resource-assertions.ts @@ -251,6 +251,15 @@ export const assertServiceResource = ( ); }; +/** + * Test utility method to validate an empty resource + * + * @param resource the Resource to validate + */ +export const assertEmptyResource = (resource: Resource) => { + assert.strictEqual(Object.keys(resource.labels).length, 0); +}; + const assertHasOneLabel = ( constants: { [key: string]: string }, resource: Resource From 8190a381d2f5b34f561eb4d7b4f3e9637145bc99 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Fri, 13 Mar 2020 19:32:00 -0700 Subject: [PATCH 02/20] feat: add AWS EC2 resource detection --- packages/opentelemetry-resources/package.json | 1 + .../src/detectors/AwsEc2Detector.ts | 98 +++++++++++++++++++ .../test/detectors/AwsEc2Detector.test.ts | 82 ++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 packages/opentelemetry-resources/src/detectors/AwsEc2Detector.ts create mode 100644 packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts diff --git a/packages/opentelemetry-resources/package.json b/packages/opentelemetry-resources/package.json index 8e9d8a852cb..831807917a4 100644 --- a/packages/opentelemetry-resources/package.json +++ b/packages/opentelemetry-resources/package.json @@ -45,6 +45,7 @@ "codecov": "^3.6.1", "gts": "^1.1.0", "mocha": "^6.2.0", + "nock": "^12.0.2", "nyc": "^15.0.0", "rimraf": "^3.0.0", "ts-mocha": "^6.0.0", diff --git a/packages/opentelemetry-resources/src/detectors/AwsEc2Detector.ts b/packages/opentelemetry-resources/src/detectors/AwsEc2Detector.ts new file mode 100644 index 00000000000..99ddbf11c28 --- /dev/null +++ b/packages/opentelemetry-resources/src/detectors/AwsEc2Detector.ts @@ -0,0 +1,98 @@ +/*! + * Copyright 2020, 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 http from 'http'; +import { Resource } from '../Resource'; +import { CLOUD_RESOURCE, HOST_RESOURCE } from '../constants'; + +/** + * 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. + */ +export class AwsEc2Detector { + static AWS_INSTANCE_IDENTITY_DOCUMENT_URI = + 'http://169.254.169.254/latest/dynamic/instance-identity/document'; + + /** + * Attempts to connect and obtain an AWS instance Identity document. 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 identity + * document fails. + */ + static async detect(): Promise { + try { + const { + accountId, + instanceId, + region, + } = await AwsEc2Detector.awsMetadataAccessor(); + return new Resource({ + [CLOUD_RESOURCE.PROVIDER]: 'aws', + [CLOUD_RESOURCE.ACCOUNT_ID]: accountId, + [CLOUD_RESOURCE.REGION]: region, + [HOST_RESOURCE.ID]: instanceId, + }); + } catch { + return Resource.empty(); + } + } + + /** + * Establishes an HTTP connection to AWS instance identity 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 static async awsMetadataAccessor(): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + reject(new Error('EC2 metadata api request timed out.')); + }, 1000); + + const req = http.get( + AwsEc2Detector.AWS_INSTANCE_IDENTITY_DOCUMENT_URI, + 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(JSON.parse(rawData)); + } catch (e) { + res.resume(); // consume response data to free up memory + reject(e); + } + } else { + res.resume(); // consume response data to free up memory + reject( + new Error('Failed to load page, status code: ' + statusCode) + ); + } + }); + } + ); + req.on('error', err => { + clearTimeout(timeoutId); + reject(err); + }); + }); + } +} diff --git a/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts b/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts new file mode 100644 index 00000000000..44d8951c428 --- /dev/null +++ b/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts @@ -0,0 +1,82 @@ +/*! + * Copyright 2020, 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 { URL } from 'url'; +import { Resource } from '../../src'; +import { AwsEc2Detector } from '../../src/detectors/AwsEc2Detector'; +import { + assertCloudResource, + assertHostResource, + assertEmptyResource, +} from '../util/resource-assertions'; + +const { origin: AWS_HOST, pathname: AWS_PATH } = new URL( + AwsEc2Detector.AWS_INSTANCE_IDENTITY_DOCUMENT_URI +); + +const mockedAwsResponse = { + instanceId: 'my-instance-id', + accountId: 'my-account-id', + region: 'my-region', +}; + +describe('AwsEc2Detector', () => { + before(() => { + nock.disableNetConnect(); + nock.cleanAll(); + }); + + after(() => { + nock.enableNetConnect(); + }); + + describe('with successful request', () => { + it('should return aws_ec2_instance resource', async () => { + const scope = nock(AWS_HOST) + .get(AWS_PATH) + .reply(200, () => mockedAwsResponse); + const resource: Resource = await AwsEc2Detector.detect(); + scope.done(); + + assert.ok(resource); + assertCloudResource(resource, { + provider: 'aws', + accountId: 'my-account-id', + region: 'my-region', + }); + assertHostResource(resource, { + id: 'my-instance-id', + }); + }); + }); + + describe('with failing request', () => { + it('should return empty resource', async () => { + const scope = nock(AWS_HOST) + .get(AWS_PATH) + .replyWithError({ + code: 'ENOTFOUND', + }); + const resource: Resource = await AwsEc2Detector.detect(); + scope.done(); + + assert.ok(resource); + assertEmptyResource(resource); + }); + }); +}); From 8c61b505946d0259f24d373b1c8fec506cce1933 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Mon, 16 Mar 2020 18:28:28 -0700 Subject: [PATCH 03/20] feat: add resource detection for GCP --- packages/opentelemetry-resources/package.json | 3 +- .../src/detectors/GcpDetector.ts | 101 +++++++++++ .../test/detectors/GcpDetector.test.ts | 170 ++++++++++++++++++ 3 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 packages/opentelemetry-resources/src/detectors/GcpDetector.ts create mode 100644 packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts diff --git a/packages/opentelemetry-resources/package.json b/packages/opentelemetry-resources/package.json index 831807917a4..acbc70a6fd8 100644 --- a/packages/opentelemetry-resources/package.json +++ b/packages/opentelemetry-resources/package.json @@ -56,6 +56,7 @@ }, "dependencies": { "@opentelemetry/api": "^0.6.1", - "@opentelemetry/base": "^0.6.1" + "@opentelemetry/base": "^0.6.1", + "gcp-metadata": "^3.5.0" } } diff --git a/packages/opentelemetry-resources/src/detectors/GcpDetector.ts b/packages/opentelemetry-resources/src/detectors/GcpDetector.ts new file mode 100644 index 00000000000..fcb2ca9dfe7 --- /dev/null +++ b/packages/opentelemetry-resources/src/detectors/GcpDetector.ts @@ -0,0 +1,101 @@ +/*! + * Copyright 2020, 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 os from 'os'; +import * as gcpMetadata from 'gcp-metadata'; +import { Resource } from '../Resource'; +import { + CLOUD_RESOURCE, + HOST_RESOURCE, + K8S_RESOURCE, + CONTAINER_RESOURCE, +} from '../constants'; + +/** + * The GcpDetector can be used to detect if a process is running in the Google + * Cloud Platofrm and return a {@link Resource} populated with metadata about + * the instance. Returns an empty Resource if detection fails. + */ +export class GcpDetector { + static async detect(): Promise { + const [projectId, instanceId, zoneId, clusterName] = await Promise.all([ + GcpDetector.getProjectId(), + GcpDetector.getInstanceId(), + GcpDetector.getZone(), + GcpDetector.getClusterName(), + ]); + + const labels: { [key: string]: string } = {}; + if (projectId) labels[CLOUD_RESOURCE.ACCOUNT_ID] = projectId; + if (instanceId) labels[HOST_RESOURCE.ID] = instanceId; + if (zoneId) labels[CLOUD_RESOURCE.ZONE] = zoneId; + if (clusterName) GcpDetector.addK8sLabels(labels, clusterName); + if (Object.keys(labels).length > 0) labels[CLOUD_RESOURCE.PROVIDER] = 'gcp'; + + return new Resource(labels); + } + + private static addK8sLabels( + labels: { [key: string]: string }, + clusterName: string + ): void { + labels[K8S_RESOURCE.CLUSTER_NAME] = clusterName; + labels[K8S_RESOURCE.NAMESPACE_NAME] = process.env.NAMESPACE || ''; + labels[K8S_RESOURCE.POD_NAME] = process.env.HOSTNAME || os.hostname(); + labels[CONTAINER_RESOURCE.NAME] = process.env.CONTAINER_NAME || ''; + } + + /** Gets project id from GCP project metadata. */ + private static async getProjectId(): Promise { + try { + return await gcpMetadata.project('project-id'); + } catch { + return ''; + } + } + + /** Gets instance id from GCP instance metadata. */ + private static async getInstanceId(): Promise { + try { + const id = await gcpMetadata.instance('id'); + return id.toString(); + } catch { + return ''; + } + } + + /** Gets zone from GCP instance metadata. */ + private static async getZone(): Promise { + try { + const zoneId = await gcpMetadata.instance('zone'); + if (zoneId) { + return zoneId.split('/').pop(); + } + return ''; + } catch { + return ''; + } + } + + /** Gets cluster name from GCP instance metadata. */ + private static async getClusterName(): Promise { + try { + return await gcpMetadata.instance('attributes/cluster-name'); + } catch { + return ''; + } + } +} diff --git a/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts b/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts new file mode 100644 index 00000000000..cb73d532eaa --- /dev/null +++ b/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts @@ -0,0 +1,170 @@ +/*! + * Copyright 2020, 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 assert from "assert"; +import { + BASE_PATH, + HEADER_NAME, + HEADER_VALUE, + HOST_ADDRESS, +} from 'gcp-metadata'; +import * as nock from 'nock'; +import { Resource } from '../../src'; +import { GcpDetector } from '../../src/detectors/GcpDetector'; +import { + assertCloudResource, + assertHostResource, + assertK8sResource, + assertContainerResource, +} from '../util/resource-assertions'; + +// NOTE: nodejs switches all incoming header names to lower case. +const HEADERS = { + [HEADER_NAME.toLowerCase()]: HEADER_VALUE, +}; +const INSTANCE_ID_PATH = BASE_PATH + '/instance/id'; +const PROJECT_ID_PATH = BASE_PATH + '/project/project-id'; +const ZONE_PATH = BASE_PATH + '/instance/zone'; +const CLUSTER_NAME_PATH = BASE_PATH + '/instance/attributes/cluster-name'; + +describe('GcpDetector', () => { + describe('.detect', () => { + before(() => { + nock.disableNetConnect(); + }); + + after(() => { + nock.enableNetConnect(); + delete process.env.KUBERNETES_SERVICE_HOST; + delete process.env.NAMESPACE; + delete process.env.CONTAINER_NAME; + delete process.env.OC_RESOURCE_TYPE; + delete process.env.OC_RESOURCE_LABELS; + delete process.env.HOSTNAME; + }); + + beforeEach(() => { + nock.cleanAll(); + delete process.env.KUBERNETES_SERVICE_HOST; + delete process.env.NAMESPACE; + delete process.env.CONTAINER_NAME; + delete process.env.OC_RESOURCE_TYPE; + delete process.env.OC_RESOURCE_LABELS; + delete process.env.HOSTNAME; + }); + + describe('when running in GCP', () => { + it('should return resource with GCP metadata', async () => { + const scope = nock(HOST_ADDRESS) + .get(INSTANCE_ID_PATH) + .reply(200, () => 4520031799277581759, HEADERS) + .get(PROJECT_ID_PATH) + .reply(200, () => 'my-project-id', HEADERS) + .get(ZONE_PATH) + .reply(200, () => 'project/zone/my-zone', HEADERS) + .get(CLUSTER_NAME_PATH) + .reply(404); + const resource: Resource = await GcpDetector.detect(); + scope.done(); + + assertCloudResource(resource, { + provider: 'gcp', + accountId: 'my-project-id', + zone: 'my-zone', + }); + assertHostResource(resource, { id: '4520031799277582000' }); + }); + + it('should populate K8s labels resource when KUBERNETES_SERVICE_HOST is set', async () => { + process.env.KUBERNETES_SERVICE_HOST = 'my-host'; + process.env.HOSTNAME = 'my-hostname'; + const scope = nock(HOST_ADDRESS) + .get(INSTANCE_ID_PATH) + .reply(200, () => 4520031799277581759, HEADERS) + .get(CLUSTER_NAME_PATH) + .reply(200, () => 'my-cluster', HEADERS) + .get(PROJECT_ID_PATH) + .reply(200, () => 'my-project-id', HEADERS) + .get(ZONE_PATH) + .reply(200, () => 'project/zone/my-zone', HEADERS); + const resource = await GcpDetector.detect(); + scope.done(); + + assertCloudResource(resource, { + provider: 'gcp', + accountId: 'my-project-id', + zone: 'my-zone', + }); + assertK8sResource(resource, { + clusterName: 'my-cluster', + podName: 'my-hostname', + namespaceName: '', + }); + assertContainerResource(resource, { name: '' }); + }); + + it('should return resource populated with data from KUBERNETES_SERVICE_HOST, NAMESPACE and CONTAINER_NAME', async () => { + process.env.KUBERNETES_SERVICE_HOST = 'my-host'; + process.env.NAMESPACE = 'my-namespace'; + process.env.HOSTNAME = 'my-hostname'; + process.env.CONTAINER_NAME = 'my-container-name'; + const scope = nock(HOST_ADDRESS) + .get(CLUSTER_NAME_PATH) + .reply(200, () => 'my-cluster', HEADERS) + .get(PROJECT_ID_PATH) + .reply(200, () => 'my-project-id', HEADERS) + .get(ZONE_PATH) + .reply(200, () => 'project/zone/my-zone', HEADERS) + .get(INSTANCE_ID_PATH) + .reply(413); + const resource = await GcpDetector.detect(); + scope.done(); + + assertCloudResource(resource, { + provider: 'gcp', + accountId: 'my-project-id', + zone: 'my-zone', + }); + assertK8sResource(resource, { + clusterName: 'my-cluster', + podName: 'my-hostname', + namespaceName: 'my-namespace', + }); + assertContainerResource(resource, { name: 'my-container-name' }); + }); + + it('should return resource and empty data for non avaiable metadata attributes', async () => { + const scope = nock(HOST_ADDRESS) + .get(PROJECT_ID_PATH) + .reply(200, () => 'my-project-id', HEADERS) + .get(ZONE_PATH) + .reply(413) + .get(INSTANCE_ID_PATH) + .reply(400, undefined, HEADERS) + .get(CLUSTER_NAME_PATH) + .reply(413); + const resource = await GcpDetector.detect(); + scope.done(); + + assertCloudResource(resource, { + provider: 'gcp', + accountId: 'my-project-id', + zone: '', + }); + }); + }); + }); +}); From 2842f8e039c53b2a422cf90d20140f554634da81 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Tue, 17 Mar 2020 15:18:43 -0700 Subject: [PATCH 04/20] refactor: create Labels interface --- packages/opentelemetry-resources/src/Resource.ts | 6 +++++- .../opentelemetry-resources/src/detectors/EnvDetector.ts | 8 +++----- .../opentelemetry-resources/src/detectors/GcpDetector.ts | 9 +++------ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/opentelemetry-resources/src/Resource.ts b/packages/opentelemetry-resources/src/Resource.ts index 471e7a943a4..9ea9d02c9f3 100644 --- a/packages/opentelemetry-resources/src/Resource.ts +++ b/packages/opentelemetry-resources/src/Resource.ts @@ -48,7 +48,7 @@ export class Resource { * about the entity as numbers, strings or booleans * TODO: Consider to add check/validation on labels. */ - readonly labels: { [key: string]: number | string | boolean } + readonly labels: Labels ) {} /** @@ -67,3 +67,7 @@ export class Resource { return new Resource(mergedLabels); } } + +export interface Labels { + [key: string]: number | string | boolean; +} diff --git a/packages/opentelemetry-resources/src/detectors/EnvDetector.ts b/packages/opentelemetry-resources/src/detectors/EnvDetector.ts index debdc40c947..05e2a305e5a 100644 --- a/packages/opentelemetry-resources/src/detectors/EnvDetector.ts +++ b/packages/opentelemetry-resources/src/detectors/EnvDetector.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Resource } from '../Resource'; +import { Resource, Labels } from '../Resource'; /** * EnvDetector can be used to detect the presence of and create a Resource @@ -69,12 +69,10 @@ export class EnvDetector { * of key/value pairs. * @returns The sanitized resource labels. */ - private static parseResourceLabels( - rawEnvLabels?: string - ): { [key: string]: string } { + private static parseResourceLabels(rawEnvLabels?: string): Labels { if (!rawEnvLabels) return {}; - const labels: { [key: string]: string } = {}; + const labels: Labels = {}; const rawLabels: string[] = rawEnvLabels.split( EnvDetector.COMMA_SEPARATOR, -1 diff --git a/packages/opentelemetry-resources/src/detectors/GcpDetector.ts b/packages/opentelemetry-resources/src/detectors/GcpDetector.ts index fcb2ca9dfe7..f83bc61e87a 100644 --- a/packages/opentelemetry-resources/src/detectors/GcpDetector.ts +++ b/packages/opentelemetry-resources/src/detectors/GcpDetector.ts @@ -16,7 +16,7 @@ import * as os from 'os'; import * as gcpMetadata from 'gcp-metadata'; -import { Resource } from '../Resource'; +import { Resource, Labels } from '../Resource'; import { CLOUD_RESOURCE, HOST_RESOURCE, @@ -38,7 +38,7 @@ export class GcpDetector { GcpDetector.getClusterName(), ]); - const labels: { [key: string]: string } = {}; + const labels: Labels = {}; if (projectId) labels[CLOUD_RESOURCE.ACCOUNT_ID] = projectId; if (instanceId) labels[HOST_RESOURCE.ID] = instanceId; if (zoneId) labels[CLOUD_RESOURCE.ZONE] = zoneId; @@ -48,10 +48,7 @@ export class GcpDetector { return new Resource(labels); } - private static addK8sLabels( - labels: { [key: string]: string }, - clusterName: string - ): void { + private static addK8sLabels(labels: Labels, clusterName: string): void { labels[K8S_RESOURCE.CLUSTER_NAME] = clusterName; labels[K8S_RESOURCE.NAMESPACE_NAME] = process.env.NAMESPACE || ''; labels[K8S_RESOURCE.POD_NAME] = process.env.HOSTNAME || os.hostname(); From 6325f1f2ac5471d2cc4c0a0597b54d0060ef0b56 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Tue, 17 Mar 2020 21:10:43 -0700 Subject: [PATCH 05/20] feat: add Resource.detect() method --- .../opentelemetry-resources/src/Resource.ts | 17 +++++++++ .../src/detectors/EnvDetector.ts | 5 +-- .../src/detectors/index.ts | 19 ++++++++++ packages/opentelemetry-resources/src/index.ts | 3 +- .../test/Resource.test.ts | 35 +++++++++++++++++-- .../test/detectors/AwsEc2Detector.test.ts | 2 +- .../test/detectors/EnvDetector.test.ts | 8 ++--- 7 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 packages/opentelemetry-resources/src/detectors/index.ts diff --git a/packages/opentelemetry-resources/src/Resource.ts b/packages/opentelemetry-resources/src/Resource.ts index 9ea9d02c9f3..96a7e84b2f5 100644 --- a/packages/opentelemetry-resources/src/Resource.ts +++ b/packages/opentelemetry-resources/src/Resource.ts @@ -16,6 +16,7 @@ import { SDK_INFO } from '@opentelemetry/base'; import { TELEMETRY_SDK_RESOURCE } from './constants'; +import { AwsEc2Detector, EnvDetector, GcpDetector } from './detectors'; /** * A Resource describes the entity for which a signals (metrics or trace) are @@ -42,6 +43,22 @@ export class Resource { }); } + static DETECTORS = [EnvDetector, AwsEc2Detector, GcpDetector]; + + /** + * Runs all resource detectors and returns the results merged into a single + * Resource. + */ + static async detect(detectors = Resource.DETECTORS): Promise { + const resources: Array = await Promise.all( + detectors.map(d => d.detect()) + ); + return resources.reduce( + (acc, resource) => acc.merge(resource), + Resource.createTelemetrySDKResource() + ); + } + constructor( /** * A dictionary of labels with string keys and values that provide information diff --git a/packages/opentelemetry-resources/src/detectors/EnvDetector.ts b/packages/opentelemetry-resources/src/detectors/EnvDetector.ts index 05e2a305e5a..3ae382f224e 100644 --- a/packages/opentelemetry-resources/src/detectors/EnvDetector.ts +++ b/packages/opentelemetry-resources/src/detectors/EnvDetector.ts @@ -42,9 +42,10 @@ export class EnvDetector { /** * Returns a {@link Resource} populated with labels from the - * OTEL_RESOURCE_LABELS environment variable + * OTEL_RESOURCE_LABELS environment variable. Note this is an async function + * to conform to the Detector interface. */ - static detect(): Resource { + static async detect(): Promise { try { const labelString = process.env.OTEL_RESOURCE_LABELS; if (!labelString) return Resource.empty(); diff --git a/packages/opentelemetry-resources/src/detectors/index.ts b/packages/opentelemetry-resources/src/detectors/index.ts new file mode 100644 index 00000000000..bfe49640ced --- /dev/null +++ b/packages/opentelemetry-resources/src/detectors/index.ts @@ -0,0 +1,19 @@ +/*! + * Copyright 2020, 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 { AwsEc2Detector } from './AwsEc2Detector'; +export { EnvDetector } from './EnvDetector'; +export { GcpDetector } from './GcpDetector'; diff --git a/packages/opentelemetry-resources/src/index.ts b/packages/opentelemetry-resources/src/index.ts index c75f0f7daf5..87b1bdf3019 100644 --- a/packages/opentelemetry-resources/src/index.ts +++ b/packages/opentelemetry-resources/src/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ -export { Resource } from './Resource'; +export { Resource, Labels } from './Resource'; +export * from './detectors'; export * from './constants'; diff --git a/packages/opentelemetry-resources/test/Resource.test.ts b/packages/opentelemetry-resources/test/Resource.test.ts index 4dc63a99958..4d4d851f3c6 100644 --- a/packages/opentelemetry-resources/test/Resource.test.ts +++ b/packages/opentelemetry-resources/test/Resource.test.ts @@ -14,10 +14,14 @@ * limitations under the License. */ -import { SDK_INFO } from '@opentelemetry/base'; +import * as nock from 'nock'; import * as assert from 'assert'; -import { Resource } from '../src/Resource'; -import { assertTelemetrySDKResource } from './util/resource-assertions'; +import { SDK_INFO } from '@opentelemetry/base'; +import { Resource, EnvDetector, K8S_RESOURCE } from '../src'; +import { + assertTelemetrySDKResource, + assertK8sResource, +} from './util/resource-assertions'; describe('Resource', () => { const resource1 = new Resource({ @@ -110,4 +114,29 @@ describe('Resource', () => { }); }); }); + + describe('.detect', () => { + before(() => { + process.env.OTEL_RESOURCE_LABELS = + 'k8s.pod.name=my-pod,k8s.cluster.name=my-cluster,k8s.namespace.name=default'; + }); + + after(() => { + delete process.env.OTEL_RESOURCE_LABELS; + }); + + it('returns merged resource', async () => { + const resource = await Resource.detect([EnvDetector]); + assertTelemetrySDKResource(resource, { + language: SDK_INFO.LANGUAGE, + name: SDK_INFO.NAME, + version: SDK_INFO.VERSION, + }); + assertK8sResource(resource, { + podName: 'my-pod', + clusterName: 'my-cluster', + namespaceName: 'default', + }); + }); + }); }); diff --git a/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts b/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts index 44d8951c428..eb483afa9a7 100644 --- a/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts @@ -18,7 +18,7 @@ import * as nock from 'nock'; import * as assert from 'assert'; import { URL } from 'url'; import { Resource } from '../../src'; -import { AwsEc2Detector } from '../../src/detectors/AwsEc2Detector'; +import { AwsEc2Detector } from '../../src'; import { assertCloudResource, assertHostResource, diff --git a/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts b/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts index 32c399dcb34..09c087f28ae 100644 --- a/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts @@ -33,8 +33,8 @@ describe('EnvDetector()', () => { delete process.env.OTEL_RESOURCE_LABELS; }); - it('should return resource information from environment variable', () => { - const resource: Resource = EnvDetector.detect(); + it('should return resource information from environment variable', async () => { + const resource: Resource = await EnvDetector.detect(); assertK8sResource(resource, { [K8S_RESOURCE.POD_NAME]: 'pod-xyz-123', [K8S_RESOURCE.CLUSTER_NAME]: 'c1', @@ -44,8 +44,8 @@ describe('EnvDetector()', () => { }); describe('with empty env', () => { - it('should return empty resource', () => { - const resource: Resource = EnvDetector.detect(); + it('should return empty resource', async () => { + const resource: Resource = await EnvDetector.detect(); assertEmptyResource(resource); }); }); From d0007e7e65671a0a130811a1836fc5718491867e Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Tue, 24 Mar 2020 17:44:48 -0700 Subject: [PATCH 06/20] refactor: improve GcpDetector --- .../src/detectors/GcpDetector.ts | 19 ++- .../test/detectors/GcpDetector.test.ts | 144 +++++++++++------- 2 files changed, 99 insertions(+), 64 deletions(-) diff --git a/packages/opentelemetry-resources/src/detectors/GcpDetector.ts b/packages/opentelemetry-resources/src/detectors/GcpDetector.ts index f83bc61e87a..d0fd14b8fc6 100644 --- a/packages/opentelemetry-resources/src/detectors/GcpDetector.ts +++ b/packages/opentelemetry-resources/src/detectors/GcpDetector.ts @@ -30,7 +30,14 @@ import { * the instance. Returns an empty Resource if detection fails. */ export class GcpDetector { + /** Determine if the GCP metadata server is currently available. */ + static async isRunningOnComputeEngine(): Promise { + return gcpMetadata.isAvailable(); + } static async detect(): Promise { + const isRunning = await GcpDetector.isRunningOnComputeEngine(); + if (!isRunning) return Resource.empty(); + const [projectId, instanceId, zoneId, clusterName] = await Promise.all([ GcpDetector.getProjectId(), GcpDetector.getInstanceId(), @@ -39,11 +46,13 @@ export class GcpDetector { ]); const labels: Labels = {}; - if (projectId) labels[CLOUD_RESOURCE.ACCOUNT_ID] = projectId; - if (instanceId) labels[HOST_RESOURCE.ID] = instanceId; - if (zoneId) labels[CLOUD_RESOURCE.ZONE] = zoneId; - if (clusterName) GcpDetector.addK8sLabels(labels, clusterName); - if (Object.keys(labels).length > 0) labels[CLOUD_RESOURCE.PROVIDER] = 'gcp'; + labels[CLOUD_RESOURCE.ACCOUNT_ID] = projectId; + labels[HOST_RESOURCE.ID] = instanceId; + labels[CLOUD_RESOURCE.ZONE] = zoneId; + labels[CLOUD_RESOURCE.PROVIDER] = 'gcp'; + + if (process.env.KUBERNETES_SERVICE_HOST) + GcpDetector.addK8sLabels(labels, clusterName); return new Resource(labels); } diff --git a/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts b/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts index cb73d532eaa..bbf17a4f3e3 100644 --- a/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts @@ -14,34 +14,37 @@ * limitations under the License. */ -//import * as assert from "assert"; import { BASE_PATH, HEADER_NAME, HEADER_VALUE, HOST_ADDRESS, -} from 'gcp-metadata'; -import * as nock from 'nock'; -import { Resource } from '../../src'; -import { GcpDetector } from '../../src/detectors/GcpDetector'; + SECONDARY_HOST_ADDRESS, + resetIsAvailableCache +} from "gcp-metadata"; +import * as nock from "nock"; +import { Resource } from "../../src"; +import { GcpDetector } from "../../src"; import { assertCloudResource, assertHostResource, assertK8sResource, assertContainerResource, -} from '../util/resource-assertions'; + assertEmptyResource +} from "../util/resource-assertions"; // NOTE: nodejs switches all incoming header names to lower case. const HEADERS = { - [HEADER_NAME.toLowerCase()]: HEADER_VALUE, + [HEADER_NAME.toLowerCase()]: HEADER_VALUE }; -const INSTANCE_ID_PATH = BASE_PATH + '/instance/id'; -const PROJECT_ID_PATH = BASE_PATH + '/project/project-id'; -const ZONE_PATH = BASE_PATH + '/instance/zone'; -const CLUSTER_NAME_PATH = BASE_PATH + '/instance/attributes/cluster-name'; +const INSTANCE_PATH = BASE_PATH + "/instance"; +const INSTANCE_ID_PATH = BASE_PATH + "/instance/id"; +const PROJECT_ID_PATH = BASE_PATH + "/project/project-id"; +const ZONE_PATH = BASE_PATH + "/instance/zone"; +const CLUSTER_NAME_PATH = BASE_PATH + "/instance/attributes/cluster-name"; -describe('GcpDetector', () => { - describe('.detect', () => { +describe("GcpDetector", () => { + describe(".detect", () => { before(() => { nock.disableNetConnect(); }); @@ -57,6 +60,7 @@ describe('GcpDetector', () => { }); beforeEach(() => { + resetIsAvailableCache(); nock.cleanAll(); delete process.env.KUBERNETES_SERVICE_HOST; delete process.env.NAMESPACE; @@ -66,104 +70,126 @@ describe('GcpDetector', () => { delete process.env.HOSTNAME; }); - describe('when running in GCP', () => { - it('should return resource with GCP metadata', async () => { + describe("when running in GCP", () => { + it("should return resource with GCP metadata", async () => { const scope = nock(HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS) .get(INSTANCE_ID_PATH) .reply(200, () => 4520031799277581759, HEADERS) .get(PROJECT_ID_PATH) - .reply(200, () => 'my-project-id', HEADERS) + .reply(200, () => "my-project-id", HEADERS) .get(ZONE_PATH) - .reply(200, () => 'project/zone/my-zone', HEADERS) + .reply(200, () => "project/zone/my-zone", HEADERS) .get(CLUSTER_NAME_PATH) .reply(404); + const secondaryScope = nock(SECONDARY_HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS); const resource: Resource = await GcpDetector.detect(); + secondaryScope.done(); scope.done(); assertCloudResource(resource, { - provider: 'gcp', - accountId: 'my-project-id', - zone: 'my-zone', + provider: "gcp", + accountId: "my-project-id", + zone: "my-zone" }); - assertHostResource(resource, { id: '4520031799277582000' }); + assertHostResource(resource, { id: "4520031799277582000" }); }); - it('should populate K8s labels resource when KUBERNETES_SERVICE_HOST is set', async () => { - process.env.KUBERNETES_SERVICE_HOST = 'my-host'; - process.env.HOSTNAME = 'my-hostname'; + it("should populate K8s labels resource when KUBERNETES_SERVICE_HOST is set", async () => { + process.env.KUBERNETES_SERVICE_HOST = "my-host"; + process.env.NAMESPACE = "my-namespace"; + process.env.HOSTNAME = "my-hostname"; + process.env.CONTAINER_NAME = "my-container-name"; const scope = nock(HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS) .get(INSTANCE_ID_PATH) .reply(200, () => 4520031799277581759, HEADERS) .get(CLUSTER_NAME_PATH) - .reply(200, () => 'my-cluster', HEADERS) + .reply(200, () => "my-cluster", HEADERS) .get(PROJECT_ID_PATH) - .reply(200, () => 'my-project-id', HEADERS) + .reply(200, () => "my-project-id", HEADERS) .get(ZONE_PATH) - .reply(200, () => 'project/zone/my-zone', HEADERS); + .reply(200, () => "project/zone/my-zone", HEADERS); + const secondaryScope = nock(SECONDARY_HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS); const resource = await GcpDetector.detect(); + secondaryScope.done(); scope.done(); assertCloudResource(resource, { - provider: 'gcp', - accountId: 'my-project-id', - zone: 'my-zone', + provider: "gcp", + accountId: "my-project-id", + zone: "my-zone" }); assertK8sResource(resource, { - clusterName: 'my-cluster', - podName: 'my-hostname', - namespaceName: '', + clusterName: "my-cluster", + podName: "my-hostname", + namespaceName: "my-namespace" }); - assertContainerResource(resource, { name: '' }); + assertContainerResource(resource, { name: "my-container-name" }); }); - it('should return resource populated with data from KUBERNETES_SERVICE_HOST, NAMESPACE and CONTAINER_NAME', async () => { - process.env.KUBERNETES_SERVICE_HOST = 'my-host'; - process.env.NAMESPACE = 'my-namespace'; - process.env.HOSTNAME = 'my-hostname'; - process.env.CONTAINER_NAME = 'my-container-name'; + it("should return resource and empty data for non-available metadata attributes", async () => { const scope = nock(HOST_ADDRESS) - .get(CLUSTER_NAME_PATH) - .reply(200, () => 'my-cluster', HEADERS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS) .get(PROJECT_ID_PATH) - .reply(200, () => 'my-project-id', HEADERS) + .reply(200, () => "my-project-id", HEADERS) .get(ZONE_PATH) - .reply(200, () => 'project/zone/my-zone', HEADERS) + .reply(413) .get(INSTANCE_ID_PATH) + .reply(400, undefined, HEADERS) + .get(CLUSTER_NAME_PATH) .reply(413); + const secondaryScope = nock(SECONDARY_HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS); const resource = await GcpDetector.detect(); + secondaryScope.done(); scope.done(); assertCloudResource(resource, { - provider: 'gcp', - accountId: 'my-project-id', - zone: 'my-zone', + provider: "gcp", + accountId: "my-project-id", + zone: "" }); - assertK8sResource(resource, { - clusterName: 'my-cluster', - podName: 'my-hostname', - namespaceName: 'my-namespace', - }); - assertContainerResource(resource, { name: 'my-container-name' }); }); - it('should return resource and empty data for non avaiable metadata attributes', async () => { + it("should retry if the initial request fails", async () => { const scope = nock(HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(500) .get(PROJECT_ID_PATH) - .reply(200, () => 'my-project-id', HEADERS) + .reply(200, () => "my-project-id", HEADERS) .get(ZONE_PATH) - .reply(413) + .reply(200, () => "project/zone/my-zone", HEADERS) .get(INSTANCE_ID_PATH) - .reply(400, undefined, HEADERS) + .reply(200, () => 4520031799277581759, HEADERS) .get(CLUSTER_NAME_PATH) .reply(413); + const secondaryScope = nock(SECONDARY_HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS); const resource = await GcpDetector.detect(); + secondaryScope.done(); scope.done(); assertCloudResource(resource, { - provider: 'gcp', - accountId: 'my-project-id', - zone: '', + accountId: "my-project-id", + zone: "my-zone" }); + + assertHostResource(resource, { id: "4520031799277582000" }); + }); + + it("returns empty resource if not detected", async () => { + const resource = await GcpDetector.detect(); + assertEmptyResource(resource); }); }); }); From 9d540360f3a7b580bd173ad315bc374ead33fe09 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Tue, 24 Mar 2020 21:19:11 -0700 Subject: [PATCH 07/20] feat: make detection platform aware; extract to detectResources() fn --- packages/opentelemetry-resources/package.json | 4 + .../opentelemetry-resources/src/Resource.ts | 17 --- packages/opentelemetry-resources/src/index.ts | 2 +- .../src/platform/browser/detect-resources.ts | 26 ++++ .../src/platform/browser/index.ts | 17 +++ .../src/platform/index.ts | 20 +++ .../src/platform/node/detect-resources.ts | 34 +++++ .../node}/detectors/AwsEc2Detector.ts | 4 +- .../node}/detectors/EnvDetector.ts | 2 +- .../node}/detectors/GcpDetector.ts | 4 +- .../{ => platform/node}/detectors/index.ts | 0 .../src/platform/node/index.ts | 18 +++ .../test/Resource.test.ts | 33 +---- .../test/detect-resources.test.ts | 139 ++++++++++++++++++ .../test/detectors/AwsEc2Detector.test.ts | 2 +- .../test/detectors/EnvDetector.test.ts | 2 +- .../test/detectors/GcpDetector.test.ts | 100 ++++++------- 17 files changed, 318 insertions(+), 106 deletions(-) create mode 100644 packages/opentelemetry-resources/src/platform/browser/detect-resources.ts create mode 100644 packages/opentelemetry-resources/src/platform/browser/index.ts create mode 100644 packages/opentelemetry-resources/src/platform/index.ts create mode 100644 packages/opentelemetry-resources/src/platform/node/detect-resources.ts rename packages/opentelemetry-resources/src/{ => platform/node}/detectors/AwsEc2Detector.ts (96%) rename packages/opentelemetry-resources/src/{ => platform/node}/detectors/EnvDetector.ts (98%) rename packages/opentelemetry-resources/src/{ => platform/node}/detectors/GcpDetector.ts (97%) rename packages/opentelemetry-resources/src/{ => platform/node}/detectors/index.ts (100%) create mode 100644 packages/opentelemetry-resources/src/platform/node/index.ts create mode 100644 packages/opentelemetry-resources/test/detect-resources.test.ts diff --git a/packages/opentelemetry-resources/package.json b/packages/opentelemetry-resources/package.json index acbc70a6fd8..dc7fce96b9a 100644 --- a/packages/opentelemetry-resources/package.json +++ b/packages/opentelemetry-resources/package.json @@ -3,6 +3,10 @@ "version": "0.6.1", "description": "OpenTelemetry SDK resources", "main": "build/src/index.js", + "browser": { + "./src/platform/index.ts": "./src/platform/browser/index.ts", + "./build/src/platform/index.js": "./build/src/platform/browser/index.js" + }, "types": "build/src/index.d.ts", "repository": "open-telemetry/opentelemetry-js", "scripts": { diff --git a/packages/opentelemetry-resources/src/Resource.ts b/packages/opentelemetry-resources/src/Resource.ts index 96a7e84b2f5..9ea9d02c9f3 100644 --- a/packages/opentelemetry-resources/src/Resource.ts +++ b/packages/opentelemetry-resources/src/Resource.ts @@ -16,7 +16,6 @@ import { SDK_INFO } from '@opentelemetry/base'; import { TELEMETRY_SDK_RESOURCE } from './constants'; -import { AwsEc2Detector, EnvDetector, GcpDetector } from './detectors'; /** * A Resource describes the entity for which a signals (metrics or trace) are @@ -43,22 +42,6 @@ export class Resource { }); } - static DETECTORS = [EnvDetector, AwsEc2Detector, GcpDetector]; - - /** - * Runs all resource detectors and returns the results merged into a single - * Resource. - */ - static async detect(detectors = Resource.DETECTORS): Promise { - const resources: Array = await Promise.all( - detectors.map(d => d.detect()) - ); - return resources.reduce( - (acc, resource) => acc.merge(resource), - Resource.createTelemetrySDKResource() - ); - } - constructor( /** * A dictionary of labels with string keys and values that provide information diff --git a/packages/opentelemetry-resources/src/index.ts b/packages/opentelemetry-resources/src/index.ts index 87b1bdf3019..22e460cf85a 100644 --- a/packages/opentelemetry-resources/src/index.ts +++ b/packages/opentelemetry-resources/src/index.ts @@ -15,5 +15,5 @@ */ export { Resource, Labels } from './Resource'; -export * from './detectors'; +export * from './platform'; export * from './constants'; diff --git a/packages/opentelemetry-resources/src/platform/browser/detect-resources.ts b/packages/opentelemetry-resources/src/platform/browser/detect-resources.ts new file mode 100644 index 00000000000..43db76639ab --- /dev/null +++ b/packages/opentelemetry-resources/src/platform/browser/detect-resources.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2020, 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'; + +/** + * Detects resources for the browser platform, which is currently only the + * telemetry SDK resource. More could be added in the future. This method + * is async to match the signature of corresponding method for node. + */ +export const detectResources = async (): Promise => { + return Resource.createTelemetrySDKResource(); +}; diff --git a/packages/opentelemetry-resources/src/platform/browser/index.ts b/packages/opentelemetry-resources/src/platform/browser/index.ts new file mode 100644 index 00000000000..560cdcf8219 --- /dev/null +++ b/packages/opentelemetry-resources/src/platform/browser/index.ts @@ -0,0 +1,17 @@ +/*! + * Copyright 2020, 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 * from './detect-resources'; diff --git a/packages/opentelemetry-resources/src/platform/index.ts b/packages/opentelemetry-resources/src/platform/index.ts new file mode 100644 index 00000000000..6c6d039c914 --- /dev/null +++ b/packages/opentelemetry-resources/src/platform/index.ts @@ -0,0 +1,20 @@ +/*! + * Copyright 2020, 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. + */ + +// Use the node platform by default. The "browser" field of package.json is used +// to override this file to use `./browser/index.ts` when packaged with +// webpack, Rollup, etc. +export * from './node'; diff --git a/packages/opentelemetry-resources/src/platform/node/detect-resources.ts b/packages/opentelemetry-resources/src/platform/node/detect-resources.ts new file mode 100644 index 00000000000..fef4bf61863 --- /dev/null +++ b/packages/opentelemetry-resources/src/platform/node/detect-resources.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2020, 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 { EnvDetector, AwsEc2Detector, GcpDetector } from './detectors'; + +const DETECTORS = [EnvDetector, AwsEc2Detector, GcpDetector]; + +/** + * Runs all resource detectors and returns the results merged into a single + * Resource. + */ +export const detectResources = async (): Promise => { + const resources: Array = await Promise.all( + DETECTORS.map(d => d.detect()) + ); + return resources.reduce( + (acc, resource) => acc.merge(resource), + Resource.createTelemetrySDKResource() + ); +}; diff --git a/packages/opentelemetry-resources/src/detectors/AwsEc2Detector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts similarity index 96% rename from packages/opentelemetry-resources/src/detectors/AwsEc2Detector.ts rename to packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts index 99ddbf11c28..e8f7cd3187e 100644 --- a/packages/opentelemetry-resources/src/detectors/AwsEc2Detector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts @@ -15,8 +15,8 @@ */ import * as http from 'http'; -import { Resource } from '../Resource'; -import { CLOUD_RESOURCE, HOST_RESOURCE } from '../constants'; +import { Resource } from '../../../Resource'; +import { CLOUD_RESOURCE, HOST_RESOURCE } from '../../../constants'; /** * The AwsEc2Detector can be used to detect if a process is running in AWS EC2 diff --git a/packages/opentelemetry-resources/src/detectors/EnvDetector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts similarity index 98% rename from packages/opentelemetry-resources/src/detectors/EnvDetector.ts rename to packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts index 3ae382f224e..0a6b6e01fd5 100644 --- a/packages/opentelemetry-resources/src/detectors/EnvDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Resource, Labels } from '../Resource'; +import { Resource, Labels } from '../../../Resource'; /** * EnvDetector can be used to detect the presence of and create a Resource diff --git a/packages/opentelemetry-resources/src/detectors/GcpDetector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts similarity index 97% rename from packages/opentelemetry-resources/src/detectors/GcpDetector.ts rename to packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts index d0fd14b8fc6..a4521fc67dc 100644 --- a/packages/opentelemetry-resources/src/detectors/GcpDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts @@ -16,13 +16,13 @@ import * as os from 'os'; import * as gcpMetadata from 'gcp-metadata'; -import { Resource, Labels } from '../Resource'; +import { Resource, Labels } from '../../../Resource'; import { CLOUD_RESOURCE, HOST_RESOURCE, K8S_RESOURCE, CONTAINER_RESOURCE, -} from '../constants'; +} from '../../../constants'; /** * The GcpDetector can be used to detect if a process is running in the Google diff --git a/packages/opentelemetry-resources/src/detectors/index.ts b/packages/opentelemetry-resources/src/platform/node/detectors/index.ts similarity index 100% rename from packages/opentelemetry-resources/src/detectors/index.ts rename to packages/opentelemetry-resources/src/platform/node/detectors/index.ts diff --git a/packages/opentelemetry-resources/src/platform/node/index.ts b/packages/opentelemetry-resources/src/platform/node/index.ts new file mode 100644 index 00000000000..d257f6e05f0 --- /dev/null +++ b/packages/opentelemetry-resources/src/platform/node/index.ts @@ -0,0 +1,18 @@ +/*! + * Copyright 2020, 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 * from './detect-resources'; +export * from './detectors'; diff --git a/packages/opentelemetry-resources/test/Resource.test.ts b/packages/opentelemetry-resources/test/Resource.test.ts index 4d4d851f3c6..2623094817d 100644 --- a/packages/opentelemetry-resources/test/Resource.test.ts +++ b/packages/opentelemetry-resources/test/Resource.test.ts @@ -14,14 +14,10 @@ * limitations under the License. */ -import * as nock from 'nock'; import * as assert from 'assert'; import { SDK_INFO } from '@opentelemetry/base'; -import { Resource, EnvDetector, K8S_RESOURCE } from '../src'; -import { - assertTelemetrySDKResource, - assertK8sResource, -} from './util/resource-assertions'; +import { Resource } from '../src'; +import { assertTelemetrySDKResource } from './util/resource-assertions'; describe('Resource', () => { const resource1 = new Resource({ @@ -114,29 +110,4 @@ describe('Resource', () => { }); }); }); - - describe('.detect', () => { - before(() => { - process.env.OTEL_RESOURCE_LABELS = - 'k8s.pod.name=my-pod,k8s.cluster.name=my-cluster,k8s.namespace.name=default'; - }); - - after(() => { - delete process.env.OTEL_RESOURCE_LABELS; - }); - - it('returns merged resource', async () => { - const resource = await Resource.detect([EnvDetector]); - assertTelemetrySDKResource(resource, { - language: SDK_INFO.LANGUAGE, - name: SDK_INFO.NAME, - version: SDK_INFO.VERSION, - }); - assertK8sResource(resource, { - podName: 'my-pod', - clusterName: 'my-cluster', - namespaceName: 'default', - }); - }); - }); }); 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 00000000000..47d4524e744 --- /dev/null +++ b/packages/opentelemetry-resources/test/detect-resources.test.ts @@ -0,0 +1,139 @@ +/*! + * Copyright 2020, 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 { Resource } from '../src'; +import { detectResources, AwsEc2Detector } from '../src'; +import { + assertServiceResource, + assertCloudResource, + assertHostResource, +} from './util/resource-assertions'; +import { + BASE_PATH, + HEADER_NAME, + HEADER_VALUE, + HOST_ADDRESS, + SECONDARY_HOST_ADDRESS, + resetIsAvailableCache, +} from 'gcp-metadata'; + +const HEADERS = { + [HEADER_NAME.toLowerCase()]: HEADER_VALUE, +}; +const INSTANCE_PATH = BASE_PATH + '/instance'; +const INSTANCE_ID_PATH = BASE_PATH + '/instance/id'; +const PROJECT_ID_PATH = BASE_PATH + '/project/project-id'; +const ZONE_PATH = BASE_PATH + '/instance/zone'; +const CLUSTER_NAME_PATH = BASE_PATH + '/instance/attributes/cluster-name'; + +const { origin: AWS_HOST, pathname: AWS_PATH } = new URL( + AwsEc2Detector.AWS_INSTANCE_IDENTITY_DOCUMENT_URI +); + +const mockedAwsResponse = { + instanceId: 'my-instance-id', + accountId: 'my-account-id', + region: 'my-region', +}; + +describe('detectResource', async () => { + before(() => { + nock.disableNetConnect(); + process.env.OTEL_RESOURCE_LABELS = + 'service.instance.id=627cc493,service.name=my-service,service.namespace=default,service.version=0.0.1'; + }); + + after(() => { + nock.cleanAll(); + nock.enableNetConnect(); + delete process.env.OTEL_RESOURCE_LABELS; + }); + + describe('in GCP environment', () => { + after(() => { + resetIsAvailableCache(); + }); + + it('returns a merged resource', async () => { + const scope = nock(HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS) + .get(INSTANCE_ID_PATH) + .reply(200, () => 452003179927758, HEADERS) + .get(PROJECT_ID_PATH) + .reply(200, () => 'my-project-id', HEADERS) + .get(ZONE_PATH) + .reply(200, () => 'project/zone/my-zone', HEADERS) + .get(CLUSTER_NAME_PATH) + .reply(404); + const secondaryScope = nock(SECONDARY_HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS); + const resource: Resource = await detectResources(); + secondaryScope.done(); + scope.done(); + + assertCloudResource(resource, { + provider: 'gcp', + accountId: 'my-project-id', + zone: 'my-zone', + }); + assertHostResource(resource, { id: '452003179927758' }); + assertServiceResource(resource, { + instanceId: '627cc493', + name: 'my-service', + namespace: 'default', + version: '0.0.1', + }); + }); + }); + + describe('in AWS environment', () => { + it('returns a merged resource', async () => { + const gcpScope = nock(HOST_ADDRESS) + .get(INSTANCE_PATH) + .replyWithError({ + code: 'ENOTFOUND', + }); + const gcpSecondaryScope = nock(SECONDARY_HOST_ADDRESS) + .get(INSTANCE_PATH) + .replyWithError({ + code: 'ENOTFOUND', + }); + const awsScope = nock(AWS_HOST) + .get(AWS_PATH) + .reply(200, () => mockedAwsResponse); + const resource: Resource = await detectResources(); + gcpSecondaryScope.done(); + gcpScope.done(); + awsScope.done(); + + assertCloudResource(resource, { + provider: 'aws', + accountId: 'my-account-id', + region: 'my-region', + }); + assertHostResource(resource, { id: 'my-instance-id' }); + assertServiceResource(resource, { + instanceId: '627cc493', + name: 'my-service', + namespace: 'default', + version: '0.0.1', + }); + }); + }); +}); diff --git a/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts b/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts index eb483afa9a7..ede409d4c65 100644 --- a/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts @@ -18,7 +18,7 @@ import * as nock from 'nock'; import * as assert from 'assert'; import { URL } from 'url'; import { Resource } from '../../src'; -import { AwsEc2Detector } from '../../src'; +import { AwsEc2Detector } from '../../src/platform/node/detectors/AwsEc2Detector'; import { assertCloudResource, assertHostResource, diff --git a/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts b/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts index 09c087f28ae..388947f8d57 100644 --- a/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts @@ -15,7 +15,7 @@ */ import { Resource } from '../../src/Resource'; -import { EnvDetector } from '../../src/detectors/EnvDetector'; +import { EnvDetector } from '../../src/platform/node/detectors/EnvDetector'; import { assertK8sResource, assertEmptyResource, diff --git a/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts b/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts index bbf17a4f3e3..c95b46a67f5 100644 --- a/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts @@ -20,31 +20,31 @@ import { HEADER_VALUE, HOST_ADDRESS, SECONDARY_HOST_ADDRESS, - resetIsAvailableCache -} from "gcp-metadata"; -import * as nock from "nock"; -import { Resource } from "../../src"; -import { GcpDetector } from "../../src"; + resetIsAvailableCache, +} from 'gcp-metadata'; +import * as nock from 'nock'; +import { Resource } from '../../src'; +import { GcpDetector } from '../../src/platform/node/detectors'; import { assertCloudResource, assertHostResource, assertK8sResource, assertContainerResource, - assertEmptyResource -} from "../util/resource-assertions"; + assertEmptyResource, +} from '../util/resource-assertions'; // NOTE: nodejs switches all incoming header names to lower case. const HEADERS = { - [HEADER_NAME.toLowerCase()]: HEADER_VALUE + [HEADER_NAME.toLowerCase()]: HEADER_VALUE, }; -const INSTANCE_PATH = BASE_PATH + "/instance"; -const INSTANCE_ID_PATH = BASE_PATH + "/instance/id"; -const PROJECT_ID_PATH = BASE_PATH + "/project/project-id"; -const ZONE_PATH = BASE_PATH + "/instance/zone"; -const CLUSTER_NAME_PATH = BASE_PATH + "/instance/attributes/cluster-name"; +const INSTANCE_PATH = BASE_PATH + '/instance'; +const INSTANCE_ID_PATH = BASE_PATH + '/instance/id'; +const PROJECT_ID_PATH = BASE_PATH + '/project/project-id'; +const ZONE_PATH = BASE_PATH + '/instance/zone'; +const CLUSTER_NAME_PATH = BASE_PATH + '/instance/attributes/cluster-name'; -describe("GcpDetector", () => { - describe(".detect", () => { +describe('GcpDetector', () => { + describe('.detect', () => { before(() => { nock.disableNetConnect(); }); @@ -70,17 +70,17 @@ describe("GcpDetector", () => { delete process.env.HOSTNAME; }); - describe("when running in GCP", () => { - it("should return resource with GCP metadata", async () => { + describe('when running in GCP', () => { + it('should return resource with GCP metadata', async () => { const scope = nock(HOST_ADDRESS) .get(INSTANCE_PATH) .reply(200, {}, HEADERS) .get(INSTANCE_ID_PATH) .reply(200, () => 4520031799277581759, HEADERS) .get(PROJECT_ID_PATH) - .reply(200, () => "my-project-id", HEADERS) + .reply(200, () => 'my-project-id', HEADERS) .get(ZONE_PATH) - .reply(200, () => "project/zone/my-zone", HEADERS) + .reply(200, () => 'project/zone/my-zone', HEADERS) .get(CLUSTER_NAME_PATH) .reply(404); const secondaryScope = nock(SECONDARY_HOST_ADDRESS) @@ -91,29 +91,29 @@ describe("GcpDetector", () => { scope.done(); assertCloudResource(resource, { - provider: "gcp", - accountId: "my-project-id", - zone: "my-zone" + provider: 'gcp', + accountId: 'my-project-id', + zone: 'my-zone', }); - assertHostResource(resource, { id: "4520031799277582000" }); + assertHostResource(resource, { id: '4520031799277582000' }); }); - it("should populate K8s labels resource when KUBERNETES_SERVICE_HOST is set", async () => { - process.env.KUBERNETES_SERVICE_HOST = "my-host"; - process.env.NAMESPACE = "my-namespace"; - process.env.HOSTNAME = "my-hostname"; - process.env.CONTAINER_NAME = "my-container-name"; + it('should populate K8s labels resource when KUBERNETES_SERVICE_HOST is set', async () => { + process.env.KUBERNETES_SERVICE_HOST = 'my-host'; + process.env.NAMESPACE = 'my-namespace'; + process.env.HOSTNAME = 'my-hostname'; + process.env.CONTAINER_NAME = 'my-container-name'; const scope = nock(HOST_ADDRESS) .get(INSTANCE_PATH) .reply(200, {}, HEADERS) .get(INSTANCE_ID_PATH) .reply(200, () => 4520031799277581759, HEADERS) .get(CLUSTER_NAME_PATH) - .reply(200, () => "my-cluster", HEADERS) + .reply(200, () => 'my-cluster', HEADERS) .get(PROJECT_ID_PATH) - .reply(200, () => "my-project-id", HEADERS) + .reply(200, () => 'my-project-id', HEADERS) .get(ZONE_PATH) - .reply(200, () => "project/zone/my-zone", HEADERS); + .reply(200, () => 'project/zone/my-zone', HEADERS); const secondaryScope = nock(SECONDARY_HOST_ADDRESS) .get(INSTANCE_PATH) .reply(200, {}, HEADERS); @@ -122,24 +122,24 @@ describe("GcpDetector", () => { scope.done(); assertCloudResource(resource, { - provider: "gcp", - accountId: "my-project-id", - zone: "my-zone" + provider: 'gcp', + accountId: 'my-project-id', + zone: 'my-zone', }); assertK8sResource(resource, { - clusterName: "my-cluster", - podName: "my-hostname", - namespaceName: "my-namespace" + clusterName: 'my-cluster', + podName: 'my-hostname', + namespaceName: 'my-namespace', }); - assertContainerResource(resource, { name: "my-container-name" }); + assertContainerResource(resource, { name: 'my-container-name' }); }); - it("should return resource and empty data for non-available metadata attributes", async () => { + it('should return resource and empty data for non-available metadata attributes', async () => { const scope = nock(HOST_ADDRESS) .get(INSTANCE_PATH) .reply(200, {}, HEADERS) .get(PROJECT_ID_PATH) - .reply(200, () => "my-project-id", HEADERS) + .reply(200, () => 'my-project-id', HEADERS) .get(ZONE_PATH) .reply(413) .get(INSTANCE_ID_PATH) @@ -154,20 +154,20 @@ describe("GcpDetector", () => { scope.done(); assertCloudResource(resource, { - provider: "gcp", - accountId: "my-project-id", - zone: "" + provider: 'gcp', + accountId: 'my-project-id', + zone: '', }); }); - it("should retry if the initial request fails", async () => { + it('should retry if the initial request fails', async () => { const scope = nock(HOST_ADDRESS) .get(INSTANCE_PATH) .reply(500) .get(PROJECT_ID_PATH) - .reply(200, () => "my-project-id", HEADERS) + .reply(200, () => 'my-project-id', HEADERS) .get(ZONE_PATH) - .reply(200, () => "project/zone/my-zone", HEADERS) + .reply(200, () => 'project/zone/my-zone', HEADERS) .get(INSTANCE_ID_PATH) .reply(200, () => 4520031799277581759, HEADERS) .get(CLUSTER_NAME_PATH) @@ -180,14 +180,14 @@ describe("GcpDetector", () => { scope.done(); assertCloudResource(resource, { - accountId: "my-project-id", - zone: "my-zone" + accountId: 'my-project-id', + zone: 'my-zone', }); - assertHostResource(resource, { id: "4520031799277582000" }); + assertHostResource(resource, { id: '4520031799277582000' }); }); - it("returns empty resource if not detected", async () => { + it('returns empty resource if not detected', async () => { const resource = await GcpDetector.detect(); assertEmptyResource(resource); }); From 4ea66ecb16b391dfde0d6b21b22d123cbc150357 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Tue, 24 Mar 2020 21:35:17 -0700 Subject: [PATCH 08/20] chore: cleanup --- .../platform/node/detectors/EnvDetector.ts | 4 +- .../platform/node/detectors/GcpDetector.ts | 8 +- .../test/detect-resources.test.ts | 2 +- .../test/detectors/GcpDetector.test.ts | 221 +++++++++--------- 4 files changed, 112 insertions(+), 123 deletions(-) diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts index 0a6b6e01fd5..0008d4f4806 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts @@ -59,9 +59,9 @@ export class EnvDetector { } /** - * Creates a label map from the OC_RESOURCE_LABELS environment variable. + * Creates a label map from the OTEL_RESOURCE_LABELS environment variable. * - * OC_RESOURCE_LABELS: A comma-separated list of labels describing the + * OTEL_RESOURCE_LABELS: A comma-separated list of labels describing the * source in more detail, e.g. “key1=val1,key2=val2”. Domain names and paths * are accepted as label keys. Values may be quoted or unquoted in general. If * a value contains whitespaces, =, or " characters, it must always be quoted. diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts index a4521fc67dc..274ec7c6c36 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts @@ -30,13 +30,8 @@ import { * the instance. Returns an empty Resource if detection fails. */ export class GcpDetector { - /** Determine if the GCP metadata server is currently available. */ - static async isRunningOnComputeEngine(): Promise { - return gcpMetadata.isAvailable(); - } static async detect(): Promise { - const isRunning = await GcpDetector.isRunningOnComputeEngine(); - if (!isRunning) return Resource.empty(); + if (!(await gcpMetadata.isAvailable())) return Resource.empty(); const [projectId, instanceId, zoneId, clusterName] = await Promise.all([ GcpDetector.getProjectId(), @@ -57,6 +52,7 @@ export class GcpDetector { return new Resource(labels); } + /** Add resource labels for K8s */ private static addK8sLabels(labels: Labels, clusterName: string): void { labels[K8S_RESOURCE.CLUSTER_NAME] = clusterName; labels[K8S_RESOURCE.NAMESPACE_NAME] = process.env.NAMESPACE || ''; diff --git a/packages/opentelemetry-resources/test/detect-resources.test.ts b/packages/opentelemetry-resources/test/detect-resources.test.ts index 47d4524e744..0537f9698e0 100644 --- a/packages/opentelemetry-resources/test/detect-resources.test.ts +++ b/packages/opentelemetry-resources/test/detect-resources.test.ts @@ -50,7 +50,7 @@ const mockedAwsResponse = { region: 'my-region', }; -describe('detectResource', async () => { +describe('detectResources', async () => { before(() => { nock.disableNetConnect(); process.env.OTEL_RESOURCE_LABELS = diff --git a/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts b/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts index c95b46a67f5..8a504b5f276 100644 --- a/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts @@ -33,7 +33,6 @@ import { assertEmptyResource, } from '../util/resource-assertions'; -// NOTE: nodejs switches all incoming header names to lower case. const HEADERS = { [HEADER_NAME.toLowerCase()]: HEADER_VALUE, }; @@ -54,8 +53,6 @@ describe('GcpDetector', () => { delete process.env.KUBERNETES_SERVICE_HOST; delete process.env.NAMESPACE; delete process.env.CONTAINER_NAME; - delete process.env.OC_RESOURCE_TYPE; - delete process.env.OC_RESOURCE_LABELS; delete process.env.HOSTNAME; }); @@ -65,132 +62,128 @@ describe('GcpDetector', () => { delete process.env.KUBERNETES_SERVICE_HOST; delete process.env.NAMESPACE; delete process.env.CONTAINER_NAME; - delete process.env.OC_RESOURCE_TYPE; - delete process.env.OC_RESOURCE_LABELS; delete process.env.HOSTNAME; }); - describe('when running in GCP', () => { - it('should return resource with GCP metadata', async () => { - const scope = nock(HOST_ADDRESS) - .get(INSTANCE_PATH) - .reply(200, {}, HEADERS) - .get(INSTANCE_ID_PATH) - .reply(200, () => 4520031799277581759, HEADERS) - .get(PROJECT_ID_PATH) - .reply(200, () => 'my-project-id', HEADERS) - .get(ZONE_PATH) - .reply(200, () => 'project/zone/my-zone', HEADERS) - .get(CLUSTER_NAME_PATH) - .reply(404); - const secondaryScope = nock(SECONDARY_HOST_ADDRESS) - .get(INSTANCE_PATH) - .reply(200, {}, HEADERS); - const resource: Resource = await GcpDetector.detect(); - secondaryScope.done(); - scope.done(); + it('should return resource with GCP metadata', async () => { + const scope = nock(HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS) + .get(INSTANCE_ID_PATH) + .reply(200, () => 4520031799277581759, HEADERS) + .get(PROJECT_ID_PATH) + .reply(200, () => 'my-project-id', HEADERS) + .get(ZONE_PATH) + .reply(200, () => 'project/zone/my-zone', HEADERS) + .get(CLUSTER_NAME_PATH) + .reply(404); + const secondaryScope = nock(SECONDARY_HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS); + const resource: Resource = await GcpDetector.detect(); + secondaryScope.done(); + scope.done(); - assertCloudResource(resource, { - provider: 'gcp', - accountId: 'my-project-id', - zone: 'my-zone', - }); - assertHostResource(resource, { id: '4520031799277582000' }); + assertCloudResource(resource, { + provider: 'gcp', + accountId: 'my-project-id', + zone: 'my-zone', }); + assertHostResource(resource, { id: '4520031799277582000' }); + }); - it('should populate K8s labels resource when KUBERNETES_SERVICE_HOST is set', async () => { - process.env.KUBERNETES_SERVICE_HOST = 'my-host'; - process.env.NAMESPACE = 'my-namespace'; - process.env.HOSTNAME = 'my-hostname'; - process.env.CONTAINER_NAME = 'my-container-name'; - const scope = nock(HOST_ADDRESS) - .get(INSTANCE_PATH) - .reply(200, {}, HEADERS) - .get(INSTANCE_ID_PATH) - .reply(200, () => 4520031799277581759, HEADERS) - .get(CLUSTER_NAME_PATH) - .reply(200, () => 'my-cluster', HEADERS) - .get(PROJECT_ID_PATH) - .reply(200, () => 'my-project-id', HEADERS) - .get(ZONE_PATH) - .reply(200, () => 'project/zone/my-zone', HEADERS); - const secondaryScope = nock(SECONDARY_HOST_ADDRESS) - .get(INSTANCE_PATH) - .reply(200, {}, HEADERS); - const resource = await GcpDetector.detect(); - secondaryScope.done(); - scope.done(); + it('should populate K8s labels resource when KUBERNETES_SERVICE_HOST is set', async () => { + process.env.KUBERNETES_SERVICE_HOST = 'my-host'; + process.env.NAMESPACE = 'my-namespace'; + process.env.HOSTNAME = 'my-hostname'; + process.env.CONTAINER_NAME = 'my-container-name'; + const scope = nock(HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS) + .get(INSTANCE_ID_PATH) + .reply(200, () => 4520031799277581759, HEADERS) + .get(CLUSTER_NAME_PATH) + .reply(200, () => 'my-cluster', HEADERS) + .get(PROJECT_ID_PATH) + .reply(200, () => 'my-project-id', HEADERS) + .get(ZONE_PATH) + .reply(200, () => 'project/zone/my-zone', HEADERS); + const secondaryScope = nock(SECONDARY_HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS); + const resource = await GcpDetector.detect(); + secondaryScope.done(); + scope.done(); - assertCloudResource(resource, { - provider: 'gcp', - accountId: 'my-project-id', - zone: 'my-zone', - }); - assertK8sResource(resource, { - clusterName: 'my-cluster', - podName: 'my-hostname', - namespaceName: 'my-namespace', - }); - assertContainerResource(resource, { name: 'my-container-name' }); + assertCloudResource(resource, { + provider: 'gcp', + accountId: 'my-project-id', + zone: 'my-zone', + }); + assertK8sResource(resource, { + clusterName: 'my-cluster', + podName: 'my-hostname', + namespaceName: 'my-namespace', }); + assertContainerResource(resource, { name: 'my-container-name' }); + }); - it('should return resource and empty data for non-available metadata attributes', async () => { - const scope = nock(HOST_ADDRESS) - .get(INSTANCE_PATH) - .reply(200, {}, HEADERS) - .get(PROJECT_ID_PATH) - .reply(200, () => 'my-project-id', HEADERS) - .get(ZONE_PATH) - .reply(413) - .get(INSTANCE_ID_PATH) - .reply(400, undefined, HEADERS) - .get(CLUSTER_NAME_PATH) - .reply(413); - const secondaryScope = nock(SECONDARY_HOST_ADDRESS) - .get(INSTANCE_PATH) - .reply(200, {}, HEADERS); - const resource = await GcpDetector.detect(); - secondaryScope.done(); - scope.done(); + it('should return resource and empty data for non-available metadata attributes', async () => { + const scope = nock(HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS) + .get(PROJECT_ID_PATH) + .reply(200, () => 'my-project-id', HEADERS) + .get(ZONE_PATH) + .reply(413) + .get(INSTANCE_ID_PATH) + .reply(400, undefined, HEADERS) + .get(CLUSTER_NAME_PATH) + .reply(413); + const secondaryScope = nock(SECONDARY_HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS); + const resource = await GcpDetector.detect(); + secondaryScope.done(); + scope.done(); - assertCloudResource(resource, { - provider: 'gcp', - accountId: 'my-project-id', - zone: '', - }); + assertCloudResource(resource, { + provider: 'gcp', + accountId: 'my-project-id', + zone: '', }); + }); - it('should retry if the initial request fails', async () => { - const scope = nock(HOST_ADDRESS) - .get(INSTANCE_PATH) - .reply(500) - .get(PROJECT_ID_PATH) - .reply(200, () => 'my-project-id', HEADERS) - .get(ZONE_PATH) - .reply(200, () => 'project/zone/my-zone', HEADERS) - .get(INSTANCE_ID_PATH) - .reply(200, () => 4520031799277581759, HEADERS) - .get(CLUSTER_NAME_PATH) - .reply(413); - const secondaryScope = nock(SECONDARY_HOST_ADDRESS) - .get(INSTANCE_PATH) - .reply(200, {}, HEADERS); - const resource = await GcpDetector.detect(); - secondaryScope.done(); - scope.done(); - - assertCloudResource(resource, { - accountId: 'my-project-id', - zone: 'my-zone', - }); + it('should retry if the initial request fails', async () => { + const scope = nock(HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(500) + .get(PROJECT_ID_PATH) + .reply(200, () => 'my-project-id', HEADERS) + .get(ZONE_PATH) + .reply(200, () => 'project/zone/my-zone', HEADERS) + .get(INSTANCE_ID_PATH) + .reply(200, () => 4520031799277581759, HEADERS) + .get(CLUSTER_NAME_PATH) + .reply(413); + const secondaryScope = nock(SECONDARY_HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS); + const resource = await GcpDetector.detect(); + secondaryScope.done(); + scope.done(); - assertHostResource(resource, { id: '4520031799277582000' }); + assertCloudResource(resource, { + accountId: 'my-project-id', + zone: 'my-zone', }); - it('returns empty resource if not detected', async () => { - const resource = await GcpDetector.detect(); - assertEmptyResource(resource); - }); + assertHostResource(resource, { id: '4520031799277582000' }); + }); + + it('returns empty resource if not detected', async () => { + const resource = await GcpDetector.detect(); + assertEmptyResource(resource); }); }); }); From 2eb7aa2ddb9825eba68d22855bfb51cde7bf6328 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Tue, 24 Mar 2020 22:26:09 -0700 Subject: [PATCH 09/20] chore: prefix private methods with _ --- .../platform/node/detectors/AwsEc2Detector.ts | 4 ++-- .../platform/node/detectors/EnvDetector.ts | 18 ++++++++--------- .../platform/node/detectors/GcpDetector.ts | 20 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts index e8f7cd3187e..486cd39bb3c 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts @@ -40,7 +40,7 @@ export class AwsEc2Detector { accountId, instanceId, region, - } = await AwsEc2Detector.awsMetadataAccessor(); + } = await AwsEc2Detector._awsMetadataAccessor(); return new Resource({ [CLOUD_RESOURCE.PROVIDER]: 'aws', [CLOUD_RESOURCE.ACCOUNT_ID]: accountId, @@ -58,7 +58,7 @@ export class AwsEc2Detector { * to get back a valid JSON document. Parses that document and stores * the identity properties in a local map. */ - private static async awsMetadataAccessor(): Promise { + private static async _awsMetadataAccessor(): Promise { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error('EC2 metadata api request timed out.')); diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts index 0008d4f4806..872c9960579 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts @@ -49,7 +49,7 @@ export class EnvDetector { try { const labelString = process.env.OTEL_RESOURCE_LABELS; if (!labelString) return Resource.empty(); - const labels = EnvDetector.parseResourceLabels( + const labels = EnvDetector._parseResourceLabels( process.env.OTEL_RESOURCE_LABELS ); return new Resource(labels); @@ -70,7 +70,7 @@ export class EnvDetector { * of key/value pairs. * @returns The sanitized resource labels. */ - private static parseResourceLabels(rawEnvLabels?: string): Labels { + private static _parseResourceLabels(rawEnvLabels?: string): Labels { if (!rawEnvLabels) return {}; const labels: Labels = {}; @@ -93,10 +93,10 @@ export class EnvDetector { .trim() .split('^"|"$') .join(''); - if (!EnvDetector.isValidAndNotEmpty(key)) { + if (!EnvDetector._isValidAndNotEmpty(key)) { throw new Error(`Label key ${EnvDetector.ERROR_MESSAGE_INVALID_CHARS}`); } - if (!EnvDetector.isValid(value)) { + if (!EnvDetector._isValid(value)) { throw new Error( `Label value ${EnvDetector.ERROR_MESSAGE_INVALID_VALUE}` ); @@ -113,14 +113,14 @@ export class EnvDetector { * @param str The String to be validated. * @returns Whether the String is valid. */ - private static isValid(name: string): boolean { + private static _isValid(name: string): boolean { return ( name.length <= EnvDetector.MAX_LENGTH && - EnvDetector.isPrintableString(name) + EnvDetector._isPrintableString(name) ); } - private static isPrintableString(str: string): boolean { + private static _isPrintableString(str: string): boolean { for (let i = 0; i < str.length; i++) { const ch: string = str.charAt(i); if (ch <= ' ' || ch >= '~') { @@ -137,7 +137,7 @@ export class EnvDetector { * @param str The String to be validated. * @returns Whether the String is valid and not empty. */ - private static isValidAndNotEmpty(str: string): boolean { - return str.length > 0 && EnvDetector.isValid(str); + private static _isValidAndNotEmpty(str: string): boolean { + return str.length > 0 && EnvDetector._isValid(str); } } diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts index 274ec7c6c36..92e1c2fb7db 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts @@ -34,10 +34,10 @@ export class GcpDetector { if (!(await gcpMetadata.isAvailable())) return Resource.empty(); const [projectId, instanceId, zoneId, clusterName] = await Promise.all([ - GcpDetector.getProjectId(), - GcpDetector.getInstanceId(), - GcpDetector.getZone(), - GcpDetector.getClusterName(), + GcpDetector._getProjectId(), + GcpDetector._getInstanceId(), + GcpDetector._getZone(), + GcpDetector._getClusterName(), ]); const labels: Labels = {}; @@ -47,13 +47,13 @@ export class GcpDetector { labels[CLOUD_RESOURCE.PROVIDER] = 'gcp'; if (process.env.KUBERNETES_SERVICE_HOST) - GcpDetector.addK8sLabels(labels, clusterName); + GcpDetector._addK8sLabels(labels, clusterName); return new Resource(labels); } /** Add resource labels for K8s */ - private static addK8sLabels(labels: Labels, clusterName: string): void { + private static _addK8sLabels(labels: Labels, clusterName: string): void { labels[K8S_RESOURCE.CLUSTER_NAME] = clusterName; labels[K8S_RESOURCE.NAMESPACE_NAME] = process.env.NAMESPACE || ''; labels[K8S_RESOURCE.POD_NAME] = process.env.HOSTNAME || os.hostname(); @@ -61,7 +61,7 @@ export class GcpDetector { } /** Gets project id from GCP project metadata. */ - private static async getProjectId(): Promise { + private static async _getProjectId(): Promise { try { return await gcpMetadata.project('project-id'); } catch { @@ -70,7 +70,7 @@ export class GcpDetector { } /** Gets instance id from GCP instance metadata. */ - private static async getInstanceId(): Promise { + private static async _getInstanceId(): Promise { try { const id = await gcpMetadata.instance('id'); return id.toString(); @@ -80,7 +80,7 @@ export class GcpDetector { } /** Gets zone from GCP instance metadata. */ - private static async getZone(): Promise { + private static async _getZone(): Promise { try { const zoneId = await gcpMetadata.instance('zone'); if (zoneId) { @@ -93,7 +93,7 @@ export class GcpDetector { } /** Gets cluster name from GCP instance metadata. */ - private static async getClusterName(): Promise { + private static async _getClusterName(): Promise { try { return await gcpMetadata.instance('attributes/cluster-name'); } catch { From c466a459fba4d43389574a3cfb0276a7683269b5 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Tue, 24 Mar 2020 22:40:55 -0700 Subject: [PATCH 10/20] fix: import URL for Node 8 --- packages/opentelemetry-resources/test/detect-resources.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opentelemetry-resources/test/detect-resources.test.ts b/packages/opentelemetry-resources/test/detect-resources.test.ts index 0537f9698e0..78a08b535a2 100644 --- a/packages/opentelemetry-resources/test/detect-resources.test.ts +++ b/packages/opentelemetry-resources/test/detect-resources.test.ts @@ -15,6 +15,7 @@ */ import * as nock from 'nock'; +import { URL } from 'url'; import { Resource } from '../src'; import { detectResources, AwsEc2Detector } from '../src'; import { From 98a41fe010163f11ae6ee431d5ff02ce8d61f04b Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Tue, 24 Mar 2020 22:47:00 -0700 Subject: [PATCH 11/20] chore: prefix private constants with _ --- .../platform/node/detectors/EnvDetector.ts | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts index 872c9960579..238649d644b 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts @@ -22,22 +22,22 @@ import { Resource, Labels } from '../../../Resource'; */ export class EnvDetector { // Type, label keys, and label values should not exceed 256 characters. - private static readonly MAX_LENGTH = 255; + private static readonly _MAX_LENGTH = 255; // OTEL_RESOURCE_LABELS is a comma-separated list of labels. - private static readonly COMMA_SEPARATOR = ','; + private static readonly _COMMA_SEPARATOR = ','; // OTEL_RESOURCE_LABELS contains key value pair separated by '='. - private static readonly LABEL_KEY_VALUE_SPLITTER = '='; + private static readonly _LABEL_KEY_VALUE_SPLITTER = '='; - private static readonly ERROR_MESSAGE_INVALID_CHARS = + private static readonly _ERROR_MESSAGE_INVALID_CHARS = 'should be a ASCII string with a length greater than 0 and not exceed ' + - EnvDetector.MAX_LENGTH + + EnvDetector._MAX_LENGTH + ' characters.'; - private static readonly ERROR_MESSAGE_INVALID_VALUE = + private static readonly _ERROR_MESSAGE_INVALID_VALUE = 'should be a ASCII string with a length not exceed ' + - EnvDetector.MAX_LENGTH + + EnvDetector._MAX_LENGTH + ' characters.'; /** @@ -75,12 +75,12 @@ export class EnvDetector { const labels: Labels = {}; const rawLabels: string[] = rawEnvLabels.split( - EnvDetector.COMMA_SEPARATOR, + EnvDetector._COMMA_SEPARATOR, -1 ); for (const rawLabel of rawLabels) { const keyValuePair: string[] = rawLabel.split( - EnvDetector.LABEL_KEY_VALUE_SPLITTER, + EnvDetector._LABEL_KEY_VALUE_SPLITTER, -1 ); if (keyValuePair.length !== 2) { @@ -94,11 +94,13 @@ export class EnvDetector { .split('^"|"$') .join(''); if (!EnvDetector._isValidAndNotEmpty(key)) { - throw new Error(`Label key ${EnvDetector.ERROR_MESSAGE_INVALID_CHARS}`); + throw new Error( + `Label key ${EnvDetector._ERROR_MESSAGE_INVALID_CHARS}` + ); } if (!EnvDetector._isValid(value)) { throw new Error( - `Label value ${EnvDetector.ERROR_MESSAGE_INVALID_VALUE}` + `Label value ${EnvDetector._ERROR_MESSAGE_INVALID_VALUE}` ); } labels[key] = value; @@ -108,14 +110,14 @@ export class EnvDetector { /** * Determines whether the given String is a valid printable ASCII string with - * a length not exceed MAX_LENGTH characters. + * a length not exceed _MAX_LENGTH characters. * * @param str The String to be validated. * @returns Whether the String is valid. */ private static _isValid(name: string): boolean { return ( - name.length <= EnvDetector.MAX_LENGTH && + name.length <= EnvDetector._MAX_LENGTH && EnvDetector._isPrintableString(name) ); } @@ -132,7 +134,7 @@ export class EnvDetector { /** * Determines whether the given String is a valid printable ASCII string with - * a length greater than 0 and not exceed MAX_LENGTH characters. + * 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. From e5d4ff1597662acd5e02be4d083f3622f638b99d Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Fri, 27 Mar 2020 15:40:36 -0700 Subject: [PATCH 12/20] chore: do not export resources --- packages/opentelemetry-resources/src/platform/node/index.ts | 1 - .../opentelemetry-resources/test/detect-resources.test.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/opentelemetry-resources/src/platform/node/index.ts b/packages/opentelemetry-resources/src/platform/node/index.ts index d257f6e05f0..560cdcf8219 100644 --- a/packages/opentelemetry-resources/src/platform/node/index.ts +++ b/packages/opentelemetry-resources/src/platform/node/index.ts @@ -15,4 +15,3 @@ */ export * from './detect-resources'; -export * from './detectors'; diff --git a/packages/opentelemetry-resources/test/detect-resources.test.ts b/packages/opentelemetry-resources/test/detect-resources.test.ts index 78a08b535a2..7658ff71575 100644 --- a/packages/opentelemetry-resources/test/detect-resources.test.ts +++ b/packages/opentelemetry-resources/test/detect-resources.test.ts @@ -16,8 +16,8 @@ import * as nock from 'nock'; import { URL } from 'url'; -import { Resource } from '../src'; -import { detectResources, AwsEc2Detector } from '../src'; +import { Resource, detectResources } from '../src'; +import { AwsEc2Detector } from '../src/platform/node/detectors'; import { assertServiceResource, assertCloudResource, From 568d72ad2d81e551b1712a3f017e6f7340679453 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Fri, 27 Mar 2020 17:27:49 -0700 Subject: [PATCH 13/20] chore: abort request on timeout --- .../src/platform/node/detectors/AwsEc2Detector.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts index 486cd39bb3c..87b25b00b36 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts @@ -61,6 +61,7 @@ export class AwsEc2Detector { private static async _awsMetadataAccessor(): Promise { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { + req.abort(); reject(new Error('EC2 metadata api request timed out.')); }, 1000); From d40326116434aea10c253b76911b0b07f3007461 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Fri, 27 Mar 2020 18:01:07 -0700 Subject: [PATCH 14/20] refactor: export instances of detectors instead of static classes --- .../src/platform/node/detect-resources.ts | 4 +- .../platform/node/detectors/AwsEc2Detector.ts | 55 +++++++++--------- .../platform/node/detectors/EnvDetector.ts | 56 ++++++++----------- .../platform/node/detectors/GcpDetector.ts | 26 +++++---- .../src/platform/node/detectors/index.ts | 6 +- .../test/detect-resources.test.ts | 4 +- .../test/detectors/AwsEc2Detector.test.ts | 10 ++-- .../test/detectors/EnvDetector.test.ts | 8 +-- .../test/detectors/GcpDetector.test.ts | 14 ++--- 9 files changed, 88 insertions(+), 95 deletions(-) diff --git a/packages/opentelemetry-resources/src/platform/node/detect-resources.ts b/packages/opentelemetry-resources/src/platform/node/detect-resources.ts index fef4bf61863..e0c44771275 100644 --- a/packages/opentelemetry-resources/src/platform/node/detect-resources.ts +++ b/packages/opentelemetry-resources/src/platform/node/detect-resources.ts @@ -15,9 +15,9 @@ */ import { Resource } from '../../Resource'; -import { EnvDetector, AwsEc2Detector, GcpDetector } from './detectors'; +import { envDetector, awsEc2Detector, gcpDetector } from './detectors'; -const DETECTORS = [EnvDetector, AwsEc2Detector, GcpDetector]; +const DETECTORS = [envDetector, awsEc2Detector, gcpDetector]; /** * Runs all resource detectors and returns the results merged into a single diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts index 87b25b00b36..08a4f23bb0c 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts @@ -23,8 +23,8 @@ import { CLOUD_RESOURCE, HOST_RESOURCE } from '../../../constants'; * and return a {@link Resource} populated with metadata about the EC2 * instance. Returns an empty Resource if detection fails. */ -export class AwsEc2Detector { - static AWS_INSTANCE_IDENTITY_DOCUMENT_URI = +class AwsEc2Detector { + readonly AWS_INSTANCE_IDENTITY_DOCUMENT_URI = 'http://169.254.169.254/latest/dynamic/instance-identity/document'; /** @@ -34,13 +34,13 @@ export class AwsEc2Detector { * empty {@link Resource} if the connection or parsing of the identity * document fails. */ - static async detect(): Promise { + async detect(): Promise { try { const { accountId, instanceId, region, - } = await AwsEc2Detector._awsMetadataAccessor(); + } = await this._awsMetadataAccessor(); return new Resource({ [CLOUD_RESOURCE.PROVIDER]: 'aws', [CLOUD_RESOURCE.ACCOUNT_ID]: accountId, @@ -58,38 +58,35 @@ export class AwsEc2Detector { * to get back a valid JSON document. Parses that document and stores * the identity properties in a local map. */ - private static async _awsMetadataAccessor(): Promise { + private async _awsMetadataAccessor(): Promise { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { req.abort(); reject(new Error('EC2 metadata api request timed out.')); }, 1000); - const req = http.get( - AwsEc2Detector.AWS_INSTANCE_IDENTITY_DOCUMENT_URI, - 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(JSON.parse(rawData)); - } catch (e) { - res.resume(); // consume response data to free up memory - reject(e); - } - } else { + const req = http.get(this.AWS_INSTANCE_IDENTITY_DOCUMENT_URI, 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(JSON.parse(rawData)); + } catch (e) { res.resume(); // consume response data to free up memory - reject( - new Error('Failed to load page, status code: ' + statusCode) - ); + reject(e); } - }); - } - ); + } else { + res.resume(); // consume response data to free up memory + reject( + new Error('Failed to load page, status code: ' + statusCode) + ); + } + }); + }); req.on('error', err => { clearTimeout(timeoutId); reject(err); @@ -97,3 +94,5 @@ export class AwsEc2Detector { }); } } + +export const awsEc2Detector = new AwsEc2Detector(); diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts index 238649d644b..320def26889 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts @@ -20,24 +20,24 @@ import { Resource, Labels } from '../../../Resource'; * EnvDetector can be used to detect the presence of and create a Resource * from the OTEL_RESOURCE_LABELS environment variable. */ -export class EnvDetector { +class EnvDetector { // Type, label keys, and label values should not exceed 256 characters. - private static readonly _MAX_LENGTH = 255; + private readonly _MAX_LENGTH = 255; // OTEL_RESOURCE_LABELS is a comma-separated list of labels. - private static readonly _COMMA_SEPARATOR = ','; + private readonly _COMMA_SEPARATOR = ','; // OTEL_RESOURCE_LABELS contains key value pair separated by '='. - private static readonly _LABEL_KEY_VALUE_SPLITTER = '='; + private readonly _LABEL_KEY_VALUE_SPLITTER = '='; - private static readonly _ERROR_MESSAGE_INVALID_CHARS = + private readonly _ERROR_MESSAGE_INVALID_CHARS = 'should be a ASCII string with a length greater than 0 and not exceed ' + - EnvDetector._MAX_LENGTH + + this._MAX_LENGTH + ' characters.'; - private static readonly _ERROR_MESSAGE_INVALID_VALUE = + private readonly _ERROR_MESSAGE_INVALID_VALUE = 'should be a ASCII string with a length not exceed ' + - EnvDetector._MAX_LENGTH + + this._MAX_LENGTH + ' characters.'; /** @@ -45,11 +45,11 @@ export class EnvDetector { * OTEL_RESOURCE_LABELS environment variable. Note this is an async function * to conform to the Detector interface. */ - static async detect(): Promise { + async detect(): Promise { try { const labelString = process.env.OTEL_RESOURCE_LABELS; if (!labelString) return Resource.empty(); - const labels = EnvDetector._parseResourceLabels( + const labels = this._parseResourceLabels( process.env.OTEL_RESOURCE_LABELS ); return new Resource(labels); @@ -70,17 +70,14 @@ export class EnvDetector { * of key/value pairs. * @returns The sanitized resource labels. */ - private static _parseResourceLabels(rawEnvLabels?: string): Labels { + private _parseResourceLabels(rawEnvLabels?: string): Labels { if (!rawEnvLabels) return {}; const labels: Labels = {}; - const rawLabels: string[] = rawEnvLabels.split( - EnvDetector._COMMA_SEPARATOR, - -1 - ); + const rawLabels: string[] = rawEnvLabels.split(this._COMMA_SEPARATOR, -1); for (const rawLabel of rawLabels) { const keyValuePair: string[] = rawLabel.split( - EnvDetector._LABEL_KEY_VALUE_SPLITTER, + this._LABEL_KEY_VALUE_SPLITTER, -1 ); if (keyValuePair.length !== 2) { @@ -93,15 +90,11 @@ export class EnvDetector { .trim() .split('^"|"$') .join(''); - if (!EnvDetector._isValidAndNotEmpty(key)) { - throw new Error( - `Label key ${EnvDetector._ERROR_MESSAGE_INVALID_CHARS}` - ); + if (!this._isValidAndNotEmpty(key)) { + throw new Error(`Label key ${this._ERROR_MESSAGE_INVALID_CHARS}`); } - if (!EnvDetector._isValid(value)) { - throw new Error( - `Label value ${EnvDetector._ERROR_MESSAGE_INVALID_VALUE}` - ); + if (!this._isValid(value)) { + throw new Error(`Label value ${this._ERROR_MESSAGE_INVALID_VALUE}`); } labels[key] = value; } @@ -115,14 +108,11 @@ export class EnvDetector { * @param str The String to be validated. * @returns Whether the String is valid. */ - private static _isValid(name: string): boolean { - return ( - name.length <= EnvDetector._MAX_LENGTH && - EnvDetector._isPrintableString(name) - ); + private _isValid(name: string): boolean { + return name.length <= this._MAX_LENGTH && this._isPrintableString(name); } - private static _isPrintableString(str: string): boolean { + private _isPrintableString(str: string): boolean { for (let i = 0; i < str.length; i++) { const ch: string = str.charAt(i); if (ch <= ' ' || ch >= '~') { @@ -139,7 +129,9 @@ export class EnvDetector { * @param str The String to be validated. * @returns Whether the String is valid and not empty. */ - private static _isValidAndNotEmpty(str: string): boolean { - return str.length > 0 && EnvDetector._isValid(str); + private _isValidAndNotEmpty(str: string): boolean { + return str.length > 0 && this._isValid(str); } } + +export const envDetector = new EnvDetector(); diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts index 92e1c2fb7db..c4ac8be48bf 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts @@ -29,15 +29,15 @@ import { * Cloud Platofrm and return a {@link Resource} populated with metadata about * the instance. Returns an empty Resource if detection fails. */ -export class GcpDetector { - static async detect(): Promise { +class GcpDetector { + async detect(): Promise { if (!(await gcpMetadata.isAvailable())) return Resource.empty(); const [projectId, instanceId, zoneId, clusterName] = await Promise.all([ - GcpDetector._getProjectId(), - GcpDetector._getInstanceId(), - GcpDetector._getZone(), - GcpDetector._getClusterName(), + this._getProjectId(), + this._getInstanceId(), + this._getZone(), + this._getClusterName(), ]); const labels: Labels = {}; @@ -47,13 +47,13 @@ export class GcpDetector { labels[CLOUD_RESOURCE.PROVIDER] = 'gcp'; if (process.env.KUBERNETES_SERVICE_HOST) - GcpDetector._addK8sLabels(labels, clusterName); + this._addK8sLabels(labels, clusterName); return new Resource(labels); } /** Add resource labels for K8s */ - private static _addK8sLabels(labels: Labels, clusterName: string): void { + private _addK8sLabels(labels: Labels, clusterName: string): void { labels[K8S_RESOURCE.CLUSTER_NAME] = clusterName; labels[K8S_RESOURCE.NAMESPACE_NAME] = process.env.NAMESPACE || ''; labels[K8S_RESOURCE.POD_NAME] = process.env.HOSTNAME || os.hostname(); @@ -61,7 +61,7 @@ export class GcpDetector { } /** Gets project id from GCP project metadata. */ - private static async _getProjectId(): Promise { + private async _getProjectId(): Promise { try { return await gcpMetadata.project('project-id'); } catch { @@ -70,7 +70,7 @@ export class GcpDetector { } /** Gets instance id from GCP instance metadata. */ - private static async _getInstanceId(): Promise { + private async _getInstanceId(): Promise { try { const id = await gcpMetadata.instance('id'); return id.toString(); @@ -80,7 +80,7 @@ export class GcpDetector { } /** Gets zone from GCP instance metadata. */ - private static async _getZone(): Promise { + private async _getZone(): Promise { try { const zoneId = await gcpMetadata.instance('zone'); if (zoneId) { @@ -93,7 +93,7 @@ export class GcpDetector { } /** Gets cluster name from GCP instance metadata. */ - private static async _getClusterName(): Promise { + private async _getClusterName(): Promise { try { return await gcpMetadata.instance('attributes/cluster-name'); } catch { @@ -101,3 +101,5 @@ export class GcpDetector { } } } + +export const gcpDetector = new GcpDetector(); diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/index.ts b/packages/opentelemetry-resources/src/platform/node/detectors/index.ts index bfe49640ced..79597f3f1c6 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/index.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -export { AwsEc2Detector } from './AwsEc2Detector'; -export { EnvDetector } from './EnvDetector'; -export { GcpDetector } from './GcpDetector'; +export { awsEc2Detector } from './AwsEc2Detector'; +export { envDetector } from './EnvDetector'; +export { gcpDetector } from './GcpDetector'; diff --git a/packages/opentelemetry-resources/test/detect-resources.test.ts b/packages/opentelemetry-resources/test/detect-resources.test.ts index 7658ff71575..462888ae410 100644 --- a/packages/opentelemetry-resources/test/detect-resources.test.ts +++ b/packages/opentelemetry-resources/test/detect-resources.test.ts @@ -17,7 +17,7 @@ import * as nock from 'nock'; import { URL } from 'url'; import { Resource, detectResources } from '../src'; -import { AwsEc2Detector } from '../src/platform/node/detectors'; +import { awsEc2Detector } from '../src/platform/node/detectors'; import { assertServiceResource, assertCloudResource, @@ -42,7 +42,7 @@ const ZONE_PATH = BASE_PATH + '/instance/zone'; const CLUSTER_NAME_PATH = BASE_PATH + '/instance/attributes/cluster-name'; const { origin: AWS_HOST, pathname: AWS_PATH } = new URL( - AwsEc2Detector.AWS_INSTANCE_IDENTITY_DOCUMENT_URI + awsEc2Detector.AWS_INSTANCE_IDENTITY_DOCUMENT_URI ); const mockedAwsResponse = { diff --git a/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts b/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts index ede409d4c65..6777d472fe9 100644 --- a/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts @@ -18,7 +18,7 @@ import * as nock from 'nock'; import * as assert from 'assert'; import { URL } from 'url'; import { Resource } from '../../src'; -import { AwsEc2Detector } from '../../src/platform/node/detectors/AwsEc2Detector'; +import { awsEc2Detector } from '../../src/platform/node/detectors/AwsEc2Detector'; import { assertCloudResource, assertHostResource, @@ -26,7 +26,7 @@ import { } from '../util/resource-assertions'; const { origin: AWS_HOST, pathname: AWS_PATH } = new URL( - AwsEc2Detector.AWS_INSTANCE_IDENTITY_DOCUMENT_URI + awsEc2Detector.AWS_INSTANCE_IDENTITY_DOCUMENT_URI ); const mockedAwsResponse = { @@ -35,7 +35,7 @@ const mockedAwsResponse = { region: 'my-region', }; -describe('AwsEc2Detector', () => { +describe('awsEc2Detector', () => { before(() => { nock.disableNetConnect(); nock.cleanAll(); @@ -50,7 +50,7 @@ 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(); scope.done(); assert.ok(resource); @@ -72,7 +72,7 @@ describe('AwsEc2Detector', () => { .replyWithError({ code: 'ENOTFOUND', }); - const resource: Resource = await AwsEc2Detector.detect(); + const resource: Resource = await awsEc2Detector.detect(); 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 388947f8d57..b8dfe508eba 100644 --- a/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts @@ -15,14 +15,14 @@ */ import { Resource } from '../../src/Resource'; -import { EnvDetector } from '../../src/platform/node/detectors/EnvDetector'; +import { envDetector } from '../../src/platform/node/detectors/EnvDetector'; import { assertK8sResource, assertEmptyResource, } from '../util/resource-assertions'; import { K8S_RESOURCE } from '../../src'; -describe('EnvDetector()', () => { +describe('envDetector()', () => { describe('with valid env', () => { before(() => { process.env.OTEL_RESOURCE_LABELS = @@ -34,7 +34,7 @@ describe('EnvDetector()', () => { }); it('should return resource information from environment variable', async () => { - const resource: Resource = await EnvDetector.detect(); + const resource: Resource = await envDetector.detect(); assertK8sResource(resource, { [K8S_RESOURCE.POD_NAME]: 'pod-xyz-123', [K8S_RESOURCE.CLUSTER_NAME]: 'c1', @@ -45,7 +45,7 @@ describe('EnvDetector()', () => { describe('with empty env', () => { it('should return empty resource', async () => { - const resource: Resource = await EnvDetector.detect(); + const resource: Resource = await envDetector.detect(); assertEmptyResource(resource); }); }); diff --git a/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts b/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts index 8a504b5f276..bcd17a6847c 100644 --- a/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts @@ -24,7 +24,7 @@ import { } from 'gcp-metadata'; import * as nock from 'nock'; import { Resource } from '../../src'; -import { GcpDetector } from '../../src/platform/node/detectors'; +import { gcpDetector } from '../../src/platform/node/detectors'; import { assertCloudResource, assertHostResource, @@ -42,7 +42,7 @@ const PROJECT_ID_PATH = BASE_PATH + '/project/project-id'; const ZONE_PATH = BASE_PATH + '/instance/zone'; const CLUSTER_NAME_PATH = BASE_PATH + '/instance/attributes/cluster-name'; -describe('GcpDetector', () => { +describe('gcpDetector', () => { describe('.detect', () => { before(() => { nock.disableNetConnect(); @@ -80,7 +80,7 @@ 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(); secondaryScope.done(); scope.done(); @@ -111,7 +111,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(); secondaryScope.done(); scope.done(); @@ -143,7 +143,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(); secondaryScope.done(); scope.done(); @@ -169,7 +169,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(); secondaryScope.done(); scope.done(); @@ -182,7 +182,7 @@ describe('GcpDetector', () => { }); it('returns empty resource if not detected', async () => { - const resource = await GcpDetector.detect(); + const resource = await gcpDetector.detect(); assertEmptyResource(resource); }); }); From 426ed854f32c7936c707f84b7956973ed66568de Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Fri, 27 Mar 2020 18:16:43 -0700 Subject: [PATCH 15/20] refactor: formalize Detector interface --- .../opentelemetry-resources/src/Resource.ts | 5 +--- packages/opentelemetry-resources/src/index.ts | 3 ++- .../src/platform/node/detect-resources.ts | 3 ++- .../platform/node/detectors/AwsEc2Detector.ts | 3 ++- .../platform/node/detectors/EnvDetector.ts | 5 ++-- .../platform/node/detectors/GcpDetector.ts | 5 ++-- packages/opentelemetry-resources/src/types.ts | 25 +++++++++++++++++++ 7 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 packages/opentelemetry-resources/src/types.ts diff --git a/packages/opentelemetry-resources/src/Resource.ts b/packages/opentelemetry-resources/src/Resource.ts index 9ea9d02c9f3..a2f8628f3a1 100644 --- a/packages/opentelemetry-resources/src/Resource.ts +++ b/packages/opentelemetry-resources/src/Resource.ts @@ -16,6 +16,7 @@ import { SDK_INFO } from '@opentelemetry/base'; import { TELEMETRY_SDK_RESOURCE } from './constants'; +import { Labels } from './types'; /** * A Resource describes the entity for which a signals (metrics or trace) are @@ -67,7 +68,3 @@ export class Resource { return new Resource(mergedLabels); } } - -export interface Labels { - [key: string]: number | string | boolean; -} diff --git a/packages/opentelemetry-resources/src/index.ts b/packages/opentelemetry-resources/src/index.ts index 22e460cf85a..13673409aa0 100644 --- a/packages/opentelemetry-resources/src/index.ts +++ b/packages/opentelemetry-resources/src/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ -export { Resource, Labels } from './Resource'; +export * from './Resource'; export * from './platform'; export * from './constants'; +export * from './types'; diff --git a/packages/opentelemetry-resources/src/platform/node/detect-resources.ts b/packages/opentelemetry-resources/src/platform/node/detect-resources.ts index e0c44771275..a70a201029e 100644 --- a/packages/opentelemetry-resources/src/platform/node/detect-resources.ts +++ b/packages/opentelemetry-resources/src/platform/node/detect-resources.ts @@ -16,8 +16,9 @@ import { Resource } from '../../Resource'; import { envDetector, awsEc2Detector, gcpDetector } from './detectors'; +import { Detector } from '../../types'; -const DETECTORS = [envDetector, awsEc2Detector, gcpDetector]; +const DETECTORS: Array = [envDetector, awsEc2Detector, gcpDetector]; /** * Runs all resource detectors and returns the results merged into a single diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts index 08a4f23bb0c..d34133319ba 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts @@ -17,13 +17,14 @@ import * as http from 'http'; import { Resource } from '../../../Resource'; import { CLOUD_RESOURCE, HOST_RESOURCE } from '../../../constants'; +import { Detector } from '../../../types'; /** * 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. */ -class AwsEc2Detector { +class AwsEc2Detector implements Detector { readonly AWS_INSTANCE_IDENTITY_DOCUMENT_URI = 'http://169.254.169.254/latest/dynamic/instance-identity/document'; diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts index 320def26889..e67fae83d6b 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts @@ -14,13 +14,14 @@ * limitations under the License. */ -import { Resource, Labels } from '../../../Resource'; +import { Resource } from '../../../Resource'; +import { Detector, Labels } from '../../../types'; /** * EnvDetector can be used to detect the presence of and create a Resource * from the OTEL_RESOURCE_LABELS environment variable. */ -class EnvDetector { +class EnvDetector implements Detector { // Type, label keys, and label values should not exceed 256 characters. private readonly _MAX_LENGTH = 255; diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts index c4ac8be48bf..6262344a73d 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts @@ -16,7 +16,8 @@ import * as os from 'os'; import * as gcpMetadata from 'gcp-metadata'; -import { Resource, Labels } from '../../../Resource'; +import { Resource } from '../../../Resource'; +import { Detector, Labels } from '../../../types'; import { CLOUD_RESOURCE, HOST_RESOURCE, @@ -29,7 +30,7 @@ import { * Cloud Platofrm and return a {@link Resource} populated with metadata about * the instance. Returns an empty Resource if detection fails. */ -class GcpDetector { +class GcpDetector implements Detector { async detect(): Promise { if (!(await gcpMetadata.isAvailable())) return Resource.empty(); diff --git a/packages/opentelemetry-resources/src/types.ts b/packages/opentelemetry-resources/src/types.ts new file mode 100644 index 00000000000..36b1b17368f --- /dev/null +++ b/packages/opentelemetry-resources/src/types.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2020, 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'; + +export interface Labels { + [key: string]: number | string | boolean; +} + +export interface Detector { + detect(): Promise; +} From 5d5b343ffbda7e221cee35fe12d5233370680e7c Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Thu, 2 Apr 2020 15:38:31 -0700 Subject: [PATCH 16/20] refactor: rename Labels -> ResourceLabels --- packages/opentelemetry-resources/src/Resource.ts | 4 ++-- .../src/platform/node/detectors/EnvDetector.ts | 6 +++--- .../src/platform/node/detectors/GcpDetector.ts | 6 +++--- packages/opentelemetry-resources/src/types.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/opentelemetry-resources/src/Resource.ts b/packages/opentelemetry-resources/src/Resource.ts index a2f8628f3a1..0a76dad9684 100644 --- a/packages/opentelemetry-resources/src/Resource.ts +++ b/packages/opentelemetry-resources/src/Resource.ts @@ -16,7 +16,7 @@ import { SDK_INFO } from '@opentelemetry/base'; import { TELEMETRY_SDK_RESOURCE } from './constants'; -import { Labels } from './types'; +import { ResourceLabels } from './types'; /** * A Resource describes the entity for which a signals (metrics or trace) are @@ -49,7 +49,7 @@ export class Resource { * about the entity as numbers, strings or booleans * TODO: Consider to add check/validation on labels. */ - readonly labels: Labels + readonly labels: ResourceLabels ) {} /** diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts index e67fae83d6b..52dd1607785 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts @@ -15,7 +15,7 @@ */ import { Resource } from '../../../Resource'; -import { Detector, Labels } from '../../../types'; +import { Detector, ResourceLabels } from '../../../types'; /** * EnvDetector can be used to detect the presence of and create a Resource @@ -71,10 +71,10 @@ class EnvDetector implements Detector { * of key/value pairs. * @returns The sanitized resource labels. */ - private _parseResourceLabels(rawEnvLabels?: string): Labels { + private _parseResourceLabels(rawEnvLabels?: string): ResourceLabels { if (!rawEnvLabels) return {}; - const labels: Labels = {}; + const labels: ResourceLabels = {}; const rawLabels: string[] = rawEnvLabels.split(this._COMMA_SEPARATOR, -1); for (const rawLabel of rawLabels) { const keyValuePair: string[] = rawLabel.split( diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts index 6262344a73d..ab1136fe8ba 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts @@ -17,7 +17,7 @@ import * as os from 'os'; import * as gcpMetadata from 'gcp-metadata'; import { Resource } from '../../../Resource'; -import { Detector, Labels } from '../../../types'; +import { Detector, ResourceLabels } from '../../../types'; import { CLOUD_RESOURCE, HOST_RESOURCE, @@ -41,7 +41,7 @@ class GcpDetector implements Detector { this._getClusterName(), ]); - const labels: Labels = {}; + const labels: ResourceLabels = {}; labels[CLOUD_RESOURCE.ACCOUNT_ID] = projectId; labels[HOST_RESOURCE.ID] = instanceId; labels[CLOUD_RESOURCE.ZONE] = zoneId; @@ -54,7 +54,7 @@ class GcpDetector implements Detector { } /** Add resource labels for K8s */ - private _addK8sLabels(labels: Labels, clusterName: string): void { + private _addK8sLabels(labels: ResourceLabels, clusterName: string): void { labels[K8S_RESOURCE.CLUSTER_NAME] = clusterName; labels[K8S_RESOURCE.NAMESPACE_NAME] = process.env.NAMESPACE || ''; labels[K8S_RESOURCE.POD_NAME] = process.env.HOSTNAME || os.hostname(); diff --git a/packages/opentelemetry-resources/src/types.ts b/packages/opentelemetry-resources/src/types.ts index 36b1b17368f..fd632e9f4cf 100644 --- a/packages/opentelemetry-resources/src/types.ts +++ b/packages/opentelemetry-resources/src/types.ts @@ -16,7 +16,7 @@ import { Resource } from './Resource'; -export interface Labels { +export interface ResourceLabels { [key: string]: number | string | boolean; } From dce371822f78b1724c9841854e4ad8885ecf0008 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Thu, 2 Apr 2020 17:49:07 -0700 Subject: [PATCH 17/20] feat: gracefully handle detectors that throw --- packages/opentelemetry-resources/package.json | 2 ++ .../src/platform/node/detect-resources.ts | 8 +++++++- .../test/detect-resources.test.ts | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/opentelemetry-resources/package.json b/packages/opentelemetry-resources/package.json index dc7fce96b9a..532fd101503 100644 --- a/packages/opentelemetry-resources/package.json +++ b/packages/opentelemetry-resources/package.json @@ -46,12 +46,14 @@ "devDependencies": { "@types/mocha": "^7.0.0", "@types/node": "^12.6.9", + "@types/sinon": "^7.0.13", "codecov": "^3.6.1", "gts": "^1.1.0", "mocha": "^6.2.0", "nock": "^12.0.2", "nyc": "^15.0.0", "rimraf": "^3.0.0", + "sinon": "^7.5.0", "ts-mocha": "^6.0.0", "ts-node": "^8.6.2", "tslint-consistent-codestyle": "^1.16.0", diff --git a/packages/opentelemetry-resources/src/platform/node/detect-resources.ts b/packages/opentelemetry-resources/src/platform/node/detect-resources.ts index a70a201029e..23512505826 100644 --- a/packages/opentelemetry-resources/src/platform/node/detect-resources.ts +++ b/packages/opentelemetry-resources/src/platform/node/detect-resources.ts @@ -26,7 +26,13 @@ const DETECTORS: Array = [envDetector, awsEc2Detector, gcpDetector]; */ export const detectResources = async (): Promise => { const resources: Array = await Promise.all( - DETECTORS.map(d => d.detect()) + DETECTORS.map(d => { + try { + return d.detect(); + } catch { + return Resource.empty(); + } + }) ); return resources.reduce( (acc, resource) => acc.merge(resource), diff --git a/packages/opentelemetry-resources/test/detect-resources.test.ts b/packages/opentelemetry-resources/test/detect-resources.test.ts index 462888ae410..2e0b68ab989 100644 --- a/packages/opentelemetry-resources/test/detect-resources.test.ts +++ b/packages/opentelemetry-resources/test/detect-resources.test.ts @@ -15,6 +15,7 @@ */ import * as nock from 'nock'; +import * as sinon from 'sinon'; import { URL } from 'url'; import { Resource, detectResources } from '../src'; import { awsEc2Detector } from '../src/platform/node/detectors'; @@ -137,4 +138,20 @@ describe('detectResources', async () => { }); }); }); + + describe('with a buggy detector', () => { + it('returns a merged resource', async () => { + const stub = sinon.stub(awsEc2Detector, 'detect').throws(); + const resource: Resource = await detectResources(); + + assertServiceResource(resource, { + instanceId: '627cc493', + name: 'my-service', + namespace: 'default', + version: '0.0.1', + }); + + stub.restore(); + }); + }); }); From b7f05924c44c7ca520396147562276036a4daf04 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Fri, 3 Apr 2020 16:16:24 -0700 Subject: [PATCH 18/20] fix: do not resume in an end event --- .../src/platform/node/detectors/AwsEc2Detector.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts index d34133319ba..0c78a0971bf 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts @@ -77,11 +77,9 @@ class AwsEc2Detector implements Detector { try { resolve(JSON.parse(rawData)); } catch (e) { - res.resume(); // consume response data to free up memory reject(e); } } else { - res.resume(); // consume response data to free up memory reject( new Error('Failed to load page, status code: ' + statusCode) ); From 7df23d5d9aaceebac00deda14624db13267bc92c Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Fri, 3 Apr 2020 16:44:08 -0700 Subject: [PATCH 19/20] docs: document interfaces and idenitity document url --- .../src/platform/node/detectors/AwsEc2Detector.ts | 4 ++++ packages/opentelemetry-resources/src/types.ts | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts index 0c78a0971bf..d3317b9c099 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts @@ -25,6 +25,10 @@ import { Detector } from '../../../types'; * instance. Returns an empty Resource if detection fails. */ class AwsEc2Detector implements Detector { + /** + * See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html + * for documentation about the AWS instance identity document endpoint. + */ readonly AWS_INSTANCE_IDENTITY_DOCUMENT_URI = 'http://169.254.169.254/latest/dynamic/instance-identity/document'; diff --git a/packages/opentelemetry-resources/src/types.ts b/packages/opentelemetry-resources/src/types.ts index fd632e9f4cf..a9705350a57 100644 --- a/packages/opentelemetry-resources/src/types.ts +++ b/packages/opentelemetry-resources/src/types.ts @@ -16,10 +16,15 @@ import { Resource } from './Resource'; +/** Interface for Resource labels */ export interface ResourceLabels { [key: string]: number | string | boolean; } +/** + * Interface for a Resource Detector. In order to detect resources in parallel + * a detector returns a Promise containing a Resource. + */ export interface Detector { detect(): Promise; } From 7a3ed9422216a84f72dcd84452af02f318cb6a1a Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Fri, 3 Apr 2020 17:46:01 -0700 Subject: [PATCH 20/20] fix: nock flakiness in resource tests --- .../test/detect-resources.test.ts | 16 ++++++----- .../test/detectors/GcpDetector.test.ts | 27 ------------------- 2 files changed, 10 insertions(+), 33 deletions(-) diff --git a/packages/opentelemetry-resources/test/detect-resources.test.ts b/packages/opentelemetry-resources/test/detect-resources.test.ts index 2e0b68ab989..24efdf76381 100644 --- a/packages/opentelemetry-resources/test/detect-resources.test.ts +++ b/packages/opentelemetry-resources/test/detect-resources.test.ts @@ -53,13 +53,13 @@ const mockedAwsResponse = { }; describe('detectResources', async () => { - before(() => { + beforeEach(() => { nock.disableNetConnect(); process.env.OTEL_RESOURCE_LABELS = 'service.instance.id=627cc493,service.name=my-service,service.namespace=default,service.version=0.0.1'; }); - after(() => { + afterEach(() => { nock.cleanAll(); nock.enableNetConnect(); delete process.env.OTEL_RESOURCE_LABELS; @@ -71,7 +71,7 @@ describe('detectResources', async () => { }); it('returns a merged resource', async () => { - const scope = nock(HOST_ADDRESS) + const gcpScope = nock(HOST_ADDRESS) .get(INSTANCE_PATH) .reply(200, {}, HEADERS) .get(INSTANCE_ID_PATH) @@ -82,12 +82,16 @@ describe('detectResources', async () => { .reply(200, () => 'project/zone/my-zone', HEADERS) .get(CLUSTER_NAME_PATH) .reply(404); - const secondaryScope = nock(SECONDARY_HOST_ADDRESS) + const gcpSecondaryScope = nock(SECONDARY_HOST_ADDRESS) .get(INSTANCE_PATH) .reply(200, {}, HEADERS); + const awsScope = nock(AWS_HOST) + .get(AWS_PATH) + .replyWithError({ code: 'ENOTFOUND' }); const resource: Resource = await detectResources(); - secondaryScope.done(); - scope.done(); + awsScope.done(); + gcpSecondaryScope.done(); + gcpScope.done(); assertCloudResource(resource, { provider: 'gcp', diff --git a/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts b/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts index bcd17a6847c..271f8f9c89b 100644 --- a/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts @@ -154,33 +154,6 @@ describe('gcpDetector', () => { }); }); - it('should retry if the initial request fails', async () => { - const scope = nock(HOST_ADDRESS) - .get(INSTANCE_PATH) - .reply(500) - .get(PROJECT_ID_PATH) - .reply(200, () => 'my-project-id', HEADERS) - .get(ZONE_PATH) - .reply(200, () => 'project/zone/my-zone', HEADERS) - .get(INSTANCE_ID_PATH) - .reply(200, () => 4520031799277581759, HEADERS) - .get(CLUSTER_NAME_PATH) - .reply(413); - const secondaryScope = nock(SECONDARY_HOST_ADDRESS) - .get(INSTANCE_PATH) - .reply(200, {}, HEADERS); - const resource = await gcpDetector.detect(); - secondaryScope.done(); - scope.done(); - - assertCloudResource(resource, { - accountId: 'my-project-id', - zone: 'my-zone', - }); - - assertHostResource(resource, { id: '4520031799277582000' }); - }); - it('returns empty resource if not detected', async () => { const resource = await gcpDetector.detect(); assertEmptyResource(resource);