From f3e812c0d6dfe977b1ee7007c3e641655e762283 Mon Sep 17 00:00:00 2001 From: Ievgenii Saikevych Date: Wed, 24 Oct 2018 17:38:12 -0700 Subject: [PATCH] Adds monitored resource to [core] --- packages/opencensus-core/package-lock.json | 142 ++++++++++++++-- packages/opencensus-core/package.json | 4 +- .../aws-identity-document-utils.ts | 100 ++++++++++++ .../monitored-resource/gcp-metadata-config.ts | 138 ++++++++++++++++ .../monitored-resource/monitored-resource.ts | 50 ++++++ .../src/common/monitored-resource/types.ts | 75 +++++++++ .../src/common/monitored-resource/util.ts | 49 ++++++ packages/opencensus-core/src/index.ts | 4 + .../test/test-monitored-resource.ts | 154 ++++++++++++++++++ 9 files changed, 704 insertions(+), 12 deletions(-) create mode 100644 packages/opencensus-core/src/common/monitored-resource/aws-identity-document-utils.ts create mode 100644 packages/opencensus-core/src/common/monitored-resource/gcp-metadata-config.ts create mode 100644 packages/opencensus-core/src/common/monitored-resource/monitored-resource.ts create mode 100644 packages/opencensus-core/src/common/monitored-resource/types.ts create mode 100644 packages/opencensus-core/src/common/monitored-resource/util.ts create mode 100644 packages/opencensus-core/test/test-monitored-resource.ts diff --git a/packages/opencensus-core/package-lock.json b/packages/opencensus-core/package-lock.json index a1da2bd4f..930076d8f 100644 --- a/packages/opencensus-core/package-lock.json +++ b/packages/opencensus-core/package-lock.json @@ -1,6 +1,6 @@ { "name": "@opencensus/core", - "version": "0.0.5", + "version": "0.0.6", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -19,10 +19,19 @@ "integrity": "sha512-nlK/iyETgafGli8Zh9zJVCTicvU3iajSkRwOh3Hhiva598CMqNJ4NcVCGMTGKpGpTYj/9R8RLzS9NAykSSCqGw==", "dev": true }, + "@types/nock": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@types/nock/-/nock-9.3.0.tgz", + "integrity": "sha512-ZHf/X8rTQ5Tb1rHjxIJYqm55uO265agE3G7NoSXVa2ep+EcJXgB2fsme+zBvK7MhrxTwkC/xkB6THyv50u0MGw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/node": { - "version": "9.4.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-9.4.7.tgz", - "integrity": "sha512-4Ba90mWNx8ddbafuyGGwjkZMigi+AWfYLSDCpovwsE63ia8w93r3oJ8PIAQc3y8U+XHcnMOHPIzNe3o438Ywcw==", + "version": "9.6.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.35.tgz", + "integrity": "sha512-h5zvHS8wXHGa+Gcqs9K8vqCgOtqjr0+NqG/DDJmQIX1wpR9HivAfgV8bjcD3mGM4bPfQw5Aneb2Pn8355L83jA==", "dev": true }, "@types/once": { @@ -148,6 +157,12 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "async-listener": { "version": "0.6.9", "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.9.tgz", @@ -302,6 +317,20 @@ "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", "dev": true }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, "chalk": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", @@ -336,6 +365,12 @@ "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", "dev": true }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, "clang-format": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/clang-format/-/clang-format-1.2.2.tgz", @@ -505,10 +540,25 @@ } } }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, "deep-extend": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true }, "diff": { @@ -608,6 +658,12 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", @@ -916,6 +972,12 @@ "integrity": "sha512-xyQpxeWWMKyJps9CuGJYeng6ssI5bpqS9ltQpdVQ90t4ql6NdnxFKh95JcRt2cun/DjMVNrdjniLPuMA69xmCw==", "dev": true }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, "latest-version": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", @@ -1170,6 +1232,40 @@ "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", "dev": true }, + "nock": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.1.tgz", + "integrity": "sha512-M0aL9IDbUFURmokoXqejZQybZk8EtlYjUBjaoICVbW62uOlyPRsnEsceyOlUik4spCOt50ptwM4BTPt20ITtcQ==", + "dev": true, + "requires": { + "chai": "^4.1.2", + "debug": "^4.1.0", + "deep-equal": "^1.0.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.5", + "mkdirp": "^0.5.0", + "propagate": "^1.0.0", + "qs": "^6.5.1", + "semver": "^5.5.0" + }, + "dependencies": { + "debug": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", + "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, "normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -4106,6 +4202,12 @@ "pify": "^3.0.0" } }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", @@ -4118,12 +4220,24 @@ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "dev": true }, + "propagate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", + "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", + "dev": true + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, "quick-lru": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", @@ -4131,12 +4245,12 @@ "dev": true }, "rc": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.6.tgz", - "integrity": "sha1-6xiYnG1PTxYsOZ953dKfODVWgJI=", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "requires": { - "deep-extend": "~0.4.0", + "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" @@ -4506,6 +4620,12 @@ "tslib": "^1.8.1" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "typescript": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz", diff --git a/packages/opencensus-core/package.json b/packages/opencensus-core/package.json index 5ae0019b2..bd1b85e71 100644 --- a/packages/opencensus-core/package.json +++ b/packages/opencensus-core/package.json @@ -40,7 +40,8 @@ "devDependencies": { "@types/continuation-local-storage": "^3.2.1", "@types/mocha": "^2.2.48", - "@types/node": "^9.4.7", + "@types/nock": "^9.3.0", + "@types/node": "^9.6.35", "@types/once": "^1.4.0", "@types/semver": "^5.5.0", "@types/shimmer": "^1.0.1", @@ -49,6 +50,7 @@ "intercept-stdout": "^0.1.2", "mocha": "^5.0.4", "ncp": "^2.0.0", + "nock": "^10.0.1", "nyc": "11.6.0", "ts-node": "^4.0.0", "typescript": "~2.6.1" diff --git a/packages/opencensus-core/src/common/monitored-resource/aws-identity-document-utils.ts b/packages/opencensus-core/src/common/monitored-resource/aws-identity-document-utils.ts new file mode 100644 index 000000000..00e9df67f --- /dev/null +++ b/packages/opencensus-core/src/common/monitored-resource/aws-identity-document-utils.ts @@ -0,0 +1,100 @@ +/** + * Copyright 2018, OpenCensus 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 + * + * http://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 {get} from 'http'; +import * as logger from '../../common/console-logger'; +import * as loggerTypes from '../../common/types'; + +/** Util methods for getting and parsing AWS instance identity document. */ +export class AwsIdentityDocumentUtils { + /** Aws instance identity URL */ + static HOST = '169.254.169.254'; + static readonly PATH = '/latest/dynamic/instance-identity/document'; + static readonly PORT = 80; + static runned = false; + static metadata: Record = {}; + static promise: Promise>; + static readonly logger: loggerTypes.Logger = logger.logger(); + + /** + * 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. + */ + static run() { + if (AwsIdentityDocumentUtils.runned) { + return AwsIdentityDocumentUtils.promise; + } + AwsIdentityDocumentUtils.promise = + AwsIdentityDocumentUtils.getDocument().then(metadata => { + if (Object.keys(metadata).length) { + AwsIdentityDocumentUtils.metadata = metadata; + AwsIdentityDocumentUtils.runned = true; + } + return metadata; + }); + return AwsIdentityDocumentUtils.promise; + } + + /** + * Fetches the requested instance metadata entry. + * @param name Attribute name relative to the computeMetadata/v1 prefix + */ + private static getDocument() { + const promise = new Promise((resolve, reject) => { + get({ + host: AwsIdentityDocumentUtils.HOST, + path: AwsIdentityDocumentUtils.PATH, + port: AwsIdentityDocumentUtils.PORT + }, + (response) => { + let body = ''; + response.on('data', chunk => body += chunk); + response.on('end', () => { + if (response.statusCode >= 200 && response.statusCode < 300) { + try { + const data = JSON.parse(body); + resolve(data); + } catch (e) { + response.resume(); + } + } else { + const errorMessage = + `Request Failed. Status code: ${response.statusCode}`; + AwsIdentityDocumentUtils.logger.error(errorMessage); + reject(errorMessage); + response.resume(); + } + }); + }); + }); + return promise.catch(e => e); + } + + /** + * AWS Instance Identity Document is a JSON file. + * See + * docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html. + */ + getMetadata() { + return AwsIdentityDocumentUtils.metadata; + } + + static isRunning() { + return AwsIdentityDocumentUtils.runned; + } +} diff --git a/packages/opencensus-core/src/common/monitored-resource/gcp-metadata-config.ts b/packages/opencensus-core/src/common/monitored-resource/gcp-metadata-config.ts new file mode 100644 index 000000000..828144b41 --- /dev/null +++ b/packages/opencensus-core/src/common/monitored-resource/gcp-metadata-config.ts @@ -0,0 +1,138 @@ +/** + * Copyright 2018, OpenCensus 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 + * + * http://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 {get} from 'http'; +import {isString} from 'util'; + +import * as logger from '../../common/console-logger'; +import * as loggerTypes from '../../common/types'; + +import {monitoredResourceAttributes} from './types'; + +export class GcpMetadataConfig { + static URL = 'metadata/computeMetadata/v1'; + static PORT = 80; + static readonly HEADER = {'Metadata-Flavor': 'Google'}; + /** Kubenertes environment variables */ + static readonly KUBERNETES_SERVICE_HOST = 'KUBERNETES_SERVICE_HOST'; + static runned = false; + /** Persistant metadata */ + static metadata: Record = {}; + static readonly logger: loggerTypes.Logger = logger.logger(); + static promise: Promise>; + + /** + * Initializes metadata service once and load gcp metadata into map. + */ + static run() { + if (GcpMetadataConfig.runned) { + return GcpMetadataConfig.promise; + } + GcpMetadataConfig.promise = + GcpMetadataConfig.getAttribute('instance_id').then(id => { + if (isString(id)) { + GcpMetadataConfig.metadata['instance_id'] = id; + GcpMetadataConfig.runned = true; + return GcpMetadataConfig.getAttributes(); + } + return Promise.resolve(GcpMetadataConfig.metadata); + }); + return GcpMetadataConfig.promise; + } + + private static getAttributes() { + let attributes; + if (GcpMetadataConfig.KUBERNETES_SERVICE_HOST in process.env) { + attributes = monitoredResourceAttributes.GKE; + } else { + attributes = monitoredResourceAttributes.GCE; + } + const promises: Array> = []; + Object.keys(attributes) + .filter(key => key !== 'instance_id') + .forEach(key => { + promises.push(GcpMetadataConfig.getAttribute(key).then(value => { + if (value) { + GcpMetadataConfig.metadata[key] = value; + } + return value; + })); + }); + return Promise.all(promises) + .then(() => GcpMetadataConfig.metadata) + .catch(e => e); + } + + /** + * Fetches the requested instance metadata entry. + * @param name Attribute name relative to the computeMetadata/v1 prefix + */ + static getAttribute(name: string): Promise { + const options = { + host: GcpMetadataConfig.URL, + path: '/' + monitoredResourceAttributes.GKE[name], + port: GcpMetadataConfig.PORT, + headers: GcpMetadataConfig.HEADER + }; + const promise = new Promise((resolve, reject) => { + try { + get(options, response => { + if (response.statusCode >= 200 && response.statusCode < 300) { + let rawData = ''; + response.on('data', chunk => rawData += chunk); + response.on('end', () => resolve(rawData)); + } else { + const errorMessage = + `Request Failed. Status code: ${response.statusCode}`; + this.logger.error(errorMessage); + response.resume(); + reject(errorMessage); + } + }).on('error', (e) => { + reject(e); + }); + } catch (e) { + reject(e); + } + }); + return promise.catch(e => e); + } + + /** + * Gets metadata for GCP GCE instance. + */ + getGceMetadata() { + return GcpMetadataConfig.metadata; + } + + /** + * Gets metadata for GCP GKE container. + */ + getGkeMetadata() { + Object.keys(monitoredResourceAttributes.GKE).forEach(key => { + const value = monitoredResourceAttributes.GKE[key]; + const attributeValue = process.env[value]; + if (attributeValue) { + GcpMetadataConfig.metadata[key] = attributeValue; + } + }); + return GcpMetadataConfig.metadata; + } + + static isRunning(): boolean { + return GcpMetadataConfig.runned; + } +} diff --git a/packages/opencensus-core/src/common/monitored-resource/monitored-resource.ts b/packages/opencensus-core/src/common/monitored-resource/monitored-resource.ts new file mode 100644 index 000000000..ac385c71a --- /dev/null +++ b/packages/opencensus-core/src/common/monitored-resource/monitored-resource.ts @@ -0,0 +1,50 @@ +/** + * Represents an auto-detected monitored resource used by application for + * exporting stats. It has a resource type associated with a mapping + * from resource labels to values. + */ +import {AwsIdentityDocumentUtils} from './aws-identity-document-utils'; +import {GcpMetadataConfig} from './gcp-metadata-config'; +import * as types from './types'; + +/** + * Represents gce_instance type monitored resource. + * For definition refer to + * https://cloud.google.com/monitoring/api/resources#tag_gce_instance + */ +export class GcpGceMonitoredResource implements types.MonitoredResource { + type = types.MonitoredResources.GCP_GCE_INSTANCE; + + getLabels() { + const gcp = new GcpMetadataConfig(); + return gcp.getGceMetadata(); + } +} + +/** + * Represents gke_container type monitored resource. + * For definition refer to + * https://cloud.google.com/monitoring/api/resources#tag_gke_container + */ +export class GcpGkeMonitoredResource implements types.MonitoredResource { + type = types.MonitoredResources.GCP_GKE_CONTAINER; + + getLabels() { + const gcp = new GcpMetadataConfig(); + return gcp.getGkeMetadata(); + } +} + +/** + * Represents aws_ec2_instance type monitored resource. + * For definition refer to + * https://cloud.google.com/monitoring/api/resources#tag_aws_ec2_instance + */ +export class AwsMonitoredResource implements types.MonitoredResource { + type = types.MonitoredResources.AWS_EC2_INSTANCE; + + getLabels() { + const aws = new AwsIdentityDocumentUtils(); + return aws.getMetadata(); + } +} diff --git a/packages/opencensus-core/src/common/monitored-resource/types.ts b/packages/opencensus-core/src/common/monitored-resource/types.ts new file mode 100644 index 000000000..d4dbdc786 --- /dev/null +++ b/packages/opencensus-core/src/common/monitored-resource/types.ts @@ -0,0 +1,75 @@ +export enum MonitoredResources { + GCP_GCE_INSTANCE = 'gce_instance', + GCP_GKE_CONTAINER = 'gke_container', + AWS_EC2_INSTANCE = 'aws_ec2_instance' +} + +export const monitoredResourceAttributes: Record< + string, Record> = { + /** + * GCE common attributes + * See: + * https://cloud.google.com/appengine/docs/flexible/python/runtime#environment_variables + */ + GCE: { + /** + * Numeric VM instance identifier assigned by Compute Engine. + */ + instance_id: 'instance/id', + /** + * ProjectID is the identifier of the GCP project associated with this + * resource, such as "my-project". + */ + project_id: 'project/project-id', + /** Compute Engine zone in which the VM is running. */ + zone: 'instance/zone', + }, + GKE: { + /** + * Name for the cluster container is running in. + */ + cluster_name: 'instance/attributes/cluster-name', + /** + * Numeric VM instance identifier assigned by Compute Engine. + */ + instance_id: 'instance/id', + /** + * ProjectID is the identifier of the GCP project associated with this + * resource, such as "my-project". + */ + project_id: 'project/project-id', + /** + * Compute Engine zone in which the VM is running. + */ + zone: 'instance/zone', + }, + AWS: { + /** + * The AWS region for the VM. The format of this field is "aws:{region}", + * where supported values for {region} are listed at + * http://docs.aws.amazon.com/general/latest/gr/rande.html. + */ + region: 'region', + /** + * The AWS account number for the VM. + */ + accountId: 'aws_account', + /** + * The instance id of the instance. + */ + instanceId: 'instance_id' + } +}; + +export type MonitoredResourceType = + MonitoredResources.GCP_GKE_CONTAINER| + MonitoredResources.GCP_GCE_INSTANCE|MonitoredResources.AWS_EC2_INSTANCE; +export type MonitoredResourceMetadata = + Promise|string>|Record|string; + +export interface MonitoredResource { + readonly type: MonitoredResourceType; + getLabels(): MonitoredResourceMetadata; +} + +export declare let monitoredResourceMetadata: Record; \ No newline at end of file diff --git a/packages/opencensus-core/src/common/monitored-resource/util.ts b/packages/opencensus-core/src/common/monitored-resource/util.ts new file mode 100644 index 000000000..9129d576e --- /dev/null +++ b/packages/opencensus-core/src/common/monitored-resource/util.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2018, OpenCensus 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 + * + * http://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 {AwsIdentityDocumentUtils} from './aws-identity-document-utils'; +import {GcpMetadataConfig} from './gcp-metadata-config'; +import * as resources from './monitored-resource'; +import {MonitoredResource} from './types'; + +/** + * Utilities for auto detecting monitored resource based on the + * environment where the application is running. + */ +export class MonitoredResourceUtil { + /** + * Returns a self-configured monitored resource, or null if the application + * is not running on a supported environment. + */ + static getDefaultResource(): Promise { + return GcpMetadataConfig.run().then(metadata => { + if (process.env['KUBERNETES_SERVICE_HOST']) { + return new resources.GcpGkeMonitoredResource(); + } + + if (GcpMetadataConfig.isRunning()) { + return new resources.GcpGceMonitoredResource(); + } + + return AwsIdentityDocumentUtils.run().then(() => { + if (AwsIdentityDocumentUtils.isRunning()) { + return new resources.AwsMonitoredResource(); + } + return Promise.resolve(null); + }); + }); + } +} diff --git a/packages/opencensus-core/src/index.ts b/packages/opencensus-core/src/index.ts index 66e65f59d..65326c08d 100644 --- a/packages/opencensus-core/src/index.ts +++ b/packages/opencensus-core/src/index.ts @@ -24,6 +24,7 @@ export * from './trace/instrumentation/types'; export * from './trace/propagation/types'; export * from './exporters/types'; export * from './common/types'; +export * from './common/monitored-resource/types'; // classes @@ -40,6 +41,9 @@ export * from './trace/instrumentation/base-plugin'; export * from './exporters/exporter-buffer'; export * from './exporters/console-exporter'; +// Monitored resource +export * from './common/monitored-resource/util'; + // STATS CLASSES // classes diff --git a/packages/opencensus-core/test/test-monitored-resource.ts b/packages/opencensus-core/test/test-monitored-resource.ts new file mode 100644 index 000000000..4fb5e99b4 --- /dev/null +++ b/packages/opencensus-core/test/test-monitored-resource.ts @@ -0,0 +1,154 @@ +/** + * Copyright 2018, OpenCensus 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 + * + * http://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 * as fs from 'fs'; +import * as nock from 'nock'; + +import * as logger from '../src/common/console-logger'; +import {AwsIdentityDocumentUtils} from '../src/common/monitored-resource/aws-identity-document-utils'; +import {GcpMetadataConfig} from '../src/common/monitored-resource/gcp-metadata-config'; +import * as resources from '../src/common/monitored-resource/monitored-resource'; +import {monitoredResourceAttributes, MonitoredResources} from '../src/common/monitored-resource/types'; +import {MonitoredResourceUtil} from '../src/common/monitored-resource/util'; + +describe('MonitoredResourceUtil get default resource', () => { + const testLogger = logger.logger(); + let dryrun = true; + const GOOGLE_APPLICATION_CREDENTIALS = + process.env.GOOGLE_APPLICATION_CREDENTIALS as string; + const OPENCENSUS_NETWORK_TESTS = + process.env.OPENCENSUS_NETWORK_TESTS as string; + let PROJECT_ID = 'fake-project-id'; + + const env = Object.assign({}, process.env); + const gcpUrl = GcpMetadataConfig.URL; + + before(() => { + if (GOOGLE_APPLICATION_CREDENTIALS) { + dryrun = !fs.existsSync(GOOGLE_APPLICATION_CREDENTIALS) && + !fs.existsSync(OPENCENSUS_NETWORK_TESTS); + if (!dryrun) { + const credentials = require(GOOGLE_APPLICATION_CREDENTIALS); + PROJECT_ID = credentials.project_id; + testLogger.debug( + 'GOOGLE_APPLICATION_CREDENTIALS: %s', + GOOGLE_APPLICATION_CREDENTIALS); + testLogger.debug('projectId = %s', PROJECT_ID); + } + } + if (dryrun) { + nock.disableNetConnect(); + } + testLogger.debug('dryrun=%s', dryrun); + GcpMetadataConfig.URL = 'fake.service.io'; + }); + + after(() => { + GcpMetadataConfig.URL = gcpUrl; + }); + + beforeEach(() => { + process.env = Object.assign({}, env); + nock.cleanAll(); + }); + + function doGcpMetadataConfigNock(key: string) { + const resource = monitoredResourceAttributes[key]; + Object.keys(resource).forEach(prop => { + const url = `http://${GcpMetadataConfig.URL}:${GcpMetadataConfig.PORT}`; + nock(url).persist().get('/' + resource[prop]).reply(200, resource[prop]); + }); + } + + describe('GcpGkeMonitoredResource', () => { + after(() => { + GcpMetadataConfig.metadata = {}; + GcpMetadataConfig.runned = false; + }); + + beforeEach(() => { + process.env['KUBERNETES_SERVICE_HOST'] = 'kubernetis.test'; + doGcpMetadataConfigNock('GKE'); + }); + + it('should initialize GCP GKE if KUBERNETES_SERVICE_HOST defined', + async () => { + await MonitoredResourceUtil.getDefaultResource().then(resource => { + assert.notEqual(resource, null); + assert.ok(resource instanceof resources.GcpGkeMonitoredResource); + assert.strictEqual( + resource.type, MonitoredResources.GCP_GKE_CONTAINER); + }); + }); + + it('should has metadata', async () => { + await MonitoredResourceUtil.getDefaultResource().then(resource => { + assert.deepEqual(resource.getLabels(), monitoredResourceAttributes.GKE); + }); + }); + }); + + describe('GcpGceMonitoredResource', () => { + after(() => { + GcpMetadataConfig.metadata = {}; + GcpMetadataConfig.runned = false; + }); + beforeEach(() => { + doGcpMetadataConfigNock('GCE'); + }); + it('should initialize GCP GCE if metadata is received', async () => { + await MonitoredResourceUtil.getDefaultResource().then(resource => { + assert.notEqual(resource, null); + assert.ok(resource instanceof resources.GcpGceMonitoredResource); + assert.strictEqual(resource.type, MonitoredResources.GCP_GCE_INSTANCE); + }); + }); + + it('should has metadata', async () => { + await MonitoredResourceUtil.getDefaultResource().then(resource => { + assert.deepEqual(resource.getLabels(), monitoredResourceAttributes.GCE); + }); + }); + }); + + describe('AwsMonitoredResource', () => { + function doNock() { + const resource = monitoredResourceAttributes.AWS; + const url = `http://${AwsIdentityDocumentUtils.HOST}:${ + AwsIdentityDocumentUtils.PORT}`; + nock(url).get(AwsIdentityDocumentUtils.PATH).reply(200, resource); + } + + it('should initialize AWS identity document util if is running on AWS', + async () => { + doNock(); + await MonitoredResourceUtil.getDefaultResource().then(resource => { + assert.notEqual(resource, null); + assert.ok(resource instanceof resources.AwsMonitoredResource); + assert.strictEqual( + resource.type, MonitoredResources.AWS_EC2_INSTANCE); + }); + }); + + it('should has metadata', async () => { + doNock(); + await MonitoredResourceUtil.getDefaultResource().then(resource => { + assert.deepEqual(resource.getLabels(), monitoredResourceAttributes.AWS); + }); + }); + }); +});