diff --git a/.circleci/checksum.sh b/.circleci/checksum.sh index af2e0f293e9..fa7cab9ae90 100644 --- a/.circleci/checksum.sh +++ b/.circleci/checksum.sh @@ -22,5 +22,6 @@ fi openssl md5 package.json >> $FILE find packages/*/package.json | xargs -I{} openssl md5 {} >> $FILE +find metapackages/*/package.json | xargs -I{} openssl md5 {} >> $FILE sort -o $FILE $FILE diff --git a/.circleci/config.yml b/.circleci/config.yml index e19df84e8a6..15b55c5a74c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,7 +5,7 @@ node_test_env: &node_test_env cache_1: &cache_1 - key: npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-F267A71D + key: npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-20B74F85 paths: - ./node_modules - ./package-lock.json @@ -25,13 +25,15 @@ cache_1: &cache_1 - packages/opentelemetry-web/node_modules cache_2: &cache_2 - key: npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-F267A71D + key: npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-20B74F85 paths: - packages/opentelemetry-plugin-grpc/node_modules - packages/opentelemetry-plugin-http/node_modules - packages/opentelemetry-plugin-https/node_modules - packages/opentelemetry-exporter-collector/node_modules - packages/opentelemetry-plugin-xml-http-request/node_modules + - packages/opentelemetry-resource-detector-aws/node_modules + - packages/opentelemetry-resource-detector-gcp/node_modules - packages/opentelemetry-resources/node_modules node_unit_tests: &node_unit_tests @@ -53,10 +55,10 @@ node_unit_tests: &node_unit_tests echo "CIRCLE_NODE_VERSION=${CIRCLE_NODE_VERSION}" - restore_cache: keys: - - npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-F267A71D + - npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-20B74F85 - restore_cache: keys: - - npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-F267A71D + - npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-20B74F85 - run: name: Install Root Dependencies command: npm install --ignore-scripts @@ -93,10 +95,10 @@ browsers_unit_tests: &browsers_unit_tests echo "CIRCLE_NODE_VERSION=${CIRCLE_NODE_VERSION}" - restore_cache: keys: - - npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-F267A71D + - npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-20B74F85 - restore_cache: keys: - - npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-F267A71D + - npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-20B74F85 - run: name: Install Root Dependencies command: npm install --ignore-scripts diff --git a/.github/workflows/backcompat.yml b/.github/workflows/backcompat.yml new file mode 100644 index 00000000000..58121302e99 --- /dev/null +++ b/.github/workflows/backcompat.yml @@ -0,0 +1,33 @@ +name: Backwards Compatability + +on: [push, pull_request] + +jobs: + types-node: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: restore lerna + uses: actions/cache@master # must use unreleased master to cache multiple paths + id: cache + with: + path: | + node_modules + packages/*/node_modules + metapackages/*/node_modules + key: ${{ runner.os }}-${{ hashFiles('**/package.json') }} + + - name: Bootstrap + if: steps.cache.outputs.cache-hit != 'true' + run: | + npm install --only=dev --ignore-scripts + npx lerna bootstrap --no-ci --ignore-scripts -- --only=dev + + - name: Install and Build API Dependencies + run: npx lerna bootstrap --no-ci --scope backcompat-* --include-filtered-dependencies + + - name: + run: | + npm run test:backcompat diff --git a/.github/workflows/canary.yaml b/.github/workflows/canary.yaml index 48e02abd3ac..beaf2e4609c 100644 --- a/.github/workflows/canary.yaml +++ b/.github/workflows/canary.yaml @@ -11,6 +11,11 @@ jobs: steps: - name: Checkout 🛎️ uses: actions/checkout@v2 + with: + # Fetch all history (needed for lerna / semantic release to correctly version) + fetch-depth: 0 + # pulls all tags (needed for lerna / semantic release to correctly version) + - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* - name: Set up Node.js uses: actions/setup-node@master with: @@ -35,6 +40,9 @@ jobs: npx lerna bootstrap --no-ci - name: Publish - run: npx lerna publish --canary --dist-tag canary --preid canary --yes + run: npx lerna publish --canary --yes env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + # Push new version tag to github + - run: git push --tags diff --git a/.gitignore b/.gitignore index 99005428477..7f151853412 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,7 @@ package.json.lerna_backup # VsCode configs .vscode/ + +#IDEA +.idea +*.iml diff --git a/.gitmodules b/.gitmodules index bebf3ea829e..1f72e80687e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ -[submodule "packages/opentelemetry-exporter-collector/src/platform/node/protos"] - path = packages/opentelemetry-exporter-collector/src/platform/node/protos +[submodule "packages/opentelemetry-exporter-collector-grpc/protos"] + path = packages/opentelemetry-exporter-collector-grpc/protos + url = https://github.com/open-telemetry/opentelemetry-proto.git +[submodule "packages/opentelemetry-exporter-collector-proto/protos"] + path = packages/opentelemetry-exporter-collector-proto/protos url = https://github.com/open-telemetry/opentelemetry-proto.git diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 813986c0558..1fcba1986ed 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,12 +86,13 @@ The `opentelemetry-js` project is written in TypeScript. - Maintainers may close, block, or put on hold pull requests even if they have strictly met these requirements. - No “changes requested” reviews. - No unresolved conversations. -- 4 approvals, including the approvals of both maintainers - - A pull request opened by an approver may be merged with only 3 reviews. +- 3 approvals, including the approvals of at least 2 maintainers + - A pull request opened by an approver may be merged with only 2 reviews. - Small (simple typo, URL, update docs, or grammatical fix) or high-priority changes may be merged more quickly or with fewer reviewers at the discretion of the maintainers. This is typically indicated with the express label. - For plugins, exporters, and propagators approval of the original code module author is preferred but not required. - New or changed functionality is tested by unit tests. - New or changed functionality is documented. +- Substantial changes should not be merged within 24 hours of opening in order to allow reviewers from all time zones to have a chance to review. ### Generating API documentation diff --git a/README.md b/README.md index 2a0659be6d4..f0247a6e02a 100644 --- a/README.md +++ b/README.md @@ -259,8 +259,8 @@ Apache 2.0 - See [LICENSE][license-url] for more information. [otel-contrib-plugin-ioredis]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/master/plugins/node/opentelemetry-plugin-ioredis [otel-contrib-plugin-mongodb]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/master/plugins/node/opentelemetry-plugin-mongodb [otel-contrib-plugin-mysql]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/master/plugins/node/opentelemetry-plugin-mysql -[otel-contrib-plugin-pg-pool]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/master/plugins/node/opentelemetry-plugin-postgres/opentelemetry-plugin-pg-pool -[otel-contrib-plugin-pg]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/master/plugins/node/opentelemetry-plugin-postgres/opentelemetry-plugin-pg +[otel-contrib-plugin-pg-pool]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/master/plugins/node/opentelemetry-plugin-pg-pool +[otel-contrib-plugin-pg]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/master/plugins/node/opentelemetry-plugin-pg [otel-contrib-plugin-redis]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/master/plugins/node/opentelemetry-plugin-redis [otel-contrib-plugin-express]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/master/plugins/node/opentelemetry-plugin-express [otel-contrib-plugin-user-interaction]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/master/plugins/web/opentelemetry-plugin-user-interaction diff --git a/backwards-compatability/node10/index.ts b/backwards-compatability/node10/index.ts new file mode 100644 index 00000000000..985d78f7d27 --- /dev/null +++ b/backwards-compatability/node10/index.ts @@ -0,0 +1,10 @@ +import {NodeSDK, api} from '@opentelemetry/sdk-node'; +import {ConsoleSpanExporter} from '@opentelemetry/tracing'; + +const sdk = new NodeSDK({ + traceExporter: new ConsoleSpanExporter(), + autoDetectResources: false, +}); +sdk.start(); + +api.trace.getTracer('test'); diff --git a/backwards-compatability/node10/package.json b/backwards-compatability/node10/package.json new file mode 100644 index 00000000000..2baf9ced1cd --- /dev/null +++ b/backwards-compatability/node10/package.json @@ -0,0 +1,20 @@ +{ + "name": "backcompat-node10", + "version": "1.0.0", + "private": true, + "description": "Backwards compatability app for node8 types and the OpenTelemetry Node.js SDK", + "main": "index.js", + "scripts": { + "test:backcompat": "tsc --noEmit index.ts" + }, + "dependencies": { + "@opentelemetry/sdk-node": "^0.10.2", + "@opentelemetry/tracing": "^0.10.2" + }, + "devDependencies": { + "@types/node": "^10.0.0", + "typescript": "^3.9.7" + }, + "author": "OpenTelemetry Authors", + "license": "Apache-2.0" +} diff --git a/backwards-compatability/node12/index.ts b/backwards-compatability/node12/index.ts new file mode 100644 index 00000000000..985d78f7d27 --- /dev/null +++ b/backwards-compatability/node12/index.ts @@ -0,0 +1,10 @@ +import {NodeSDK, api} from '@opentelemetry/sdk-node'; +import {ConsoleSpanExporter} from '@opentelemetry/tracing'; + +const sdk = new NodeSDK({ + traceExporter: new ConsoleSpanExporter(), + autoDetectResources: false, +}); +sdk.start(); + +api.trace.getTracer('test'); diff --git a/backwards-compatability/node12/package.json b/backwards-compatability/node12/package.json new file mode 100644 index 00000000000..6b9e008d52c --- /dev/null +++ b/backwards-compatability/node12/package.json @@ -0,0 +1,20 @@ +{ + "name": "backcompat-node12", + "version": "1.0.0", + "private": true, + "description": "Backwards compatability app for node8 types and the OpenTelemetry Node.js SDK", + "main": "index.js", + "scripts": { + "test:backcompat": "tsc --noEmit index.ts" + }, + "dependencies": { + "@opentelemetry/sdk-node": "^0.10.2", + "@opentelemetry/tracing": "^0.10.2" + }, + "devDependencies": { + "@types/node": "^12.0.0", + "typescript": "^3.9.7" + }, + "author": "OpenTelemetry Authors", + "license": "Apache-2.0" +} diff --git a/backwards-compatability/node8/index.ts b/backwards-compatability/node8/index.ts new file mode 100644 index 00000000000..985d78f7d27 --- /dev/null +++ b/backwards-compatability/node8/index.ts @@ -0,0 +1,10 @@ +import {NodeSDK, api} from '@opentelemetry/sdk-node'; +import {ConsoleSpanExporter} from '@opentelemetry/tracing'; + +const sdk = new NodeSDK({ + traceExporter: new ConsoleSpanExporter(), + autoDetectResources: false, +}); +sdk.start(); + +api.trace.getTracer('test'); diff --git a/backwards-compatability/node8/package.json b/backwards-compatability/node8/package.json new file mode 100644 index 00000000000..876d187879b --- /dev/null +++ b/backwards-compatability/node8/package.json @@ -0,0 +1,20 @@ +{ + "name": "backcompat-node8", + "version": "1.0.0", + "private": true, + "description": "Backwards compatability app for node8 types and the OpenTelemetry Node.js SDK", + "main": "index.js", + "scripts": { + "test:backcompat": "tsc --noEmit index.ts" + }, + "dependencies": { + "@opentelemetry/sdk-node": "^0.10.2", + "@opentelemetry/tracing": "^0.10.2" + }, + "devDependencies": { + "@types/node": "^8.0.0", + "typescript": "^3.9.7" + }, + "author": "OpenTelemetry Authors", + "license": "Apache-2.0" +} diff --git a/doc/batcher-api.md b/doc/batcher-api.md index 7c9092ff156..7cb7c9c0360 100644 --- a/doc/batcher-api.md +++ b/doc/batcher-api.md @@ -56,7 +56,7 @@ import { export class CustomBatcher extends UngroupedBatcher { aggregatorFor (metricDescriptor: MetricDescriptor) { - if (metricDescriptor.labels === 'metricToBeAveraged') { + if (metricDescriptor.name === 'requests') { return new AverageAggregator(10); } // this is exactly what the "UngroupedBatcher" does, we will re-use it @@ -138,7 +138,7 @@ const meter = new MeterProvider({ interval: 1000, }).getMeter('example-custom-batcher'); -const requestsLatency = meter.createMeasure('requests', { +const requestsLatency = meter.createValueRecorder('requests', { monotonic: true, description: 'Average latency' }); diff --git a/examples/collector-exporter-node/metrics.js b/examples/collector-exporter-node/metrics.js index f91e105c201..b6696778c8f 100644 --- a/examples/collector-exporter-node/metrics.js +++ b/examples/collector-exporter-node/metrics.js @@ -1,17 +1,21 @@ 'use strict'; +const { ConsoleLogger, LogLevel } = require('@opentelemetry/core'); const { CollectorMetricExporter } = require('@opentelemetry/exporter-collector'); +// const { CollectorMetricExporter } = require('@opentelemetry/exporter-collector-grpc'); +// const { CollectorMetricExporter } = require('@opentelemetry/exporter-collector-proto'); const { MeterProvider } = require('@opentelemetry/metrics'); const metricExporter = new CollectorMetricExporter({ serviceName: 'basic-metric-service', - // logger: new ConsoleLogger(LogLevel.DEBUG), + // url: 'http://localhost:55681/v1/metrics', + logger: new ConsoleLogger(LogLevel.DEBUG), }); const meter = new MeterProvider({ exporter: metricExporter, interval: 1000, -}).getMeter('example-prometheus'); +}).getMeter('example-exporter-collector'); const requestCounter = meter.createCounter('requests', { description: 'Example of a Counter', diff --git a/examples/collector-exporter-node/package.json b/examples/collector-exporter-node/package.json index 7dfeb695fb8..49ac1964341 100644 --- a/examples/collector-exporter-node/package.json +++ b/examples/collector-exporter-node/package.json @@ -31,6 +31,8 @@ "@opentelemetry/api": "^0.10.2", "@opentelemetry/core": "^0.10.2", "@opentelemetry/exporter-collector": "^0.10.2", + "@opentelemetry/exporter-collector-grpc": "^0.10.2", + "@opentelemetry/exporter-collector-proto": "^0.10.2", "@opentelemetry/metrics": "^0.10.2", "@opentelemetry/tracing": "^0.10.2" }, diff --git a/examples/collector-exporter-node/tracing.js b/examples/collector-exporter-node/tracing.js index 4e654c7e1b0..cb522c01ee0 100644 --- a/examples/collector-exporter-node/tracing.js +++ b/examples/collector-exporter-node/tracing.js @@ -3,7 +3,9 @@ const opentelemetry = require('@opentelemetry/api'); // const { ConsoleLogger, LogLevel} = require('@opentelemetry/core'); const { BasicTracerProvider, ConsoleSpanExporter, SimpleSpanProcessor } = require('@opentelemetry/tracing'); -const { CollectorTraceExporter, CollectorProtocolNode } = require('@opentelemetry/exporter-collector'); +const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector'); +// const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector-grpc'); +// const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector-proto'); const exporter = new CollectorTraceExporter({ serviceName: 'basic-service', @@ -11,8 +13,6 @@ const exporter = new CollectorTraceExporter({ // headers: { // foo: 'bar' // }, - protocolNode: CollectorProtocolNode.HTTP_PROTO, - // protocolNode: CollectorProtocolNode.HTTP_JSON, }); const provider = new BasicTracerProvider(); diff --git a/examples/tracer-web/examples/metrics/index.html b/examples/tracer-web/examples/metrics/index.html new file mode 100644 index 00000000000..7951c564ce5 --- /dev/null +++ b/examples/tracer-web/examples/metrics/index.html @@ -0,0 +1,36 @@ + + + + + + Metrics Example + + + + + + + + + + Example of using metrics with Collector Exporter + +
+ + +
+ +If you run the collector from example "opentelemetry-exporter-collector" you should see traces at:
+http://localhost:9090/ + + + + diff --git a/examples/tracer-web/examples/metrics/index.js b/examples/tracer-web/examples/metrics/index.js new file mode 100644 index 00000000000..6aefa8cdde3 --- /dev/null +++ b/examples/tracer-web/examples/metrics/index.js @@ -0,0 +1,51 @@ +'use strict'; + +const { ConsoleLogger, LogLevel } = require('@opentelemetry/core'); +const { CollectorMetricExporter } = require('@opentelemetry/exporter-collector'); +const { MeterProvider } = require('@opentelemetry/metrics'); + +const metricExporter = new CollectorMetricExporter({ + serviceName: 'basic-metric-service', + logger: new ConsoleLogger(LogLevel.DEBUG), +}); + +let interval; +let meter; + +function stopMetrics() { + console.log('STOPPING METRICS'); + clearInterval(interval); + meter.shutdown(); +} + +function startMetrics() { + console.log('STARTING METRICS'); + meter = new MeterProvider({ + exporter: metricExporter, + interval: 1000, + }).getMeter('example-exporter-collector'); + + const requestCounter = meter.createCounter('requests', { + description: 'Example of a Counter', + }); + + const upDownCounter = meter.createUpDownCounter('test_up_down_counter', { + description: 'Example of a UpDownCounter', + }); + + const labels = { pid: process.pid, environment: 'staging' }; + + interval = setInterval(() => { + requestCounter.bind(labels).add(1); + upDownCounter.bind(labels).add(Math.random() > 0.5 ? 1 : -1); + }, 1000); +} + +const addClickEvents = () => { + const startBtn = document.getElementById('startBtn'); + const stopBtn = document.getElementById('stopBtn'); + startBtn.addEventListener('click', startMetrics); + stopBtn.addEventListener('click', stopMetrics); +}; + +window.addEventListener('load', addClickEvents); diff --git a/examples/tracer-web/package.json b/examples/tracer-web/package.json index 606b33ec52a..f144e0464e6 100644 --- a/examples/tracer-web/package.json +++ b/examples/tracer-web/package.json @@ -37,9 +37,10 @@ "@opentelemetry/context-zone": "^0.10.2", "@opentelemetry/core": "^0.10.2", "@opentelemetry/exporter-collector": "^0.10.2", - "@opentelemetry/plugin-document-load": "^0.6.1", + "@opentelemetry/metrics": "^0.10.2", + "@opentelemetry/plugin-document-load": "^0.9.0", "@opentelemetry/plugin-fetch": "^0.10.2", - "@opentelemetry/plugin-user-interaction": "^0.6.1", + "@opentelemetry/plugin-user-interaction": "^0.9.0", "@opentelemetry/plugin-xml-http-request": "^0.10.2", "@opentelemetry/tracing": "^0.10.2", "@opentelemetry/web": "^0.10.2" diff --git a/examples/tracer-web/webpack.config.js b/examples/tracer-web/webpack.config.js index 3e1c14ef2f2..3859f2f8fd9 100644 --- a/examples/tracer-web/webpack.config.js +++ b/examples/tracer-web/webpack.config.js @@ -8,6 +8,7 @@ const common = { mode: 'development', entry: { 'document-load': 'examples/document-load/index.js', + metrics: 'examples/metrics/index.js', fetch: 'examples/fetch/index.js', 'xml-http-request': 'examples/xml-http-request/index.js', 'user-interaction': 'examples/user-interaction/index.js', diff --git a/integration-tests/propagation-validation-server/package.json b/integration-tests/propagation-validation-server/package.json index 6b07b78a7cc..8db10f01081 100644 --- a/integration-tests/propagation-validation-server/package.json +++ b/integration-tests/propagation-validation-server/package.json @@ -12,8 +12,8 @@ "@opentelemetry/context-async-hooks": "^0.10.2", "@opentelemetry/core": "^0.10.2", "@opentelemetry/tracing": "^0.10.2", - "axios": "^0.19.2", - "body-parser": "^1.19.0", - "express": "^4.17.1" + "axios": "0.19.2", + "body-parser": "1.19.0", + "express": "4.17.1" } } diff --git a/lerna.json b/lerna.json index addb3cac49a..1ac1a01554a 100644 --- a/lerna.json +++ b/lerna.json @@ -3,6 +3,7 @@ "npmClient": "npm", "packages": [ "benchmark/*", + "backwards-compatability/*", "metapackages/*", "packages/*", "integration-tests/*" diff --git a/package.json b/package.json index 84a81e169bb..22057d47711 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "compile": "lerna run compile", "test": "lerna run test", "test:browser": "lerna run test:browser", + "test:backcompat": "lerna run test:backcompat", "bootstrap": "lerna bootstrap", "bump": "lerna publish", "codecov": "lerna run codecov", @@ -42,8 +43,8 @@ "devDependencies": { "@commitlint/cli": "9.1.1", "@commitlint/config-conventional": "9.1.1", - "@typescript-eslint/eslint-plugin": "3.8.0", - "@typescript-eslint/parser": "3.8.0", + "@typescript-eslint/eslint-plugin": "3.9.0", + "@typescript-eslint/parser": "3.9.0", "beautify-benchmark": "0.2.4", "benchmark": "2.1.4", "eslint": "7.6.0", diff --git a/packages/opentelemetry-api/package.json b/packages/opentelemetry-api/package.json index de1e6f0827f..22edcdd6420 100644 --- a/packages/opentelemetry-api/package.json +++ b/packages/opentelemetry-api/package.json @@ -55,8 +55,9 @@ "@opentelemetry/context-base": "^0.10.2" }, "devDependencies": { - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", + "@types/sinon": "4.3.1", "@types/webpack-env": "1.15.2", "codecov": "3.7.2", "gts": "2.0.2", @@ -70,6 +71,7 @@ "linkinator": "2.1.1", "mocha": "7.2.0", "nyc": "15.1.0", + "sinon": "9.0.3", "ts-loader": "8.0.2", "ts-mocha": "7.0.0", "typedoc": "0.18.0", diff --git a/packages/opentelemetry-api/src/api/global-utils.ts b/packages/opentelemetry-api/src/api/global-utils.ts index a294f345d58..407399c8fbc 100644 --- a/packages/opentelemetry-api/src/api/global-utils.ts +++ b/packages/opentelemetry-api/src/api/global-utils.ts @@ -15,7 +15,7 @@ */ import { ContextManager } from '@opentelemetry/context-base'; -import { HttpTextPropagator } from '../context/propagation/HttpTextPropagator'; +import { TextMapPropagator } from '../context/propagation/TextMapPropagator'; import { MeterProvider } from '../metrics/MeterProvider'; import { TracerProvider } from '../trace/tracer_provider'; import { _globalThis } from '../platform'; @@ -35,7 +35,7 @@ type Get = (version: number) => T; type OtelGlobal = Partial<{ [GLOBAL_CONTEXT_MANAGER_API_KEY]: Get; [GLOBAL_METRICS_API_KEY]: Get; - [GLOBAL_PROPAGATION_API_KEY]: Get; + [GLOBAL_PROPAGATION_API_KEY]: Get; [GLOBAL_TRACE_API_KEY]: Get; }>; diff --git a/packages/opentelemetry-api/src/api/propagation.ts b/packages/opentelemetry-api/src/api/propagation.ts index 989e497b666..92a046a6590 100644 --- a/packages/opentelemetry-api/src/api/propagation.ts +++ b/packages/opentelemetry-api/src/api/propagation.ts @@ -16,8 +16,8 @@ import { Context } from '@opentelemetry/context-base'; import { defaultGetter, GetterFunction } from '../context/propagation/getter'; -import { HttpTextPropagator } from '../context/propagation/HttpTextPropagator'; -import { NOOP_HTTP_TEXT_PROPAGATOR } from '../context/propagation/NoopHttpTextPropagator'; +import { TextMapPropagator } from '../context/propagation/TextMapPropagator'; +import { NOOP_TEXT_MAP_PROPAGATOR } from '../context/propagation/NoopTextMapPropagator'; import { defaultSetter, SetterFunction } from '../context/propagation/setter'; import { ContextAPI } from './context'; import { @@ -50,9 +50,7 @@ export class PropagationAPI { /** * Set the current propagator. Returns the initialized propagator */ - public setGlobalPropagator( - propagator: HttpTextPropagator - ): HttpTextPropagator { + public setGlobalPropagator(propagator: TextMapPropagator): TextMapPropagator { if (_global[GLOBAL_PROPAGATION_API_KEY]) { // global propagator has already been set return this._getGlobalPropagator(); @@ -61,7 +59,7 @@ export class PropagationAPI { _global[GLOBAL_PROPAGATION_API_KEY] = makeGetter( API_BACKWARDS_COMPATIBILITY_VERSION, propagator, - NOOP_HTTP_TEXT_PROPAGATOR + NOOP_TEXT_MAP_PROPAGATOR ); return propagator; @@ -102,11 +100,11 @@ export class PropagationAPI { delete _global[GLOBAL_PROPAGATION_API_KEY]; } - private _getGlobalPropagator(): HttpTextPropagator { + private _getGlobalPropagator(): TextMapPropagator { return ( _global[GLOBAL_PROPAGATION_API_KEY]?.( API_BACKWARDS_COMPATIBILITY_VERSION - ) ?? NOOP_HTTP_TEXT_PROPAGATOR + ) ?? NOOP_TEXT_MAP_PROPAGATOR ); } } diff --git a/packages/opentelemetry-api/src/api/trace.ts b/packages/opentelemetry-api/src/api/trace.ts index 0ba58af77ae..f0c2005577a 100644 --- a/packages/opentelemetry-api/src/api/trace.ts +++ b/packages/opentelemetry-api/src/api/trace.ts @@ -15,8 +15,10 @@ */ import { NOOP_TRACER_PROVIDER } from '../trace/NoopTracerProvider'; +import { ProxyTracerProvider } from '../trace/ProxyTracerProvider'; import { Tracer } from '../trace/tracer'; import { TracerProvider } from '../trace/tracer_provider'; +import { isSpanContextValid } from '../trace/spancontext-utils'; import { API_BACKWARDS_COMPATIBILITY_VERSION, GLOBAL_TRACE_API_KEY, @@ -30,6 +32,8 @@ import { export class TraceAPI { private static _instance?: TraceAPI; + private _proxyTracerProvider = new ProxyTracerProvider(); + /** Empty private constructor prevents end users from constructing a new instance of the API */ private constructor() {} @@ -51,9 +55,11 @@ export class TraceAPI { return this.getTracerProvider(); } + this._proxyTracerProvider.setDelegate(provider); + _global[GLOBAL_TRACE_API_KEY] = makeGetter( API_BACKWARDS_COMPATIBILITY_VERSION, - provider, + this._proxyTracerProvider, NOOP_TRACER_PROVIDER ); @@ -66,7 +72,7 @@ export class TraceAPI { public getTracerProvider(): TracerProvider { return ( _global[GLOBAL_TRACE_API_KEY]?.(API_BACKWARDS_COMPATIBILITY_VERSION) ?? - NOOP_TRACER_PROVIDER + this._proxyTracerProvider ); } @@ -80,5 +86,8 @@ export class TraceAPI { /** Remove the global tracer provider */ public disable() { delete _global[GLOBAL_TRACE_API_KEY]; + this._proxyTracerProvider = new ProxyTracerProvider(); } + + public isSpanContextValid = isSpanContextValid; } diff --git a/packages/opentelemetry-api/src/common/Exception.ts b/packages/opentelemetry-api/src/common/Exception.ts new file mode 100644 index 00000000000..0fa98e741a1 --- /dev/null +++ b/packages/opentelemetry-api/src/common/Exception.ts @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +interface ExceptionWithCode { + code: string; + name?: string; + message?: string; + stack?: string; +} + +interface ExceptionWithMessage { + code?: string; + message: string; + name?: string; + stack?: string; +} + +interface ExceptionWithName { + code?: string; + message?: string; + name: string; + stack?: string; +} + +/** + * Defines Exception. + * + * string or an object with one of (message or name or code) and optional stack + */ +export type Exception = + | ExceptionWithCode + | ExceptionWithMessage + | ExceptionWithName + | string; diff --git a/packages/opentelemetry-api/src/context/propagation/NoopHttpTextPropagator.ts b/packages/opentelemetry-api/src/context/propagation/NoopTextMapPropagator.ts similarity index 79% rename from packages/opentelemetry-api/src/context/propagation/NoopHttpTextPropagator.ts rename to packages/opentelemetry-api/src/context/propagation/NoopTextMapPropagator.ts index 7782ba30b9c..c2155fadc98 100644 --- a/packages/opentelemetry-api/src/context/propagation/NoopHttpTextPropagator.ts +++ b/packages/opentelemetry-api/src/context/propagation/NoopTextMapPropagator.ts @@ -15,12 +15,12 @@ */ import { Context } from '@opentelemetry/context-base'; -import { HttpTextPropagator } from './HttpTextPropagator'; +import { TextMapPropagator } from './TextMapPropagator'; /** - * No-op implementations of {@link HttpTextPropagator}. + * No-op implementations of {@link TextMapPropagator}. */ -export class NoopHttpTextPropagator implements HttpTextPropagator { +export class NoopTextMapPropagator implements TextMapPropagator { /** Noop inject function does nothing */ inject(context: Context, carrier: unknown, setter: Function): void {} /** Noop extract function does nothing and returns the input context */ @@ -29,4 +29,4 @@ export class NoopHttpTextPropagator implements HttpTextPropagator { } } -export const NOOP_HTTP_TEXT_PROPAGATOR = new NoopHttpTextPropagator(); +export const NOOP_TEXT_MAP_PROPAGATOR = new NoopTextMapPropagator(); diff --git a/packages/opentelemetry-api/src/context/propagation/HttpTextPropagator.ts b/packages/opentelemetry-api/src/context/propagation/TextMapPropagator.ts similarity index 95% rename from packages/opentelemetry-api/src/context/propagation/HttpTextPropagator.ts rename to packages/opentelemetry-api/src/context/propagation/TextMapPropagator.ts index 6c6f61d6684..f56112495d7 100644 --- a/packages/opentelemetry-api/src/context/propagation/HttpTextPropagator.ts +++ b/packages/opentelemetry-api/src/context/propagation/TextMapPropagator.ts @@ -29,11 +29,11 @@ import { GetterFunction } from './getter'; * usually implemented via library-specific request interceptors, where the * client-side injects values and the server-side extracts them. */ -export interface HttpTextPropagator { +export interface TextMapPropagator { /** * Injects values from a given `Context` into a carrier. * - * OpenTelemetry defines a common set of format values (HttpTextPropagator), + * OpenTelemetry defines a common set of format values (TextMapPropagator), * and each has an expected `carrier` type. * * @param context the Context from which to extract values to transmit over diff --git a/packages/opentelemetry-api/src/index.ts b/packages/opentelemetry-api/src/index.ts index f9bcf406644..0a7e8bbd857 100644 --- a/packages/opentelemetry-api/src/index.ts +++ b/packages/opentelemetry-api/src/index.ts @@ -14,11 +14,12 @@ * limitations under the License. */ +export * from './common/Exception'; export * from './common/Logger'; export * from './common/Time'; export * from './context/propagation/getter'; -export * from './context/propagation/HttpTextPropagator'; -export * from './context/propagation/NoopHttpTextPropagator'; +export * from './context/propagation/TextMapPropagator'; +export * from './context/propagation/NoopTextMapPropagator'; export * from './context/propagation/setter'; export * from './correlation_context/CorrelationContext'; export * from './correlation_context/EntryValue'; @@ -39,6 +40,8 @@ export * from './trace/link'; export * from './trace/NoopSpan'; export * from './trace/NoopTracer'; export * from './trace/NoopTracerProvider'; +export * from './trace/ProxyTracer'; +export * from './trace/ProxyTracerProvider'; export * from './trace/Sampler'; export * from './trace/SamplingResult'; export * from './trace/span_context'; @@ -52,6 +55,12 @@ export * from './trace/trace_state'; export * from './trace/tracer_provider'; export * from './trace/tracer'; +export { + INVALID_SPANID, + INVALID_TRACEID, + INVALID_SPAN_CONTEXT, +} from './trace/spancontext-utils'; + export { Context } from '@opentelemetry/context-base'; import { ContextAPI } from './api/context'; diff --git a/packages/opentelemetry-api/src/metrics/BoundInstrument.ts b/packages/opentelemetry-api/src/metrics/BoundInstrument.ts index 4ffbd143c7e..0d5554771e6 100644 --- a/packages/opentelemetry-api/src/metrics/BoundInstrument.ts +++ b/packages/opentelemetry-api/src/metrics/BoundInstrument.ts @@ -14,9 +14,6 @@ * limitations under the License. */ -import { CorrelationContext } from '../correlation_context/CorrelationContext'; -import { SpanContext } from '../trace/span_context'; - /** An Instrument for Counter Metric. */ export interface BoundCounter { /** @@ -31,18 +28,8 @@ export interface BoundValueRecorder { /** * Records the given value to this value recorder. * @param value to record. - * @param correlationContext the correlationContext associated with the - * values. - * @param spanContext the {@link SpanContext} that identifies the {@link Span} - * which the values are associated with. */ record(value: number): void; - record(value: number, correlationContext: CorrelationContext): void; - record( - value: number, - correlationContext: CorrelationContext, - spanContext: SpanContext - ): void; } /** An Instrument for Base Observer */ diff --git a/packages/opentelemetry-api/src/metrics/Metric.ts b/packages/opentelemetry-api/src/metrics/Metric.ts index ee7fd29ab44..0022c2a5b63 100644 --- a/packages/opentelemetry-api/src/metrics/Metric.ts +++ b/packages/opentelemetry-api/src/metrics/Metric.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { CorrelationContext } from '../correlation_context/CorrelationContext'; -import { SpanContext } from '../trace/span_context'; import { BoundBaseObserver, BoundCounter, @@ -51,12 +49,6 @@ export interface MetricOptions { */ disabled?: boolean; - /** - * (Measure only, default true) Asserts that this metric will only accept - * non-negative values (e.g. disk usage). - */ - absolute?: boolean; - /** * Indicates the type of the recorded value. * @default {@link ValueType.DOUBLE} @@ -148,19 +140,6 @@ export interface ValueRecorder extends UnboundMetric { * Records the given value to this value recorder. */ record(value: number, labels?: Labels): void; - - record( - value: number, - labels: Labels, - correlationContext: CorrelationContext - ): void; - - record( - value: number, - labels: Labels, - correlationContext: CorrelationContext, - spanContext: SpanContext - ): void; } /** Base interface for the Observer metrics. */ diff --git a/packages/opentelemetry-api/src/metrics/NoopMeter.ts b/packages/opentelemetry-api/src/metrics/NoopMeter.ts index 47df1ab970d..765a0fd8ae3 100644 --- a/packages/opentelemetry-api/src/metrics/NoopMeter.ts +++ b/packages/opentelemetry-api/src/metrics/NoopMeter.ts @@ -131,32 +131,24 @@ export class NoopMetric implements UnboundMetric { } } -export class NoopCounterMetric extends NoopMetric +export class NoopCounterMetric + extends NoopMetric implements Counter { add(value: number, labels: Labels) { this.bind(labels).add(value); } } -export class NoopValueRecorderMetric extends NoopMetric +export class NoopValueRecorderMetric + extends NoopMetric implements ValueRecorder { - record( - value: number, - labels: Labels, - correlationContext?: CorrelationContext, - spanContext?: SpanContext - ) { - if (typeof correlationContext === 'undefined') { - this.bind(labels).record(value); - } else if (typeof spanContext === 'undefined') { - this.bind(labels).record(value, correlationContext); - } else { - this.bind(labels).record(value, correlationContext, spanContext); - } + record(value: number, labels: Labels) { + this.bind(labels).record(value); } } -export class NoopBaseObserverMetric extends NoopMetric +export class NoopBaseObserverMetric + extends NoopMetric implements BaseObserver { observation() { return { @@ -166,7 +158,8 @@ export class NoopBaseObserverMetric extends NoopMetric } } -export class NoopBatchObserverMetric extends NoopMetric +export class NoopBatchObserverMetric + extends NoopMetric implements BatchObserver {} export class NoopBoundCounter implements BoundCounter { diff --git a/packages/opentelemetry-api/src/trace/NoopSpan.ts b/packages/opentelemetry-api/src/trace/NoopSpan.ts index a2a3bcef9f6..5f599dfda99 100644 --- a/packages/opentelemetry-api/src/trace/NoopSpan.ts +++ b/packages/opentelemetry-api/src/trace/NoopSpan.ts @@ -14,20 +14,13 @@ * limitations under the License. */ +import { Exception } from '../common/Exception'; import { TimeInput } from '../common/Time'; import { Attributes } from './attributes'; import { Span } from './span'; import { SpanContext } from './span_context'; import { Status } from './status'; -import { TraceFlags } from './trace_flags'; - -export const INVALID_TRACE_ID = '0'; -export const INVALID_SPAN_ID = '0'; -const INVALID_SPAN_CONTEXT: SpanContext = { - traceId: INVALID_TRACE_ID, - spanId: INVALID_SPAN_ID, - traceFlags: TraceFlags.NONE, -}; +import { INVALID_SPAN_CONTEXT } from './spancontext-utils'; /** * The NoopSpan is the default {@link Span} that is used when no Span @@ -76,6 +69,9 @@ export class NoopSpan implements Span { isRecording(): boolean { return false; } + + // By default does nothing + recordException(exception: Exception, time?: TimeInput): void {} } export const NOOP_SPAN = new NoopSpan(); diff --git a/packages/opentelemetry-api/src/trace/ProxyTracer.ts b/packages/opentelemetry-api/src/trace/ProxyTracer.ts new file mode 100644 index 00000000000..e2216eed5e1 --- /dev/null +++ b/packages/opentelemetry-api/src/trace/ProxyTracer.ts @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Span, SpanOptions, Tracer } from '..'; +import { NOOP_TRACER } from './NoopTracer'; +import { ProxyTracerProvider } from './ProxyTracerProvider'; + +/** + * Proxy tracer provided by the proxy tracer provider + */ +export class ProxyTracer implements Tracer { + // When a real implementation is provided, this will be it + private _delegate?: Tracer; + + constructor( + private _provider: ProxyTracerProvider, + public readonly name: string, + public readonly version?: string + ) {} + + getCurrentSpan(): Span | undefined { + return this._getTracer().getCurrentSpan(); + } + + startSpan(name: string, options?: SpanOptions): Span { + return this._getTracer().startSpan(name, options); + } + + withSpan ReturnType>( + span: Span, + fn: T + ): ReturnType { + return this._getTracer().withSpan(span, fn); + } + + bind(target: T, span?: Span): T { + return this._getTracer().bind(target, span); + } + + /** + * Try to get a tracer from the proxy tracer provider. + * If the proxy tracer provider has no delegate, return a noop tracer. + */ + private _getTracer() { + if (this._delegate) { + return this._delegate; + } + + const tracer = this._provider.getDelegateTracer(this.name, this.version); + + if (!tracer) { + return NOOP_TRACER; + } + + this._delegate = tracer; + return this._delegate; + } +} diff --git a/packages/opentelemetry-api/src/trace/ProxyTracerProvider.ts b/packages/opentelemetry-api/src/trace/ProxyTracerProvider.ts new file mode 100644 index 00000000000..e124dc95066 --- /dev/null +++ b/packages/opentelemetry-api/src/trace/ProxyTracerProvider.ts @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Tracer } from './tracer'; +import { TracerProvider } from './tracer_provider'; +import { ProxyTracer } from './ProxyTracer'; +import { NOOP_TRACER_PROVIDER } from './NoopTracerProvider'; + +/** + * Tracer provider which provides {@link ProxyTracer}s. + * + * Before a delegate is set, tracers provided are NoOp. + * When a delegate is set, traces are provided from the delegate. + * When a delegate is set after tracers have already been provided, + * all tracers already provided will use the provided delegate implementation. + */ +export class ProxyTracerProvider implements TracerProvider { + private _delegate?: TracerProvider; + + /** + * Get a {@link ProxyTracer} + */ + getTracer(name: string, version?: string): Tracer { + return ( + this.getDelegateTracer(name, version) ?? + new ProxyTracer(this, name, version) + ); + } + + getDelegate(): TracerProvider { + return this._delegate ?? NOOP_TRACER_PROVIDER; + } + + /** + * Set the delegate tracer provider + */ + setDelegate(delegate: TracerProvider) { + this._delegate = delegate; + } + + getDelegateTracer(name: string, version?: string): Tracer | undefined { + return this._delegate?.getTracer(name, version); + } +} diff --git a/packages/opentelemetry-api/src/trace/span.ts b/packages/opentelemetry-api/src/trace/span.ts index 13124fff3c5..966cd1a95dd 100644 --- a/packages/opentelemetry-api/src/trace/span.ts +++ b/packages/opentelemetry-api/src/trace/span.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Exception } from '../common/Exception'; import { Attributes } from './attributes'; import { SpanContext } from './span_context'; import { Status } from './status'; @@ -114,4 +115,12 @@ export interface Span { * with the `AddEvent` operation and attributes using `setAttributes`. */ isRecording(): boolean; + + /** + * Sets exception as a span event + * @param exception the exception the only accepted values are string or Error + * @param [time] the time to set as Span's event time. If not provided, + * use the current time. + */ + recordException(exception: Exception, time?: TimeInput): void; } diff --git a/packages/opentelemetry-core/src/trace/spancontext-utils.ts b/packages/opentelemetry-api/src/trace/spancontext-utils.ts similarity index 55% rename from packages/opentelemetry-core/src/trace/spancontext-utils.ts rename to packages/opentelemetry-api/src/trace/spancontext-utils.ts index 31719aa6e18..ebcc3ce24fb 100644 --- a/packages/opentelemetry-core/src/trace/spancontext-utils.ts +++ b/packages/opentelemetry-api/src/trace/spancontext-utils.ts @@ -13,24 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { SpanContext } from './span_context'; +import { TraceFlags } from './trace_flags'; -import { SpanContext, TraceFlags } from '@opentelemetry/api'; - -export const INVALID_SPANID = '0'; -export const INVALID_TRACEID = '0'; +const VALID_TRACEID_REGEX = /^([0-9a-f]{32})$/i; +const VALID_SPANID_REGEX = /^[0-9a-f]{16}$/i; +export const INVALID_SPANID = '0000000000000000'; +export const INVALID_TRACEID = '00000000000000000000000000000000'; export const INVALID_SPAN_CONTEXT: SpanContext = { traceId: INVALID_TRACEID, spanId: INVALID_SPANID, traceFlags: TraceFlags.NONE, }; +export function isValidTraceId(traceId: string): boolean { + return VALID_TRACEID_REGEX.test(traceId) && traceId !== INVALID_TRACEID; +} + +export function isValidSpanId(spanId: string): boolean { + return VALID_SPANID_REGEX.test(spanId) && spanId !== INVALID_SPANID; +} + /** * Returns true if this {@link SpanContext} is valid. * @return true if this {@link SpanContext} is valid. */ -export function isValid(spanContext: SpanContext): boolean { +export function isSpanContextValid(spanContext: SpanContext): boolean { return ( - spanContext.traceId !== INVALID_TRACEID && - spanContext.spanId !== INVALID_SPANID + isValidTraceId(spanContext.traceId) && isValidSpanId(spanContext.spanId) ); } diff --git a/packages/opentelemetry-api/test/noop-implementations/noop-span.test.ts b/packages/opentelemetry-api/test/noop-implementations/noop-span.test.ts index 177d60d7bfe..8d224123a45 100644 --- a/packages/opentelemetry-api/test/noop-implementations/noop-span.test.ts +++ b/packages/opentelemetry-api/test/noop-implementations/noop-span.test.ts @@ -17,8 +17,8 @@ import * as assert from 'assert'; import { CanonicalCode, - INVALID_SPAN_ID, - INVALID_TRACE_ID, + INVALID_SPANID, + INVALID_TRACEID, NoopSpan, TraceFlags, } from '../../src'; @@ -45,8 +45,8 @@ describe('NoopSpan', () => { assert.ok(!span.isRecording()); assert.deepStrictEqual(span.context(), { - traceId: INVALID_TRACE_ID, - spanId: INVALID_SPAN_ID, + traceId: INVALID_TRACEID, + spanId: INVALID_SPANID, traceFlags: TraceFlags.NONE, }); span.end(); diff --git a/packages/opentelemetry-api/test/proxy-implementations/proxy-tracer.test.ts b/packages/opentelemetry-api/test/proxy-implementations/proxy-tracer.test.ts new file mode 100644 index 00000000000..7a110cbfa35 --- /dev/null +++ b/packages/opentelemetry-api/test/proxy-implementations/proxy-tracer.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { + NoopSpan, + NOOP_SPAN, + ProxyTracerProvider, + SpanKind, + TracerProvider, + ProxyTracer, + Tracer, + Span, + NoopTracer, +} from '../../src'; + +describe('ProxyTracer', () => { + let provider: ProxyTracerProvider; + + beforeEach(() => { + provider = new ProxyTracerProvider(); + }); + + describe('when no delegate is set', () => { + it('should return proxy tracers', () => { + const tracer = provider.getTracer('test'); + + assert.ok(tracer instanceof ProxyTracer); + }); + + it('startSpan should return Noop Spans', () => { + const tracer = provider.getTracer('test'); + + assert.deepStrictEqual(tracer.startSpan('span-name'), NOOP_SPAN); + assert.deepStrictEqual( + tracer.startSpan('span-name1', { kind: SpanKind.CLIENT }), + NOOP_SPAN + ); + assert.deepStrictEqual( + tracer.startSpan('span-name2', { + kind: SpanKind.CLIENT, + }), + NOOP_SPAN + ); + + assert.deepStrictEqual(tracer.getCurrentSpan(), NOOP_SPAN); + }); + }); + + describe('when delegate is set before getTracer', () => { + let delegate: TracerProvider; + const sandbox = sinon.createSandbox(); + let getTracerStub: sinon.SinonStub; + + beforeEach(() => { + getTracerStub = sandbox.stub().returns(new NoopTracer()); + delegate = { + getTracer: getTracerStub, + }; + provider.setDelegate(delegate); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return tracers directly from the delegate', () => { + const tracer = provider.getTracer('test', 'v0'); + + sandbox.assert.calledOnce(getTracerStub); + assert.strictEqual(getTracerStub.firstCall.returnValue, tracer); + assert.deepEqual(getTracerStub.firstCall.args, ['test', 'v0']); + }); + }); + + describe('when delegate is set after getTracer', () => { + let tracer: Tracer; + let delegate: TracerProvider; + let delegateSpan: Span; + let delegateTracer: Tracer; + + beforeEach(() => { + delegateSpan = new NoopSpan(); + delegateTracer = { + bind(target) { + return target; + }, + getCurrentSpan() { + return delegateSpan; + }, + startSpan() { + return delegateSpan; + }, + withSpan(span, fn) { + return fn(); + }, + }; + + tracer = provider.getTracer('test'); + + delegate = { + getTracer() { + return delegateTracer; + }, + }; + provider.setDelegate(delegate); + }); + + it('should create spans using the delegate tracer', () => { + const span = tracer.startSpan('test'); + + assert.strictEqual(span, delegateSpan); + }); + }); +}); diff --git a/packages/opentelemetry-core/test/trace/spancontext-utils.test.ts b/packages/opentelemetry-api/test/trace/spancontext-utils.test.ts similarity index 85% rename from packages/opentelemetry-core/test/trace/spancontext-utils.test.ts rename to packages/opentelemetry-api/test/trace/spancontext-utils.test.ts index 9ac47df5652..e194f1d38be 100644 --- a/packages/opentelemetry-core/test/trace/spancontext-utils.test.ts +++ b/packages/opentelemetry-api/test/trace/spancontext-utils.test.ts @@ -16,7 +16,7 @@ import * as assert from 'assert'; import * as context from '../../src/trace/spancontext-utils'; -import { TraceFlags } from '@opentelemetry/api'; +import { TraceFlags } from '../../src'; describe('spancontext-utils', () => { it('should return true for valid spancontext', () => { @@ -25,7 +25,7 @@ describe('spancontext-utils', () => { spanId: '6e0c63257de34c92', traceFlags: TraceFlags.NONE, }; - assert.ok(context.isValid(spanContext)); + assert.ok(context.isSpanContextValid(spanContext)); }); it('should return false when traceId is invalid', () => { @@ -34,7 +34,7 @@ describe('spancontext-utils', () => { spanId: '6e0c63257de34c92', traceFlags: TraceFlags.NONE, }; - assert.ok(!context.isValid(spanContext)); + assert.ok(!context.isSpanContextValid(spanContext)); }); it('should return false when spanId is invalid', () => { @@ -43,7 +43,7 @@ describe('spancontext-utils', () => { spanId: context.INVALID_SPANID, traceFlags: TraceFlags.NONE, }; - assert.ok(!context.isValid(spanContext)); + assert.ok(!context.isSpanContextValid(spanContext)); }); it('should return false when traceId & spanId is invalid', () => { @@ -52,6 +52,6 @@ describe('spancontext-utils', () => { spanId: context.INVALID_SPANID, traceFlags: TraceFlags.NONE, }; - assert.ok(!context.isValid(spanContext)); + assert.ok(!context.isSpanContextValid(spanContext)); }); }); diff --git a/packages/opentelemetry-context-async-hooks/package.json b/packages/opentelemetry-context-async-hooks/package.json index c600920b7c4..12334541239 100644 --- a/packages/opentelemetry-context-async-hooks/package.json +++ b/packages/opentelemetry-context-async-hooks/package.json @@ -42,7 +42,7 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "@types/shimmer": "1.0.1", "codecov": "3.7.2", diff --git a/packages/opentelemetry-context-base/package.json b/packages/opentelemetry-context-base/package.json index e3c0ecc80b2..908d83c88b6 100644 --- a/packages/opentelemetry-context-base/package.json +++ b/packages/opentelemetry-context-base/package.json @@ -44,7 +44,7 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "codecov": "3.7.2", "gts": "2.0.2", diff --git a/packages/opentelemetry-context-zone-peer-dep/package.json b/packages/opentelemetry-context-zone-peer-dep/package.json index b902a8e70d5..9d82bd42595 100644 --- a/packages/opentelemetry-context-zone-peer-dep/package.json +++ b/packages/opentelemetry-context-zone-peer-dep/package.json @@ -43,7 +43,7 @@ }, "devDependencies": { "@babel/core": "7.11.1", - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "@types/sinon": "9.0.4", "@types/webpack-env": "1.15.2", @@ -61,7 +61,7 @@ "mocha": "7.2.0", "nyc": "15.1.0", "rimraf": "3.0.2", - "sinon": "9.0.2", + "sinon": "9.0.3", "ts-loader": "8.0.2", "ts-mocha": "7.0.0", "ts-node": "8.10.2", diff --git a/packages/opentelemetry-context-zone/package.json b/packages/opentelemetry-context-zone/package.json index abeddc37248..7d1fb7113d5 100644 --- a/packages/opentelemetry-context-zone/package.json +++ b/packages/opentelemetry-context-zone/package.json @@ -40,7 +40,7 @@ }, "devDependencies": { "@babel/core": "7.11.1", - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "@types/sinon": "9.0.4", "@types/webpack-env": "1.15.2", @@ -55,7 +55,7 @@ "mocha": "7.2.0", "nyc": "15.1.0", "rimraf": "3.0.2", - "sinon": "9.0.2", + "sinon": "9.0.3", "ts-loader": "8.0.2", "ts-mocha": "7.0.0", "ts-node": "8.10.2", diff --git a/packages/opentelemetry-core/package.json b/packages/opentelemetry-core/package.json index bce617b5eea..f0b2832e065 100644 --- a/packages/opentelemetry-core/package.json +++ b/packages/opentelemetry-core/package.json @@ -52,9 +52,9 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", - "@types/semver": "7.3.1", + "@types/semver": "7.3.2", "@types/sinon": "9.0.4", "@types/webpack-env": "1.15.2", "codecov": "3.7.2", @@ -69,7 +69,7 @@ "mocha": "7.2.0", "nyc": "15.1.0", "rimraf": "3.0.2", - "sinon": "9.0.2", + "sinon": "9.0.3", "ts-loader": "8.0.2", "ts-mocha": "7.0.0", "ts-node": "8.10.2", diff --git a/packages/opentelemetry-core/src/context/context.ts b/packages/opentelemetry-core/src/context/context.ts index c3a6dc33f50..6edbfa0a432 100644 --- a/packages/opentelemetry-core/src/context/context.ts +++ b/packages/opentelemetry-core/src/context/context.ts @@ -26,6 +26,13 @@ export const ACTIVE_SPAN_KEY = Context.createKey( const EXTRACTED_SPAN_CONTEXT_KEY = Context.createKey( 'OpenTelemetry Context Key EXTRACTED_SPAN_CONTEXT' ); +/** + * Shared key for indicating if instrumentation should be suppressed beyond + * this current scope. + */ +export const SUPPRESS_INSTRUMENTATION_KEY = Context.createKey( + 'OpenTelemetry Context Key SUPPRESS_INSTRUMENTATION' +); /** * Return the active span if one exists @@ -84,3 +91,33 @@ export function getParentSpanContext( ): SpanContext | undefined { return getActiveSpan(context)?.context() || getExtractedSpanContext(context); } + +/** + * Sets value on context to indicate that instrumentation should + * be suppressed beyond this current scope. + * + * @param context context to set the suppress instrumentation value on. + */ +export function suppressInstrumentation(context: Context): Context { + return context.setValue(SUPPRESS_INSTRUMENTATION_KEY, true); +} + +/** + * Sets value on context to indicate that instrumentation should + * no-longer be suppressed beyond this current scope. + * + * @param context context to set the suppress instrumentation value on. + */ +export function unsuppressInstrumentation(context: Context): Context { + return context.setValue(SUPPRESS_INSTRUMENTATION_KEY, false); +} + +/** + * Return current suppress instrumentation value for the given context, + * if it exists. + * + * @param context context check for the suppress instrumentation value. + */ +export function isInstrumentationSuppressed(context: Context): boolean { + return Boolean(context.getValue(SUPPRESS_INSTRUMENTATION_KEY)); +} diff --git a/packages/opentelemetry-core/src/context/propagation/B3Propagator.ts b/packages/opentelemetry-core/src/context/propagation/B3Propagator.ts index 129e457cb6e..43da0b1189d 100644 --- a/packages/opentelemetry-core/src/context/propagation/B3Propagator.ts +++ b/packages/opentelemetry-core/src/context/propagation/B3Propagator.ts @@ -17,7 +17,7 @@ import { Context, GetterFunction, - HttpTextPropagator, + TextMapPropagator, SetterFunction, TraceFlags, } from '@opentelemetry/api'; @@ -120,7 +120,7 @@ function getTraceFlags( * Propagator for the B3 HTTP header format. * Based on: https://github.com/openzipkin/b3-propagation */ -export class B3Propagator implements HttpTextPropagator { +export class B3Propagator implements TextMapPropagator { inject(context: Context, carrier: unknown, setter: SetterFunction) { const spanContext = getParentSpanContext(context); if (!spanContext) return; diff --git a/packages/opentelemetry-core/src/context/propagation/HttpTraceContext.ts b/packages/opentelemetry-core/src/context/propagation/HttpTraceContext.ts index 2c732e6cd74..2ba75607d62 100644 --- a/packages/opentelemetry-core/src/context/propagation/HttpTraceContext.ts +++ b/packages/opentelemetry-core/src/context/propagation/HttpTraceContext.ts @@ -17,7 +17,7 @@ import { Context, GetterFunction, - HttpTextPropagator, + TextMapPropagator, SetterFunction, SpanContext, TraceFlags, @@ -27,8 +27,14 @@ import { getParentSpanContext, setExtractedSpanContext } from '../context'; export const TRACE_PARENT_HEADER = 'traceparent'; export const TRACE_STATE_HEADER = 'tracestate'; -const VALID_TRACE_PARENT_REGEX = /^(?!ff)[\da-f]{2}-([\da-f]{32})-([\da-f]{16})-([\da-f]{2})(-|$)/; + const VERSION = '00'; +const VERSION_PART_COUNT = 4; // Version 00 only allows the specific 4 fields. + +const VERSION_REGEX = /^(?!ff)[\da-f]{2}$/; +const TRACE_ID_REGEX = /^(?![0]{32})[\da-f]{32}$/; +const PARENT_ID_REGEX = /^(?![0]{16})[\da-f]{16}$/; +const FLAGS_REGEX = /^[\da-f]{2}$/; /** * Parses information from the [traceparent] span tag and converts it into {@link SpanContext} @@ -41,19 +47,33 @@ const VERSION = '00'; * For more information see {@link https://www.w3.org/TR/trace-context/} */ export function parseTraceParent(traceParent: string): SpanContext | null { - const match = traceParent.match(VALID_TRACE_PARENT_REGEX); + const trimmed = traceParent.trim(); + const traceParentParts = trimmed.split('-'); + + // Current version must be structured correctly. + // For future versions, we can grab just the parts we do support. if ( - !match || - match[1] === '00000000000000000000000000000000' || - match[2] === '0000000000000000' + traceParentParts[0] === VERSION && + traceParentParts.length !== VERSION_PART_COUNT ) { return null; } + const [version, traceId, parentId, flags] = traceParentParts; + const isValidParent = + VERSION_REGEX.test(version) && + TRACE_ID_REGEX.test(traceId) && + PARENT_ID_REGEX.test(parentId) && + FLAGS_REGEX.test(flags); + + if (!isValidParent) { + return null; + } + return { - traceId: match[1], - spanId: match[2], - traceFlags: parseInt(match[3], 16), + traceId: traceId, + spanId: parentId, + traceFlags: parseInt(flags, 16), }; } @@ -63,7 +83,7 @@ export function parseTraceParent(traceParent: string): SpanContext | null { * Based on the Trace Context specification: * https://www.w3.org/TR/trace-context/ */ -export class HttpTraceContext implements HttpTextPropagator { +export class HttpTraceContext implements TextMapPropagator { inject(context: Context, carrier: unknown, setter: SetterFunction) { const spanContext = getParentSpanContext(context); if (!spanContext) return; diff --git a/packages/opentelemetry-core/src/context/propagation/composite.ts b/packages/opentelemetry-core/src/context/propagation/composite.ts index 8b5d5d08573..e0adfb585ec 100644 --- a/packages/opentelemetry-core/src/context/propagation/composite.ts +++ b/packages/opentelemetry-core/src/context/propagation/composite.ts @@ -17,7 +17,7 @@ import { Context, GetterFunction, - HttpTextPropagator, + TextMapPropagator, Logger, SetterFunction, } from '@opentelemetry/api'; @@ -25,8 +25,8 @@ import { NoopLogger } from '../../common/NoopLogger'; import { CompositePropagatorConfig } from './types'; /** Combines multiple propagators into a single propagator. */ -export class CompositePropagator implements HttpTextPropagator { - private readonly _propagators: HttpTextPropagator[]; +export class CompositePropagator implements TextMapPropagator { + private readonly _propagators: TextMapPropagator[]; private readonly _logger: Logger; /** diff --git a/packages/opentelemetry-core/src/context/propagation/types.ts b/packages/opentelemetry-core/src/context/propagation/types.ts index 9b7b2213d7a..490c6ee56fd 100644 --- a/packages/opentelemetry-core/src/context/propagation/types.ts +++ b/packages/opentelemetry-core/src/context/propagation/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { HttpTextPropagator, Logger } from '@opentelemetry/api'; +import { TextMapPropagator, Logger } from '@opentelemetry/api'; /** Configuration object for composite propagator */ export interface CompositePropagatorConfig { @@ -23,7 +23,7 @@ export interface CompositePropagatorConfig { * list order. If a propagator later in the list writes the same context * key as a propagator earlier in the list, the later on will "win". */ - propagators?: HttpTextPropagator[]; + propagators?: TextMapPropagator[]; /** Instance of logger */ logger?: Logger; diff --git a/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts b/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts index 0101e901f55..c2db6651762 100644 --- a/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts +++ b/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts @@ -18,7 +18,7 @@ import { Context, CorrelationContext, GetterFunction, - HttpTextPropagator, + TextMapPropagator, SetterFunction, } from '@opentelemetry/api'; import { @@ -49,7 +49,7 @@ type KeyPair = { * Based on the Correlation Context specification: * https://w3c.github.io/correlation-context/ */ -export class HttpCorrelationContext implements HttpTextPropagator { +export class HttpCorrelationContext implements TextMapPropagator { inject(context: Context, carrier: unknown, setter: SetterFunction) { const correlationContext = getCorrelationContext(context); if (!correlationContext) return; @@ -91,13 +91,15 @@ export class HttpCorrelationContext implements HttpTextPropagator { return context; } const pairs = headerValue.split(ITEMS_SEPARATOR); - if (pairs.length == 1) return context; pairs.forEach(entry => { const keyPair = this._parsePairKeyValue(entry); if (keyPair) { correlationContext[keyPair.key] = { value: keyPair.value }; } }); + if (Object.entries(correlationContext).length === 0) { + return context; + } return setCorrelationContext(context, correlationContext); } @@ -107,7 +109,7 @@ export class HttpCorrelationContext implements HttpTextPropagator { const keyPairPart = valueProps.shift(); if (!keyPairPart) return; const keyPair = keyPairPart.split(KEY_PAIR_SEPARATOR); - if (keyPair.length <= 1) return; + if (keyPair.length != 2) return; const key = decodeURIComponent(keyPair[0].trim()); let value = decodeURIComponent(keyPair[1].trim()); if (valueProps.length > 0) { diff --git a/packages/opentelemetry-core/src/index.ts b/packages/opentelemetry-core/src/index.ts index d1bf17a580f..ecdf972421f 100644 --- a/packages/opentelemetry-core/src/index.ts +++ b/packages/opentelemetry-core/src/index.ts @@ -33,7 +33,6 @@ export * from './trace/sampler/AlwaysOffSampler'; export * from './trace/sampler/AlwaysOnSampler'; export * from './trace/sampler/ParentOrElseSampler'; export * from './trace/sampler/ProbabilitySampler'; -export * from './trace/spancontext-utils'; export * from './trace/TraceState'; export * from './trace/IdGenerator'; export * from './utils/url'; diff --git a/packages/opentelemetry-core/src/platform/browser/BasePlugin.ts b/packages/opentelemetry-core/src/platform/browser/BasePlugin.ts index 4ace89ab48e..bf6e227d8a0 100644 --- a/packages/opentelemetry-core/src/platform/browser/BasePlugin.ts +++ b/packages/opentelemetry-core/src/platform/browser/BasePlugin.ts @@ -23,7 +23,8 @@ import { import { BaseAbstractPlugin } from '../BaseAbstractPlugin'; /** This class represent the base to patch plugin. */ -export abstract class BasePlugin extends BaseAbstractPlugin +export abstract class BasePlugin + extends BaseAbstractPlugin implements Plugin { enable( moduleExports: T, diff --git a/packages/opentelemetry-core/src/platform/browser/RandomIdGenerator.ts b/packages/opentelemetry-core/src/platform/browser/RandomIdGenerator.ts index 7ab4ce1ea35..1094fe4e83b 100644 --- a/packages/opentelemetry-core/src/platform/browser/RandomIdGenerator.ts +++ b/packages/opentelemetry-core/src/platform/browser/RandomIdGenerator.ts @@ -15,50 +15,36 @@ */ import { IdGenerator } from '../../trace/IdGenerator'; -type WindowWithMsCrypto = Window & { - msCrypto?: Crypto; -}; -const cryptoLib = window.crypto || (window as WindowWithMsCrypto).msCrypto; const SPAN_ID_BYTES = 8; const TRACE_ID_BYTES = 16; -const randomBytesArray = new Uint8Array(TRACE_ID_BYTES); export class RandomIdGenerator implements IdGenerator { /** * Returns a random 16-byte trace ID formatted/encoded as a 32 lowercase hex * characters corresponding to 128 bits. */ - generateTraceId(): string { - cryptoLib.getRandomValues(randomBytesArray); - return this.toHex(randomBytesArray.slice(0, TRACE_ID_BYTES)); - } + generateTraceId = getIdGenerator(TRACE_ID_BYTES); /** * Returns a random 8-byte span ID formatted/encoded as a 16 lowercase hex * characters corresponding to 64 bits. */ - generateSpanId(): string { - cryptoLib.getRandomValues(randomBytesArray); - return this.toHex(randomBytesArray.slice(0, SPAN_ID_BYTES)); - } - - /** - * Get the hex string representation of a byte array - * - * @param byteArray - */ - private toHex(byteArray: Uint8Array) { - const chars: number[] = new Array(byteArray.length * 2); - const alpha = 'a'.charCodeAt(0) - 10; - const digit = '0'.charCodeAt(0); + generateSpanId = getIdGenerator(SPAN_ID_BYTES); +} - let p = 0; - for (let i = 0; i < byteArray.length; i++) { - let nibble = (byteArray[i] >>> 4) & 0xf; - chars[p++] = nibble > 9 ? nibble + alpha : nibble + digit; - nibble = byteArray[i] & 0xf; - chars[p++] = nibble > 9 ? nibble + alpha : nibble + digit; +const SHARED_CHAR_CODES_ARRAY = Array(32); +function getIdGenerator(bytes: number): () => string { + return function generateId() { + for (let i = 0; i < bytes * 2; i++) { + SHARED_CHAR_CODES_ARRAY[i] = Math.floor(Math.random() * 16) + 48; + // valid hex characters in the range 48-57 and 97-102 + if (SHARED_CHAR_CODES_ARRAY[i] >= 58) { + SHARED_CHAR_CODES_ARRAY[i] += 39; + } } - return String.fromCharCode.apply(null, chars); - } + return String.fromCharCode.apply( + null, + SHARED_CHAR_CODES_ARRAY.slice(0, bytes * 2) + ); + }; } diff --git a/packages/opentelemetry-core/src/platform/browser/ShutdownNotifier.ts b/packages/opentelemetry-core/src/platform/browser/ShutdownNotifier.ts new file mode 100644 index 00000000000..05ccc38e011 --- /dev/null +++ b/packages/opentelemetry-core/src/platform/browser/ShutdownNotifier.ts @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Adds an event listener to trigger a callback when an unload event in the window is detected + */ +export function notifyOnGlobalShutdown(cb: () => void): () => void { + window.addEventListener('unload', cb, { once: true }); + return function removeCallbackFromGlobalShutdown() { + window.removeEventListener('unload', cb, false); + }; +} + +/** + * Warning: meant for internal use only! Closes the current window, triggering the unload event + */ +export function _invokeGlobalShutdown() { + window.close(); +} diff --git a/packages/opentelemetry-core/src/platform/browser/index.ts b/packages/opentelemetry-core/src/platform/browser/index.ts index 85023842c4d..e14dac0d3bb 100644 --- a/packages/opentelemetry-core/src/platform/browser/index.ts +++ b/packages/opentelemetry-core/src/platform/browser/index.ts @@ -21,3 +21,4 @@ export * from './RandomIdGenerator'; export * from './performance'; export * from './sdk-info'; export * from './timer-util'; +export * from './ShutdownNotifier'; diff --git a/packages/opentelemetry-core/src/platform/node/BasePlugin.ts b/packages/opentelemetry-core/src/platform/node/BasePlugin.ts index d1ebc622cf4..3581cee96ac 100644 --- a/packages/opentelemetry-core/src/platform/node/BasePlugin.ts +++ b/packages/opentelemetry-core/src/platform/node/BasePlugin.ts @@ -27,7 +27,8 @@ import * as path from 'path'; import { BaseAbstractPlugin } from '../BaseAbstractPlugin'; /** This class represent the base to patch plugin. */ -export abstract class BasePlugin extends BaseAbstractPlugin +export abstract class BasePlugin + extends BaseAbstractPlugin implements Plugin { enable( moduleExports: T, diff --git a/packages/opentelemetry-core/src/platform/node/RandomIdGenerator.ts b/packages/opentelemetry-core/src/platform/node/RandomIdGenerator.ts index b262ab1da83..219bcd04645 100644 --- a/packages/opentelemetry-core/src/platform/node/RandomIdGenerator.ts +++ b/packages/opentelemetry-core/src/platform/node/RandomIdGenerator.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -import * as crypto from 'crypto'; import { IdGenerator } from '../../trace/IdGenerator'; - const SPAN_ID_BYTES = 8; const TRACE_ID_BYTES = 16; @@ -25,15 +23,33 @@ export class RandomIdGenerator implements IdGenerator { * Returns a random 16-byte trace ID formatted/encoded as a 32 lowercase hex * characters corresponding to 128 bits. */ - generateTraceId(): string { - return crypto.randomBytes(TRACE_ID_BYTES).toString('hex'); - } + generateTraceId = getIdGenerator(TRACE_ID_BYTES); /** * Returns a random 8-byte span ID formatted/encoded as a 16 lowercase hex * characters corresponding to 64 bits. */ - generateSpanId(): string { - return crypto.randomBytes(SPAN_ID_BYTES).toString('hex'); - } + generateSpanId = getIdGenerator(SPAN_ID_BYTES); +} + +const SHARED_BUFFER = Buffer.allocUnsafe(TRACE_ID_BYTES); +function getIdGenerator(bytes: number): () => string { + return function generateId() { + for (let i = 0; i < bytes / 4; i++) { + // unsigned right shift drops decimal part of the number + // it is required because if a number between 2**32 and 2**32 - 1 is generated, an out of range error is thrown by writeUInt32BE + SHARED_BUFFER.writeUInt32BE((Math.random() * 2 ** 32) >>> 0, i * 4); + } + + // If buffer is all 0, set the last byte to 1 to guarantee a valid w3c id is generated + for (let i = 0; i < bytes; i++) { + if (SHARED_BUFFER[i] > 0) { + break; + } else if (i === bytes - 1) { + SHARED_BUFFER[bytes - 1] = 1; + } + } + + return SHARED_BUFFER.toString('hex', 0, bytes); + }; } diff --git a/packages/opentelemetry-core/src/platform/node/ShutdownNotifier.ts b/packages/opentelemetry-core/src/platform/node/ShutdownNotifier.ts new file mode 100644 index 00000000000..f9868105aff --- /dev/null +++ b/packages/opentelemetry-core/src/platform/node/ShutdownNotifier.ts @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Adds an event listener to trigger a callback when a SIGTERM is detected in the process + */ +export function notifyOnGlobalShutdown(cb: () => void): () => void { + process.once('SIGTERM', cb); + return function removeCallbackFromGlobalShutdown() { + process.removeListener('SIGTERM', cb); + }; +} + +/** + * Warning: meant for internal use only! Sends a SIGTERM to the current process + */ +export function _invokeGlobalShutdown() { + process.kill(process.pid, 'SIGTERM'); +} diff --git a/packages/opentelemetry-core/src/platform/node/index.ts b/packages/opentelemetry-core/src/platform/node/index.ts index 85023842c4d..e14dac0d3bb 100644 --- a/packages/opentelemetry-core/src/platform/node/index.ts +++ b/packages/opentelemetry-core/src/platform/node/index.ts @@ -21,3 +21,4 @@ export * from './RandomIdGenerator'; export * from './performance'; export * from './sdk-info'; export * from './timer-util'; +export * from './ShutdownNotifier'; diff --git a/packages/opentelemetry-core/src/platform/node/timer-util.ts b/packages/opentelemetry-core/src/platform/node/timer-util.ts index 92668d5ba13..155562146f2 100644 --- a/packages/opentelemetry-core/src/platform/node/timer-util.ts +++ b/packages/opentelemetry-core/src/platform/node/timer-util.ts @@ -13,6 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export function unrefTimer(timer: NodeJS.Timeout): void { +export function unrefTimer(timer: NodeJS.Timer): void { timer.unref(); } diff --git a/packages/opentelemetry-core/src/trace/NoRecordingSpan.ts b/packages/opentelemetry-core/src/trace/NoRecordingSpan.ts index 57c91f41085..589ea5c1cf2 100644 --- a/packages/opentelemetry-core/src/trace/NoRecordingSpan.ts +++ b/packages/opentelemetry-core/src/trace/NoRecordingSpan.ts @@ -14,8 +14,11 @@ * limitations under the License. */ -import { SpanContext, NoopSpan } from '@opentelemetry/api'; -import { INVALID_SPAN_CONTEXT } from '../trace/spancontext-utils'; +import { + SpanContext, + NoopSpan, + INVALID_SPAN_CONTEXT, +} from '@opentelemetry/api'; /** * The NoRecordingSpan extends the {@link NoopSpan}, making all operations no-op diff --git a/packages/opentelemetry-core/src/trace/TraceState.ts b/packages/opentelemetry-core/src/trace/TraceState.ts index 0ef420dbe3d..c7c18802e71 100644 --- a/packages/opentelemetry-core/src/trace/TraceState.ts +++ b/packages/opentelemetry-core/src/trace/TraceState.ts @@ -68,10 +68,11 @@ export class TraceState implements api.TraceState { .split(LIST_MEMBERS_SEPARATOR) .reverse() // Store in reverse so new keys (.set(...)) will be placed at the beginning .reduce((agg: Map, part: string) => { - const i = part.indexOf(LIST_MEMBER_KEY_VALUE_SPLITTER); + const listMember = part.trim(); // Optional Whitespace (OWS) handling + const i = listMember.indexOf(LIST_MEMBER_KEY_VALUE_SPLITTER); if (i !== -1) { - const key = part.slice(0, i); - const value = part.slice(i + 1, part.length); + const key = listMember.slice(0, i); + const value = listMember.slice(i + 1, part.length); if (validateKey(key) && validateValue(value)) { agg.set(key, value); } else { diff --git a/packages/opentelemetry-core/test/context/HttpTraceContext.test.ts b/packages/opentelemetry-core/test/context/HttpTraceContext.test.ts index a4602e7da8c..cf2852112f6 100644 --- a/packages/opentelemetry-core/test/context/HttpTraceContext.test.ts +++ b/packages/opentelemetry-core/test/context/HttpTraceContext.test.ts @@ -147,6 +147,19 @@ describe('HttpTraceContext', () => { ); }); + it('should return null if matching version but extra fields (invalid)', () => { + // Version 00 (our current) consists of {version}-{traceId}-{parentId}-{flags} + carrier[TRACE_PARENT_HEADER] = + '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01-extra'; + + assert.deepStrictEqual( + getExtractedSpanContext( + httpTraceContext.extract(Context.ROOT_CONTEXT, carrier, defaultGetter) + ), + undefined + ); + }); + it('extracts traceparent from list of header', () => { carrier[TRACE_PARENT_HEADER] = [ '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01', @@ -245,6 +258,19 @@ describe('HttpTraceContext', () => { }); }); + it('should handle OWS in tracestate list members', () => { + carrier[TRACE_PARENT_HEADER] = + '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01'; + carrier[TRACE_STATE_HEADER] = 'foo=1 \t , \t bar=2, \t baz=3 '; + const extractedSpanContext = getExtractedSpanContext( + httpTraceContext.extract(Context.ROOT_CONTEXT, carrier, defaultGetter) + ); + + assert.deepStrictEqual(extractedSpanContext!.traceState!.get('foo'), '1'); + assert.deepStrictEqual(extractedSpanContext!.traceState!.get('bar'), '2'); + assert.deepStrictEqual(extractedSpanContext!.traceState!.get('baz'), '3'); + }); + it('should fail gracefully on bad responses from getter', () => { const ctx1 = httpTraceContext.extract( Context.ROOT_CONTEXT, diff --git a/packages/opentelemetry-core/test/context/composite.test.ts b/packages/opentelemetry-core/test/context/composite.test.ts index a26c6c5ac37..6047f40fba4 100644 --- a/packages/opentelemetry-core/test/context/composite.test.ts +++ b/packages/opentelemetry-core/test/context/composite.test.ts @@ -17,7 +17,7 @@ import { defaultGetter, defaultSetter, - HttpTextPropagator, + TextMapPropagator, SpanContext, } from '@opentelemetry/api'; import { Context } from '@opentelemetry/context-base'; @@ -154,7 +154,7 @@ describe('Composite Propagator', () => { }); }); -class ThrowingPropagator implements HttpTextPropagator { +class ThrowingPropagator implements TextMapPropagator { inject(context: Context, carrier: unknown) { throw new Error('this propagator throws'); } diff --git a/packages/opentelemetry-core/test/context/context.test.ts b/packages/opentelemetry-core/test/context/context.test.ts new file mode 100644 index 00000000000..1755727d493 --- /dev/null +++ b/packages/opentelemetry-core/test/context/context.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as assert from 'assert'; + +import { + SUPPRESS_INSTRUMENTATION_KEY, + suppressInstrumentation, + unsuppressInstrumentation, + isInstrumentationSuppressed, +} from '../../src/context/context'; +import { Context } from '@opentelemetry/api'; + +describe('Context Helpers', () => { + describe('suppressInstrumentation', () => { + it('should set suppress to true', () => { + const expectedValue = true; + const context = suppressInstrumentation(Context.ROOT_CONTEXT); + + const value = context.getValue(SUPPRESS_INSTRUMENTATION_KEY); + const boolValue = value as boolean; + + assert.equal(boolValue, expectedValue); + }); + }); + + describe('unsuppressInstrumentation', () => { + it('should set suppress to false', () => { + const expectedValue = false; + const context = unsuppressInstrumentation(Context.ROOT_CONTEXT); + + const value = context.getValue(SUPPRESS_INSTRUMENTATION_KEY); + const boolValue = value as boolean; + + assert.equal(boolValue, expectedValue); + }); + }); + + describe('isInstrumentationSuppressed', () => { + it('should get value as bool', () => { + const expectedValue = true; + const context = Context.ROOT_CONTEXT.setValue( + SUPPRESS_INSTRUMENTATION_KEY, + expectedValue + ); + + const value = isInstrumentationSuppressed(context); + + assert.equal(value, expectedValue); + }); + + describe('when suppress instrumentation set to null', () => { + const context = Context.ROOT_CONTEXT.setValue( + SUPPRESS_INSTRUMENTATION_KEY, + null + ); + + it('should return false', () => { + const value = isInstrumentationSuppressed(context); + + assert.equal(value, false); + }); + }); + + describe('when suppress instrumentation set to undefined', () => { + const context = Context.ROOT_CONTEXT.setValue( + SUPPRESS_INSTRUMENTATION_KEY, + undefined + ); + + it('should return false', () => { + const value = isInstrumentationSuppressed(context); + + assert.equal(value, false); + }); + }); + }); +}); diff --git a/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts b/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts index 8d294d83629..981f7a78c78 100644 --- a/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts +++ b/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts @@ -152,19 +152,49 @@ describe('HttpCorrelationContext', () => { }); it('should gracefully handle an invalid header', () => { - const testCases: Record = { - invalidNoKeyValuePair: '289371298nekjh2939299283jbk2b', - invalidDoubleEqual: 'key1==value;key2=value2', - invalidWrongKeyValueFormat: 'key1:value;key2=value2', - invalidDoubleSemicolon: 'key1:value;;key2=value2', + const testCases: Record< + string, + { + header: string; + correlationContext: CorrelationContext | undefined; + } + > = { + invalidNoKeyValuePair: { + header: '289371298nekjh2939299283jbk2b', + correlationContext: undefined, + }, + invalidDoubleEqual: { + header: 'key1==value;key2=value2', + correlationContext: undefined, + }, + invalidWrongKeyValueFormat: { + header: 'key1:value;key2=value2', + correlationContext: undefined, + }, + invalidDoubleSemicolon: { + header: 'key1:value;;key2=value2', + correlationContext: undefined, + }, + mixInvalidAndValidKeys: { + header: 'key1==value,key2=value2', + correlationContext: { + key2: { + value: 'value2', + }, + }, + }, }; Object.getOwnPropertyNames(testCases).forEach(testCase => { - carrier[CORRELATION_CONTEXT_HEADER] = testCases[testCase]; + carrier[CORRELATION_CONTEXT_HEADER] = testCases[testCase].header; const extractedSpanContext = getCorrelationContext( httpTraceContext.extract(Context.ROOT_CONTEXT, carrier, defaultGetter) ); - assert.deepStrictEqual(extractedSpanContext, undefined, testCase); + assert.deepStrictEqual( + extractedSpanContext, + testCases[testCase].correlationContext, + testCase + ); }); }); }); diff --git a/packages/opentelemetry-exporter-collector-grpc/.eslintignore b/packages/opentelemetry-exporter-collector-grpc/.eslintignore new file mode 100644 index 00000000000..378eac25d31 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/.eslintignore @@ -0,0 +1 @@ +build diff --git a/packages/opentelemetry-exporter-collector-grpc/.eslintrc.js b/packages/opentelemetry-exporter-collector-grpc/.eslintrc.js new file mode 100644 index 00000000000..fc4d0381204 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + "env": { + "mocha": true, + "commonjs": true, + "node": true, + }, + ...require('../../eslint.config.js') +} diff --git a/packages/opentelemetry-exporter-collector-grpc/.npmignore b/packages/opentelemetry-exporter-collector-grpc/.npmignore new file mode 100644 index 00000000000..9505ba9450f --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/packages/opentelemetry-exporter-collector-grpc/LICENSE b/packages/opentelemetry-exporter-collector-grpc/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/packages/opentelemetry-exporter-collector-grpc/README.md b/packages/opentelemetry-exporter-collector-grpc/README.md new file mode 100644 index 00000000000..a48e2fae3ec --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/README.md @@ -0,0 +1,141 @@ +# OpenTelemetry Collector Exporter for node with grpc + +[![Gitter chat][gitter-image]][gitter-url] +[![NPM Published Version][npm-img]][npm-url] +[![dependencies][dependencies-image]][dependencies-url] +[![devDependencies][devDependencies-image]][devDependencies-url] +[![Apache License][license-image]][license-image] + +This module provides exporter for web and node to be used with [opentelemetry-collector][opentelemetry-collector-url] - last tested with version **0.6.0**. + +## Installation + +```bash +npm install --save @opentelemetry/exporter-collector-grpc +``` + +## Traces in Node - GRPC + +The CollectorTraceExporter in Node expects the URL to only be the hostname. It will not work with `/v1/trace`. + +```js +const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/tracing'); +const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector-grpc'); + +const collectorOptions = { + serviceName: 'basic-service', + url: '' // url is optional and can be omitted - default is localhost:55680 +}; + +const provider = new BasicTracerProvider(); +const exporter = new CollectorTraceExporter(collectorOptions); +provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); + +provider.register(); + +``` + +By default, plaintext connection is used. In order to use TLS in Node.js, provide `credentials` option like so: + +```js +const fs = require('fs'); +const grpc = require('grpc'); +const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/tracing'); +const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector-grpc'); + +const collectorOptions = { + serviceName: 'basic-service', + url: '', // url is optional and can be omitted - default is localhost:55680 + credentials: grpc.credentials.createSsl( + fs.readFileSync('./ca.crt'), + fs.readFileSync('./client.key'), + fs.readFileSync('./client.crt') + ) +}; + +const provider = new BasicTracerProvider(); +const exporter = new CollectorTraceExporter(collectorOptions); +provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); + +provider.register(); +``` + +To see how to generate credentials, you can refer to the script used to generate certificates for tests [here](./test/certs/regenerate.sh) + +The exporter can be configured to send custom metadata with each request as in the example below: + +```js +const grpc = require('grpc'); +const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/tracing'); +const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector-grpc'); + +const metadata = new grpc.Metadata(); +metadata.set('k', 'v'); + +const collectorOptions = { + serviceName: 'basic-service', + url: '', // url is optional and can be omitted - default is localhost:55680 + metadata, // // an optional grpc.Metadata object to be sent with each request +}; + +const provider = new BasicTracerProvider(); +const exporter = new CollectorTraceExporter(collectorOptions); +provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); + +provider.register(); +``` + +Note, that this will only work if TLS is also configured on the server. + +## Metrics in Node - GRPC + +The CollectorTraceExporter in Node expects the URL to only be the hostname. It will not work with `/v1/metrics`. All options that work with trace also work with metrics. + +```js +const { MeterProvider } = require('@opentelemetry/metrics'); +const { CollectorMetricExporter } = require('@opentelemetry/exporter-collector-grpc'); +const collectorOptions = { + serviceName: 'basic-service', + url: '', // url is optional and can be omitted - default is localhost:55681 +}; +const exporter = new CollectorMetricExporter(collectorOptions); + +// Register the exporter +const meter = new MeterProvider({ + exporter, + interval: 60000, +}).getMeter('example-meter'); + +// Now, start recording data +const counter = meter.createCounter('metric_name'); +counter.add(10, { 'key': 'value' }); + +``` + +## Running opentelemetry-collector locally to see the traces + +1. Go to examples/collector-exporter-node +2. run `npm run docker:start` +3. Open page at `http://localhost:9411/zipkin/` to observe the traces + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us on [gitter][gitter-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[gitter-image]: https://badges.gitter.im/open-telemetry/opentelemetry-js.svg +[gitter-url]: https://gitter.im/open-telemetry/opentelemetry-node?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge +[license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/master/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[dependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/status.svg?path=packages/opentelemetry-exporter-collector-grpc +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-exporter-collector-grpc +[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-exporter-collector-grpc +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-exporter-collector-grpc&type=dev +[npm-url]: https://www.npmjs.com/package/@opentelemetry/exporter-collector-grpc +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fexporter-collector-grpc.svg +[opentelemetry-collector-url]: https://github.com/open-telemetry/opentelemetry-collector diff --git a/packages/opentelemetry-exporter-collector-grpc/package.json b/packages/opentelemetry-exporter-collector-grpc/package.json new file mode 100644 index 00000000000..9810965cdab --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/package.json @@ -0,0 +1,76 @@ +{ + "name": "@opentelemetry/exporter-collector-grpc", + "version": "0.10.2", + "description": "OpenTelemetry Collector Exporter allows user to send collected traces to the OpenTelemetry Collector", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js", + "scripts": { + "clean": "rimraf build/*", + "compile": "npm run version:update && tsc -p .", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "postcompile": "npm run submodule && npm run protos:copy", + "precompile": "tsc --version", + "prepare": "npm run compile", + "protos:copy": "cpx protos/opentelemetry/**/*.* build/protos/opentelemetry", + "submodule": "git submodule sync --recursive && git submodule update --init --recursive", + "tdd": "npm run test -- --watch-extensions ts --watch", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", + "version:update": "node ../../scripts/version-update.js", + "watch": "npm run protos:copy && tsc -w" + }, + "keywords": [ + "opentelemetry", + "nodejs", + "grpc", + "tracing", + "profiling", + "metrics", + "stats" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts", + "build/src/**/*.proto", + "doc", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@babel/core": "7.11.1", + "@types/mocha": "8.0.2", + "@types/node": "14.0.27", + "@types/sinon": "9.0.4", + "codecov": "3.7.2", + "cpx": "1.5.0", + "gts": "2.0.2", + "mocha": "7.2.0", + "nyc": "15.1.0", + "rimraf": "3.0.2", + "sinon": "9.0.3", + "ts-loader": "8.0.2", + "ts-mocha": "7.0.0", + "ts-node": "8.10.2", + "typescript": "3.9.7" + }, + "dependencies": { + "@grpc/proto-loader": "^0.5.4", + "@opentelemetry/api": "^0.10.2", + "@opentelemetry/core": "^0.10.2", + "@opentelemetry/exporter-collector": "^0.10.2", + "@opentelemetry/metrics": "^0.10.2", + "@opentelemetry/resources": "^0.10.2", + "@opentelemetry/tracing": "^0.10.2", + "grpc": "^1.24.2" + } +} diff --git a/packages/opentelemetry-exporter-collector-grpc/protos b/packages/opentelemetry-exporter-collector-grpc/protos new file mode 160000 index 00000000000..e43e1abc404 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/protos @@ -0,0 +1 @@ +Subproject commit e43e1abc40428a6ee98e3bfd79bec1dfa2ed18cd diff --git a/packages/opentelemetry-exporter-collector-grpc/src/CollectorExporterNodeBase.ts b/packages/opentelemetry-exporter-collector-grpc/src/CollectorExporterNodeBase.ts new file mode 100644 index 00000000000..b00c6b0c561 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/src/CollectorExporterNodeBase.ts @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + CollectorExporterBase, + collectorTypes, +} from '@opentelemetry/exporter-collector'; +import type { Metadata } from 'grpc'; +import { + CollectorExporterConfigNode, + GRPCQueueItem, + ServiceClientType, +} from './types'; +import { ServiceClient } from './types'; + +/** + * Collector Metric Exporter abstract base class + */ +export abstract class CollectorExporterNodeBase< + ExportItem, + ServiceRequest +> extends CollectorExporterBase< + CollectorExporterConfigNode, + ExportItem, + ServiceRequest +> { + grpcQueue: GRPCQueueItem[] = []; + metadata?: Metadata; + serviceClient?: ServiceClient = undefined; + private _send!: Function; + + constructor(config: CollectorExporterConfigNode = {}) { + super(config); + if (config.headers) { + this.logger.warn('Headers cannot be set when using grpc'); + } + this.metadata = config.metadata; + } + + onInit(config: CollectorExporterConfigNode): void { + this._isShutdown = false; + // defer to next tick and lazy load to avoid loading grpc too early + // and making this impossible to be instrumented + setImmediate(() => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { onInit } = require('./util'); + onInit(this, config); + }); + } + + send( + objects: ExportItem[], + onSuccess: () => void, + onError: (error: collectorTypes.CollectorExporterError) => void + ): void { + if (this._isShutdown) { + this.logger.debug('Shutdown already started. Cannot send objects'); + return; + } + if (!this._send) { + // defer to next tick and lazy load to avoid loading grpc too early + // and making this impossible to be instrumented + setImmediate(() => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { send } = require('./util'); + this._send = send; + this._send(this, objects, onSuccess, onError); + }); + } else { + this._send(this, objects, onSuccess, onError); + } + } + + onShutdown(): void { + this._isShutdown = true; + if (this.serviceClient) { + this.serviceClient.close(); + } + } + + abstract getServiceProtoPath(): string; + abstract getServiceClientType(): ServiceClientType; +} diff --git a/packages/opentelemetry-exporter-collector-grpc/src/CollectorMetricExporter.ts b/packages/opentelemetry-exporter-collector-grpc/src/CollectorMetricExporter.ts new file mode 100644 index 00000000000..33fd740f10d --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/src/CollectorMetricExporter.ts @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + collectorTypes, + toCollectorExportMetricServiceRequest, +} from '@opentelemetry/exporter-collector'; +import { MetricRecord, MetricExporter } from '@opentelemetry/metrics'; +import { CollectorExporterConfigNode, ServiceClientType } from './types'; +import { CollectorExporterNodeBase } from './CollectorExporterNodeBase'; + +const DEFAULT_SERVICE_NAME = 'collector-metric-exporter'; +const DEFAULT_COLLECTOR_URL = 'localhost:55680'; + +/** + * Collector Metric Exporter for Node + */ +export class CollectorMetricExporter + extends CollectorExporterNodeBase< + MetricRecord, + collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest + > + implements MetricExporter { + // Converts time to nanoseconds + protected readonly _startTime = new Date().getTime() * 1000000; + + convert( + metrics: MetricRecord[] + ): collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest { + return toCollectorExportMetricServiceRequest( + metrics, + this._startTime, + this + ); + } + + getDefaultUrl(config: CollectorExporterConfigNode): string { + if (!config.url) { + return DEFAULT_COLLECTOR_URL; + } + return config.url; + } + + getDefaultServiceName(config: CollectorExporterConfigNode): string { + return config.serviceName || DEFAULT_SERVICE_NAME; + } + + getServiceClientType() { + return ServiceClientType.METRICS; + } + + getServiceProtoPath(): string { + return 'opentelemetry/proto/collector/metrics/v1/metrics_service.proto'; + } +} diff --git a/packages/opentelemetry-exporter-collector-grpc/src/CollectorTraceExporter.ts b/packages/opentelemetry-exporter-collector-grpc/src/CollectorTraceExporter.ts new file mode 100644 index 00000000000..2b607aaba64 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/src/CollectorTraceExporter.ts @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ReadableSpan, SpanExporter } from '@opentelemetry/tracing'; +import { CollectorExporterNodeBase } from './CollectorExporterNodeBase'; +import { + collectorTypes, + toCollectorExportTraceServiceRequest, +} from '@opentelemetry/exporter-collector'; +import { CollectorExporterConfigNode, ServiceClientType } from './types'; + +const DEFAULT_SERVICE_NAME = 'collector-trace-exporter'; +const DEFAULT_COLLECTOR_URL = 'localhost:55680'; + +/** + * Collector Trace Exporter for Node + */ +export class CollectorTraceExporter + extends CollectorExporterNodeBase< + ReadableSpan, + collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest + > + implements SpanExporter { + convert( + spans: ReadableSpan[] + ): collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest { + return toCollectorExportTraceServiceRequest(spans, this); + } + + getDefaultUrl(config: CollectorExporterConfigNode): string { + if (!config.url) { + return DEFAULT_COLLECTOR_URL; + } + return config.url; + } + + getDefaultServiceName(config: CollectorExporterConfigNode): string { + return config.serviceName || DEFAULT_SERVICE_NAME; + } + + getServiceClientType() { + return ServiceClientType.SPANS; + } + + getServiceProtoPath(): string { + return 'opentelemetry/proto/collector/trace/v1/trace_service.proto'; + } +} diff --git a/packages/opentelemetry-exporter-collector-grpc/src/index.ts b/packages/opentelemetry-exporter-collector-grpc/src/index.ts new file mode 100644 index 00000000000..fcbe012b52b --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/src/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './CollectorTraceExporter'; +export * from './CollectorMetricExporter'; diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/types.ts b/packages/opentelemetry-exporter-collector-grpc/src/types.ts similarity index 82% rename from packages/opentelemetry-exporter-collector/src/platform/node/types.ts rename to packages/opentelemetry-exporter-collector-grpc/src/types.ts index 59992146daf..aa8f98a0a4f 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/node/types.ts +++ b/packages/opentelemetry-exporter-collector-grpc/src/types.ts @@ -14,12 +14,8 @@ * limitations under the License. */ +import { collectorTypes } from '@opentelemetry/exporter-collector'; import * as grpc from 'grpc'; -import { CollectorProtocolNode } from '../../enums'; -import { - CollectorExporterError, - CollectorExporterConfigBase, -} from '../../types'; /** * Queue item to be used to save temporary spans/metrics in case the GRPC service @@ -28,7 +24,7 @@ import { export interface GRPCQueueItem { objects: ExportedItem[]; onSuccess: () => void; - onError: (error: CollectorExporterError) => void; + onError: (error: collectorTypes.CollectorExporterError) => void; } /** @@ -46,8 +42,12 @@ export interface ServiceClient extends grpc.Client { * Collector Exporter Config for Node */ export interface CollectorExporterConfigNode - extends CollectorExporterConfigBase { + extends collectorTypes.CollectorExporterConfigBase { credentials?: grpc.ChannelCredentials; metadata?: grpc.Metadata; - protocolNode?: CollectorProtocolNode; +} + +export enum ServiceClientType { + SPANS, + METRICS, } diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/utilWithGrpc.ts b/packages/opentelemetry-exporter-collector-grpc/src/util.ts similarity index 87% rename from packages/opentelemetry-exporter-collector/src/platform/node/utilWithGrpc.ts rename to packages/opentelemetry-exporter-collector-grpc/src/util.ts index 8d1e3d1e334..b1800665426 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/node/utilWithGrpc.ts +++ b/packages/opentelemetry-exporter-collector-grpc/src/util.ts @@ -15,16 +15,18 @@ */ import * as protoLoader from '@grpc/proto-loader'; +import { collectorTypes } from '@opentelemetry/exporter-collector'; import * as grpc from 'grpc'; import * as path from 'path'; -import { ServiceClientType } from '../../types'; -import * as collectorTypes from '../../types'; -import { CollectorExporterConfigNode, GRPCQueueItem } from './types'; -import { removeProtocol } from './util'; +import { + CollectorExporterConfigNode, + GRPCQueueItem, + ServiceClientType, +} from './types'; import { CollectorExporterNodeBase } from './CollectorExporterNodeBase'; -export function initWithGrpc( +export function onInit( collector: CollectorExporterNodeBase, config: CollectorExporterConfigNode ): void { @@ -33,7 +35,7 @@ export function initWithGrpc( const credentials: grpc.ChannelCredentials = config.credentials || grpc.credentials.createInsecure(); - const includeDirs = [path.resolve(__dirname, 'protos')]; + const includeDirs = [path.resolve(__dirname, '..', 'protos')]; protoLoader .load(collector.getServiceProtoPath(), { @@ -68,7 +70,7 @@ export function initWithGrpc( }); } -export function sendWithGrpc( +export function send( collector: CollectorExporterNodeBase, objects: ExportItem[], onSuccess: () => void, @@ -98,3 +100,7 @@ export function sendWithGrpc( }); } } + +function removeProtocol(url: string): string { + return url.replace(/^https?:\/\//, ''); +} diff --git a/packages/opentelemetry-exporter-collector-grpc/src/version.ts b/packages/opentelemetry-exporter-collector-grpc/src/version.ts new file mode 100644 index 00000000000..ea45ee2fc46 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/src/version.ts @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// this is autogenerated file, see scripts/version-update.js +export const VERSION = '0.10.2'; diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/README.md b/packages/opentelemetry-exporter-collector-grpc/submodule.md similarity index 100% rename from packages/opentelemetry-exporter-collector/src/platform/node/README.md rename to packages/opentelemetry-exporter-collector-grpc/submodule.md diff --git a/packages/opentelemetry-exporter-collector-grpc/test/CollectorMetricExporter.test.ts b/packages/opentelemetry-exporter-collector-grpc/test/CollectorMetricExporter.test.ts new file mode 100644 index 00000000000..901093f6b6b --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/test/CollectorMetricExporter.test.ts @@ -0,0 +1,238 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as protoLoader from '@grpc/proto-loader'; +import * as grpc from 'grpc'; +import * as path from 'path'; +import * as fs from 'fs'; + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { collectorTypes } from '@opentelemetry/exporter-collector'; +import { MetricRecord } from '@opentelemetry/metrics'; +import { CollectorMetricExporter } from '../src'; +import { + mockCounter, + mockObserver, + mockHistogram, + ensureExportedCounterIsCorrect, + ensureExportedObserverIsCorrect, + ensureMetadataIsCorrect, + ensureResourceIsCorrect, + ensureExportedHistogramIsCorrect, + ensureExportedValueRecorderIsCorrect, + mockValueRecorder, +} from './helper'; +import { ConsoleLogger, LogLevel } from '@opentelemetry/core'; + +const metricsServiceProtoPath = + 'opentelemetry/proto/collector/metrics/v1/metrics_service.proto'; +const includeDirs = [path.resolve(__dirname, '../protos')]; + +const address = 'localhost:1501'; + +type TestParams = { + useTLS?: boolean; + metadata?: grpc.Metadata; +}; + +const metadata = new grpc.Metadata(); +metadata.set('k', 'v'); + +const testCollectorMetricExporter = (params: TestParams) => + describe(`CollectorMetricExporter - node ${ + params.useTLS ? 'with' : 'without' + } TLS, ${params.metadata ? 'with' : 'without'} metadata`, () => { + let collectorExporter: CollectorMetricExporter; + let server: grpc.Server; + let exportedData: + | collectorTypes.opentelemetryProto.metrics.v1.ResourceMetrics[] + | undefined; + let metrics: MetricRecord[]; + let reqMetadata: grpc.Metadata | undefined; + + before(done => { + server = new grpc.Server(); + protoLoader + .load(metricsServiceProtoPath, { + keepCase: false, + longs: String, + enums: String, + defaults: true, + oneofs: true, + includeDirs, + }) + .then((packageDefinition: protoLoader.PackageDefinition) => { + const packageObject: any = grpc.loadPackageDefinition( + packageDefinition + ); + server.addService( + packageObject.opentelemetry.proto.collector.metrics.v1 + .MetricsService.service, + { + Export: (data: { + request: collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest; + metadata: grpc.Metadata; + }) => { + try { + exportedData = data.request.resourceMetrics; + reqMetadata = data.metadata; + } catch (e) { + exportedData = undefined; + } + }, + } + ); + const credentials = params.useTLS + ? grpc.ServerCredentials.createSsl( + fs.readFileSync('./test/certs/ca.crt'), + [ + { + cert_chain: fs.readFileSync('./test/certs/server.crt'), + private_key: fs.readFileSync('./test/certs/server.key'), + }, + ] + ) + : grpc.ServerCredentials.createInsecure(); + server.bind(address, credentials); + server.start(); + done(); + }); + }); + + after(() => { + server.forceShutdown(); + }); + + beforeEach(done => { + const credentials = params.useTLS + ? grpc.credentials.createSsl( + fs.readFileSync('./test/certs/ca.crt'), + fs.readFileSync('./test/certs/client.key'), + fs.readFileSync('./test/certs/client.crt') + ) + : undefined; + collectorExporter = new CollectorMetricExporter({ + url: address, + credentials, + serviceName: 'basic-service', + metadata: params.metadata, + }); + // Overwrites the start time to make tests consistent + Object.defineProperty(collectorExporter, '_startTime', { + value: 1592602232694000000, + }); + metrics = []; + metrics.push(mockCounter()); + metrics.push(mockObserver()); + metrics.push(mockHistogram()); + metrics.push(mockValueRecorder()); + + metrics[0].aggregator.update(1); + + metrics[1].aggregator.update(3); + metrics[1].aggregator.update(6); + + metrics[2].aggregator.update(7); + metrics[2].aggregator.update(14); + metrics[3].aggregator.update(5); + done(); + }); + + afterEach(() => { + exportedData = undefined; + reqMetadata = undefined; + }); + + describe('instance', () => { + it('should warn about headers', () => { + const logger = new ConsoleLogger(LogLevel.DEBUG); + const spyLoggerWarn = sinon.stub(logger, 'warn'); + collectorExporter = new CollectorMetricExporter({ + logger, + serviceName: 'basic-service', + url: address, + headers: { + foo: 'bar', + }, + }); + const args = spyLoggerWarn.args[0]; + assert.strictEqual(args[0], 'Headers cannot be set when using grpc'); + }); + }); + + describe('export', () => { + it('should export metrics', done => { + const responseSpy = sinon.spy(); + collectorExporter.export(metrics, responseSpy); + setTimeout(() => { + assert.ok( + typeof exportedData !== 'undefined', + 'resource' + " doesn't exist" + ); + let resource; + if (exportedData) { + resource = exportedData[0].resource; + const counter = + exportedData[0].instrumentationLibraryMetrics[0].metrics[0]; + const observer = + exportedData[1].instrumentationLibraryMetrics[0].metrics[0]; + const histogram = + exportedData[2].instrumentationLibraryMetrics[0].metrics[0]; + const recorder = + exportedData[3].instrumentationLibraryMetrics[0].metrics[0]; + ensureExportedCounterIsCorrect(counter); + ensureExportedObserverIsCorrect(observer); + ensureExportedHistogramIsCorrect(histogram); + ensureExportedValueRecorderIsCorrect(recorder); + assert.ok( + typeof resource !== 'undefined', + "resource doesn't exist" + ); + if (resource) { + ensureResourceIsCorrect(resource); + } + } + if (params.metadata && reqMetadata) { + ensureMetadataIsCorrect(reqMetadata, params.metadata); + } + done(); + }, 500); + }); + }); + }); + +describe('CollectorMetricExporter - node (getDefaultUrl)', () => { + it('should default to localhost', done => { + const collectorExporter = new CollectorMetricExporter({}); + setTimeout(() => { + assert.strictEqual(collectorExporter['url'], 'localhost:55680'); + done(); + }); + }); + it('should keep the URL if included', done => { + const url = 'http://foo.bar.com'; + const collectorExporter = new CollectorMetricExporter({ url }); + setTimeout(() => { + assert.strictEqual(collectorExporter['url'], url); + done(); + }); + }); +}); + +testCollectorMetricExporter({ useTLS: true }); +testCollectorMetricExporter({ useTLS: false }); +testCollectorMetricExporter({ metadata }); diff --git a/packages/opentelemetry-exporter-collector-grpc/test/CollectorTraceExporter.test.ts b/packages/opentelemetry-exporter-collector-grpc/test/CollectorTraceExporter.test.ts new file mode 100644 index 00000000000..e27623d6c75 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/test/CollectorTraceExporter.test.ts @@ -0,0 +1,213 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as protoLoader from '@grpc/proto-loader'; +import { collectorTypes } from '@opentelemetry/exporter-collector'; +import { ConsoleLogger, LogLevel } from '@opentelemetry/core'; +import { + BasicTracerProvider, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; + +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as grpc from 'grpc'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { CollectorTraceExporter } from '../src'; + +import { + ensureExportedSpanIsCorrect, + ensureMetadataIsCorrect, + ensureResourceIsCorrect, + mockedReadableSpan, +} from './helper'; + +const traceServiceProtoPath = + 'opentelemetry/proto/collector/trace/v1/trace_service.proto'; +const includeDirs = [path.resolve(__dirname, '../protos')]; + +const address = 'localhost:1501'; + +type TestParams = { + useTLS?: boolean; + metadata?: grpc.Metadata; +}; + +const metadata = new grpc.Metadata(); +metadata.set('k', 'v'); + +const testCollectorExporter = (params: TestParams) => + describe(`CollectorTraceExporter - node ${ + params.useTLS ? 'with' : 'without' + } TLS, ${params.metadata ? 'with' : 'without'} metadata`, () => { + let collectorExporter: CollectorTraceExporter; + let server: grpc.Server; + let exportedData: + | collectorTypes.opentelemetryProto.trace.v1.ResourceSpans + | undefined; + let reqMetadata: grpc.Metadata | undefined; + + before(done => { + server = new grpc.Server(); + protoLoader + .load(traceServiceProtoPath, { + keepCase: false, + longs: String, + enums: String, + defaults: true, + oneofs: true, + includeDirs, + }) + .then((packageDefinition: protoLoader.PackageDefinition) => { + const packageObject: any = grpc.loadPackageDefinition( + packageDefinition + ); + server.addService( + packageObject.opentelemetry.proto.collector.trace.v1.TraceService + .service, + { + Export: (data: { + request: collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; + metadata: grpc.Metadata; + }) => { + try { + exportedData = data.request.resourceSpans[0]; + reqMetadata = data.metadata; + } catch (e) { + exportedData = undefined; + } + }, + } + ); + const credentials = params.useTLS + ? grpc.ServerCredentials.createSsl( + fs.readFileSync('./test/certs/ca.crt'), + [ + { + cert_chain: fs.readFileSync('./test/certs/server.crt'), + private_key: fs.readFileSync('./test/certs/server.key'), + }, + ] + ) + : grpc.ServerCredentials.createInsecure(); + server.bind(address, credentials); + server.start(); + done(); + }); + }); + + after(() => { + server.forceShutdown(); + }); + + beforeEach(done => { + const credentials = params.useTLS + ? grpc.credentials.createSsl( + fs.readFileSync('./test/certs/ca.crt'), + fs.readFileSync('./test/certs/client.key'), + fs.readFileSync('./test/certs/client.crt') + ) + : undefined; + collectorExporter = new CollectorTraceExporter({ + serviceName: 'basic-service', + url: address, + credentials, + metadata: params.metadata, + }); + + const provider = new BasicTracerProvider(); + provider.addSpanProcessor(new SimpleSpanProcessor(collectorExporter)); + done(); + }); + + afterEach(() => { + exportedData = undefined; + reqMetadata = undefined; + }); + + describe('instance', () => { + it('should warn about headers when using grpc', () => { + const logger = new ConsoleLogger(LogLevel.DEBUG); + const spyLoggerWarn = sinon.stub(logger, 'warn'); + collectorExporter = new CollectorTraceExporter({ + logger, + serviceName: 'basic-service', + url: address, + headers: { + foo: 'bar', + }, + }); + const args = spyLoggerWarn.args[0]; + assert.strictEqual(args[0], 'Headers cannot be set when using grpc'); + }); + }); + + describe('export', () => { + it('should export spans', done => { + const responseSpy = sinon.spy(); + const spans = [Object.assign({}, mockedReadableSpan)]; + collectorExporter.export(spans, responseSpy); + setTimeout(() => { + assert.ok( + typeof exportedData !== 'undefined', + 'resource' + " doesn't exist" + ); + let spans; + let resource; + if (exportedData) { + spans = exportedData.instrumentationLibrarySpans[0].spans; + resource = exportedData.resource; + ensureExportedSpanIsCorrect(spans[0]); + + assert.ok( + typeof resource !== 'undefined', + "resource doesn't exist" + ); + if (resource) { + ensureResourceIsCorrect(resource); + } + } + if (params.metadata && reqMetadata) { + ensureMetadataIsCorrect(reqMetadata, params.metadata); + } + done(); + }, 200); + }); + }); + }); + +describe('CollectorTraceExporter - node (getDefaultUrl)', () => { + it('should default to localhost', done => { + const collectorExporter = new CollectorTraceExporter({}); + setTimeout(() => { + assert.strictEqual(collectorExporter['url'], 'localhost:55680'); + done(); + }); + }); + it('should keep the URL if included', done => { + const url = 'http://foo.bar.com'; + const collectorExporter = new CollectorTraceExporter({ url }); + setTimeout(() => { + assert.strictEqual(collectorExporter['url'], url); + done(); + }); + }); +}); + +testCollectorExporter({ useTLS: true }); +testCollectorExporter({ useTLS: false }); +testCollectorExporter({ metadata }); diff --git a/packages/opentelemetry-exporter-collector-grpc/test/certs/ca.crt b/packages/opentelemetry-exporter-collector-grpc/test/certs/ca.crt new file mode 100644 index 00000000000..455c498aa28 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/test/certs/ca.crt @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFPjCCAyYCCQDSzsM0Ou9GwDANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJD +TDELMAkGA1UECAwCUk0xGjAYBgNVBAcMEU9wZW5UZWxlbWV0cnlUZXN0MQ0wCwYD +VQQKDARSb290MQ0wCwYDVQQLDARUZXN0MQswCQYDVQQDDAJjYTAeFw0yMDA1MTUx +NTQ0MzVaFw0yMTA1MTUxNTQ0MzVaMGExCzAJBgNVBAYTAkNMMQswCQYDVQQIDAJS +TTEaMBgGA1UEBwwRT3BlblRlbGVtZXRyeVRlc3QxDTALBgNVBAoMBFJvb3QxDTAL +BgNVBAsMBFRlc3QxCzAJBgNVBAMMAmNhMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAs1AVbpZ642HATrkqW0WpzsOAne677zDftkvIhWcto3x+nwP6kSOE +vHtPR7xem9Yl5LUy1aDpd0WnBSke1JIYdJCAmmlitFVShrpolGRb9MqYJPXp5FfH +OFltziG00/MSKwNv7GiwN3ehyvzfS9L46mCcUWnQLJkjkThvlV0JRCfaTBRF3m8M +fKYvQ71G/9ZwbRvRqPCk8CZmzhqKLvRFBmzM2FGj0CY5fFqPcBRM08MWNkxAR/4B +IGKTaz5qzaFEvxHgQMQaXOQZYeNwiCFBoGygOId96x8GX9AT1PwW2ltMU3rNtVCf +9xu3JUREHjkIReNqM9h1qq5YIfrEQYeM1Q5Kyr3+Bpj6EhZqGmfc37z/nootxG3z +VmYZ4+z0zx24s117J7CfD2OLL2OaLyWheXXYqB0gOgoTwwwTsB5DYOv15fjsqs3F +kuYR/hbxs1GQO9RcOmlvynIleiVkm1x+UmOuIltfMjolBPc7ZKKxjlAxbC4oY7Za +3th3UkDIVFJmWsJhj+z87qLq0EW4m5UYV3uIUDN4P6Pko3iTqKG2qUtnnhrlbvhd +/YfSCWJRMSlgCfKFuhGkiVDEpJhza5LxNeM2EYD/PIydotyASw2Btp+VowC6yDJV +yR2cTVEGeYxQXpOI0wqJT8DrhWsdAqioLtaFxNJkdTKWAbfC8MP5wp8CAwEAATAN +BgkqhkiG9w0BAQsFAAOCAgEAP7u8IlEOTBrL3OISH9vUqFbiRdTzPfpFJ2ZVxM3H +C4iLdndKVmJLRJyMeGhD/kEnTMmHrt/mZTw6tI87+PE1ZMqSe4+q2NlHz0BouiQa +ukGj+OzZ4gw+IlDfyiXtsggCb1dRZldGoddiP8ldP0ohvR7nErG0RrRuBp860yPD +qBzItTzpC4dNVBbOBf+m9T914dsznFKlyU+QSVA2TXpJnmfEKCwlyk2gVH9olQlG +ND4cBdnOnarV5eflIj+LXjZh2wt/F0qLpTmUmxEyCc1M1il+hC6hnbarzin+8Cxu +VqjKzG7KcLxlWx9wj6ruBA1kPL0Jx31c8wDJ8b7HtsDzehcwrKKnZwA3qs3r417c +n7Dddbix9Gxxi2MTY83Q3MKbVj+oKxz0wZxa29fvlf3Gv98wzSMcS2cK+bjQwwuJ +WQxH9KksKU6g1Dv3fVz2E5CP9gwHaQBVBNSKxlqQsB2nhNglpigmglCKrfX07c7x +ryzoDE1E7tYguyWa4W+LFJ85EirUkGIBL7IoGCsol/elF6noGiuaNMO3KsWmp/C6 +YsXQJPWrnep93CCZdZ7bY6L6BTPdz1RaXMh8Rc65MlIlTzxPnhFTYrXz/FlK2uv7 +lPvT0+cGOvuiN26vqfKnrid1I2theKhKDWSdv3Rshg0ZJatNWS0u8gTE4f+qCjHP +9CI= +-----END CERTIFICATE----- diff --git a/packages/opentelemetry-exporter-collector-grpc/test/certs/ca.key b/packages/opentelemetry-exporter-collector-grpc/test/certs/ca.key new file mode 100644 index 00000000000..e8b01e04ea1 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/test/certs/ca.key @@ -0,0 +1,54 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,C088BF4BACFE1D5E + +TKzb0xd1SS8So+VGtAOqj7XhYJNaTSl7HrF5UXoL835lzU6qIdgJWp8REOATdYTP +wqL5x3OlRy/X9GUtXApQx4OoCy1hOMXB10/T1nD+EuxBf4ChEtRow1synEfOVlX8 +JZvRHuvN1AGnOzn8YpCnZ19ufw9ASX1cOFjefJKiR8vi32/LEO5No2jqODTWK3V2 +ijiV01hDkbiWvIoxcLQRXm+F2TAZ7MYz/DEjtbAr+4vCDMobJicWHim6yHpor/B0 +7bBVEsR0/R7kb+fLtv9cBDUqu40m7LfuMFtJDD5deRce2hSs+rm9nO01qvo5KvR5 +XA9WdKdFjk3WKjE0uAhRCzXXvRO1S9i6Ym0E3zoW6zcXItQUo30BhBgn4DALMMw/ +aLAsq0trmXqTiJCq8QDYgQOj59jwVxMuAsvinhqBI8koy92hBiXAhZd0r2+2jm/b +yqELuX+0b+FW0hSRL/BsXaTXrzW9cSpSM+EsCtoZloNecGGKNUIhVF6+LmALQ5xD +5dwIIooQTpNzLpc55rK6C01VWQLRWClJdbASdYD5hmY/0KNq/LB7F4TY9DjnJnWx +Lrkalyl8lv1oZHjPUqA8NAY+Rf+Ps6BxxP2ShAfVwybVFh0ACh5stWpAbmWId86p +vnf4gW2y5g4p9HNK/+XuFJ4PQj4/SJNRrc7HvwlCnAg1lXRYtt2C2awbKPzBU7bw +4sqOKlIOSeox6x3APcO+nTuYZf2XJ9s/jtlPqPgGBaaWB6IANiMBwi2LnVCjxaL5 +tjiBQlwcYSla7YPz7AAuRYcv2zPJVSk8pZqObBZO+1JN/BJf0LUqW4fOKSwud8gG +rDHp5YS/+MOnygvuyooqdFoFwS6/fKzdLKz5Ug0ZsIPEVdd0gQUrNReATptmRuxJ +/dA58RLpsosCz2iMkYxEJ75acmPsZU6DZCHrI/WwDR6xOVN+3YttpEoGXa16D7Hk +Pa+tmObX3aK+iAQBoSsiztxaBYRNc+QbpKl1/qU86+2m8yXnsbKDXk3WnFVMBCw2 +VbdgD7Rx72sYhzn2VPGmoRkOn/yOkhful7R/tNTK040FuBQaFWer5yDsUlWIoYgd +wnTdSdXisib4rfq/t50xfCGS67eyaH/CMbAni/x+eikDFAA3/OLMM+46hZaoZHqP +sOcbcD+JUIwo00xW2Xv2gF8NT4mcdVphRs9u1pcoyZCQm4OuE4qfJhYH2k48imCC +yfQVgr/fitMm9/oNcEkCuGI5iNm0f88dIKZSuAaxBQ9AXxRjgGVxjdasTcFwkMMo +ahgasfOXq53HoPgX7UOB9V4DdtzwwUg2cS3G0aC8Z2botQ7JlA87QvHddLPrFE3r +ybHIgxOOhabCNpO0ER0xaaS6dKhq/oEuh4owPm7fnfx6lYVmxELJoyuGvGJjlDjk +Zks4Du6Ew6KuZRbGJQOod+FAT1uCIOt83Vslp+3rURe9NmUmU6xHSOnb3La3pLco +upb7x8ufsE8y143uyiqDAyF7MluCl/Cc0rO7BPOu/QsXUcm+oE/b+WLCfDkWETHp +6UK6bW9gi3iohm1S5ViLLSQGcXF62rkP0PQMZpxemQdsKJaynjUmtY13h65L8GRh +4Btxb3/fZgsBDT8us5SP1qSNFsygJwKuRGLaGqrbx+o/deA7kSwX/UFrAemAkysE +1WuFvGlrhTUXcYmjKGbP+78IyPuhcG+lxp1QZXpdIv9Bos2m475we1gSAi2qOF02 +2op60zNo8ZsBRSI/QKtojfG+0SlCNO7owzu+j6PH+7rHpSL1DaPK9C1xwxQCsRaO +MIU+ELIWboJK3lNChQ11mnyMjoIMsfR9fP7Cmr4FuvCHYQbCFERLOzJ6FU7974+b +ul6VAsbvsutLRziQ3LN+QdQRsrrvq9YU0CgB8jLUHf137x4Goegb3cxlDjwzpGkt +R3HM1KAbxcbyziQz2NuSZK5Jfg/OO+C6o5HN2j3IfhQyM1PZ7MsO6sEaRWBxgC99 +xjXYUyDRt2Ho1mFmRtdXjmeGExz3QBQ7X66swHwMcBov6uL9x060VXfzFB6Gbn6O +2UabP4eriWuGUSk/fVBg3jqe+iMMM4z++mScmCqWUnp6lzUSzhsCyZ6a/11zsyvF +Lq8GDu+4rCFzj8/jgE3rqPHGPM7cgn8kv7IC1cOMDMWmELPZW38bxbPYPbNiNgtv +Cq0OjCCSyB307gC2VjwbXyN7AAT0mul7BhQOxU/qIqRoGKUGuQLWIp42Fe0TAe8x +Im1baX8SV35KagGLvcBlw1uwA6olzo4WyxH2SyVEfYxBqek7DmZ8LUwH7s+Xs2+M +svr++dv3drLOdz75Wj7N6KiK0KDxv5EHLiP3YD8/UqP3GzMDv+yj3lpVOcE40kEo +HWhlv7X7fZWUCV9iiRSKWzYBhps0LWjJ4ryB/5wU5X/iSTLyP9cYPKiQIFyaWDK6 +POcYrgNN62e32PScENlwy+YuL4xuaa3KnOTS4e4emjzdH576y213D+n7bpFVOvi0 +JEm8qJJ7PgrwnuGcnNjIfIJNDrLqXDYJWn0K59Pjfd0i3VRhOiNFzcIRnNePR//h +lwBlhy0+XpUvxNEt9Ju+xaaSxg16cyKlz6lz8P+4TGuw8cgXdSXcZw6w+RDdmiv/ +NkVUPEwtMh3+H6L4Lfy9h0HA0bnpnOdgbfeTbHHv5/ViJd7cAjF4Z7PTEpC8nT++ +RTqp4q1upJjb5vk2IkrvhPAO/ZjK01ijSx/sieYoSxp2+vme/4yYloD3IjoUR3SB +0DOv5ATQUNABKAOkZkkpeA0IRuPdbLqpd4FQLYi08oJbOEiVkCUzmBwxbvCAkN83 +KCey8TP/OXVg9+lsh5UgaVPNZmNWGabHIsAnp4TszQZWsxAywOvBSWAb+Z8GOCTP +8T24RYphijZALkXzssYeCZ6qOl/V6YKa7dkIrWAyVRsZKQYH73HzJr7qR0N84eXu +4yyi8rb31d/6Gl+ZyvvDMeQBOFlKtHRx01VG/jLlq2qBuv4lY+UFFDpV2l7F4rVV +IwAuU/pYcuJ97bocLvrdCZJIdszlNgGHpKcBn4MWT+lcod/iBsloXy6J6kluaXBu +q8Ub9zwiF/aKM29CcBRnIHMIVSZ5FY9/Zbu8EhnZjTe7NUNNWi9uV0Arht5S/3RS +-----END RSA PRIVATE KEY----- diff --git a/packages/opentelemetry-exporter-collector-grpc/test/certs/client.crt b/packages/opentelemetry-exporter-collector-grpc/test/certs/client.crt new file mode 100644 index 00000000000..9534695d808 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/test/certs/client.crt @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFPzCCAycCAQEwDQYJKoZIhvcNAQEFBQAwYTELMAkGA1UEBhMCQ0wxCzAJBgNV +BAgMAlJNMRowGAYDVQQHDBFPcGVuVGVsZW1ldHJ5VGVzdDENMAsGA1UECgwEUm9v +dDENMAsGA1UECwwEVGVzdDELMAkGA1UEAwwCY2EwHhcNMjAwNTE1MTU0NDM3WhcN +MjEwNTE1MTU0NDM3WjBqMQswCQYDVQQGEwJDTDELMAkGA1UECAwCUk0xGjAYBgNV +BAcMEU9wZW5UZWxlbWV0cnlUZXN0MQ0wCwYDVQQKDARUZXN0MQ8wDQYDVQQLDAZD +bGllbnQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAMm4t0aiZqouBsW/VilH/McgrMECz6RYMnAxAZVG0AwvlzZPMc46 +Vpbggpsn5j/N/teragpiqIwIIN+1apGXGmAg4IDgyrswq37Oj4JrvmzXWK1PGGFs +YpWISmNR1DKkEL8ts41KDEZejsItFYctnvIctRYPoYB+6No2iddj5gioHyq/yDLN +zD0c0C3r9tXm+Ed9BO4pgu6Rl6zuPf3sttE5eNa/O6qV1dD3nxnpPS3fIbXqKviD ++xhgXrfLM43X0QBQt6sPFuunpcvhWDsgtWMQ6EShQUhb0DXr6PgGXj/1Vl3nVsxP +4gnCOE5x13jzw/tqijbKin2+dpEGdi+c0QeVfDWoMZA9mlitZiLsenKdB8sYaoCw +QZHu3zzfXruMqA6x6DyLPa6PEFzw4v5PAvsd4Re0cLTBDsw1Fdx/eGzBg7k1KCFZ +HA3RdzNqCMvxcumH7hUg1n0cEtHX/bVSdpndK7iWVPbDYv98bFNOq8fZzsoqZgOk +Jl4TJyil/oPDkzowc8F8+p4vWdgHevjkqk5rtyMLBb6KnUmJgYPef7FuZ97oSi+r +TrAUs595+RZefDRdu5MGV/2NMbpN992Yewg7LTiP+gwNuYBDQmEYyQf0sxMNcwXc +ZVrWw+RdI8udSFowmOd/g0NNz3CaAXX8n6BLMJBBxRx0zet/88VFtLNrAgMBAAEw +DQYJKoZIhvcNAQEFBQADggIBADfQTBf/n+r+E6/GH3kyiI4jg0vIlkOlABsypKvY +iPXGTrtTlFB4s18/f0I416ez1U129OYyE2mUHKDKAUHu/Qf3Cl5N983DCx7czVJZ +Maxafe7DS5rAwF1wpfxR6u4Ti0gK0HO29bsCDah5C5+s4Vzv5t6AFmyg+ESQG6cM +vbkIs5nbcU1ydMdfvSb3vmjvPLh41lWnRVkkbjgzTS312EnHmqV3wIx12UAb16J4 +zXOjI+7JU9TZRnTEf3xOyByA5h8pCYha3nOlETR+vRN1byUYesCWsgj0wFU1u6K6 +AqSMU4sqtNIIlwN50CPLvYjB3FBPh8DpB5iQ4GxM636X06dQqQF7n4cWMOMHRlT1 +DgafEpVdxSeJMzuBQHJzF0UbyaAwKkDKGuAZWfihlNEUMdVm4EvKpE82cevM/2Mo +VEuPlcmf+D0ERu6bK5RAjXkH+cxYWXJGRtx823IEEgXOk0F4AMCaMiuNHI7buBi7 +AnBvIUv67b6FRS6Hw8sMDNvVTpavsnUKwSJJUATPU+rRIgD3Dl7SJ9XqmFdgPO+E +eRxvCCZvzEL77SLslv6CkKLseNQQ7MrOgTotYOrHA/AwF1GtFSDoYTRifKGynRPO +Vg3CscBOkIz9Plmy6dq8CEIygdmcN2Bb8BwA97q1epU4vzmx7fhqLLyMq+YztPRp +6SLz +-----END CERTIFICATE----- diff --git a/packages/opentelemetry-exporter-collector-grpc/test/certs/client.csr b/packages/opentelemetry-exporter-collector-grpc/test/certs/client.csr new file mode 100644 index 00000000000..2c7d0f9c04b --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/test/certs/client.csr @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIErzCCApcCAQAwajELMAkGA1UEBhMCQ0wxCzAJBgNVBAgMAlJNMRowGAYDVQQH +DBFPcGVuVGVsZW1ldHJ5VGVzdDENMAsGA1UECgwEVGVzdDEPMA0GA1UECwwGQ2xp +ZW50MRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDJuLdGomaqLgbFv1YpR/zHIKzBAs+kWDJwMQGVRtAML5c2TzHOOlaW +4IKbJ+Y/zf7Xq2oKYqiMCCDftWqRlxpgIOCA4Mq7MKt+zo+Ca75s11itTxhhbGKV +iEpjUdQypBC/LbONSgxGXo7CLRWHLZ7yHLUWD6GAfujaNonXY+YIqB8qv8gyzcw9 +HNAt6/bV5vhHfQTuKYLukZes7j397LbROXjWvzuqldXQ958Z6T0t3yG16ir4g/sY +YF63yzON19EAULerDxbrp6XL4Vg7ILVjEOhEoUFIW9A16+j4Bl4/9VZd51bMT+IJ +wjhOcdd488P7aoo2yop9vnaRBnYvnNEHlXw1qDGQPZpYrWYi7HpynQfLGGqAsEGR +7t883167jKgOseg8iz2ujxBc8OL+TwL7HeEXtHC0wQ7MNRXcf3hswYO5NSghWRwN +0XczagjL8XLph+4VINZ9HBLR1/21UnaZ3Su4llT2w2L/fGxTTqvH2c7KKmYDpCZe +Eycopf6Dw5M6MHPBfPqeL1nYB3r45KpOa7cjCwW+ip1JiYGD3n+xbmfe6Eovq06w +FLOfefkWXnw0XbuTBlf9jTG6TffdmHsIOy04j/oMDbmAQ0JhGMkH9LMTDXMF3GVa +1sPkXSPLnUhaMJjnf4NDTc9wmgF1/J+gSzCQQcUcdM3rf/PFRbSzawIDAQABoAAw +DQYJKoZIhvcNAQELBQADggIBAFjedQr52vLv7YxeLxIvyHrMhbx7Iz4ztj3NlnOJ +EMGm7pcum/rGol1z8m7Y3mFbfJJp8IY/jn1w92x+M9pc6zsRo9MsKdqEAKhAjwVh +jYNBWHekrcwGIy6YUSFvZeUZ82IxFcf6N70CH4sLUJLbZXcd5Nui8mZJCPC4SLoC +E51P0vUClnS/l4O+Dz/IfBy9cSvGg3YvF8GGmW7IZdTD4bWg9O8lQi0zcnDGR0Er +N1Tegoe38Mrx49IHpWMEQzJhI6R22CQ0wtk6e8oBuz2No8hnY0yrAvBGI9v8GUE3 +FJAQxHzyUXCA50IcHFruevsgEzixmYb8OfDd1LC3nZJHfq2r5j0jOU6XXxukH8R3 +UyGIf8UpJQqBKHe0Ld0tOWSyByiWHvw4/Nir/DhANezIEsq4A0Y9hq6y2GTtFUnx +HdsYqTmVlrghBiqZF2H9f7YWaRBnsbu6Kkpyc55r8pBZMT2Myu2Gjq/8GAWtEy1J +BYmQfIZUnYksFaZiXvSiyfNaX5M7nvddxkBCyhtwtCzVutL+ZoqwXD2PPaUCuBbu +lu4M7iSjKiibiCqQEVyRPn2o8V4R5r0NmqS+B9CYJECeAnLPO49Z3l4wdJUEww9i +U14lM75e2tfFzaa/ZqOCQFuu84NKacTJUALpdg1aHcPtTG51F2U8EwsoZEBxUBb+ +WR7X +-----END CERTIFICATE REQUEST----- diff --git a/packages/opentelemetry-exporter-collector-grpc/test/certs/client.key b/packages/opentelemetry-exporter-collector-grpc/test/certs/client.key new file mode 100644 index 00000000000..e0fea66664c --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/test/certs/client.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEAybi3RqJmqi4Gxb9WKUf8xyCswQLPpFgycDEBlUbQDC+XNk8x +zjpWluCCmyfmP83+16tqCmKojAgg37VqkZcaYCDggODKuzCrfs6Pgmu+bNdYrU8Y +YWxilYhKY1HUMqQQvy2zjUoMRl6Owi0Vhy2e8hy1Fg+hgH7o2jaJ12PmCKgfKr/I +Ms3MPRzQLev21eb4R30E7imC7pGXrO49/ey20Tl41r87qpXV0PefGek9Ld8hteoq ++IP7GGBet8szjdfRAFC3qw8W66ely+FYOyC1YxDoRKFBSFvQNevo+AZeP/VWXedW +zE/iCcI4TnHXePPD+2qKNsqKfb52kQZ2L5zRB5V8NagxkD2aWK1mIux6cp0Hyxhq +gLBBke7fPN9eu4yoDrHoPIs9ro8QXPDi/k8C+x3hF7RwtMEOzDUV3H94bMGDuTUo +IVkcDdF3M2oIy/Fy6YfuFSDWfRwS0df9tVJ2md0ruJZU9sNi/3xsU06rx9nOyipm +A6QmXhMnKKX+g8OTOjBzwXz6ni9Z2Ad6+OSqTmu3IwsFvoqdSYmBg95/sW5n3uhK +L6tOsBSzn3n5Fl58NF27kwZX/Y0xuk333Zh7CDstOI/6DA25gENCYRjJB/SzEw1z +BdxlWtbD5F0jy51IWjCY53+DQ03PcJoBdfyfoEswkEHFHHTN63/zxUW0s2sCAwEA +AQKCAgEAjZvNlZl2RuuOt41teAdgLY4DmG9XwwBjUB0nBlsyvAtAtNB9n0+W783m +AfPNkGcVCuP7yhSeS8d9BG6/xDr2Oht6Xx7vUt+E1L0/Q4hNouy+BNQswl+rCVwn +FHgiZfaFByCXFo2v9kp1H1006rOdDEwY18bbUnBFGMMGmx03JEaZspH1gay1PwWW +I1at7lV5X/4k0uhzUPUGLFEHVdWyNUiKSv7ubP9InaznlPIGj8g/Swx7ZACK6f7l +H1NX+rBRuU3w0fYC2iXTnz+vh7qbe1MoKt2lDZ3emavl3Q/jZDTfj4ZSiZVekgk1 +K+SBJhjCMSIGqxYeiM2HQKHvn9cPaWtEH+B3zPSauURngPxhayLsVywrqAIqh2gI +iQXnqajwn/g6KF+eEYfdJyPUv0DZgS9e8I8jeGf6Dax4SYWEtl835+r7FsejXLXZ +ehYhIdjyG16+NpLcc5d7/xaSbu9cB7I64raQCnmVbSo/iixd3TwVgFsufRqSgL++ +xa33Y0n4Tq3HgIFg2vlX+6T0RGtWRw73gmk4SXc55wG2v5a2emhQEijfoLPHEQZw +6Xd7qHHJtzxAP+Ifp3IlQ6vW0S27SIiLmQoSZBd3So5r0iF5ufIWe6215EmCdQdt +y6t000Lc8wk/0p50nlaF3Gq4dVUwkXfse/Spb+cbu4t2hSGuC4kCggEBAOuZc3MP +8OZ7vuiCgkRsE+9vfouOxmUbeP0pQzDhG/havRG6J6PG5zltmZFqJh/JvFibnRhD +UZebL9+ugYbVqSPaijuW4MpP1RSZJprxKcwiXkvIXOmB4rDbrBT8OinN7KOXDG9D +6HpeLcRG38ayMfCPMCrNjHW1J/qwJHxycuLme76d7fevxGhojJE6tICasE9SVoF7 +lc+GK/tQKbjztF1QJHXgELSDRP+uHZx7G231HiOqomMIdI0F4fXJHWk2sYBJ33zn +1/c0hPhMks1eXQiod5jXfDtwoaaArkV7S7uahDpJmi2I0HNesWoMrUKeGEEJf9mR +qHSyHozsqqmyPwUCggEBANswSrFUc1oJfA39VTFwLW54VMhb7JuKM+2h6lrZTenK +m1IwZ3sNBub6mjDtPVBG/pvIYwAAfx1liOZgyKyDj0ticWF1sAfFnWKKN7OJTW7v +45Y8oFg10CHNKOWaJd0eAEhoFHW1kPMqrM6d6uYHf60ayQTkyloKkEakBiq7YkhK +ilExk1jyqiJFU/WFEvb6kL5yg1bn1NswaOebpvXSI0z8IzUoVfRXjXB0okOrgiEI +Cn3jOO2b1hF9PHVCYbiIJnoNIhP+DdEoTpCyQy8FwWXGvtgEdwfGm8PH0iH17ehY +D8ODb3NV3HyLzoORLnqHN6G7XF2N3Y2yL2jnLBpJU68CggEBAMp514lkgtFiOiDS +wKeTBtL4zBWeP4z3PlS8GH2yiPo46VKJ3LVZJLDrK1aYlmktVAwGuMz4Ve/oNA2V +iMXbbABfOfuaYFgeoe6Q7GeuqRBB3S5d5NPdh3gdYleqqUXyLtQs5UfeYbaAp+6O +RpUZ4edu96NhgbxLUy+UH9c/+NJd6K1aRwBd83sTlvLdM/Fuf+W7ypJ/JrHyCmxy +aVkFQNYNITiYt2Kbijn+Zn5sIpeuWBeo9uQLiTcFfjtge0FH+uZZFpPfIHDYlwpZ +rLSIy4W8WwRk9OSUmKhi4OLf4qc5VThOtw05DoSINgsBGAovmoKSamkOUGryBWVx +o/4xLQ0CggEAabWtoD5hb3/5g2m1R6WZU5jXEtY6k30gtC+Nrgj1aZacOBQ+I/tR +Y95itMwF8Qx8SLdo/5w9sfjBAJKW1ZSRbELq+Zzfq6/jyp1sZbsHTESHl3JfxosV +eOfQHIOuVSjd7A2+KFLLuGrRcsh4fD4Llnm/jwukh65mjJsYmk1LBiBk+umU7aYC +5YpYBqYKUnDfk+n4a9ZdMuTzAxhvekjBW6SSelWctr3u6dhmVYqGtNWC8dm/H+Ez +abXjjY3ZQTzwiZaB4/B3y3LMCT7f5fK5phMnAVmN6oMfplldf6Fy/sZRu/JMsuwq +7SokDBHdv5ws+WQ6FKiRvH++G7K582d/4wKCAQBb6GKm0GXD0Cj0S7jGCUtOzSKx +k35cWe3YUByFQ5cN5O1kRr4xBgQin7X0Xn2WY1xCMRocslpScfVgE2WJcbVaoiqI +V7dq4N1ZhkL9dWy25Q4vmnHZU6NEZMrIC6Upd9X7uhamLJWMEqUeitI43CtjB+hF +bnD66o3ne+5QjENKOcRtssv92gUnbAtRzuy9clq5aTk37cV9e1iHTPvnILeX6hzK +szMF6wpmfbn0uzwD6HMKdGFoocc3h/0iXtk1zFTIQt7BB/aCA0VYKToCb5flgFb2 +BoswTm+ui/s2fQYlMb864gIceJBOI4+zgNeKMSrKLfp42QD3DhMtWbfpvygY +-----END RSA PRIVATE KEY----- diff --git a/packages/opentelemetry-exporter-collector-grpc/test/certs/regenerate.sh b/packages/opentelemetry-exporter-collector-grpc/test/certs/regenerate.sh new file mode 100755 index 00000000000..bb6ec4a9b52 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/test/certs/regenerate.sh @@ -0,0 +1,19 @@ +#! /bin/sh +# +# Usage: regenerate.sh +# +# regenerate.sh regenerates certificates that are used to test gRPC with TLS +# Make sure you run it in test/certs directory. +# It also serves as a documentation on how existing certificates were generated. + +rm ca.crt ca.key client.crt client.csr client.key server.crt server.csr server.key +openssl genrsa -passout pass:1111 -des3 -out ca.key 4096 +openssl req -passin pass:1111 -new -x509 -days 365 -key ca.key -out ca.crt -subj "/C=CL/ST=RM/L=OpenTelemetryTest/O=Root/OU=Test/CN=ca" +openssl genrsa -passout pass:1111 -des3 -out server.key 4096 +openssl req -passin pass:1111 -new -key server.key -out server.csr -subj "/C=CL/ST=RM/L=OpenTelemetryTest/O=Test/OU=Server/CN=localhost" +openssl x509 -req -passin pass:1111 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt +openssl rsa -passin pass:1111 -in server.key -out server.key +openssl genrsa -passout pass:1111 -des3 -out client.key 4096 +openssl req -passin pass:1111 -new -key client.key -out client.csr -subj "/C=CL/ST=RM/L=OpenTelemetryTest/O=Test/OU=Client/CN=localhost" +openssl x509 -passin pass:1111 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt +openssl rsa -passin pass:1111 -in client.key -out client.key diff --git a/packages/opentelemetry-exporter-collector-grpc/test/certs/server.crt b/packages/opentelemetry-exporter-collector-grpc/test/certs/server.crt new file mode 100644 index 00000000000..62f91722a93 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/test/certs/server.crt @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFPzCCAycCAQEwDQYJKoZIhvcNAQEFBQAwYTELMAkGA1UEBhMCQ0wxCzAJBgNV +BAgMAlJNMRowGAYDVQQHDBFPcGVuVGVsZW1ldHJ5VGVzdDENMAsGA1UECgwEUm9v +dDENMAsGA1UECwwEVGVzdDELMAkGA1UEAwwCY2EwHhcNMjAwNTE1MTU0NDM2WhcN +MjEwNTE1MTU0NDM2WjBqMQswCQYDVQQGEwJDTDELMAkGA1UECAwCUk0xGjAYBgNV +BAcMEU9wZW5UZWxlbWV0cnlUZXN0MQ0wCwYDVQQKDARUZXN0MQ8wDQYDVQQLDAZT +ZXJ2ZXIxEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBANQwHRfyj/d8Hh0qgDlxdtSxloRs8ZvBIwt6Accd1hUqs8dC0c9V +5XXOcfmusb3Fo8NKXn6IIPCEy1spFCe4EBW4obSgkJEVdPwsMsXUPLek/6K5S6uE +FhnGLUJJ57gAjh9LGdMTDp5szLO7dTYrHzdGZYhmTAyiA9JDN6iYlpWkK4p2IBcN +diu26KWp9+sJKw8Ly/7o5QD4wyc6hGok0v0nwimXZo78EJYBu6BDGuLyAgvq8zLV +sgXi4aYROsmVrg2IJbe8+PtPBNwkoAuR4QC3hRTV3bXyZdbIC0KbOekegAHTeXYz +Ap0HVkCsb/vOLiGuju/mKZFZKp5/PKf8Jdv/zDTIm8TwBvvtQKT4qmAYUkKTXRrO +OWK1pCakVLV7FGREDi+/bxhcQJt5yopLGT5NSoUF3RR+17KZ/5lSPEh5OMSprVyR +789KvY1z79JWt3zB6fIfQ936PyNh++SKxFmlnLuGK5wf58jefwSjGEkY2YAE66Y6 +8Kqg3/W8JsjTFBntBtD3xY1t0c4Hh2f3epQPrzwHx9pywgh+H2TIwnnUyEPLqdYp +SEsbnvdbLB8FZm2fwPZ1MZOZOGrKcnCMkMPE1DOIkxeFDx8xbeHRepSRJSbemY1l +tt+afAnM18mJf36gO8NnM56Me//FSTWbWaQlmUBAwSDlHxYfD9TgCjbBAgMBAAEw +DQYJKoZIhvcNAQEFBQADggIBAEt57zbZpIaQiw0BvZenLWhWvBA0j1cFk7eVG+Nl +Zo7+UniFH+1Io/gXJaJmJZ09d3ku4ZB+V44ka1N9J7qnnqXYOxRGT2H6owaWeOLl +FQ8tR1NQQA7p2uNWJclBsuPghzRCSFZw2auu8OKRtM/0VgbskNIN+H0EVhEeYjtd +ZzojPoa7AmH7P4SC1KMvY6qNmab9F8TBD19DPfoA/EpYboMQiK7DwPPuvrAdHcJB +KPLxyzabqFEqouwStqKUmKqbASOR+qJNac/RQTbN6yP4Lu9wTUm1OYaR4ot87dOR +ZhCznzlaJ2DsvFuoOKN/7Bezq+rXhIyCrH9VH0PjWwbO9FIfeZlHgmAmJnJCXb6F +bW6m+ha/63kiPU1NlTJRPukcR0vW/P0XSOcRvvje/07uJOOG5ypnQf6k7neR5e81 +1ZHPKCHba7bh08vKW5LbXwU4Ng7vRc42h6+iN0mogjj+B2oYt432L3howc8np2vF +eLCRxq/9pRut2QkfivT/GHkV/J+RxoEFDrZrTd15q1mLQnPCJOT+QmAMPfZydyZM +FsQUd6kzEWgZ4dHKqEikC0IBG+2xrrvHgKiB5Y1o0K/hEFfQOFCct6c9thXqMYhA +w/2HXXjfWLVBbGjJ4VemU1YFKyMZ+mxM1sJmPc/KkG/NjKf9wFFwFRpT3OIlF+BK +u8P4 +-----END CERTIFICATE----- diff --git a/packages/opentelemetry-exporter-collector-grpc/test/certs/server.csr b/packages/opentelemetry-exporter-collector-grpc/test/certs/server.csr new file mode 100644 index 00000000000..967316e1713 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/test/certs/server.csr @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIErzCCApcCAQAwajELMAkGA1UEBhMCQ0wxCzAJBgNVBAgMAlJNMRowGAYDVQQH +DBFPcGVuVGVsZW1ldHJ5VGVzdDENMAsGA1UECgwEVGVzdDEPMA0GA1UECwwGU2Vy +dmVyMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDUMB0X8o/3fB4dKoA5cXbUsZaEbPGbwSMLegHHHdYVKrPHQtHPVeV1 +znH5rrG9xaPDSl5+iCDwhMtbKRQnuBAVuKG0oJCRFXT8LDLF1Dy3pP+iuUurhBYZ +xi1CSee4AI4fSxnTEw6ebMyzu3U2Kx83RmWIZkwMogPSQzeomJaVpCuKdiAXDXYr +tuilqffrCSsPC8v+6OUA+MMnOoRqJNL9J8Ipl2aO/BCWAbugQxri8gIL6vMy1bIF +4uGmETrJla4NiCW3vPj7TwTcJKALkeEAt4UU1d218mXWyAtCmznpHoAB03l2MwKd +B1ZArG/7zi4hro7v5imRWSqefzyn/CXb/8w0yJvE8Ab77UCk+KpgGFJCk10azjli +taQmpFS1exRkRA4vv28YXECbecqKSxk+TUqFBd0Ufteymf+ZUjxIeTjEqa1cke/P +Sr2Nc+/SVrd8wenyH0Pd+j8jYfvkisRZpZy7hiucH+fI3n8EoxhJGNmABOumOvCq +oN/1vCbI0xQZ7QbQ98WNbdHOB4dn93qUD688B8facsIIfh9kyMJ51MhDy6nWKUhL +G573WywfBWZtn8D2dTGTmThqynJwjJDDxNQziJMXhQ8fMW3h0XqUkSUm3pmNZbbf +mnwJzNfJiX9+oDvDZzOejHv/xUk1m1mkJZlAQMEg5R8WHw/U4Ao2wQIDAQABoAAw +DQYJKoZIhvcNAQELBQADggIBAIBAt/12a6kkCFaRe256Umrj3/2DPA+gVqaVwlsi +xEGuO3GpBv7D6+lrlwNhLLSFOEkqoB4t/hjfGyabENXrCgyjMEoq/YKfwJvO4FPv +UkjaEWsCxmuwTS0qm8gXQy9PAwSI8EF2jOoRtvpCXl7bDQRJRIgKwZFI+jCEZvgj +Sk8fZGOH9yPEjx0KpvEw3jl/kbdSJu+CFTr981yLKjeG0lMknc/sQwH87tco4icj +t2Deaow6UOc0VaTmsWMLwIWrG/5TQPj+tL/600mBs5iQCOVio+hbzOHmDb48Ztao +CD4z8w8PAHxO79Vx0Wjt26cl6pKL58uke3G41Aq8//YLpSUUvIx0bYOwobDd4Ev5 +Emklvmcf3hAAzVQ7g8kDD82RDPRKtDl6e26+q2MQT31HuGbKB+5xpi113dSoB2CO +NSAgn3heoj5OM7heKwh6p6j0r1gT8WjXDMXQdKgekTGaUxeOSmccvMk4U0LN3JpK +JqaH178OucI9aRxGVjQFErW7xbKOViHP+NxNKj1pnerd7PX0wF/g107v2eSb6l/5 +K0UsM/l7MsINkx/1p+Qqu26t3i3Azw/MxKJqOVAlcb2LrACBj80BXBcJLW/My3kY +0XzK1siVSL17lL4KYBLO7kVR3F1+m+aQPrYJsLEKCAGxsfiFRBhXa6pfvp+fd5Hs +/xFM +-----END CERTIFICATE REQUEST----- diff --git a/packages/opentelemetry-exporter-collector-grpc/test/certs/server.key b/packages/opentelemetry-exporter-collector-grpc/test/certs/server.key new file mode 100644 index 00000000000..4831771d2b3 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/test/certs/server.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKgIBAAKCAgEA1DAdF/KP93weHSqAOXF21LGWhGzxm8EjC3oBxx3WFSqzx0LR +z1Xldc5x+a6xvcWjw0pefogg8ITLWykUJ7gQFbihtKCQkRV0/CwyxdQ8t6T/orlL +q4QWGcYtQknnuACOH0sZ0xMOnmzMs7t1NisfN0ZliGZMDKID0kM3qJiWlaQrinYg +Fw12K7bopan36wkrDwvL/ujlAPjDJzqEaiTS/SfCKZdmjvwQlgG7oEMa4vICC+rz +MtWyBeLhphE6yZWuDYglt7z4+08E3CSgC5HhALeFFNXdtfJl1sgLQps56R6AAdN5 +djMCnQdWQKxv+84uIa6O7+YpkVkqnn88p/wl2//MNMibxPAG++1ApPiqYBhSQpNd +Gs45YrWkJqRUtXsUZEQOL79vGFxAm3nKiksZPk1KhQXdFH7Xspn/mVI8SHk4xKmt +XJHvz0q9jXPv0la3fMHp8h9D3fo/I2H75IrEWaWcu4YrnB/nyN5/BKMYSRjZgATr +pjrwqqDf9bwmyNMUGe0G0PfFjW3RzgeHZ/d6lA+vPAfH2nLCCH4fZMjCedTIQ8up +1ilISxue91ssHwVmbZ/A9nUxk5k4aspycIyQw8TUM4iTF4UPHzFt4dF6lJElJt6Z +jWW235p8CczXyYl/fqA7w2cznox7/8VJNZtZpCWZQEDBIOUfFh8P1OAKNsECAwEA +AQKCAgBaxLY9X0sMwHCVY2/0osAFnm5X+c6lJUqbhzapee7xoRHExKXB/umoqoaB +G6T3HEvAp9iiYhNNMFFZjsoLb6aZ1CCAh0swdTBVC4cwr2jF2nRspL1lApz9q5QC +zmCsirhBVLwYWgef58TtgdxTLsEswRV/8trHcKsX0B9IJPYNz2u80GlL0ztg2d7N +t1bRmVttFUvPoMsNzlyVNGgei+Ah4VciuZxqwBNMSDN+DBa9TG9pr7kXXujHsdV7 +V9WBFGGfckVIQzNzNctLbPN135KT3u20CwTL54R/C5YdiQ+N1LlHjrJfyNRuXgwc +oGdLHVkImYaVwyy2+6DKqn1FEw0SNrHQxbYHqHZf22F4tQYw8jE1Me1o89cG6n8t +RDZxm/7JcHg1Pq2WZMO61Xn+m2kTt6dVrPfl4n70CSZxaelV5UesBqbrZOHOiE4d +WQRGfhw7Sg+YFrNvevN/8p9Z99ubbRNflRgz5juZstk1j6ZESEO9fs1omgXGOeoN +BzAYp1odSAeeMlkfIaNo2QpLcBMnc6nQSYNld2QIg4k+1VhQUbkxRLGh4C3gs35I +ujRLRujCOye9ybv2MiDTqahK/mKCmldLWmXInUdMGTdMdUlYpBvtq1G8RBQHCwBl +2F3BTlITzKcVz3nvUiqqZzjm3eR4WEdTPMX4jr2iDR/kh61nfQKCAQEA9rgYScAp +KS3C8Fa6WX8vRPFTMOJGpo1GET38K7iRVO4SxWQqWzoH16ZE2bN01lyzjvfqPoRR +eOBdpyaJU6onjE+XLK9qoNgrW7HaInuNF4zWTndo4UwTXnE9l2qm3rMgjngXla6l +PuC6QVsPu2eGhmyWMtVKAmlMFYT2p7P+cSEwNZnCVmeMdviqO8aGMOuHNBEJ408O +oI41+rvffjogvNPnvDN1DQntl134CLxa+jlpAcr9KgVfMZpOqR+wvcV4JZSkPflp +HRFWlcOk2dWnqrIAkNcmVs+P6tB/d7sdj8hGHw0xJ9o+UYBmdJnj9N49dc9TggJo +asVIQ2CFKQVPgwKCAQEA3Ct7yVXwZwgxHBg4ouLaCXZ4/oouBjuwEtx+SPujs79S +IbM8v03YuxR3SWEqnB+P6g/Sx3EijYhz95nbzhN1gR482n+aHgtrMKGF8V4ROwOq +F3xXhx15qfn53G9SQvo1jOBsKQgxCH+MDrfa2rUGaesMVSIw3rMImiCqT329mDEX +oMpCfPUNPTXNIBJnMRcFkENK9XBN2tO7puvgi57EzseUP0jhnBYIZigjuYDDnys3 +xax5r7+o7ialJvUuuvlrHiYc+km8Qg9lDWloayZPOTGY1lEAwqdAyuVhXKF92hJe +o9Y4aD33FLaKrbHm/zfj4+L8Yuh+c0NXuhTkiLIpawKCAQEAhSgo260dyf7LxoFY +hDMTpQcGWkzVytBWr7mfn003CvqPIQAFqETytJ4lbMXhWkygEJqXT3SEsFOP2EYB +OimMvLq8Ib7vMq5ZAF1GGPRL2xkFFUZ3UZmInqFJl65VL77H5HzGZd/jicMqY1mt +bPzb6zMyAW+CSTjhen/PzAVmX1KFPXimHZI3ioJ9BlQIWuDTkPNdPdSOVXNLiO7b +GbpvrtpDqRywoP/pvpdV5gkapRBVL0WKS6KolRHuQHM9Jb8tMENAPb6dz7Vq4Nu9 +3l/k5Ui663FjXNkbmKU9FrbjppV12w54qESu+7fsFCR2ltNXonzqWjHIf0/Ix6yR +Uelu1wKCAQEAxUq17zHybfFaSImv3s6XgZlHTRi3q2A7JHuvMmlERWMxDv/VdLwm +dWYeioPmseZaiOzK/Wt1Agz/liWqYRzw09Yrw8RKb5fd4sMrCqI3oIFlHwyORoZ0 +KovVieG7fkdGS0ojwhUUE0BwWhQIqqlC6RD2iSdNUZJvJ+YTl43eoo2DVdNJBz50 +MaCPgqjbDZNKqf6TIiMTsP7BDhAatCJ+y6juQFNnz/2yYxCfCrDHG0+X96vZk1KU +52t73NAiouu0QFz45JPEfhHbhMwrBLFclqzJ/2qw2r0Tg31O5LnV099YLUpeW5MD +YO0+ke10SMlljiUt8tfR0CnNZ/Mm4xN7pwKCAQEAplZEytHOTmb5eaFYc8uiTAp+ +p1qCriIlw5T5akw1ESSKbEQTXmKqqwHP9pvFtg9Vd1M2ccmZT4Lk4+AL4sgHcs6p +asX3xz4/A9mqJKLruFd4lGhY14HV9JA1n0xVFnV5KK/7y+9Y1ZLvcGv/jzd/EXcL +T2OZ8wCTRdT6oi4+HsWaitHfNiJ1zvBgwWY9wEHofdPHIJwp8gNh6RD+M2WjHl4v +0GCGQaoEaIAePCn0R8WISviLhAymu9sIIov/WMBQQbsc03JlKSRsd/s5FYObUBfX +iBzCgMvuGWuFeBTB7LYgzina0IFwJqxy6Z7ySgZJKigPkhhrG/iD/QxuT2MvxQ== +-----END RSA PRIVATE KEY----- diff --git a/packages/opentelemetry-exporter-collector-grpc/test/helper.ts b/packages/opentelemetry-exporter-collector-grpc/test/helper.ts new file mode 100644 index 00000000000..efdf6c811ef --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/test/helper.ts @@ -0,0 +1,518 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TraceFlags, ValueType } from '@opentelemetry/api'; +import { ReadableSpan } from '@opentelemetry/tracing'; +import { Resource } from '@opentelemetry/resources'; +import { collectorTypes } from '@opentelemetry/exporter-collector'; +import * as assert from 'assert'; +import { + MetricRecord, + MetricKind, + SumAggregator, + MinMaxLastSumCountAggregator, + HistogramAggregator, +} from '@opentelemetry/metrics'; +import * as grpc from 'grpc'; + +const traceIdArr = [ + 31, + 16, + 8, + 220, + 142, + 39, + 14, + 133, + 196, + 10, + 13, + 124, + 57, + 57, + 178, + 120, +]; +const spanIdArr = [94, 16, 114, 97, 246, 79, 165, 62]; +const parentIdArr = [120, 168, 145, 80, 152, 134, 67, 136]; + +export function mockCounter(): MetricRecord { + return { + descriptor: { + name: 'test-counter', + description: 'sample counter description', + unit: '1', + metricKind: MetricKind.COUNTER, + valueType: ValueType.INT, + }, + labels: {}, + aggregator: new SumAggregator(), + resource: new Resource({ + service: 'ui', + version: 1, + cost: 112.12, + }), + instrumentationLibrary: { name: 'default', version: '0.0.1' }, + }; +} + +export function mockDoubleCounter(): MetricRecord { + return { + descriptor: { + name: 'test-counter', + description: 'sample counter description', + unit: '1', + metricKind: MetricKind.COUNTER, + valueType: ValueType.DOUBLE, + }, + labels: {}, + aggregator: new SumAggregator(), + resource: new Resource({ + service: 'ui', + version: 1, + cost: 112.12, + }), + instrumentationLibrary: { name: 'default', version: '0.0.1' }, + }; +} + +export function mockObserver(): MetricRecord { + return { + descriptor: { + name: 'test-observer', + description: 'sample observer description', + unit: '2', + metricKind: MetricKind.VALUE_OBSERVER, + valueType: ValueType.DOUBLE, + }, + labels: {}, + aggregator: new MinMaxLastSumCountAggregator(), + resource: new Resource({ + service: 'ui', + version: 1, + cost: 112.12, + }), + instrumentationLibrary: { name: 'default', version: '0.0.1' }, + }; +} + +export function mockValueRecorder(): MetricRecord { + return { + descriptor: { + name: 'test-recorder', + description: 'sample recorder description', + unit: '3', + metricKind: MetricKind.VALUE_RECORDER, + valueType: ValueType.INT, + }, + labels: {}, + aggregator: new MinMaxLastSumCountAggregator(), + resource: new Resource({ + service: 'ui', + version: 1, + cost: 112.12, + }), + instrumentationLibrary: { name: 'default', version: '0.0.1' }, + }; +} + +export function mockHistogram(): MetricRecord { + return { + descriptor: { + name: 'test-hist', + description: 'sample observer description', + unit: '2', + metricKind: MetricKind.VALUE_OBSERVER, + valueType: ValueType.DOUBLE, + }, + labels: {}, + aggregator: new HistogramAggregator([10, 20]), + resource: new Resource({ + service: 'ui', + version: 1, + cost: 112.12, + }), + instrumentationLibrary: { name: 'default', version: '0.0.1' }, + }; +} + +export const mockedReadableSpan: ReadableSpan = { + name: 'documentFetch', + kind: 0, + spanContext: { + traceId: '1f1008dc8e270e85c40a0d7c3939b278', + spanId: '5e107261f64fa53e', + traceFlags: TraceFlags.SAMPLED, + }, + parentSpanId: '78a8915098864388', + startTime: [1574120165, 429803070], + endTime: [1574120165, 438688070], + ended: true, + status: { code: 0 }, + attributes: { component: 'document-load' }, + links: [ + { + context: { + traceId: '1f1008dc8e270e85c40a0d7c3939b278', + spanId: '78a8915098864388', + }, + attributes: { component: 'document-load' }, + }, + ], + events: [ + { name: 'fetchStart', time: [1574120165, 429803070] }, + { + name: 'domainLookupStart', + time: [1574120165, 429803070], + }, + { name: 'domainLookupEnd', time: [1574120165, 429803070] }, + { + name: 'connectStart', + time: [1574120165, 429803070], + }, + { name: 'connectEnd', time: [1574120165, 429803070] }, + { + name: 'requestStart', + time: [1574120165, 435513070], + }, + { name: 'responseStart', time: [1574120165, 436923070] }, + { + name: 'responseEnd', + time: [1574120165, 438688070], + }, + ], + duration: [0, 8885000], + resource: new Resource({ + service: 'ui', + version: 1, + cost: 112.12, + }), + instrumentationLibrary: { name: 'default', version: '0.0.1' }, +}; + +export function ensureExportedEventsAreCorrect( + events: collectorTypes.opentelemetryProto.trace.v1.Span.Event[] +) { + assert.deepStrictEqual( + events, + [ + { + attributes: [], + timeUnixNano: '1574120165429803008', + name: 'fetchStart', + droppedAttributesCount: 0, + }, + { + attributes: [], + timeUnixNano: '1574120165429803008', + name: 'domainLookupStart', + droppedAttributesCount: 0, + }, + { + attributes: [], + timeUnixNano: '1574120165429803008', + name: 'domainLookupEnd', + droppedAttributesCount: 0, + }, + { + attributes: [], + timeUnixNano: '1574120165429803008', + name: 'connectStart', + droppedAttributesCount: 0, + }, + { + attributes: [], + timeUnixNano: '1574120165429803008', + name: 'connectEnd', + droppedAttributesCount: 0, + }, + { + attributes: [], + timeUnixNano: '1574120165435513088', + name: 'requestStart', + droppedAttributesCount: 0, + }, + { + attributes: [], + timeUnixNano: '1574120165436923136', + name: 'responseStart', + droppedAttributesCount: 0, + }, + { + attributes: [], + timeUnixNano: '1574120165438688000', + name: 'responseEnd', + droppedAttributesCount: 0, + }, + ], + 'exported events are incorrect' + ); +} + +export function ensureExportedAttributesAreCorrect( + attributes: collectorTypes.opentelemetryProto.common.v1.KeyValue[] +) { + assert.deepStrictEqual( + attributes, + [ + { + key: 'component', + value: { + stringValue: 'document-load', + value: 'stringValue', + }, + }, + ], + 'exported attributes are incorrect' + ); +} + +export function ensureExportedLinksAreCorrect( + attributes: collectorTypes.opentelemetryProto.trace.v1.Span.Link[] +) { + assert.deepStrictEqual( + attributes, + [ + { + attributes: [ + { + key: 'component', + value: { + stringValue: 'document-load', + value: 'stringValue', + }, + }, + ], + traceId: Buffer.from(traceIdArr), + spanId: Buffer.from(parentIdArr), + traceState: '', + droppedAttributesCount: 0, + }, + ], + 'exported links are incorrect' + ); +} + +export function ensureExportedSpanIsCorrect( + span: collectorTypes.opentelemetryProto.trace.v1.Span +) { + if (span.attributes) { + ensureExportedAttributesAreCorrect(span.attributes); + } + if (span.events) { + ensureExportedEventsAreCorrect(span.events); + } + if (span.links) { + ensureExportedLinksAreCorrect(span.links); + } + assert.deepStrictEqual( + span.traceId, + Buffer.from(traceIdArr), + 'traceId is wrong' + ); + assert.deepStrictEqual( + span.spanId, + Buffer.from(spanIdArr), + 'spanId is wrong' + ); + assert.strictEqual(span.traceState, '', 'traceState is wrong'); + assert.deepStrictEqual( + span.parentSpanId, + Buffer.from(parentIdArr), + 'parentIdArr is wrong' + ); + assert.strictEqual(span.name, 'documentFetch', 'name is wrong'); + assert.strictEqual(span.kind, 'INTERNAL', 'kind is wrong'); + assert.strictEqual( + span.startTimeUnixNano, + '1574120165429803008', + 'startTimeUnixNano is wrong' + ); + assert.strictEqual( + span.endTimeUnixNano, + '1574120165438688000', + 'endTimeUnixNano is wrong' + ); + assert.strictEqual( + span.droppedAttributesCount, + 0, + 'droppedAttributesCount is wrong' + ); + assert.strictEqual(span.droppedEventsCount, 0, 'droppedEventsCount is wrong'); + assert.strictEqual(span.droppedLinksCount, 0, 'droppedLinksCount is wrong'); + assert.deepStrictEqual( + span.status, + { code: 'Ok', message: '' }, + 'status is wrong' + ); +} + +export function ensureExportedCounterIsCorrect( + metric: collectorTypes.opentelemetryProto.metrics.v1.Metric +) { + assert.deepStrictEqual(metric.metricDescriptor, { + name: 'test-counter', + description: 'sample counter description', + unit: '1', + type: 'MONOTONIC_INT64', + temporality: 'CUMULATIVE', + }); + assert.deepStrictEqual(metric.doubleDataPoints, []); + assert.deepStrictEqual(metric.summaryDataPoints, []); + assert.deepStrictEqual(metric.histogramDataPoints, []); + assert.ok(metric.int64DataPoints); + assert.deepStrictEqual(metric.int64DataPoints[0].labels, []); + assert.deepStrictEqual(metric.int64DataPoints[0].value, '1'); + assert.deepStrictEqual( + metric.int64DataPoints[0].startTimeUnixNano, + '1592602232694000128' + ); +} + +export function ensureExportedObserverIsCorrect( + metric: collectorTypes.opentelemetryProto.metrics.v1.Metric +) { + assert.deepStrictEqual(metric.metricDescriptor, { + name: 'test-observer', + description: 'sample observer description', + unit: '2', + type: 'SUMMARY', + temporality: 'DELTA', + }); + + assert.deepStrictEqual(metric.int64DataPoints, []); + assert.deepStrictEqual(metric.doubleDataPoints, []); + assert.deepStrictEqual(metric.histogramDataPoints, []); + assert.ok(metric.summaryDataPoints); + assert.deepStrictEqual(metric.summaryDataPoints[0].labels, []); + assert.deepStrictEqual(metric.summaryDataPoints[0].sum, 9); + assert.deepStrictEqual(metric.summaryDataPoints[0].count, '2'); + assert.deepStrictEqual( + metric.summaryDataPoints[0].startTimeUnixNano, + '1592602232694000128' + ); + assert.deepStrictEqual(metric.summaryDataPoints[0].percentileValues, [ + { percentile: 0, value: 3 }, + { percentile: 100, value: 6 }, + ]); +} + +export function ensureExportedHistogramIsCorrect( + metric: collectorTypes.opentelemetryProto.metrics.v1.Metric +) { + assert.deepStrictEqual(metric.metricDescriptor, { + name: 'test-hist', + description: 'sample observer description', + unit: '2', + type: 'HISTOGRAM', + temporality: 'DELTA', + }); + assert.deepStrictEqual(metric.int64DataPoints, []); + assert.deepStrictEqual(metric.summaryDataPoints, []); + assert.deepStrictEqual(metric.doubleDataPoints, []); + assert.ok(metric.histogramDataPoints); + assert.deepStrictEqual(metric.histogramDataPoints[0].labels, []); + assert.deepStrictEqual(metric.histogramDataPoints[0].count, '2'); + assert.deepStrictEqual(metric.histogramDataPoints[0].sum, 21); + assert.deepStrictEqual(metric.histogramDataPoints[0].buckets, [ + { count: '1', exemplar: null }, + { count: '1', exemplar: null }, + { count: '0', exemplar: null }, + ]); + assert.deepStrictEqual(metric.histogramDataPoints[0].explicitBounds, [ + 10, + 20, + ]); + assert.deepStrictEqual( + metric.histogramDataPoints[0].startTimeUnixNano, + '1592602232694000128' + ); +} + +export function ensureExportedValueRecorderIsCorrect( + metric: collectorTypes.opentelemetryProto.metrics.v1.Metric +) { + assert.deepStrictEqual(metric.metricDescriptor, { + name: 'test-recorder', + description: 'sample recorder description', + unit: '3', + type: 'SUMMARY', + temporality: 'DELTA', + }); + assert.deepStrictEqual(metric.histogramDataPoints, []); + assert.deepStrictEqual(metric.int64DataPoints, []); + assert.deepStrictEqual(metric.doubleDataPoints, []); + assert.ok(metric.summaryDataPoints); + assert.deepStrictEqual(metric.summaryDataPoints[0].labels, []); + assert.deepStrictEqual( + metric.summaryDataPoints[0].startTimeUnixNano, + '1592602232694000128' + ); + assert.deepStrictEqual(metric.summaryDataPoints[0].percentileValues, [ + { percentile: 0, value: 5 }, + { percentile: 100, value: 5 }, + ]); + assert.deepStrictEqual(metric.summaryDataPoints[0].count, '1'); + assert.deepStrictEqual(metric.summaryDataPoints[0].sum, 5); +} + +export function ensureResourceIsCorrect( + resource: collectorTypes.opentelemetryProto.resource.v1.Resource +) { + assert.deepStrictEqual(resource, { + attributes: [ + { + key: 'service.name', + value: { + stringValue: 'basic-service', + value: 'stringValue', + }, + }, + { + key: 'service', + value: { + stringValue: 'ui', + value: 'stringValue', + }, + }, + { + key: 'version', + value: { + doubleValue: 1, + value: 'doubleValue', + }, + }, + { + key: 'cost', + value: { + doubleValue: 112.12, + value: 'doubleValue', + }, + }, + ], + droppedAttributesCount: 0, + }); +} + +export function ensureMetadataIsCorrect( + actual: grpc.Metadata, + expected: grpc.Metadata +) { + //ignore user agent + expected.remove('user-agent'); + actual.remove('user-agent'); + assert.deepStrictEqual(actual.getMap(), expected.getMap()); +} diff --git a/packages/opentelemetry-exporter-collector-grpc/tsconfig.json b/packages/opentelemetry-exporter-collector-grpc/tsconfig.json new file mode 100644 index 00000000000..a2042cd68b1 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-grpc/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/packages/opentelemetry-exporter-collector-proto/.eslintignore b/packages/opentelemetry-exporter-collector-proto/.eslintignore new file mode 100644 index 00000000000..378eac25d31 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/.eslintignore @@ -0,0 +1 @@ +build diff --git a/packages/opentelemetry-exporter-collector-proto/.eslintrc.js b/packages/opentelemetry-exporter-collector-proto/.eslintrc.js new file mode 100644 index 00000000000..fc4d0381204 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + "env": { + "mocha": true, + "commonjs": true, + "node": true, + }, + ...require('../../eslint.config.js') +} diff --git a/packages/opentelemetry-exporter-collector-proto/.npmignore b/packages/opentelemetry-exporter-collector-proto/.npmignore new file mode 100644 index 00000000000..9505ba9450f --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/packages/opentelemetry-exporter-collector-proto/LICENSE b/packages/opentelemetry-exporter-collector-proto/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/packages/opentelemetry-exporter-collector-proto/README.md b/packages/opentelemetry-exporter-collector-proto/README.md new file mode 100644 index 00000000000..3331b2b4767 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/README.md @@ -0,0 +1,88 @@ +# OpenTelemetry Collector Exporter for node with protobuf + +[![Gitter chat][gitter-image]][gitter-url] +[![NPM Published Version][npm-img]][npm-url] +[![dependencies][dependencies-image]][dependencies-url] +[![devDependencies][devDependencies-image]][devDependencies-url] +[![Apache License][license-image]][license-image] + +This module provides exporter for web and node to be used with [opentelemetry-collector][opentelemetry-collector-url] - last tested with version **0.6.0**. + +## Installation + +```bash +npm install --save @opentelemetry/exporter-collector-proto +``` + +## Traces in Node - PROTO over http + +```js +const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/tracing'); +const { CollectorExporter } = require('@opentelemetry/exporter-collector-proto'); + +const collectorOptions = { + serviceName: 'basic-service', + url: '', // url is optional and can be omitted - default is http://localhost:55681/v1/trace + headers: { + foo: 'bar' + }, //an optional object containing custom headers to be sent with each request will only work with http +}; + +const provider = new BasicTracerProvider(); +const exporter = new CollectorExporter(collectorOptions); +provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); + +provider.register(); + +``` + +## Metrics in Node - PROTO over http + +```js +const { MeterProvider } = require('@opentelemetry/metrics'); +const { CollectorMetricExporter } = require('@opentelemetry/exporter-collector-proto'); +const collectorOptions = { + serviceName: 'basic-service', + url: '', // url is optional and can be omitted - default is http://localhost:55681/v1/metrics +}; +const exporter = new CollectorMetricExporter(collectorOptions); + +// Register the exporter +const meter = new MeterProvider({ + exporter, + interval: 60000, +}).getMeter('example-meter'); + +// Now, start recording data +const counter = meter.createCounter('metric_name'); +counter.add(10, { 'key': 'value' }); + +``` + +## Running opentelemetry-collector locally to see the traces + +1. Go to examples/collector-exporter-node +2. run `npm run docker:start` +3. Open page at `http://localhost:9411/zipkin/` to observe the traces + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us on [gitter][gitter-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[gitter-image]: https://badges.gitter.im/open-telemetry/opentelemetry-js.svg +[gitter-url]: https://gitter.im/open-telemetry/opentelemetry-node?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge +[license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/master/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[dependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/status.svg?path=packages/opentelemetry-exporter-collector-proto +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-exporter-collector-proto +[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-exporter-collector-proto +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-exporter-collector-proto&type=dev +[npm-url]: https://www.npmjs.com/package/@opentelemetry/exporter-collector-proto +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fexporter-collector-proto.svg +[opentelemetry-collector-url]: https://github.com/open-telemetry/opentelemetry-collector diff --git a/packages/opentelemetry-exporter-collector-proto/package.json b/packages/opentelemetry-exporter-collector-proto/package.json new file mode 100644 index 00000000000..59350ba9253 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/package.json @@ -0,0 +1,76 @@ +{ + "name": "@opentelemetry/exporter-collector-proto", + "version": "0.10.2", + "description": "OpenTelemetry Collector Exporter allows user to send collected traces to the OpenTelemetry Collector", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js", + "scripts": { + "clean": "rimraf build/*", + "compile": "npm run version:update && tsc -p .", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "postcompile": "npm run submodule && npm run protos:copy", + "precompile": "tsc --version", + "prepare": "npm run compile", + "protos:copy": "cpx protos/opentelemetry/**/*.* build/protos/opentelemetry", + "submodule": "git submodule sync --recursive && git submodule update --init --recursive", + "tdd": "npm run test -- --watch-extensions ts --watch", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", + "version:update": "node ../../scripts/version-update.js", + "watch": "npm run protos:copy && tsc -w" + }, + "keywords": [ + "opentelemetry", + "nodejs", + "protobuf", + "tracing", + "profiling", + "metrics", + "stats" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts", + "build/src/**/*.proto", + "doc", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@babel/core": "7.11.1", + "@types/mocha": "8.0.2", + "@types/node": "14.0.27", + "@types/sinon": "9.0.4", + "codecov": "3.7.2", + "cpx": "1.5.0", + "gts": "2.0.2", + "mocha": "7.2.0", + "nyc": "15.1.0", + "rimraf": "3.0.2", + "sinon": "9.0.3", + "ts-loader": "8.0.2", + "ts-mocha": "7.0.0", + "ts-node": "8.10.2", + "typescript": "3.9.7" + }, + "dependencies": { + "@grpc/proto-loader": "^0.5.4", + "@opentelemetry/api": "^0.10.2", + "@opentelemetry/core": "^0.10.2", + "@opentelemetry/exporter-collector": "^0.10.2", + "@opentelemetry/metrics": "^0.10.2", + "@opentelemetry/resources": "^0.10.2", + "@opentelemetry/tracing": "^0.10.2", + "protobufjs": "^6.9.0" + } +} diff --git a/packages/opentelemetry-exporter-collector-proto/protos b/packages/opentelemetry-exporter-collector-proto/protos new file mode 160000 index 00000000000..e43e1abc404 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/protos @@ -0,0 +1 @@ +Subproject commit e43e1abc40428a6ee98e3bfd79bec1dfa2ed18cd diff --git a/packages/opentelemetry-exporter-collector-proto/src/CollectorExporterNodeBase.ts b/packages/opentelemetry-exporter-collector-proto/src/CollectorExporterNodeBase.ts new file mode 100644 index 00000000000..fc56d69f39a --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/src/CollectorExporterNodeBase.ts @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + CollectorExporterNodeBase as CollectorExporterBaseMain, + collectorTypes, +} from '@opentelemetry/exporter-collector'; +import { ServiceClientType } from './types'; + +/** + * Collector Metric Exporter abstract base class + */ +export abstract class CollectorExporterNodeBase< + ExportItem, + ServiceRequest +> extends CollectorExporterBaseMain { + private _send!: Function; + onInit(config: collectorTypes.CollectorExporterConfigBase): void { + this._isShutdown = false; + // defer to next tick and lazy load to avoid loading protobufjs too early + // and making this impossible to be instrumented + setImmediate(() => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { onInit } = require('./util'); + onInit(this, config); + }); + } + + send( + objects: ExportItem[], + onSuccess: () => void, + onError: (error: collectorTypes.CollectorExporterError) => void + ): void { + if (this._isShutdown) { + this.logger.debug('Shutdown already started. Cannot send objects'); + return; + } + if (!this._send) { + // defer to next tick and lazy load to avoid loading protobufjs too early + // and making this impossible to be instrumented + setImmediate(() => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { send } = require('./util'); + this._send = send; + this._send(this, objects, onSuccess, onError); + }); + } else { + this._send(this, objects, onSuccess, onError); + } + } + + abstract getServiceClientType(): ServiceClientType; +} diff --git a/packages/opentelemetry-exporter-collector-proto/src/CollectorMetricExporter.ts b/packages/opentelemetry-exporter-collector-proto/src/CollectorMetricExporter.ts new file mode 100644 index 00000000000..d10611b3a90 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/src/CollectorMetricExporter.ts @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + collectorTypes, + toCollectorExportMetricServiceRequest, +} from '@opentelemetry/exporter-collector'; +import { MetricRecord, MetricExporter } from '@opentelemetry/metrics'; +import { ServiceClientType } from './types'; +import { CollectorExporterNodeBase } from './CollectorExporterNodeBase'; + +const DEFAULT_SERVICE_NAME = 'collector-metric-exporter'; +const DEFAULT_COLLECTOR_URL = 'http://localhost:55681/v1/metrics'; + +/** + * Collector Metric Exporter for Node with protobuf + */ +export class CollectorMetricExporter + extends CollectorExporterNodeBase< + MetricRecord, + collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest + > + implements MetricExporter { + // Converts time to nanoseconds + protected readonly _startTime = new Date().getTime() * 1000000; + + convert( + metrics: MetricRecord[] + ): collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest { + return toCollectorExportMetricServiceRequest( + metrics, + this._startTime, + this + ); + } + + getDefaultUrl(config: collectorTypes.CollectorExporterConfigBase): string { + if (!config.url) { + return DEFAULT_COLLECTOR_URL; + } + return config.url; + } + + getDefaultServiceName( + config: collectorTypes.CollectorExporterConfigBase + ): string { + return config.serviceName || DEFAULT_SERVICE_NAME; + } + + getServiceClientType() { + return ServiceClientType.METRICS; + } +} diff --git a/packages/opentelemetry-exporter-collector-proto/src/CollectorTraceExporter.ts b/packages/opentelemetry-exporter-collector-proto/src/CollectorTraceExporter.ts new file mode 100644 index 00000000000..79ca82049c5 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/src/CollectorTraceExporter.ts @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ReadableSpan, SpanExporter } from '@opentelemetry/tracing'; +import { CollectorExporterNodeBase } from './CollectorExporterNodeBase'; +import { + collectorTypes, + toCollectorExportTraceServiceRequest, +} from '@opentelemetry/exporter-collector'; +import { ServiceClientType } from './types'; + +const DEFAULT_SERVICE_NAME = 'collector-trace-exporter'; +const DEFAULT_COLLECTOR_URL = 'http://localhost:55681/v1/trace'; + +/** + * Collector Trace Exporter for Node with protobuf + */ +export class CollectorTraceExporter + extends CollectorExporterNodeBase< + ReadableSpan, + collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest + > + implements SpanExporter { + convert( + spans: ReadableSpan[] + ): collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest { + return toCollectorExportTraceServiceRequest(spans, this); + } + + getDefaultUrl(config: collectorTypes.CollectorExporterConfigBase): string { + if (!config.url) { + return DEFAULT_COLLECTOR_URL; + } + return config.url; + } + + getDefaultServiceName( + config: collectorTypes.CollectorExporterConfigBase + ): string { + return config.serviceName || DEFAULT_SERVICE_NAME; + } + + getServiceClientType() { + return ServiceClientType.SPANS; + } +} diff --git a/packages/opentelemetry-exporter-collector-proto/src/index.ts b/packages/opentelemetry-exporter-collector-proto/src/index.ts new file mode 100644 index 00000000000..fcbe012b52b --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/src/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './CollectorTraceExporter'; +export * from './CollectorMetricExporter'; diff --git a/packages/opentelemetry-exporter-collector/src/enums.ts b/packages/opentelemetry-exporter-collector-proto/src/types.ts similarity index 80% rename from packages/opentelemetry-exporter-collector/src/enums.ts rename to packages/opentelemetry-exporter-collector-proto/src/types.ts index 8e8e27a299c..389cfb1f522 100644 --- a/packages/opentelemetry-exporter-collector/src/enums.ts +++ b/packages/opentelemetry-exporter-collector-proto/src/types.ts @@ -14,12 +14,7 @@ * limitations under the License. */ -/** - * Collector transport protocol node options - * Default is GRPC - */ -export enum CollectorProtocolNode { - GRPC, - HTTP_JSON, - HTTP_PROTO, +export enum ServiceClientType { + SPANS, + METRICS, } diff --git a/packages/opentelemetry-exporter-collector-proto/src/util.ts b/packages/opentelemetry-exporter-collector-proto/src/util.ts new file mode 100644 index 00000000000..d43fc8645d7 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/src/util.ts @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + collectorTypes, + sendWithHttp, +} from '@opentelemetry/exporter-collector'; +import * as path from 'path'; + +import { ServiceClientType } from './types'; +import { CollectorExporterNodeBase } from './CollectorExporterNodeBase'; +import type { Type } from 'protobufjs'; +import * as protobufjs from 'protobufjs'; + +let ExportRequestProto: Type | undefined; + +export function getExportRequestProto(): Type | undefined { + return ExportRequestProto; +} + +export function onInit( + collector: CollectorExporterNodeBase, + _config: collectorTypes.CollectorExporterConfigBase +): void { + const dir = path.resolve(__dirname, '..', 'protos'); + const root = new protobufjs.Root(); + root.resolvePath = function (origin, target) { + return `${dir}/${target}`; + }; + if (collector.getServiceClientType() === ServiceClientType.SPANS) { + const proto = root.loadSync([ + 'opentelemetry/proto/common/v1/common.proto', + 'opentelemetry/proto/resource/v1/resource.proto', + 'opentelemetry/proto/trace/v1/trace.proto', + 'opentelemetry/proto/collector/trace/v1/trace_service.proto', + ]); + ExportRequestProto = proto?.lookupType('ExportTraceServiceRequest'); + } else { + const proto = root.loadSync([ + 'opentelemetry/proto/common/v1/common.proto', + 'opentelemetry/proto/resource/v1/resource.proto', + 'opentelemetry/proto/metrics/v1/metrics.proto', + 'opentelemetry/proto/collector/metrics/v1/metrics_service.proto', + ]); + ExportRequestProto = proto?.lookupType('ExportMetricsServiceRequest'); + } +} + +export function send( + collector: CollectorExporterNodeBase, + objects: ExportItem[], + onSuccess: () => void, + onError: (error: collectorTypes.CollectorExporterError) => void +): void { + const serviceRequest = collector.convert(objects); + + const message = getExportRequestProto()?.create(serviceRequest); + if (message) { + const body = getExportRequestProto()?.encode(message).finish(); + if (body) { + sendWithHttp( + collector, + Buffer.from(body), + 'application/x-protobuf', + onSuccess, + onError + ); + } + } else { + onError({ + message: 'No proto', + }); + } +} diff --git a/packages/opentelemetry-exporter-collector-proto/src/version.ts b/packages/opentelemetry-exporter-collector-proto/src/version.ts new file mode 100644 index 00000000000..ea45ee2fc46 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/src/version.ts @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// this is autogenerated file, see scripts/version-update.js +export const VERSION = '0.10.2'; diff --git a/packages/opentelemetry-exporter-collector-proto/submodule.md b/packages/opentelemetry-exporter-collector-proto/submodule.md new file mode 100644 index 00000000000..66ca6c5f992 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/submodule.md @@ -0,0 +1,46 @@ +# Important + +**Submodule is always pointing to certain revision number. So updating the master of the submodule repo will not have impact on your code. +Knowing this if you want to change the submodule to point to a different version (when for example proto has changed) here is how to do it:** + +## Updating submodule to point to certain revision number + +1. Make sure you are in the same folder as this instruction + +2. Update your submodules by running this command + + ```shell script + git submodule sync --recursive + git submodule update --init --recursive + ``` + +3. Find the SHA which you want to update to and copy it (the long one) +the latest sha when this guide was written is `b54688569186e0b862bf7462a983ccf2c50c0547` + +4. Enter a submodule directory from this directory + + ```shell script + cd protos + ``` + +5. Updates files in the submodule tree to given commit: + + ```shell script + git checkout -q + ``` + +6. Return to the main directory: + + ```shell script + cd ../ + ``` + +7. Please run `git status` you should see something like `Head detached at`. This is correct, go to next step + +8. Now thing which is very important. You have to commit this to apply these changes + + ```shell script + git commit -am "chore: updating submodule for opentelemetry-proto" + ``` + +9. If you look now at git log you will notice that the folder `protos` has been changed and it will show what was the previous sha and what is current one. diff --git a/packages/opentelemetry-exporter-collector/test/node/CollectorMetricExporterWithJson.test.ts b/packages/opentelemetry-exporter-collector-proto/test/CollectorMetricExporter.test.ts similarity index 72% rename from packages/opentelemetry-exporter-collector/test/node/CollectorMetricExporterWithJson.test.ts rename to packages/opentelemetry-exporter-collector-proto/test/CollectorMetricExporter.test.ts index 433b9d867a5..83f3f7a101f 100644 --- a/packages/opentelemetry-exporter-collector/test/node/CollectorMetricExporterWithJson.test.ts +++ b/packages/opentelemetry-exporter-collector-proto/test/CollectorMetricExporter.test.ts @@ -14,26 +14,26 @@ * limitations under the License. */ +import { collectorTypes } from '@opentelemetry/exporter-collector'; + import * as core from '@opentelemetry/core'; import * as http from 'http'; import * as assert from 'assert'; import * as sinon from 'sinon'; -import { CollectorProtocolNode } from '../../src/enums'; -import { CollectorMetricExporter } from '../../src/platform/node'; -import { CollectorExporterConfigNode } from '../../src/platform/node/types'; -import * as collectorTypes from '../../src/types'; +import { CollectorMetricExporter } from '../src'; +import { getExportRequestProto } from '../src/util'; import { mockCounter, mockObserver, mockHistogram, ensureExportMetricsServiceRequestIsSet, - ensureCounterIsCorrect, mockValueRecorder, - ensureValueRecorderIsCorrect, - ensureHistogramIsCorrect, - ensureObserverIsCorrect, -} from '../helper'; + ensureExportedCounterIsCorrect, + ensureExportedObserverIsCorrect, + ensureExportedHistogramIsCorrect, + ensureExportedValueRecorderIsCorrect, +} from './helper'; import { MetricRecord } from '@opentelemetry/metrics'; const fakeRequest = { @@ -50,9 +50,12 @@ const mockResError = { statusCode: 400, }; -describe('CollectorMetricExporter - node with json over http', () => { +// send is lazy loading file so need to wait a bit +const waitTimeMS = 20; + +describe('CollectorMetricExporter - node with proto over http', () => { let collectorExporter: CollectorMetricExporter; - let collectorExporterConfig: CollectorExporterConfigNode; + let collectorExporterConfig: collectorTypes.CollectorExporterConfigBase; let spyRequest: sinon.SinonSpy; let spyWrite: sinon.SinonSpy; let metrics: MetricRecord[]; @@ -64,7 +67,6 @@ describe('CollectorMetricExporter - node with json over http', () => { headers: { foo: 'bar', }, - protocolNode: CollectorProtocolNode.HTTP_JSON, hostname: 'foo', logger: new core.NoopLogger(), serviceName: 'bar', @@ -104,7 +106,7 @@ describe('CollectorMetricExporter - node with json over http', () => { assert.strictEqual(options.method, 'POST'); assert.strictEqual(options.path, '/'); done(); - }); + }, waitTimeMS); }); it('should set custom headers', done => { @@ -115,7 +117,7 @@ describe('CollectorMetricExporter - node with json over http', () => { const options = args[0]; assert.strictEqual(options.headers['foo'], 'bar'); done(); - }); + }, waitTimeMS); }); it('should successfully send metrics', done => { @@ -123,9 +125,10 @@ describe('CollectorMetricExporter - node with json over http', () => { setTimeout(() => { const writeArgs = spyWrite.args[0]; - const json = JSON.parse( - writeArgs[0] - ) as collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest; + const ExportTraceServiceRequestProto = getExportRequestProto(); + const data = ExportTraceServiceRequestProto?.decode(writeArgs[0]); + const json = data?.toJSON() as collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest; + const metric1 = json.resourceMetrics[0].instrumentationLibraryMetrics[0].metrics[0]; const metric2 = @@ -135,33 +138,21 @@ describe('CollectorMetricExporter - node with json over http', () => { const metric4 = json.resourceMetrics[3].instrumentationLibraryMetrics[0].metrics[0]; assert.ok(typeof metric1 !== 'undefined', "counter doesn't exist"); - ensureCounterIsCorrect( - metric1, - core.hrTimeToNanoseconds(metrics[0].aggregator.toPoint().timestamp) - ); + ensureExportedCounterIsCorrect(metric1); assert.ok(typeof metric2 !== 'undefined', "observer doesn't exist"); - ensureObserverIsCorrect( - metric2, - core.hrTimeToNanoseconds(metrics[1].aggregator.toPoint().timestamp) - ); + ensureExportedObserverIsCorrect(metric2); assert.ok(typeof metric3 !== 'undefined', "histogram doesn't exist"); - ensureHistogramIsCorrect( - metric3, - core.hrTimeToNanoseconds(metrics[2].aggregator.toPoint().timestamp) - ); + ensureExportedHistogramIsCorrect(metric3); assert.ok( typeof metric4 !== 'undefined', "value recorder doesn't exist" ); - ensureValueRecorderIsCorrect( - metric4, - core.hrTimeToNanoseconds(metrics[3].aggregator.toPoint().timestamp) - ); + ensureExportedValueRecorderIsCorrect(metric4); ensureExportMetricsServiceRequestIsSet(json); done(); - }); + }, waitTimeMS); }); it('should log the successful message', done => { @@ -182,7 +173,7 @@ describe('CollectorMetricExporter - node with json over http', () => { assert.strictEqual(responseSpy.args[0][0], 0); done(); }); - }); + }, waitTimeMS); }); it('should log the error message', done => { @@ -202,30 +193,7 @@ describe('CollectorMetricExporter - node with json over http', () => { assert.strictEqual(responseSpy.args[0][0], 1); done(); }); - }); - }); - }); - describe('CollectorMetricExporter - node (getDefaultUrl)', () => { - it('should default to localhost', done => { - const collectorExporter = new CollectorMetricExporter({ - protocolNode: CollectorProtocolNode.HTTP_JSON, - }); - setTimeout(() => { - assert.strictEqual( - collectorExporter['url'], - 'http://localhost:55681/v1/metrics' - ); - done(); - }); - }); - - it('should keep the URL if included', done => { - const url = 'http://foo.bar.com'; - const collectorExporter = new CollectorMetricExporter({ url }); - setTimeout(() => { - assert.strictEqual(collectorExporter['url'], url); - done(); - }); + }, waitTimeMS); }); }); }); diff --git a/packages/opentelemetry-exporter-collector/test/node/CollectorTraceExporterWithProto.test.ts b/packages/opentelemetry-exporter-collector-proto/test/CollectorTraceExporter.test.ts similarity index 77% rename from packages/opentelemetry-exporter-collector/test/node/CollectorTraceExporterWithProto.test.ts rename to packages/opentelemetry-exporter-collector-proto/test/CollectorTraceExporter.test.ts index e847f6bea3b..c0eccf6df0c 100644 --- a/packages/opentelemetry-exporter-collector/test/node/CollectorTraceExporterWithProto.test.ts +++ b/packages/opentelemetry-exporter-collector-proto/test/CollectorTraceExporter.test.ts @@ -14,22 +14,21 @@ * limitations under the License. */ +import { collectorTypes } from '@opentelemetry/exporter-collector'; + import * as core from '@opentelemetry/core'; import { ReadableSpan } from '@opentelemetry/tracing'; import * as http from 'http'; import * as assert from 'assert'; import * as sinon from 'sinon'; -import { CollectorProtocolNode } from '../../src/enums'; -import { CollectorTraceExporter } from '../../src/platform/node'; -import { CollectorExporterConfigNode } from '../../src/platform/node/types'; -import { getExportTraceServiceRequestProto } from '../../src/platform/node/utilWithJsonProto'; -import * as collectorTypes from '../../src/types'; +import { CollectorTraceExporter } from '../src'; +import { getExportRequestProto } from '../src/util'; import { ensureExportTraceServiceRequestIsSet, ensureProtoSpanIsCorrect, mockedReadableSpan, -} from '../helper'; +} from './helper'; const fakeRequest = { end: function () {}, @@ -45,9 +44,12 @@ const mockResError = { statusCode: 400, }; +// send is lazy loading file so need to wait a bit +const waitTimeMS = 20; + describe('CollectorExporter - node with proto over http', () => { let collectorExporter: CollectorTraceExporter; - let collectorExporterConfig: CollectorExporterConfigNode; + let collectorExporterConfig: collectorTypes.CollectorExporterConfigBase; let spyRequest: sinon.SinonSpy; let spyWrite: sinon.SinonSpy; let spans: ReadableSpan[]; @@ -59,7 +61,6 @@ describe('CollectorExporter - node with proto over http', () => { headers: { foo: 'bar', }, - protocolNode: CollectorProtocolNode.HTTP_PROTO, hostname: 'foo', logger: new core.NoopLogger(), serviceName: 'bar', @@ -86,7 +87,7 @@ describe('CollectorExporter - node with proto over http', () => { assert.strictEqual(options.method, 'POST'); assert.strictEqual(options.path, '/'); done(); - }); + }, waitTimeMS); }); it('should set custom headers', done => { @@ -97,7 +98,7 @@ describe('CollectorExporter - node with proto over http', () => { const options = args[0]; assert.strictEqual(options.headers['foo'], 'bar'); done(); - }); + }, waitTimeMS); }); it('should successfully send the spans', done => { @@ -105,7 +106,7 @@ describe('CollectorExporter - node with proto over http', () => { setTimeout(() => { const writeArgs = spyWrite.args[0]; - const ExportTraceServiceRequestProto = getExportTraceServiceRequestProto(); + const ExportTraceServiceRequestProto = getExportRequestProto(); const data = ExportTraceServiceRequestProto?.decode(writeArgs[0]); const json = data?.toJSON() as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; const span1 = @@ -118,7 +119,7 @@ describe('CollectorExporter - node with proto over http', () => { ensureExportTraceServiceRequestIsSet(json); done(); - }); + }, waitTimeMS); }); it('should log the successful message', done => { @@ -139,7 +140,7 @@ describe('CollectorExporter - node with proto over http', () => { assert.strictEqual(responseSpy.args[0][0], 0); done(); }); - }); + }, waitTimeMS); }); it('should log the error message', done => { @@ -159,30 +160,7 @@ describe('CollectorExporter - node with proto over http', () => { assert.strictEqual(responseSpy.args[0][0], 1); done(); }); - }); - }); - }); - describe('CollectorTraceExporter - node (getDefaultUrl)', () => { - it('should default to localhost', done => { - const collectorExporter = new CollectorTraceExporter({ - protocolNode: CollectorProtocolNode.HTTP_PROTO, - }); - setTimeout(() => { - assert.strictEqual( - collectorExporter['url'], - 'http://localhost:55681/v1/trace' - ); - done(); - }); - }); - - it('should keep the URL if included', done => { - const url = 'http://foo.bar.com'; - const collectorExporter = new CollectorTraceExporter({ url }); - setTimeout(() => { - assert.strictEqual(collectorExporter['url'], url); - done(); - }); + }, waitTimeMS); }); }); }); diff --git a/packages/opentelemetry-exporter-collector-proto/test/certs/ca.crt b/packages/opentelemetry-exporter-collector-proto/test/certs/ca.crt new file mode 100644 index 00000000000..455c498aa28 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/test/certs/ca.crt @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFPjCCAyYCCQDSzsM0Ou9GwDANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJD +TDELMAkGA1UECAwCUk0xGjAYBgNVBAcMEU9wZW5UZWxlbWV0cnlUZXN0MQ0wCwYD +VQQKDARSb290MQ0wCwYDVQQLDARUZXN0MQswCQYDVQQDDAJjYTAeFw0yMDA1MTUx +NTQ0MzVaFw0yMTA1MTUxNTQ0MzVaMGExCzAJBgNVBAYTAkNMMQswCQYDVQQIDAJS +TTEaMBgGA1UEBwwRT3BlblRlbGVtZXRyeVRlc3QxDTALBgNVBAoMBFJvb3QxDTAL +BgNVBAsMBFRlc3QxCzAJBgNVBAMMAmNhMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAs1AVbpZ642HATrkqW0WpzsOAne677zDftkvIhWcto3x+nwP6kSOE +vHtPR7xem9Yl5LUy1aDpd0WnBSke1JIYdJCAmmlitFVShrpolGRb9MqYJPXp5FfH +OFltziG00/MSKwNv7GiwN3ehyvzfS9L46mCcUWnQLJkjkThvlV0JRCfaTBRF3m8M +fKYvQ71G/9ZwbRvRqPCk8CZmzhqKLvRFBmzM2FGj0CY5fFqPcBRM08MWNkxAR/4B +IGKTaz5qzaFEvxHgQMQaXOQZYeNwiCFBoGygOId96x8GX9AT1PwW2ltMU3rNtVCf +9xu3JUREHjkIReNqM9h1qq5YIfrEQYeM1Q5Kyr3+Bpj6EhZqGmfc37z/nootxG3z +VmYZ4+z0zx24s117J7CfD2OLL2OaLyWheXXYqB0gOgoTwwwTsB5DYOv15fjsqs3F +kuYR/hbxs1GQO9RcOmlvynIleiVkm1x+UmOuIltfMjolBPc7ZKKxjlAxbC4oY7Za +3th3UkDIVFJmWsJhj+z87qLq0EW4m5UYV3uIUDN4P6Pko3iTqKG2qUtnnhrlbvhd +/YfSCWJRMSlgCfKFuhGkiVDEpJhza5LxNeM2EYD/PIydotyASw2Btp+VowC6yDJV +yR2cTVEGeYxQXpOI0wqJT8DrhWsdAqioLtaFxNJkdTKWAbfC8MP5wp8CAwEAATAN +BgkqhkiG9w0BAQsFAAOCAgEAP7u8IlEOTBrL3OISH9vUqFbiRdTzPfpFJ2ZVxM3H +C4iLdndKVmJLRJyMeGhD/kEnTMmHrt/mZTw6tI87+PE1ZMqSe4+q2NlHz0BouiQa +ukGj+OzZ4gw+IlDfyiXtsggCb1dRZldGoddiP8ldP0ohvR7nErG0RrRuBp860yPD +qBzItTzpC4dNVBbOBf+m9T914dsznFKlyU+QSVA2TXpJnmfEKCwlyk2gVH9olQlG +ND4cBdnOnarV5eflIj+LXjZh2wt/F0qLpTmUmxEyCc1M1il+hC6hnbarzin+8Cxu +VqjKzG7KcLxlWx9wj6ruBA1kPL0Jx31c8wDJ8b7HtsDzehcwrKKnZwA3qs3r417c +n7Dddbix9Gxxi2MTY83Q3MKbVj+oKxz0wZxa29fvlf3Gv98wzSMcS2cK+bjQwwuJ +WQxH9KksKU6g1Dv3fVz2E5CP9gwHaQBVBNSKxlqQsB2nhNglpigmglCKrfX07c7x +ryzoDE1E7tYguyWa4W+LFJ85EirUkGIBL7IoGCsol/elF6noGiuaNMO3KsWmp/C6 +YsXQJPWrnep93CCZdZ7bY6L6BTPdz1RaXMh8Rc65MlIlTzxPnhFTYrXz/FlK2uv7 +lPvT0+cGOvuiN26vqfKnrid1I2theKhKDWSdv3Rshg0ZJatNWS0u8gTE4f+qCjHP +9CI= +-----END CERTIFICATE----- diff --git a/packages/opentelemetry-exporter-collector-proto/test/certs/ca.key b/packages/opentelemetry-exporter-collector-proto/test/certs/ca.key new file mode 100644 index 00000000000..e8b01e04ea1 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/test/certs/ca.key @@ -0,0 +1,54 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,C088BF4BACFE1D5E + +TKzb0xd1SS8So+VGtAOqj7XhYJNaTSl7HrF5UXoL835lzU6qIdgJWp8REOATdYTP +wqL5x3OlRy/X9GUtXApQx4OoCy1hOMXB10/T1nD+EuxBf4ChEtRow1synEfOVlX8 +JZvRHuvN1AGnOzn8YpCnZ19ufw9ASX1cOFjefJKiR8vi32/LEO5No2jqODTWK3V2 +ijiV01hDkbiWvIoxcLQRXm+F2TAZ7MYz/DEjtbAr+4vCDMobJicWHim6yHpor/B0 +7bBVEsR0/R7kb+fLtv9cBDUqu40m7LfuMFtJDD5deRce2hSs+rm9nO01qvo5KvR5 +XA9WdKdFjk3WKjE0uAhRCzXXvRO1S9i6Ym0E3zoW6zcXItQUo30BhBgn4DALMMw/ +aLAsq0trmXqTiJCq8QDYgQOj59jwVxMuAsvinhqBI8koy92hBiXAhZd0r2+2jm/b +yqELuX+0b+FW0hSRL/BsXaTXrzW9cSpSM+EsCtoZloNecGGKNUIhVF6+LmALQ5xD +5dwIIooQTpNzLpc55rK6C01VWQLRWClJdbASdYD5hmY/0KNq/LB7F4TY9DjnJnWx +Lrkalyl8lv1oZHjPUqA8NAY+Rf+Ps6BxxP2ShAfVwybVFh0ACh5stWpAbmWId86p +vnf4gW2y5g4p9HNK/+XuFJ4PQj4/SJNRrc7HvwlCnAg1lXRYtt2C2awbKPzBU7bw +4sqOKlIOSeox6x3APcO+nTuYZf2XJ9s/jtlPqPgGBaaWB6IANiMBwi2LnVCjxaL5 +tjiBQlwcYSla7YPz7AAuRYcv2zPJVSk8pZqObBZO+1JN/BJf0LUqW4fOKSwud8gG +rDHp5YS/+MOnygvuyooqdFoFwS6/fKzdLKz5Ug0ZsIPEVdd0gQUrNReATptmRuxJ +/dA58RLpsosCz2iMkYxEJ75acmPsZU6DZCHrI/WwDR6xOVN+3YttpEoGXa16D7Hk +Pa+tmObX3aK+iAQBoSsiztxaBYRNc+QbpKl1/qU86+2m8yXnsbKDXk3WnFVMBCw2 +VbdgD7Rx72sYhzn2VPGmoRkOn/yOkhful7R/tNTK040FuBQaFWer5yDsUlWIoYgd +wnTdSdXisib4rfq/t50xfCGS67eyaH/CMbAni/x+eikDFAA3/OLMM+46hZaoZHqP +sOcbcD+JUIwo00xW2Xv2gF8NT4mcdVphRs9u1pcoyZCQm4OuE4qfJhYH2k48imCC +yfQVgr/fitMm9/oNcEkCuGI5iNm0f88dIKZSuAaxBQ9AXxRjgGVxjdasTcFwkMMo +ahgasfOXq53HoPgX7UOB9V4DdtzwwUg2cS3G0aC8Z2botQ7JlA87QvHddLPrFE3r +ybHIgxOOhabCNpO0ER0xaaS6dKhq/oEuh4owPm7fnfx6lYVmxELJoyuGvGJjlDjk +Zks4Du6Ew6KuZRbGJQOod+FAT1uCIOt83Vslp+3rURe9NmUmU6xHSOnb3La3pLco +upb7x8ufsE8y143uyiqDAyF7MluCl/Cc0rO7BPOu/QsXUcm+oE/b+WLCfDkWETHp +6UK6bW9gi3iohm1S5ViLLSQGcXF62rkP0PQMZpxemQdsKJaynjUmtY13h65L8GRh +4Btxb3/fZgsBDT8us5SP1qSNFsygJwKuRGLaGqrbx+o/deA7kSwX/UFrAemAkysE +1WuFvGlrhTUXcYmjKGbP+78IyPuhcG+lxp1QZXpdIv9Bos2m475we1gSAi2qOF02 +2op60zNo8ZsBRSI/QKtojfG+0SlCNO7owzu+j6PH+7rHpSL1DaPK9C1xwxQCsRaO +MIU+ELIWboJK3lNChQ11mnyMjoIMsfR9fP7Cmr4FuvCHYQbCFERLOzJ6FU7974+b +ul6VAsbvsutLRziQ3LN+QdQRsrrvq9YU0CgB8jLUHf137x4Goegb3cxlDjwzpGkt +R3HM1KAbxcbyziQz2NuSZK5Jfg/OO+C6o5HN2j3IfhQyM1PZ7MsO6sEaRWBxgC99 +xjXYUyDRt2Ho1mFmRtdXjmeGExz3QBQ7X66swHwMcBov6uL9x060VXfzFB6Gbn6O +2UabP4eriWuGUSk/fVBg3jqe+iMMM4z++mScmCqWUnp6lzUSzhsCyZ6a/11zsyvF +Lq8GDu+4rCFzj8/jgE3rqPHGPM7cgn8kv7IC1cOMDMWmELPZW38bxbPYPbNiNgtv +Cq0OjCCSyB307gC2VjwbXyN7AAT0mul7BhQOxU/qIqRoGKUGuQLWIp42Fe0TAe8x +Im1baX8SV35KagGLvcBlw1uwA6olzo4WyxH2SyVEfYxBqek7DmZ8LUwH7s+Xs2+M +svr++dv3drLOdz75Wj7N6KiK0KDxv5EHLiP3YD8/UqP3GzMDv+yj3lpVOcE40kEo +HWhlv7X7fZWUCV9iiRSKWzYBhps0LWjJ4ryB/5wU5X/iSTLyP9cYPKiQIFyaWDK6 +POcYrgNN62e32PScENlwy+YuL4xuaa3KnOTS4e4emjzdH576y213D+n7bpFVOvi0 +JEm8qJJ7PgrwnuGcnNjIfIJNDrLqXDYJWn0K59Pjfd0i3VRhOiNFzcIRnNePR//h +lwBlhy0+XpUvxNEt9Ju+xaaSxg16cyKlz6lz8P+4TGuw8cgXdSXcZw6w+RDdmiv/ +NkVUPEwtMh3+H6L4Lfy9h0HA0bnpnOdgbfeTbHHv5/ViJd7cAjF4Z7PTEpC8nT++ +RTqp4q1upJjb5vk2IkrvhPAO/ZjK01ijSx/sieYoSxp2+vme/4yYloD3IjoUR3SB +0DOv5ATQUNABKAOkZkkpeA0IRuPdbLqpd4FQLYi08oJbOEiVkCUzmBwxbvCAkN83 +KCey8TP/OXVg9+lsh5UgaVPNZmNWGabHIsAnp4TszQZWsxAywOvBSWAb+Z8GOCTP +8T24RYphijZALkXzssYeCZ6qOl/V6YKa7dkIrWAyVRsZKQYH73HzJr7qR0N84eXu +4yyi8rb31d/6Gl+ZyvvDMeQBOFlKtHRx01VG/jLlq2qBuv4lY+UFFDpV2l7F4rVV +IwAuU/pYcuJ97bocLvrdCZJIdszlNgGHpKcBn4MWT+lcod/iBsloXy6J6kluaXBu +q8Ub9zwiF/aKM29CcBRnIHMIVSZ5FY9/Zbu8EhnZjTe7NUNNWi9uV0Arht5S/3RS +-----END RSA PRIVATE KEY----- diff --git a/packages/opentelemetry-exporter-collector-proto/test/certs/client.crt b/packages/opentelemetry-exporter-collector-proto/test/certs/client.crt new file mode 100644 index 00000000000..9534695d808 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/test/certs/client.crt @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFPzCCAycCAQEwDQYJKoZIhvcNAQEFBQAwYTELMAkGA1UEBhMCQ0wxCzAJBgNV +BAgMAlJNMRowGAYDVQQHDBFPcGVuVGVsZW1ldHJ5VGVzdDENMAsGA1UECgwEUm9v +dDENMAsGA1UECwwEVGVzdDELMAkGA1UEAwwCY2EwHhcNMjAwNTE1MTU0NDM3WhcN +MjEwNTE1MTU0NDM3WjBqMQswCQYDVQQGEwJDTDELMAkGA1UECAwCUk0xGjAYBgNV +BAcMEU9wZW5UZWxlbWV0cnlUZXN0MQ0wCwYDVQQKDARUZXN0MQ8wDQYDVQQLDAZD +bGllbnQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAMm4t0aiZqouBsW/VilH/McgrMECz6RYMnAxAZVG0AwvlzZPMc46 +Vpbggpsn5j/N/teragpiqIwIIN+1apGXGmAg4IDgyrswq37Oj4JrvmzXWK1PGGFs +YpWISmNR1DKkEL8ts41KDEZejsItFYctnvIctRYPoYB+6No2iddj5gioHyq/yDLN +zD0c0C3r9tXm+Ed9BO4pgu6Rl6zuPf3sttE5eNa/O6qV1dD3nxnpPS3fIbXqKviD ++xhgXrfLM43X0QBQt6sPFuunpcvhWDsgtWMQ6EShQUhb0DXr6PgGXj/1Vl3nVsxP +4gnCOE5x13jzw/tqijbKin2+dpEGdi+c0QeVfDWoMZA9mlitZiLsenKdB8sYaoCw +QZHu3zzfXruMqA6x6DyLPa6PEFzw4v5PAvsd4Re0cLTBDsw1Fdx/eGzBg7k1KCFZ +HA3RdzNqCMvxcumH7hUg1n0cEtHX/bVSdpndK7iWVPbDYv98bFNOq8fZzsoqZgOk +Jl4TJyil/oPDkzowc8F8+p4vWdgHevjkqk5rtyMLBb6KnUmJgYPef7FuZ97oSi+r +TrAUs595+RZefDRdu5MGV/2NMbpN992Yewg7LTiP+gwNuYBDQmEYyQf0sxMNcwXc +ZVrWw+RdI8udSFowmOd/g0NNz3CaAXX8n6BLMJBBxRx0zet/88VFtLNrAgMBAAEw +DQYJKoZIhvcNAQEFBQADggIBADfQTBf/n+r+E6/GH3kyiI4jg0vIlkOlABsypKvY +iPXGTrtTlFB4s18/f0I416ez1U129OYyE2mUHKDKAUHu/Qf3Cl5N983DCx7czVJZ +Maxafe7DS5rAwF1wpfxR6u4Ti0gK0HO29bsCDah5C5+s4Vzv5t6AFmyg+ESQG6cM +vbkIs5nbcU1ydMdfvSb3vmjvPLh41lWnRVkkbjgzTS312EnHmqV3wIx12UAb16J4 +zXOjI+7JU9TZRnTEf3xOyByA5h8pCYha3nOlETR+vRN1byUYesCWsgj0wFU1u6K6 +AqSMU4sqtNIIlwN50CPLvYjB3FBPh8DpB5iQ4GxM636X06dQqQF7n4cWMOMHRlT1 +DgafEpVdxSeJMzuBQHJzF0UbyaAwKkDKGuAZWfihlNEUMdVm4EvKpE82cevM/2Mo +VEuPlcmf+D0ERu6bK5RAjXkH+cxYWXJGRtx823IEEgXOk0F4AMCaMiuNHI7buBi7 +AnBvIUv67b6FRS6Hw8sMDNvVTpavsnUKwSJJUATPU+rRIgD3Dl7SJ9XqmFdgPO+E +eRxvCCZvzEL77SLslv6CkKLseNQQ7MrOgTotYOrHA/AwF1GtFSDoYTRifKGynRPO +Vg3CscBOkIz9Plmy6dq8CEIygdmcN2Bb8BwA97q1epU4vzmx7fhqLLyMq+YztPRp +6SLz +-----END CERTIFICATE----- diff --git a/packages/opentelemetry-exporter-collector-proto/test/certs/client.csr b/packages/opentelemetry-exporter-collector-proto/test/certs/client.csr new file mode 100644 index 00000000000..2c7d0f9c04b --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/test/certs/client.csr @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIErzCCApcCAQAwajELMAkGA1UEBhMCQ0wxCzAJBgNVBAgMAlJNMRowGAYDVQQH +DBFPcGVuVGVsZW1ldHJ5VGVzdDENMAsGA1UECgwEVGVzdDEPMA0GA1UECwwGQ2xp +ZW50MRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDJuLdGomaqLgbFv1YpR/zHIKzBAs+kWDJwMQGVRtAML5c2TzHOOlaW +4IKbJ+Y/zf7Xq2oKYqiMCCDftWqRlxpgIOCA4Mq7MKt+zo+Ca75s11itTxhhbGKV +iEpjUdQypBC/LbONSgxGXo7CLRWHLZ7yHLUWD6GAfujaNonXY+YIqB8qv8gyzcw9 +HNAt6/bV5vhHfQTuKYLukZes7j397LbROXjWvzuqldXQ958Z6T0t3yG16ir4g/sY +YF63yzON19EAULerDxbrp6XL4Vg7ILVjEOhEoUFIW9A16+j4Bl4/9VZd51bMT+IJ +wjhOcdd488P7aoo2yop9vnaRBnYvnNEHlXw1qDGQPZpYrWYi7HpynQfLGGqAsEGR +7t883167jKgOseg8iz2ujxBc8OL+TwL7HeEXtHC0wQ7MNRXcf3hswYO5NSghWRwN +0XczagjL8XLph+4VINZ9HBLR1/21UnaZ3Su4llT2w2L/fGxTTqvH2c7KKmYDpCZe +Eycopf6Dw5M6MHPBfPqeL1nYB3r45KpOa7cjCwW+ip1JiYGD3n+xbmfe6Eovq06w +FLOfefkWXnw0XbuTBlf9jTG6TffdmHsIOy04j/oMDbmAQ0JhGMkH9LMTDXMF3GVa +1sPkXSPLnUhaMJjnf4NDTc9wmgF1/J+gSzCQQcUcdM3rf/PFRbSzawIDAQABoAAw +DQYJKoZIhvcNAQELBQADggIBAFjedQr52vLv7YxeLxIvyHrMhbx7Iz4ztj3NlnOJ +EMGm7pcum/rGol1z8m7Y3mFbfJJp8IY/jn1w92x+M9pc6zsRo9MsKdqEAKhAjwVh +jYNBWHekrcwGIy6YUSFvZeUZ82IxFcf6N70CH4sLUJLbZXcd5Nui8mZJCPC4SLoC +E51P0vUClnS/l4O+Dz/IfBy9cSvGg3YvF8GGmW7IZdTD4bWg9O8lQi0zcnDGR0Er +N1Tegoe38Mrx49IHpWMEQzJhI6R22CQ0wtk6e8oBuz2No8hnY0yrAvBGI9v8GUE3 +FJAQxHzyUXCA50IcHFruevsgEzixmYb8OfDd1LC3nZJHfq2r5j0jOU6XXxukH8R3 +UyGIf8UpJQqBKHe0Ld0tOWSyByiWHvw4/Nir/DhANezIEsq4A0Y9hq6y2GTtFUnx +HdsYqTmVlrghBiqZF2H9f7YWaRBnsbu6Kkpyc55r8pBZMT2Myu2Gjq/8GAWtEy1J +BYmQfIZUnYksFaZiXvSiyfNaX5M7nvddxkBCyhtwtCzVutL+ZoqwXD2PPaUCuBbu +lu4M7iSjKiibiCqQEVyRPn2o8V4R5r0NmqS+B9CYJECeAnLPO49Z3l4wdJUEww9i +U14lM75e2tfFzaa/ZqOCQFuu84NKacTJUALpdg1aHcPtTG51F2U8EwsoZEBxUBb+ +WR7X +-----END CERTIFICATE REQUEST----- diff --git a/packages/opentelemetry-exporter-collector-proto/test/certs/client.key b/packages/opentelemetry-exporter-collector-proto/test/certs/client.key new file mode 100644 index 00000000000..e0fea66664c --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/test/certs/client.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEAybi3RqJmqi4Gxb9WKUf8xyCswQLPpFgycDEBlUbQDC+XNk8x +zjpWluCCmyfmP83+16tqCmKojAgg37VqkZcaYCDggODKuzCrfs6Pgmu+bNdYrU8Y +YWxilYhKY1HUMqQQvy2zjUoMRl6Owi0Vhy2e8hy1Fg+hgH7o2jaJ12PmCKgfKr/I +Ms3MPRzQLev21eb4R30E7imC7pGXrO49/ey20Tl41r87qpXV0PefGek9Ld8hteoq ++IP7GGBet8szjdfRAFC3qw8W66ely+FYOyC1YxDoRKFBSFvQNevo+AZeP/VWXedW +zE/iCcI4TnHXePPD+2qKNsqKfb52kQZ2L5zRB5V8NagxkD2aWK1mIux6cp0Hyxhq +gLBBke7fPN9eu4yoDrHoPIs9ro8QXPDi/k8C+x3hF7RwtMEOzDUV3H94bMGDuTUo +IVkcDdF3M2oIy/Fy6YfuFSDWfRwS0df9tVJ2md0ruJZU9sNi/3xsU06rx9nOyipm +A6QmXhMnKKX+g8OTOjBzwXz6ni9Z2Ad6+OSqTmu3IwsFvoqdSYmBg95/sW5n3uhK +L6tOsBSzn3n5Fl58NF27kwZX/Y0xuk333Zh7CDstOI/6DA25gENCYRjJB/SzEw1z +BdxlWtbD5F0jy51IWjCY53+DQ03PcJoBdfyfoEswkEHFHHTN63/zxUW0s2sCAwEA +AQKCAgEAjZvNlZl2RuuOt41teAdgLY4DmG9XwwBjUB0nBlsyvAtAtNB9n0+W783m +AfPNkGcVCuP7yhSeS8d9BG6/xDr2Oht6Xx7vUt+E1L0/Q4hNouy+BNQswl+rCVwn +FHgiZfaFByCXFo2v9kp1H1006rOdDEwY18bbUnBFGMMGmx03JEaZspH1gay1PwWW +I1at7lV5X/4k0uhzUPUGLFEHVdWyNUiKSv7ubP9InaznlPIGj8g/Swx7ZACK6f7l +H1NX+rBRuU3w0fYC2iXTnz+vh7qbe1MoKt2lDZ3emavl3Q/jZDTfj4ZSiZVekgk1 +K+SBJhjCMSIGqxYeiM2HQKHvn9cPaWtEH+B3zPSauURngPxhayLsVywrqAIqh2gI +iQXnqajwn/g6KF+eEYfdJyPUv0DZgS9e8I8jeGf6Dax4SYWEtl835+r7FsejXLXZ +ehYhIdjyG16+NpLcc5d7/xaSbu9cB7I64raQCnmVbSo/iixd3TwVgFsufRqSgL++ +xa33Y0n4Tq3HgIFg2vlX+6T0RGtWRw73gmk4SXc55wG2v5a2emhQEijfoLPHEQZw +6Xd7qHHJtzxAP+Ifp3IlQ6vW0S27SIiLmQoSZBd3So5r0iF5ufIWe6215EmCdQdt +y6t000Lc8wk/0p50nlaF3Gq4dVUwkXfse/Spb+cbu4t2hSGuC4kCggEBAOuZc3MP +8OZ7vuiCgkRsE+9vfouOxmUbeP0pQzDhG/havRG6J6PG5zltmZFqJh/JvFibnRhD +UZebL9+ugYbVqSPaijuW4MpP1RSZJprxKcwiXkvIXOmB4rDbrBT8OinN7KOXDG9D +6HpeLcRG38ayMfCPMCrNjHW1J/qwJHxycuLme76d7fevxGhojJE6tICasE9SVoF7 +lc+GK/tQKbjztF1QJHXgELSDRP+uHZx7G231HiOqomMIdI0F4fXJHWk2sYBJ33zn +1/c0hPhMks1eXQiod5jXfDtwoaaArkV7S7uahDpJmi2I0HNesWoMrUKeGEEJf9mR +qHSyHozsqqmyPwUCggEBANswSrFUc1oJfA39VTFwLW54VMhb7JuKM+2h6lrZTenK +m1IwZ3sNBub6mjDtPVBG/pvIYwAAfx1liOZgyKyDj0ticWF1sAfFnWKKN7OJTW7v +45Y8oFg10CHNKOWaJd0eAEhoFHW1kPMqrM6d6uYHf60ayQTkyloKkEakBiq7YkhK +ilExk1jyqiJFU/WFEvb6kL5yg1bn1NswaOebpvXSI0z8IzUoVfRXjXB0okOrgiEI +Cn3jOO2b1hF9PHVCYbiIJnoNIhP+DdEoTpCyQy8FwWXGvtgEdwfGm8PH0iH17ehY +D8ODb3NV3HyLzoORLnqHN6G7XF2N3Y2yL2jnLBpJU68CggEBAMp514lkgtFiOiDS +wKeTBtL4zBWeP4z3PlS8GH2yiPo46VKJ3LVZJLDrK1aYlmktVAwGuMz4Ve/oNA2V +iMXbbABfOfuaYFgeoe6Q7GeuqRBB3S5d5NPdh3gdYleqqUXyLtQs5UfeYbaAp+6O +RpUZ4edu96NhgbxLUy+UH9c/+NJd6K1aRwBd83sTlvLdM/Fuf+W7ypJ/JrHyCmxy +aVkFQNYNITiYt2Kbijn+Zn5sIpeuWBeo9uQLiTcFfjtge0FH+uZZFpPfIHDYlwpZ +rLSIy4W8WwRk9OSUmKhi4OLf4qc5VThOtw05DoSINgsBGAovmoKSamkOUGryBWVx +o/4xLQ0CggEAabWtoD5hb3/5g2m1R6WZU5jXEtY6k30gtC+Nrgj1aZacOBQ+I/tR +Y95itMwF8Qx8SLdo/5w9sfjBAJKW1ZSRbELq+Zzfq6/jyp1sZbsHTESHl3JfxosV +eOfQHIOuVSjd7A2+KFLLuGrRcsh4fD4Llnm/jwukh65mjJsYmk1LBiBk+umU7aYC +5YpYBqYKUnDfk+n4a9ZdMuTzAxhvekjBW6SSelWctr3u6dhmVYqGtNWC8dm/H+Ez +abXjjY3ZQTzwiZaB4/B3y3LMCT7f5fK5phMnAVmN6oMfplldf6Fy/sZRu/JMsuwq +7SokDBHdv5ws+WQ6FKiRvH++G7K582d/4wKCAQBb6GKm0GXD0Cj0S7jGCUtOzSKx +k35cWe3YUByFQ5cN5O1kRr4xBgQin7X0Xn2WY1xCMRocslpScfVgE2WJcbVaoiqI +V7dq4N1ZhkL9dWy25Q4vmnHZU6NEZMrIC6Upd9X7uhamLJWMEqUeitI43CtjB+hF +bnD66o3ne+5QjENKOcRtssv92gUnbAtRzuy9clq5aTk37cV9e1iHTPvnILeX6hzK +szMF6wpmfbn0uzwD6HMKdGFoocc3h/0iXtk1zFTIQt7BB/aCA0VYKToCb5flgFb2 +BoswTm+ui/s2fQYlMb864gIceJBOI4+zgNeKMSrKLfp42QD3DhMtWbfpvygY +-----END RSA PRIVATE KEY----- diff --git a/packages/opentelemetry-exporter-collector-proto/test/certs/regenerate.sh b/packages/opentelemetry-exporter-collector-proto/test/certs/regenerate.sh new file mode 100755 index 00000000000..bb6ec4a9b52 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/test/certs/regenerate.sh @@ -0,0 +1,19 @@ +#! /bin/sh +# +# Usage: regenerate.sh +# +# regenerate.sh regenerates certificates that are used to test gRPC with TLS +# Make sure you run it in test/certs directory. +# It also serves as a documentation on how existing certificates were generated. + +rm ca.crt ca.key client.crt client.csr client.key server.crt server.csr server.key +openssl genrsa -passout pass:1111 -des3 -out ca.key 4096 +openssl req -passin pass:1111 -new -x509 -days 365 -key ca.key -out ca.crt -subj "/C=CL/ST=RM/L=OpenTelemetryTest/O=Root/OU=Test/CN=ca" +openssl genrsa -passout pass:1111 -des3 -out server.key 4096 +openssl req -passin pass:1111 -new -key server.key -out server.csr -subj "/C=CL/ST=RM/L=OpenTelemetryTest/O=Test/OU=Server/CN=localhost" +openssl x509 -req -passin pass:1111 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt +openssl rsa -passin pass:1111 -in server.key -out server.key +openssl genrsa -passout pass:1111 -des3 -out client.key 4096 +openssl req -passin pass:1111 -new -key client.key -out client.csr -subj "/C=CL/ST=RM/L=OpenTelemetryTest/O=Test/OU=Client/CN=localhost" +openssl x509 -passin pass:1111 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt +openssl rsa -passin pass:1111 -in client.key -out client.key diff --git a/packages/opentelemetry-exporter-collector-proto/test/certs/server.crt b/packages/opentelemetry-exporter-collector-proto/test/certs/server.crt new file mode 100644 index 00000000000..62f91722a93 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/test/certs/server.crt @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFPzCCAycCAQEwDQYJKoZIhvcNAQEFBQAwYTELMAkGA1UEBhMCQ0wxCzAJBgNV +BAgMAlJNMRowGAYDVQQHDBFPcGVuVGVsZW1ldHJ5VGVzdDENMAsGA1UECgwEUm9v +dDENMAsGA1UECwwEVGVzdDELMAkGA1UEAwwCY2EwHhcNMjAwNTE1MTU0NDM2WhcN +MjEwNTE1MTU0NDM2WjBqMQswCQYDVQQGEwJDTDELMAkGA1UECAwCUk0xGjAYBgNV +BAcMEU9wZW5UZWxlbWV0cnlUZXN0MQ0wCwYDVQQKDARUZXN0MQ8wDQYDVQQLDAZT +ZXJ2ZXIxEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBANQwHRfyj/d8Hh0qgDlxdtSxloRs8ZvBIwt6Accd1hUqs8dC0c9V +5XXOcfmusb3Fo8NKXn6IIPCEy1spFCe4EBW4obSgkJEVdPwsMsXUPLek/6K5S6uE +FhnGLUJJ57gAjh9LGdMTDp5szLO7dTYrHzdGZYhmTAyiA9JDN6iYlpWkK4p2IBcN +diu26KWp9+sJKw8Ly/7o5QD4wyc6hGok0v0nwimXZo78EJYBu6BDGuLyAgvq8zLV +sgXi4aYROsmVrg2IJbe8+PtPBNwkoAuR4QC3hRTV3bXyZdbIC0KbOekegAHTeXYz +Ap0HVkCsb/vOLiGuju/mKZFZKp5/PKf8Jdv/zDTIm8TwBvvtQKT4qmAYUkKTXRrO +OWK1pCakVLV7FGREDi+/bxhcQJt5yopLGT5NSoUF3RR+17KZ/5lSPEh5OMSprVyR +789KvY1z79JWt3zB6fIfQ936PyNh++SKxFmlnLuGK5wf58jefwSjGEkY2YAE66Y6 +8Kqg3/W8JsjTFBntBtD3xY1t0c4Hh2f3epQPrzwHx9pywgh+H2TIwnnUyEPLqdYp +SEsbnvdbLB8FZm2fwPZ1MZOZOGrKcnCMkMPE1DOIkxeFDx8xbeHRepSRJSbemY1l +tt+afAnM18mJf36gO8NnM56Me//FSTWbWaQlmUBAwSDlHxYfD9TgCjbBAgMBAAEw +DQYJKoZIhvcNAQEFBQADggIBAEt57zbZpIaQiw0BvZenLWhWvBA0j1cFk7eVG+Nl +Zo7+UniFH+1Io/gXJaJmJZ09d3ku4ZB+V44ka1N9J7qnnqXYOxRGT2H6owaWeOLl +FQ8tR1NQQA7p2uNWJclBsuPghzRCSFZw2auu8OKRtM/0VgbskNIN+H0EVhEeYjtd +ZzojPoa7AmH7P4SC1KMvY6qNmab9F8TBD19DPfoA/EpYboMQiK7DwPPuvrAdHcJB +KPLxyzabqFEqouwStqKUmKqbASOR+qJNac/RQTbN6yP4Lu9wTUm1OYaR4ot87dOR +ZhCznzlaJ2DsvFuoOKN/7Bezq+rXhIyCrH9VH0PjWwbO9FIfeZlHgmAmJnJCXb6F +bW6m+ha/63kiPU1NlTJRPukcR0vW/P0XSOcRvvje/07uJOOG5ypnQf6k7neR5e81 +1ZHPKCHba7bh08vKW5LbXwU4Ng7vRc42h6+iN0mogjj+B2oYt432L3howc8np2vF +eLCRxq/9pRut2QkfivT/GHkV/J+RxoEFDrZrTd15q1mLQnPCJOT+QmAMPfZydyZM +FsQUd6kzEWgZ4dHKqEikC0IBG+2xrrvHgKiB5Y1o0K/hEFfQOFCct6c9thXqMYhA +w/2HXXjfWLVBbGjJ4VemU1YFKyMZ+mxM1sJmPc/KkG/NjKf9wFFwFRpT3OIlF+BK +u8P4 +-----END CERTIFICATE----- diff --git a/packages/opentelemetry-exporter-collector-proto/test/certs/server.csr b/packages/opentelemetry-exporter-collector-proto/test/certs/server.csr new file mode 100644 index 00000000000..967316e1713 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/test/certs/server.csr @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIErzCCApcCAQAwajELMAkGA1UEBhMCQ0wxCzAJBgNVBAgMAlJNMRowGAYDVQQH +DBFPcGVuVGVsZW1ldHJ5VGVzdDENMAsGA1UECgwEVGVzdDEPMA0GA1UECwwGU2Vy +dmVyMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDUMB0X8o/3fB4dKoA5cXbUsZaEbPGbwSMLegHHHdYVKrPHQtHPVeV1 +znH5rrG9xaPDSl5+iCDwhMtbKRQnuBAVuKG0oJCRFXT8LDLF1Dy3pP+iuUurhBYZ +xi1CSee4AI4fSxnTEw6ebMyzu3U2Kx83RmWIZkwMogPSQzeomJaVpCuKdiAXDXYr +tuilqffrCSsPC8v+6OUA+MMnOoRqJNL9J8Ipl2aO/BCWAbugQxri8gIL6vMy1bIF +4uGmETrJla4NiCW3vPj7TwTcJKALkeEAt4UU1d218mXWyAtCmznpHoAB03l2MwKd +B1ZArG/7zi4hro7v5imRWSqefzyn/CXb/8w0yJvE8Ab77UCk+KpgGFJCk10azjli +taQmpFS1exRkRA4vv28YXECbecqKSxk+TUqFBd0Ufteymf+ZUjxIeTjEqa1cke/P +Sr2Nc+/SVrd8wenyH0Pd+j8jYfvkisRZpZy7hiucH+fI3n8EoxhJGNmABOumOvCq +oN/1vCbI0xQZ7QbQ98WNbdHOB4dn93qUD688B8facsIIfh9kyMJ51MhDy6nWKUhL +G573WywfBWZtn8D2dTGTmThqynJwjJDDxNQziJMXhQ8fMW3h0XqUkSUm3pmNZbbf +mnwJzNfJiX9+oDvDZzOejHv/xUk1m1mkJZlAQMEg5R8WHw/U4Ao2wQIDAQABoAAw +DQYJKoZIhvcNAQELBQADggIBAIBAt/12a6kkCFaRe256Umrj3/2DPA+gVqaVwlsi +xEGuO3GpBv7D6+lrlwNhLLSFOEkqoB4t/hjfGyabENXrCgyjMEoq/YKfwJvO4FPv +UkjaEWsCxmuwTS0qm8gXQy9PAwSI8EF2jOoRtvpCXl7bDQRJRIgKwZFI+jCEZvgj +Sk8fZGOH9yPEjx0KpvEw3jl/kbdSJu+CFTr981yLKjeG0lMknc/sQwH87tco4icj +t2Deaow6UOc0VaTmsWMLwIWrG/5TQPj+tL/600mBs5iQCOVio+hbzOHmDb48Ztao +CD4z8w8PAHxO79Vx0Wjt26cl6pKL58uke3G41Aq8//YLpSUUvIx0bYOwobDd4Ev5 +Emklvmcf3hAAzVQ7g8kDD82RDPRKtDl6e26+q2MQT31HuGbKB+5xpi113dSoB2CO +NSAgn3heoj5OM7heKwh6p6j0r1gT8WjXDMXQdKgekTGaUxeOSmccvMk4U0LN3JpK +JqaH178OucI9aRxGVjQFErW7xbKOViHP+NxNKj1pnerd7PX0wF/g107v2eSb6l/5 +K0UsM/l7MsINkx/1p+Qqu26t3i3Azw/MxKJqOVAlcb2LrACBj80BXBcJLW/My3kY +0XzK1siVSL17lL4KYBLO7kVR3F1+m+aQPrYJsLEKCAGxsfiFRBhXa6pfvp+fd5Hs +/xFM +-----END CERTIFICATE REQUEST----- diff --git a/packages/opentelemetry-exporter-collector-proto/test/certs/server.key b/packages/opentelemetry-exporter-collector-proto/test/certs/server.key new file mode 100644 index 00000000000..4831771d2b3 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/test/certs/server.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKgIBAAKCAgEA1DAdF/KP93weHSqAOXF21LGWhGzxm8EjC3oBxx3WFSqzx0LR +z1Xldc5x+a6xvcWjw0pefogg8ITLWykUJ7gQFbihtKCQkRV0/CwyxdQ8t6T/orlL +q4QWGcYtQknnuACOH0sZ0xMOnmzMs7t1NisfN0ZliGZMDKID0kM3qJiWlaQrinYg +Fw12K7bopan36wkrDwvL/ujlAPjDJzqEaiTS/SfCKZdmjvwQlgG7oEMa4vICC+rz +MtWyBeLhphE6yZWuDYglt7z4+08E3CSgC5HhALeFFNXdtfJl1sgLQps56R6AAdN5 +djMCnQdWQKxv+84uIa6O7+YpkVkqnn88p/wl2//MNMibxPAG++1ApPiqYBhSQpNd +Gs45YrWkJqRUtXsUZEQOL79vGFxAm3nKiksZPk1KhQXdFH7Xspn/mVI8SHk4xKmt +XJHvz0q9jXPv0la3fMHp8h9D3fo/I2H75IrEWaWcu4YrnB/nyN5/BKMYSRjZgATr +pjrwqqDf9bwmyNMUGe0G0PfFjW3RzgeHZ/d6lA+vPAfH2nLCCH4fZMjCedTIQ8up +1ilISxue91ssHwVmbZ/A9nUxk5k4aspycIyQw8TUM4iTF4UPHzFt4dF6lJElJt6Z +jWW235p8CczXyYl/fqA7w2cznox7/8VJNZtZpCWZQEDBIOUfFh8P1OAKNsECAwEA +AQKCAgBaxLY9X0sMwHCVY2/0osAFnm5X+c6lJUqbhzapee7xoRHExKXB/umoqoaB +G6T3HEvAp9iiYhNNMFFZjsoLb6aZ1CCAh0swdTBVC4cwr2jF2nRspL1lApz9q5QC +zmCsirhBVLwYWgef58TtgdxTLsEswRV/8trHcKsX0B9IJPYNz2u80GlL0ztg2d7N +t1bRmVttFUvPoMsNzlyVNGgei+Ah4VciuZxqwBNMSDN+DBa9TG9pr7kXXujHsdV7 +V9WBFGGfckVIQzNzNctLbPN135KT3u20CwTL54R/C5YdiQ+N1LlHjrJfyNRuXgwc +oGdLHVkImYaVwyy2+6DKqn1FEw0SNrHQxbYHqHZf22F4tQYw8jE1Me1o89cG6n8t +RDZxm/7JcHg1Pq2WZMO61Xn+m2kTt6dVrPfl4n70CSZxaelV5UesBqbrZOHOiE4d +WQRGfhw7Sg+YFrNvevN/8p9Z99ubbRNflRgz5juZstk1j6ZESEO9fs1omgXGOeoN +BzAYp1odSAeeMlkfIaNo2QpLcBMnc6nQSYNld2QIg4k+1VhQUbkxRLGh4C3gs35I +ujRLRujCOye9ybv2MiDTqahK/mKCmldLWmXInUdMGTdMdUlYpBvtq1G8RBQHCwBl +2F3BTlITzKcVz3nvUiqqZzjm3eR4WEdTPMX4jr2iDR/kh61nfQKCAQEA9rgYScAp +KS3C8Fa6WX8vRPFTMOJGpo1GET38K7iRVO4SxWQqWzoH16ZE2bN01lyzjvfqPoRR +eOBdpyaJU6onjE+XLK9qoNgrW7HaInuNF4zWTndo4UwTXnE9l2qm3rMgjngXla6l +PuC6QVsPu2eGhmyWMtVKAmlMFYT2p7P+cSEwNZnCVmeMdviqO8aGMOuHNBEJ408O +oI41+rvffjogvNPnvDN1DQntl134CLxa+jlpAcr9KgVfMZpOqR+wvcV4JZSkPflp +HRFWlcOk2dWnqrIAkNcmVs+P6tB/d7sdj8hGHw0xJ9o+UYBmdJnj9N49dc9TggJo +asVIQ2CFKQVPgwKCAQEA3Ct7yVXwZwgxHBg4ouLaCXZ4/oouBjuwEtx+SPujs79S +IbM8v03YuxR3SWEqnB+P6g/Sx3EijYhz95nbzhN1gR482n+aHgtrMKGF8V4ROwOq +F3xXhx15qfn53G9SQvo1jOBsKQgxCH+MDrfa2rUGaesMVSIw3rMImiCqT329mDEX +oMpCfPUNPTXNIBJnMRcFkENK9XBN2tO7puvgi57EzseUP0jhnBYIZigjuYDDnys3 +xax5r7+o7ialJvUuuvlrHiYc+km8Qg9lDWloayZPOTGY1lEAwqdAyuVhXKF92hJe +o9Y4aD33FLaKrbHm/zfj4+L8Yuh+c0NXuhTkiLIpawKCAQEAhSgo260dyf7LxoFY +hDMTpQcGWkzVytBWr7mfn003CvqPIQAFqETytJ4lbMXhWkygEJqXT3SEsFOP2EYB +OimMvLq8Ib7vMq5ZAF1GGPRL2xkFFUZ3UZmInqFJl65VL77H5HzGZd/jicMqY1mt +bPzb6zMyAW+CSTjhen/PzAVmX1KFPXimHZI3ioJ9BlQIWuDTkPNdPdSOVXNLiO7b +GbpvrtpDqRywoP/pvpdV5gkapRBVL0WKS6KolRHuQHM9Jb8tMENAPb6dz7Vq4Nu9 +3l/k5Ui663FjXNkbmKU9FrbjppV12w54qESu+7fsFCR2ltNXonzqWjHIf0/Ix6yR +Uelu1wKCAQEAxUq17zHybfFaSImv3s6XgZlHTRi3q2A7JHuvMmlERWMxDv/VdLwm +dWYeioPmseZaiOzK/Wt1Agz/liWqYRzw09Yrw8RKb5fd4sMrCqI3oIFlHwyORoZ0 +KovVieG7fkdGS0ojwhUUE0BwWhQIqqlC6RD2iSdNUZJvJ+YTl43eoo2DVdNJBz50 +MaCPgqjbDZNKqf6TIiMTsP7BDhAatCJ+y6juQFNnz/2yYxCfCrDHG0+X96vZk1KU +52t73NAiouu0QFz45JPEfhHbhMwrBLFclqzJ/2qw2r0Tg31O5LnV099YLUpeW5MD +YO0+ke10SMlljiUt8tfR0CnNZ/Mm4xN7pwKCAQEAplZEytHOTmb5eaFYc8uiTAp+ +p1qCriIlw5T5akw1ESSKbEQTXmKqqwHP9pvFtg9Vd1M2ccmZT4Lk4+AL4sgHcs6p +asX3xz4/A9mqJKLruFd4lGhY14HV9JA1n0xVFnV5KK/7y+9Y1ZLvcGv/jzd/EXcL +T2OZ8wCTRdT6oi4+HsWaitHfNiJ1zvBgwWY9wEHofdPHIJwp8gNh6RD+M2WjHl4v +0GCGQaoEaIAePCn0R8WISviLhAymu9sIIov/WMBQQbsc03JlKSRsd/s5FYObUBfX +iBzCgMvuGWuFeBTB7LYgzina0IFwJqxy6Z7ySgZJKigPkhhrG/iD/QxuT2MvxQ== +-----END RSA PRIVATE KEY----- diff --git a/packages/opentelemetry-exporter-collector-proto/test/helper.ts b/packages/opentelemetry-exporter-collector-proto/test/helper.ts new file mode 100644 index 00000000000..dd66fea9942 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/test/helper.ts @@ -0,0 +1,496 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TraceFlags, ValueType } from '@opentelemetry/api'; +import { ReadableSpan } from '@opentelemetry/tracing'; +import { Resource } from '@opentelemetry/resources'; +import { collectorTypes } from '@opentelemetry/exporter-collector'; +import * as assert from 'assert'; +import { + MetricRecord, + MetricKind, + SumAggregator, + MinMaxLastSumCountAggregator, + HistogramAggregator, +} from '@opentelemetry/metrics'; + +export function mockCounter(): MetricRecord { + return { + descriptor: { + name: 'test-counter', + description: 'sample counter description', + unit: '1', + metricKind: MetricKind.COUNTER, + valueType: ValueType.INT, + }, + labels: {}, + aggregator: new SumAggregator(), + resource: new Resource({ + service: 'ui', + version: 1, + cost: 112.12, + }), + instrumentationLibrary: { name: 'default', version: '0.0.1' }, + }; +} + +export function mockDoubleCounter(): MetricRecord { + return { + descriptor: { + name: 'test-counter', + description: 'sample counter description', + unit: '1', + metricKind: MetricKind.COUNTER, + valueType: ValueType.DOUBLE, + }, + labels: {}, + aggregator: new SumAggregator(), + resource: new Resource({ + service: 'ui', + version: 1, + cost: 112.12, + }), + instrumentationLibrary: { name: 'default', version: '0.0.1' }, + }; +} + +export function mockObserver(): MetricRecord { + return { + descriptor: { + name: 'test-observer', + description: 'sample observer description', + unit: '2', + metricKind: MetricKind.VALUE_OBSERVER, + valueType: ValueType.DOUBLE, + }, + labels: {}, + aggregator: new MinMaxLastSumCountAggregator(), + resource: new Resource({ + service: 'ui', + version: 1, + cost: 112.12, + }), + instrumentationLibrary: { name: 'default', version: '0.0.1' }, + }; +} + +export function mockValueRecorder(): MetricRecord { + return { + descriptor: { + name: 'test-recorder', + description: 'sample recorder description', + unit: '3', + metricKind: MetricKind.VALUE_RECORDER, + valueType: ValueType.INT, + }, + labels: {}, + aggregator: new MinMaxLastSumCountAggregator(), + resource: new Resource({ + service: 'ui', + version: 1, + cost: 112.12, + }), + instrumentationLibrary: { name: 'default', version: '0.0.1' }, + }; +} + +export function mockHistogram(): MetricRecord { + return { + descriptor: { + name: 'test-hist', + description: 'sample observer description', + unit: '2', + metricKind: MetricKind.VALUE_OBSERVER, + valueType: ValueType.DOUBLE, + }, + labels: {}, + aggregator: new HistogramAggregator([10, 20]), + resource: new Resource({ + service: 'ui', + version: 1, + cost: 112.12, + }), + instrumentationLibrary: { name: 'default', version: '0.0.1' }, + }; +} + +const traceIdBase64 = 'HxAI3I4nDoXECg18OTmyeA=='; +const spanIdBase64 = 'XhByYfZPpT4='; +const parentIdBase64 = 'eKiRUJiGQ4g='; + +export const mockedReadableSpan: ReadableSpan = { + name: 'documentFetch', + kind: 0, + spanContext: { + traceId: '1f1008dc8e270e85c40a0d7c3939b278', + spanId: '5e107261f64fa53e', + traceFlags: TraceFlags.SAMPLED, + }, + parentSpanId: '78a8915098864388', + startTime: [1574120165, 429803070], + endTime: [1574120165, 438688070], + ended: true, + status: { code: 0 }, + attributes: { component: 'document-load' }, + links: [ + { + context: { + traceId: '1f1008dc8e270e85c40a0d7c3939b278', + spanId: '78a8915098864388', + }, + attributes: { component: 'document-load' }, + }, + ], + events: [ + { name: 'fetchStart', time: [1574120165, 429803070] }, + { + name: 'domainLookupStart', + time: [1574120165, 429803070], + }, + { name: 'domainLookupEnd', time: [1574120165, 429803070] }, + { + name: 'connectStart', + time: [1574120165, 429803070], + }, + { name: 'connectEnd', time: [1574120165, 429803070] }, + { + name: 'requestStart', + time: [1574120165, 435513070], + }, + { name: 'responseStart', time: [1574120165, 436923070] }, + { + name: 'responseEnd', + time: [1574120165, 438688070], + }, + ], + duration: [0, 8885000], + resource: new Resource({ + service: 'ui', + version: 1, + cost: 112.12, + }), + instrumentationLibrary: { name: 'default', version: '0.0.1' }, +}; + +export function ensureProtoEventsAreCorrect( + events: collectorTypes.opentelemetryProto.trace.v1.Span.Event[] +) { + assert.deepStrictEqual( + events, + [ + { + timeUnixNano: '1574120165429803008', + name: 'fetchStart', + droppedAttributesCount: 0, + }, + { + timeUnixNano: '1574120165429803008', + name: 'domainLookupStart', + droppedAttributesCount: 0, + }, + { + timeUnixNano: '1574120165429803008', + name: 'domainLookupEnd', + droppedAttributesCount: 0, + }, + { + timeUnixNano: '1574120165429803008', + name: 'connectStart', + droppedAttributesCount: 0, + }, + { + timeUnixNano: '1574120165429803008', + name: 'connectEnd', + droppedAttributesCount: 0, + }, + { + timeUnixNano: '1574120165435513088', + name: 'requestStart', + droppedAttributesCount: 0, + }, + { + timeUnixNano: '1574120165436923136', + name: 'responseStart', + droppedAttributesCount: 0, + }, + { + timeUnixNano: '1574120165438688000', + name: 'responseEnd', + droppedAttributesCount: 0, + }, + ], + 'events are incorrect' + ); +} + +export function ensureProtoAttributesAreCorrect( + attributes: collectorTypes.opentelemetryProto.common.v1.KeyValue[] +) { + assert.deepStrictEqual( + attributes, + [ + { + key: 'component', + value: { + stringValue: 'document-load', + }, + }, + ], + 'attributes are incorrect' + ); +} + +export function ensureProtoLinksAreCorrect( + attributes: collectorTypes.opentelemetryProto.trace.v1.Span.Link[] +) { + assert.deepStrictEqual( + attributes, + [ + { + traceId: traceIdBase64, + spanId: parentIdBase64, + attributes: [ + { + key: 'component', + value: { + stringValue: 'document-load', + }, + }, + ], + droppedAttributesCount: 0, + }, + ], + 'links are incorrect' + ); +} + +export function ensureProtoSpanIsCorrect( + span: collectorTypes.opentelemetryProto.trace.v1.Span +) { + if (span.attributes) { + ensureProtoAttributesAreCorrect(span.attributes); + } + if (span.events) { + ensureProtoEventsAreCorrect(span.events); + } + if (span.links) { + ensureProtoLinksAreCorrect(span.links); + } + assert.deepStrictEqual(span.traceId, traceIdBase64, 'traceId is wrong'); + assert.deepStrictEqual(span.spanId, spanIdBase64, 'spanId is wrong'); + assert.deepStrictEqual( + span.parentSpanId, + parentIdBase64, + 'parentIdArr is wrong' + ); + assert.strictEqual(span.name, 'documentFetch', 'name is wrong'); + assert.strictEqual(span.kind, 'INTERNAL', 'kind is wrong'); + assert.strictEqual( + span.startTimeUnixNano, + '1574120165429803008', + 'startTimeUnixNano is wrong' + ); + assert.strictEqual( + span.endTimeUnixNano, + '1574120165438688000', + 'endTimeUnixNano is wrong' + ); + assert.strictEqual( + span.droppedAttributesCount, + 0, + 'droppedAttributesCount is wrong' + ); + assert.strictEqual(span.droppedEventsCount, 0, 'droppedEventsCount is wrong'); + assert.strictEqual(span.droppedLinksCount, 0, 'droppedLinksCount is wrong'); + assert.deepStrictEqual(span.status, { code: 'Ok' }, 'status is wrong'); +} + +export function ensureExportedCounterIsCorrect( + metric: collectorTypes.opentelemetryProto.metrics.v1.Metric +) { + assert.deepStrictEqual(metric.metricDescriptor, { + name: 'test-counter', + description: 'sample counter description', + unit: '1', + type: 'MONOTONIC_INT64', + temporality: 'CUMULATIVE', + }); + assert.deepStrictEqual(metric.doubleDataPoints, undefined); + assert.deepStrictEqual(metric.summaryDataPoints, undefined); + assert.deepStrictEqual(metric.histogramDataPoints, undefined); + assert.ok(metric.int64DataPoints); + assert.deepStrictEqual(metric.int64DataPoints[0].labels, undefined); + assert.deepStrictEqual(metric.int64DataPoints[0].value, '1'); + assert.deepStrictEqual( + metric.int64DataPoints[0].startTimeUnixNano, + '1592602232694000128' + ); +} + +export function ensureExportedObserverIsCorrect( + metric: collectorTypes.opentelemetryProto.metrics.v1.Metric +) { + assert.deepStrictEqual(metric.metricDescriptor, { + name: 'test-observer', + description: 'sample observer description', + unit: '2', + type: 'SUMMARY', + temporality: 'DELTA', + }); + + assert.deepStrictEqual(metric.int64DataPoints, undefined); + assert.deepStrictEqual(metric.doubleDataPoints, undefined); + assert.deepStrictEqual(metric.histogramDataPoints, undefined); + assert.ok(metric.summaryDataPoints); + assert.deepStrictEqual(metric.summaryDataPoints[0].labels, undefined); + assert.deepStrictEqual(metric.summaryDataPoints[0].sum, 9); + assert.deepStrictEqual(metric.summaryDataPoints[0].count, '2'); + assert.deepStrictEqual( + metric.summaryDataPoints[0].startTimeUnixNano, + '1592602232694000128' + ); + assert.deepStrictEqual(metric.summaryDataPoints[0].percentileValues, [ + { percentile: 0, value: 3 }, + { percentile: 100, value: 6 }, + ]); +} + +export function ensureExportedHistogramIsCorrect( + metric: collectorTypes.opentelemetryProto.metrics.v1.Metric +) { + assert.deepStrictEqual(metric.metricDescriptor, { + name: 'test-hist', + description: 'sample observer description', + unit: '2', + type: 'HISTOGRAM', + temporality: 'DELTA', + }); + assert.deepStrictEqual(metric.int64DataPoints, undefined); + assert.deepStrictEqual(metric.summaryDataPoints, undefined); + assert.deepStrictEqual(metric.doubleDataPoints, undefined); + assert.ok(metric.histogramDataPoints); + assert.deepStrictEqual(metric.histogramDataPoints[0].labels, undefined); + assert.deepStrictEqual(metric.histogramDataPoints[0].count, '2'); + assert.deepStrictEqual(metric.histogramDataPoints[0].sum, 21); + assert.deepStrictEqual(metric.histogramDataPoints[0].buckets, [ + { count: '1' }, + { count: '1' }, + { count: '0' }, + ]); + assert.deepStrictEqual(metric.histogramDataPoints[0].explicitBounds, [ + 10, + 20, + ]); + assert.deepStrictEqual( + metric.histogramDataPoints[0].startTimeUnixNano, + '1592602232694000128' + ); +} + +export function ensureExportedValueRecorderIsCorrect( + metric: collectorTypes.opentelemetryProto.metrics.v1.Metric +) { + assert.deepStrictEqual(metric.metricDescriptor, { + name: 'test-recorder', + description: 'sample recorder description', + unit: '3', + type: 'SUMMARY', + temporality: 'DELTA', + }); + assert.deepStrictEqual(metric.histogramDataPoints, undefined); + assert.deepStrictEqual(metric.int64DataPoints, undefined); + assert.deepStrictEqual(metric.doubleDataPoints, undefined); + assert.ok(metric.summaryDataPoints); + assert.deepStrictEqual(metric.summaryDataPoints[0].labels, undefined); + assert.deepStrictEqual( + metric.summaryDataPoints[0].startTimeUnixNano, + '1592602232694000128' + ); + assert.deepStrictEqual(metric.summaryDataPoints[0].percentileValues, [ + { percentile: 0, value: 5 }, + { percentile: 100, value: 5 }, + ]); + assert.deepStrictEqual(metric.summaryDataPoints[0].count, '1'); + assert.deepStrictEqual(metric.summaryDataPoints[0].sum, 5); +} + +export function ensureExportTraceServiceRequestIsSet( + json: collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest +) { + const resourceSpans = json.resourceSpans; + assert.strictEqual( + resourceSpans && resourceSpans.length, + 1, + 'resourceSpans is missing' + ); + + const resource = resourceSpans[0].resource; + assert.strictEqual(!!resource, true, 'resource is missing'); + + const instrumentationLibrarySpans = + resourceSpans[0].instrumentationLibrarySpans; + assert.strictEqual( + instrumentationLibrarySpans && instrumentationLibrarySpans.length, + 1, + 'instrumentationLibrarySpans is missing' + ); + + const instrumentationLibrary = + instrumentationLibrarySpans[0].instrumentationLibrary; + assert.strictEqual( + !!instrumentationLibrary, + true, + 'instrumentationLibrary is missing' + ); + + const spans = instrumentationLibrarySpans[0].spans; + assert.strictEqual(spans && spans.length, 1, 'spans are missing'); +} + +export function ensureExportMetricsServiceRequestIsSet( + json: collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest +) { + const resourceMetrics = json.resourceMetrics; + assert.strictEqual( + resourceMetrics.length, + 4, + 'resourceMetrics has incorrect length' + ); + + const resource = resourceMetrics[0].resource; + assert.strictEqual(!!resource, true, 'resource is missing'); + + const instrumentationLibraryMetrics = + resourceMetrics[0].instrumentationLibraryMetrics; + assert.strictEqual( + instrumentationLibraryMetrics && instrumentationLibraryMetrics.length, + 1, + 'instrumentationLibraryMetrics is missing' + ); + + const instrumentationLibrary = + instrumentationLibraryMetrics[0].instrumentationLibrary; + assert.strictEqual( + !!instrumentationLibrary, + true, + 'instrumentationLibrary is missing' + ); + + const metric1 = resourceMetrics[0].instrumentationLibraryMetrics[0].metrics; + const metric2 = resourceMetrics[1].instrumentationLibraryMetrics[0].metrics; + assert.strictEqual(metric1.length, 1, 'Metrics are missing'); + assert.strictEqual(metric2.length, 1, 'Metrics are missing'); +} diff --git a/packages/opentelemetry-exporter-collector-proto/tsconfig.json b/packages/opentelemetry-exporter-collector-proto/tsconfig.json new file mode 100644 index 00000000000..a2042cd68b1 --- /dev/null +++ b/packages/opentelemetry-exporter-collector-proto/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/packages/opentelemetry-exporter-collector/README.md b/packages/opentelemetry-exporter-collector/README.md index 9acc5967dea..c398fc56abb 100644 --- a/packages/opentelemetry-exporter-collector/README.md +++ b/packages/opentelemetry-exporter-collector/README.md @@ -61,110 +61,13 @@ counter.add(10, { 'key': 'value' }); ``` -## Traces in Node - GRPC - -The CollectorTraceExporter in Node expects the URL to only be the hostname. It will not work with `/v1/trace`. - -```js -const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/tracing'); -const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector'); - -const collectorOptions = { - serviceName: 'basic-service', - url: '' // url is optional and can be omitted - default is localhost:55680 -}; - -const provider = new BasicTracerProvider(); -const exporter = new CollectorTraceExporter(collectorOptions); -provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); - -provider.register(); - -``` - -By default, plaintext connection is used. In order to use TLS in Node.js, provide `credentials` option like so: - -```js -const fs = require('fs'); -const grpc = require('grpc'); -const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/tracing'); -const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector'); - -const collectorOptions = { - serviceName: 'basic-service', - url: '', // url is optional and can be omitted - default is localhost:55680 - credentials: grpc.credentials.createSsl( - fs.readFileSync('./ca.crt'), - fs.readFileSync('./client.key'), - fs.readFileSync('./client.crt') - ) -}; - -const provider = new BasicTracerProvider(); -const exporter = new CollectorTraceExporter(collectorOptions); -provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); - -provider.register(); -``` - -To see how to generate credentials, you can refer to the script used to generate certificates for tests [here](./test/certs/regenerate.sh) - -The exporter can be configured to send custom metadata with each request as in the example below: - -```js -const grpc = require('grpc'); -const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/tracing'); -const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector'); - -const metadata = new grpc.Metadata(); -metadata.set('k', 'v'); - -const collectorOptions = { - serviceName: 'basic-service', - url: '', // url is optional and can be omitted - default is localhost:55680 - metadata, // // an optional grpc.Metadata object to be sent with each request -}; - -const provider = new BasicTracerProvider(); -const exporter = new CollectorTraceExporter(collectorOptions); -provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); - -provider.register(); -``` - -Note, that this will only work if TLS is also configured on the server. - ## Traces in Node - JSON over http ```js const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/tracing'); -const { CollectorExporter, CollectorTransportNode } = require('@opentelemetry/exporter-collector'); - -const collectorOptions = { - protocolNode: CollectorTransportNode.HTTP_JSON, - serviceName: 'basic-service', - url: '', // url is optional and can be omitted - default is http://localhost:55681/v1/trace - headers: { - foo: 'bar' - }, //an optional object containing custom headers to be sent with each request will only work with http -}; - -const provider = new BasicTracerProvider(); -const exporter = new CollectorExporter(collectorOptions); -provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); - -provider.register(); - -``` - -## Traces in Node - PROTO over http - -```js -const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/tracing'); -const { CollectorExporter, CollectorTransportNode } = require('@opentelemetry/exporter-collector'); +const { CollectorExporter } = require('@opentelemetry/exporter-collector'); const collectorOptions = { - protocolNode: CollectorTransportNode.HTTP_PROTO, serviceName: 'basic-service', url: '', // url is optional and can be omitted - default is http://localhost:55681/v1/trace headers: { @@ -182,14 +85,12 @@ provider.register(); ## Metrics in Node -The CollectorTraceExporter in Node expects the URL to only be the hostname. It will not work with `/v1/metrics`. All options that work with trace also work with metrics. - ```js const { MeterProvider } = require('@opentelemetry/metrics'); const { CollectorMetricExporter } = require('@opentelemetry/exporter-collector'); const collectorOptions = { serviceName: 'basic-service', - url: '', // url is optional and can be omitted - default is localhost:55681 + url: '', // url is optional and can be omitted - default is http://localhost:55681/v1/metrics }; const exporter = new CollectorMetricExporter(collectorOptions); @@ -205,6 +106,14 @@ counter.add(10, { 'key': 'value' }); ``` +## GRPC + +For GRPC please check [npm-url-grpc] + +## PROTOBUF + +For PROTOBUF please check [npm-url-proto] + ## Running opentelemetry-collector locally to see the traces 1. Go to examples/collector-exporter-node @@ -230,5 +139,7 @@ Apache 2.0 - See [LICENSE][license-url] for more information. [devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-exporter-collector [devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-exporter-collector&type=dev [npm-url]: https://www.npmjs.com/package/@opentelemetry/exporter-collector +[npm-url-grpc]: https://www.npmjs.com/package/@opentelemetry/exporter-collector-grpc +[npm-url-proto]: https://www.npmjs.com/package/@opentelemetry/exporter-collector-proto [npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fexporter-collector.svg [opentelemetry-collector-url]: https://github.com/open-telemetry/opentelemetry-collector diff --git a/packages/opentelemetry-exporter-collector/package.json b/packages/opentelemetry-exporter-collector/package.json index be4ce13c960..4bf9bed3b7d 100644 --- a/packages/opentelemetry-exporter-collector/package.json +++ b/packages/opentelemetry-exporter-collector/package.json @@ -10,22 +10,19 @@ "./build/src/platform/index.js": "./build/src/platform/browser/index.js" }, "scripts": { - "lint": "eslint . --ext .ts", - "lint:fix": "eslint . --ext .ts --fix", "clean": "rimraf build/*", "codecov:browser": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", - "precompile": "tsc --version", "compile": "npm run version:update && tsc -p .", - "postcompile": "npm run submodule && npm run protos:copy", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "precompile": "tsc --version", "prepare": "npm run compile", - "protos:copy": "cpx src/platform/node/protos/opentelemetry/**/*.* build/src/platform/node/protos/opentelemetry", - "submodule": "git submodule sync --recursive && git submodule update --init --recursive", "tdd": "npm run test -- --watch-extensions ts --watch", "tdd:browser": "karma start", "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts' --exclude 'test/browser/**/*.ts'", "test:browser": "nyc karma start --single-run", "version:update": "node ../../scripts/version-update.js", - "watch": "npm run protos:copy && tsc -w" + "watch": "tsc -w" }, "keywords": [ "opentelemetry", @@ -45,7 +42,6 @@ "build/src/**/*.js", "build/src/**/*.js.map", "build/src/**/*.d.ts", - "build/src/**/*.proto", "doc", "LICENSE", "README.md" @@ -55,7 +51,7 @@ }, "devDependencies": { "@babel/core": "7.11.1", - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "@types/sinon": "9.0.4", "@types/webpack-env": "1.15.2", @@ -73,7 +69,7 @@ "mocha": "7.2.0", "nyc": "15.1.0", "rimraf": "3.0.2", - "sinon": "9.0.2", + "sinon": "9.0.3", "ts-loader": "8.0.2", "ts-mocha": "7.0.0", "ts-node": "8.10.2", @@ -83,13 +79,11 @@ "webpack-merge": "5.1.1" }, "dependencies": { - "@grpc/proto-loader": "^0.5.4", "@opentelemetry/api": "^0.10.2", "@opentelemetry/core": "^0.10.2", "@opentelemetry/metrics": "^0.10.2", "@opentelemetry/resources": "^0.10.2", "@opentelemetry/tracing": "^0.10.2", - "grpc": "^1.24.2", - "protobufjs": "^6.9.0" + "axios": "^0.19.2" } } diff --git a/packages/opentelemetry-exporter-collector/src/index.ts b/packages/opentelemetry-exporter-collector/src/index.ts index 79ddd59b38a..7d549152f16 100644 --- a/packages/opentelemetry-exporter-collector/src/index.ts +++ b/packages/opentelemetry-exporter-collector/src/index.ts @@ -14,5 +14,8 @@ * limitations under the License. */ +export * from './CollectorExporterBase'; export * from './platform'; -export * from './enums'; +export * as collectorTypes from './types'; +export { toCollectorExportTraceServiceRequest } from './transform'; +export { toCollectorExportMetricServiceRequest } from './transformMetrics'; diff --git a/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorExporterBrowserBase.ts b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorExporterBrowserBase.ts index ab35cd98ac8..1c17f5b2035 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorExporterBrowserBase.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorExporterBrowserBase.ts @@ -15,7 +15,7 @@ */ import { CollectorExporterBase } from '../../CollectorExporterBase'; -import { CollectorExporterConfigBrowser } from './types'; +import { CollectorExporterConfigBase } from '../../types'; import * as collectorTypes from '../../types'; import { parseHeaders } from '../../util'; import { sendWithBeacon, sendWithXhr } from './util'; @@ -27,7 +27,7 @@ export abstract class CollectorExporterBrowserBase< ExportItem, ServiceRequest > extends CollectorExporterBase< - CollectorExporterConfigBrowser, + CollectorExporterConfigBase, ExportItem, ServiceRequest > { @@ -37,7 +37,7 @@ export abstract class CollectorExporterBrowserBase< /** * @param config */ - constructor(config: CollectorExporterConfigBrowser = {}) { + constructor(config: CollectorExporterConfigBase = {}) { super(config); this._useXHR = !!config.headers || typeof navigator.sendBeacon !== 'function'; diff --git a/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorMetricExporter.ts b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorMetricExporter.ts index 772f7fca297..09224a90aa0 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorMetricExporter.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorMetricExporter.ts @@ -15,12 +15,12 @@ */ import { MetricRecord, MetricExporter } from '@opentelemetry/metrics'; +import { CollectorExporterConfigBase } from '../../types'; import * as collectorTypes from '../../types'; import { CollectorExporterBrowserBase } from './CollectorExporterBrowserBase'; import { toCollectorExportMetricServiceRequest } from '../../transformMetrics'; -import { CollectorExporterConfigBrowser } from './types'; -const DEFAULT_COLLECTOR_URL = 'http://localhost:55680/v1/metrics'; +const DEFAULT_COLLECTOR_URL = 'http://localhost:55681/v1/metrics'; const DEFAULT_SERVICE_NAME = 'collector-metric-exporter'; /** @@ -45,11 +45,11 @@ export class CollectorMetricExporter ); } - getDefaultUrl(config: CollectorExporterConfigBrowser): string { + getDefaultUrl(config: CollectorExporterConfigBase): string { return config.url || DEFAULT_COLLECTOR_URL; } - getDefaultServiceName(config: CollectorExporterConfigBrowser): string { + getDefaultServiceName(config: CollectorExporterConfigBase): string { return config.serviceName || DEFAULT_SERVICE_NAME; } } diff --git a/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorTraceExporter.ts b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorTraceExporter.ts index 6fb89b46cf8..0884703cbe7 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorTraceExporter.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorTraceExporter.ts @@ -14,10 +14,10 @@ * limitations under the License. */ +import { CollectorExporterConfigBase } from '../../types'; import { CollectorExporterBrowserBase } from './CollectorExporterBrowserBase'; import { ReadableSpan, SpanExporter } from '@opentelemetry/tracing'; import { toCollectorExportTraceServiceRequest } from '../../transform'; -import { CollectorExporterConfigBrowser } from './types'; import * as collectorTypes from '../../types'; const DEFAULT_SERVICE_NAME = 'collector-trace-exporter'; @@ -38,11 +38,11 @@ export class CollectorTraceExporter return toCollectorExportTraceServiceRequest(spans, this); } - getDefaultUrl(config: CollectorExporterConfigBrowser) { + getDefaultUrl(config: CollectorExporterConfigBase) { return config.url || DEFAULT_COLLECTOR_URL; } - getDefaultServiceName(config: CollectorExporterConfigBrowser): string { + getDefaultServiceName(config: CollectorExporterConfigBase): string { return config.serviceName || DEFAULT_SERVICE_NAME; } } diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/CollectorExporterNodeBase.ts b/packages/opentelemetry-exporter-collector/src/platform/node/CollectorExporterNodeBase.ts index 743fbad4de2..9217357be63 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/node/CollectorExporterNodeBase.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/node/CollectorExporterNodeBase.ts @@ -14,19 +14,11 @@ * limitations under the License. */ -import { Metadata } from 'grpc'; import { CollectorExporterBase } from '../../CollectorExporterBase'; -import { ServiceClientType } from '../../types'; -import { CollectorExporterConfigNode, GRPCQueueItem } from './types'; -import { ServiceClient } from './types'; -import { CollectorProtocolNode } from '../../enums'; +import { CollectorExporterConfigBase } from '../../types'; import * as collectorTypes from '../../types'; import { parseHeaders } from '../../util'; -import { initWithJson, sendWithJson } from './utilWithJson'; -import { initWithGrpc, sendWithGrpc } from './utilWithGrpc'; -import { initWithJsonProto, sendWithJsonProto } from './utilWithJsonProto'; - -const DEFAULT_SERVICE_NAME = 'collector-metric-exporter'; +import { sendWithHttp } from './util'; /** * Collector Metric Exporter abstract base class @@ -35,55 +27,25 @@ export abstract class CollectorExporterNodeBase< ExportItem, ServiceRequest > extends CollectorExporterBase< - CollectorExporterConfigNode, + CollectorExporterConfigBase, ExportItem, ServiceRequest > { DEFAULT_HEADERS: Record = { [collectorTypes.OT_REQUEST_HEADER]: '1', }; - grpcQueue: GRPCQueueItem[] = []; - metadata?: Metadata; - serviceClient?: ServiceClient = undefined; headers: Record; - protected readonly _protocol: CollectorProtocolNode; - - constructor(config: CollectorExporterConfigNode = {}) { + constructor(config: CollectorExporterConfigBase = {}) { super(config); - this._protocol = - typeof config.protocolNode !== 'undefined' - ? config.protocolNode - : CollectorProtocolNode.GRPC; - if (this._protocol === CollectorProtocolNode.GRPC) { - this.logger.debug('CollectorExporter - using grpc'); - if (config.headers) { - this.logger.warn('Headers cannot be set when using grpc'); - } - } else { - if (this._protocol === CollectorProtocolNode.HTTP_JSON) { - this.logger.debug('CollectorExporter - using json over http'); - } else { - this.logger.debug('CollectorExporter - using proto over http'); - } - if (config.metadata) { - this.logger.warn('Metadata cannot be set when using http'); - } + if ((config as any).metadata) { + this.logger.warn('Metadata cannot be set when using http'); } this.headers = parseHeaders(config.headers, this.logger) || this.DEFAULT_HEADERS; - this.metadata = config.metadata; } - onInit(config: CollectorExporterConfigNode): void { + onInit(config: CollectorExporterConfigBase): void { this._isShutdown = false; - - if (config.protocolNode === CollectorProtocolNode.HTTP_JSON) { - initWithJson(this, config); - } else if (config.protocolNode === CollectorProtocolNode.HTTP_PROTO) { - initWithJsonProto(this, config); - } else { - initWithGrpc(this, config); - } } send( @@ -95,26 +57,16 @@ export abstract class CollectorExporterNodeBase< this.logger.debug('Shutdown already started. Cannot send objects'); return; } - if (this._protocol === CollectorProtocolNode.HTTP_JSON) { - sendWithJson(this, objects, onSuccess, onError); - } else if (this._protocol === CollectorProtocolNode.HTTP_PROTO) { - sendWithJsonProto(this, objects, onSuccess, onError); - } else { - sendWithGrpc(this, objects, onSuccess, onError); - } - } - - onShutdown(): void { - this._isShutdown = true; - if (this.serviceClient) { - this.serviceClient.close(); - } - } + const serviceRequest = this.convert(objects); - getDefaultServiceName(config: CollectorExporterConfigNode): string { - return config.serviceName || DEFAULT_SERVICE_NAME; + sendWithHttp( + this, + JSON.stringify(serviceRequest), + 'application/json', + onSuccess, + onError + ); } - abstract getServiceProtoPath(): string; - abstract getServiceClientType(): ServiceClientType; + onShutdown(): void {} } diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/CollectorMetricExporter.ts b/packages/opentelemetry-exporter-collector/src/platform/node/CollectorMetricExporter.ts index 8eebc7bdebe..f86c397fbe6 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/node/CollectorMetricExporter.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/node/CollectorMetricExporter.ts @@ -15,16 +15,13 @@ */ import { MetricRecord, MetricExporter } from '@opentelemetry/metrics'; -import { ServiceClientType } from '../../types'; +import { CollectorExporterConfigBase } from '../../types'; import * as collectorTypes from '../../types'; -import { CollectorExporterConfigNode } from './types'; -import { CollectorProtocolNode } from '../../enums'; import { CollectorExporterNodeBase } from './CollectorExporterNodeBase'; import { toCollectorExportMetricServiceRequest } from '../../transformMetrics'; const DEFAULT_SERVICE_NAME = 'collector-metric-exporter'; -const DEFAULT_COLLECTOR_URL_GRPC = 'localhost:55680'; -const DEFAULT_COLLECTOR_URL_JSON = 'http://localhost:55681/v1/metrics'; +const DEFAULT_COLLECTOR_URL = 'http://localhost:55681/v1/metrics'; /** * Collector Metric Exporter for Node @@ -48,24 +45,14 @@ export class CollectorMetricExporter ); } - getDefaultUrl(config: CollectorExporterConfigNode): string { + getDefaultUrl(config: CollectorExporterConfigBase): string { if (!config.url) { - return config.protocolNode === CollectorProtocolNode.HTTP_JSON - ? DEFAULT_COLLECTOR_URL_JSON - : DEFAULT_COLLECTOR_URL_GRPC; + return DEFAULT_COLLECTOR_URL; } return config.url; } - getDefaultServiceName(config: CollectorExporterConfigNode): string { + getDefaultServiceName(config: CollectorExporterConfigBase): string { return config.serviceName || DEFAULT_SERVICE_NAME; } - - getServiceClientType() { - return ServiceClientType.METRICS; - } - - getServiceProtoPath(): string { - return 'opentelemetry/proto/collector/metrics/v1/metrics_service.proto'; - } } diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/CollectorTraceExporter.ts b/packages/opentelemetry-exporter-collector/src/platform/node/CollectorTraceExporter.ts index 45f7194f55b..667c133201b 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/node/CollectorTraceExporter.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/node/CollectorTraceExporter.ts @@ -15,17 +15,13 @@ */ import { ReadableSpan, SpanExporter } from '@opentelemetry/tracing'; -import { ServiceClientType } from '../../types'; +import { CollectorExporterConfigBase } from '../../types'; import { CollectorExporterNodeBase } from './CollectorExporterNodeBase'; import * as collectorTypes from '../../types'; -import { CollectorProtocolNode } from '../../enums'; -import { CollectorExporterConfigNode } from './types'; import { toCollectorExportTraceServiceRequest } from '../../transform'; const DEFAULT_SERVICE_NAME = 'collector-trace-exporter'; -const DEFAULT_COLLECTOR_URL_GRPC = 'localhost:55680'; -const DEFAULT_COLLECTOR_URL_JSON = 'http://localhost:55681/v1/trace'; -const DEFAULT_COLLECTOR_URL_JSON_PROTO = 'http://localhost:55681/v1/trace'; +const DEFAULT_COLLECTOR_URL = 'http://localhost:55681/v1/trace'; /** * Collector Trace Exporter for Node @@ -42,28 +38,14 @@ export class CollectorTraceExporter return toCollectorExportTraceServiceRequest(spans, this); } - getDefaultUrl(config: CollectorExporterConfigNode): string { + getDefaultUrl(config: CollectorExporterConfigBase): string { if (!config.url) { - if (config.protocolNode === CollectorProtocolNode.HTTP_JSON) { - return DEFAULT_COLLECTOR_URL_JSON; - } else if (config.protocolNode === CollectorProtocolNode.HTTP_PROTO) { - return DEFAULT_COLLECTOR_URL_JSON_PROTO; - } else { - return DEFAULT_COLLECTOR_URL_GRPC; - } + return DEFAULT_COLLECTOR_URL; } return config.url; } - getDefaultServiceName(config: CollectorExporterConfigNode): string { + getDefaultServiceName(config: CollectorExporterConfigBase): string { return config.serviceName || DEFAULT_SERVICE_NAME; } - - getServiceClientType() { - return ServiceClientType.SPANS; - } - - getServiceProtoPath(): string { - return 'opentelemetry/proto/collector/trace/v1/trace_service.proto'; - } } diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/index.ts b/packages/opentelemetry-exporter-collector/src/platform/node/index.ts index fcbe012b52b..6b647adc24a 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/node/index.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/node/index.ts @@ -16,3 +16,5 @@ export * from './CollectorTraceExporter'; export * from './CollectorMetricExporter'; +export * from './CollectorExporterNodeBase'; +export * from './util'; diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/protos b/packages/opentelemetry-exporter-collector/src/platform/node/protos deleted file mode 160000 index 9ffeee0ec53..00000000000 --- a/packages/opentelemetry-exporter-collector/src/platform/node/protos +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9ffeee0ec532efe02285af84880deb2a53a3eab1 diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/util.ts b/packages/opentelemetry-exporter-collector/src/platform/node/util.ts index c7a7e24a572..eddb1101677 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/node/util.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/node/util.ts @@ -19,10 +19,6 @@ import * as https from 'https'; import * as collectorTypes from '../../types'; import { CollectorExporterNodeBase } from './CollectorExporterNodeBase'; -export function removeProtocol(url: string): string { - return url.replace(/^https?:\/\//, ''); -} - /** * Sends data using http * @param collector diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/utilWithJson.ts b/packages/opentelemetry-exporter-collector/src/platform/node/utilWithJson.ts deleted file mode 100644 index 72e2c3198b0..00000000000 --- a/packages/opentelemetry-exporter-collector/src/platform/node/utilWithJson.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as collectorTypes from '../../types'; -import { CollectorExporterNodeBase } from './CollectorExporterNodeBase'; -import { CollectorExporterConfigNode } from './types'; -import { sendWithHttp } from './util'; - -export function initWithJson( - _collector: CollectorExporterNodeBase, - _config: CollectorExporterConfigNode -): void { - // nothing to be done for json yet -} - -export function sendWithJson( - collector: CollectorExporterNodeBase, - objects: ExportItem[], - onSuccess: () => void, - onError: (error: collectorTypes.CollectorExporterError) => void -): void { - const serviceRequest = collector.convert(objects); - - sendWithHttp( - collector, - JSON.stringify(serviceRequest), - 'application/json', - onSuccess, - onError - ); -} diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/utilWithJsonProto.ts b/packages/opentelemetry-exporter-collector/src/platform/node/utilWithJsonProto.ts deleted file mode 100644 index 15025ed56ed..00000000000 --- a/packages/opentelemetry-exporter-collector/src/platform/node/utilWithJsonProto.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as path from 'path'; -import { Type } from 'protobufjs'; -import * as protobufjs from 'protobufjs'; -import * as collectorTypes from '../../types'; -import { CollectorExporterNodeBase } from './CollectorExporterNodeBase'; -import { CollectorExporterConfigNode } from './types'; -import { sendWithHttp } from './util'; - -let ExportTraceServiceRequestProto: Type | undefined; - -export function getExportTraceServiceRequestProto(): Type | undefined { - return ExportTraceServiceRequestProto; -} - -export function initWithJsonProto( - _collector: CollectorExporterNodeBase, - _config: CollectorExporterConfigNode -): void { - const dir = path.resolve(__dirname, 'protos'); - const root = new protobufjs.Root(); - root.resolvePath = function (origin, target) { - return `${dir}/${target}`; - }; - const proto = root.loadSync([ - 'opentelemetry/proto/common/v1/common.proto', - 'opentelemetry/proto/resource/v1/resource.proto', - 'opentelemetry/proto/trace/v1/trace.proto', - 'opentelemetry/proto/collector/trace/v1/trace_service.proto', - ]); - ExportTraceServiceRequestProto = proto?.lookupType( - 'ExportTraceServiceRequest' - ); -} - -export function sendWithJsonProto( - collector: CollectorExporterNodeBase, - objects: ExportItem[], - onSuccess: () => void, - onError: (error: collectorTypes.CollectorExporterError) => void -): void { - const serviceRequest = collector.convert(objects); - - const message = ExportTraceServiceRequestProto?.create(serviceRequest); - if (message) { - const body = ExportTraceServiceRequestProto?.encode(message).finish(); - if (body) { - sendWithHttp( - collector, - Buffer.from(body), - 'application/x-protobuf', - onSuccess, - onError - ); - } - } else { - onError({ - message: 'No proto', - }); - } -} diff --git a/packages/opentelemetry-exporter-collector/src/transform.ts b/packages/opentelemetry-exporter-collector/src/transform.ts index 4c11bf8c4a1..005f41a1bc4 100644 --- a/packages/opentelemetry-exporter-collector/src/transform.ts +++ b/packages/opentelemetry-exporter-collector/src/transform.ts @@ -188,7 +188,7 @@ export function toCollectorResource( const attr = Object.assign( {}, additionalAttributes, - resource ? resource.labels : {} + resource ? resource.attributes : {} ); const resourceProto: opentelemetryProto.resource.v1.Resource = { attributes: toCollectorAttributes(attr), diff --git a/packages/opentelemetry-exporter-collector/src/transformMetrics.ts b/packages/opentelemetry-exporter-collector/src/transformMetrics.ts index f192d117bfb..c35609860fd 100644 --- a/packages/opentelemetry-exporter-collector/src/transformMetrics.ts +++ b/packages/opentelemetry-exporter-collector/src/transformMetrics.ts @@ -38,7 +38,7 @@ export function toCollectorLabels( labels: api.Labels ): opentelemetryProto.common.v1.StringKeyValue[] { return Object.entries(labels).map(([key, value]) => { - return { key, value }; + return { key, value: String(value) }; }); } diff --git a/packages/opentelemetry-exporter-collector/src/types.ts b/packages/opentelemetry-exporter-collector/src/types.ts index 5d95654fb01..23ff4da1a55 100644 --- a/packages/opentelemetry-exporter-collector/src/types.ts +++ b/packages/opentelemetry-exporter-collector/src/types.ts @@ -308,8 +308,3 @@ export const COLLECTOR_SPAN_KIND_MAPPING = { [SpanKind.PRODUCER]: opentelemetryProto.trace.v1.Span.SpanKind.PRODUCER, [SpanKind.CONSUMER]: opentelemetryProto.trace.v1.Span.SpanKind.CONSUMER, }; - -export enum ServiceClientType { - SPANS, - METRICS, -} diff --git a/packages/opentelemetry-exporter-collector/test/browser/CollectorMetricExporter.test.ts b/packages/opentelemetry-exporter-collector/test/browser/CollectorMetricExporter.test.ts index 8187bebdf12..50b62794987 100644 --- a/packages/opentelemetry-exporter-collector/test/browser/CollectorMetricExporter.test.ts +++ b/packages/opentelemetry-exporter-collector/test/browser/CollectorMetricExporter.test.ts @@ -18,6 +18,7 @@ import { NoopLogger } from '@opentelemetry/core'; import * as assert from 'assert'; import * as sinon from 'sinon'; import { CollectorMetricExporter } from '../../src/platform/browser/index'; +import { CollectorExporterConfigBase } from '../../src/types'; import * as collectorTypes from '../../src/types'; import { MetricRecord } from '@opentelemetry/metrics'; import { @@ -33,7 +34,6 @@ import { ensureValueRecorderIsCorrect, ensureHistogramIsCorrect, } from '../helper'; -import { CollectorExporterConfigBrowser } from '../../src/platform/browser/types'; import { hrTimeToNanoseconds } from '@opentelemetry/core'; const sendBeacon = navigator.sendBeacon; @@ -342,7 +342,7 @@ describe('CollectorMetricExporter - web', () => { foo: 'bar', bar: 'baz', }; - let collectorExporterConfig: CollectorExporterConfigBrowser; + let collectorExporterConfig: CollectorExporterConfigBase; beforeEach(() => { collectorExporterConfig = { diff --git a/packages/opentelemetry-exporter-collector/test/browser/CollectorTraceExporter.test.ts b/packages/opentelemetry-exporter-collector/test/browser/CollectorTraceExporter.test.ts index bfb916b3338..018d9ec8f9d 100644 --- a/packages/opentelemetry-exporter-collector/test/browser/CollectorTraceExporter.test.ts +++ b/packages/opentelemetry-exporter-collector/test/browser/CollectorTraceExporter.test.ts @@ -19,8 +19,8 @@ import { ReadableSpan } from '@opentelemetry/tracing'; import * as assert from 'assert'; import * as sinon from 'sinon'; import { CollectorTraceExporter } from '../../src/platform/browser/index'; +import { CollectorExporterConfigBase } from '../../src/types'; import * as collectorTypes from '../../src/types'; -import { CollectorExporterConfigBrowser } from '../../src/platform/browser/types'; import { ensureSpanIsCorrect, @@ -33,7 +33,7 @@ const sendBeacon = navigator.sendBeacon; describe('CollectorTraceExporter - web', () => { let collectorTraceExporter: CollectorTraceExporter; - let collectorExporterConfig: CollectorExporterConfigBrowser; + let collectorExporterConfig: CollectorExporterConfigBase; let spyOpen: sinon.SinonSpy; let spySend: sinon.SinonSpy; let spyBeacon: sinon.SinonSpy; diff --git a/packages/opentelemetry-exporter-collector/test/common/transformMetrics.test.ts b/packages/opentelemetry-exporter-collector/test/common/transformMetrics.test.ts index e5a42194b20..5c725a9007e 100644 --- a/packages/opentelemetry-exporter-collector/test/common/transformMetrics.test.ts +++ b/packages/opentelemetry-exporter-collector/test/common/transformMetrics.test.ts @@ -34,6 +34,7 @@ import { import { MetricRecord, SumAggregator } from '@opentelemetry/metrics'; import { hrTimeToNanoseconds } from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; + describe('transformMetrics', () => { describe('toCollectorMetric', () => { const counter: MetricRecord = mockCounter(); @@ -72,6 +73,7 @@ describe('transformMetrics', () => { // ValueRecorder recorder.aggregator.update(5); }); + it('should convert metric', () => { ensureCounterIsCorrect( transform.toCollectorMetric(counter, 1592602232694000000), @@ -102,6 +104,28 @@ describe('transformMetrics', () => { ); assert.deepStrictEqual(emptyMetric.int64DataPoints, []); }); + + it('should convert metric labels value to string', () => { + const metric = transform.toCollectorMetric( + { + descriptor: { + name: 'name', + description: 'description', + unit: 'unit', + metricKind: 0, + valueType: 0, + }, + labels: { foo: (1 as unknown) as string }, + aggregator: new SumAggregator(), + resource: new Resource({}), + instrumentationLibrary: { name: 'x', version: 'y' }, + }, + 1592602232694000000 + ); + const collectorMetric = + metric.int64DataPoints && metric.int64DataPoints[0]; + assert.strictEqual(collectorMetric?.labels[0].value, '1'); + }); }); describe('toCollectorMetricDescriptor', () => { describe('groupMetricsByResourceAndLibrary', () => { diff --git a/packages/opentelemetry-exporter-collector/test/helper.ts b/packages/opentelemetry-exporter-collector/test/helper.ts index be51825e8f3..b74c09ac20d 100644 --- a/packages/opentelemetry-exporter-collector/test/helper.ts +++ b/packages/opentelemetry-exporter-collector/test/helper.ts @@ -28,7 +28,6 @@ import { HistogramAggregator, } from '@opentelemetry/metrics'; import { InstrumentationLibrary } from '@opentelemetry/core'; -import * as grpc from 'grpc'; if (typeof Buffer === 'undefined') { (window as any).Buffer = { @@ -38,27 +37,6 @@ if (typeof Buffer === 'undefined') { }; } -const traceIdArr = [ - 31, - 16, - 8, - 220, - 142, - 39, - 14, - 133, - 196, - 10, - 13, - 124, - 57, - 57, - 178, - 120, -]; -const spanIdArr = [94, 16, 114, 97, 246, 79, 165, 62]; -const parentIdArr = [120, 168, 145, 80, 152, 134, 67, 136]; - export function mockCounter(): MetricRecord { return { descriptor: { @@ -362,149 +340,6 @@ export const multiInstrumentationLibraryTrace: ReadableSpan[] = [ }, ]; -export function ensureExportedEventsAreCorrect( - events: opentelemetryProto.trace.v1.Span.Event[] -) { - assert.deepStrictEqual( - events, - [ - { - attributes: [], - timeUnixNano: '1574120165429803008', - name: 'fetchStart', - droppedAttributesCount: 0, - }, - { - attributes: [], - timeUnixNano: '1574120165429803008', - name: 'domainLookupStart', - droppedAttributesCount: 0, - }, - { - attributes: [], - timeUnixNano: '1574120165429803008', - name: 'domainLookupEnd', - droppedAttributesCount: 0, - }, - { - attributes: [], - timeUnixNano: '1574120165429803008', - name: 'connectStart', - droppedAttributesCount: 0, - }, - { - attributes: [], - timeUnixNano: '1574120165429803008', - name: 'connectEnd', - droppedAttributesCount: 0, - }, - { - attributes: [], - timeUnixNano: '1574120165435513088', - name: 'requestStart', - droppedAttributesCount: 0, - }, - { - attributes: [], - timeUnixNano: '1574120165436923136', - name: 'responseStart', - droppedAttributesCount: 0, - }, - { - attributes: [], - timeUnixNano: '1574120165438688000', - name: 'responseEnd', - droppedAttributesCount: 0, - }, - ], - 'exported events are incorrect' - ); -} - -export function ensureExportedAttributesAreCorrect( - attributes: opentelemetryProto.common.v1.KeyValue[], - usingGRPC = false -) { - if (usingGRPC) { - assert.deepStrictEqual( - attributes, - [ - { - key: 'component', - value: { - stringValue: 'document-load', - value: 'stringValue', - }, - }, - ], - 'exported attributes are incorrect' - ); - } else { - assert.deepStrictEqual( - attributes, - [ - { - key: 'component', - value: { - stringValue: 'document-load', - }, - }, - ], - 'exported attributes are incorrect' - ); - } -} - -export function ensureExportedLinksAreCorrect( - attributes: opentelemetryProto.trace.v1.Span.Link[], - usingGRPC = false -) { - if (usingGRPC) { - assert.deepStrictEqual( - attributes, - [ - { - attributes: [ - { - key: 'component', - value: { - stringValue: 'document-load', - value: 'stringValue', - }, - }, - ], - traceId: Buffer.from(traceIdArr), - spanId: Buffer.from(parentIdArr), - traceState: '', - droppedAttributesCount: 0, - }, - ], - 'exported links are incorrect' - ); - } else { - assert.deepStrictEqual( - attributes, - [ - { - attributes: [ - { - key: 'component', - value: { - stringValue: 'document-load', - }, - }, - ], - traceId: Buffer.from(traceIdArr), - spanId: Buffer.from(parentIdArr), - traceState: '', - droppedAttributesCount: 0, - }, - ], - 'exported links are incorrect' - ); - } -} - export function ensureEventsAreCorrect( events: opentelemetryProto.trace.v1.Span.Event[] ) { @@ -564,57 +399,6 @@ export function ensureEventsAreCorrect( ); } -export function ensureProtoEventsAreCorrect( - events: opentelemetryProto.trace.v1.Span.Event[] -) { - assert.deepStrictEqual( - events, - [ - { - timeUnixNano: '1574120165429803008', - name: 'fetchStart', - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165429803008', - name: 'domainLookupStart', - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165429803008', - name: 'domainLookupEnd', - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165429803008', - name: 'connectStart', - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165429803008', - name: 'connectEnd', - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165435513088', - name: 'requestStart', - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165436923136', - name: 'responseStart', - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165438688000', - name: 'responseEnd', - droppedAttributesCount: 0, - }, - ], - 'events are incorrect' - ); -} - export function ensureAttributesAreCorrect( attributes: opentelemetryProto.common.v1.KeyValue[] ) { @@ -632,23 +416,6 @@ export function ensureAttributesAreCorrect( ); } -export function ensureProtoAttributesAreCorrect( - attributes: opentelemetryProto.common.v1.KeyValue[] -) { - assert.deepStrictEqual( - attributes, - [ - { - key: 'component', - value: { - stringValue: 'document-load', - }, - }, - ], - 'attributes are incorrect' - ); -} - export function ensureLinksAreCorrect( attributes: opentelemetryProto.trace.v1.Span.Link[] ) { @@ -673,30 +440,6 @@ export function ensureLinksAreCorrect( ); } -export function ensureProtoLinksAreCorrect( - attributes: opentelemetryProto.trace.v1.Span.Link[] -) { - assert.deepStrictEqual( - attributes, - [ - { - traceId: traceIdBase64, - spanId: parentIdBase64, - attributes: [ - { - key: 'component', - value: { - stringValue: 'document-load', - }, - }, - ], - droppedAttributesCount: 0, - }, - ], - 'links are incorrect' - ); -} - export function ensureSpanIsCorrect( span: collectorTypes.opentelemetryProto.trace.v1.Span ) { @@ -742,102 +485,6 @@ export function ensureSpanIsCorrect( assert.deepStrictEqual(span.status, { code: 0 }, 'status is wrong'); } -export function ensureProtoSpanIsCorrect( - span: collectorTypes.opentelemetryProto.trace.v1.Span -) { - if (span.attributes) { - ensureProtoAttributesAreCorrect(span.attributes); - } - if (span.events) { - ensureProtoEventsAreCorrect(span.events); - } - if (span.links) { - ensureProtoLinksAreCorrect(span.links); - } - assert.deepStrictEqual(span.traceId, traceIdBase64, 'traceId is wrong'); - assert.deepStrictEqual(span.spanId, spanIdBase64, 'spanId is wrong'); - assert.deepStrictEqual( - span.parentSpanId, - parentIdBase64, - 'parentIdArr is wrong' - ); - assert.strictEqual(span.name, 'documentFetch', 'name is wrong'); - assert.strictEqual(span.kind, 'INTERNAL', 'kind is wrong'); - assert.strictEqual( - span.startTimeUnixNano, - '1574120165429803008', - 'startTimeUnixNano is wrong' - ); - assert.strictEqual( - span.endTimeUnixNano, - '1574120165438688000', - 'endTimeUnixNano is wrong' - ); - assert.strictEqual( - span.droppedAttributesCount, - 0, - 'droppedAttributesCount is wrong' - ); - assert.strictEqual(span.droppedEventsCount, 0, 'droppedEventsCount is wrong'); - assert.strictEqual(span.droppedLinksCount, 0, 'droppedLinksCount is wrong'); - assert.deepStrictEqual(span.status, { code: 'Ok' }, 'status is wrong'); -} - -export function ensureExportedSpanIsCorrect( - span: collectorTypes.opentelemetryProto.trace.v1.Span, - usingGRPC = false -) { - if (span.attributes) { - ensureExportedAttributesAreCorrect(span.attributes, usingGRPC); - } - if (span.events) { - ensureExportedEventsAreCorrect(span.events); - } - if (span.links) { - ensureExportedLinksAreCorrect(span.links, usingGRPC); - } - assert.deepStrictEqual( - span.traceId, - Buffer.from(traceIdArr), - 'traceId is wrong' - ); - assert.deepStrictEqual( - span.spanId, - Buffer.from(spanIdArr), - 'spanId is wrong' - ); - assert.strictEqual(span.traceState, '', 'traceState is wrong'); - assert.deepStrictEqual( - span.parentSpanId, - Buffer.from(parentIdArr), - 'parentIdArr is wrong' - ); - assert.strictEqual(span.name, 'documentFetch', 'name is wrong'); - assert.strictEqual(span.kind, 'INTERNAL', 'kind is wrong'); - assert.strictEqual( - span.startTimeUnixNano, - '1574120165429803008', - 'startTimeUnixNano is wrong' - ); - assert.strictEqual( - span.endTimeUnixNano, - '1574120165438688000', - 'endTimeUnixNano is wrong' - ); - assert.strictEqual( - span.droppedAttributesCount, - 0, - 'droppedAttributesCount is wrong' - ); - assert.strictEqual(span.droppedEventsCount, 0, 'droppedEventsCount is wrong'); - assert.strictEqual(span.droppedLinksCount, 0, 'droppedLinksCount is wrong'); - assert.deepStrictEqual( - span.status, - { code: 'Ok', message: '' }, - 'status is wrong' - ); -} - export function ensureWebResourceIsCorrect( resource: collectorTypes.opentelemetryProto.resource.v1.Resource ) { @@ -1023,186 +670,6 @@ export function ensureHistogramIsCorrect( }); } -export function ensureExportedCounterIsCorrect( - metric: collectorTypes.opentelemetryProto.metrics.v1.Metric -) { - assert.deepStrictEqual(metric.metricDescriptor, { - name: 'test-counter', - description: 'sample counter description', - unit: '1', - type: 'MONOTONIC_INT64', - temporality: 'CUMULATIVE', - }); - assert.deepStrictEqual(metric.doubleDataPoints, []); - assert.deepStrictEqual(metric.summaryDataPoints, []); - assert.deepStrictEqual(metric.histogramDataPoints, []); - assert.ok(metric.int64DataPoints); - assert.deepStrictEqual(metric.int64DataPoints[0].labels, []); - assert.deepStrictEqual(metric.int64DataPoints[0].value, '1'); - assert.deepStrictEqual( - metric.int64DataPoints[0].startTimeUnixNano, - '1592602232694000128' - ); -} - -export function ensureExportedObserverIsCorrect( - metric: collectorTypes.opentelemetryProto.metrics.v1.Metric -) { - assert.deepStrictEqual(metric.metricDescriptor, { - name: 'test-observer', - description: 'sample observer description', - unit: '2', - type: 'SUMMARY', - temporality: 'DELTA', - }); - - assert.deepStrictEqual(metric.int64DataPoints, []); - assert.deepStrictEqual(metric.doubleDataPoints, []); - assert.deepStrictEqual(metric.histogramDataPoints, []); - assert.ok(metric.summaryDataPoints); - assert.deepStrictEqual(metric.summaryDataPoints[0].labels, []); - assert.deepStrictEqual(metric.summaryDataPoints[0].sum, 9); - assert.deepStrictEqual(metric.summaryDataPoints[0].count, '2'); - assert.deepStrictEqual( - metric.summaryDataPoints[0].startTimeUnixNano, - '1592602232694000128' - ); - assert.deepStrictEqual(metric.summaryDataPoints[0].percentileValues, [ - { percentile: 0, value: 3 }, - { percentile: 100, value: 6 }, - ]); -} - -export function ensureExportedHistogramIsCorrect( - metric: collectorTypes.opentelemetryProto.metrics.v1.Metric -) { - assert.deepStrictEqual(metric.metricDescriptor, { - name: 'test-hist', - description: 'sample observer description', - unit: '2', - type: 'HISTOGRAM', - temporality: 'DELTA', - }); - assert.deepStrictEqual(metric.int64DataPoints, []); - assert.deepStrictEqual(metric.summaryDataPoints, []); - assert.deepStrictEqual(metric.doubleDataPoints, []); - assert.ok(metric.histogramDataPoints); - assert.deepStrictEqual(metric.histogramDataPoints[0].labels, []); - assert.deepStrictEqual(metric.histogramDataPoints[0].count, '2'); - assert.deepStrictEqual(metric.histogramDataPoints[0].sum, 21); - assert.deepStrictEqual(metric.histogramDataPoints[0].buckets, [ - { count: '1', exemplar: null }, - { count: '1', exemplar: null }, - { count: '0', exemplar: null }, - ]); - assert.deepStrictEqual(metric.histogramDataPoints[0].explicitBounds, [ - 10, - 20, - ]); - assert.deepStrictEqual( - metric.histogramDataPoints[0].startTimeUnixNano, - '1592602232694000128' - ); -} - -export function ensureExportedValueRecorderIsCorrect( - metric: collectorTypes.opentelemetryProto.metrics.v1.Metric -) { - assert.deepStrictEqual(metric.metricDescriptor, { - name: 'test-recorder', - description: 'sample recorder description', - unit: '3', - type: 'SUMMARY', - temporality: 'DELTA', - }); - assert.deepStrictEqual(metric.histogramDataPoints, []); - assert.deepStrictEqual(metric.int64DataPoints, []); - assert.deepStrictEqual(metric.doubleDataPoints, []); - assert.ok(metric.summaryDataPoints); - assert.deepStrictEqual(metric.summaryDataPoints[0].labels, []); - assert.deepStrictEqual( - metric.summaryDataPoints[0].startTimeUnixNano, - '1592602232694000128' - ); - assert.deepStrictEqual(metric.summaryDataPoints[0].percentileValues, [ - { percentile: 0, value: 5 }, - { percentile: 100, value: 5 }, - ]); - assert.deepStrictEqual(metric.summaryDataPoints[0].count, '1'); - assert.deepStrictEqual(metric.summaryDataPoints[0].sum, 5); -} - -export function ensureResourceIsCorrect( - resource: collectorTypes.opentelemetryProto.resource.v1.Resource, - usingGRPC = true -) { - if (usingGRPC) { - assert.deepStrictEqual(resource, { - attributes: [ - { - key: 'service.name', - value: { - stringValue: 'basic-service', - value: 'stringValue', - }, - }, - { - key: 'service', - value: { - stringValue: 'ui', - value: 'stringValue', - }, - }, - { - key: 'version', - value: { - doubleValue: 1, - value: 'doubleValue', - }, - }, - { - key: 'cost', - value: { - doubleValue: 112.12, - value: 'doubleValue', - }, - }, - ], - droppedAttributesCount: 0, - }); - } else { - assert.deepStrictEqual(resource, { - attributes: [ - { - key: 'service.name', - value: { - stringValue: 'basic-service', - }, - }, - { - key: 'service', - value: { - stringValue: 'ui', - }, - }, - { - key: 'version', - value: { - doubleValue: 1, - }, - }, - { - key: 'cost', - value: { - doubleValue: 112.12, - }, - }, - ], - droppedAttributesCount: 0, - }); - } -} - export function ensureExportTraceServiceRequestIsSet( json: collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest ) { @@ -1271,16 +738,6 @@ export function ensureExportMetricsServiceRequestIsSet( assert.strictEqual(metric2.length, 1, 'Metrics are missing'); } -export function ensureMetadataIsCorrect( - actual: grpc.Metadata, - expected: grpc.Metadata -) { - //ignore user agent - expected.remove('user-agent'); - actual.remove('user-agent'); - assert.deepStrictEqual(actual.getMap(), expected.getMap()); -} - export function ensureHeadersContain( actual: { [key: string]: string }, expected: { [key: string]: string } diff --git a/packages/opentelemetry-exporter-collector/test/node/CollectorExporterWithProto.test.ts b/packages/opentelemetry-exporter-collector/test/node/CollectorExporterWithProto.test.ts deleted file mode 100644 index e847f6bea3b..00000000000 --- a/packages/opentelemetry-exporter-collector/test/node/CollectorExporterWithProto.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as core from '@opentelemetry/core'; -import { ReadableSpan } from '@opentelemetry/tracing'; -import * as http from 'http'; -import * as assert from 'assert'; -import * as sinon from 'sinon'; -import { CollectorProtocolNode } from '../../src/enums'; -import { CollectorTraceExporter } from '../../src/platform/node'; -import { CollectorExporterConfigNode } from '../../src/platform/node/types'; -import { getExportTraceServiceRequestProto } from '../../src/platform/node/utilWithJsonProto'; -import * as collectorTypes from '../../src/types'; - -import { - ensureExportTraceServiceRequestIsSet, - ensureProtoSpanIsCorrect, - mockedReadableSpan, -} from '../helper'; - -const fakeRequest = { - end: function () {}, - on: function () {}, - write: function () {}, -}; - -const mockRes = { - statusCode: 200, -}; - -const mockResError = { - statusCode: 400, -}; - -describe('CollectorExporter - node with proto over http', () => { - let collectorExporter: CollectorTraceExporter; - let collectorExporterConfig: CollectorExporterConfigNode; - let spyRequest: sinon.SinonSpy; - let spyWrite: sinon.SinonSpy; - let spans: ReadableSpan[]; - describe('export', () => { - beforeEach(() => { - spyRequest = sinon.stub(http, 'request').returns(fakeRequest as any); - spyWrite = sinon.stub(fakeRequest, 'write'); - collectorExporterConfig = { - headers: { - foo: 'bar', - }, - protocolNode: CollectorProtocolNode.HTTP_PROTO, - hostname: 'foo', - logger: new core.NoopLogger(), - serviceName: 'bar', - attributes: {}, - url: 'http://foo.bar.com', - }; - collectorExporter = new CollectorTraceExporter(collectorExporterConfig); - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); - }); - afterEach(() => { - spyRequest.restore(); - spyWrite.restore(); - }); - - it('should open the connection', done => { - collectorExporter.export(spans, () => {}); - - setTimeout(() => { - const args = spyRequest.args[0]; - const options = args[0]; - - assert.strictEqual(options.hostname, 'foo.bar.com'); - assert.strictEqual(options.method, 'POST'); - assert.strictEqual(options.path, '/'); - done(); - }); - }); - - it('should set custom headers', done => { - collectorExporter.export(spans, () => {}); - - setTimeout(() => { - const args = spyRequest.args[0]; - const options = args[0]; - assert.strictEqual(options.headers['foo'], 'bar'); - done(); - }); - }); - - it('should successfully send the spans', done => { - collectorExporter.export(spans, () => {}); - - setTimeout(() => { - const writeArgs = spyWrite.args[0]; - const ExportTraceServiceRequestProto = getExportTraceServiceRequestProto(); - const data = ExportTraceServiceRequestProto?.decode(writeArgs[0]); - const json = data?.toJSON() as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; - const span1 = - json.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; - assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); - if (span1) { - ensureProtoSpanIsCorrect(span1); - } - - ensureExportTraceServiceRequestIsSet(json); - - done(); - }); - }); - - it('should log the successful message', done => { - const spyLoggerDebug = sinon.stub(collectorExporter.logger, 'debug'); - const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); - - const responseSpy = sinon.spy(); - collectorExporter.export(spans, responseSpy); - - setTimeout(() => { - const args = spyRequest.args[0]; - const callback = args[1]; - callback(mockRes); - setTimeout(() => { - const response: any = spyLoggerDebug.args[1][0]; - assert.strictEqual(response, 'statusCode: 200'); - assert.strictEqual(spyLoggerError.args.length, 0); - assert.strictEqual(responseSpy.args[0][0], 0); - done(); - }); - }); - }); - - it('should log the error message', done => { - const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); - - const responseSpy = sinon.spy(); - collectorExporter.export(spans, responseSpy); - - setTimeout(() => { - const args = spyRequest.args[0]; - const callback = args[1]; - callback(mockResError); - setTimeout(() => { - const response: any = spyLoggerError.args[0][0]; - assert.strictEqual(response, 'statusCode: 400'); - - assert.strictEqual(responseSpy.args[0][0], 1); - done(); - }); - }); - }); - }); - describe('CollectorTraceExporter - node (getDefaultUrl)', () => { - it('should default to localhost', done => { - const collectorExporter = new CollectorTraceExporter({ - protocolNode: CollectorProtocolNode.HTTP_PROTO, - }); - setTimeout(() => { - assert.strictEqual( - collectorExporter['url'], - 'http://localhost:55681/v1/trace' - ); - done(); - }); - }); - - it('should keep the URL if included', done => { - const url = 'http://foo.bar.com'; - const collectorExporter = new CollectorTraceExporter({ url }); - setTimeout(() => { - assert.strictEqual(collectorExporter['url'], url); - done(); - }); - }); - }); -}); diff --git a/packages/opentelemetry-exporter-collector/test/node/CollectorMetricExporter.test.ts b/packages/opentelemetry-exporter-collector/test/node/CollectorMetricExporter.test.ts index 18f3b7a4441..372fe441335 100644 --- a/packages/opentelemetry-exporter-collector/test/node/CollectorMetricExporter.test.ts +++ b/packages/opentelemetry-exporter-collector/test/node/CollectorMetricExporter.test.ts @@ -14,124 +14,81 @@ * limitations under the License. */ -import * as protoLoader from '@grpc/proto-loader'; -import * as grpc from 'grpc'; -import * as path from 'path'; -import * as fs from 'fs'; - +import { ConsoleLogger, LogLevel } from '@opentelemetry/core'; +import * as core from '@opentelemetry/core'; +import * as http from 'http'; import * as assert from 'assert'; import * as sinon from 'sinon'; import { CollectorMetricExporter } from '../../src/platform/node'; +import { CollectorExporterConfigBase } from '../../src/types'; import * as collectorTypes from '../../src/types'; -import { MetricRecord } from '@opentelemetry/metrics'; + import { mockCounter, mockObserver, mockHistogram, - ensureExportedCounterIsCorrect, - ensureExportedObserverIsCorrect, - ensureMetadataIsCorrect, - ensureResourceIsCorrect, - ensureExportedHistogramIsCorrect, - ensureExportedValueRecorderIsCorrect, + ensureExportMetricsServiceRequestIsSet, + ensureCounterIsCorrect, mockValueRecorder, + ensureValueRecorderIsCorrect, + ensureHistogramIsCorrect, + ensureObserverIsCorrect, } from '../helper'; -import { ConsoleLogger, LogLevel } from '@opentelemetry/core'; -import { CollectorProtocolNode } from '../../src'; - -const metricsServiceProtoPath = - 'opentelemetry/proto/collector/metrics/v1/metrics_service.proto'; -const includeDirs = [path.resolve(__dirname, '../../src/platform/node/protos')]; +import { MetricRecord } from '@opentelemetry/metrics'; -const address = 'localhost:1501'; +const fakeRequest = { + end: function () {}, + on: function () {}, + write: function () {}, +}; -type TestParams = { - useTLS?: boolean; - metadata?: grpc.Metadata; +const mockRes = { + statusCode: 200, }; -const metadata = new grpc.Metadata(); -metadata.set('k', 'v'); - -const testCollectorMetricExporter = (params: TestParams) => - describe(`CollectorMetricExporter - node ${ - params.useTLS ? 'with' : 'without' - } TLS, ${params.metadata ? 'with' : 'without'} metadata`, () => { - let collectorExporter: CollectorMetricExporter; - let server: grpc.Server; - let exportedData: - | collectorTypes.opentelemetryProto.metrics.v1.ResourceMetrics[] - | undefined; - let metrics: MetricRecord[]; - let reqMetadata: grpc.Metadata | undefined; - - before(done => { - server = new grpc.Server(); - protoLoader - .load(metricsServiceProtoPath, { - keepCase: false, - longs: String, - enums: String, - defaults: true, - oneofs: true, - includeDirs, - }) - .then((packageDefinition: protoLoader.PackageDefinition) => { - const packageObject: any = grpc.loadPackageDefinition( - packageDefinition - ); - server.addService( - packageObject.opentelemetry.proto.collector.metrics.v1 - .MetricsService.service, - { - Export: (data: { - request: collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest; - metadata: grpc.Metadata; - }) => { - try { - exportedData = data.request.resourceMetrics; - reqMetadata = data.metadata; - } catch (e) { - exportedData = undefined; - } - }, - } - ); - const credentials = params.useTLS - ? grpc.ServerCredentials.createSsl( - fs.readFileSync('./test/certs/ca.crt'), - [ - { - cert_chain: fs.readFileSync('./test/certs/server.crt'), - private_key: fs.readFileSync('./test/certs/server.key'), - }, - ] - ) - : grpc.ServerCredentials.createInsecure(); - server.bind(address, credentials); - server.start(); - done(); - }); - }); +const mockResError = { + statusCode: 400, +}; - after(() => { - server.forceShutdown(); - }); +const address = 'localhost:1501'; - beforeEach(done => { - const credentials = params.useTLS - ? grpc.credentials.createSsl( - fs.readFileSync('./test/certs/ca.crt'), - fs.readFileSync('./test/certs/client.key'), - fs.readFileSync('./test/certs/client.crt') - ) - : undefined; +describe('CollectorMetricExporter - node with json over http', () => { + let collectorExporter: CollectorMetricExporter; + let collectorExporterConfig: CollectorExporterConfigBase; + let spyRequest: sinon.SinonSpy; + let spyWrite: sinon.SinonSpy; + let metrics: MetricRecord[]; + describe('instance', () => { + it('should warn about metadata when using json', () => { + const metadata = 'foo'; + const logger = new ConsoleLogger(LogLevel.DEBUG); + const spyLoggerWarn = sinon.stub(logger, 'warn'); collectorExporter = new CollectorMetricExporter({ - url: address, - credentials, + logger, serviceName: 'basic-service', - metadata: params.metadata, - }); + url: address, + metadata, + } as any); + const args = spyLoggerWarn.args[0]; + assert.strictEqual(args[0], 'Metadata cannot be set when using http'); + }); + }); + + describe('export', () => { + beforeEach(() => { + spyRequest = sinon.stub(http, 'request').returns(fakeRequest as any); + spyWrite = sinon.stub(fakeRequest, 'write'); + collectorExporterConfig = { + headers: { + foo: 'bar', + }, + hostname: 'foo', + logger: new core.NoopLogger(), + serviceName: 'bar', + attributes: {}, + url: 'http://foo.bar.com', + }; + collectorExporter = new CollectorMetricExporter(collectorExporterConfig); // Overwrites the start time to make tests consistent Object.defineProperty(collectorExporter, '_startTime', { value: 1592602232694000000, @@ -141,114 +98,149 @@ const testCollectorMetricExporter = (params: TestParams) => metrics.push(mockObserver()); metrics.push(mockHistogram()); metrics.push(mockValueRecorder()); - metrics[0].aggregator.update(1); - metrics[1].aggregator.update(3); metrics[1].aggregator.update(6); - metrics[2].aggregator.update(7); metrics[2].aggregator.update(14); metrics[3].aggregator.update(5); - done(); }); - afterEach(() => { - exportedData = undefined; - reqMetadata = undefined; + spyRequest.restore(); + spyWrite.restore(); }); - describe('instance', () => { - it('should warn about headers when using grpc', () => { - const logger = new ConsoleLogger(LogLevel.DEBUG); - const spyLoggerWarn = sinon.stub(logger, 'warn'); - collectorExporter = new CollectorMetricExporter({ - logger, - serviceName: 'basic-service', - url: address, - headers: { - foo: 'bar', - }, - }); - const args = spyLoggerWarn.args[0]; - assert.strictEqual(args[0], 'Headers cannot be set when using grpc'); + it('should open the connection', done => { + collectorExporter.export(metrics, () => {}); + + setTimeout(() => { + const args = spyRequest.args[0]; + const options = args[0]; + + assert.strictEqual(options.hostname, 'foo.bar.com'); + assert.strictEqual(options.method, 'POST'); + assert.strictEqual(options.path, '/'); + done(); }); - it('should warn about metadata when using json', () => { - const metadata = new grpc.Metadata(); - metadata.set('k', 'v'); - const logger = new ConsoleLogger(LogLevel.DEBUG); - const spyLoggerWarn = sinon.stub(logger, 'warn'); - collectorExporter = new CollectorMetricExporter({ - logger, - serviceName: 'basic-service', - url: address, - metadata, - protocolNode: CollectorProtocolNode.HTTP_JSON, - }); - const args = spyLoggerWarn.args[0]; - assert.strictEqual(args[0], 'Metadata cannot be set when using http'); + }); + + it('should set custom headers', done => { + collectorExporter.export(metrics, () => {}); + + setTimeout(() => { + const args = spyRequest.args[0]; + const options = args[0]; + assert.strictEqual(options.headers['foo'], 'bar'); + done(); }); }); - describe('export', () => { - it('should export metrics', done => { - const responseSpy = sinon.spy(); - collectorExporter.export(metrics, responseSpy); + it('should successfully send metrics', done => { + collectorExporter.export(metrics, () => {}); + + setTimeout(() => { + const writeArgs = spyWrite.args[0]; + const json = JSON.parse( + writeArgs[0] + ) as collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest; + const metric1 = + json.resourceMetrics[0].instrumentationLibraryMetrics[0].metrics[0]; + const metric2 = + json.resourceMetrics[1].instrumentationLibraryMetrics[0].metrics[0]; + const metric3 = + json.resourceMetrics[2].instrumentationLibraryMetrics[0].metrics[0]; + const metric4 = + json.resourceMetrics[3].instrumentationLibraryMetrics[0].metrics[0]; + assert.ok(typeof metric1 !== 'undefined', "counter doesn't exist"); + ensureCounterIsCorrect( + metric1, + core.hrTimeToNanoseconds(metrics[0].aggregator.toPoint().timestamp) + ); + assert.ok(typeof metric2 !== 'undefined', "observer doesn't exist"); + ensureObserverIsCorrect( + metric2, + core.hrTimeToNanoseconds(metrics[1].aggregator.toPoint().timestamp) + ); + assert.ok(typeof metric3 !== 'undefined', "histogram doesn't exist"); + ensureHistogramIsCorrect( + metric3, + core.hrTimeToNanoseconds(metrics[2].aggregator.toPoint().timestamp) + ); + assert.ok( + typeof metric4 !== 'undefined', + "value recorder doesn't exist" + ); + ensureValueRecorderIsCorrect( + metric4, + core.hrTimeToNanoseconds(metrics[3].aggregator.toPoint().timestamp) + ); + + ensureExportMetricsServiceRequestIsSet(json); + + done(); + }); + }); + + it('should log the successful message', done => { + const spyLoggerDebug = sinon.stub(collectorExporter.logger, 'debug'); + const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); + + const responseSpy = sinon.spy(); + collectorExporter.export(metrics, responseSpy); + + setTimeout(() => { + const args = spyRequest.args[0]; + const callback = args[1]; + callback(mockRes); setTimeout(() => { - assert.ok( - typeof exportedData !== 'undefined', - 'resource' + " doesn't exist" - ); - let resource; - if (exportedData) { - resource = exportedData[0].resource; - const counter = - exportedData[0].instrumentationLibraryMetrics[0].metrics[0]; - const observer = - exportedData[1].instrumentationLibraryMetrics[0].metrics[0]; - const histogram = - exportedData[2].instrumentationLibraryMetrics[0].metrics[0]; - const recorder = - exportedData[3].instrumentationLibraryMetrics[0].metrics[0]; - ensureExportedCounterIsCorrect(counter); - ensureExportedObserverIsCorrect(observer); - ensureExportedHistogramIsCorrect(histogram); - ensureExportedValueRecorderIsCorrect(recorder); - assert.ok( - typeof resource !== 'undefined', - "resource doesn't exist" - ); - if (resource) { - ensureResourceIsCorrect(resource, true); - } - } - if (params.metadata && reqMetadata) { - ensureMetadataIsCorrect(reqMetadata, params.metadata); - } + const response: any = spyLoggerDebug.args[1][0]; + assert.strictEqual(response, 'statusCode: 200'); + assert.strictEqual(spyLoggerError.args.length, 0); + assert.strictEqual(responseSpy.args[0][0], 0); done(); - }, 500); + }); }); }); - }); -describe('CollectorMetricExporter - node (getDefaultUrl)', () => { - it('should default to localhost', done => { - const collectorExporter = new CollectorMetricExporter({}); - setTimeout(() => { - assert.strictEqual(collectorExporter['url'], 'localhost:55680'); - done(); + it('should log the error message', done => { + const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); + + const responseSpy = sinon.spy(); + collectorExporter.export(metrics, responseSpy); + + setTimeout(() => { + const args = spyRequest.args[0]; + const callback = args[1]; + callback(mockResError); + setTimeout(() => { + const response: any = spyLoggerError.args[0][0]; + assert.strictEqual(response, 'statusCode: 400'); + + assert.strictEqual(responseSpy.args[0][0], 1); + done(); + }); + }); }); }); - it('should keep the URL if included', done => { - const url = 'http://foo.bar.com'; - const collectorExporter = new CollectorMetricExporter({ url }); - setTimeout(() => { - assert.strictEqual(collectorExporter['url'], url); - done(); + describe('CollectorMetricExporter - node (getDefaultUrl)', () => { + it('should default to localhost', done => { + const collectorExporter = new CollectorMetricExporter(); + setTimeout(() => { + assert.strictEqual( + collectorExporter['url'], + 'http://localhost:55681/v1/metrics' + ); + done(); + }); + }); + + it('should keep the URL if included', done => { + const url = 'http://foo.bar.com'; + const collectorExporter = new CollectorMetricExporter({ url }); + setTimeout(() => { + assert.strictEqual(collectorExporter['url'], url); + done(); + }); }); }); }); - -testCollectorMetricExporter({ useTLS: true }); -testCollectorMetricExporter({ useTLS: false }); -testCollectorMetricExporter({ metadata }); diff --git a/packages/opentelemetry-exporter-collector/test/node/CollectorTraceExporter.test.ts b/packages/opentelemetry-exporter-collector/test/node/CollectorTraceExporter.test.ts index 4c70e068ff3..c90dce9c8eb 100644 --- a/packages/opentelemetry-exporter-collector/test/node/CollectorTraceExporter.test.ts +++ b/packages/opentelemetry-exporter-collector/test/node/CollectorTraceExporter.test.ts @@ -14,216 +14,188 @@ * limitations under the License. */ -import * as protoLoader from '@grpc/proto-loader'; import { ConsoleLogger, LogLevel } from '@opentelemetry/core'; -import { - BasicTracerProvider, - SimpleSpanProcessor, -} from '@opentelemetry/tracing'; - +import * as core from '@opentelemetry/core'; +import { ReadableSpan } from '@opentelemetry/tracing'; +import * as http from 'http'; import * as assert from 'assert'; -import * as fs from 'fs'; -import * as grpc from 'grpc'; -import * as path from 'path'; import * as sinon from 'sinon'; -import { CollectorProtocolNode } from '../../src'; import { CollectorTraceExporter } from '../../src/platform/node'; +import { CollectorExporterConfigBase } from '../../src/types'; import * as collectorTypes from '../../src/types'; import { - ensureExportedSpanIsCorrect, - ensureMetadataIsCorrect, - ensureResourceIsCorrect, + ensureExportTraceServiceRequestIsSet, + ensureSpanIsCorrect, mockedReadableSpan, } from '../helper'; -const traceServiceProtoPath = - 'opentelemetry/proto/collector/trace/v1/trace_service.proto'; -const includeDirs = [path.resolve(__dirname, '../../src/platform/node/protos')]; +const fakeRequest = { + end: function () {}, + on: function () {}, + write: function () {}, +}; -const address = 'localhost:1501'; +const mockRes = { + statusCode: 200, +}; -type TestParams = { - useTLS?: boolean; - metadata?: grpc.Metadata; +const mockResError = { + statusCode: 400, }; +const address = 'localhost:1501'; -const metadata = new grpc.Metadata(); -metadata.set('k', 'v'); - -const testCollectorExporter = (params: TestParams) => - describe(`CollectorTraceExporter - node ${ - params.useTLS ? 'with' : 'without' - } TLS, ${params.metadata ? 'with' : 'without'} metadata`, () => { - let collectorExporter: CollectorTraceExporter; - let server: grpc.Server; - let exportedData: - | collectorTypes.opentelemetryProto.trace.v1.ResourceSpans - | undefined; - let reqMetadata: grpc.Metadata | undefined; - - before(done => { - server = new grpc.Server(); - protoLoader - .load(traceServiceProtoPath, { - keepCase: false, - longs: String, - enums: String, - defaults: true, - oneofs: true, - includeDirs, - }) - .then((packageDefinition: protoLoader.PackageDefinition) => { - const packageObject: any = grpc.loadPackageDefinition( - packageDefinition - ); - server.addService( - packageObject.opentelemetry.proto.collector.trace.v1.TraceService - .service, - { - Export: (data: { - request: collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; - metadata: grpc.Metadata; - }) => { - try { - exportedData = data.request.resourceSpans[0]; - reqMetadata = data.metadata; - } catch (e) { - exportedData = undefined; - } - }, - } - ); - const credentials = params.useTLS - ? grpc.ServerCredentials.createSsl( - fs.readFileSync('./test/certs/ca.crt'), - [ - { - cert_chain: fs.readFileSync('./test/certs/server.crt'), - private_key: fs.readFileSync('./test/certs/server.key'), - }, - ] - ) - : grpc.ServerCredentials.createInsecure(); - server.bind(address, credentials); - server.start(); - done(); - }); +describe('CollectorTraceExporter - node with json over http', () => { + let collectorExporter: CollectorTraceExporter; + let collectorExporterConfig: CollectorExporterConfigBase; + let spyRequest: sinon.SinonSpy; + let spyWrite: sinon.SinonSpy; + let spans: ReadableSpan[]; + describe('instance', () => { + it('should warn about metadata when using json', () => { + const metadata = 'foo'; + const logger = new ConsoleLogger(LogLevel.DEBUG); + const spyLoggerWarn = sinon.stub(logger, 'warn'); + collectorExporter = new CollectorTraceExporter({ + logger, + serviceName: 'basic-service', + metadata, + url: address, + } as any); + const args = spyLoggerWarn.args[0]; + assert.strictEqual(args[0], 'Metadata cannot be set when using http'); }); + }); - after(() => { - server.forceShutdown(); + describe('export', () => { + beforeEach(() => { + spyRequest = sinon.stub(http, 'request').returns(fakeRequest as any); + spyWrite = sinon.stub(fakeRequest, 'write'); + collectorExporterConfig = { + headers: { + foo: 'bar', + }, + hostname: 'foo', + logger: new core.NoopLogger(), + serviceName: 'bar', + attributes: {}, + url: 'http://foo.bar.com', + }; + collectorExporter = new CollectorTraceExporter(collectorExporterConfig); + spans = []; + spans.push(Object.assign({}, mockedReadableSpan)); + }); + afterEach(() => { + spyRequest.restore(); + spyWrite.restore(); }); - beforeEach(done => { - const credentials = params.useTLS - ? grpc.credentials.createSsl( - fs.readFileSync('./test/certs/ca.crt'), - fs.readFileSync('./test/certs/client.key'), - fs.readFileSync('./test/certs/client.crt') - ) - : undefined; - collectorExporter = new CollectorTraceExporter({ - serviceName: 'basic-service', - url: address, - credentials, - metadata: params.metadata, - }); + it('should open the connection', done => { + collectorExporter.export(spans, () => {}); + + setTimeout(() => { + const args = spyRequest.args[0]; + const options = args[0]; - const provider = new BasicTracerProvider(); - provider.addSpanProcessor(new SimpleSpanProcessor(collectorExporter)); - done(); + assert.strictEqual(options.hostname, 'foo.bar.com'); + assert.strictEqual(options.method, 'POST'); + assert.strictEqual(options.path, '/'); + done(); + }); }); - afterEach(() => { - exportedData = undefined; - reqMetadata = undefined; + it('should set custom headers', done => { + collectorExporter.export(spans, () => {}); + + setTimeout(() => { + const args = spyRequest.args[0]; + const options = args[0]; + assert.strictEqual(options.headers['foo'], 'bar'); + done(); + }); }); - describe('instance', () => { - it('should warn about headers when using grpc', () => { - const logger = new ConsoleLogger(LogLevel.DEBUG); - const spyLoggerWarn = sinon.stub(logger, 'warn'); - collectorExporter = new CollectorTraceExporter({ - logger, - serviceName: 'basic-service', - url: address, - headers: { - foo: 'bar', - }, - }); - const args = spyLoggerWarn.args[0]; - assert.strictEqual(args[0], 'Headers cannot be set when using grpc'); + it('should successfully send the spans', done => { + collectorExporter.export(spans, () => {}); + + setTimeout(() => { + const writeArgs = spyWrite.args[0]; + const json = JSON.parse( + writeArgs[0] + ) as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; + const span1 = + json.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; + assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); + if (span1) { + ensureSpanIsCorrect(span1); + } + + ensureExportTraceServiceRequestIsSet(json); + + done(); }); - it('should warn about metadata when using json', () => { - const metadata = new grpc.Metadata(); - metadata.set('k', 'v'); - const logger = new ConsoleLogger(LogLevel.DEBUG); - const spyLoggerWarn = sinon.stub(logger, 'warn'); - collectorExporter = new CollectorTraceExporter({ - logger, - serviceName: 'basic-service', - url: address, - metadata, - protocolNode: CollectorProtocolNode.HTTP_JSON, + }); + + it('should log the successful message', done => { + const spyLoggerDebug = sinon.stub(collectorExporter.logger, 'debug'); + const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); + + const responseSpy = sinon.spy(); + collectorExporter.export(spans, responseSpy); + + setTimeout(() => { + const args = spyRequest.args[0]; + const callback = args[1]; + callback(mockRes); + setTimeout(() => { + const response: any = spyLoggerDebug.args[1][0]; + assert.strictEqual(response, 'statusCode: 200'); + assert.strictEqual(spyLoggerError.args.length, 0); + assert.strictEqual(responseSpy.args[0][0], 0); + done(); }); - const args = spyLoggerWarn.args[0]; - assert.strictEqual(args[0], 'Metadata cannot be set when using http'); }); }); - describe('export', () => { - it('should export spans', done => { - const responseSpy = sinon.spy(); - const spans = [Object.assign({}, mockedReadableSpan)]; - collectorExporter.export(spans, responseSpy); + it('should log the error message', done => { + const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); + + const responseSpy = sinon.spy(); + collectorExporter.export(spans, responseSpy); + + setTimeout(() => { + const args = spyRequest.args[0]; + const callback = args[1]; + callback(mockResError); setTimeout(() => { - assert.ok( - typeof exportedData !== 'undefined', - 'resource' + " doesn't exist" - ); - let spans; - let resource; - if (exportedData) { - spans = exportedData.instrumentationLibrarySpans[0].spans; - resource = exportedData.resource; - ensureExportedSpanIsCorrect(spans[0], true); - - assert.ok( - typeof resource !== 'undefined', - "resource doesn't exist" - ); - if (resource) { - ensureResourceIsCorrect(resource, true); - } - } - if (params.metadata && reqMetadata) { - ensureMetadataIsCorrect(reqMetadata, params.metadata); - } + const response: any = spyLoggerError.args[0][0]; + assert.strictEqual(response, 'statusCode: 400'); + + assert.strictEqual(responseSpy.args[0][0], 1); done(); - }, 200); + }); }); }); }); - -describe('CollectorTraceExporter - node (getDefaultUrl)', () => { - it('should default to localhost', done => { - const collectorExporter = new CollectorTraceExporter({}); - setTimeout(() => { - assert.strictEqual(collectorExporter['url'], 'localhost:55680'); - done(); + describe('CollectorTraceExporter - node (getDefaultUrl)', () => { + it('should default to localhost', done => { + const collectorExporter = new CollectorTraceExporter(); + setTimeout(() => { + assert.strictEqual( + collectorExporter['url'], + 'http://localhost:55681/v1/trace' + ); + done(); + }); }); - }); - it('should keep the URL if included', done => { - const url = 'http://foo.bar.com'; - const collectorExporter = new CollectorTraceExporter({ url }); - setTimeout(() => { - assert.strictEqual(collectorExporter['url'], url); - done(); + + it('should keep the URL if included', done => { + const url = 'http://foo.bar.com'; + const collectorExporter = new CollectorTraceExporter({ url }); + setTimeout(() => { + assert.strictEqual(collectorExporter['url'], url); + done(); + }); }); }); }); - -testCollectorExporter({ useTLS: true }); -testCollectorExporter({ useTLS: false }); -testCollectorExporter({ metadata }); diff --git a/packages/opentelemetry-exporter-collector/test/node/CollectorTraceExporterWithJson.test.ts b/packages/opentelemetry-exporter-collector/test/node/CollectorTraceExporterWithJson.test.ts deleted file mode 100644 index bb9cd8f0e98..00000000000 --- a/packages/opentelemetry-exporter-collector/test/node/CollectorTraceExporterWithJson.test.ts +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as core from '@opentelemetry/core'; -import { ReadableSpan } from '@opentelemetry/tracing'; -import * as http from 'http'; -import * as assert from 'assert'; -import * as sinon from 'sinon'; -import { CollectorProtocolNode } from '../../src/enums'; -import { CollectorTraceExporter } from '../../src/platform/node'; -import { CollectorExporterConfigNode } from '../../src/platform/node/types'; -import * as collectorTypes from '../../src/types'; - -import { - ensureExportTraceServiceRequestIsSet, - ensureSpanIsCorrect, - mockedReadableSpan, -} from '../helper'; - -const fakeRequest = { - end: function () {}, - on: function () {}, - write: function () {}, -}; - -const mockRes = { - statusCode: 200, -}; - -const mockResError = { - statusCode: 400, -}; - -describe('CollectorTraceExporter - node with json over http', () => { - let collectorExporter: CollectorTraceExporter; - let collectorExporterConfig: CollectorExporterConfigNode; - let spyRequest: sinon.SinonSpy; - let spyWrite: sinon.SinonSpy; - let spans: ReadableSpan[]; - describe('export', () => { - beforeEach(() => { - spyRequest = sinon.stub(http, 'request').returns(fakeRequest as any); - spyWrite = sinon.stub(fakeRequest, 'write'); - collectorExporterConfig = { - headers: { - foo: 'bar', - }, - protocolNode: CollectorProtocolNode.HTTP_JSON, - hostname: 'foo', - logger: new core.NoopLogger(), - serviceName: 'bar', - attributes: {}, - url: 'http://foo.bar.com', - }; - collectorExporter = new CollectorTraceExporter(collectorExporterConfig); - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); - }); - afterEach(() => { - spyRequest.restore(); - spyWrite.restore(); - }); - - it('should open the connection', done => { - collectorExporter.export(spans, () => {}); - - setTimeout(() => { - const args = spyRequest.args[0]; - const options = args[0]; - - assert.strictEqual(options.hostname, 'foo.bar.com'); - assert.strictEqual(options.method, 'POST'); - assert.strictEqual(options.path, '/'); - done(); - }); - }); - - it('should set custom headers', done => { - collectorExporter.export(spans, () => {}); - - setTimeout(() => { - const args = spyRequest.args[0]; - const options = args[0]; - assert.strictEqual(options.headers['foo'], 'bar'); - done(); - }); - }); - - it('should successfully send the spans', done => { - collectorExporter.export(spans, () => {}); - - setTimeout(() => { - const writeArgs = spyWrite.args[0]; - const json = JSON.parse( - writeArgs[0] - ) as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; - const span1 = - json.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; - assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); - if (span1) { - ensureSpanIsCorrect(span1); - } - - ensureExportTraceServiceRequestIsSet(json); - - done(); - }); - }); - - it('should log the successful message', done => { - const spyLoggerDebug = sinon.stub(collectorExporter.logger, 'debug'); - const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); - - const responseSpy = sinon.spy(); - collectorExporter.export(spans, responseSpy); - - setTimeout(() => { - const args = spyRequest.args[0]; - const callback = args[1]; - callback(mockRes); - setTimeout(() => { - const response: any = spyLoggerDebug.args[1][0]; - assert.strictEqual(response, 'statusCode: 200'); - assert.strictEqual(spyLoggerError.args.length, 0); - assert.strictEqual(responseSpy.args[0][0], 0); - done(); - }); - }); - }); - - it('should log the error message', done => { - const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); - - const responseSpy = sinon.spy(); - collectorExporter.export(spans, responseSpy); - - setTimeout(() => { - const args = spyRequest.args[0]; - const callback = args[1]; - callback(mockResError); - setTimeout(() => { - const response: any = spyLoggerError.args[0][0]; - assert.strictEqual(response, 'statusCode: 400'); - - assert.strictEqual(responseSpy.args[0][0], 1); - done(); - }); - }); - }); - }); - describe('CollectorTraceExporter - node (getDefaultUrl)', () => { - it('should default to localhost', done => { - const collectorExporter = new CollectorTraceExporter({ - protocolNode: CollectorProtocolNode.HTTP_JSON, - }); - setTimeout(() => { - assert.strictEqual( - collectorExporter['url'], - 'http://localhost:55681/v1/trace' - ); - done(); - }); - }); - - it('should keep the URL if included', done => { - const url = 'http://foo.bar.com'; - const collectorExporter = new CollectorTraceExporter({ url }); - setTimeout(() => { - assert.strictEqual(collectorExporter['url'], url); - done(); - }); - }); - }); -}); diff --git a/packages/opentelemetry-exporter-jaeger/README.md b/packages/opentelemetry-exporter-jaeger/README.md index 054678da8b5..a00da2606c1 100644 --- a/packages/opentelemetry-exporter-jaeger/README.md +++ b/packages/opentelemetry-exporter-jaeger/README.md @@ -1,4 +1,4 @@ -# OpenTelemetry Jaeger Trace Exporter +# OpenTelemetry Jaeger Trace Exporter for Node.js [![Gitter chat][gitter-image]][gitter-url] [![NPM Published Version][npm-img]][npm-url] @@ -83,8 +83,8 @@ tracer.addSpanProcessor(new BatchSpanProcessor(exporter)); You can use built-in `SimpleSpanProcessor` or `BatchSpanProcessor` or write your own. -- [SimpleSpanProcessor](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/sdk-tracing.md#simple-processor): The implementation of `SpanProcessor` that passes ended span directly to the configured `SpanExporter`. -- [BatchSpanProcessor](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/sdk-tracing.md#batching-processor): The implementation of the `SpanProcessor` that batches ended spans and pushes them to the configured `SpanExporter`. It is recommended to use this `SpanProcessor` for better performance and optimization. +- [SimpleSpanProcessor](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/sdk.md#simple-processor): The implementation of `SpanProcessor` that passes ended span directly to the configured `SpanExporter`. +- [BatchSpanProcessor](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/sdk.md#batching-processor): The implementation of the `SpanProcessor` that batches ended spans and pushes them to the configured `SpanExporter`. It is recommended to use this `SpanProcessor` for better performance and optimization. ## Useful links diff --git a/packages/opentelemetry-exporter-jaeger/package.json b/packages/opentelemetry-exporter-jaeger/package.json index fb2ca5b650d..ada8a6cbd25 100644 --- a/packages/opentelemetry-exporter-jaeger/package.json +++ b/packages/opentelemetry-exporter-jaeger/package.json @@ -43,7 +43,7 @@ }, "devDependencies": { "@opentelemetry/resources": "^0.10.2", - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "codecov": "3.7.2", "gts": "2.0.2", diff --git a/packages/opentelemetry-exporter-jaeger/src/transform.ts b/packages/opentelemetry-exporter-jaeger/src/transform.ts index 7daa6dac772..17c1cbfbbf8 100644 --- a/packages/opentelemetry-exporter-jaeger/src/transform.ts +++ b/packages/opentelemetry-exporter-jaeger/src/transform.ts @@ -15,7 +15,6 @@ */ import { Link, CanonicalCode, SpanKind } from '@opentelemetry/api'; -import { Resource } from '@opentelemetry/resources'; import { ReadableSpan } from '@opentelemetry/tracing'; import { hrTimeToMilliseconds, @@ -65,10 +64,10 @@ export function spanToThrift(span: ReadableSpan): ThriftSpan { if (span.kind !== undefined) { tags.push({ key: 'span.kind', value: SpanKind[span.kind] }); } - Object.keys(span.resource.labels).forEach(name => + Object.keys(span.resource.attributes).forEach(name => tags.push({ key: name, - value: toTagValue(span.resource.labels[name]), + value: toTagValue(span.resource.attributes[name]), }) ); diff --git a/packages/opentelemetry-exporter-prometheus/package.json b/packages/opentelemetry-exporter-prometheus/package.json index 2823636de7e..5c7de77d312 100644 --- a/packages/opentelemetry-exporter-prometheus/package.json +++ b/packages/opentelemetry-exporter-prometheus/package.json @@ -41,7 +41,7 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "codecov": "3.7.2", "gts": "2.0.2", @@ -55,7 +55,6 @@ "dependencies": { "@opentelemetry/api": "^0.10.2", "@opentelemetry/core": "^0.10.2", - "@opentelemetry/metrics": "^0.10.2", - "prom-client": "^11.5.3" + "@opentelemetry/metrics": "^0.10.2" } } diff --git a/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts b/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts new file mode 100644 index 00000000000..3d6035cc4f4 --- /dev/null +++ b/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts @@ -0,0 +1,184 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as api from '@opentelemetry/api'; +import { ExportResult, NoopLogger } from '@opentelemetry/core'; +import { MetricExporter, MetricRecord } from '@opentelemetry/metrics'; +import { createServer, IncomingMessage, Server, ServerResponse } from 'http'; +import * as url from 'url'; +import { ExporterConfig } from './export/types'; +import { PrometheusSerializer } from './PrometheusSerializer'; +import { PrometheusLabelsBatcher } from './PrometheusLabelsBatcher'; + +export class PrometheusExporter implements MetricExporter { + static readonly DEFAULT_OPTIONS = { + port: 9464, + startServer: false, + endpoint: '/metrics', + prefix: '', + }; + + private readonly _logger: api.Logger; + private readonly _port: number; + private readonly _endpoint: string; + private readonly _server: Server; + private readonly _prefix?: string; + private _serializer: PrometheusSerializer; + private _batcher = new PrometheusLabelsBatcher(); + + // This will be required when histogram is implemented. Leaving here so it is not forgotten + // Histogram cannot have a label named 'le' + // private static readonly RESERVED_HISTOGRAM_LABEL = 'le'; + + /** + * Constructor + * @param config Exporter configuration + * @param callback Callback to be called after a server was started + */ + constructor(config: ExporterConfig = {}, callback?: () => void) { + this._logger = config.logger || new NoopLogger(); + this._port = config.port || PrometheusExporter.DEFAULT_OPTIONS.port; + this._prefix = config.prefix || PrometheusExporter.DEFAULT_OPTIONS.prefix; + this._server = createServer(this._requestHandler); + this._serializer = new PrometheusSerializer(this._prefix); + + this._endpoint = ( + config.endpoint || PrometheusExporter.DEFAULT_OPTIONS.endpoint + ).replace(/^([^/])/, '/$1'); + + if (config.startServer || PrometheusExporter.DEFAULT_OPTIONS.startServer) { + this.startServer(callback); + } else if (callback) { + callback(); + } + } + + /** + * Saves the current values of all exported {@link MetricRecord}s so that + * they can be pulled by the Prometheus backend. + * + * In its current state, the exporter saves the current values of all metrics + * when export is called and returns them when the export endpoint is called. + * In the future, this should be a no-op and the exporter should reach into + * the metrics when the export endpoint is called. As there is currently no + * interface to do this, this is our only option. + * + * @param records Metrics to be sent to the prometheus backend + * @param cb result callback to be called on finish + */ + export(records: MetricRecord[], cb: (result: ExportResult) => void) { + if (!this._server) { + // It is conceivable that the _server may not be started as it is an async startup + // However unlikely, if this happens the caller may retry the export + cb(ExportResult.FAILED_RETRYABLE); + return; + } + + this._logger.debug('Prometheus exporter export'); + + for (const record of records) { + this._batcher.process(record); + } + + cb(ExportResult.SUCCESS); + } + + /** + * Shuts down the export server and clears the registry + * + * @param cb called when server is stopped + */ + shutdown(cb?: () => void) { + this.stopServer(cb); + } + + /** + * Stops the Prometheus export server + * @param callback A callback that will be executed once the server is stopped + */ + stopServer(callback?: () => void) { + if (!this._server) { + this._logger.debug( + 'Prometheus stopServer() was called but server was never started.' + ); + if (callback) { + callback(); + } + } else { + this._server.close(() => { + this._logger.debug('Prometheus exporter was stopped'); + if (callback) { + callback(); + } + }); + } + } + + /** + * Starts the Prometheus export server + * + * @param callback called once the server is ready + */ + startServer(callback?: () => void) { + this._server.listen(this._port, () => { + this._logger.debug( + `Prometheus exporter started on port ${this._port} at endpoint ${this._endpoint}` + ); + if (callback) { + callback(); + } + }); + } + + /** + * Request handler used by http library to respond to incoming requests + * for the current state of metrics by the Prometheus backend. + * + * @param request Incoming HTTP request to export server + * @param response HTTP response object used to respond to request + */ + private _requestHandler = ( + request: IncomingMessage, + response: ServerResponse + ) => { + if (url.parse(request.url!).pathname === this._endpoint) { + this._exportMetrics(response); + } else { + this._notFound(response); + } + }; + + /** + * Responds to incoming message with current state of all metrics. + */ + private _exportMetrics = (response: ServerResponse) => { + response.statusCode = 200; + response.setHeader('content-type', 'text/plain'); + if (!this._batcher.hasMetric) { + response.end('# no registered metrics'); + return; + } + response.end(this._serializer.serialize(this._batcher.checkPointSet())); + }; + + /** + * Responds with 404 status code to all requests that do not match the configured endpoint. + */ + private _notFound = (response: ServerResponse) => { + response.statusCode = 404; + response.end(); + }; +} diff --git a/packages/opentelemetry-exporter-prometheus/src/PrometheusLabelsBatcher.ts b/packages/opentelemetry-exporter-prometheus/src/PrometheusLabelsBatcher.ts new file mode 100644 index 00000000000..5d123d4ef45 --- /dev/null +++ b/packages/opentelemetry-exporter-prometheus/src/PrometheusLabelsBatcher.ts @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + MetricRecord, + MetricDescriptor, + AggregatorKind, +} from '@opentelemetry/metrics'; +import { PrometheusCheckpoint } from './types'; + +interface BatcherCheckpoint { + descriptor: MetricDescriptor; + aggregatorKind: AggregatorKind; + records: Map; +} + +export class PrometheusLabelsBatcher { + private _batchMap = new Map(); + + get hasMetric() { + return this._batchMap.size > 0; + } + + process(record: MetricRecord) { + const name = record.descriptor.name; + let item = this._batchMap.get(name); + if (item === undefined) { + item = { + descriptor: record.descriptor, + aggregatorKind: record.aggregator.kind, + records: new Map(), + }; + this._batchMap.set(name, item); + } + const recordMap = item.records; + const labels = Object.keys(record.labels) + .map(k => `${k}=${record.labels[k]}`) + .join(','); + recordMap.set(labels, record); + } + + checkPointSet(): PrometheusCheckpoint[] { + return Array.from(this._batchMap.values()).map( + ({ descriptor, aggregatorKind, records }) => { + return { + descriptor, + aggregatorKind, + records: Array.from(records.values()), + }; + } + ); + } +} diff --git a/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts b/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts new file mode 100644 index 00000000000..7e70eb986f5 --- /dev/null +++ b/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts @@ -0,0 +1,259 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + MetricRecord, + AggregatorKind, + Distribution, + MetricKind, +} from '@opentelemetry/metrics'; +import { PrometheusCheckpoint } from './types'; +import { Labels } from '@opentelemetry/api'; +import { hrTimeToMilliseconds } from '@opentelemetry/core'; + +type PrometheusDataTypeLiteral = + | 'counter' + | 'gauge' + | 'histogram' + | 'summary' + | 'untyped'; + +function escapeString(str: string) { + return str.replace(/\\/g, '\\\\').replace(/\n/g, '\\n'); +} + +function escapeLabelValue(str: string) { + if (typeof str !== 'string') { + str = String(str); + } + return escapeString(str).replace(/"/g, '\\"'); +} + +const invalidCharacterRegex = /[^a-z0-9_]/gi; +/** + * Ensures metric names are valid Prometheus metric names by removing + * characters allowed by OpenTelemetry but disallowed by Prometheus. + * + * https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels + * + * 1. Names must match `[a-zA-Z_:][a-zA-Z0-9_:]*` + * + * 2. Colons are reserved for user defined recording rules. + * They should not be used by exporters or direct instrumentation. + * + * OpenTelemetry metric names are already validated in the Meter when they are created, + * and they match the format `[a-zA-Z][a-zA-Z0-9_.\-]*` which is very close to a valid + * prometheus metric name, so we only need to strip characters valid in OpenTelemetry + * but not valid in prometheus and replace them with '_'. + * + * @param name name to be sanitized + */ +function sanitizePrometheusMetricName(name: string): string { + return name.replace(invalidCharacterRegex, '_'); // replace all invalid characters with '_' +} + +function valueString(value: number) { + if (Number.isNaN(value)) { + return 'Nan'; + } else if (!Number.isFinite(value)) { + if (value < 0) { + return '-Inf'; + } else { + return '+Inf'; + } + } else { + return `${value}`; + } +} + +function toPrometheusType( + metricKind: MetricKind, + aggregatorKind: AggregatorKind +): PrometheusDataTypeLiteral { + switch (aggregatorKind) { + case AggregatorKind.SUM: + if ( + metricKind === MetricKind.COUNTER || + metricKind === MetricKind.SUM_OBSERVER + ) { + return 'counter'; + } + /** MetricKind.UP_DOWN_COUNTER and MetricKind.UP_DOWN_SUM_OBSERVER */ + return 'gauge'; + case AggregatorKind.LAST_VALUE: + return 'gauge'; + case AggregatorKind.DISTRIBUTION: + return 'summary'; + case AggregatorKind.HISTOGRAM: + return 'histogram'; + default: + return 'untyped'; + } +} + +function stringify( + metricName: string, + labels: Labels, + value: number, + timestamp?: number, + additionalLabels?: Labels +) { + let hasLabel = false; + let labelsStr = ''; + + for (const [key, val] of Object.entries(labels)) { + hasLabel = true; + labelsStr += `${labelsStr.length > 0 ? ',' : ''}${key}="${escapeLabelValue( + val + )}"`; + } + if (additionalLabels) { + for (const [key, val] of Object.entries(additionalLabels)) { + hasLabel = true; + labelsStr += `${ + labelsStr.length > 0 ? ',' : '' + }${key}="${escapeLabelValue(val)}"`; + } + } + + if (hasLabel) { + metricName += `{${labelsStr}}`; + } + + return `${metricName} ${valueString(value)}${ + timestamp !== undefined ? ' ' + String(timestamp) : '' + }\n`; +} + +export class PrometheusSerializer { + private _prefix: string | undefined; + private _appendTimestamp: boolean; + + constructor(prefix?: string, appendTimestamp = true) { + if (prefix) { + this._prefix = prefix + '_'; + } + this._appendTimestamp = appendTimestamp; + } + + serialize(checkpointSet: PrometheusCheckpoint[]): string { + let str = ''; + for (const checkpoint of checkpointSet) { + str += this.serializeCheckpointSet(checkpoint) + '\n'; + } + return str; + } + + serializeCheckpointSet(checkpoint: PrometheusCheckpoint): string { + let name = sanitizePrometheusMetricName( + escapeString(checkpoint.descriptor.name) + ); + if (this._prefix) { + name = `${this._prefix}${name}`; + } + const help = `# HELP ${name} ${escapeString( + checkpoint.descriptor.description || 'description missing' + )}`; + const type = `# TYPE ${name} ${toPrometheusType( + checkpoint.descriptor.metricKind, + checkpoint.aggregatorKind + )}`; + + const results = checkpoint.records + .map(it => this.serializeRecord(name, it)) + .join(''); + + return `${help}\n${type}\n${results}`.trim(); + } + + serializeRecord(name: string, record: MetricRecord): string { + let results = ''; + switch (record.aggregator.kind) { + case AggregatorKind.SUM: + case AggregatorKind.LAST_VALUE: { + const { value, timestamp: hrtime } = record.aggregator.toPoint(); + const timestamp = hrTimeToMilliseconds(hrtime); + results += stringify( + name, + record.labels, + value, + this._appendTimestamp ? timestamp : undefined, + undefined + ); + break; + } + case AggregatorKind.DISTRIBUTION: { + const { value, timestamp: hrtime } = record.aggregator.toPoint(); + const timestamp = hrTimeToMilliseconds(hrtime); + for (const key of ['count', 'sum'] as (keyof Distribution)[]) { + results += stringify( + name + '_' + key, + record.labels, + value[key], + this._appendTimestamp ? timestamp : undefined, + undefined + ); + } + results += stringify( + name, + record.labels, + value.min, + this._appendTimestamp ? timestamp : undefined, + { + quantile: '0', + } + ); + results += stringify( + name, + record.labels, + value.max, + this._appendTimestamp ? timestamp : undefined, + { + quantile: '1', + } + ); + break; + } + case AggregatorKind.HISTOGRAM: { + const { value, timestamp: hrtime } = record.aggregator.toPoint(); + const timestamp = hrTimeToMilliseconds(hrtime); + /** Histogram["bucket"] is not typed with `number` */ + for (const key of ['count', 'sum'] as ('count' | 'sum')[]) { + results += stringify( + name + '_' + key, + record.labels, + value[key], + this._appendTimestamp ? timestamp : undefined, + undefined + ); + } + for (const [idx, val] of value.buckets.counts.entries()) { + const upperBound = value.buckets.boundaries[idx]; + results += stringify( + name + '_bucket', + record.labels, + val, + this._appendTimestamp ? timestamp : undefined, + { + le: upperBound === undefined ? '+Inf' : String(upperBound), + } + ); + } + break; + } + } + return results; + } +} diff --git a/packages/opentelemetry-exporter-prometheus/src/index.ts b/packages/opentelemetry-exporter-prometheus/src/index.ts index be7bd5f868e..bcf661b337c 100644 --- a/packages/opentelemetry-exporter-prometheus/src/index.ts +++ b/packages/opentelemetry-exporter-prometheus/src/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export * from './prometheus'; +export * from './PrometheusExporter'; export * from './export/types'; diff --git a/packages/opentelemetry-exporter-prometheus/src/prometheus.ts b/packages/opentelemetry-exporter-prometheus/src/prometheus.ts deleted file mode 100644 index af0c551dbab..00000000000 --- a/packages/opentelemetry-exporter-prometheus/src/prometheus.ts +++ /dev/null @@ -1,322 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as api from '@opentelemetry/api'; -import { - ExportResult, - hrTimeToMilliseconds, - NoopLogger, -} from '@opentelemetry/core'; -import { - Distribution, - Histogram, - MetricDescriptor, - MetricExporter, - MetricKind, - MetricRecord, - Sum, -} from '@opentelemetry/metrics'; -import { createServer, IncomingMessage, Server, ServerResponse } from 'http'; -import { Counter, Gauge, Metric, Registry } from 'prom-client'; -import * as url from 'url'; -import { ExporterConfig } from './export/types'; - -export class PrometheusExporter implements MetricExporter { - static readonly DEFAULT_OPTIONS = { - port: 9464, - startServer: false, - endpoint: '/metrics', - prefix: '', - }; - - private readonly _registry = new Registry(); - private readonly _logger: api.Logger; - private readonly _port: number; - private readonly _endpoint: string; - private readonly _server: Server; - private readonly _prefix?: string; - private readonly _invalidCharacterRegex = /[^a-z0-9_]/gi; - - // This will be required when histogram is implemented. Leaving here so it is not forgotten - // Histogram cannot have a label named 'le' - // private static readonly RESERVED_HISTOGRAM_LABEL = 'le'; - - /** - * Constructor - * @param config Exporter configuration - * @param callback Callback to be called after a server was started - */ - constructor(config: ExporterConfig = {}, callback?: () => void) { - this._logger = config.logger || new NoopLogger(); - this._port = config.port || PrometheusExporter.DEFAULT_OPTIONS.port; - this._prefix = config.prefix || PrometheusExporter.DEFAULT_OPTIONS.prefix; - this._server = createServer(this._requestHandler); - - this._endpoint = ( - config.endpoint || PrometheusExporter.DEFAULT_OPTIONS.endpoint - ).replace(/^([^/])/, '/$1'); - - if (config.startServer || PrometheusExporter.DEFAULT_OPTIONS.startServer) { - this.startServer(callback); - } else if (callback) { - callback(); - } - } - - /** - * Saves the current values of all exported {@link MetricRecord}s so that - * they can be pulled by the Prometheus backend. - * - * In its current state, the exporter saves the current values of all metrics - * when export is called and returns them when the export endpoint is called. - * In the future, this should be a no-op and the exporter should reach into - * the metrics when the export endpoint is called. As there is currently no - * interface to do this, this is our only option. - * - * @param records Metrics to be sent to the prometheus backend - * @param cb result callback to be called on finish - */ - export(records: MetricRecord[], cb: (result: ExportResult) => void) { - if (!this._server) { - // It is conceivable that the _server may not be started as it is an async startup - // However unlikely, if this happens the caller may retry the export - cb(ExportResult.FAILED_RETRYABLE); - return; - } - - this._logger.debug('Prometheus exporter export'); - - for (const record of records) { - this._updateMetric(record); - } - - cb(ExportResult.SUCCESS); - } - - /** - * Shuts down the export server and clears the registry - * - * @param cb called when server is stopped - */ - shutdown(cb?: () => void) { - this._registry.clear(); - this.stopServer(cb); - } - - /** - * Updates the value of a single metric in the registry - * - * @param record Metric value to be saved - */ - private _updateMetric(record: MetricRecord) { - const metric = this._registerMetric(record); - if (!metric) return; - - const point = record.aggregator.toPoint(); - - const labels = record.labels; - - if (metric instanceof Counter) { - // Prometheus counter saves internal state and increments by given value. - // MetricRecord value is the current state, not the delta to be incremented by. - // Currently, _registerMetric creates a new counter every time the value changes, - // so the increment here behaves as a set value (increment from 0) - metric.inc( - labels, - point.value as Sum, - hrTimeToMilliseconds(point.timestamp) - ); - } - - if (metric instanceof Gauge) { - if (typeof point.value === 'number') { - if ( - record.descriptor.metricKind === MetricKind.VALUE_OBSERVER || - record.descriptor.metricKind === MetricKind.VALUE_RECORDER - ) { - metric.set( - labels, - point.value, - hrTimeToMilliseconds(point.timestamp) - ); - } else { - metric.set(labels, point.value); - } - } else if ((point.value as Histogram).buckets) { - metric.set( - labels, - (point.value as Histogram).sum, - hrTimeToMilliseconds(point.timestamp) - ); - } else if (typeof (point.value as Distribution).last === 'number') { - metric.set( - labels, - (point.value as Distribution).last, - hrTimeToMilliseconds(point.timestamp) - ); - } - } - } - - private _registerMetric(record: MetricRecord): Metric | undefined { - const metricName = this._getPrometheusMetricName(record.descriptor); - const metric = this._registry.getSingleMetric(metricName); - - /** - * Prometheus library does aggregation, which means its inc method must be called with - * the value to be incremented by. It does not have a set method. As our MetricRecord - * contains the current value, not the value to be incremented by, we destroy and - * recreate counters when they are updated. - * - * This works because counters are identified by their name and no other internal ID - * https://prometheus.io/docs/instrumenting/exposition_formats/ - */ - if (metric instanceof Counter) { - metric.remove(...Object.values(record.labels)); - } - - if (metric) return metric; - - return this._newMetric(record, metricName); - } - - private _newMetric(record: MetricRecord, name: string): Metric | undefined { - const metricObject = { - name, - // prom-client throws with empty description which is our default - help: record.descriptor.description || 'description missing', - labelNames: Object.keys(record.labels), - // list of registries to register the newly created metric - registers: [this._registry], - }; - - switch (record.descriptor.metricKind) { - case MetricKind.COUNTER: - return new Counter(metricObject); - case MetricKind.UP_DOWN_COUNTER: - return new Gauge(metricObject); - // case MetricKind.VALUE_RECORDER: - // case MetricKind.SUM_OBSERVER: - // case MetricKind.UP_DOWN_SUM_OBSERVER: - case MetricKind.VALUE_OBSERVER: - return new Gauge(metricObject); - default: - // Other metric types are currently unimplemented - return undefined; - } - } - - private _getPrometheusMetricName(descriptor: MetricDescriptor): string { - return this._sanitizePrometheusMetricName( - this._prefix ? `${this._prefix}_${descriptor.name}` : descriptor.name - ); - } - - /** - * Ensures metric names are valid Prometheus metric names by removing - * characters allowed by OpenTelemetry but disallowed by Prometheus. - * - * https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels - * - * 1. Names must match `[a-zA-Z_:][a-zA-Z0-9_:]*` - * - * 2. Colons are reserved for user defined recording rules. - * They should not be used by exporters or direct instrumentation. - * - * OpenTelemetry metric names are already validated in the Meter when they are created, - * and they match the format `[a-zA-Z][a-zA-Z0-9_.\-]*` which is very close to a valid - * prometheus metric name, so we only need to strip characters valid in OpenTelemetry - * but not valid in prometheus and replace them with '_'. - * - * @param name name to be sanitized - */ - private _sanitizePrometheusMetricName(name: string): string { - return name.replace(this._invalidCharacterRegex, '_'); // replace all invalid characters with '_' - } - - /** - * Stops the Prometheus export server - * @param callback A callback that will be executed once the server is stopped - */ - stopServer(callback?: () => void) { - if (!this._server) { - this._logger.debug( - 'Prometheus stopServer() was called but server was never started.' - ); - if (callback) { - callback(); - } - } else { - this._server.close(() => { - this._logger.debug('Prometheus exporter was stopped'); - if (callback) { - callback(); - } - }); - } - } - - /** - * Starts the Prometheus export server - * - * @param callback called once the server is ready - */ - startServer(callback?: () => void) { - this._server.listen(this._port, () => { - this._logger.debug( - `Prometheus exporter started on port ${this._port} at endpoint ${this._endpoint}` - ); - if (callback) { - callback(); - } - }); - } - - /** - * Request handler used by http library to respond to incoming requests - * for the current state of metrics by the Prometheus backend. - * - * @param request Incoming HTTP request to export server - * @param response HTTP response object used to respond to request - */ - private _requestHandler = ( - request: IncomingMessage, - response: ServerResponse - ) => { - if (url.parse(request.url!).pathname === this._endpoint) { - this._exportMetrics(response); - } else { - this._notFound(response); - } - }; - - /** - * Responds to incoming message with current state of all metrics. - */ - private _exportMetrics = (response: ServerResponse) => { - response.statusCode = 200; - response.setHeader('content-type', this._registry.contentType); - response.end(this._registry.metrics() || '# no registered metrics'); - }; - - /** - * Responds with 404 status code to all requests that do not match the configured endpoint. - */ - private _notFound = (response: ServerResponse) => { - response.statusCode = 404; - response.end(); - }; -} diff --git a/packages/opentelemetry-exporter-collector/src/platform/browser/types.ts b/packages/opentelemetry-exporter-prometheus/src/types.ts similarity index 72% rename from packages/opentelemetry-exporter-collector/src/platform/browser/types.ts rename to packages/opentelemetry-exporter-prometheus/src/types.ts index dffdf01c26b..343dc991970 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/browser/types.ts +++ b/packages/opentelemetry-exporter-prometheus/src/types.ts @@ -14,12 +14,14 @@ * limitations under the License. */ -import { CollectorExporterConfigBase } from '../../types'; +import { + MetricDescriptor, + AggregatorKind, + MetricRecord, +} from '@opentelemetry/metrics'; -/** - * Collector Exporter Config for Web - */ -export interface CollectorExporterConfigBrowser - extends CollectorExporterConfigBase { - headers?: { [key: string]: string }; +export interface PrometheusCheckpoint { + descriptor: MetricDescriptor; + aggregatorKind: AggregatorKind; + records: MetricRecord[]; } diff --git a/packages/opentelemetry-exporter-prometheus/test/ExactBatcher.ts b/packages/opentelemetry-exporter-prometheus/test/ExactBatcher.ts new file mode 100644 index 00000000000..4d4a6fa972f --- /dev/null +++ b/packages/opentelemetry-exporter-prometheus/test/ExactBatcher.ts @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + Batcher, + MetricDescriptor, + Aggregator, + MetricRecord, +} from '@opentelemetry/metrics'; + +type Constructor = new (...args: T[]) => R; + +export class ExactBatcher extends Batcher { + private readonly args: ConstructorParameters>; + public aggregators: R[] = []; + + constructor( + private readonly aggregator: Constructor, + ...args: ConstructorParameters> + ) { + super(); + this.args = args; + } + + aggregatorFor(metricDescriptor: MetricDescriptor): Aggregator { + const aggregator = new this.aggregator(...this.args); + this.aggregators.push(aggregator); + return aggregator; + } + + process(record: MetricRecord): void { + const labels = Object.keys(record.labels) + .map(k => `${k}=${record.labels[k]}`) + .join(','); + this._batchMap.set(record.descriptor.name + labels, record); + } +} diff --git a/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts b/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts similarity index 66% rename from packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts rename to packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts index 12acad6669b..d3e50edfa04 100644 --- a/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts +++ b/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts @@ -14,35 +14,28 @@ * limitations under the License. */ -import { HrTime, ObserverResult } from '@opentelemetry/api'; +import { ObserverResult } from '@opentelemetry/api'; +import { + notifyOnGlobalShutdown, + _invokeGlobalShutdown, +} from '@opentelemetry/core'; import { CounterMetric, SumAggregator, Meter, MeterProvider, - Point, - Sum, + MinMaxLastSumCountAggregator, } from '@opentelemetry/metrics'; import * as assert from 'assert'; import * as http from 'http'; import { PrometheusExporter } from '../src'; - -const mockedHrTime: HrTime = [1586347902211, 0]; -const mockedTimeMS = 1586347902211000; +import { mockAggregator, mockedHrTimeMs } from './util'; describe('PrometheusExporter', () => { - let toPoint: () => Point; - before(() => { - toPoint = SumAggregator.prototype.toPoint; - SumAggregator.prototype.toPoint = function (): Point { - const point = toPoint.apply(this); - point.timestamp = mockedHrTime; - return point; - }; - }); - after(() => { - SumAggregator.prototype.toPoint = toPoint; - }); + let removeEvent: Function | undefined; + mockAggregator(SumAggregator); + mockAggregator(MinMaxLastSumCountAggregator); + describe('constructor', () => { it('should construct an exporter', () => { const exporter = new PrometheusExporter(); @@ -185,16 +178,27 @@ describe('PrometheusExporter', () => { describe('export', () => { let exporter: PrometheusExporter; + let meterProvider: MeterProvider; let meter: Meter; beforeEach(done => { exporter = new PrometheusExporter(); - meter = new MeterProvider().getMeter('test-prometheus'); + meterProvider = new MeterProvider({ + interval: Math.pow(2, 31) - 1, + gracefulShutdown: true, + }); + meter = meterProvider.getMeter('test-prometheus', '1', { + exporter: exporter, + }); exporter.startServer(done); }); afterEach(done => { exporter.shutdown(done); + if (removeEvent) { + removeEvent(); + removeEvent = undefined; + } }); it('should export a count aggregation', done => { @@ -206,6 +210,7 @@ describe('PrometheusExporter', () => { boundCounter.add(10); meter.collect().then(() => { exporter.export(meter.getBatcher().checkPointSet(), () => { + // TODO: Remove this special case once the PR is ready. // This is to test the special case where counters are destroyed // and recreated in the exporter in order to get around prom-client's // aggregation and use ours. @@ -225,7 +230,7 @@ describe('PrometheusExporter', () => { assert.deepStrictEqual(lines, [ '# HELP counter a test description', '# TYPE counter counter', - `counter{key1="labelValue1"} 20 ${mockedTimeMS}`, + `counter{key1="labelValue1"} 20 ${mockedHrTimeMs}`, '', ]); @@ -240,7 +245,7 @@ describe('PrometheusExporter', () => { it('should export an observer aggregation', done => { function getCpuUsage() { - return Math.random(); + return 0.999; } meter.createValueObserver( @@ -265,20 +270,15 @@ describe('PrometheusExporter', () => { const body = chunk.toString(); const lines = body.split('\n'); - assert.strictEqual( - lines[0], - '# HELP metric_observer a test description' - ); - assert.strictEqual(lines[1], '# TYPE metric_observer gauge'); - - const line3 = lines[2].split(' '); - assert.strictEqual( - line3[0], - 'metric_observer{pid="123",core="1"}' - ); - assert.ok( - parseFloat(line3[1]) >= 0 && parseFloat(line3[1]) <= 1 - ); + assert.deepStrictEqual(lines, [ + '# HELP metric_observer a test description', + '# TYPE metric_observer summary', + `metric_observer_count{pid="123",core="1"} 1 ${mockedHrTimeMs}`, + `metric_observer_sum{pid="123",core="1"} 0.999 ${mockedHrTimeMs}`, + `metric_observer{pid="123",core="1",quantile="0"} 0.999 ${mockedHrTimeMs}`, + `metric_observer{pid="123",core="1",quantile="1"} 0.999 ${mockedHrTimeMs}`, + '', + ]); done(); }); @@ -307,8 +307,8 @@ describe('PrometheusExporter', () => { assert.deepStrictEqual(lines, [ '# HELP counter a test description', '# TYPE counter counter', - `counter{counterKey1="labelValue1"} 10 ${mockedTimeMS}`, - `counter{counterKey1="labelValue2"} 20 ${mockedTimeMS}`, + `counter{counterKey1="labelValue1"} 10 ${mockedHrTimeMs}`, + `counter{counterKey1="labelValue2"} 20 ${mockedHrTimeMs}`, '', ]); @@ -320,6 +320,70 @@ describe('PrometheusExporter', () => { }); }); + it('should export multiple labels on graceful shutdown', done => { + const counter = meter.createCounter('counter', { + description: 'a test description', + }) as CounterMetric; + + counter.bind({ counterKey1: 'labelValue1' }).add(10); + counter.bind({ counterKey1: 'labelValue2' }).add(20); + counter.bind({ counterKey1: 'labelValue3' }).add(30); + + removeEvent = notifyOnGlobalShutdown(() => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.deepStrictEqual(lines, [ + '# HELP counter a test description', + '# TYPE counter counter', + `counter{counterKey1="labelValue1"} 10 ${mockedHrTimeMs}`, + `counter{counterKey1="labelValue2"} 20 ${mockedHrTimeMs}`, + `counter{counterKey1="labelValue3"} 30 ${mockedHrTimeMs}`, + '', + ]); + + done(); + }); + }) + .on('error', errorHandler(done)); + }); + _invokeGlobalShutdown(); + }); + + it('should export multiple labels on manual shutdown', done => { + const counter = meter.createCounter('counter', { + description: 'a test description', + }) as CounterMetric; + + counter.bind({ counterKey1: 'labelValue1' }).add(10); + counter.bind({ counterKey1: 'labelValue2' }).add(20); + counter.bind({ counterKey1: 'labelValue3' }).add(30); + meterProvider.shutdown(() => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.deepStrictEqual(lines, [ + '# HELP counter a test description', + '# TYPE counter counter', + `counter{counterKey1="labelValue1"} 10 ${mockedHrTimeMs}`, + `counter{counterKey1="labelValue2"} 20 ${mockedHrTimeMs}`, + `counter{counterKey1="labelValue3"} 30 ${mockedHrTimeMs}`, + '', + ]); + + done(); + }); + }) + .on('error', errorHandler(done)); + }); + }); + it('should export a comment if no metrics are registered', done => { exporter.export([], () => { http @@ -353,7 +417,7 @@ describe('PrometheusExporter', () => { assert.deepStrictEqual(lines, [ '# HELP counter description missing', '# TYPE counter counter', - `counter{key1="labelValue1"} 10 ${mockedTimeMS}`, + `counter{key1="labelValue1"} 10 ${mockedHrTimeMs}`, '', ]); @@ -380,7 +444,7 @@ describe('PrometheusExporter', () => { assert.deepStrictEqual(lines, [ '# HELP counter_bad_name description missing', '# TYPE counter_bad_name counter', - `counter_bad_name{key1="labelValue1"} 10 ${mockedTimeMS}`, + `counter_bad_name{key1="labelValue1"} 10 ${mockedHrTimeMs}`, '', ]); @@ -406,7 +470,120 @@ describe('PrometheusExporter', () => { assert.deepStrictEqual(chunk.toString().split('\n'), [ '# HELP counter a test description', '# TYPE counter gauge', - 'counter{key1="labelValue1"} 20', + `counter{key1="labelValue1"} 20 ${mockedHrTimeMs}`, + '', + ]); + + done(); + }); + }) + .on('error', errorHandler(done)); + }); + }); + }); + + it('should export a SumObserver as a counter', done => { + function getValue() { + return 20; + } + + meter.createSumObserver( + 'sum_observer', + { + description: 'a test description', + }, + (observerResult: ObserverResult) => { + observerResult.observe(getValue(), { + key1: 'labelValue1', + }); + } + ); + + meter.collect().then(() => { + exporter.export(meter.getBatcher().checkPointSet(), () => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.deepStrictEqual(lines, [ + '# HELP sum_observer a test description', + '# TYPE sum_observer counter', + `sum_observer{key1="labelValue1"} 20 ${mockedHrTimeMs}`, + '', + ]); + }); + + done(); + }) + .on('error', errorHandler(done)); + }); + }); + }); + + it('should export a UpDownSumObserver as a gauge', done => { + function getValue() { + return 20; + } + + meter.createUpDownSumObserver( + 'updown_observer', + { + description: 'a test description', + }, + (observerResult: ObserverResult) => { + observerResult.observe(getValue(), { + key1: 'labelValue1', + }); + } + ); + + meter.collect().then(() => { + exporter.export(meter.getBatcher().checkPointSet(), () => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.deepStrictEqual(lines, [ + '# HELP updown_observer a test description', + '# TYPE updown_observer gauge', + `updown_observer{key1="labelValue1"} 20 ${mockedHrTimeMs}`, + '', + ]); + }); + + done(); + }) + .on('error', errorHandler(done)); + }); + }); + }); + + it('should export a ValueRecorder as a summary', done => { + const valueRecorder = meter.createValueRecorder('value_recorder', { + description: 'a test description', + }); + + valueRecorder.bind({ key1: 'labelValue1' }).record(20); + + meter.collect().then(() => { + exporter.export(meter.getBatcher().checkPointSet(), () => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.deepStrictEqual(lines, [ + '# HELP value_recorder a test description', + '# TYPE value_recorder summary', + `value_recorder_count{key1="labelValue1"} 1 ${mockedHrTimeMs}`, + `value_recorder_sum{key1="labelValue1"} 20 ${mockedHrTimeMs}`, + `value_recorder{key1="labelValue1",quantile="0"} 20 ${mockedHrTimeMs}`, + `value_recorder{key1="labelValue1",quantile="1"} 20 ${mockedHrTimeMs}`, '', ]); @@ -456,7 +633,7 @@ describe('PrometheusExporter', () => { assert.deepStrictEqual(lines, [ '# HELP test_prefix_counter description missing', '# TYPE test_prefix_counter counter', - `test_prefix_counter{key1="labelValue1"} 10 ${mockedTimeMS}`, + `test_prefix_counter{key1="labelValue1"} 10 ${mockedHrTimeMs}`, '', ]); @@ -485,7 +662,7 @@ describe('PrometheusExporter', () => { assert.deepStrictEqual(lines, [ '# HELP counter description missing', '# TYPE counter counter', - `counter{key1="labelValue1"} 10 ${mockedTimeMS}`, + `counter{key1="labelValue1"} 10 ${mockedHrTimeMs}`, '', ]); @@ -514,7 +691,7 @@ describe('PrometheusExporter', () => { assert.deepStrictEqual(lines, [ '# HELP counter description missing', '# TYPE counter counter', - `counter{key1="labelValue1"} 10 ${mockedTimeMS}`, + `counter{key1="labelValue1"} 10 ${mockedHrTimeMs}`, '', ]); diff --git a/packages/opentelemetry-exporter-prometheus/test/PrometheusLabelsBatcher.test.ts b/packages/opentelemetry-exporter-prometheus/test/PrometheusLabelsBatcher.test.ts new file mode 100644 index 00000000000..27a500700b5 --- /dev/null +++ b/packages/opentelemetry-exporter-prometheus/test/PrometheusLabelsBatcher.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as assert from 'assert'; +import { PrometheusLabelsBatcher } from '../src/PrometheusLabelsBatcher'; +import { + CounterMetric, + AggregatorKind, + MeterProvider, + Meter, +} from '@opentelemetry/metrics'; +import { Labels } from '@opentelemetry/api'; + +describe('PrometheusBatcher', () => { + let meter: Meter; + before(() => { + meter = new MeterProvider({}).getMeter('test'); + }); + + describe('constructor', () => { + it('should construct a batcher', () => { + const batcher = new PrometheusLabelsBatcher(); + assert(batcher instanceof PrometheusLabelsBatcher); + }); + }); + + describe('process', () => { + it('should aggregate metric records with same metric name', async () => { + const batcher = new PrometheusLabelsBatcher(); + const counter = meter.createCounter('test_counter') as CounterMetric; + counter.bind({ val: '1' }).add(1); + counter.bind({ val: '2' }).add(1); + + const records = await counter.getMetricRecord(); + records.forEach(it => batcher.process(it)); + + const checkPointSet = batcher.checkPointSet(); + assert.strictEqual(checkPointSet.length, 1); + assert.strictEqual(checkPointSet[0].descriptor.name, 'test_counter'); + assert.strictEqual(checkPointSet[0].aggregatorKind, AggregatorKind.SUM); + assert.strictEqual(checkPointSet[0].records.length, 2); + }); + + it('should recognize identical labels with different key-insertion order', async () => { + const batcher = new PrometheusLabelsBatcher(); + const counter = meter.createCounter('test_counter') as CounterMetric; + + const label1: Labels = {}; + label1.key1 = '1'; + label1.key2 = '2'; + + const label2: Labels = {}; + label2.key2 = '2'; + label2.key1 = '1'; + + counter.bind(label1).add(1); + counter.bind(label2).add(1); + + const records = await counter.getMetricRecord(); + records.forEach(it => batcher.process(it)); + + const checkPointSet = batcher.checkPointSet(); + assert.strictEqual(checkPointSet.length, 1); + const checkPoint = checkPointSet[0]; + assert.strictEqual(checkPoint.descriptor.name, 'test_counter'); + assert.strictEqual(checkPoint.aggregatorKind, AggregatorKind.SUM); + assert.strictEqual(checkPoint.records.length, 1); + const record = checkPoint.records[0]; + assert.strictEqual(record.aggregator.toPoint().value, 2); + }); + }); +}); diff --git a/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts b/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts new file mode 100644 index 00000000000..b706e635d95 --- /dev/null +++ b/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts @@ -0,0 +1,462 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + SumAggregator, + MinMaxLastSumCountAggregator, + HistogramAggregator, + MeterProvider, + CounterMetric, + ValueRecorderMetric, + UpDownCounterMetric, +} from '@opentelemetry/metrics'; +import * as assert from 'assert'; +import { Labels } from '@opentelemetry/api'; +import { PrometheusSerializer } from '../src/PrometheusSerializer'; +import { PrometheusLabelsBatcher } from '../src/PrometheusLabelsBatcher'; +import { ExactBatcher } from './ExactBatcher'; +import { mockedHrTimeMs, mockAggregator } from './util'; + +const labels = { + foo1: 'bar1', + foo2: 'bar2', +}; + +describe('PrometheusSerializer', () => { + describe('constructor', () => { + it('should construct a serializer', () => { + const serializer = new PrometheusSerializer(); + assert(serializer instanceof PrometheusSerializer); + }); + }); + + describe('serialize a metric record', () => { + describe('with SumAggregator', () => { + mockAggregator(SumAggregator); + + it('should serialize metric record with sum aggregator', async () => { + const serializer = new PrometheusSerializer(); + + const meter = new MeterProvider({ + batcher: new ExactBatcher(SumAggregator), + }).getMeter('test'); + const counter = meter.createCounter('test') as CounterMetric; + counter.bind(labels).add(1); + + const records = await counter.getMetricRecord(); + const record = records[0]; + + const result = serializer.serializeRecord( + record.descriptor.name, + record + ); + assert.strictEqual( + result, + `test{foo1="bar1",foo2="bar2"} 1 ${mockedHrTimeMs}\n` + ); + }); + + it('serialize metric record with sum aggregator without timestamp', async () => { + const serializer = new PrometheusSerializer(undefined, false); + + const meter = new MeterProvider({ + batcher: new ExactBatcher(SumAggregator), + }).getMeter('test'); + const counter = meter.createCounter('test') as CounterMetric; + counter.bind(labels).add(1); + + const records = await counter.getMetricRecord(); + const record = records[0]; + + const result = serializer.serializeRecord( + record.descriptor.name, + record + ); + assert.strictEqual(result, 'test{foo1="bar1",foo2="bar2"} 1\n'); + }); + }); + + describe('with MinMaxLastSumCountAggregator', () => { + mockAggregator(MinMaxLastSumCountAggregator); + + it('should serialize metric record with sum aggregator', async () => { + const serializer = new PrometheusSerializer(); + + const meter = new MeterProvider({ + batcher: new ExactBatcher(MinMaxLastSumCountAggregator), + }).getMeter('test'); + const counter = meter.createCounter('test') as CounterMetric; + counter.bind(labels).add(1); + + const records = await counter.getMetricRecord(); + const record = records[0]; + + const result = serializer.serializeRecord( + record.descriptor.name, + record + ); + assert.strictEqual( + result, + `test_count{foo1="bar1",foo2="bar2"} 1 ${mockedHrTimeMs}\n` + + `test_sum{foo1="bar1",foo2="bar2"} 1 ${mockedHrTimeMs}\n` + + `test{foo1="bar1",foo2="bar2",quantile="0"} 1 ${mockedHrTimeMs}\n` + + `test{foo1="bar1",foo2="bar2",quantile="1"} 1 ${mockedHrTimeMs}\n` + ); + }); + + it('serialize metric record with sum aggregator without timestamp', async () => { + const serializer = new PrometheusSerializer(undefined, false); + + const meter = new MeterProvider({ + batcher: new ExactBatcher(MinMaxLastSumCountAggregator), + }).getMeter('test'); + const counter = meter.createCounter('test') as CounterMetric; + counter.bind(labels).add(1); + + const records = await counter.getMetricRecord(); + const record = records[0]; + + const result = serializer.serializeRecord( + record.descriptor.name, + record + ); + assert.strictEqual( + result, + 'test_count{foo1="bar1",foo2="bar2"} 1\n' + + 'test_sum{foo1="bar1",foo2="bar2"} 1\n' + + 'test{foo1="bar1",foo2="bar2",quantile="0"} 1\n' + + 'test{foo1="bar1",foo2="bar2",quantile="1"} 1\n' + ); + }); + }); + + describe('with HistogramAggregator', () => { + mockAggregator(HistogramAggregator); + + it('should serialize metric record with sum aggregator', async () => { + const serializer = new PrometheusSerializer(); + + const batcher = new ExactBatcher(HistogramAggregator, [1, 10, 100]); + const meter = new MeterProvider({ batcher }).getMeter('test'); + const recorder = meter.createValueRecorder('test', { + description: 'foobar', + }) as ValueRecorderMetric; + recorder.bind(labels).record(5); + + const records = await recorder.getMetricRecord(); + const record = records[0]; + + const result = serializer.serializeRecord( + record.descriptor.name, + record + ); + assert.strictEqual( + result, + `test_count{foo1="bar1",foo2="bar2"} 1 ${mockedHrTimeMs}\n` + + `test_sum{foo1="bar1",foo2="bar2"} 5 ${mockedHrTimeMs}\n` + + `test_bucket{foo1="bar1",foo2="bar2",le="1"} 0 ${mockedHrTimeMs}\n` + + `test_bucket{foo1="bar1",foo2="bar2",le="10"} 1 ${mockedHrTimeMs}\n` + + `test_bucket{foo1="bar1",foo2="bar2",le="100"} 0 ${mockedHrTimeMs}\n` + + `test_bucket{foo1="bar1",foo2="bar2",le="+Inf"} 0 ${mockedHrTimeMs}\n` + ); + }); + + it('serialize metric record with sum aggregator without timestamp', async () => { + const serializer = new PrometheusSerializer(undefined, false); + + const batcher = new ExactBatcher(HistogramAggregator, [1, 10, 100]); + const meter = new MeterProvider({ batcher }).getMeter('test'); + const recorder = meter.createValueRecorder('test', { + description: 'foobar', + }) as ValueRecorderMetric; + recorder.bind(labels).record(5); + + const records = await recorder.getMetricRecord(); + const record = records[0]; + + const result = serializer.serializeRecord( + record.descriptor.name, + record + ); + assert.strictEqual( + result, + 'test_count{foo1="bar1",foo2="bar2"} 1\n' + + 'test_sum{foo1="bar1",foo2="bar2"} 5\n' + + 'test_bucket{foo1="bar1",foo2="bar2",le="1"} 0\n' + + 'test_bucket{foo1="bar1",foo2="bar2",le="10"} 1\n' + + 'test_bucket{foo1="bar1",foo2="bar2",le="100"} 0\n' + + 'test_bucket{foo1="bar1",foo2="bar2",le="+Inf"} 0\n' + ); + }); + }); + }); + + describe('serialize a checkpoint set', () => { + describe('with SumAggregator', () => { + mockAggregator(SumAggregator); + + it('should serialize metric record with sum aggregator', async () => { + const serializer = new PrometheusSerializer(); + + const meter = new MeterProvider({ + batcher: new ExactBatcher(SumAggregator), + }).getMeter('test'); + const batcher = new PrometheusLabelsBatcher(); + const counter = meter.createCounter('test', { + description: 'foobar', + }) as CounterMetric; + counter.bind({ val: '1' }).add(1); + counter.bind({ val: '2' }).add(1); + + const records = await counter.getMetricRecord(); + records.forEach(it => batcher.process(it)); + const checkPointSet = batcher.checkPointSet(); + + const result = serializer.serialize(checkPointSet); + assert.strictEqual( + result, + '# HELP test foobar\n' + + '# TYPE test counter\n' + + `test{val="1"} 1 ${mockedHrTimeMs}\n` + + `test{val="2"} 1 ${mockedHrTimeMs}\n` + ); + }); + + it('serialize metric record with sum aggregator without timestamp', async () => { + const serializer = new PrometheusSerializer(undefined, false); + + const meter = new MeterProvider({ + batcher: new ExactBatcher(SumAggregator), + }).getMeter('test'); + const batcher = new PrometheusLabelsBatcher(); + const counter = meter.createCounter('test', { + description: 'foobar', + }) as CounterMetric; + counter.bind({ val: '1' }).add(1); + counter.bind({ val: '2' }).add(1); + + const records = await counter.getMetricRecord(); + records.forEach(it => batcher.process(it)); + const checkPointSet = batcher.checkPointSet(); + + const result = serializer.serialize(checkPointSet); + assert.strictEqual( + result, + '# HELP test foobar\n' + + '# TYPE test counter\n' + + 'test{val="1"} 1\n' + + 'test{val="2"} 1\n' + ); + }); + }); + + describe('with MinMaxLastSumCountAggregator', () => { + mockAggregator(MinMaxLastSumCountAggregator); + + it('serialize metric record with MinMaxLastSumCountAggregator aggregator', async () => { + const serializer = new PrometheusSerializer(); + + const meter = new MeterProvider({ + batcher: new ExactBatcher(MinMaxLastSumCountAggregator), + }).getMeter('test'); + const batcher = new PrometheusLabelsBatcher(); + const counter = meter.createCounter('test', { + description: 'foobar', + }) as CounterMetric; + counter.bind({ val: '1' }).add(1); + counter.bind({ val: '2' }).add(1); + + const records = await counter.getMetricRecord(); + records.forEach(it => batcher.process(it)); + const checkPointSet = batcher.checkPointSet(); + + const result = serializer.serialize(checkPointSet); + assert.strictEqual( + result, + '# HELP test foobar\n' + + '# TYPE test summary\n' + + `test_count{val="1"} 1 ${mockedHrTimeMs}\n` + + `test_sum{val="1"} 1 ${mockedHrTimeMs}\n` + + `test{val="1",quantile="0"} 1 ${mockedHrTimeMs}\n` + + `test{val="1",quantile="1"} 1 ${mockedHrTimeMs}\n` + + `test_count{val="2"} 1 ${mockedHrTimeMs}\n` + + `test_sum{val="2"} 1 ${mockedHrTimeMs}\n` + + `test{val="2",quantile="0"} 1 ${mockedHrTimeMs}\n` + + `test{val="2",quantile="1"} 1 ${mockedHrTimeMs}\n` + ); + }); + }); + + describe('with HistogramAggregator', () => { + mockAggregator(HistogramAggregator); + + it('serialize metric record with MinMaxLastSumCountAggregator aggregator', async () => { + const serializer = new PrometheusSerializer(); + + const batcher = new ExactBatcher(HistogramAggregator, [1, 10, 100]); + const meter = new MeterProvider({ batcher }).getMeter('test'); + const recorder = meter.createValueRecorder('test', { + description: 'foobar', + }) as ValueRecorderMetric; + recorder.bind({ val: '1' }).record(5); + recorder.bind({ val: '2' }).record(5); + + const records = await recorder.getMetricRecord(); + const labelBatcher = new PrometheusLabelsBatcher(); + records.forEach(it => labelBatcher.process(it)); + const checkPointSet = labelBatcher.checkPointSet(); + + const result = serializer.serialize(checkPointSet); + assert.strictEqual( + result, + '# HELP test foobar\n' + + '# TYPE test histogram\n' + + `test_count{val="1"} 1 ${mockedHrTimeMs}\n` + + `test_sum{val="1"} 5 ${mockedHrTimeMs}\n` + + `test_bucket{val="1",le="1"} 0 ${mockedHrTimeMs}\n` + + `test_bucket{val="1",le="10"} 1 ${mockedHrTimeMs}\n` + + `test_bucket{val="1",le="100"} 0 ${mockedHrTimeMs}\n` + + `test_bucket{val="1",le="+Inf"} 0 ${mockedHrTimeMs}\n` + + `test_count{val="2"} 1 ${mockedHrTimeMs}\n` + + `test_sum{val="2"} 5 ${mockedHrTimeMs}\n` + + `test_bucket{val="2",le="1"} 0 ${mockedHrTimeMs}\n` + + `test_bucket{val="2",le="10"} 1 ${mockedHrTimeMs}\n` + + `test_bucket{val="2",le="100"} 0 ${mockedHrTimeMs}\n` + + `test_bucket{val="2",le="+Inf"} 0 ${mockedHrTimeMs}\n` + ); + }); + }); + }); + + describe('serialize non-normalized values', () => { + describe('with SumAggregator', () => { + mockAggregator(SumAggregator); + + it('should serialize records without labels', async () => { + const serializer = new PrometheusSerializer(); + + const meter = new MeterProvider({ + batcher: new ExactBatcher(SumAggregator), + }).getMeter('test'); + const counter = meter.createCounter('test') as CounterMetric; + counter.bind({}).add(1); + + const records = await counter.getMetricRecord(); + const record = records[0]; + + const result = serializer.serializeRecord( + record.descriptor.name, + record + ); + assert.strictEqual(result, `test 1 ${mockedHrTimeMs}\n`); + }); + + it('should serialize non-string label values', async () => { + const serializer = new PrometheusSerializer(); + + const meter = new MeterProvider({ + batcher: new ExactBatcher(SumAggregator), + }).getMeter('test'); + const counter = meter.createCounter('test') as CounterMetric; + counter + .bind(({ + object: {}, + NaN: NaN, + null: null, + undefined: undefined, + } as unknown) as Labels) + .add(1); + const records = await counter.getMetricRecord(); + const record = records[0]; + + const result = serializer.serializeRecord( + record.descriptor.name, + record + ); + assert.strictEqual( + result, + `test{object="[object Object]",NaN="NaN",null="null",undefined="undefined"} 1 ${mockedHrTimeMs}\n` + ); + }); + + it('should serialize non-finite values', async () => { + const serializer = new PrometheusSerializer(); + const cases = [ + [NaN, 'Nan'], + [-Infinity, '-Inf'], + [+Infinity, '+Inf'], + ] as [number, string][]; + + for (const esac of cases) { + const meter = new MeterProvider({ + batcher: new ExactBatcher(SumAggregator), + }).getMeter('test'); + const counter = meter.createUpDownCounter( + 'test' + ) as UpDownCounterMetric; + counter.bind(labels).add(esac[0]); + const records = await counter.getMetricRecord(); + const record = records[0]; + + const result = serializer.serializeRecord( + record.descriptor.name, + record + ); + assert.strictEqual( + result, + `test{foo1="bar1",foo2="bar2"} ${esac[1]} ${mockedHrTimeMs}\n` + ); + } + }); + + it('should escape backslash (\\), double-quote ("), and line feed (\\n) in label values', async () => { + const serializer = new PrometheusSerializer(); + + const meter = new MeterProvider({ + batcher: new ExactBatcher(SumAggregator), + }).getMeter('test'); + const counter = meter.createCounter('test') as CounterMetric; + counter + .bind(({ + backslash: '\u005c', // \ => \\ (\u005c\u005c) + doubleQuote: '\u0022', // " => \" (\u005c\u0022) + lineFeed: '\u000a', // ↵ => \n (\u005c\u006e) + backslashN: '\u005c\u006e', // \n => \\n (\u005c\u005c\u006e) + backslashDoubleQuote: '\u005c\u0022', // \" => \\\" (\u005c\u005c\u005c\u0022) + backslashLineFeed: '\u005c\u000a', // \↵ => \\\n (\u005c\u005c\u005c\u006e) + } as unknown) as Labels) + .add(1); + const records = await counter.getMetricRecord(); + const record = records[0]; + + const result = serializer.serializeRecord( + record.descriptor.name, + record + ); + assert.strictEqual( + result, + 'test{' + + 'backslash="\u005c\u005c",' + + 'doubleQuote="\u005c\u0022",' + + 'lineFeed="\u005c\u006e",' + + 'backslashN="\u005c\u005c\u006e",' + + 'backslashDoubleQuote="\u005c\u005c\u005c\u0022",' + + 'backslashLineFeed="\u005c\u005c\u005c\u006e"' + + `} 1 ${mockedHrTimeMs}\n` + ); + }); + }); + }); +}); diff --git a/packages/opentelemetry-exporter-prometheus/test/util.ts b/packages/opentelemetry-exporter-prometheus/test/util.ts new file mode 100644 index 00000000000..697dd8ee547 --- /dev/null +++ b/packages/opentelemetry-exporter-prometheus/test/util.ts @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Point, Sum } from '@opentelemetry/metrics'; +import { HrTime } from '@opentelemetry/api'; + +export const mockedHrTime: HrTime = [1586347902, 211_000_000]; +export const mockedHrTimeMs = 1586347902211; +export function mockAggregator(Aggregator: any) { + let toPoint: () => Point; + before(() => { + toPoint = Aggregator.prototype.toPoint; + Aggregator.prototype.toPoint = function (): Point { + const point = toPoint.apply(this); + point.timestamp = mockedHrTime; + return point; + }; + }); + after(() => { + Aggregator.prototype.toPoint = toPoint; + }); +} diff --git a/packages/opentelemetry-exporter-zipkin/README.md b/packages/opentelemetry-exporter-zipkin/README.md index f6b78c6a1cc..ccd4404c677 100644 --- a/packages/opentelemetry-exporter-zipkin/README.md +++ b/packages/opentelemetry-exporter-zipkin/README.md @@ -44,8 +44,8 @@ tracer.addSpanProcessor(new BatchSpanProcessor(exporter)); You can use built-in `SimpleSpanProcessor` or `BatchSpanProcessor` or write your own. -- [SimpleSpanProcessor](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/sdk-tracing.md#simple-processor): The implementation of `SpanProcessor` that passes ended span directly to the configured `SpanExporter`. -- [BatchSpanProcessor](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/sdk-tracing.md#batching-processor): The implementation of the `SpanProcessor` that batches ended spans and pushes them to the configured `SpanExporter`. It is recommended to use this `SpanProcessor` for better performance and optimization. +- [SimpleSpanProcessor](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/sdk.md#simple-processor): The implementation of `SpanProcessor` that passes ended span directly to the configured `SpanExporter`. +- [BatchSpanProcessor](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/sdk.md#batching-processor): The implementation of the `SpanProcessor` that batches ended spans and pushes them to the configured `SpanExporter`. It is recommended to use this `SpanProcessor` for better performance and optimization. ## Viewing your traces diff --git a/packages/opentelemetry-exporter-zipkin/package.json b/packages/opentelemetry-exporter-zipkin/package.json index 70ee2794054..c306b0ba372 100644 --- a/packages/opentelemetry-exporter-zipkin/package.json +++ b/packages/opentelemetry-exporter-zipkin/package.json @@ -40,7 +40,7 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "codecov": "3.7.2", "gts": "2.0.2", diff --git a/packages/opentelemetry-exporter-zipkin/src/transform.ts b/packages/opentelemetry-exporter-zipkin/src/transform.ts index aebd956a936..1fa071542ad 100644 --- a/packages/opentelemetry-exporter-zipkin/src/transform.ts +++ b/packages/opentelemetry-exporter-zipkin/src/transform.ts @@ -83,8 +83,8 @@ export function _toZipkinTags( tags[statusDescriptionTagName] = status.message; } - Object.keys(resource.labels).forEach( - name => (tags[name] = resource.labels[name]) + Object.keys(resource.attributes).forEach( + name => (tags[name] = resource.attributes[name]) ); return tags; diff --git a/packages/opentelemetry-exporter-zipkin/src/zipkin.ts b/packages/opentelemetry-exporter-zipkin/src/zipkin.ts index fc74a290c58..54010b3af3a 100644 --- a/packages/opentelemetry-exporter-zipkin/src/zipkin.ts +++ b/packages/opentelemetry-exporter-zipkin/src/zipkin.ts @@ -27,7 +27,7 @@ import { statusDescriptionTagName, } from './transform'; import { OT_REQUEST_HEADER } from './utils'; -import { Resource, SERVICE_RESOURCE } from '@opentelemetry/resources'; +import { SERVICE_RESOURCE } from '@opentelemetry/resources'; /** * Zipkin Exporter */ @@ -73,7 +73,7 @@ export class ZipkinExporter implements SpanExporter { ) { if (typeof this._serviceName !== 'string') { this._serviceName = String( - spans[0].resource.labels[SERVICE_RESOURCE.NAME] || + spans[0].resource.attributes[SERVICE_RESOURCE.NAME] || this.DEFAULT_SERVICE_NAME ); } diff --git a/packages/opentelemetry-grpc-utils/package.json b/packages/opentelemetry-grpc-utils/package.json index 6cedc2507ba..5ba29de58a4 100644 --- a/packages/opentelemetry-grpc-utils/package.json +++ b/packages/opentelemetry-grpc-utils/package.json @@ -50,7 +50,7 @@ "@opentelemetry/tracing": "^0.10.2", "@types/mocha": "7.0.2", "@types/node": "14.0.27", - "@types/semver": "7.3.1", + "@types/semver": "7.3.2", "@types/shimmer": "1.0.1", "@types/sinon": "9.0.4", "codecov": "3.7.2", @@ -61,7 +61,7 @@ "nyc": "15.1.0", "rimraf": "3.0.2", "semver": "7.3.2", - "sinon": "9.0.2", + "sinon": "9.0.3", "ts-mocha": "7.0.0", "ts-node": "8.10.2", "typescript": "3.9.7" diff --git a/packages/opentelemetry-grpc-utils/test/grpcUtils.test.ts b/packages/opentelemetry-grpc-utils/test/grpcUtils.test.ts index e5f49b8b55a..15f3284961b 100644 --- a/packages/opentelemetry-grpc-utils/test/grpcUtils.test.ts +++ b/packages/opentelemetry-grpc-utils/test/grpcUtils.test.ts @@ -19,6 +19,7 @@ import { NoopTracerProvider, SpanKind, propagation, + PluginConfig, } from '@opentelemetry/api'; import { NoopLogger, HttpTraceContext, BasePlugin } from '@opentelemetry/core'; import { NodeTracerProvider } from '@opentelemetry/node'; @@ -68,6 +69,7 @@ type ServerWriteableStream = type ServerDuplexStream = | grpcNapi.ServerDuplexStream | grpcJs.ServerDuplexStream; +type Metadata = grpcNapi.Metadata | grpcJs.Metadata; type TestGrpcClient = (typeof grpcJs | typeof grpcNapi)['Client'] & { unaryMethod: any; @@ -78,6 +80,15 @@ type TestGrpcClient = (typeof grpcJs | typeof grpcNapi)['Client'] & { bidiStreamMethod: any; }; +interface TestGrpcCall { + description: string; + methodName: string; + method: Function; + request: TestRequestResponse | TestRequestResponse[]; + result: TestRequestResponse | TestRequestResponse[]; + metadata?: Metadata; +} + // Compare two arrays using an equal function f const arrayIsEqual = (f: any) => ([x, ...xs]: any) => ([y, ...ys]: any): any => x === undefined && y === undefined @@ -109,11 +120,13 @@ export const runTests = ( const grpcClient = { unaryMethod: ( client: TestGrpcClient, - request: TestRequestResponse + request: TestRequestResponse, + metadata: Metadata = new grpc.Metadata() ): Promise => { return new Promise((resolve, reject) => { return client.unaryMethod( request, + metadata, (err: ServiceError, response: TestRequestResponse) => { if (err) { reject(err); @@ -127,11 +140,13 @@ export const runTests = ( UnaryMethod: ( client: TestGrpcClient, - request: TestRequestResponse + request: TestRequestResponse, + metadata: Metadata = new grpc.Metadata() ): Promise => { return new Promise((resolve, reject) => { return client.UnaryMethod( request, + metadata, (err: ServiceError, response: TestRequestResponse) => { if (err) { reject(err); @@ -145,11 +160,13 @@ export const runTests = ( camelCaseMethod: ( client: TestGrpcClient, - request: TestRequestResponse + request: TestRequestResponse, + metadata: Metadata = new grpc.Metadata() ): Promise => { return new Promise((resolve, reject) => { return client.camelCaseMethod( request, + metadata, (err: ServiceError, response: TestRequestResponse) => { if (err) { reject(err); @@ -163,10 +180,12 @@ export const runTests = ( clientStreamMethod: ( client: TestGrpcClient, - request: TestRequestResponse[] + request: TestRequestResponse[], + metadata: Metadata = new grpc.Metadata() ): Promise => { return new Promise((resolve, reject) => { const writeStream = client.clientStreamMethod( + metadata, (err: ServiceError, response: TestRequestResponse) => { if (err) { reject(err); @@ -185,11 +204,12 @@ export const runTests = ( serverStreamMethod: ( client: TestGrpcClient, - request: TestRequestResponse + request: TestRequestResponse, + metadata: Metadata = new grpc.Metadata() ): Promise => { return new Promise((resolve, reject) => { const result: TestRequestResponse[] = []; - const readStream = client.serverStreamMethod(request); + const readStream = client.serverStreamMethod(request, metadata); readStream.on('data', (data: TestRequestResponse) => { result.push(data); @@ -205,11 +225,12 @@ export const runTests = ( bidiStreamMethod: ( client: TestGrpcClient, - request: TestRequestResponse[] + request: TestRequestResponse[], + metadata: Metadata = new grpc.Metadata() ): Promise => { return new Promise((resolve, reject) => { const result: TestRequestResponse[] = []; - const bidiStream = client.bidiStreamMethod([]); + const bidiStream = client.bidiStreamMethod(metadata); bidiStream.on('data', (data: TestRequestResponse) => { result.push(data); @@ -403,7 +424,7 @@ export const runTests = ( return sum + x.num; }, 0), }; - const methodList = [ + const methodList: TestGrpcCall[] = [ { description: 'unary call', methodName: 'UnaryMethod', @@ -458,7 +479,7 @@ export const runTests = ( }: create a rootSpan for client and a childSpan for server - ${ method.description }`, async () => { - const args = [client, method.request]; + const args = [client, method.request, method.metadata]; await (method.method as any) .apply({}, args) .then((result: TestRequestResponse | TestRequestResponse[]) => { @@ -509,7 +530,7 @@ export const runTests = ( } assert.deepStrictEqual(rootSpan, span); - const args = [client, method.request]; + const args = [client, method.request, method.metadata]; await (method.method as any) .apply({}, args) .then(() => { @@ -718,5 +739,108 @@ export const runTests = ( }); }); }); + + describe('Test filtering requests using metadata', () => { + const logger = new NoopLogger(); + const provider = new NodeTracerProvider({ logger }); + provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + beforeEach(() => { + memoryExporter.reset(); + }); + + before(async () => { + const config = { + // TODO: add plugin options here once supported + }; + const patchedGrpc = plugin.enable(grpc, provider, logger, config); + + const packageDefinition = await protoLoader.load(PROTO_PATH, options); + const proto = patchedGrpc.loadPackageDefinition(packageDefinition) + .pkg_test; + + server = await startServer(patchedGrpc, proto); + client = createClient(patchedGrpc, proto); + }); + + after(done => { + client.close(); + server.tryShutdown(() => { + plugin.disable(); + done(); + }); + }); + + methodList.map(method => { + const metadata = new grpc.Metadata(); + metadata.set('x-opentelemetry-outgoing-request', '1'); + describe(`Test should not create spans for grpc remote method ${method.description} when metadata has otel header`, () => { + before(() => { + method.metadata = metadata; + }); + + after(() => { + delete method.metadata; + }); + + runTest(method, provider, false); + }); + }); + }); + + describe('Test filtering requests using options', () => { + const logger = new NoopLogger(); + const provider = new NodeTracerProvider({ logger }); + const checkSpans: { [key: string]: boolean } = { + unaryMethod: false, + UnaryMethod: false, + camelCaseMethod: false, + ClientStreamMethod: true, + ServerStreamMethod: true, + BidiStreamMethod: false, + }; + provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + beforeEach(() => { + memoryExporter.reset(); + }); + + before(async () => { + const config = { + ignoreGrpcMethods: [ + 'UnaryMethod', + new RegExp(/^camel.*Method$/), + (str: string) => str === 'BidiStreamMethod', + ], + }; + const patchedGrpc = plugin.enable( + grpc, + provider, + logger, + config as PluginConfig + ); + + const packageDefinition = await protoLoader.load(PROTO_PATH, options); + const proto = patchedGrpc.loadPackageDefinition(packageDefinition) + .pkg_test; + + server = await startServer(patchedGrpc, proto); + client = createClient(patchedGrpc, proto); + }); + + after(done => { + client.close(); + server.tryShutdown(() => { + plugin.disable(); + done(); + }); + }); + + methodList.map(method => { + describe(`Test should ${ + checkSpans[method.methodName] ? '' : 'not ' + }create spans for grpc remote method ${method.methodName}`, () => { + runTest(method, provider, checkSpans[method.methodName]); + }); + }); + }); }); }; diff --git a/packages/opentelemetry-metrics/package.json b/packages/opentelemetry-metrics/package.json index 3ae12218367..8facdc3aacc 100644 --- a/packages/opentelemetry-metrics/package.json +++ b/packages/opentelemetry-metrics/package.json @@ -42,7 +42,7 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "@types/sinon": "9.0.4", "codecov": "3.7.2", @@ -50,7 +50,7 @@ "mocha": "7.2.0", "nyc": "15.1.0", "rimraf": "3.0.2", - "sinon": "9.0.2", + "sinon": "9.0.3", "ts-mocha": "7.0.0", "ts-node": "8.10.2", "typescript": "3.9.7" diff --git a/packages/opentelemetry-metrics/src/BaseObserverMetric.ts b/packages/opentelemetry-metrics/src/BaseObserverMetric.ts index 733d701ce3e..238a2961422 100644 --- a/packages/opentelemetry-metrics/src/BaseObserverMetric.ts +++ b/packages/opentelemetry-metrics/src/BaseObserverMetric.ts @@ -28,7 +28,8 @@ const NOOP_CALLBACK = () => {}; * This is a SDK implementation of Base Observer Metric. * All observers should extend this class */ -export abstract class BaseObserverMetric extends Metric +export abstract class BaseObserverMetric + extends Metric implements api.BaseObserver { protected _callback: (observerResult: api.ObserverResult) => unknown; @@ -36,7 +37,7 @@ export abstract class BaseObserverMetric extends Metric name: string, options: api.MetricOptions, private readonly _batcher: Batcher, - resource: Resource, + resource: Promise | Resource, metricKind: MetricKind, instrumentationLibrary: InstrumentationLibrary, callback?: (observerResult: api.ObserverResult) => unknown diff --git a/packages/opentelemetry-metrics/src/BatchObserverMetric.ts b/packages/opentelemetry-metrics/src/BatchObserverMetric.ts index 17e80634c49..ef56090dee2 100644 --- a/packages/opentelemetry-metrics/src/BatchObserverMetric.ts +++ b/packages/opentelemetry-metrics/src/BatchObserverMetric.ts @@ -27,7 +27,8 @@ const NOOP_CALLBACK = () => {}; const MAX_TIMEOUT_UPDATE_MS = 500; /** This is a SDK implementation of Batch Observer Metric. */ -export class BatchObserverMetric extends Metric +export class BatchObserverMetric + extends Metric implements api.BatchObserver { private _callback: (observerResult: api.BatchObserverResult) => void; private _maxTimeoutUpdateMS: number; @@ -36,7 +37,7 @@ export class BatchObserverMetric extends Metric name: string, options: api.BatchMetricOptions, private readonly _batcher: Batcher, - resource: Resource, + resource: Promise | Resource, instrumentationLibrary: InstrumentationLibrary, callback?: (observerResult: api.BatchObserverResult) => void ) { diff --git a/packages/opentelemetry-metrics/src/BoundInstrument.ts b/packages/opentelemetry-metrics/src/BoundInstrument.ts index dad6ce01eca..e84bb724876 100644 --- a/packages/opentelemetry-metrics/src/BoundInstrument.ts +++ b/packages/opentelemetry-metrics/src/BoundInstrument.ts @@ -38,6 +38,14 @@ export class BaseBoundInstrument { update(value: number): void { if (this._disabled) return; + if (typeof value !== 'number') { + this._logger.error( + `Metric cannot accept a non-number value for ${Object.values( + this._labels + )}.` + ); + return; + } if (this._valueType === api.ValueType.INT && !Number.isInteger(value)) { this._logger.warn( @@ -64,7 +72,8 @@ export class BaseBoundInstrument { * BoundCounter allows the SDK to observe/record a single metric event. The * value of single instrument in the `Counter` associated with specified Labels. */ -export class BoundCounter extends BaseBoundInstrument +export class BoundCounter + extends BaseBoundInstrument implements api.BoundCounter { constructor( labels: api.Labels, @@ -93,7 +102,8 @@ export class BoundCounter extends BaseBoundInstrument * The value of single instrument in the `UpDownCounter` associated with * specified Labels. */ -export class BoundUpDownCounter extends BaseBoundInstrument +export class BoundUpDownCounter + extends BaseBoundInstrument implements api.BoundCounter { constructor( labels: api.Labels, @@ -113,36 +123,20 @@ export class BoundUpDownCounter extends BaseBoundInstrument /** * BoundMeasure is an implementation of the {@link BoundMeasure} interface. */ -export class BoundValueRecorder extends BaseBoundInstrument +export class BoundValueRecorder + extends BaseBoundInstrument implements api.BoundValueRecorder { - private readonly _absolute: boolean; - constructor( labels: api.Labels, disabled: boolean, - absolute: boolean, valueType: api.ValueType, logger: api.Logger, aggregator: Aggregator ) { super(labels, logger, disabled, valueType, aggregator); - this._absolute = absolute; } - record( - value: number, - correlationContext?: api.CorrelationContext, - spanContext?: api.SpanContext - ): void { - if (this._absolute && value < 0) { - this._logger.error( - `Absolute ValueRecorder cannot contain negative values for $${Object.values( - this._labels - )}` - ); - return; - } - + record(value: number): void { this.update(value); } } @@ -150,7 +144,9 @@ export class BoundValueRecorder extends BaseBoundInstrument /** * BoundObserver is an implementation of the {@link BoundObserver} interface. */ -export class BoundObserver extends BaseBoundInstrument { +export class BoundObserver + extends BaseBoundInstrument + implements api.BoundBaseObserver { constructor( labels: api.Labels, disabled: boolean, diff --git a/packages/opentelemetry-metrics/src/CounterMetric.ts b/packages/opentelemetry-metrics/src/CounterMetric.ts index c3f35fb8bb4..ef5a5078bb4 100644 --- a/packages/opentelemetry-metrics/src/CounterMetric.ts +++ b/packages/opentelemetry-metrics/src/CounterMetric.ts @@ -28,7 +28,7 @@ export class CounterMetric extends Metric implements api.Counter { name: string, options: api.MetricOptions, private readonly _batcher: Batcher, - resource: Resource, + resource: Promise | Resource, instrumentationLibrary: InstrumentationLibrary ) { super(name, options, MetricKind.COUNTER, resource, instrumentationLibrary); diff --git a/packages/opentelemetry-metrics/src/Meter.ts b/packages/opentelemetry-metrics/src/Meter.ts index e6c33621137..9fcd344ebdb 100644 --- a/packages/opentelemetry-metrics/src/Meter.ts +++ b/packages/opentelemetry-metrics/src/Meter.ts @@ -30,6 +30,7 @@ import { DEFAULT_METRIC_OPTIONS, DEFAULT_CONFIG, MeterConfig } from './types'; import { Batcher, UngroupedBatcher } from './export/Batcher'; import { PushController } from './export/Controller'; import { NoopExporter } from './export/NoopExporter'; +import { MeterProvider } from '.'; /** * Meter is an implementation of the {@link Meter} interface. @@ -38,24 +39,26 @@ export class Meter implements api.Meter { private readonly _logger: api.Logger; private readonly _metrics = new Map>(); private readonly _batcher: Batcher; - private readonly _resource: Resource; + private readonly _resource: Promise; private readonly _instrumentationLibrary: InstrumentationLibrary; + private readonly _controller: PushController; /** * Constructs a new Meter instance. */ constructor( + meterProvider: MeterProvider, instrumentationLibrary: InstrumentationLibrary, config: MeterConfig = DEFAULT_CONFIG ) { this._logger = config.logger || new ConsoleLogger(config.logLevel); this._batcher = config.batcher ?? new UngroupedBatcher(); - this._resource = config.resource || Resource.createTelemetrySDKResource(); + this._resource = meterProvider.resource; this._instrumentationLibrary = instrumentationLibrary; // start the push controller const exporter = config.exporter || new NoopExporter(); const interval = config.interval; - new PushController(this, exporter, interval); + this._controller = new PushController(this, exporter, interval); } /** @@ -76,7 +79,6 @@ export class Meter implements api.Meter { const opt: api.MetricOptions = { logger: this._logger, ...DEFAULT_METRIC_OPTIONS, - absolute: true, // value recorders are defined as absolute by default ...options, }; @@ -309,6 +311,10 @@ export class Meter implements api.Meter { return this._batcher; } + async shutdown(): Promise { + await this._controller.shutdown(); + } + /** * Registers metric to register. * @param name The name of the metric. diff --git a/packages/opentelemetry-metrics/src/MeterProvider.ts b/packages/opentelemetry-metrics/src/MeterProvider.ts index b178593dfa4..2de94f343b7 100644 --- a/packages/opentelemetry-metrics/src/MeterProvider.ts +++ b/packages/opentelemetry-metrics/src/MeterProvider.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ConsoleLogger } from '@opentelemetry/core'; +import { ConsoleLogger, notifyOnGlobalShutdown } from '@opentelemetry/core'; import * as api from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; import { Meter } from '.'; @@ -26,16 +26,29 @@ import { DEFAULT_CONFIG, MeterConfig } from './types'; export class MeterProvider implements api.MeterProvider { private readonly _config: MeterConfig; private readonly _meters: Map = new Map(); - readonly resource: Resource; + readonly resource: Promise; + private _cleanNotifyOnGlobalShutdown: Function | undefined; readonly logger: api.Logger; constructor(config: MeterConfig = DEFAULT_CONFIG) { this.logger = config.logger ?? new ConsoleLogger(config.logLevel); - this.resource = config.resource ?? Resource.createTelemetrySDKResource(); + if (config.resource) { + this.resource = + config.resource instanceof Promise + ? config.resource + : Promise.resolve(config.resource); + } else { + this.resource = Promise.resolve(Resource.createTelemetrySDKResource()); + } this._config = Object.assign({}, config, { logger: this.logger, resource: this.resource, }); + if (this._config.gracefulShutdown) { + this._cleanNotifyOnGlobalShutdown = notifyOnGlobalShutdown( + this._shutdownAllMeters.bind(this) + ); + } } /** @@ -48,10 +61,29 @@ export class MeterProvider implements api.MeterProvider { if (!this._meters.has(key)) { this._meters.set( key, - new Meter({ name, version }, config || this._config) + new Meter(this, { name, version }, config || this._config) ); } return this._meters.get(key)!; } + + shutdown(cb: () => void = () => {}): void { + this._shutdownAllMeters().then(() => { + setTimeout(cb, 0); + }); + if (this._cleanNotifyOnGlobalShutdown) { + this._cleanNotifyOnGlobalShutdown(); + this._cleanNotifyOnGlobalShutdown = undefined; + } + } + + private _shutdownAllMeters() { + if (this._config.exporter) { + this._config.exporter.shutdown(); + } + return Promise.all( + Array.from(this._meters, ([_, meter]) => meter.shutdown()) + ); + } } diff --git a/packages/opentelemetry-metrics/src/Metric.ts b/packages/opentelemetry-metrics/src/Metric.ts index ab5b1c9acb7..ed8abf94160 100644 --- a/packages/opentelemetry-metrics/src/Metric.ts +++ b/packages/opentelemetry-metrics/src/Metric.ts @@ -34,7 +34,7 @@ export abstract class Metric private readonly _name: string, private readonly _options: api.MetricOptions, private readonly _kind: MetricKind, - public resource: Resource, + public resource: Promise | Resource, readonly instrumentationLibrary: InstrumentationLibrary ) { this._disabled = !!_options.disabled; @@ -78,24 +78,23 @@ export abstract class Metric } getMetricRecord(): Promise { - return new Promise(resolve => { - if (this.resource instanceof Promise) { - this.resource.then(resource => { - this.resource = resource; - this.getMetricRecord().then(resolve); - }); - } else { - resolve( - Array.from(this._instruments.values()).map(instrument => ({ - descriptor: this._descriptor, - labels: instrument.getLabels(), - aggregator: instrument.getAggregator(), - resource: this.resource, - instrumentationLibrary: this.instrumentationLibrary, - })) - ); - } - }); + if (this.resource instanceof Promise) { + return this.resource.then(() => { + this.resource = this.resource; + return this.getMetricRecord(); + }) + } + + const resource = this.resource; + + return Promise.resolve(Array.from(this._instruments.values()).map(instrument => ({ + descriptor: this._descriptor, + labels: instrument.getLabels(), + aggregator: instrument.getAggregator(), + resource: resource, + instrumentationLibrary: this.instrumentationLibrary, + }))); + } private _getMetricDescriptor(): MetricDescriptor { diff --git a/packages/opentelemetry-metrics/src/SumObserverMetric.ts b/packages/opentelemetry-metrics/src/SumObserverMetric.ts index 892ca934664..38a8a4a603c 100644 --- a/packages/opentelemetry-metrics/src/SumObserverMetric.ts +++ b/packages/opentelemetry-metrics/src/SumObserverMetric.ts @@ -24,13 +24,14 @@ import { Batcher } from './export/Batcher'; import { MetricKind } from './export/types'; /** This is a SDK implementation of SumObserver Metric. */ -export class SumObserverMetric extends BaseObserverMetric +export class SumObserverMetric + extends BaseObserverMetric implements api.SumObserver { constructor( name: string, options: api.MetricOptions, batcher: Batcher, - resource: Resource, + resource: Promise | Resource, instrumentationLibrary: InstrumentationLibrary, callback?: (observerResult: api.ObserverResult) => unknown ) { diff --git a/packages/opentelemetry-metrics/src/UpDownCounterMetric.ts b/packages/opentelemetry-metrics/src/UpDownCounterMetric.ts index 14eb1dc6f37..3d5e37a1fa6 100644 --- a/packages/opentelemetry-metrics/src/UpDownCounterMetric.ts +++ b/packages/opentelemetry-metrics/src/UpDownCounterMetric.ts @@ -23,13 +23,14 @@ import { Batcher } from './export/Batcher'; import { Metric } from './Metric'; /** This is a SDK implementation of UpDownCounter Metric. */ -export class UpDownCounterMetric extends Metric +export class UpDownCounterMetric + extends Metric implements api.UpDownCounter { constructor( name: string, options: api.MetricOptions, private readonly _batcher: Batcher, - resource: Resource, + resource: Promise | Resource, instrumentationLibrary: InstrumentationLibrary ) { super( diff --git a/packages/opentelemetry-metrics/src/UpDownSumObserverMetric.ts b/packages/opentelemetry-metrics/src/UpDownSumObserverMetric.ts index c96b34d7fcd..c9e17996312 100644 --- a/packages/opentelemetry-metrics/src/UpDownSumObserverMetric.ts +++ b/packages/opentelemetry-metrics/src/UpDownSumObserverMetric.ts @@ -22,13 +22,14 @@ import { Batcher } from './export/Batcher'; import { MetricKind } from './export/types'; /** This is a SDK implementation of UpDownSumObserver Metric. */ -export class UpDownSumObserverMetric extends BaseObserverMetric +export class UpDownSumObserverMetric + extends BaseObserverMetric implements api.UpDownSumObserver { constructor( name: string, options: api.MetricOptions, batcher: Batcher, - resource: Resource, + resource: Promise | Resource, instrumentationLibrary: InstrumentationLibrary, callback?: (observerResult: api.ObserverResult) => unknown ) { diff --git a/packages/opentelemetry-metrics/src/ValueObserverMetric.ts b/packages/opentelemetry-metrics/src/ValueObserverMetric.ts index e71227db43e..444f5897211 100644 --- a/packages/opentelemetry-metrics/src/ValueObserverMetric.ts +++ b/packages/opentelemetry-metrics/src/ValueObserverMetric.ts @@ -21,13 +21,14 @@ import { Batcher } from './export/Batcher'; import { MetricKind } from './export/types'; /** This is a SDK implementation of Value Observer Metric. */ -export class ValueObserverMetric extends BaseObserverMetric +export class ValueObserverMetric + extends BaseObserverMetric implements api.ValueObserver { constructor( name: string, options: api.MetricOptions, batcher: Batcher, - resource: Resource, + resource: Promise | Resource, instrumentationLibrary: InstrumentationLibrary, callback?: (observerResult: api.ObserverResult) => unknown ) { diff --git a/packages/opentelemetry-metrics/src/ValueRecorderMetric.ts b/packages/opentelemetry-metrics/src/ValueRecorderMetric.ts index e23e3daa72a..eee04dbcc4c 100644 --- a/packages/opentelemetry-metrics/src/ValueRecorderMetric.ts +++ b/packages/opentelemetry-metrics/src/ValueRecorderMetric.ts @@ -23,15 +23,14 @@ import { MetricKind } from './export/types'; import { Metric } from './Metric'; /** This is a SDK implementation of Value Recorder Metric. */ -export class ValueRecorderMetric extends Metric +export class ValueRecorderMetric + extends Metric implements api.ValueRecorder { - protected readonly _absolute: boolean; - constructor( name: string, options: api.MetricOptions, private readonly _batcher: Batcher, - resource: Resource, + resource: Promise | Resource, instrumentationLibrary: InstrumentationLibrary ) { super( @@ -41,14 +40,12 @@ export class ValueRecorderMetric extends Metric resource, instrumentationLibrary ); - - this._absolute = options.absolute !== undefined ? options.absolute : true; // Absolute default is true } + protected _makeInstrument(labels: api.Labels): BoundValueRecorder { return new BoundValueRecorder( labels, this._disabled, - this._absolute, this._valueType, this._logger, this._batcher.aggregatorFor(this._descriptor) diff --git a/packages/opentelemetry-metrics/src/export/Controller.ts b/packages/opentelemetry-metrics/src/export/Controller.ts index 0b63ba12cf0..7af62bc97d4 100644 --- a/packages/opentelemetry-metrics/src/export/Controller.ts +++ b/packages/opentelemetry-metrics/src/export/Controller.ts @@ -38,12 +38,25 @@ export class PushController extends Controller { unrefTimer(this._timer); } - private _collect() { - this._meter.collect(); - this._exporter.export(this._meter.getBatcher().checkPointSet(), result => { - if (result !== ExportResult.SUCCESS) { - // @todo: log error - } + async shutdown(): Promise { + clearInterval(this._timer); + await this._collect(); + } + + private async _collect(): Promise { + await this._meter.collect(); + return new Promise((resolve, reject) => { + this._exporter.export( + this._meter.getBatcher().checkPointSet(), + result => { + if (result === ExportResult.SUCCESS) { + resolve(); + } else { + // @todo log error + reject(); + } + } + ); }); } } diff --git a/packages/opentelemetry-metrics/src/export/aggregators/Histogram.ts b/packages/opentelemetry-metrics/src/export/aggregators/Histogram.ts index 17709521778..a44ea9836bb 100644 --- a/packages/opentelemetry-metrics/src/export/aggregators/Histogram.ts +++ b/packages/opentelemetry-metrics/src/export/aggregators/Histogram.ts @@ -39,7 +39,7 @@ export class HistogramAggregator implements HistogramAggregatorType { } // we need to an ordered set to be able to correctly compute count for each // boundary since we'll iterate on each in order. - this._boundaries = boundaries.sort(); + this._boundaries = boundaries.sort((a, b) => a - b); this._current = this._newEmptyCheckpoint(); this._lastUpdateTime = hrTime(); } diff --git a/packages/opentelemetry-metrics/src/export/types.ts b/packages/opentelemetry-metrics/src/export/types.ts index 37f8912acc2..99ff678047b 100644 --- a/packages/opentelemetry-metrics/src/export/types.ts +++ b/packages/opentelemetry-metrics/src/export/types.ts @@ -53,21 +53,19 @@ export interface Distribution { export interface Histogram { /** - * Buckets are implemented using two different array: - * - boundaries contains every boundary (which are upper boundary for each slice) - * - counts contains count of event for each slice + * Buckets are implemented using two different arrays: + * - boundaries: contains every finite bucket boundary, which are inclusive lower bounds + * - counts: contains event counts for each bucket * - * Note that we'll always have n+1 (where n is the number of boundaries) slice - * because we need to count event that are above the highest boundary. This is the - * reason why it's not implement using array of object, because the last slice - * dont have any boundary. + * Note that we'll always have n+1 buckets, where n is the number of boundaries. + * This is because we need to count events that are below the lowest boundary. * - * Example if we measure the values: [5, 30, 5, 40, 5, 15, 15, 15, 25] + * Example: if we measure the values: [5, 30, 5, 40, 5, 15, 15, 15, 25] * with the boundaries [ 10, 20, 30 ], we will have the following state: * * buckets: { * boundaries: [10, 20, 30], - * counts: [3, 3, 2, 1], + * counts: [3, 3, 1, 2], * } */ buckets: { diff --git a/packages/opentelemetry-metrics/src/types.ts b/packages/opentelemetry-metrics/src/types.ts index cc01af9f8d9..3860ef5fbfe 100644 --- a/packages/opentelemetry-metrics/src/types.ts +++ b/packages/opentelemetry-metrics/src/types.ts @@ -35,21 +35,24 @@ export interface MeterConfig { interval?: number; /** Resource associated with metric telemetry */ - resource?: Resource; + resource?: Resource | Promise; /** Metric batcher. */ batcher?: Batcher; + + /** Bool for whether or not graceful shutdown is enabled. If disabled metrics will not be exported when SIGTERM is recieved */ + gracefulShutdown?: boolean; } /** Default Meter configuration. */ export const DEFAULT_CONFIG = { logLevel: getEnv().OTEL_LOG_LEVEL, + gracefulShutdown: true, }; /** The default metric creation options value. */ export const DEFAULT_METRIC_OPTIONS = { disabled: false, - absolute: false, description: '', unit: '1', valueType: api.ValueType.DOUBLE, diff --git a/packages/opentelemetry-metrics/test/Meter.test.ts b/packages/opentelemetry-metrics/test/Meter.test.ts index 074415dc981..5d26e1c1b1b 100644 --- a/packages/opentelemetry-metrics/test/Meter.test.ts +++ b/packages/opentelemetry-metrics/test/Meter.test.ts @@ -41,6 +41,35 @@ import { Resource } from '@opentelemetry/resources'; import { UpDownSumObserverMetric } from '../src/UpDownSumObserverMetric'; import { hashLabels } from '../src/Utils'; import { Batcher } from '../src/export/Batcher'; +import { ValueType } from '@opentelemetry/api'; + +const nonNumberValues = [ + // type undefined + undefined, + // type null + null, + // type function + function () {}, + // type boolean + true, + false, + // type string + '1', + // type object + {}, + // type symbol + // symbols cannot be cast to number, early errors will be thrown. +]; + +if (Number(process.versions.node.match(/^\d+/)) >= 10) { + nonNumberValues.push( + // type bigint + // Preferring BigInt builtin object instead of bigint literal to keep Node.js v8.x working. + // TODO: should metric instruments support bigint? + // @ts-ignore + BigInt(1) // eslint-disable-line node/no-unsupported-features/es-builtins + ); +} describe('Meter', () => { let meter: Meter; @@ -132,7 +161,7 @@ describe('Meter', () => { const [record] = await counter.getMetricRecord(); assert.ok(record.resource instanceof Resource); - assert.deepStrictEqual(record.resource.labels, { foo: 'bar' }); + assert.deepStrictEqual(record.resource.attributes, { foo: 'bar' }); }); it('should pipe through instrumentation library', async () => { @@ -395,6 +424,56 @@ describe('Meter', () => { assert.strictEqual(record1.aggregator.toPoint().value, 20); assert.strictEqual(boundCounter, boundCounter1); }); + + it('should truncate non-integer values for INT valueType', async () => { + const upDownCounter = meter.createUpDownCounter('name', { + valueType: ValueType.INT, + }); + const boundCounter = upDownCounter.bind(labels); + + [-1.1, 2.2].forEach(val => { + boundCounter.add(val); + }); + await meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + assert.strictEqual(record1.aggregator.toPoint().value, 1); + }); + + it('should ignore non-number values for INT valueType', async () => { + const upDownCounter = meter.createUpDownCounter('name', { + valueType: ValueType.DOUBLE, + }); + const boundCounter = upDownCounter.bind(labels); + + await Promise.all( + nonNumberValues.map(async val => { + // @ts-expect-error + boundCounter.add(val); + await meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + + assert.strictEqual(record1.aggregator.toPoint().value, 0); + }) + ); + }); + + it('should ignore non-number values for DOUBLE valueType', async () => { + const upDownCounter = meter.createUpDownCounter('name', { + valueType: ValueType.DOUBLE, + }); + const boundCounter = upDownCounter.bind(labels); + + await Promise.all( + nonNumberValues.map(async val => { + // @ts-expect-error + boundCounter.add(val); + await meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + + assert.strictEqual(record1.aggregator.toPoint().value, 0); + }) + ); + }); }); describe('.unbind()', () => { @@ -497,31 +576,6 @@ describe('Meter', () => { assert.ok(valueRecorder instanceof Metric); }); - it('should be absolute by default', () => { - const valueRecorder = meter.createValueRecorder('name', { - description: 'desc', - unit: '1', - disabled: false, - }); - assert.strictEqual( - (valueRecorder as ValueRecorderMetric)['_absolute'], - true - ); - }); - - it('should be able to set absolute to false', () => { - const valueRecorder = meter.createValueRecorder('name', { - description: 'desc', - unit: '1', - disabled: false, - absolute: false, - }); - assert.strictEqual( - (valueRecorder as ValueRecorderMetric)['_absolute'], - false - ); - }); - it('should pipe through resource', async () => { const valueRecorder = meter.createValueRecorder( 'name' @@ -580,10 +634,12 @@ describe('Meter', () => { assert.doesNotThrow(() => boundValueRecorder.record(10)); }); - it('should not accept negative values by default', async () => { - const valueRecorder = meter.createValueRecorder('name'); + it('should not set the instrument data when disabled', async () => { + const valueRecorder = meter.createValueRecorder('name', { + disabled: true, + }) as ValueRecorderMetric; const boundValueRecorder = valueRecorder.bind(labels); - boundValueRecorder.record(-10); + boundValueRecorder.record(10); await meter.collect(); const [record1] = meter.getBatcher().checkPointSet(); @@ -599,57 +655,30 @@ describe('Meter', () => { ); }); - it('should not set the instrument data when disabled', async () => { - const valueRecorder = meter.createValueRecorder('name', { - disabled: true, - }) as ValueRecorderMetric; + it('should accept negative (and positive) values', async () => { + const valueRecorder = meter.createValueRecorder('name'); const boundValueRecorder = valueRecorder.bind(labels); - boundValueRecorder.record(10); + boundValueRecorder.record(-10); + boundValueRecorder.record(50); await meter.collect(); const [record1] = meter.getBatcher().checkPointSet(); assert.deepStrictEqual( record1.aggregator.toPoint().value as Distribution, { - count: 0, - last: 0, - max: -Infinity, - min: Infinity, - sum: 0, + count: 2, + last: 50, + max: 50, + min: -10, + sum: 40, } ); + assert.ok( + hrTimeToNanoseconds(record1.aggregator.toPoint().timestamp) > + hrTimeToNanoseconds(performanceTimeOrigin) + ); }); - it( - 'should accept negative (and positive) values when absolute is set' + - ' to false', - async () => { - const valueRecorder = meter.createValueRecorder('name', { - absolute: false, - }); - const boundValueRecorder = valueRecorder.bind(labels); - boundValueRecorder.record(-10); - boundValueRecorder.record(50); - - await meter.collect(); - const [record1] = meter.getBatcher().checkPointSet(); - assert.deepStrictEqual( - record1.aggregator.toPoint().value as Distribution, - { - count: 2, - last: 50, - max: 50, - min: -10, - sum: 40, - } - ); - assert.ok( - hrTimeToNanoseconds(record1.aggregator.toPoint().timestamp) > - hrTimeToNanoseconds(performanceTimeOrigin) - ); - } - ); - it('should return same instrument on same label values', async () => { const valueRecorder = meter.createValueRecorder( 'name' @@ -672,6 +701,33 @@ describe('Meter', () => { ); assert.strictEqual(boundValueRecorder1, boundValueRecorder2); }); + + it('should ignore non-number values', async () => { + const valueRecorder = meter.createValueRecorder( + 'name' + ) as ValueRecorderMetric; + const boundValueRecorder = valueRecorder.bind(labels); + + await Promise.all( + nonNumberValues.map(async val => { + // @ts-expect-error + boundValueRecorder.record(val); + await meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + + assert.deepStrictEqual( + record1.aggregator.toPoint().value as Distribution, + { + count: 0, + last: 0, + max: -Infinity, + min: Infinity, + sum: 0, + } + ); + }) + ); + }); }); describe('.unbind()', () => { diff --git a/packages/opentelemetry-metrics/test/MeterProvider.test.ts b/packages/opentelemetry-metrics/test/MeterProvider.test.ts index 7156e12e7cc..55cdafd66be 100644 --- a/packages/opentelemetry-metrics/test/MeterProvider.test.ts +++ b/packages/opentelemetry-metrics/test/MeterProvider.test.ts @@ -15,10 +15,29 @@ */ import * as assert from 'assert'; +import * as sinon from 'sinon'; import { MeterProvider, Meter, CounterMetric } from '../src'; -import { NoopLogger } from '@opentelemetry/core'; +import { + NoopLogger, + notifyOnGlobalShutdown, + _invokeGlobalShutdown, +} from '@opentelemetry/core'; describe('MeterProvider', () => { + let removeEvent: Function | undefined; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + if (removeEvent) { + removeEvent(); + removeEvent = undefined; + } + }); + describe('constructor', () => { it('should construct an instance without any options', () => { const provider = new MeterProvider(); @@ -73,4 +92,61 @@ describe('MeterProvider', () => { assert.notEqual(meter3, meter4); }); }); + + describe('shutdown()', () => { + it('should call shutdown when SIGTERM is received', () => { + const meterProvider = new MeterProvider({ + interval: Math.pow(2, 31) - 1, + gracefulShutdown: true, + }); + const shutdownStub1 = sandbox.stub( + meterProvider.getMeter('meter1'), + 'shutdown' + ); + const shutdownStub2 = sandbox.stub( + meterProvider.getMeter('meter2'), + 'shutdown' + ); + removeEvent = notifyOnGlobalShutdown(() => { + sinon.assert.calledOnce(shutdownStub1); + sinon.assert.calledOnce(shutdownStub2); + }); + _invokeGlobalShutdown(); + }); + + it('should call shutdown when manually invoked', () => { + const meterProvider = new MeterProvider({ + interval: Math.pow(2, 31) - 1, + gracefulShutdown: true, + }); + const sandbox = sinon.createSandbox(); + const shutdownStub1 = sandbox.stub( + meterProvider.getMeter('meter1'), + 'shutdown' + ); + const shutdownStub2 = sandbox.stub( + meterProvider.getMeter('meter2'), + 'shutdown' + ); + meterProvider.shutdown(() => { + sinon.assert.calledOnce(shutdownStub1); + sinon.assert.calledOnce(shutdownStub2); + }); + }); + + it('should not trigger shutdown if graceful shutdown is turned off', () => { + const meterProvider = new MeterProvider({ + interval: Math.pow(2, 31) - 1, + gracefulShutdown: false, + }); + const shutdownStub = sandbox.stub( + meterProvider.getMeter('meter1'), + 'shutdown' + ); + removeEvent = notifyOnGlobalShutdown(() => { + sinon.assert.notCalled(shutdownStub); + }); + _invokeGlobalShutdown(); + }); + }); }); diff --git a/packages/opentelemetry-metrics/test/export/aggregators/Histogram.test.ts b/packages/opentelemetry-metrics/test/export/aggregators/Histogram.test.ts index 9a2b43938c4..f2882858c23 100644 --- a/packages/opentelemetry-metrics/test/export/aggregators/Histogram.test.ts +++ b/packages/opentelemetry-metrics/test/export/aggregators/Histogram.test.ts @@ -27,9 +27,23 @@ describe('HistogramAggregator', () => { }); it('should sort boundaries', () => { - const aggregator = new HistogramAggregator([500, 300, 700]); + const aggregator = new HistogramAggregator([ + 200, + 500, + 300, + 700, + 1000, + 1500, + ]); const point = aggregator.toPoint().value as Histogram; - assert.deepEqual(point.buckets.boundaries, [300, 500, 700]); + assert.deepEqual(point.buckets.boundaries, [ + 200, + 300, + 500, + 700, + 1000, + 1500, + ]); }); it('should throw if no boundaries are defined', () => { @@ -72,6 +86,17 @@ describe('HistogramAggregator', () => { assert.equal(point.buckets.counts[1], 0); assert.equal(point.buckets.counts[2], 1); }); + + it('should update the third bucket since boundaries are inclusive lower bounds', () => { + const aggregator = new HistogramAggregator([100, 200]); + aggregator.update(200); + const point = aggregator.toPoint().value as Histogram; + assert.equal(point.count, 1); + assert.equal(point.sum, 200); + assert.equal(point.buckets.counts[0], 0); + assert.equal(point.buckets.counts[1], 0); + assert.equal(point.buckets.counts[2], 1); + }); }); describe('.count', () => { diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index 2cc7cbb7131..02fef94268a 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -44,9 +44,9 @@ "devDependencies": { "@opentelemetry/context-base": "^0.10.2", "@opentelemetry/resources": "^0.10.2", - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", - "@types/semver": "7.3.1", + "@types/semver": "7.3.2", "@types/shimmer": "1.0.1", "codecov": "3.7.2", "gts": "2.0.2", diff --git a/packages/opentelemetry-node/src/config.ts b/packages/opentelemetry-node/src/config.ts index 1d04cd868ef..7b2933c3ea8 100644 --- a/packages/opentelemetry-node/src/config.ts +++ b/packages/opentelemetry-node/src/config.ts @@ -38,4 +38,6 @@ export const DEFAULT_INSTRUMENTATION_PLUGINS: Plugins = { ioredis: { enabled: true, path: '@opentelemetry/plugin-ioredis' }, 'pg-pool': { enabled: true, path: '@opentelemetry/plugin-pg-pool' }, express: { enabled: true, path: '@opentelemetry/plugin-express' }, + '@hapi/hapi': { enabled: true, path: '@opentelemetry/hapi-instrumentation' }, + koa: { enabled: true, path: '@opentelemetry/koa-instrumentation' }, }; diff --git a/packages/opentelemetry-node/test/NodeTracerProvider.test.ts b/packages/opentelemetry-node/test/NodeTracerProvider.test.ts index 1846819bf4b..d1b00006b13 100644 --- a/packages/opentelemetry-node/test/NodeTracerProvider.test.ts +++ b/packages/opentelemetry-node/test/NodeTracerProvider.test.ts @@ -193,7 +193,7 @@ describe('NodeTracerProvider', () => { assert.ok(span); assert.ok(span.resource instanceof Resource); assert.equal( - span.resource.labels[TELEMETRY_SDK_RESOURCE.LANGUAGE], + span.resource.attributes[TELEMETRY_SDK_RESOURCE.LANGUAGE], 'nodejs' ); }); diff --git a/packages/opentelemetry-node/test/registration.test.ts b/packages/opentelemetry-node/test/registration.test.ts index af4f08183c9..aef79760f51 100644 --- a/packages/opentelemetry-node/test/registration.test.ts +++ b/packages/opentelemetry-node/test/registration.test.ts @@ -16,9 +16,10 @@ import { context, - NoopHttpTextPropagator, + NoopTextMapPropagator, propagation, trace, + ProxyTracerProvider, } from '@opentelemetry/api'; import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; import { NoopContextManager } from '@opentelemetry/context-base'; @@ -43,14 +44,17 @@ describe('API registration', () => { assert.ok( propagation['_getGlobalPropagator']() instanceof CompositePropagator ); - assert.ok(trace.getTracerProvider() === tracerProvider); + const apiTracerProvider = trace.getTracerProvider(); + + assert.ok(apiTracerProvider instanceof ProxyTracerProvider); + assert.ok(apiTracerProvider.getDelegate() === tracerProvider); }); it('should register configured implementations', () => { const tracerProvider = new NodeTracerProvider(); const contextManager = new NoopContextManager(); - const propagator = new NoopHttpTextPropagator(); + const propagator = new NoopTextMapPropagator(); tracerProvider.register({ contextManager, @@ -60,7 +64,9 @@ describe('API registration', () => { assert.ok(context['_getContextManager']() === contextManager); assert.ok(propagation['_getGlobalPropagator']() === propagator); - assert.ok(trace.getTracerProvider() === tracerProvider); + const apiTracerProvider = trace.getTracerProvider(); + assert.ok(apiTracerProvider instanceof ProxyTracerProvider); + assert.ok(apiTracerProvider.getDelegate() === tracerProvider); }); it('should skip null context manager', () => { @@ -74,7 +80,10 @@ describe('API registration', () => { assert.ok( propagation['_getGlobalPropagator']() instanceof CompositePropagator ); - assert.ok(trace.getTracerProvider() === tracerProvider); + + const apiTracerProvider = trace.getTracerProvider(); + assert.ok(apiTracerProvider instanceof ProxyTracerProvider); + assert.ok(apiTracerProvider.getDelegate() === tracerProvider); }); it('should skip null propagator', () => { @@ -84,12 +93,15 @@ describe('API registration', () => { }); assert.ok( - propagation['_getGlobalPropagator']() instanceof NoopHttpTextPropagator + propagation['_getGlobalPropagator']() instanceof NoopTextMapPropagator ); assert.ok( context['_getContextManager']() instanceof AsyncHooksContextManager ); - assert.ok(trace.getTracerProvider() === tracerProvider); + + const apiTracerProvider = trace.getTracerProvider(); + assert.ok(apiTracerProvider instanceof ProxyTracerProvider); + assert.ok(apiTracerProvider.getDelegate() === tracerProvider); }); }); diff --git a/packages/opentelemetry-plugin-fetch/package.json b/packages/opentelemetry-plugin-fetch/package.json index a2b8dced049..62eda21d84d 100644 --- a/packages/opentelemetry-plugin-fetch/package.json +++ b/packages/opentelemetry-plugin-fetch/package.json @@ -47,7 +47,7 @@ "@babel/core": "7.11.1", "@opentelemetry/context-zone": "^0.10.2", "@opentelemetry/tracing": "^0.10.2", - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "@types/shimmer": "1.0.1", "@types/sinon": "7.5.2", diff --git a/packages/opentelemetry-plugin-grpc-js/README.md b/packages/opentelemetry-plugin-grpc-js/README.md index 4206a3050c9..abceb32d704 100644 --- a/packages/opentelemetry-plugin-grpc-js/README.md +++ b/packages/opentelemetry-plugin-grpc-js/README.md @@ -32,6 +32,7 @@ const provider = new NodeTracerProvider({ enabled: true, // You may use a package name or absolute path to the file. path: '@opentelemetry/plugin-grpc-js', + // gRPC-js plugin options } } }); @@ -47,6 +48,14 @@ const provider = new NodeTracerProvider(); +### gRPC-js Plugin Options + +gRPC-js plugin accepts the following configuration: + +| Options | Type | Description | +| ------- | ---- | ----------- | +| [`ignoreGrpcMethods`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-plugin-grpc-js/src/types.ts#L24) | `IgnoreMatcher[]` | gRPC plugin will not trace any methods that match anything in this list. You may pass a string (case-insensitive match), a `RegExp` object, or a filter function. | + ## Useful links - For more information on OpenTelemetry, visit: diff --git a/packages/opentelemetry-plugin-grpc-js/package.json b/packages/opentelemetry-plugin-grpc-js/package.json index f45b2258cbc..7493356fa48 100644 --- a/packages/opentelemetry-plugin-grpc-js/package.json +++ b/packages/opentelemetry-plugin-grpc-js/package.json @@ -50,9 +50,9 @@ "@opentelemetry/grpc-utils": "^0.10.2", "@opentelemetry/node": "^0.10.2", "@opentelemetry/tracing": "^0.10.2", - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", - "@types/semver": "7.3.1", + "@types/semver": "7.3.2", "@types/shimmer": "1.0.1", "@types/sinon": "9.0.4", "codecov": "3.7.2", @@ -61,7 +61,7 @@ "nyc": "15.1.0", "rimraf": "3.0.2", "semver": "7.3.2", - "sinon": "9.0.2", + "sinon": "9.0.3", "ts-mocha": "7.0.0", "ts-node": "8.10.2", "typescript": "3.9.7" diff --git a/packages/opentelemetry-plugin-grpc-js/src/client/loadPackageDefinition.ts b/packages/opentelemetry-plugin-grpc-js/src/client/loadPackageDefinition.ts index 46f0602cfa7..f3bbc1f0e9a 100644 --- a/packages/opentelemetry-plugin-grpc-js/src/client/loadPackageDefinition.ts +++ b/packages/opentelemetry-plugin-grpc-js/src/client/loadPackageDefinition.ts @@ -58,7 +58,7 @@ function _patchLoadedPackage( if (typeof service === 'function') { shimmer.massWrap( service.prototype, - getMethodsToWrap(service, service.service), + getMethodsToWrap.call(this, service, service.service), getPatchedClientMethods.call(this) ); } else if (typeof service.format !== 'string') { diff --git a/packages/opentelemetry-plugin-grpc-js/src/client/patchClient.ts b/packages/opentelemetry-plugin-grpc-js/src/client/patchClient.ts index 70de6e9d1db..5685c18fa42 100644 --- a/packages/opentelemetry-plugin-grpc-js/src/client/patchClient.ts +++ b/packages/opentelemetry-plugin-grpc-js/src/client/patchClient.ts @@ -40,7 +40,7 @@ export function patchClient( const client = original.call(this, methods, serviceName, options); shimmer.massWrap( client.prototype, - getMethodsToWrap(client, methods), + getMethodsToWrap.call(plugin, client, methods), getPatchedClientMethods.call(plugin) ); return client; diff --git a/packages/opentelemetry-plugin-grpc-js/src/client/utils.ts b/packages/opentelemetry-plugin-grpc-js/src/client/utils.ts index f1b177ccf6e..9c9b2b029f2 100644 --- a/packages/opentelemetry-plugin-grpc-js/src/client/utils.ts +++ b/packages/opentelemetry-plugin-grpc-js/src/client/utils.ts @@ -29,6 +29,8 @@ import { grpcStatusCodeToSpanStatus, grpcStatusCodeToCanonicalCode, CALL_SPAN_ENDED, + containsOtelMetadata, + methodIsIgnored, } from '../utils'; import { EventEmitter } from 'events'; @@ -37,6 +39,7 @@ import { EventEmitter } from 'events'; * with both possible casings e.g. "TestMethod" & "testMethod" */ export function getMethodsToWrap( + this: GrpcJsPlugin, client: typeof grpcJs.Client, methods: { [key: string]: { originalName?: string } } ): string[] { @@ -44,15 +47,17 @@ export function getMethodsToWrap( // For a method defined in .proto as "UnaryMethod" Object.entries(methods).forEach(([name, { originalName }]) => { - methodList.push(name); // adds camel case method name: "unaryMethod" - if ( - originalName && - // eslint-disable-next-line no-prototype-builtins - client.prototype.hasOwnProperty(originalName) && - name !== originalName // do not add duplicates - ) { - // adds original method name: "UnaryMethod", - methodList.push(originalName); + if (!methodIsIgnored(name, this._config.ignoreGrpcMethods)) { + methodList.push(name); // adds camel case method name: "unaryMethod" + if ( + originalName && + // eslint-disable-next-line no-prototype-builtins + client.prototype.hasOwnProperty(originalName) && + name !== originalName // do not add duplicates + ) { + // adds original method name: "UnaryMethod", + methodList.push(originalName); + } } }); @@ -71,11 +76,15 @@ export function getPatchedClientMethods( return function clientMethodTrace(this: grpcJs.Client) { const name = `grpc.${original.path.replace('/', '')}`; const args = [...arguments]; + const metadata = getMetadata.call(plugin, original, args); + if (containsOtelMetadata(metadata)) { + return original.apply(this, args); + } const span = plugin.tracer.startSpan(name, { kind: SpanKind.CLIENT, }); return plugin.tracer.withSpan(span, () => - makeGrpcClientRemoteCall(original, args, this, plugin)(span) + makeGrpcClientRemoteCall(original, args, metadata, this, plugin)(span) ); }; }; @@ -88,6 +97,7 @@ export function getPatchedClientMethods( export function makeGrpcClientRemoteCall( original: GrpcClientFunc, args: unknown[], + metadata: grpcJs.Metadata, self: grpcJs.Client, plugin: GrpcJsPlugin ): (span: Span) => EventEmitter { @@ -127,7 +137,6 @@ export function makeGrpcClientRemoteCall( } return (span: Span) => { - const metadata = getMetadata.call(plugin, original, args); // if unary or clientStream if (!original.responseStream) { const callbackFuncIndex = args.findIndex(arg => { diff --git a/packages/opentelemetry-plugin-grpc-js/src/grpcJs.ts b/packages/opentelemetry-plugin-grpc-js/src/grpcJs.ts index e3ec95db0c5..c95db012f36 100644 --- a/packages/opentelemetry-plugin-grpc-js/src/grpcJs.ts +++ b/packages/opentelemetry-plugin-grpc-js/src/grpcJs.ts @@ -21,6 +21,7 @@ import { patchClient, patchLoadPackageDefinition } from './client'; import { patchServer } from './server'; import { VERSION } from './version'; import { Tracer, Logger } from '@opentelemetry/api'; +import { GrpcPluginOptions } from './types'; /** * @grpc/grpc-js gRPC instrumentation plugin for Opentelemetry @@ -31,6 +32,8 @@ export class GrpcJsPlugin extends BasePlugin { readonly supportedVersions = ['1.*']; + protected _config!: GrpcPluginOptions; + constructor(readonly moduleName: string) { super('@opentelemetry/plugin-grpc-js', VERSION); } diff --git a/packages/opentelemetry-plugin-grpc-js/src/server/patchServer.ts b/packages/opentelemetry-plugin-grpc-js/src/server/patchServer.ts index f75dd294a5a..0e7a37db87e 100644 --- a/packages/opentelemetry-plugin-grpc-js/src/server/patchServer.ts +++ b/packages/opentelemetry-plugin-grpc-js/src/server/patchServer.ts @@ -18,7 +18,11 @@ import type * as grpcJs from '@grpc/grpc-js'; import type { HandleCall } from '@grpc/grpc-js/build/src/server-call'; import { GrpcJsPlugin } from '../grpcJs'; import * as shimmer from 'shimmer'; -import { ServerCallWithMeta, SendUnaryDataCallback } from '../types'; +import { + ServerCallWithMeta, + SendUnaryDataCallback, + IgnoreMatcher, +} from '../types'; import { context, SpanOptions, @@ -29,6 +33,7 @@ import { import { RpcAttribute } from '@opentelemetry/semantic-conventions'; import { clientStreamAndUnaryHandler } from './clientStreamAndUnary'; import { serverStreamAndBidiHandler } from './serverStreamAndBidi'; +import { containsOtelMetadata, methodIsIgnored } from '../utils'; type ServerRegisterFunction = typeof grpcJs.Server.prototype.register; @@ -41,6 +46,7 @@ export function patchServer( ): (originalRegister: ServerRegisterFunction) => ServerRegisterFunction { return (originalRegister: ServerRegisterFunction) => { const plugin = this; + const config = this._config; plugin.logger.debug('patched gRPC server'); return function register( @@ -72,6 +78,21 @@ export function patchServer( ) { const self = this; + if ( + shouldNotTraceServerCall( + call.metadata, + name, + config.ignoreGrpcMethods + ) + ) { + return handleUntracedServerFunction( + type, + originalFunc, + call, + callback + ); + } + const spanName = `grpc.${name.replace('/', '')}`; const spanOptions: SpanOptions = { kind: SpanKind.SERVER, @@ -111,6 +132,24 @@ export function patchServer( }; } +/** + * Returns true if the server call should not be traced. + */ +function shouldNotTraceServerCall( + metadata: grpcJs.Metadata, + methodName: string, + ignoreGrpcMethods?: IgnoreMatcher[] +): boolean { + const parsedName = methodName.split('/'); + return ( + containsOtelMetadata(metadata) || + methodIsIgnored( + parsedName[parsedName.length - 1] || methodName, + ignoreGrpcMethods + ) + ); +} + /** * Patch callback or EventEmitter provided by `originalFunc` and set appropriate `span` * properties based on its result. @@ -152,3 +191,27 @@ function handleServerFunction( break; } } + +/** + * Does not patch any callbacks or EventEmitters to omit tracing on requests + * that should not be traced. + */ +function handleUntracedServerFunction( + type: string, + originalFunc: HandleCall, + call: ServerCallWithMeta, + callback: SendUnaryDataCallback +): void { + switch (type) { + case 'unary': + case 'clientStream': + case 'client_stream': + return (originalFunc as Function).call({}, call, callback); + case 'serverStream': + case 'server_stream': + case 'bidi': + return (originalFunc as Function).call({}, call); + default: + break; + } +} diff --git a/packages/opentelemetry-plugin-grpc-js/src/types.ts b/packages/opentelemetry-plugin-grpc-js/src/types.ts index a41dbf48d35..0e26b59ddfb 100644 --- a/packages/opentelemetry-plugin-grpc-js/src/types.ts +++ b/packages/opentelemetry-plugin-grpc-js/src/types.ts @@ -17,6 +17,16 @@ import type * as grpcJs from '@grpc/grpc-js'; import type { EventEmitter } from 'events'; import type { CALL_SPAN_ENDED } from './utils'; +import { PluginConfig } from '@opentelemetry/api'; + +export type IgnoreMatcher = string | RegExp | ((str: string) => boolean); + +export interface GrpcPluginOptions extends PluginConfig { + /* Omits tracing on any gRPC methods that match any of + * the IgnoreMatchers in the ignoreGrpcMethods list + */ + ignoreGrpcMethods?: IgnoreMatcher[]; +} /** * Server Unary callback type diff --git a/packages/opentelemetry-plugin-grpc-js/src/utils.ts b/packages/opentelemetry-plugin-grpc-js/src/utils.ts index fcae2564423..bdb3806a872 100644 --- a/packages/opentelemetry-plugin-grpc-js/src/utils.ts +++ b/packages/opentelemetry-plugin-grpc-js/src/utils.ts @@ -16,6 +16,7 @@ import { CanonicalCode, Status } from '@opentelemetry/api'; import type * as grpcTypes from '@grpc/grpc-js'; // For types only +import { IgnoreMatcher } from './types'; /** * Symbol to include on grpc call if it has already emitted an error event. @@ -24,6 +25,11 @@ import type * as grpcTypes from '@grpc/grpc-js'; // For types only */ export const CALL_SPAN_ENDED = Symbol('opentelemetry call span ended'); +/** + * Metadata key used to denote an outgoing opentelemetry request. + */ +const OTEL_OUTGOING_REQUEST_KEY = 'x-opentelemetry-outgoing-request'; + /** * Convert a grpc status code to an opentelemetry Canonical code. For now, the enums are exactly the same * @param status @@ -44,3 +50,56 @@ export const grpcStatusCodeToCanonicalCode = ( export const grpcStatusCodeToSpanStatus = (status: number): Status => { return { code: status }; }; + +/** + * Returns true if the metadata contains + * the opentelemetry outgoing request header. + */ +export const containsOtelMetadata = (metadata: grpcTypes.Metadata): boolean => { + return metadata.get(OTEL_OUTGOING_REQUEST_KEY).length > 0; +}; + +/** + * Returns true if methodName matches pattern + * @param methodName the name of the method + * @param pattern Match pattern + */ +const satisfiesPattern = ( + methodName: string, + pattern: IgnoreMatcher +): boolean => { + if (typeof pattern === 'string') { + return pattern.toLowerCase() === methodName.toLowerCase(); + } else if (pattern instanceof RegExp) { + return pattern.test(methodName); + } else if (typeof pattern === 'function') { + return pattern(methodName); + } else { + return false; + } +}; + +/** + * Returns true if the current plugin configuration + * ignores the given method. + * @param methodName the name of the method + * @param ignoredMethods a list of matching patterns + * @param onException an error handler for matching exceptions + */ +export const methodIsIgnored = ( + methodName: string, + ignoredMethods?: IgnoreMatcher[] +): boolean => { + if (!ignoredMethods) { + // No ignored gRPC methods + return false; + } + + for (const pattern of ignoredMethods) { + if (satisfiesPattern(methodName, pattern)) { + return true; + } + } + + return false; +}; diff --git a/packages/opentelemetry-plugin-grpc/README.md b/packages/opentelemetry-plugin-grpc/README.md index 064b5be954b..9d750900090 100644 --- a/packages/opentelemetry-plugin-grpc/README.md +++ b/packages/opentelemetry-plugin-grpc/README.md @@ -32,6 +32,7 @@ const provider = new NodeTracerProvider({ enabled: true, // You may use a package name or absolute path to the file. path: '@opentelemetry/plugin-grpc', + // gRPC plugin options } } }); @@ -47,6 +48,14 @@ const provider = new NodeTracerProvider(); See [examples/grpc](https://github.com/open-telemetry/opentelemetry-js/tree/master/examples/grpc) for a short example. +### gRPC Plugin Options + +gRPC plugin accepts the following configuration: + +| Options | Type | Description | +| ------- | ---- | ----------- | +| [`ignoreGrpcMethods`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-plugin-grpc/src/types.ts#L32) | `IgnoreMatcher[]` | gRPC plugin will not trace any methods that match anything in this list. You may pass a string (case-insensitive match), a `RegExp` object, or a filter function. | + ## Useful links - For more information on OpenTelemetry, visit: diff --git a/packages/opentelemetry-plugin-grpc/package.json b/packages/opentelemetry-plugin-grpc/package.json index afbff373723..30f6d389aa5 100644 --- a/packages/opentelemetry-plugin-grpc/package.json +++ b/packages/opentelemetry-plugin-grpc/package.json @@ -47,9 +47,9 @@ "@opentelemetry/grpc-utils": "^0.10.2", "@opentelemetry/node": "^0.10.2", "@opentelemetry/tracing": "^0.10.2", - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", - "@types/semver": "7.3.1", + "@types/semver": "7.3.2", "@types/shimmer": "1.0.1", "@types/sinon": "9.0.4", "codecov": "3.7.2", @@ -60,7 +60,7 @@ "nyc": "15.1.0", "rimraf": "3.0.2", "semver": "7.3.2", - "sinon": "9.0.2", + "sinon": "9.0.3", "ts-mocha": "7.0.0", "ts-node": "8.10.2", "typescript": "3.9.7" diff --git a/packages/opentelemetry-plugin-grpc/src/grpc.ts b/packages/opentelemetry-plugin-grpc/src/grpc.ts index 133757f7c27..d93ba660e17 100644 --- a/packages/opentelemetry-plugin-grpc/src/grpc.ts +++ b/packages/opentelemetry-plugin-grpc/src/grpc.ts @@ -42,6 +42,8 @@ import { findIndex, _grpcStatusCodeToCanonicalCode, _grpcStatusCodeToSpanStatus, + _methodIsIgnored, + _containsOtelMetadata, } from './utils'; import { VERSION } from './version'; @@ -154,7 +156,22 @@ export class GrpcPlugin extends BasePlugin { callback: SendUnaryDataCallback ) { const self = this; - + if (plugin._shouldNotTraceServerCall(call, name)) { + switch (type) { + case 'unary': + case 'client_stream': + return (originalFunc as Function).call( + self, + call, + callback + ); + case 'server_stream': + case 'bidi': + return (originalFunc as Function).call(self, call); + default: + return originalResult; + } + } const spanName = `grpc.${name.replace('/', '')}`; const spanOptions: SpanOptions = { kind: SpanKind.SERVER, @@ -212,6 +229,23 @@ export class GrpcPlugin extends BasePlugin { }; } + /** + * Returns true if the server call should not be traced. + */ + private _shouldNotTraceServerCall( + call: ServerCallWithMeta, + name: string + ): boolean { + const parsedName = name.split('/'); + return ( + _containsOtelMetadata(call.metadata) || + _methodIsIgnored( + parsedName[parsedName.length - 1] || name, + this._config.ignoreGrpcMethods + ) + ); + } + private _clientStreamAndUnaryHandler( plugin: GrpcPlugin, span: Span, @@ -333,18 +367,19 @@ export class GrpcPlugin extends BasePlugin { // For a method defined in .proto as "UnaryMethod" Object.entries(methods).forEach(([name, { originalName }]) => { - methodList.push(name); // adds camel case method name: "unaryMethod" - if ( - originalName && - // eslint-disable-next-line no-prototype-builtins - client.prototype.hasOwnProperty(originalName) && - name !== originalName // do not add duplicates - ) { - // adds original method name: "UnaryMethod", - methodList.push(originalName); + if (!_methodIsIgnored(name, this._config.ignoreGrpcMethods)) { + methodList.push(name); // adds camel case method name: "unaryMethod" + if ( + originalName && + // eslint-disable-next-line no-prototype-builtins + client.prototype.hasOwnProperty(originalName) && + name !== originalName // do not add duplicates + ) { + // adds original method name: "UnaryMethod", + methodList.push(originalName); + } } }); - return methodList; } @@ -355,11 +390,21 @@ export class GrpcPlugin extends BasePlugin { return function clientMethodTrace(this: grpcTypes.Client) { const name = `grpc.${original.path.replace('/', '')}`; const args = Array.prototype.slice.call(arguments); + const metadata = plugin._getMetadata(original, args); + if (_containsOtelMetadata(metadata)) { + return original.apply(this, args); + } const span = plugin._tracer.startSpan(name, { kind: SpanKind.CLIENT, }); return plugin._tracer.withSpan(span, () => - plugin._makeGrpcClientRemoteCall(original, args, this, plugin)(span) + plugin._makeGrpcClientRemoteCall( + original, + args, + metadata, + this, + plugin + )(span) ); }; }; @@ -371,6 +416,7 @@ export class GrpcPlugin extends BasePlugin { private _makeGrpcClientRemoteCall( original: GrpcClientFunc, args: any[], + metadata: grpcTypes.Metadata, self: grpcTypes.Client, plugin: GrpcPlugin ) { @@ -415,7 +461,6 @@ export class GrpcPlugin extends BasePlugin { return original.apply(self, args); } - const metadata = this._getMetadata(original, args); // if unary or clientStream if (!original.responseStream) { const callbackFuncIndex = findIndex(args, arg => { diff --git a/packages/opentelemetry-plugin-grpc/src/types.ts b/packages/opentelemetry-plugin-grpc/src/types.ts index 56ee679c63f..e630fa9d956 100644 --- a/packages/opentelemetry-plugin-grpc/src/types.ts +++ b/packages/opentelemetry-plugin-grpc/src/types.ts @@ -16,9 +16,12 @@ import * as grpcModule from 'grpc'; import * as events from 'events'; +import { PluginConfig } from '@opentelemetry/api'; export type grpc = typeof grpcModule; +export type IgnoreMatcher = string | RegExp | ((str: string) => boolean); + export type SendUnaryDataCallback = ( error: grpcModule.ServiceError | null, value?: any, @@ -26,8 +29,12 @@ export type SendUnaryDataCallback = ( flags?: grpcModule.writeFlags ) => void; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface GrpcPluginOptions {} +export interface GrpcPluginOptions extends PluginConfig { + /* Omits tracing on any gRPC methods that match any of + * the IgnoreMatchers in the ignoreGrpcMethods list + */ + ignoreGrpcMethods?: IgnoreMatcher[]; +} interface GrpcStatus { code: number; diff --git a/packages/opentelemetry-plugin-grpc/src/utils.ts b/packages/opentelemetry-plugin-grpc/src/utils.ts index 9c9e5a336b7..4841fc9d036 100644 --- a/packages/opentelemetry-plugin-grpc/src/utils.ts +++ b/packages/opentelemetry-plugin-grpc/src/utils.ts @@ -16,6 +16,12 @@ import { CanonicalCode, Status } from '@opentelemetry/api'; import * as grpcTypes from 'grpc'; // For types only +import { IgnoreMatcher } from './types'; + +/** + * Metadata key used to denote an outgoing opentelemetry request. + */ +const _otRequestHeader = 'x-opentelemetry-outgoing-request'; // Equivalent to lodash _.findIndex export const findIndex: (args: T[], fn: (arg: T) => boolean) => number = ( @@ -48,3 +54,58 @@ export const _grpcStatusCodeToCanonicalCode = ( export const _grpcStatusCodeToSpanStatus = (status: number): Status => { return { code: status }; }; + +/** + * Returns true if the metadata contains + * the opentelemetry outgoing request header. + */ +export const _containsOtelMetadata = ( + metadata: grpcTypes.Metadata +): boolean => { + return metadata.get(_otRequestHeader).length > 0; +}; + +/** + * Returns true if methodName matches pattern + * @param methodName the name of the method + * @param pattern Match pattern + */ +const _satisfiesPattern = ( + methodName: string, + pattern: IgnoreMatcher +): boolean => { + if (typeof pattern === 'string') { + return pattern.toLowerCase() === methodName.toLowerCase(); + } else if (pattern instanceof RegExp) { + return pattern.test(methodName); + } else if (typeof pattern === 'function') { + return pattern(methodName); + } else { + return false; + } +}; + +/** + * Returns true if the current plugin configuration + * ignores the given method. + * @param methodName the name of the method + * @param ignoredMethods a list of matching patterns + * @param onException an error handler for matching exceptions + */ +export const _methodIsIgnored = ( + methodName: string, + ignoredMethods?: IgnoreMatcher[] +): boolean => { + if (!ignoredMethods) { + // No ignored gRPC methods + return false; + } + + for (const pattern of ignoredMethods) { + if (_satisfiesPattern(methodName, pattern)) { + return true; + } + } + + return false; +}; diff --git a/packages/opentelemetry-plugin-http/package.json b/packages/opentelemetry-plugin-http/package.json index b3656fa9d50..6914c301866 100644 --- a/packages/opentelemetry-plugin-http/package.json +++ b/packages/opentelemetry-plugin-http/package.json @@ -15,7 +15,8 @@ "precompile": "tsc --version", "version:update": "node ../../scripts/version-update.js", "compile": "npm run version:update && tsc -p .", - "prepare": "npm run compile" + "prepare": "npm run compile", + "watch": "tsc -w" }, "keywords": [ "opentelemetry", @@ -47,10 +48,10 @@ "@opentelemetry/node": "^0.10.2", "@opentelemetry/tracing": "^0.10.2", "@types/got": "9.6.11", - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "@types/request-promise-native": "1.0.17", - "@types/semver": "7.3.1", + "@types/semver": "7.3.2", "@types/shimmer": "1.0.1", "@types/sinon": "9.0.4", "@types/superagent": "4.1.9", @@ -64,8 +65,8 @@ "request": "2.88.2", "request-promise-native": "1.0.9", "rimraf": "3.0.2", - "sinon": "9.0.2", - "superagent": "5.3.1", + "sinon": "9.0.3", + "superagent": "6.0.0", "ts-mocha": "7.0.0", "ts-node": "8.10.2", "typescript": "3.9.7" diff --git a/packages/opentelemetry-plugin-http/src/utils.ts b/packages/opentelemetry-plugin-http/src/utils.ts index 90b4c0fe925..cfc9b43cbc4 100644 --- a/packages/opentelemetry-plugin-http/src/utils.ts +++ b/packages/opentelemetry-plugin-http/src/utils.ts @@ -46,6 +46,7 @@ export const HTTP_STATUS_SPECIAL_CASES: SpecialHttpStatusCodeMapping = { 403: CanonicalCode.PERMISSION_DENIED, 404: CanonicalCode.NOT_FOUND, 429: CanonicalCode.RESOURCE_EXHAUSTED, + 499: CanonicalCode.CANCELLED, 501: CanonicalCode.UNIMPLEMENTED, 503: CanonicalCode.UNAVAILABLE, 504: CanonicalCode.DEADLINE_EXCEEDED, diff --git a/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts index fe7ef01e51e..0c787501a94 100644 --- a/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts +++ b/packages/opentelemetry-plugin-http/test/utils/DummyPropagation.ts @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Context, HttpTextPropagator, TraceFlags } from '@opentelemetry/api'; +import { Context, TextMapPropagator, TraceFlags } from '@opentelemetry/api'; import { getParentSpanContext, setExtractedSpanContext, } from '@opentelemetry/core'; import * as http from 'http'; -export class DummyPropagation implements HttpTextPropagator { +export class DummyPropagation implements TextMapPropagator { static TRACE_CONTEXT_KEY = 'x-dummy-trace-id'; static SPAN_CONTEXT_KEY = 'x-dummy-span-id'; extract(context: Context, carrier: http.OutgoingHttpHeaders) { diff --git a/packages/opentelemetry-plugin-https/package.json b/packages/opentelemetry-plugin-https/package.json index 5877b7f6165..cea443846a6 100644 --- a/packages/opentelemetry-plugin-https/package.json +++ b/packages/opentelemetry-plugin-https/package.json @@ -47,10 +47,10 @@ "@opentelemetry/node": "^0.10.2", "@opentelemetry/tracing": "^0.10.2", "@types/got": "9.6.11", - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "@types/request-promise-native": "1.0.17", - "@types/semver": "7.3.1", + "@types/semver": "7.3.2", "@types/shimmer": "1.0.1", "@types/sinon": "9.0.4", "@types/superagent": "4.1.9", @@ -64,8 +64,8 @@ "request": "2.88.2", "request-promise-native": "1.0.9", "rimraf": "3.0.2", - "sinon": "9.0.2", - "superagent": "5.3.1", + "sinon": "9.0.3", + "superagent": "6.0.0", "ts-mocha": "7.0.0", "ts-node": "8.10.2", "typescript": "3.9.7" diff --git a/packages/opentelemetry-plugin-https/test/utils/DummyPropagation.ts b/packages/opentelemetry-plugin-https/test/utils/DummyPropagation.ts index 906efe79684..4acd3835dc4 100644 --- a/packages/opentelemetry-plugin-https/test/utils/DummyPropagation.ts +++ b/packages/opentelemetry-plugin-https/test/utils/DummyPropagation.ts @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Context, HttpTextPropagator, TraceFlags } from '@opentelemetry/api'; +import { Context, TextMapPropagator, TraceFlags } from '@opentelemetry/api'; import { setExtractedSpanContext, getParentSpanContext, } from '@opentelemetry/core'; import * as http from 'http'; -export class DummyPropagation implements HttpTextPropagator { +export class DummyPropagation implements TextMapPropagator { static TRACE_CONTEXT_KEY = 'x-dummy-trace-id'; static SPAN_CONTEXT_KEY = 'x-dummy-span-id'; extract(context: Context, carrier: http.OutgoingHttpHeaders) { diff --git a/packages/opentelemetry-plugin-xml-http-request/package.json b/packages/opentelemetry-plugin-xml-http-request/package.json index 2ec4f9ee190..4dfca75f1ee 100644 --- a/packages/opentelemetry-plugin-xml-http-request/package.json +++ b/packages/opentelemetry-plugin-xml-http-request/package.json @@ -47,7 +47,7 @@ "@babel/core": "7.11.1", "@opentelemetry/context-zone": "^0.10.2", "@opentelemetry/tracing": "^0.10.2", - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "@types/shimmer": "1.0.1", "@types/sinon": "9.0.4", @@ -65,7 +65,7 @@ "mocha": "7.2.0", "nyc": "15.1.0", "rimraf": "3.0.2", - "sinon": "9.0.2", + "sinon": "9.0.3", "ts-loader": "8.0.2", "ts-mocha": "7.0.0", "ts-node": "8.10.2", diff --git a/packages/opentelemetry-plugin-xml-http-request/src/xhr.ts b/packages/opentelemetry-plugin-xml-http-request/src/xhr.ts index c28d658f7b4..8252b15fc82 100644 --- a/packages/opentelemetry-plugin-xml-http-request/src/xhr.ts +++ b/packages/opentelemetry-plugin-xml-http-request/src/xhr.ts @@ -272,8 +272,9 @@ export class XMLHttpRequestPlugin extends BasePlugin { this._logger.debug('ignoring span as url matches ignored url'); return; } + const spanName = `HTTP ${method.toUpperCase()}`; - const currentSpan = this._tracer.startSpan(url, { + const currentSpan = this._tracer.startSpan(spanName, { kind: api.SpanKind.CLIENT, attributes: { [HttpAttribute.HTTP_METHOD]: method, diff --git a/packages/opentelemetry-plugin-xml-http-request/test/unmocked.test.ts b/packages/opentelemetry-plugin-xml-http-request/test/unmocked.test.ts new file mode 100644 index 00000000000..2423eda95a4 --- /dev/null +++ b/packages/opentelemetry-plugin-xml-http-request/test/unmocked.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { XMLHttpRequestPlugin } from '../src'; +import { ReadableSpan, SpanProcessor } from '@opentelemetry/tracing'; +import { WebTracerProvider } from '@opentelemetry/web'; +import assert = require('assert'); +import { HttpAttribute } from '@opentelemetry/semantic-conventions'; + +class TestSpanProcessor implements SpanProcessor { + spans: ReadableSpan[] = []; + + forceFlush(callback: () => void): void {} + onStart(span: ReadableSpan): void {} + shutdown(callback: () => void): void {} + + onEnd(span: ReadableSpan): void { + this.spans.push(span); + } +} + +describe('unmocked xhr', () => { + let testSpans: TestSpanProcessor; + let provider: WebTracerProvider; + beforeEach(() => { + provider = new WebTracerProvider({ + plugins: [new XMLHttpRequestPlugin()], + }); + testSpans = new TestSpanProcessor(); + provider.addSpanProcessor(testSpans); + }); + afterEach(() => { + // nop + }); + + it('should find resource with a relative url', done => { + const xhr = new XMLHttpRequest(); + let path = location.pathname; + path = path.substring(path.lastIndexOf('/') + 1); + xhr.open('GET', path); + xhr.addEventListener('loadend', () => { + setTimeout(() => { + assert.strictEqual(testSpans.spans.length, 1); + const span = testSpans.spans[0]; + // content length comes from the PerformanceTiming resource; this ensures that our + // matching logic found the right one + assert.ok( + (span.attributes[HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH] as any) > + 0 + ); + done(); + }, 500); + }); + xhr.send(); + }); +}); diff --git a/packages/opentelemetry-plugin-xml-http-request/test/xhr.test.ts b/packages/opentelemetry-plugin-xml-http-request/test/xhr.test.ts index 4491cb0fb9e..269e239ddf1 100644 --- a/packages/opentelemetry-plugin-xml-http-request/test/xhr.test.ts +++ b/packages/opentelemetry-plugin-xml-http-request/test/xhr.test.ts @@ -263,7 +263,7 @@ describe('xhr', () => { it('span should have correct name', () => { const span: tracing.ReadableSpan = exportSpy.args[1][0][0]; - assert.strictEqual(span.name, url, 'span has wrong name'); + assert.strictEqual(span.name, 'HTTP GET', 'span has wrong name'); }); it('span should have correct kind', () => { diff --git a/packages/opentelemetry-resource-detector-aws/.eslintignore b/packages/opentelemetry-resource-detector-aws/.eslintignore new file mode 100644 index 00000000000..378eac25d31 --- /dev/null +++ b/packages/opentelemetry-resource-detector-aws/.eslintignore @@ -0,0 +1 @@ +build diff --git a/packages/opentelemetry-resource-detector-aws/.eslintrc.js b/packages/opentelemetry-resource-detector-aws/.eslintrc.js new file mode 100644 index 00000000000..9dfe62f9b8c --- /dev/null +++ b/packages/opentelemetry-resource-detector-aws/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + "env": { + "mocha": true, + "commonjs": true, + "node": true, + "browser": true + }, + ...require('../../eslint.config.js') +} diff --git a/packages/opentelemetry-resource-detector-aws/.npmignore b/packages/opentelemetry-resource-detector-aws/.npmignore new file mode 100644 index 00000000000..9505ba9450f --- /dev/null +++ b/packages/opentelemetry-resource-detector-aws/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/packages/opentelemetry-resource-detector-aws/LICENSE b/packages/opentelemetry-resource-detector-aws/LICENSE new file mode 100644 index 00000000000..6b91a297c81 --- /dev/null +++ b/packages/opentelemetry-resource-detector-aws/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + 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 + + 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. diff --git a/packages/opentelemetry-resource-detector-aws/README.md b/packages/opentelemetry-resource-detector-aws/README.md new file mode 100644 index 00000000000..e3551cc496a --- /dev/null +++ b/packages/opentelemetry-resource-detector-aws/README.md @@ -0,0 +1,44 @@ +# OpenTelemetry Resource Detector for AWS + +[![Gitter chat][gitter-image]][gitter-url] +[![NPM Published Version][npm-img]][npm-url] +[![dependencies][dependencies-image]][dependencies-url] +[![devDependencies][devDependencies-image]][devDependencies-url] +[![Apache License][license-image]][license-image] + +The OpenTelemetry Resource is an immutable representation of the entity producing telemetry. For example, a process producing telemetry that is running in a container on Kubernetes has a Pod name, it is in a namespace and possibly is part of a Deployment which also has a name. All three of these attributes can be included in the `Resource`. + +[This document][resource-semantic_conventions] defines standard attributes for resources. + +## Installation + +```bash +npm install --save @opentelemetry/resource-detector-aws +``` + +## Usage + +> TODO + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us on [gitter][gitter-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[gitter-image]: https://badges.gitter.im/open-telemetry/opentelemetry-js.svg +[gitter-url]: https://gitter.im/open-telemetry/opentelemetry-node?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge +[license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/master/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[dependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/status.svg?path=packages/opentelemetry-resources +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-resources +[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-resources +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-resources&type=dev +[npm-url]: https://www.npmjs.com/package/@opentelemetry/resources +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fresources.svg + +[resource-semantic_conventions]: https://github.com/open-telemetry/opentelemetry-specification/tree/master/specification/resource/semantic_conventions diff --git a/packages/opentelemetry-resource-detector-aws/package.json b/packages/opentelemetry-resource-detector-aws/package.json new file mode 100644 index 00000000000..8cc341f78b2 --- /dev/null +++ b/packages/opentelemetry-resource-detector-aws/package.json @@ -0,0 +1,63 @@ +{ + "name": "@opentelemetry/resource-detector-aws", + "version": "0.10.2", + "description": "OpenTelemetry SDK resource detector for AWS", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js", + "scripts": { + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", + "tdd": "npm run test -- --watch-extensions ts --watch", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", + "clean": "rimraf build/*", + "precompile": "tsc --version", + "version:update": "node ../../scripts/version-update.js", + "compile": "npm run version:update && tsc -p .", + "prepare": "npm run compile" + }, + "keywords": [ + "opentelemetry", + "nodejs", + "resources", + "stats", + "profiling" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts", + "doc", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@types/mocha": "8.0.1", + "@types/node": "14.0.27", + "@types/sinon": "9.0.4", + "codecov": "3.7.2", + "gts": "2.0.2", + "mocha": "7.2.0", + "nock": "12.0.3", + "nyc": "15.1.0", + "rimraf": "3.0.2", + "sinon": "9.0.2", + "ts-mocha": "7.0.0", + "ts-node": "8.10.2", + "typescript": "3.9.7" + }, + "dependencies": { + "@opentelemetry/api": "^0.10.2", + "@opentelemetry/core": "^0.10.2", + "@opentelemetry/resources": "^0.10.2" + } +} diff --git a/packages/opentelemetry-resource-detector-aws/src/detectors/AwsBeanstalkDetector.ts b/packages/opentelemetry-resource-detector-aws/src/detectors/AwsBeanstalkDetector.ts new file mode 100644 index 00000000000..f50225857b9 --- /dev/null +++ b/packages/opentelemetry-resource-detector-aws/src/detectors/AwsBeanstalkDetector.ts @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Detector, + Resource, + SERVICE_RESOURCE, + ResourceDetectionConfigWithLogger, +} from '@opentelemetry/resources'; +import * as fs from 'fs'; +import * as util from 'util'; + +/** + * The AwsBeanstalkDetector can be used to detect if a process is running in AWS Elastic + * Beanstalk and return a {@link Resource} populated with data about the beanstalk + * plugins of AWS X-Ray. Returns an empty Resource if detection fails. + * + * See https://docs.amazonaws.cn/en_us/xray/latest/devguide/xray-guide.pdf + * for more details about detecting information of Elastic Beanstalk plugins + */ + +const DEFAULT_BEANSTALK_CONF_PATH = + '/var/elasticbeanstalk/xray/environment.conf'; +const WIN_OS_BEANSTALK_CONF_PATH = + 'C:\\Program Files\\Amazon\\XRay\\environment.conf'; + +export class AwsBeanstalkDetector implements Detector { + BEANSTALK_CONF_PATH: string; + private static readFileAsync = util.promisify(fs.readFile); + private static fileAccessAsync = util.promisify(fs.access); + + constructor() { + if (process.platform === 'win32') { + this.BEANSTALK_CONF_PATH = WIN_OS_BEANSTALK_CONF_PATH; + } else { + this.BEANSTALK_CONF_PATH = DEFAULT_BEANSTALK_CONF_PATH; + } + } + + async detect(config: ResourceDetectionConfigWithLogger): Promise { + try { + await AwsBeanstalkDetector.fileAccessAsync( + this.BEANSTALK_CONF_PATH, + fs.constants.R_OK + ); + + const rawData = await AwsBeanstalkDetector.readFileAsync( + this.BEANSTALK_CONF_PATH, + 'utf8' + ); + const parsedData = JSON.parse(rawData); + + return new Resource({ + [SERVICE_RESOURCE.NAME]: 'elastic_beanstalk', + [SERVICE_RESOURCE.NAMESPACE]: parsedData.environment_name, + [SERVICE_RESOURCE.VERSION]: parsedData.version_label, + [SERVICE_RESOURCE.INSTANCE_ID]: parsedData.deployment_id, + }); + } catch (e) { + config.logger.debug(`AwsBeanstalkDetector failed: ${e.message}`); + return Resource.empty(); + } + } +} + +export const awsBeanstalkDetector = new AwsBeanstalkDetector(); diff --git a/packages/opentelemetry-resource-detector-aws/src/detectors/AwsEc2Detector.ts b/packages/opentelemetry-resource-detector-aws/src/detectors/AwsEc2Detector.ts new file mode 100644 index 00000000000..04342d91e85 --- /dev/null +++ b/packages/opentelemetry-resource-detector-aws/src/detectors/AwsEc2Detector.ts @@ -0,0 +1,160 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Detector, + Resource, + CLOUD_RESOURCE, + HOST_RESOURCE, + ResourceDetectionConfigWithLogger, +} from '@opentelemetry/resources'; +import * as http from 'http'; + +/** + * 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 implements Detector { + /** + * See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html + * for documentation about the AWS instance identity document + * and standard of IMDSv2. + */ + readonly AWS_IDMS_ENDPOINT = '169.254.169.254'; + readonly AWS_INSTANCE_TOKEN_DOCUMENT_PATH = '/latest/api/token'; + readonly AWS_INSTANCE_IDENTITY_DOCUMENT_PATH = + '/latest/dynamic/instance-identity/document'; + readonly AWS_INSTANCE_HOST_DOCUMENT_PATH = '/latest/meta-data/hostname'; + readonly AWS_METADATA_TTL_HEADER = 'X-aws-ec2-metadata-token-ttl-seconds'; + readonly AWS_METADATA_TOKEN_HEADER = 'X-aws-ec2-metadata-token'; + readonly MILLISECOND_TIME_OUT = 1000; + + /** + * 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. Returns a promise containing an + * empty {@link Resource} if the connection or parsing of the identity + * document fails. + * + * @param config (unused) The resource detection config with a required logger + */ + async detect(_config: ResourceDetectionConfigWithLogger): Promise { + const token = await this._fetchToken(); + const { + accountId, + instanceId, + instanceType, + region, + availabilityZone, + } = await this._fetchIdentity(token); + const hostname = await this._fetchHost(token); + + return new Resource({ + [CLOUD_RESOURCE.PROVIDER]: 'aws', + [CLOUD_RESOURCE.ACCOUNT_ID]: accountId, + [CLOUD_RESOURCE.REGION]: region, + [CLOUD_RESOURCE.ZONE]: availabilityZone, + [HOST_RESOURCE.ID]: instanceId, + [HOST_RESOURCE.TYPE]: instanceType, + [HOST_RESOURCE.NAME]: hostname, + [HOST_RESOURCE.HOSTNAME]: hostname, + }); + } + + private async _fetchToken(): Promise { + const options = { + host: this.AWS_IDMS_ENDPOINT, + path: this.AWS_INSTANCE_TOKEN_DOCUMENT_PATH, + method: 'PUT', + timeout: this.MILLISECOND_TIME_OUT, + headers: { + [this.AWS_METADATA_TTL_HEADER]: '60', + }, + }; + return await this._fetchString(options); + } + + private async _fetchIdentity(token: string): Promise { + const options = { + host: this.AWS_IDMS_ENDPOINT, + path: this.AWS_INSTANCE_IDENTITY_DOCUMENT_PATH, + method: 'GET', + timeout: this.MILLISECOND_TIME_OUT, + headers: { + [this.AWS_METADATA_TOKEN_HEADER]: token, + }, + }; + const identity = await this._fetchString(options); + return JSON.parse(identity); + } + + private async _fetchHost(token: string): Promise { + const options = { + host: this.AWS_IDMS_ENDPOINT, + path: this.AWS_INSTANCE_HOST_DOCUMENT_PATH, + method: 'GET', + timeout: this.MILLISECOND_TIME_OUT, + headers: { + [this.AWS_METADATA_TOKEN_HEADER]: token, + }, + }; + return await this._fetchString(options); + } + + /** + * Establishes an HTTP connection to AWS instance document url. + * If the application is running on an EC2 instance, we should be able + * to get back a valid JSON document. Parses that document and stores + * the identity properties in a local map. + */ + private async _fetchString(options: http.RequestOptions): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + req.abort(); + reject(new Error('EC2 metadata api request timed out.')); + }, 1000); + + const req = http.request(options, res => { + clearTimeout(timeoutId); + const { statusCode } = res; + res.setEncoding('utf8'); + let rawData = ''; + res.on('data', chunk => (rawData += chunk)); + res.on('end', () => { + if (statusCode && statusCode >= 200 && statusCode < 300) { + try { + resolve(rawData); + } catch (e) { + reject(e); + } + } else { + reject( + new Error('Failed to load page, status code: ' + statusCode) + ); + } + }); + }); + req.on('error', err => { + clearTimeout(timeoutId); + reject(err); + }); + req.end(); + }); + } +} + +export const awsEc2Detector = new AwsEc2Detector(); diff --git a/packages/opentelemetry-resource-detector-aws/src/detectors/index.ts b/packages/opentelemetry-resource-detector-aws/src/detectors/index.ts new file mode 100644 index 00000000000..4bd440dd06c --- /dev/null +++ b/packages/opentelemetry-resource-detector-aws/src/detectors/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './AwsEc2Detector'; +export * from './AwsBeanstalkDetector'; diff --git a/packages/opentelemetry-resource-detector-aws/src/index.ts b/packages/opentelemetry-resource-detector-aws/src/index.ts new file mode 100644 index 00000000000..0acba8788cf --- /dev/null +++ b/packages/opentelemetry-resource-detector-aws/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './detectors'; diff --git a/packages/opentelemetry-resource-detector-aws/src/version.ts b/packages/opentelemetry-resource-detector-aws/src/version.ts new file mode 100644 index 00000000000..ea45ee2fc46 --- /dev/null +++ b/packages/opentelemetry-resource-detector-aws/src/version.ts @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// this is autogenerated file, see scripts/version-update.js +export const VERSION = '0.10.2'; diff --git a/packages/opentelemetry-resource-detector-aws/test/detectors/AwsBeanstalkDetector.test.ts b/packages/opentelemetry-resource-detector-aws/test/detectors/AwsBeanstalkDetector.test.ts new file mode 100644 index 00000000000..9ccebd70669 --- /dev/null +++ b/packages/opentelemetry-resource-detector-aws/test/detectors/AwsBeanstalkDetector.test.ts @@ -0,0 +1,134 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { awsBeanstalkDetector, AwsBeanstalkDetector } from '../../src'; +import { + assertEmptyResource, + assertServiceResource, +} from '@opentelemetry/resources/test/util/resource-assertions'; +import { NoopLogger } from '@opentelemetry/core'; + +describe('BeanstalkResourceDetector', () => { + const err = new Error('failed to read config file'); + const data = { + version_label: 'app-5a56-170119_190650-stage-170119_190650', + deployment_id: '32', + environment_name: 'scorekeep', + }; + const noisyData = { + noise: 'noise', + version_label: 'app-5a56-170119_190650-stage-170119_190650', + deployment_id: '32', + environment_name: 'scorekeep', + }; + + let readStub, fileStub; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should successfully return resource data', async () => { + fileStub = sandbox + .stub(AwsBeanstalkDetector, 'fileAccessAsync' as any) + .resolves(); + readStub = sandbox + .stub(AwsBeanstalkDetector, 'readFileAsync' as any) + .resolves(JSON.stringify(data)); + sandbox.stub(JSON, 'parse').returns(data); + + const resource = await awsBeanstalkDetector.detect({ + logger: new NoopLogger(), + }); + + sandbox.assert.calledOnce(fileStub); + sandbox.assert.calledOnce(readStub); + assert.ok(resource); + assertServiceResource(resource, { + name: 'elastic_beanstalk', + namespace: 'scorekeep', + version: 'app-5a56-170119_190650-stage-170119_190650', + instanceId: '32', + }); + }); + + it('should successfully return resource data with noise', async () => { + fileStub = sandbox + .stub(AwsBeanstalkDetector, 'fileAccessAsync' as any) + .resolves(); + readStub = sandbox + .stub(AwsBeanstalkDetector, 'readFileAsync' as any) + .resolves(JSON.stringify(noisyData)); + sandbox.stub(JSON, 'parse').returns(noisyData); + + const resource = await awsBeanstalkDetector.detect({ + logger: new NoopLogger(), + }); + + sandbox.assert.calledOnce(fileStub); + sandbox.assert.calledOnce(readStub); + assert.ok(resource); + assertServiceResource(resource, { + name: 'elastic_beanstalk', + namespace: 'scorekeep', + version: 'app-5a56-170119_190650-stage-170119_190650', + instanceId: '32', + }); + }); + + it('should return empty resource when failing to read file', async () => { + fileStub = sandbox + .stub(AwsBeanstalkDetector, 'fileAccessAsync' as any) + .resolves(); + readStub = sandbox + .stub(AwsBeanstalkDetector, 'readFileAsync' as any) + .rejects(err); + + const resource = await awsBeanstalkDetector.detect({ + logger: new NoopLogger(), + }); + + sandbox.assert.calledOnce(fileStub); + sandbox.assert.calledOnce(readStub); + assert.ok(resource); + assertEmptyResource(resource); + }); + + it('should return empty resource when config file does not exist', async () => { + fileStub = sandbox + .stub(AwsBeanstalkDetector, 'fileAccessAsync' as any) + .rejects(err); + readStub = sandbox + .stub(AwsBeanstalkDetector, 'readFileAsync' as any) + .resolves(JSON.stringify(data)); + + const resource = await awsBeanstalkDetector.detect({ + logger: new NoopLogger(), + }); + + sandbox.assert.calledOnce(fileStub); + sandbox.assert.notCalled(readStub); + assert.ok(resource); + assertEmptyResource(resource); + }); +}); diff --git a/packages/opentelemetry-resource-detector-aws/test/detectors/AwsEc2Detector.test.ts b/packages/opentelemetry-resource-detector-aws/test/detectors/AwsEc2Detector.test.ts new file mode 100644 index 00000000000..99461164fd1 --- /dev/null +++ b/packages/opentelemetry-resource-detector-aws/test/detectors/AwsEc2Detector.test.ts @@ -0,0 +1,165 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as nock from 'nock'; +import * as assert from 'assert'; +import { Resource } from '@opentelemetry/resources'; +import { awsEc2Detector } from '../../src'; +import { + assertCloudResource, + assertHostResource, +} from '@opentelemetry/resources/test/util/resource-assertions'; +import { NoopLogger } from '@opentelemetry/core'; + +const AWS_HOST = 'http://' + awsEc2Detector.AWS_IDMS_ENDPOINT; +const AWS_TOKEN_PATH = awsEc2Detector.AWS_INSTANCE_TOKEN_DOCUMENT_PATH; +const AWS_IDENTITY_PATH = awsEc2Detector.AWS_INSTANCE_IDENTITY_DOCUMENT_PATH; +const AWS_HOST_PATH = awsEc2Detector.AWS_INSTANCE_HOST_DOCUMENT_PATH; +const AWS_METADATA_TTL_HEADER = awsEc2Detector.AWS_METADATA_TTL_HEADER; +const AWS_METADATA_TOKEN_HEADER = awsEc2Detector.AWS_METADATA_TOKEN_HEADER; + +const mockedTokenResponse = 'my-token'; +const mockedIdentityResponse = { + instanceId: 'my-instance-id', + instanceType: 'my-instance-type', + accountId: 'my-account-id', + region: 'my-region', + availabilityZone: 'my-zone', +}; +const mockedHostResponse = 'my-hostname'; + +describe('awsEc2Detector', () => { + beforeEach(() => { + nock.disableNetConnect(); + nock.cleanAll(); + }); + + afterEach(() => { + nock.enableNetConnect(); + }); + + describe('with successful request', () => { + it('should return aws_ec2_instance resource', async () => { + const scope = nock(AWS_HOST) + .persist() + .put(AWS_TOKEN_PATH) + .matchHeader(AWS_METADATA_TTL_HEADER, '60') + .reply(200, () => mockedTokenResponse) + .get(AWS_IDENTITY_PATH) + .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) + .reply(200, () => mockedIdentityResponse) + .get(AWS_HOST_PATH) + .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) + .reply(200, () => mockedHostResponse); + + const resource: Resource = await awsEc2Detector.detect({ + logger: new NoopLogger(), + }); + + scope.done(); + + assert.ok(resource); + assertCloudResource(resource, { + provider: 'aws', + accountId: 'my-account-id', + region: 'my-region', + zone: 'my-zone', + }); + assertHostResource(resource, { + id: 'my-instance-id', + hostType: 'my-instance-type', + name: 'my-hostname', + hostName: 'my-hostname', + }); + }); + }); + + describe('with unsuccessful request', () => { + it('should throw when receiving error response code', async () => { + const expectedError = new Error('Failed to load page, status code: 404'); + const scope = nock(AWS_HOST) + .persist() + .put(AWS_TOKEN_PATH) + .matchHeader(AWS_METADATA_TTL_HEADER, '60') + .reply(200, () => mockedTokenResponse) + .get(AWS_IDENTITY_PATH) + .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) + .reply(200, () => mockedIdentityResponse) + .get(AWS_HOST_PATH) + .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) + .reply(404, () => new Error()); + + try { + await awsEc2Detector.detect({ + logger: new NoopLogger(), + }); + assert.ok(false, 'Expected to throw'); + } catch (err) { + assert.deepStrictEqual(err, expectedError); + } + + scope.done(); + }); + + it('should throw when timed out', async () => { + const expectedError = new Error('EC2 metadata api request timed out.'); + const scope = nock(AWS_HOST) + .put(AWS_TOKEN_PATH) + .matchHeader(AWS_METADATA_TTL_HEADER, '60') + .reply(200, () => mockedTokenResponse) + .get(AWS_IDENTITY_PATH) + .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) + .reply(200, () => mockedIdentityResponse) + .get(AWS_HOST_PATH) + .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) + .delayConnection(2000) + .reply(200, () => mockedHostResponse); + + try { + await awsEc2Detector.detect({ + logger: new NoopLogger(), + }); + assert.ok(false, 'Expected to throw'); + } catch (err) { + assert.deepStrictEqual(err, expectedError); + } + + scope.done(); + }); + + it('should throw when replied with an Error', async () => { + const expectedError = new Error('NOT FOUND'); + const scope = nock(AWS_HOST) + .put(AWS_TOKEN_PATH) + .matchHeader(AWS_METADATA_TTL_HEADER, '60') + .reply(200, () => mockedTokenResponse) + .get(AWS_IDENTITY_PATH) + .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) + .replyWithError(expectedError.message); + + try { + await awsEc2Detector.detect({ + logger: new NoopLogger(), + }); + assert.ok(false, 'Expected to throw'); + } catch (err) { + assert.deepStrictEqual(err, expectedError); + } + + scope.done(); + }); + }); +}); diff --git a/packages/opentelemetry-resource-detector-aws/tsconfig.json b/packages/opentelemetry-resource-detector-aws/tsconfig.json new file mode 100644 index 00000000000..e4b3b29e6a2 --- /dev/null +++ b/packages/opentelemetry-resource-detector-aws/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/packages/opentelemetry-resource-detector-gcp/.eslintignore b/packages/opentelemetry-resource-detector-gcp/.eslintignore new file mode 100644 index 00000000000..378eac25d31 --- /dev/null +++ b/packages/opentelemetry-resource-detector-gcp/.eslintignore @@ -0,0 +1 @@ +build diff --git a/packages/opentelemetry-resource-detector-gcp/.eslintrc.js b/packages/opentelemetry-resource-detector-gcp/.eslintrc.js new file mode 100644 index 00000000000..9dfe62f9b8c --- /dev/null +++ b/packages/opentelemetry-resource-detector-gcp/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + "env": { + "mocha": true, + "commonjs": true, + "node": true, + "browser": true + }, + ...require('../../eslint.config.js') +} diff --git a/packages/opentelemetry-resource-detector-gcp/.npmignore b/packages/opentelemetry-resource-detector-gcp/.npmignore new file mode 100644 index 00000000000..9505ba9450f --- /dev/null +++ b/packages/opentelemetry-resource-detector-gcp/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/packages/opentelemetry-resource-detector-gcp/LICENSE b/packages/opentelemetry-resource-detector-gcp/LICENSE new file mode 100644 index 00000000000..6b91a297c81 --- /dev/null +++ b/packages/opentelemetry-resource-detector-gcp/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + 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 + + 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. diff --git a/packages/opentelemetry-resource-detector-gcp/README.md b/packages/opentelemetry-resource-detector-gcp/README.md new file mode 100644 index 00000000000..8f493387b0a --- /dev/null +++ b/packages/opentelemetry-resource-detector-gcp/README.md @@ -0,0 +1,46 @@ +# OpenTelemetry Resource Detector for GCP + +[![Gitter chat][gitter-image]][gitter-url] +[![NPM Published Version][npm-img]][npm-url] +[![dependencies][dependencies-image]][dependencies-url] +[![devDependencies][devDependencies-image]][devDependencies-url] +[![Apache License][license-image]][license-image] + +The OpenTelemetry Resource is an immutable representation of the entity producing telemetry. For example, a process producing telemetry that is running in a container on Kubernetes has a Pod name, it is in a namespace and possibly is part of a Deployment which also has a name. All three of these attributes can be included in the `Resource`. + +[This document][resource-semantic_conventions] defines standard attributes for resources. + +## Installation + +The GCP resource detector requires Node.JS 10+ due to a dependency on [`gcp-metadata`](https://www.npmjs.com/package/gcp-metadata) which uses features only available in Node.JS 10+. + +```bash +npm install --save @opentelemetry/resource-detector-gcp +``` + +## Usage + +> TODO + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us on [gitter][gitter-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[gitter-image]: https://badges.gitter.im/open-telemetry/opentelemetry-js.svg +[gitter-url]: https://gitter.im/open-telemetry/opentelemetry-node?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge +[license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/master/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[dependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/status.svg?path=packages/opentelemetry-resources +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-resources +[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-resources +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-resources&type=dev +[npm-url]: https://www.npmjs.com/package/@opentelemetry/resources +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fresources.svg + +[resource-semantic_conventions]: https://github.com/open-telemetry/opentelemetry-specification/tree/master/specification/resource/semantic_conventions diff --git a/packages/opentelemetry-resource-detector-gcp/package.json b/packages/opentelemetry-resource-detector-gcp/package.json new file mode 100644 index 00000000000..e22b8261bb5 --- /dev/null +++ b/packages/opentelemetry-resource-detector-gcp/package.json @@ -0,0 +1,63 @@ +{ + "name": "@opentelemetry/resource-detector-gcp", + "version": "0.10.2", + "description": "OpenTelemetry SDK resource detector for GCP", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js", + "scripts": { + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", + "tdd": "npm run test -- --watch-extensions ts --watch", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", + "clean": "rimraf build/*", + "precompile": "tsc --version", + "version:update": "node ../../scripts/version-update.js", + "compile": "npm run version:update && tsc -p .", + "prepare": "npm run compile" + }, + "keywords": [ + "opentelemetry", + "nodejs", + "resources", + "stats", + "profiling" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0.0" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts", + "doc", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@opentelemetry/core": "^0.10.2", + "@types/mocha": "8.0.1", + "@types/node": "14.0.27", + "@types/semver": "7.3.3", + "codecov": "3.7.2", + "gts": "2.0.2", + "mocha": "7.2.0", + "nock": "12.0.3", + "nyc": "15.1.0", + "rimraf": "3.0.2", + "ts-mocha": "7.0.0", + "ts-node": "8.10.2", + "typescript": "3.9.7" + }, + "dependencies": { + "@opentelemetry/resources": "^0.10.2", + "gcp-metadata": "^4.1.4", + "semver": "7.3.2" + } +} diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts b/packages/opentelemetry-resource-detector-gcp/src/detectors/GcpDetector.ts similarity index 72% rename from packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts rename to packages/opentelemetry-resource-detector-gcp/src/detectors/GcpDetector.ts index a93173d1a11..ed01accf629 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/GcpDetector.ts +++ b/packages/opentelemetry-resource-detector-gcp/src/detectors/GcpDetector.ts @@ -15,16 +15,18 @@ */ import * as os from 'os'; +import * as semver from 'semver'; import * as gcpMetadata from 'gcp-metadata'; -import { Resource } from '../../../Resource'; -import { Detector, ResourceLabels } from '../../../types'; import { + Detector, + ResourceDetectionConfigWithLogger, + Resource, + ResourceAttributes, CLOUD_RESOURCE, HOST_RESOURCE, K8S_RESOURCE, CONTAINER_RESOURCE, -} from '../../../constants'; -import { ResourceDetectionConfigWithLogger } from '../../../config'; +} from '@opentelemetry/resources'; /** * The GcpDetector can be used to detect if a process is running in the Google @@ -35,13 +37,16 @@ class GcpDetector implements Detector { /** * Attempts to connect and obtain instance configuration data from the GCP metadata service. * If the connection is succesful it returns a promise containing a {@link Resource} - * populated with instance metadata as labels. Returns a promise containing an + * populated with instance metadata. Returns a promise containing an * empty {@link Resource} if the connection or parsing of the metadata fails. * * @param config The resource detection config with a required logger */ async detect(config: ResourceDetectionConfigWithLogger): Promise { - if (!(await gcpMetadata.isAvailable())) { + if ( + !semver.satisfies(process.version, '>=10') || + !(await gcpMetadata.isAvailable()) + ) { config.logger.debug('GcpDetector failed: GCP Metadata unavailable.'); return Resource.empty(); } @@ -53,24 +58,27 @@ class GcpDetector implements Detector { this._getClusterName(), ]); - const labels: ResourceLabels = {}; - labels[CLOUD_RESOURCE.ACCOUNT_ID] = projectId; - labels[HOST_RESOURCE.ID] = instanceId; - labels[CLOUD_RESOURCE.ZONE] = zoneId; - labels[CLOUD_RESOURCE.PROVIDER] = 'gcp'; + const attributes: ResourceAttributes = {}; + attributes[CLOUD_RESOURCE.ACCOUNT_ID] = projectId; + attributes[HOST_RESOURCE.ID] = instanceId; + attributes[CLOUD_RESOURCE.ZONE] = zoneId; + attributes[CLOUD_RESOURCE.PROVIDER] = 'gcp'; if (process.env.KUBERNETES_SERVICE_HOST) - this._addK8sLabels(labels, clusterName); + this._addK8sAttributes(attributes, clusterName); - return new Resource(labels); + return new Resource(attributes); } - /** Add resource labels for K8s */ - 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(); - labels[CONTAINER_RESOURCE.NAME] = process.env.CONTAINER_NAME || ''; + /** Add resource attributes for K8s */ + private _addK8sAttributes( + attributes: ResourceAttributes, + clusterName: string + ): void { + attributes[K8S_RESOURCE.CLUSTER_NAME] = clusterName; + attributes[K8S_RESOURCE.NAMESPACE_NAME] = process.env.NAMESPACE || ''; + attributes[K8S_RESOURCE.POD_NAME] = process.env.HOSTNAME || os.hostname(); + attributes[CONTAINER_RESOURCE.NAME] = process.env.CONTAINER_NAME || ''; } /** Gets project id from GCP project metadata. */ diff --git a/packages/opentelemetry-resource-detector-gcp/src/detectors/index.ts b/packages/opentelemetry-resource-detector-gcp/src/detectors/index.ts new file mode 100644 index 00000000000..9e856721bc4 --- /dev/null +++ b/packages/opentelemetry-resource-detector-gcp/src/detectors/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './GcpDetector'; diff --git a/packages/opentelemetry-resource-detector-gcp/src/index.ts b/packages/opentelemetry-resource-detector-gcp/src/index.ts new file mode 100644 index 00000000000..f281d4fdcdf --- /dev/null +++ b/packages/opentelemetry-resource-detector-gcp/src/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './detectors'; + +// Internal - used for tests only +export { resetIsAvailableCache } from 'gcp-metadata'; diff --git a/packages/opentelemetry-resource-detector-gcp/src/version.ts b/packages/opentelemetry-resource-detector-gcp/src/version.ts new file mode 100644 index 00000000000..ea45ee2fc46 --- /dev/null +++ b/packages/opentelemetry-resource-detector-gcp/src/version.ts @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// this is autogenerated file, see scripts/version-update.js +export const VERSION = '0.10.2'; diff --git a/packages/opentelemetry-resource-detector-gcp/test/detectors/GcpDetector.test.ts b/packages/opentelemetry-resource-detector-gcp/test/detectors/GcpDetector.test.ts new file mode 100644 index 00000000000..4d227919f2e --- /dev/null +++ b/packages/opentelemetry-resource-detector-gcp/test/detectors/GcpDetector.test.ts @@ -0,0 +1,169 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + BASE_PATH, + HEADER_NAME, + HEADER_VALUE, + HOST_ADDRESS, + SECONDARY_HOST_ADDRESS, + resetIsAvailableCache, +} from 'gcp-metadata'; +import * as nock from 'nock'; +import * as semver from 'semver'; +import { gcpDetector } from '../../src'; +import { + assertCloudResource, + assertHostResource, + assertK8sResource, + assertContainerResource, + assertEmptyResource, +} from '@opentelemetry/resources/test/util/resource-assertions'; +import { NoopLogger } from '@opentelemetry/core'; +import { Resource } from '@opentelemetry/resources'; + +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'; + +(semver.satisfies(process.version, '>=10') ? describe : describe.skip)( + '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.HOSTNAME; + }); + + beforeEach(() => { + resetIsAvailableCache(); + nock.cleanAll(); + delete process.env.KUBERNETES_SERVICE_HOST; + delete process.env.NAMESPACE; + delete process.env.CONTAINER_NAME; + delete process.env.HOSTNAME; + }); + + 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({ + logger: new NoopLogger(), + }); + secondaryScope.done(); + scope.done(); + + assertCloudResource(resource, { + provider: 'gcp', + accountId: 'my-project-id', + zone: 'my-zone', + }); + assertHostResource(resource, { id: '4520031799277582000' }); + }); + + it('should populate K8s attributes 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({ logger: new NoopLogger() }); + 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' }); + }); + + 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({ logger: new NoopLogger() }); + secondaryScope.done(); + scope.done(); + + assertCloudResource(resource, { + provider: 'gcp', + accountId: 'my-project-id', + zone: '', + }); + }); + + it('returns empty resource if not detected', async () => { + const resource = await gcpDetector.detect({ logger: new NoopLogger() }); + assertEmptyResource(resource); + }); + }); + } +); diff --git a/packages/opentelemetry-resource-detector-gcp/tsconfig.json b/packages/opentelemetry-resource-detector-gcp/tsconfig.json new file mode 100644 index 00000000000..e4b3b29e6a2 --- /dev/null +++ b/packages/opentelemetry-resource-detector-gcp/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/packages/opentelemetry-resources/LICENSE b/packages/opentelemetry-resources/LICENSE index b0e74c7d159..6b91a297c81 100644 --- a/packages/opentelemetry-resources/LICENSE +++ b/packages/opentelemetry-resources/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [2020] [name of copyright owner] + 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. diff --git a/packages/opentelemetry-resources/package.json b/packages/opentelemetry-resources/package.json index 27e0e1d4184..51f6ea5efc9 100644 --- a/packages/opentelemetry-resources/package.json +++ b/packages/opentelemetry-resources/package.json @@ -45,7 +45,7 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "@types/sinon": "9.0.4", "codecov": "3.7.2", @@ -54,14 +54,13 @@ "nock": "12.0.3", "nyc": "15.1.0", "rimraf": "3.0.2", - "sinon": "9.0.2", + "sinon": "9.0.3", "ts-mocha": "7.0.0", "ts-node": "8.10.2", "typescript": "3.9.7" }, "dependencies": { "@opentelemetry/api": "^0.10.2", - "@opentelemetry/core": "^0.10.2", - "gcp-metadata": "^3.5.0" + "@opentelemetry/core": "^0.10.2" } } diff --git a/packages/opentelemetry-resources/src/Resource.ts b/packages/opentelemetry-resources/src/Resource.ts index f8dc503c1cb..eaf17b245fa 100644 --- a/packages/opentelemetry-resources/src/Resource.ts +++ b/packages/opentelemetry-resources/src/Resource.ts @@ -16,7 +16,7 @@ import { SDK_INFO } from '@opentelemetry/core'; import { TELEMETRY_SDK_RESOURCE } from './constants'; -import { ResourceLabels } from './types'; +import { ResourceAttributes } from './types'; /** * A Resource describes the entity for which a signals (metrics or trace) are @@ -45,11 +45,11 @@ export class Resource { constructor( /** - * A dictionary of labels with string keys and values that provide information - * about the entity as numbers, strings or booleans - * TODO: Consider to add check/validation on labels. + * A dictionary of attributes with string keys and values that provide + * information about the entity as numbers, strings or booleans + * TODO: Consider to add check/validation on attributes. */ - readonly labels: ResourceLabels + readonly attributes: ResourceAttributes ) {} /** @@ -61,10 +61,14 @@ export class Resource { * @returns the newly merged Resource. */ merge(other: Resource | null): Resource { - if (!other || !Object.keys(other.labels).length) return this; + if (!other || !Object.keys(other.attributes).length) return this; - // Labels from resource overwrite labels from other resource. - const mergedLabels = Object.assign({}, other.labels, this.labels); - return new Resource(mergedLabels); + // Attributes from resource overwrite attributes from other resource. + const mergedAttributes = Object.assign( + {}, + other.attributes, + this.attributes + ); + return new Resource(mergedAttributes); } } diff --git a/packages/opentelemetry-resources/src/config.ts b/packages/opentelemetry-resources/src/config.ts index 8eb9007eb6a..250d055f7ee 100644 --- a/packages/opentelemetry-resources/src/config.ts +++ b/packages/opentelemetry-resources/src/config.ts @@ -15,6 +15,7 @@ */ import { Logger } from '@opentelemetry/api'; +import type { Detector } from './types'; /** * ResourceDetectionConfig provides an interface for configuring resource auto-detection. @@ -22,6 +23,7 @@ import { Logger } from '@opentelemetry/api'; export interface ResourceDetectionConfig { /** Optional Logger. */ logger?: Logger; + detectors?: Array; } /** diff --git a/packages/opentelemetry-resources/src/index.ts b/packages/opentelemetry-resources/src/index.ts index f5a851015ae..2cfef2d182f 100644 --- a/packages/opentelemetry-resources/src/index.ts +++ b/packages/opentelemetry-resources/src/index.ts @@ -18,3 +18,4 @@ export * from './Resource'; export * from './platform'; export * from './constants'; export * from './types'; +export * from './config'; diff --git a/packages/opentelemetry-resources/src/platform/index.ts b/packages/opentelemetry-resources/src/platform/index.ts index a12506ffa92..cdaf8858ce5 100644 --- a/packages/opentelemetry-resources/src/platform/index.ts +++ b/packages/opentelemetry-resources/src/platform/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + 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 index ca7add14b22..37fa35278d5 100644 --- a/packages/opentelemetry-resources/src/platform/node/detect-resources.ts +++ b/packages/opentelemetry-resources/src/platform/node/detect-resources.ts @@ -15,8 +15,6 @@ */ import { Resource } from '../../Resource'; -import { envDetector, awsEc2Detector, gcpDetector } from './detectors'; -import { Detector } from '../../types'; import { ResourceDetectionConfig, ResourceDetectionConfigWithLogger, @@ -25,8 +23,6 @@ import { Logger } from '@opentelemetry/api'; import * as util from 'util'; import { NoopLogger } from '@opentelemetry/core'; -const DETECTORS: Array = [envDetector, awsEc2Detector, gcpDetector]; - /** * Runs all resource detectors and returns the results merged into a single * Resource. @@ -44,10 +40,13 @@ export const detectResources = async ( ); const resources: Array = await Promise.all( - DETECTORS.map(d => { + (internalConfig.detectors || []).map(async d => { try { - return d.detect(internalConfig); - } catch { + const resource = await d.detect(internalConfig); + config.logger?.debug(`${d.constructor.name} found resource.`, resource); + return resource; + } catch (e) { + config.logger?.debug(`${d.constructor.name} failed: ${e.message}`); return Resource.empty(); } }) @@ -69,19 +68,15 @@ export const detectResources = async ( * @param resources The array of {@link Resource} that should be logged. Empty entried will be ignored. */ const logResources = (logger: Logger, resources: Array) => { - resources.forEach((resource, index) => { + resources.forEach(resource => { // Print only populated resources - if (Object.keys(resource.labels).length > 0) { - const resourceDebugString = util.inspect(resource.labels, { + if (Object.keys(resource.attributes).length > 0) { + const resourceDebugString = util.inspect(resource.attributes, { depth: 2, breakLength: Infinity, sorted: true, compact: false, }); - const detectorName = DETECTORS[index].constructor - ? DETECTORS[index].constructor.name - : 'Unknown detector'; - logger.debug(`${detectorName} found resource.`); logger.debug(resourceDebugString); } }); diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts b/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts deleted file mode 100644 index 76fd11527ae..00000000000 --- a/packages/opentelemetry-resources/src/platform/node/detectors/AwsEc2Detector.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as http from 'http'; -import { Resource } from '../../../Resource'; -import { CLOUD_RESOURCE, HOST_RESOURCE } from '../../../constants'; -import { Detector } from '../../../types'; -import { ResourceDetectionConfigWithLogger } from '../../../config'; - -/** - * The AwsEc2Detector can be used to detect if a process is running in AWS EC2 - * and return a {@link Resource} populated with metadata about the EC2 - * 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'; - - /** - * 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. - * - * @param config The resource detection config with a required logger - */ - async detect(config: ResourceDetectionConfigWithLogger): Promise { - try { - const { - accountId, - instanceId, - instanceType, - region, - availabilityZone, - } = await this._awsMetadataAccessor(); - return new Resource({ - [CLOUD_RESOURCE.PROVIDER]: 'aws', - [CLOUD_RESOURCE.ACCOUNT_ID]: accountId, - [CLOUD_RESOURCE.REGION]: region, - [CLOUD_RESOURCE.ZONE]: availabilityZone, - [HOST_RESOURCE.ID]: instanceId, - [HOST_RESOURCE.TYPE]: instanceType, - }); - } catch (e) { - config.logger.debug(`AwsEc2Detector failed: ${e.message}`); - 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 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(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) { - reject(e); - } - } else { - reject( - new Error('Failed to load page, status code: ' + statusCode) - ); - } - }); - }); - req.on('error', err => { - clearTimeout(timeoutId); - reject(err); - }); - }); - } -} - -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 718c566f126..a448a11d1f0 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/EnvDetector.ts @@ -14,22 +14,25 @@ * limitations under the License. */ -import { Resource } from '../../../Resource'; -import { Detector, ResourceLabels } from '../../../types'; -import { ResourceDetectionConfigWithLogger } from '../../../config'; +import { + Detector, + Resource, + ResourceDetectionConfigWithLogger, + ResourceAttributes, +} from '../../../'; /** * EnvDetector can be used to detect the presence of and create a Resource - * from the OTEL_RESOURCE_LABELS environment variable. + * from the OTEL_RESOURCE_ATTRIBUTES environment variable. */ class EnvDetector implements Detector { - // Type, label keys, and label values should not exceed 256 characters. + // Type, attribute keys, and attribute values should not exceed 256 characters. private readonly _MAX_LENGTH = 255; - // OTEL_RESOURCE_LABELS is a comma-separated list of labels. + // OTEL_RESOURCE_ATTRIBUTES is a comma-separated list of attributes. private readonly _COMMA_SEPARATOR = ','; - // OTEL_RESOURCE_LABELS contains key value pair separated by '='. + // OTEL_RESOURCE_ATTRIBUTES contains key value pair separated by '='. private readonly _LABEL_KEY_VALUE_SPLITTER = '='; private readonly _ERROR_MESSAGE_INVALID_CHARS = @@ -43,25 +46,23 @@ class EnvDetector implements Detector { ' characters.'; /** - * Returns a {@link Resource} populated with labels from the - * OTEL_RESOURCE_LABELS environment variable. Note this is an async function - * to conform to the Detector interface. + * Returns a {@link Resource} populated with attributes from the + * OTEL_RESOURCE_ATTRIBUTES environment variable. Note this is an async + * function to conform to the Detector interface. * * @param config The resource detection config with a required logger */ async detect(config: ResourceDetectionConfigWithLogger): Promise { try { - const labelString = process.env.OTEL_RESOURCE_LABELS; - if (!labelString) { + const rawAttributes = process.env.OTEL_RESOURCE_ATTRIBUTES; + if (!rawAttributes) { config.logger.debug( - 'EnvDetector failed: Environment variable "OTEL_RESOURCE_LABELS" is missing.' + 'EnvDetector failed: Environment variable "OTEL_RESOURCE_ATTRIBUTES" is missing.' ); return Resource.empty(); } - const labels = this._parseResourceLabels( - process.env.OTEL_RESOURCE_LABELS - ); - return new Resource(labels); + const attributes = this._parseResourceAttributes(rawAttributes); + return new Resource(attributes); } catch (e) { config.logger.debug(`EnvDetector failed: ${e.message}`); return Resource.empty(); @@ -69,24 +70,31 @@ class EnvDetector implements Detector { } /** - * Creates a label map from the OTEL_RESOURCE_LABELS environment variable. + * Creates an attribute map from the OTEL_RESOURCE_ATTRIBUTES environment + * variable. * - * 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. + * OTEL_RESOURCE_ATTRIBUTES: A comma-separated list of attributes describing + * the source in more detail, e.g. “key1=val1,key2=val2”. Domain names and + * paths are accepted as attribute keys. Values may be quoted or unquoted in + * general. If a value contains whitespaces, =, or " characters, it must + * always be quoted. * - * @param rawEnvLabels The resource labels as a comma-seperated list + * @param rawEnvAttributes The resource attributes as a comma-seperated list * of key/value pairs. - * @returns The sanitized resource labels. + * @returns The sanitized resource attributes. */ - private _parseResourceLabels(rawEnvLabels?: string): ResourceLabels { - if (!rawEnvLabels) return {}; + private _parseResourceAttributes( + rawEnvAttributes?: string + ): ResourceAttributes { + if (!rawEnvAttributes) return {}; - const labels: ResourceLabels = {}; - const rawLabels: string[] = rawEnvLabels.split(this._COMMA_SEPARATOR, -1); - for (const rawLabel of rawLabels) { - const keyValuePair: string[] = rawLabel.split( + const attributes: ResourceAttributes = {}; + const rawAttributes: string[] = rawEnvAttributes.split( + this._COMMA_SEPARATOR, + -1 + ); + for (const rawAttribute of rawAttributes) { + const keyValuePair: string[] = rawAttribute.split( this._LABEL_KEY_VALUE_SPLITTER, -1 ); @@ -98,14 +106,14 @@ class EnvDetector implements Detector { key = key.trim(); value = value.trim().split('^"|"$').join(''); if (!this._isValidAndNotEmpty(key)) { - throw new Error(`Label key ${this._ERROR_MESSAGE_INVALID_CHARS}`); + throw new Error(`Attribute key ${this._ERROR_MESSAGE_INVALID_CHARS}`); } if (!this._isValid(value)) { - throw new Error(`Label value ${this._ERROR_MESSAGE_INVALID_VALUE}`); + throw new Error(`Attribute value ${this._ERROR_MESSAGE_INVALID_VALUE}`); } - labels[key] = value; + attributes[key] = value; } - return labels; + return attributes; } /** diff --git a/packages/opentelemetry-resources/src/platform/node/detectors/index.ts b/packages/opentelemetry-resources/src/platform/node/detectors/index.ts index c0c3c37b2c8..f2b4223be18 100644 --- a/packages/opentelemetry-resources/src/platform/node/detectors/index.ts +++ b/packages/opentelemetry-resources/src/platform/node/detectors/index.ts @@ -14,6 +14,4 @@ * limitations under the License. */ -export { awsEc2Detector } from './AwsEc2Detector'; -export { envDetector } from './EnvDetector'; -export { gcpDetector } from './GcpDetector'; +export * from './EnvDetector'; diff --git a/packages/opentelemetry-resources/src/platform/node/index.ts b/packages/opentelemetry-resources/src/platform/node/index.ts index f90eb34a5fe..7e82a09dd5d 100644 --- a/packages/opentelemetry-resources/src/platform/node/index.ts +++ b/packages/opentelemetry-resources/src/platform/node/index.ts @@ -15,3 +15,4 @@ */ export * from './detect-resources'; +export * from './detectors'; diff --git a/packages/opentelemetry-resources/src/types.ts b/packages/opentelemetry-resources/src/types.ts index c33562312f8..d31d17515d2 100644 --- a/packages/opentelemetry-resources/src/types.ts +++ b/packages/opentelemetry-resources/src/types.ts @@ -17,8 +17,8 @@ import { Resource } from './Resource'; import { ResourceDetectionConfigWithLogger } from './config'; -/** Interface for Resource labels */ -export interface ResourceLabels { +/** Interface for Resource attributes */ +export interface ResourceAttributes { [key: string]: number | string | boolean; } diff --git a/packages/opentelemetry-resources/test/Resource.test.ts b/packages/opentelemetry-resources/test/Resource.test.ts index 0c5d0feb9fa..3203148fc42 100644 --- a/packages/opentelemetry-resources/test/Resource.test.ts +++ b/packages/opentelemetry-resources/test/Resource.test.ts @@ -44,11 +44,11 @@ describe('Resource', () => { 'k8s.io/location': 'location', }); const actualResource = resource1.merge(resource2); - assert.strictEqual(Object.keys(actualResource.labels).length, 5); + assert.strictEqual(Object.keys(actualResource.attributes).length, 5); assert.deepStrictEqual(actualResource, expectedResource); }); - it('should return merged resource when collision in labels', () => { + it('should return merged resource when collision in attributes', () => { const expectedResource = new Resource({ 'k8s.io/container/name': 'c1', 'k8s.io/namespace/name': 'default', @@ -56,25 +56,25 @@ describe('Resource', () => { 'k8s.io/location': 'location1', }); const actualResource = resource1.merge(resource3); - assert.strictEqual(Object.keys(actualResource.labels).length, 4); + assert.strictEqual(Object.keys(actualResource.attributes).length, 4); assert.deepStrictEqual(actualResource, expectedResource); }); it('should return merged resource when first resource is empty', () => { const actualResource = emptyResource.merge(resource2); - assert.strictEqual(Object.keys(actualResource.labels).length, 2); + assert.strictEqual(Object.keys(actualResource.attributes).length, 2); assert.deepStrictEqual(actualResource, resource2); }); it('should return merged resource when other resource is empty', () => { const actualResource = resource1.merge(emptyResource); - assert.strictEqual(Object.keys(actualResource.labels).length, 3); + assert.strictEqual(Object.keys(actualResource.attributes).length, 3); assert.deepStrictEqual(actualResource, resource1); }); it('should return merged resource when other resource is null', () => { const actualResource = resource1.merge(null); - assert.strictEqual(Object.keys(actualResource.labels).length, 3); + assert.strictEqual(Object.keys(actualResource.attributes).length, 3); assert.deepStrictEqual(actualResource, resource1); }); @@ -84,15 +84,15 @@ describe('Resource', () => { 'custom.number': 42, 'custom.boolean': true, }); - assert.equal(resource.labels['custom.string'], 'strvalue'); - assert.equal(resource.labels['custom.number'], 42); - assert.equal(resource.labels['custom.boolean'], true); + assert.equal(resource.attributes['custom.string'], 'strvalue'); + assert.equal(resource.attributes['custom.number'], 42); + assert.equal(resource.attributes['custom.boolean'], true); }); describe('.empty()', () => { it('should return an empty resource', () => { const resource = Resource.empty(); - assert.equal(Object.entries(resource.labels), 0); + assert.equal(Object.entries(resource.attributes), 0); }); it('should return the same empty resource', () => { diff --git a/packages/opentelemetry-resources/test/detect-resources.test.ts b/packages/opentelemetry-resources/test/detect-resources.test.ts deleted file mode 100644 index b607efa0d69..00000000000 --- a/packages/opentelemetry-resources/test/detect-resources.test.ts +++ /dev/null @@ -1,275 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as nock from 'nock'; -import * as sinon from 'sinon'; -import * as assert from 'assert'; -import { URL } from 'url'; -import { Resource, detectResources } from '../src'; -import { awsEc2Detector } from '../src/platform/node/detectors'; -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', - instanceType: 'my-instance-type', - accountId: 'my-account-id', - region: 'my-region', - availabilityZone: 'my-zone', -}; - -describe('detectResources', async () => { - beforeEach(() => { - nock.disableNetConnect(); - process.env.OTEL_RESOURCE_LABELS = - 'service.instance.id=627cc493,service.name=my-service,service.namespace=default,service.version=0.0.1'; - }); - - afterEach(() => { - nock.cleanAll(); - nock.enableNetConnect(); - delete process.env.OTEL_RESOURCE_LABELS; - }); - - describe('in GCP environment', () => { - after(() => { - resetIsAvailableCache(); - }); - - it('returns a merged resource', async () => { - const gcpScope = 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 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(); - awsScope.done(); - gcpSecondaryScope.done(); - gcpScope.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', - zone: 'my-zone', - }); - assertHostResource(resource, { - id: 'my-instance-id', - hostType: 'my-instance-type', - }); - assertServiceResource(resource, { - instanceId: '627cc493', - name: 'my-service', - namespace: 'default', - version: '0.0.1', - }); - }); - }); - - 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(); - }); - }); - - describe('with a debug logger', () => { - // Local functions to test if a mocked method is ever called with a specific argument or regex matching for an argument. - // Needed because of race condition with parallel detectors. - const callArgsContains = ( - mockedFunction: sinon.SinonSpy, - arg: any - ): boolean => { - return mockedFunction.getCalls().some(call => { - return call.args.some(callarg => arg === callarg); - }); - }; - const callArgsMatches = ( - mockedFunction: sinon.SinonSpy, - regex: RegExp - ): boolean => { - return mockedFunction.getCalls().some(call => { - return regex.test(call.args.toString()); - }); - }; - - it('prints detected resources and debug messages to the logger', async () => { - // This test depends on the env detector to be functioning as intended - const mockedLoggerMethod = sinon.fake(); - await detectResources({ - logger: { - debug: mockedLoggerMethod, - info: sinon.fake(), - warn: sinon.fake(), - error: sinon.fake(), - }, - }); - - // Test for AWS and GCP Detector failure - assert.ok( - callArgsContains( - mockedLoggerMethod, - 'GcpDetector failed: GCP Metadata unavailable.' - ) - ); - assert.ok( - callArgsContains( - mockedLoggerMethod, - 'AwsEc2Detector failed: Nock: Disallowed net connect for "169.254.169.254:80/latest/dynamic/instance-identity/document"' - ) - ); - // Test that the Env Detector successfully found its resource and populated it with the right values. - assert.ok( - callArgsContains(mockedLoggerMethod, 'EnvDetector found resource.') - ); - // Regex formatting accounts for whitespace variations in util.inspect output over different node versions - assert.ok( - callArgsMatches( - mockedLoggerMethod, - /{\s+'service\.instance\.id':\s+'627cc493',\s+'service\.name':\s+'my-service',\s+'service\.namespace':\s+'default',\s+'service\.version':\s+'0\.0\.1'\s+}\s*/ - ) - ); - }); - - describe('with missing environemnt variable', () => { - beforeEach(() => { - delete process.env.OTEL_RESOURCE_LABELS; - }); - - it('prints correct error messages when EnvDetector has no env variable', async () => { - const mockedLoggerMethod = sinon.fake(); - await detectResources({ - logger: { - debug: mockedLoggerMethod, - info: sinon.fake(), - warn: sinon.fake(), - error: sinon.fake(), - }, - }); - - assert.ok( - callArgsContains( - mockedLoggerMethod, - 'EnvDetector failed: Environment variable "OTEL_RESOURCE_LABELS" is missing.' - ) - ); - }); - }); - - describe('with a faulty environment variable', () => { - beforeEach(() => { - process.env.OTEL_RESOURCE_LABELS = 'bad=~label'; - }); - - it('prints correct error messages when EnvDetector has an invalid variable', async () => { - const mockedLoggerMethod = sinon.fake(); - await detectResources({ - logger: { - debug: mockedLoggerMethod, - info: sinon.fake(), - warn: sinon.fake(), - error: sinon.fake(), - }, - }); - - assert.ok( - callArgsContains( - mockedLoggerMethod, - 'EnvDetector failed: Label value should be a ASCII string with a length not exceed 255 characters.' - ) - ); - }); - }); - }); -}); diff --git a/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts b/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts deleted file mode 100644 index 9b9e6eb0355..00000000000 --- a/packages/opentelemetry-resources/test/detectors/AwsEc2Detector.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * 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 { - assertCloudResource, - assertHostResource, - assertEmptyResource, -} from '../util/resource-assertions'; -import { NoopLogger } from '@opentelemetry/core'; - -const { origin: AWS_HOST, pathname: AWS_PATH } = new URL( - awsEc2Detector.AWS_INSTANCE_IDENTITY_DOCUMENT_URI -); - -const mockedAwsResponse = { - instanceId: 'my-instance-id', - instanceType: 'my-instance-type', - accountId: 'my-account-id', - region: 'my-region', - availabilityZone: 'my-zone', -}; - -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({ - logger: new NoopLogger(), - }); - scope.done(); - - assert.ok(resource); - assertCloudResource(resource, { - provider: 'aws', - accountId: 'my-account-id', - region: 'my-region', - zone: 'my-zone', - }); - assertHostResource(resource, { - id: 'my-instance-id', - hostType: 'my-instance-type', - }); - }); - }); - - 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({ - logger: new NoopLogger(), - }); - scope.done(); - - assert.ok(resource); - assertEmptyResource(resource); - }); - }); -}); diff --git a/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts b/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts index 894de6e8700..df45725cb9d 100644 --- a/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/EnvDetector.test.ts @@ -14,24 +14,22 @@ * limitations under the License. */ -import { Resource } from '../../src/Resource'; -import { envDetector } from '../../src/platform/node/detectors/EnvDetector'; +import { envDetector, K8S_RESOURCE, Resource } from '../../src'; import { assertK8sResource, assertEmptyResource, } from '../util/resource-assertions'; -import { K8S_RESOURCE } from '../../src'; import { NoopLogger } from '@opentelemetry/core'; describe('envDetector()', () => { describe('with valid env', () => { before(() => { - process.env.OTEL_RESOURCE_LABELS = + process.env.OTEL_RESOURCE_ATTRIBUTES = 'k8s.pod.name="pod-xyz-123",k8s.cluster.name="c1",k8s.namespace.name="default"'; }); after(() => { - delete process.env.OTEL_RESOURCE_LABELS; + delete process.env.OTEL_RESOURCE_ATTRIBUTES; }); it('should return resource information from environment variable', async () => { diff --git a/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts b/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts deleted file mode 100644 index c476feb5a7f..00000000000 --- a/packages/opentelemetry-resources/test/detectors/GcpDetector.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - BASE_PATH, - HEADER_NAME, - HEADER_VALUE, - HOST_ADDRESS, - SECONDARY_HOST_ADDRESS, - 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'; -import { NoopLogger } from '@opentelemetry/core'; - -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'; - -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.HOSTNAME; - }); - - beforeEach(() => { - resetIsAvailableCache(); - nock.cleanAll(); - delete process.env.KUBERNETES_SERVICE_HOST; - delete process.env.NAMESPACE; - delete process.env.CONTAINER_NAME; - delete process.env.HOSTNAME; - }); - - 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({ - logger: new NoopLogger(), - }); - secondaryScope.done(); - 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.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({ logger: new NoopLogger() }); - 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' }); - }); - - 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({ logger: new NoopLogger() }); - secondaryScope.done(); - scope.done(); - - assertCloudResource(resource, { - provider: 'gcp', - accountId: 'my-project-id', - zone: '', - }); - }); - - it('returns empty resource if not detected', async () => { - const resource = await gcpDetector.detect({ logger: new NoopLogger() }); - assertEmptyResource(resource); - }); - }); -}); diff --git a/packages/opentelemetry-resources/test/resource-assertions.test.ts b/packages/opentelemetry-resources/test/resource-assertions.test.ts index 20e626daac0..c3a99f59989 100644 --- a/packages/opentelemetry-resources/test/resource-assertions.test.ts +++ b/packages/opentelemetry-resources/test/resource-assertions.test.ts @@ -39,7 +39,7 @@ describe('assertCloudResource', () => { assertCloudResource(resource, {}); }); - it('validates optional labels', () => { + it('validates optional attributes', () => { const resource = new Resource({ [CLOUD_RESOURCE.PROVIDER]: 'gcp', [CLOUD_RESOURCE.ACCOUNT_ID]: 'opentelemetry', @@ -63,7 +63,7 @@ describe('assertContainerResource', () => { assertContainerResource(resource, {}); }); - it('validates optional labels', () => { + it('validates optional attributes', () => { const resource = new Resource({ [CONTAINER_RESOURCE.NAME]: 'opentelemetry-autoconf', [CONTAINER_RESOURCE.IMAGE_NAME]: 'gcr.io/opentelemetry/operator', @@ -85,7 +85,7 @@ describe('assertHostResource', () => { assertHostResource(resource, {}); }); - it('validates optional labels', () => { + it('validates optional attributes', () => { const resource = new Resource({ [HOST_RESOURCE.HOSTNAME]: 'opentelemetry-test-hostname', [HOST_RESOURCE.ID]: 'opentelemetry-test-id', @@ -116,7 +116,7 @@ describe('assertK8sResource', () => { assertK8sResource(resource, {}); }); - it('validates optional labels', () => { + it('validates optional attributes', () => { const resource = new Resource({ [K8S_RESOURCE.CLUSTER_NAME]: 'opentelemetry-cluster', [K8S_RESOURCE.NAMESPACE_NAME]: 'default', @@ -142,7 +142,7 @@ describe('assertTelemetrySDKResource', () => { assertTelemetrySDKResource(resource, {}); }); - it('validates optional labels', () => { + it('validates optional attributes', () => { const resource = new Resource({ [TELEMETRY_SDK_RESOURCE.NAME]: 'opentelemetry', [TELEMETRY_SDK_RESOURCE.LANGUAGE]: 'nodejs', @@ -157,7 +157,7 @@ describe('assertTelemetrySDKResource', () => { }); describe('assertServiceResource', () => { - it('validates required labels', () => { + it('validates required attributes', () => { const resource = new Resource({ [SERVICE_RESOURCE.NAME]: 'shoppingcart', [SERVICE_RESOURCE.INSTANCE_ID]: '627cc493-f310-47de-96bd-71410b7dec09', @@ -168,7 +168,7 @@ describe('assertServiceResource', () => { }); }); - it('validates optional labels', () => { + it('validates optional attributes', () => { const resource = new Resource({ [SERVICE_RESOURCE.NAME]: 'shoppingcart', [SERVICE_RESOURCE.INSTANCE_ID]: '627cc493-f310-47de-96bd-71410b7dec09', diff --git a/packages/opentelemetry-resources/test/util/resource-assertions.ts b/packages/opentelemetry-resources/test/util/resource-assertions.ts index a6db54b320d..422415537c3 100644 --- a/packages/opentelemetry-resources/test/util/resource-assertions.ts +++ b/packages/opentelemetry-resources/test/util/resource-assertions.ts @@ -30,7 +30,7 @@ import { * Test utility method to validate a cloud resource * * @param resource the Resource to validate - * @param validations validations for the resource labels + * @param validations validations for the resource attributes */ export const assertCloudResource = ( resource: Resource, @@ -44,28 +44,31 @@ export const assertCloudResource = ( assertHasOneLabel(CLOUD_RESOURCE, resource); if (validations.provider) assert.strictEqual( - resource.labels[CLOUD_RESOURCE.PROVIDER], + resource.attributes[CLOUD_RESOURCE.PROVIDER], validations.provider ); if (validations.accountId) assert.strictEqual( - resource.labels[CLOUD_RESOURCE.ACCOUNT_ID], + resource.attributes[CLOUD_RESOURCE.ACCOUNT_ID], validations.accountId ); if (validations.region) assert.strictEqual( - resource.labels[CLOUD_RESOURCE.REGION], + resource.attributes[CLOUD_RESOURCE.REGION], validations.region ); if (validations.zone) - assert.strictEqual(resource.labels[CLOUD_RESOURCE.ZONE], validations.zone); + assert.strictEqual( + resource.attributes[CLOUD_RESOURCE.ZONE], + validations.zone + ); }; /** * Test utility method to validate a container resource * * @param resource the Resource to validate - * @param validations validations for the resource labels + * @param validations validations for the resource attributes */ export const assertContainerResource = ( resource: Resource, @@ -78,17 +81,17 @@ export const assertContainerResource = ( assertHasOneLabel(CONTAINER_RESOURCE, resource); if (validations.name) assert.strictEqual( - resource.labels[CONTAINER_RESOURCE.NAME], + resource.attributes[CONTAINER_RESOURCE.NAME], validations.name ); if (validations.imageName) assert.strictEqual( - resource.labels[CONTAINER_RESOURCE.IMAGE_NAME], + resource.attributes[CONTAINER_RESOURCE.IMAGE_NAME], validations.imageName ); if (validations.imageTag) assert.strictEqual( - resource.labels[CONTAINER_RESOURCE.IMAGE_TAG], + resource.attributes[CONTAINER_RESOURCE.IMAGE_TAG], validations.imageTag ); }; @@ -97,7 +100,7 @@ export const assertContainerResource = ( * Test utility method to validate a host resource * * @param resource the Resource to validate - * @param validations validations for the resource labels + * @param validations validations for the resource attributes */ export const assertHostResource = ( resource: Resource, @@ -114,31 +117,34 @@ export const assertHostResource = ( assertHasOneLabel(HOST_RESOURCE, resource); if (validations.hostName) assert.strictEqual( - resource.labels[HOST_RESOURCE.HOSTNAME], + resource.attributes[HOST_RESOURCE.HOSTNAME], validations.hostName ); if (validations.id) - assert.strictEqual(resource.labels[HOST_RESOURCE.ID], validations.id); + assert.strictEqual(resource.attributes[HOST_RESOURCE.ID], validations.id); if (validations.name) - assert.strictEqual(resource.labels[HOST_RESOURCE.NAME], validations.name); + assert.strictEqual( + resource.attributes[HOST_RESOURCE.NAME], + validations.name + ); if (validations.hostType) assert.strictEqual( - resource.labels[HOST_RESOURCE.TYPE], + resource.attributes[HOST_RESOURCE.TYPE], validations.hostType ); if (validations.imageName) assert.strictEqual( - resource.labels[HOST_RESOURCE.IMAGE_NAME], + resource.attributes[HOST_RESOURCE.IMAGE_NAME], validations.imageName ); if (validations.imageId) assert.strictEqual( - resource.labels[HOST_RESOURCE.IMAGE_ID], + resource.attributes[HOST_RESOURCE.IMAGE_ID], validations.imageId ); if (validations.imageVersion) assert.strictEqual( - resource.labels[HOST_RESOURCE.IMAGE_VERSION], + resource.attributes[HOST_RESOURCE.IMAGE_VERSION], validations.imageVersion ); }; @@ -147,7 +153,7 @@ export const assertHostResource = ( * Test utility method to validate a K8s resource * * @param resource the Resource to validate - * @param validations validations for the resource labels + * @param validations validations for the resource attributes */ export const assertK8sResource = ( resource: Resource, @@ -161,22 +167,22 @@ export const assertK8sResource = ( assertHasOneLabel(K8S_RESOURCE, resource); if (validations.clusterName) assert.strictEqual( - resource.labels[K8S_RESOURCE.CLUSTER_NAME], + resource.attributes[K8S_RESOURCE.CLUSTER_NAME], validations.clusterName ); if (validations.namespaceName) assert.strictEqual( - resource.labels[K8S_RESOURCE.NAMESPACE_NAME], + resource.attributes[K8S_RESOURCE.NAMESPACE_NAME], validations.namespaceName ); if (validations.podName) assert.strictEqual( - resource.labels[K8S_RESOURCE.POD_NAME], + resource.attributes[K8S_RESOURCE.POD_NAME], validations.podName ); if (validations.deploymentName) assert.strictEqual( - resource.labels[K8S_RESOURCE.DEPLOYMENT_NAME], + resource.attributes[K8S_RESOURCE.DEPLOYMENT_NAME], validations.deploymentName ); }; @@ -185,7 +191,7 @@ export const assertK8sResource = ( * Test utility method to validate a telemetry sdk resource * * @param resource the Resource to validate - * @param validations validations for the resource labels + * @param validations validations for the resource attributes */ export const assertTelemetrySDKResource = ( resource: Resource, @@ -204,17 +210,17 @@ export const assertTelemetrySDKResource = ( if (validations.name) assert.strictEqual( - resource.labels[TELEMETRY_SDK_RESOURCE.NAME], + resource.attributes[TELEMETRY_SDK_RESOURCE.NAME], validations.name ); if (validations.language) assert.strictEqual( - resource.labels[TELEMETRY_SDK_RESOURCE.LANGUAGE], + resource.attributes[TELEMETRY_SDK_RESOURCE.LANGUAGE], validations.language ); if (validations.version) assert.strictEqual( - resource.labels[TELEMETRY_SDK_RESOURCE.VERSION], + resource.attributes[TELEMETRY_SDK_RESOURCE.VERSION], validations.version ); }; @@ -223,7 +229,7 @@ export const assertTelemetrySDKResource = ( * Test utility method to validate a service resource * * @param resource the Resource to validate - * @param validations validations for the resource labels + * @param validations validations for the resource attributes */ export const assertServiceResource = ( resource: Resource, @@ -234,19 +240,22 @@ export const assertServiceResource = ( version?: string; } ) => { - assert.strictEqual(resource.labels[SERVICE_RESOURCE.NAME], validations.name); assert.strictEqual( - resource.labels[SERVICE_RESOURCE.INSTANCE_ID], + resource.attributes[SERVICE_RESOURCE.NAME], + validations.name + ); + assert.strictEqual( + resource.attributes[SERVICE_RESOURCE.INSTANCE_ID], validations.instanceId ); if (validations.namespace) assert.strictEqual( - resource.labels[SERVICE_RESOURCE.NAMESPACE], + resource.attributes[SERVICE_RESOURCE.NAMESPACE], validations.namespace ); if (validations.version) assert.strictEqual( - resource.labels[SERVICE_RESOURCE.VERSION], + resource.attributes[SERVICE_RESOURCE.VERSION], validations.version ); }; @@ -257,7 +266,7 @@ export const assertServiceResource = ( * @param resource the Resource to validate */ export const assertEmptyResource = (resource: Resource) => { - assert.strictEqual(Object.keys(resource.labels).length, 0); + assert.strictEqual(Object.keys(resource.attributes).length, 0); }; const assertHasOneLabel = ( @@ -266,12 +275,12 @@ const assertHasOneLabel = ( ): void => { const hasOne = Object.values(constants).reduce( // eslint-disable-next-line no-prototype-builtins - (found, key) => found || resource.labels.hasOwnProperty(key), + (found, key) => found || resource.attributes.hasOwnProperty(key), false ); assert.ok( hasOne, - 'Resource must have one of the following labels: ' + + 'Resource must have one of the following attributes: ' + Object.values(constants).join(', ') ); }; diff --git a/packages/opentelemetry-resources/test/util/sample-detector.ts b/packages/opentelemetry-resources/test/util/sample-detector.ts new file mode 100644 index 00000000000..1ed6f258efd --- /dev/null +++ b/packages/opentelemetry-resources/test/util/sample-detector.ts @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Detector, Resource, CLOUD_RESOURCE, HOST_RESOURCE } from '../../src'; + +class SampleDetector implements Detector { + async detect(): Promise { + return new Resource({ + [CLOUD_RESOURCE.PROVIDER]: 'provider', + [CLOUD_RESOURCE.ACCOUNT_ID]: 'accountId', + [CLOUD_RESOURCE.REGION]: 'region', + [CLOUD_RESOURCE.ZONE]: 'zone', + [HOST_RESOURCE.ID]: 'instanceId', + [HOST_RESOURCE.TYPE]: 'instanceType', + }); + } +} + +export const sampleDetector = new SampleDetector(); diff --git a/packages/opentelemetry-sdk-node/README.md b/packages/opentelemetry-sdk-node/README.md index cecb31b09e2..9426c9ee93e 100644 --- a/packages/opentelemetry-sdk-node/README.md +++ b/packages/opentelemetry-sdk-node/README.md @@ -70,7 +70,7 @@ Detect resources automatically from the environment using the default resource d Use a custom context manager. Default: [AsyncHooksContextManager](../opentelemetry-context-async-hooks/README.md) -### httpTextPropagator +### textMapPropagator Use a custom propagator. Default: [CompositePropagator](../opentelemetry-core/src/context/propagation/composite.ts) using [W3C Trace Context](../opentelemetry-core/README.md#httptracecontext-propagator) and [Correlation Context](../opentelemetry-core/README.md#correlation-context-propagator) diff --git a/packages/opentelemetry-sdk-node/package.json b/packages/opentelemetry-sdk-node/package.json index 8f79ccaf658..2351f7cfbdb 100644 --- a/packages/opentelemetry-sdk-node/package.json +++ b/packages/opentelemetry-sdk-node/package.json @@ -48,19 +48,25 @@ "@opentelemetry/metrics": "^0.10.2", "@opentelemetry/node": "^0.10.2", "@opentelemetry/resources": "^0.10.2", - "@opentelemetry/tracing": "^0.10.2" + "@opentelemetry/tracing": "^0.10.2", + "@opentelemetry/resource-detector-aws": "^0.10.2", + "@opentelemetry/resource-detector-gcp": "^0.10.2", + "nock": "12.0.3" }, "devDependencies": { "@opentelemetry/context-async-hooks": "^0.10.2", "@types/mocha": "7.0.2", "@types/node": "14.0.27", + "@types/semver": "7.3.3", "@types/sinon": "9.0.4", "codecov": "3.7.2", + "gcp-metadata": "^4.1.4", "gts": "2.0.2", "istanbul-instrumenter-loader": "3.0.1", "mocha": "7.2.0", "nyc": "15.1.0", - "sinon": "9.0.2", + "semver": "7.3.2", + "sinon": "9.0.3", "ts-loader": "7.0.5", "ts-mocha": "7.0.0", "typescript": "3.9.7" diff --git a/packages/opentelemetry-sdk-node/src/sdk.ts b/packages/opentelemetry-sdk-node/src/sdk.ts index cae8a6d0c95..9c8ad47d897 100644 --- a/packages/opentelemetry-sdk-node/src/sdk.ts +++ b/packages/opentelemetry-sdk-node/src/sdk.ts @@ -14,13 +14,20 @@ * limitations under the License. */ -import { HttpTextPropagator, metrics } from '@opentelemetry/api'; +import { TextMapPropagator, metrics } from '@opentelemetry/api'; import { ContextManager } from '@opentelemetry/context-base'; import { MeterConfig, MeterProvider } from '@opentelemetry/metrics'; import { NodeTracerConfig, NodeTracerProvider } from '@opentelemetry/node'; -import { detectResources, Resource } from '@opentelemetry/resources'; +import { + detectResources, + Resource, + ResourceDetectionConfig, + envDetector, +} from '@opentelemetry/resources'; import { BatchSpanProcessor, SpanProcessor } from '@opentelemetry/tracing'; import { NodeSDKConfiguration } from './types'; +import { awsEc2Detector } from '@opentelemetry/resource-detector-aws'; +import { gcpDetector } from '@opentelemetry/resource-detector-gcp'; const MAX_RESOURCE_WAIT_TIME_MS = 2000; /** This class represents everything needed to register a fully configured OpenTelemetry Node.js SDK */ @@ -29,7 +36,7 @@ export class NodeSDK { tracerConfig: NodeTracerConfig; spanProcessor: SpanProcessor; contextManager?: ContextManager; - httpTextPropagator?: HttpTextPropagator; + textMapPropagator?: TextMapPropagator; }; private _meterProviderConfig?: MeterConfig; @@ -72,7 +79,7 @@ export class NodeSDK { tracerProviderConfig, spanProcessor, configuration.contextManager, - configuration.httpTextPropagator + configuration.textMapPropagator ); } @@ -104,13 +111,13 @@ export class NodeSDK { tracerConfig: NodeTracerConfig, spanProcessor: SpanProcessor, contextManager?: ContextManager, - httpTextPropagator?: HttpTextPropagator + textMapPropagator?: TextMapPropagator ) { this._tracerProviderConfig = { tracerConfig, spanProcessor, contextManager, - httpTextPropagator, + textMapPropagator, }; } @@ -120,10 +127,16 @@ export class NodeSDK { } /** Detect resource attributes */ - private _detectResources(): Promise { + private _detectResources(config?: ResourceDetectionConfig): Promise { if (!this._autoDetectResources) { return Promise.resolve(this._resource); } + + const internalConfig: ResourceDetectionConfig = { + detectors: [awsEc2Detector, gcpDetector, envDetector], + ...config, + }; + return new Promise(resolve => { let resolved = false; setTimeout(() => { @@ -132,7 +145,7 @@ export class NodeSDK { resolve(this._resource); } }, MAX_RESOURCE_WAIT_TIME_MS); - detectResources().then( + detectResources(internalConfig).then( resource => { if (!resolved) { resolved = true; @@ -167,13 +180,13 @@ export class NodeSDK { if (this._tracerProviderConfig) { const tracerProvider = new NodeTracerProvider({ ...this._tracerProviderConfig.tracerConfig, - resource: (this._detectResources() as unknown) as Resource, + resource: this._detectResources(), }); tracerProvider.addSpanProcessor(this._tracerProviderConfig.spanProcessor); tracerProvider.register({ contextManager: this._tracerProviderConfig.contextManager, - propagator: this._tracerProviderConfig.httpTextPropagator, + propagator: this._tracerProviderConfig.textMapPropagator, }); } diff --git a/packages/opentelemetry-sdk-node/src/types.ts b/packages/opentelemetry-sdk-node/src/types.ts index a093cbcfa4c..4675c88b3d4 100644 --- a/packages/opentelemetry-sdk-node/src/types.ts +++ b/packages/opentelemetry-sdk-node/src/types.ts @@ -21,7 +21,7 @@ export interface NodeSDKConfiguration { autoDetectResources: boolean; contextManager: ContextManager; defaultAttributes: api.Attributes; - httpTextPropagator: api.HttpTextPropagator; + textMapPropagator: api.TextMapPropagator; logger: api.Logger; logLevel: core.LogLevel; metricBatcher: metrics.Batcher; diff --git a/packages/opentelemetry-sdk-node/test/sdk.test.ts b/packages/opentelemetry-sdk-node/test/sdk.test.ts index 73c19169910..8b3c004dedb 100644 --- a/packages/opentelemetry-sdk-node/test/sdk.test.ts +++ b/packages/opentelemetry-sdk-node/test/sdk.test.ts @@ -14,14 +14,17 @@ * limitations under the License. */ +import * as nock from 'nock'; +import * as semver from 'semver'; import { context, metrics, - NoopHttpTextPropagator, + NoopTextMapPropagator, NoopMeterProvider, NoopTracerProvider, propagation, trace, + ProxyTracerProvider, } from '@opentelemetry/api'; import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; import { NoopContextManager } from '@opentelemetry/context-base'; @@ -36,12 +39,54 @@ import * as assert from 'assert'; import { NodeSDK } from '../src'; import * as NodeConfig from '@opentelemetry/node/build/src/config'; import * as Sinon from 'sinon'; +import { awsEc2Detector } from '@opentelemetry/resource-detector-aws'; +import { resetIsAvailableCache } from '@opentelemetry/resource-detector-gcp'; +import { + assertServiceResource, + assertCloudResource, + assertHostResource, +} from '@opentelemetry/resources/test/util/resource-assertions'; +import { + BASE_PATH, + HEADER_NAME, + HEADER_VALUE, + HOST_ADDRESS, + SECONDARY_HOST_ADDRESS, +} from 'gcp-metadata'; +import { Resource } from '@opentelemetry/resources'; + +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 AWS_HOST = 'http://' + awsEc2Detector.AWS_IDMS_ENDPOINT; +const AWS_TOKEN_PATH = awsEc2Detector.AWS_INSTANCE_TOKEN_DOCUMENT_PATH; +const AWS_IDENTITY_PATH = awsEc2Detector.AWS_INSTANCE_IDENTITY_DOCUMENT_PATH; +const AWS_HOST_PATH = awsEc2Detector.AWS_INSTANCE_HOST_DOCUMENT_PATH; +const AWS_METADATA_TTL_HEADER = awsEc2Detector.AWS_METADATA_TTL_HEADER; +const AWS_METADATA_TOKEN_HEADER = awsEc2Detector.AWS_METADATA_TOKEN_HEADER; + +const mockedTokenResponse = 'my-token'; +const mockedIdentityResponse = { + instanceId: 'my-instance-id', + instanceType: 'my-instance-type', + accountId: 'my-account-id', + region: 'my-region', + availabilityZone: 'my-zone', +}; +const mockedHostResponse = 'my-hostname'; [true, false].forEach(autoDetectResources => { describe(`Node SDK autoDetectResources = "${autoDetectResources}"`, () => { before(() => { // Disable attempted load of default plugins Sinon.replace(NodeConfig, 'DEFAULT_INSTRUMENTATION_PLUGINS', {}); + nock.disableNetConnect(); }); after(() => { Sinon.restore(); @@ -61,16 +106,18 @@ import * as Sinon from 'sinon'; }); sdk.start(); - assert.ok( context['_getContextManager']() instanceof NoopContextManager ); assert.ok( propagation['_getGlobalPropagator']() instanceof - NoopHttpTextPropagator + NoopTextMapPropagator ); - assert.ok(trace.getTracerProvider() instanceof NoopTracerProvider); + const apiTracerProvider = trace.getTracerProvider(); + assert.ok(apiTracerProvider instanceof ProxyTracerProvider); + assert.ok(apiTracerProvider.getDelegate() instanceof NoopTracerProvider); + assert.ok(metrics.getMeterProvider() instanceof NoopMeterProvider); }); @@ -83,6 +130,15 @@ import * as Sinon from 'sinon'; sdk.start(); assert.ok(metrics.getMeterProvider() instanceof NoopMeterProvider); + assert.ok( + context['_getContextManager']() instanceof AsyncHooksContextManager + ); + assert.ok( + propagation['_getGlobalPropagator']() instanceof CompositePropagator + ); + const apiTracerProvider = trace.getTracerProvider(); + assert.ok(apiTracerProvider instanceof ProxyTracerProvider); + assert.ok(apiTracerProvider.getDelegate() instanceof NodeTracerProvider); assert.ok( context['_getContextManager']() instanceof AsyncHooksContextManager @@ -90,7 +146,6 @@ import * as Sinon from 'sinon'; assert.ok( propagation['_getGlobalPropagator']() instanceof CompositePropagator ); - assert.ok(trace.getTracerProvider() instanceof NodeTracerProvider); }); it('should register a tracer provider if a span processor is provided', async () => { @@ -103,16 +158,17 @@ import * as Sinon from 'sinon'; }); sdk.start(); - - assert.ok(metrics.getMeterProvider() instanceof NoopMeterProvider); - assert.ok( context['_getContextManager']() instanceof AsyncHooksContextManager ); assert.ok( propagation['_getGlobalPropagator']() instanceof CompositePropagator ); - assert.ok(trace.getTracerProvider() instanceof NodeTracerProvider); + const apiTracerProvider = trace.getTracerProvider(); + assert.ok(apiTracerProvider instanceof ProxyTracerProvider); + assert.ok(apiTracerProvider.getDelegate() instanceof NodeTracerProvider); + + assert.ok(metrics.getMeterProvider() instanceof NoopMeterProvider); }); it('should register a meter provider if an exporter is provided', async () => { @@ -122,6 +178,10 @@ import * as Sinon from 'sinon'; metricExporter: exporter, autoDetectResources, }); + assert.ok(context['_getContextManager']() instanceof NoopContextManager); + assert.ok( + propagation['_getGlobalPropagator']() instanceof NoopTextMapPropagator + ); sdk.start(); @@ -130,13 +190,270 @@ import * as Sinon from 'sinon'; ); assert.ok( propagation['_getGlobalPropagator']() instanceof - NoopHttpTextPropagator + NoopTextMapPropagator ); - assert.ok(trace.getTracerProvider() instanceof NoopTracerProvider); + const apiTracerProvider = trace.getTracerProvider(); + assert.ok(apiTracerProvider instanceof ProxyTracerProvider); + assert.ok(apiTracerProvider.getDelegate() instanceof NoopTracerProvider); assert.ok(metrics.getMeterProvider() instanceof MeterProvider); }); }); }); + + describe('detectResources', async () => { + beforeEach(() => { + nock.disableNetConnect(); + process.env.OTEL_RESOURCE_ATTRIBUTES = + 'service.instance.id=627cc493,service.name=my-service,service.namespace=default,service.version=0.0.1'; + }); + + afterEach(() => { + nock.cleanAll(); + nock.enableNetConnect(); + delete process.env.OTEL_RESOURCE_ATTRIBUTES; + }); + + // GCP detector only works in 10+ + (semver.satisfies(process.version, '>=10') ? describe : describe.skip)( + 'in GCP environment', + async () => { + beforeEach(resetIsAvailableCache); + after(resetIsAvailableCache); + it('returns a merged resource', async () => { + const sdk = new NodeSDK({ + autoDetectResources: true, + }); + const gcpScope = 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 gcpSecondaryScope = nock(SECONDARY_HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS); + + const resource = await sdk["_detectResources"](); + + gcpSecondaryScope.done(); + gcpScope.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 sdk = new NodeSDK({ + autoDetectResources: true, + }); + const awsScope = nock(AWS_HOST) + .persist() + .put(AWS_TOKEN_PATH) + .matchHeader(AWS_METADATA_TTL_HEADER, '60') + .reply(200, () => mockedTokenResponse) + .get(AWS_IDENTITY_PATH) + .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) + .reply(200, () => mockedIdentityResponse) + .get(AWS_HOST_PATH) + .matchHeader(AWS_METADATA_TOKEN_HEADER, mockedTokenResponse) + .reply(200, () => mockedHostResponse); + const resource = await sdk["_detectResources"](); + awsScope.done(); + + assertCloudResource(resource, { + provider: 'aws', + accountId: 'my-account-id', + region: 'my-region', + zone: 'my-zone', + }); + assertHostResource(resource, { + id: 'my-instance-id', + hostType: 'my-instance-type', + name: 'my-hostname', + hostName: 'my-hostname', + }); + assertServiceResource(resource, { + instanceId: '627cc493', + name: 'my-service', + namespace: 'default', + version: '0.0.1', + }); + }); + }); + + describe('in no environment', () => { + it('should return empty resource', async () => { + const scope = nock(AWS_HOST).put(AWS_TOKEN_PATH).replyWithError({ + code: 'ENOTFOUND', + }); + const sdk = new NodeSDK({ + autoDetectResources: true, + }); + const resource = await sdk["_detectResources"]({ + detectors: [awsEc2Detector], + }); + assert.ok(resource); + assert.deepStrictEqual(resource, Resource.createTelemetrySDKResource()); + + scope.done(); + }); + }); + + describe('with a buggy detector', () => { + it('returns a merged resource', async () => { + const sdk = new NodeSDK({ + autoDetectResources: true, + }); + const stub = Sinon.stub(awsEc2Detector, 'detect').throws(); + const resource = await sdk["_detectResources"](); + + assertServiceResource(resource, { + instanceId: '627cc493', + name: 'my-service', + namespace: 'default', + version: '0.0.1', + }); + + stub.restore(); + }); + }); + + describe('with a debug logger', () => { + // Local functions to test if a mocked method is ever called with a specific argument or regex matching for an argument. + // Needed because of race condition with parallel detectors. + const callArgsContains = ( + mockedFunction: sinon.SinonSpy, + arg: any + ): boolean => { + return mockedFunction.getCalls().some(call => { + return call.args.some(callarg => arg === callarg); + }); + }; + const callArgsMatches = ( + mockedFunction: sinon.SinonSpy, + regex: RegExp + ): boolean => { + return mockedFunction.getCalls().some(call => { + return regex.test(call.args.toString()); + }); + }; + + it('prints detected resources and debug messages to the logger', async () => { + const sdk = new NodeSDK({ + autoDetectResources: true, + }); + // This test depends on the env detector to be functioning as intended + const mockedLoggerMethod = Sinon.fake(); + await sdk["_detectResources"]({ + logger: { + debug: mockedLoggerMethod, + info: Sinon.fake(), + warn: Sinon.fake(), + error: Sinon.fake(), + }, + }); + + // Test for AWS and GCP Detector failure + assert.ok( + callArgsContains( + mockedLoggerMethod, + 'GcpDetector failed: GCP Metadata unavailable.' + ) + ); + assert.ok( + callArgsContains( + mockedLoggerMethod, + 'AwsEc2Detector failed: Nock: Disallowed net connect for "169.254.169.254:80/latest/api/token"' + ) + ); + // Test that the Env Detector successfully found its resource and populated it with the right values. + assert.ok( + callArgsContains(mockedLoggerMethod, 'EnvDetector found resource.') + ); + // Regex formatting accounts for whitespace variations in util.inspect output over different node versions + assert.ok( + callArgsMatches( + mockedLoggerMethod, + /{\s+'service\.instance\.id':\s+'627cc493',\s+'service\.name':\s+'my-service',\s+'service\.namespace':\s+'default',\s+'service\.version':\s+'0\.0\.1'\s+}\s*/ + ) + ); + }); + + describe('with missing environment variable', () => { + beforeEach(() => { + delete process.env.OTEL_RESOURCE_ATTRIBUTES; + }); + + it('prints correct error messages when EnvDetector has no env variable', async () => { + const sdk = new NodeSDK({ + autoDetectResources: true, + }); + const mockedLoggerMethod = Sinon.fake(); + await sdk["_detectResources"]({ + logger: { + debug: mockedLoggerMethod, + info: Sinon.fake(), + warn: Sinon.fake(), + error: Sinon.fake(), + }, + }); + + assert.ok( + callArgsContains( + mockedLoggerMethod, + 'EnvDetector failed: Environment variable "OTEL_RESOURCE_ATTRIBUTES" is missing.' + ) + ); + }); + }); + + describe('with a faulty environment variable', () => { + beforeEach(() => { + process.env.OTEL_RESOURCE_ATTRIBUTES = 'bad=~attribute'; + }); + + it('prints correct error messages when EnvDetector has an invalid variable', async () => { + const sdk = new NodeSDK({ + autoDetectResources: true, + }); + const mockedLoggerMethod = Sinon.fake(); + await sdk["_detectResources"]({ + logger: { + debug: mockedLoggerMethod, + info: Sinon.fake(), + warn: Sinon.fake(), + error: Sinon.fake(), + }, + }); + + assert.ok( + callArgsContains( + mockedLoggerMethod, + 'EnvDetector failed: Attribute value should be a ASCII string with a length not exceed 255 characters.' + ) + ); + }); + }); + }); + }); }); diff --git a/packages/opentelemetry-semantic-conventions/package.json b/packages/opentelemetry-semantic-conventions/package.json index c6b0d2f1db3..ade303f31b7 100644 --- a/packages/opentelemetry-semantic-conventions/package.json +++ b/packages/opentelemetry-semantic-conventions/package.json @@ -14,7 +14,8 @@ "precompile": "tsc --version", "version:update": "node ../../scripts/version-update.js", "compile": "npm run version:update && tsc -p .", - "prepare": "npm run compile" + "prepare": "npm run compile", + "watch": "tsc -w" }, "keywords": [ "opentelemetry", @@ -40,7 +41,7 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "@types/sinon": "9.0.4", "codecov": "3.7.2", @@ -49,7 +50,7 @@ "nock": "12.0.3", "nyc": "15.1.0", "rimraf": "3.0.2", - "sinon": "9.0.2", + "sinon": "9.0.3", "ts-mocha": "7.0.0", "ts-node": "8.10.2", "typescript": "3.9.7" diff --git a/packages/opentelemetry-semantic-conventions/src/trace/exception.ts b/packages/opentelemetry-semantic-conventions/src/trace/exception.ts new file mode 100644 index 00000000000..cf7dc596bef --- /dev/null +++ b/packages/opentelemetry-semantic-conventions/src/trace/exception.ts @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const ExceptionAttribute = { + MESSAGE: 'exception.message', + STACKTRACE: 'exception.stacktrace', + TYPE: 'exception.type', +}; + +export const ExceptionEventName = 'exception'; diff --git a/packages/opentelemetry-semantic-conventions/src/trace/index.ts b/packages/opentelemetry-semantic-conventions/src/trace/index.ts index ca18acaea63..9852f831bb8 100644 --- a/packages/opentelemetry-semantic-conventions/src/trace/index.ts +++ b/packages/opentelemetry-semantic-conventions/src/trace/index.ts @@ -14,8 +14,9 @@ * limitations under the License. */ +export * from './database'; +export * from './exception'; export * from './general'; -export * from './rpc'; export * from './http'; -export * from './database'; export * from './os'; +export * from './rpc'; diff --git a/packages/opentelemetry-shim-opentracing/package.json b/packages/opentelemetry-shim-opentracing/package.json index bc256349ff5..70129eda4d6 100644 --- a/packages/opentelemetry-shim-opentracing/package.json +++ b/packages/opentelemetry-shim-opentracing/package.json @@ -41,7 +41,7 @@ }, "devDependencies": { "@opentelemetry/tracing": "^0.10.2", - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "codecov": "3.7.2", "gts": "2.0.2", diff --git a/packages/opentelemetry-shim-opentracing/test/Shim.test.ts b/packages/opentelemetry-shim-opentracing/test/Shim.test.ts index 51b0bdd890b..10f2e8b2f7c 100644 --- a/packages/opentelemetry-shim-opentracing/test/Shim.test.ts +++ b/packages/opentelemetry-shim-opentracing/test/Shim.test.ts @@ -19,13 +19,12 @@ import * as opentracing from 'opentracing'; import { BasicTracerProvider, Span } from '@opentelemetry/tracing'; import { TracerShim, SpanShim, SpanContextShim } from '../src/shim'; import { - INVALID_SPAN_CONTEXT, timeInputToHrTime, HttpTraceContext, CompositePropagator, HttpCorrelationContext, } from '@opentelemetry/core'; -import { propagation } from '@opentelemetry/api'; +import { INVALID_SPAN_CONTEXT, propagation } from '@opentelemetry/api'; import { performance } from 'perf_hooks'; describe('OpenTracing Shim', () => { diff --git a/packages/opentelemetry-tracing/package.json b/packages/opentelemetry-tracing/package.json index 9a00d4c8471..0ea862b4c1f 100644 --- a/packages/opentelemetry-tracing/package.json +++ b/packages/opentelemetry-tracing/package.json @@ -50,7 +50,7 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "@types/sinon": "9.0.4", "@types/webpack-env": "1.15.2", @@ -66,7 +66,7 @@ "mocha": "7.2.0", "nyc": "15.1.0", "rimraf": "3.0.2", - "sinon": "9.0.2", + "sinon": "9.0.3", "ts-loader": "8.0.2", "ts-mocha": "7.0.0", "ts-node": "8.10.2", @@ -77,6 +77,7 @@ "@opentelemetry/api": "^0.10.2", "@opentelemetry/context-base": "^0.10.2", "@opentelemetry/core": "^0.10.2", - "@opentelemetry/resources": "^0.10.2" + "@opentelemetry/resources": "^0.10.2", + "@opentelemetry/semantic-conventions": "^0.10.2" } } diff --git a/packages/opentelemetry-tracing/src/BasicTracerProvider.ts b/packages/opentelemetry-tracing/src/BasicTracerProvider.ts index cd19857f43d..6ad3feefdcd 100644 --- a/packages/opentelemetry-tracing/src/BasicTracerProvider.ts +++ b/packages/opentelemetry-tracing/src/BasicTracerProvider.ts @@ -20,6 +20,7 @@ import { HttpTraceContext, HttpCorrelationContext, CompositePropagator, + notifyOnGlobalShutdown, } from '@opentelemetry/core'; import { SpanProcessor, Tracer } from '.'; import { DEFAULT_CONFIG } from './config'; @@ -35,18 +36,31 @@ export class BasicTracerProvider implements api.TracerProvider { private readonly _config: TracerConfig; private readonly _registeredSpanProcessors: SpanProcessor[] = []; private readonly _tracers: Map = new Map(); + private _cleanNotifyOnGlobalShutdown: Function | undefined; activeSpanProcessor = new NoopSpanProcessor(); readonly logger: api.Logger; - readonly resource: Resource; + readonly resource: Promise; constructor(config: TracerConfig = DEFAULT_CONFIG) { this.logger = config.logger ?? new ConsoleLogger(config.logLevel); - this.resource = config.resource ?? Resource.createTelemetrySDKResource(); + if (config.resource) { + this.resource = + config.resource instanceof Promise + ? config.resource + : Promise.resolve(config.resource); + } else { + this.resource = Promise.resolve(Resource.createTelemetrySDKResource()); + } this._config = Object.assign({}, config, { logger: this.logger, resource: this.resource, }); + if (this._config.gracefulShutdown) { + this._cleanNotifyOnGlobalShutdown = notifyOnGlobalShutdown( + this._shutdownActiveProcessor.bind(this) + ); + } } getTracer(name: string, version = '*', config?: TracerConfig): Tracer { @@ -99,4 +113,16 @@ export class BasicTracerProvider implements api.TracerProvider { api.propagation.setGlobalPropagator(config.propagator); } } + + shutdown(cb: () => void = () => {}) { + this.activeSpanProcessor.shutdown(cb); + if (this._cleanNotifyOnGlobalShutdown) { + this._cleanNotifyOnGlobalShutdown(); + this._cleanNotifyOnGlobalShutdown = undefined; + } + } + + private _shutdownActiveProcessor() { + this.activeSpanProcessor.shutdown(); + } } diff --git a/packages/opentelemetry-tracing/src/Span.ts b/packages/opentelemetry-tracing/src/Span.ts index 0b62468aa2b..429dcf72993 100644 --- a/packages/opentelemetry-tracing/src/Span.ts +++ b/packages/opentelemetry-tracing/src/Span.ts @@ -20,18 +20,21 @@ import { hrTimeDuration, InstrumentationLibrary, isTimeInput, - timeInputToHrTime, + timeInputToHrTime } from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; -import { ReadableSpan } from './export/ReadableSpan'; -import { Tracer } from './Tracer'; +import { + ExceptionAttribute, + ExceptionEventName +} from '@opentelemetry/semantic-conventions'; import { SpanProcessor } from './SpanProcessor'; +import { Tracer } from './Tracer'; import { TraceParams } from './types'; /** * This class represents a span. */ -export class Span implements api.Span, ReadableSpan { +export class Span implements api.Span { // Below properties are included to implement ReadableSpan for export // purposes but are not intended to be written-to directly. readonly spanContext: api.SpanContext; @@ -41,7 +44,7 @@ export class Span implements api.Span, ReadableSpan { readonly links: api.Link[] = []; readonly events: api.TimedEvent[] = []; readonly startTime: api.HrTime; - resource: Resource; + private _resource: Resource | Promise; readonly instrumentationLibrary: InstrumentationLibrary; name: string; status: api.Status = { @@ -70,12 +73,15 @@ export class Span implements api.Span, ReadableSpan { this.kind = kind; this.links = links; this.startTime = timeInputToHrTime(startTime); - this.resource = parentTracer.resource; + this._resource = parentTracer.resource; this.instrumentationLibrary = parentTracer.instrumentationLibrary; this._logger = parentTracer.logger; this._traceParams = parentTracer.getActiveTraceParams(); this._spanProcessor = parentTracer.getActiveSpanProcessor(); - this._spanProcessor.onStart(this); + + this._resolveResource().then(() => { + this._spanProcessor.onStart(this); + }); } context(): api.SpanContext { @@ -171,21 +177,44 @@ export class Span implements api.Span, ReadableSpan { ); } - if (this.resource instanceof Promise) { - this.resource.then(resource => { - this.resource = resource; - this._spanProcessor.onEnd(this); - }); - return; - } - - this._spanProcessor.onEnd(this); + this._resolveResource().then(() => { + this._spanProcessor.onEnd(this); + }); } isRecording(): boolean { return true; } + recordException(exception: api.Exception, time: api.TimeInput = hrTime()) { + const attributes: api.Attributes = {}; + if (typeof exception === 'string') { + attributes[ExceptionAttribute.MESSAGE] = exception; + } else if (exception) { + if (exception.code) { + attributes[ExceptionAttribute.TYPE] = exception.code; + } else if (exception.name) { + attributes[ExceptionAttribute.TYPE] = exception.name; + } + if (exception.message) { + attributes[ExceptionAttribute.MESSAGE] = exception.message; + } + if (exception.stack) { + attributes[ExceptionAttribute.STACKTRACE] = exception.stack; + } + } + + // these are minimum requirements from spec + if ( + attributes[ExceptionAttribute.TYPE] || + attributes[ExceptionAttribute.MESSAGE] + ) { + this.addEvent(ExceptionEventName, attributes as api.Attributes, time); + } else { + this._logger.warn(`Failed to record an exception ${exception}`); + } + } + get duration(): api.HrTime { return this._duration; } @@ -194,6 +223,13 @@ export class Span implements api.Span, ReadableSpan { return this._ended; } + get resource(): Resource { + if (this._resource instanceof Promise) { + return Resource.createTelemetrySDKResource(); + } + return this._resource; + } + private _isSpanEnded(): boolean { if (this._ended) { this._logger.warn( @@ -204,4 +240,12 @@ export class Span implements api.Span, ReadableSpan { } return this._ended; } + + private async _resolveResource() { + try { + this._resource = await this._resource; + } catch (err) { + this._logger.error(`Resource failed to resolve: ${err?.message}`); + } + } } diff --git a/packages/opentelemetry-tracing/src/Tracer.ts b/packages/opentelemetry-tracing/src/Tracer.ts index 7dc27545973..15561ff740d 100644 --- a/packages/opentelemetry-tracing/src/Tracer.ts +++ b/packages/opentelemetry-tracing/src/Tracer.ts @@ -20,11 +20,11 @@ import { getActiveSpan, getParentSpanContext, InstrumentationLibrary, - isValid, NoRecordingSpan, IdGenerator, RandomIdGenerator, setActiveSpan, + isInstrumentationSuppressed, } from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; import { BasicTracerProvider } from './BasicTracerProvider'; @@ -39,7 +39,8 @@ export class Tracer implements api.Tracer { private readonly _sampler: api.Sampler; private readonly _traceParams: TraceParams; private readonly _idGenerator: IdGenerator; - public resource: Resource; + + public readonly resource: Promise; readonly instrumentationLibrary: InstrumentationLibrary; readonly logger: api.Logger; @@ -56,11 +57,6 @@ export class Tracer implements api.Tracer { this._traceParams = localConfig.traceParams; this._idGenerator = config.idGenerator || new RandomIdGenerator(); this.resource = _tracerProvider.resource; - if (this.resource instanceof Promise) { - this.resource.then(resource => { - this.resource = resource; - }); - } this.instrumentationLibrary = instrumentationLibrary; this.logger = config.logger || new ConsoleLogger(config.logLevel); } @@ -74,11 +70,16 @@ export class Tracer implements api.Tracer { options: api.SpanOptions = {}, context = api.context.active() ): api.Span { + if (isInstrumentationSuppressed(context)) { + this.logger.debug('Instrumentation suppressed, returning Noop Span'); + return api.NOOP_SPAN; + } + const parentContext = getParent(options, context); const spanId = this._idGenerator.generateSpanId(); let traceId; let traceState; - if (!parentContext || !isValid(parentContext)) { + if (!parentContext || !api.trace.isSpanContextValid(parentContext)) { // New root span. traceId = this._idGenerator.generateTraceId(); } else { @@ -86,6 +87,7 @@ export class Tracer implements api.Tracer { traceId = parentContext.traceId; traceState = parentContext.traceState; } + const spanKind = options.kind ?? api.SpanKind.INTERNAL; const links = options.links ?? []; const attributes = options.attributes ?? {}; diff --git a/packages/opentelemetry-tracing/src/config.ts b/packages/opentelemetry-tracing/src/config.ts index 180dda3d771..4bbb6e11cfc 100644 --- a/packages/opentelemetry-tracing/src/config.ts +++ b/packages/opentelemetry-tracing/src/config.ts @@ -37,4 +37,5 @@ export const DEFAULT_CONFIG = { numberOfLinksPerSpan: DEFAULT_MAX_LINKS_PER_SPAN, numberOfEventsPerSpan: DEFAULT_MAX_EVENTS_PER_SPAN, }, + gracefulShutdown: true, }; diff --git a/packages/opentelemetry-tracing/src/export/BatchSpanProcessor.ts b/packages/opentelemetry-tracing/src/export/BatchSpanProcessor.ts index a81e747ac8d..866e499cbb3 100644 --- a/packages/opentelemetry-tracing/src/export/BatchSpanProcessor.ts +++ b/packages/opentelemetry-tracing/src/export/BatchSpanProcessor.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { unrefTimer } from '@opentelemetry/core'; +import { context } from '@opentelemetry/api'; +import { unrefTimer, suppressInstrumentation } from '@opentelemetry/core'; import { SpanProcessor } from '../SpanProcessor'; import { BufferConfig } from '../types'; import { ReadableSpan } from './ReadableSpan'; @@ -88,7 +89,12 @@ export class BatchSpanProcessor implements SpanProcessor { setTimeout(cb, 0); return; } - this._exporter.export(this._finishedSpans, cb); + + // prevent downstream exporter calls from generating spans + context.with(suppressInstrumentation(context.active()), () => { + this._exporter.export(this._finishedSpans, cb); + }); + this._finishedSpans = []; } diff --git a/packages/opentelemetry-tracing/src/export/InMemorySpanExporter.ts b/packages/opentelemetry-tracing/src/export/InMemorySpanExporter.ts index 04eec133182..4a37a0744c3 100644 --- a/packages/opentelemetry-tracing/src/export/InMemorySpanExporter.ts +++ b/packages/opentelemetry-tracing/src/export/InMemorySpanExporter.ts @@ -20,12 +20,16 @@ import { ExportResult } from '@opentelemetry/core'; /** * This class can be used for testing purposes. It stores the exported spans - * in a list in memory that can be retrieve using the `getFinishedSpans()` + * in a list in memory that can be retrieved using the `getFinishedSpans()` * method. */ export class InMemorySpanExporter implements SpanExporter { private _finishedSpans: ReadableSpan[] = []; - private _stopped = false; + /** + * Indicates if the exporter has been "shutdown." + * When false, exported spans will not be stored in-memory. + */ + protected _stopped = false; export( spans: ReadableSpan[], @@ -33,7 +37,8 @@ export class InMemorySpanExporter implements SpanExporter { ): void { if (this._stopped) return resultCallback(ExportResult.FAILED_NOT_RETRYABLE); this._finishedSpans.push(...spans); - return resultCallback(ExportResult.SUCCESS); + + setTimeout(() => resultCallback(ExportResult.SUCCESS), 0); } shutdown(): void { diff --git a/packages/opentelemetry-tracing/src/export/SimpleSpanProcessor.ts b/packages/opentelemetry-tracing/src/export/SimpleSpanProcessor.ts index 294b61777a1..c63b6fbff4b 100644 --- a/packages/opentelemetry-tracing/src/export/SimpleSpanProcessor.ts +++ b/packages/opentelemetry-tracing/src/export/SimpleSpanProcessor.ts @@ -17,6 +17,8 @@ import { SpanProcessor } from '../SpanProcessor'; import { SpanExporter } from './SpanExporter'; import { ReadableSpan } from './ReadableSpan'; +import { context } from '@opentelemetry/api'; +import { suppressInstrumentation } from '@opentelemetry/core'; /** * An implementation of the {@link SpanProcessor} that converts the {@link Span} @@ -40,7 +42,11 @@ export class SimpleSpanProcessor implements SpanProcessor { if (this._isShutdown) { return; } - this._exporter.export([span], () => {}); + + // prevent downstream exporter calls from generating spans + context.with(suppressInstrumentation(context.active()), () => { + this._exporter.export([span], () => {}); + }); } shutdown(cb: () => void = () => {}): void { diff --git a/packages/opentelemetry-tracing/src/types.ts b/packages/opentelemetry-tracing/src/types.ts index 832b3b48988..06f180afa81 100644 --- a/packages/opentelemetry-tracing/src/types.ts +++ b/packages/opentelemetry-tracing/src/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { HttpTextPropagator, Logger, Sampler } from '@opentelemetry/api'; +import { TextMapPropagator, Logger, Sampler } from '@opentelemetry/api'; import { LogLevel, IdGenerator } from '@opentelemetry/core'; import { ContextManager } from '@opentelemetry/context-base'; @@ -41,7 +41,10 @@ export interface TracerConfig { traceParams?: TraceParams; /** Resource associated with trace telemetry */ - resource?: Resource; + resource?: Resource | Promise; + + /** Bool for whether or not graceful shutdown is enabled. If disabled spans will not be exported when SIGTERM is recieved */ + gracefulShutdown?: boolean; /** * Generator of trace and span IDs @@ -57,7 +60,7 @@ export interface TracerConfig { */ export interface SDKRegistrationConfig { /** Propagator to register as the global propagator */ - propagator?: HttpTextPropagator | null; + propagator?: TextMapPropagator | null; /** Context manager to register as the global context manager */ contextManager?: ContextManager | null; diff --git a/packages/opentelemetry-tracing/test/BasicTracerProvider.test.ts b/packages/opentelemetry-tracing/test/BasicTracerProvider.test.ts index 5933d04fd42..2e95fce0203 100644 --- a/packages/opentelemetry-tracing/test/BasicTracerProvider.test.ts +++ b/packages/opentelemetry-tracing/test/BasicTracerProvider.test.ts @@ -24,14 +24,29 @@ import { setActiveSpan, setExtractedSpanContext, TraceState, + notifyOnGlobalShutdown, + _invokeGlobalShutdown, } from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; import * as assert from 'assert'; +import * as sinon from 'sinon'; import { BasicTracerProvider, Span } from '../src'; describe('BasicTracerProvider', () => { + let sandbox: sinon.SinonSandbox; + let removeEvent: Function | undefined; + beforeEach(() => { context.disable(); + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + if (removeEvent) { + removeEvent(); + removeEvent = undefined; + } }); describe('constructor', () => { @@ -352,4 +367,43 @@ describe('BasicTracerProvider', () => { assert.ok(tracerProvider.resource instanceof Resource); }); }); + + describe('.shutdown()', () => { + it('should trigger shutdown when SIGTERM is recieved', () => { + const tracerProvider = new BasicTracerProvider(); + const shutdownStub = sandbox.stub( + tracerProvider.getActiveSpanProcessor(), + 'shutdown' + ); + removeEvent = notifyOnGlobalShutdown(() => { + sinon.assert.calledOnce(shutdownStub); + }); + _invokeGlobalShutdown(); + }); + + it('should trigger shutdown when manually invoked', () => { + const tracerProvider = new BasicTracerProvider(); + const shutdownStub = sandbox.stub( + tracerProvider.getActiveSpanProcessor(), + 'shutdown' + ); + tracerProvider.shutdown(); + sinon.assert.calledOnce(shutdownStub); + }); + + it('should not trigger shutdown if graceful shutdown is turned off', () => { + const tracerProvider = new BasicTracerProvider({ + gracefulShutdown: false, + }); + const sandbox = sinon.createSandbox(); + const shutdownStub = sandbox.stub( + tracerProvider.getActiveSpanProcessor(), + 'shutdown' + ); + removeEvent = notifyOnGlobalShutdown(() => { + sinon.assert.notCalled(shutdownStub); + }); + _invokeGlobalShutdown(); + }); + }); }); diff --git a/packages/opentelemetry-tracing/test/MultiSpanProcessor.test.ts b/packages/opentelemetry-tracing/test/MultiSpanProcessor.test.ts index c4e4aa35fd9..7db10cd26e4 100644 --- a/packages/opentelemetry-tracing/test/MultiSpanProcessor.test.ts +++ b/packages/opentelemetry-tracing/test/MultiSpanProcessor.test.ts @@ -23,6 +23,10 @@ import { Span, SpanProcessor, } from '../src'; +import { + notifyOnGlobalShutdown, + _invokeGlobalShutdown, +} from '@opentelemetry/core'; import { MultiSpanProcessor } from '../src/MultiSpanProcessor'; class TestProcessor implements SpanProcessor { @@ -38,6 +42,14 @@ class TestProcessor implements SpanProcessor { } describe('MultiSpanProcessor', () => { + let removeEvent: Function | undefined; + afterEach(() => { + if (removeEvent) { + removeEvent(); + removeEvent = undefined; + } + }); + it('should handle empty span processor', () => { const multiSpanProcessor = new MultiSpanProcessor([]); @@ -84,6 +96,51 @@ describe('MultiSpanProcessor', () => { assert.strictEqual(processor1.spans.length, processor2.spans.length); }); + it('should export spans on graceful shutdown from two span processor', () => { + const processor1 = new TestProcessor(); + const processor2 = new TestProcessor(); + const multiSpanProcessor = new MultiSpanProcessor([processor1, processor2]); + + const tracerProvider = new BasicTracerProvider(); + tracerProvider.addSpanProcessor(multiSpanProcessor); + const tracer = tracerProvider.getTracer('default'); + const span = tracer.startSpan('one'); + assert.strictEqual(processor1.spans.length, 0); + assert.strictEqual(processor1.spans.length, processor2.spans.length); + + span.end(); + assert.strictEqual(processor1.spans.length, 1); + assert.strictEqual(processor1.spans.length, processor2.spans.length); + + removeEvent = notifyOnGlobalShutdown(() => { + assert.strictEqual(processor1.spans.length, 0); + assert.strictEqual(processor1.spans.length, processor2.spans.length); + }); + _invokeGlobalShutdown(); + }); + + it('should export spans on manual shutdown from two span processor', () => { + const processor1 = new TestProcessor(); + const processor2 = new TestProcessor(); + const multiSpanProcessor = new MultiSpanProcessor([processor1, processor2]); + + const tracerProvider = new BasicTracerProvider(); + tracerProvider.addSpanProcessor(multiSpanProcessor); + const tracer = tracerProvider.getTracer('default'); + const span = tracer.startSpan('one'); + assert.strictEqual(processor1.spans.length, 0); + assert.strictEqual(processor1.spans.length, processor2.spans.length); + + span.end(); + assert.strictEqual(processor1.spans.length, 1); + assert.strictEqual(processor1.spans.length, processor2.spans.length); + + tracerProvider.shutdown(() => { + assert.strictEqual(processor1.spans.length, 0); + assert.strictEqual(processor1.spans.length, processor2.spans.length); + }); + }); + it('should force span processors to flush', () => { let flushed = false; const processor: SpanProcessor = { diff --git a/packages/opentelemetry-tracing/test/Span.test.ts b/packages/opentelemetry-tracing/test/Span.test.ts index 8b13b829ac8..c4bc9c7ffd5 100644 --- a/packages/opentelemetry-tracing/test/Span.test.ts +++ b/packages/opentelemetry-tracing/test/Span.test.ts @@ -15,6 +15,7 @@ */ import { Resource } from '@opentelemetry/resources'; +import { ExceptionAttribute } from '@opentelemetry/semantic-conventions'; import * as assert from 'assert'; import * as sinon from 'sinon'; import { @@ -23,6 +24,7 @@ import { TraceFlags, SpanContext, LinkContext, + Exception, } from '@opentelemetry/api'; import { BasicTracerProvider, @@ -76,7 +78,7 @@ describe('Span', () => { const span = new Span(tracer, name, spanContext, SpanKind.SERVER); assert.ok( hrTimeToMilliseconds(span.startTime) > - hrTimeToMilliseconds(performanceTimeOrigin) + hrTimeToMilliseconds(performanceTimeOrigin) ); }); @@ -90,7 +92,7 @@ describe('Span', () => { assert.ok( hrTimeToMilliseconds(span.endTime) > - hrTimeToMilliseconds(performanceTimeOrigin), + hrTimeToMilliseconds(performanceTimeOrigin), 'end time must be bigger than time origin' ); }); @@ -106,7 +108,7 @@ describe('Span', () => { span.addEvent('my-event'); assert.ok( hrTimeToMilliseconds(span.events[0].time) > - hrTimeToMilliseconds(performanceTimeOrigin) + hrTimeToMilliseconds(performanceTimeOrigin) ); }); @@ -403,10 +405,97 @@ describe('Span', () => { setTimeout(() => { const exportedSpan = (spy.args[0][0][0] as unknown) as ReadableSpan; assert.deepStrictEqual(exportedSpan.spanContext, span.context()); - assert.deepStrictEqual(exportedSpan.resource.labels, { + assert.deepStrictEqual(exportedSpan.resource.attributes, { foo: 'bar', }); }, 10); }); }); + + describe('recordException', () => { + const invalidExceptions: any[] = [ + 1, + null, + undefined, + { foo: 'bar' }, + { stack: 'bar' }, + ['a', 'b', 'c'], + ]; + + invalidExceptions.forEach(key => { + describe(`when exception is (${JSON.stringify(key)})`, () => { + it('should NOT record an exception', () => { + const span = new Span(tracer, name, spanContext, SpanKind.CLIENT); + assert.strictEqual(span.events.length, 0); + span.recordException(key); + assert.strictEqual(span.events.length, 0); + }); + }); + }); + + describe('when exception type is "string"', () => { + let error: Exception; + beforeEach(() => { + error = 'boom'; + }); + it('should record an exception', () => { + const span = new Span(tracer, name, spanContext, SpanKind.CLIENT); + assert.strictEqual(span.events.length, 0); + span.recordException(error); + + const event = span.events[0]; + assert.strictEqual(event.name, 'exception'); + assert.deepStrictEqual(event.attributes, { + 'exception.message': 'boom', + }); + assert.ok(event.time[0] > 0); + }); + }); + + const errorsObj = [ + { + description: 'code', + obj: { code: 'Error', message: 'boom', stack: 'bar' }, + }, + { + description: 'name', + obj: { name: 'Error', message: 'boom', stack: 'bar' }, + }, + ]; + errorsObj.forEach(errorObj => { + describe(`when exception type is an object with ${errorObj.description}`, () => { + const error: Exception = errorObj.obj; + it('should record an exception', () => { + const span = new Span(tracer, name, spanContext, SpanKind.CLIENT); + assert.strictEqual(span.events.length, 0); + span.recordException(error); + + const event = span.events[0]; + assert.ok(event.time[0] > 0); + assert.strictEqual(event.name, 'exception'); + + assert.ok(event.attributes); + + const type = event.attributes[ExceptionAttribute.TYPE]; + const message = event.attributes[ExceptionAttribute.MESSAGE]; + const stacktrace = String( + event.attributes[ExceptionAttribute.STACKTRACE] + ); + assert.strictEqual(type, 'Error'); + assert.strictEqual(message, 'boom'); + assert.strictEqual(stacktrace, 'bar'); + }); + }); + }); + + describe('when time is provided', () => { + it('should record an exception with provided time', () => { + const span = new Span(tracer, name, spanContext, SpanKind.CLIENT); + assert.strictEqual(span.events.length, 0); + span.recordException('boom', [0, 123]); + const event = span.events[0]; + assert.deepStrictEqual(event.time, [0, 123]); + }); + }); + }); }); diff --git a/packages/opentelemetry-tracing/test/Tracer.test.ts b/packages/opentelemetry-tracing/test/Tracer.test.ts index 8bf0481c85c..9851aee624d 100644 --- a/packages/opentelemetry-tracing/test/Tracer.test.ts +++ b/packages/opentelemetry-tracing/test/Tracer.test.ts @@ -19,6 +19,8 @@ import { NoopSpan, Sampler, SamplingDecision, + Context, + NOOP_SPAN, TraceFlags, } from '@opentelemetry/api'; import { BasicTracerProvider, Tracer, Span } from '../src'; @@ -27,6 +29,7 @@ import { NoopLogger, AlwaysOnSampler, AlwaysOffSampler, + suppressInstrumentation, } from '@opentelemetry/core'; describe('Tracer', () => { @@ -115,6 +118,25 @@ describe('Tracer', () => { assert.strictEqual(lib.version, '0.0.1'); }); + describe('when suppressInstrumentation true', () => { + const context = suppressInstrumentation(Context.ROOT_CONTEXT); + + it('should return cached no-op span ', done => { + const tracer = new Tracer( + { name: 'default', version: '0.0.1' }, + { sampler: new TestSampler() }, + tracerProvider + ); + + const span = tracer.startSpan('span3', undefined, context); + + assert.equal(span, NOOP_SPAN); + span.end(); + + done(); + }); + }); + if (typeof process !== 'undefined' && process.release.name === 'node') { it('should sample a trace when OTEL_SAMPLING_PROBABILITY is invalid', () => { process.env.OTEL_SAMPLING_PROBABILITY = 'invalid value'; diff --git a/packages/opentelemetry-tracing/test/export/BatchSpanProcessor.test.ts b/packages/opentelemetry-tracing/test/export/BatchSpanProcessor.test.ts index 73ac0a00538..56364a10747 100644 --- a/packages/opentelemetry-tracing/test/export/BatchSpanProcessor.test.ts +++ b/packages/opentelemetry-tracing/test/export/BatchSpanProcessor.test.ts @@ -23,6 +23,9 @@ import { InMemorySpanExporter, Span, } from '../../src'; +import { context } from '@opentelemetry/api'; +import { TestTracingSpanExporter } from './TestTracingSpanExporter'; +import { TestStackContextManager } from './TestStackContextManager'; function createSampledSpan(spanName: string): Span { const tracer = new BasicTracerProvider({ @@ -214,7 +217,6 @@ describe('BatchSpanProcessor', () => { it('should call an async callback when shutdown is complete', done => { let exportedSpans = 0; sinon.stub(exporter, 'export').callsFake((spans, callback) => { - console.log('uh, export?'); setTimeout(() => { exportedSpans = exportedSpans + spans.length; callback(ExportResult.SUCCESS); @@ -227,5 +229,32 @@ describe('BatchSpanProcessor', () => { }); }); }); + + describe('flushing spans with exporter triggering instrumentation', () => { + beforeEach(() => { + const contextManager = new TestStackContextManager().enable(); + context.setGlobalContextManager(contextManager); + }); + + afterEach(() => { + context.disable(); + }); + + it('should prevent instrumentation prior to export', done => { + const testTracingExporter = new TestTracingSpanExporter(); + const processor = new BatchSpanProcessor(testTracingExporter); + + const span = createSampledSpan('test'); + processor.onStart(span); + processor.onEnd(span); + + processor.forceFlush(() => { + const exporterCreatedSpans = testTracingExporter.getExporterCreatedSpans(); + assert.equal(exporterCreatedSpans.length, 0); + + done(); + }); + }); + }); }); }); diff --git a/packages/opentelemetry-tracing/test/export/InMemorySpanExporter.test.ts b/packages/opentelemetry-tracing/test/export/InMemorySpanExporter.test.ts index 47af46580e2..b8c05e4a0fe 100644 --- a/packages/opentelemetry-tracing/test/export/InMemorySpanExporter.test.ts +++ b/packages/opentelemetry-tracing/test/export/InMemorySpanExporter.test.ts @@ -24,13 +24,13 @@ import { context } from '@opentelemetry/api'; import { ExportResult, setActiveSpan } from '@opentelemetry/core'; describe('InMemorySpanExporter', () => { - const memoryExporter = new InMemorySpanExporter(); - const provider = new BasicTracerProvider(); - provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + let memoryExporter: InMemorySpanExporter; + let provider: BasicTracerProvider; - afterEach(() => { - // reset spans in memory. - memoryExporter.reset(); + beforeEach(() => { + memoryExporter = new InMemorySpanExporter(); + provider = new BasicTracerProvider(); + provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); }); it('should get finished spans', () => { diff --git a/packages/opentelemetry-tracing/test/export/SimpleSpanProcessor.test.ts b/packages/opentelemetry-tracing/test/export/SimpleSpanProcessor.test.ts index 8b67013153e..584f4b32ec4 100644 --- a/packages/opentelemetry-tracing/test/export/SimpleSpanProcessor.test.ts +++ b/packages/opentelemetry-tracing/test/export/SimpleSpanProcessor.test.ts @@ -21,7 +21,9 @@ import { InMemorySpanExporter, SimpleSpanProcessor, } from '../../src'; -import { SpanContext, SpanKind, TraceFlags } from '@opentelemetry/api'; +import { SpanContext, SpanKind, TraceFlags, context } from '@opentelemetry/api'; +import { TestTracingSpanExporter } from './TestTracingSpanExporter'; +import { TestStackContextManager } from './TestStackContextManager'; describe('SimpleSpanProcessor', () => { const provider = new BasicTracerProvider(); @@ -80,16 +82,20 @@ describe('SimpleSpanProcessor', () => { processor.shutdown(); assert.strictEqual(exporter.getFinishedSpans().length, 0); }); + }); - describe('force flush', () => { - it('should call an async callback when flushing is complete', done => { + describe('force flush', () => { + describe('when flushing complete', () => { + it('should call an async callback', done => { const processor = new SimpleSpanProcessor(exporter); processor.forceFlush(() => { done(); }); }); + }); - it('should call an async callback when shutdown is complete', done => { + describe('when shutdown is complete', () => { + it('should call an async callback', done => { const processor = new SimpleSpanProcessor(exporter); processor.shutdown(() => { done(); @@ -97,4 +103,37 @@ describe('SimpleSpanProcessor', () => { }); }); }); + + describe('onEnd', () => { + beforeEach(() => { + const contextManager = new TestStackContextManager().enable(); + context.setGlobalContextManager(contextManager); + }); + + afterEach(() => { + context.disable(); + }); + + it('should prevent instrumentation prior to export', () => { + const testTracingExporter = new TestTracingSpanExporter(); + const processor = new SimpleSpanProcessor(testTracingExporter); + + const spanContext: SpanContext = { + traceId: 'a3cda95b652f4a1592b449d5929fda1b', + spanId: '5e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + const span = new Span( + provider.getTracer('default'), + 'span-name', + spanContext, + SpanKind.CLIENT + ); + + processor.onEnd(span); + + const exporterCreatedSpans = testTracingExporter.getExporterCreatedSpans(); + assert.equal(exporterCreatedSpans.length, 0); + }); + }); }); diff --git a/packages/opentelemetry-tracing/test/export/TestStackContextManager.ts b/packages/opentelemetry-tracing/test/export/TestStackContextManager.ts new file mode 100644 index 00000000000..3062ea10690 --- /dev/null +++ b/packages/opentelemetry-tracing/test/export/TestStackContextManager.ts @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ContextManager, Context } from '@opentelemetry/context-base'; + +/** + * A test-only ContextManager that uses an in-memory stack to keep track of + * the active context. + * + * This is not intended for advanced or asynchronous use cases. + */ +export class TestStackContextManager implements ContextManager { + private _contextStack: Context[] = []; + + active(): Context { + return ( + this._contextStack[this._contextStack.length - 1] ?? Context.ROOT_CONTEXT + ); + } + + with ReturnType>( + context: Context, + fn: T + ): ReturnType { + this._contextStack.push(context); + try { + return fn(); + } finally { + this._contextStack.pop(); + } + } + + bind(target: T, context?: Context): T { + throw new Error('Method not implemented.'); + } + + enable(): this { + return this; + } + + disable(): this { + return this; + } +} diff --git a/packages/opentelemetry-tracing/test/export/TestTracingSpanExporter.ts b/packages/opentelemetry-tracing/test/export/TestTracingSpanExporter.ts new file mode 100644 index 00000000000..0aba00b0542 --- /dev/null +++ b/packages/opentelemetry-tracing/test/export/TestTracingSpanExporter.ts @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + BasicTracerProvider, + InMemorySpanExporter, + ReadableSpan, + Tracer, + SpanProcessor, +} from '../../src'; +import { ExportResult, NoopLogger, AlwaysOnSampler } from '@opentelemetry/core'; + +/** + * A test-only span exporter that naively simulates triggering instrumentation + * (creating new spans) during export. + */ +export class TestTracingSpanExporter extends InMemorySpanExporter { + private _exporterCreatedSpans: ReadableSpan[] = []; + private _tracer: Tracer; + + constructor() { + super(); + + const tracerProvider = new BasicTracerProvider({ + logger: new NoopLogger(), + }); + + const spanProcessor: SpanProcessor = { + forceFlush: () => {}, + onStart: () => {}, + shutdown: () => {}, + onEnd: span => { + this._exporterCreatedSpans.push(span); + }, + }; + + tracerProvider.addSpanProcessor(spanProcessor); + + this._tracer = new Tracer( + { name: 'default', version: '0.0.1' }, + { sampler: new AlwaysOnSampler() }, + tracerProvider + ); + } + + export( + spans: ReadableSpan[], + resultCallback: (result: ExportResult) => void + ): void { + if (!this._stopped) { + // Simulates an instrumented exporter by creating a span on the tracer. + const createdSpan = this._tracer.startSpan('exporter-created-span'); + createdSpan.end(); + } + + super.export(spans, resultCallback); + } + + shutdown(): void { + super.shutdown(); + this._exporterCreatedSpans = []; + } + + reset() { + super.reset(); + this._exporterCreatedSpans = []; + } + + getExporterCreatedSpans(): ReadableSpan[] { + return this._exporterCreatedSpans; + } +} diff --git a/packages/opentelemetry-web/package.json b/packages/opentelemetry-web/package.json index 20faef57af7..a83c421cd53 100644 --- a/packages/opentelemetry-web/package.json +++ b/packages/opentelemetry-web/package.json @@ -47,7 +47,7 @@ "@opentelemetry/context-zone": "^0.10.2", "@opentelemetry/resources": "^0.10.2", "@types/jquery": "3.5.1", - "@types/mocha": "8.0.1", + "@types/mocha": "8.0.2", "@types/node": "14.0.27", "@types/sinon": "9.0.4", "@types/webpack-env": "1.15.2", @@ -65,7 +65,7 @@ "mocha": "7.2.0", "nyc": "15.1.0", "rimraf": "3.0.2", - "sinon": "9.0.2", + "sinon": "9.0.3", "ts-loader": "8.0.2", "ts-mocha": "7.0.0", "ts-node": "8.10.2", diff --git a/packages/opentelemetry-web/src/utils.ts b/packages/opentelemetry-web/src/utils.ts index a1d539987a4..385cb1d05fa 100644 --- a/packages/opentelemetry-web/src/utils.ts +++ b/packages/opentelemetry-web/src/utils.ts @@ -28,6 +28,9 @@ import { } from '@opentelemetry/core'; import { HttpAttribute } from '@opentelemetry/semantic-conventions'; +// Used to normalize relative URLs +const urlNormalizingA = document.createElement('a'); + /** * Helper function to be able to use enum as typed key in type and in interface when using forEach * @param obj @@ -127,6 +130,10 @@ export function getResource( >(), initiatorType?: string ): PerformanceResourceTimingInfo { + // de-relativize the URL before usage (does no harm to absolute URLs) + urlNormalizingA.href = spanUrl; + spanUrl = urlNormalizingA.href; + const filteredResources = filterResourcesForSpan( spanUrl, startTimeHR, diff --git a/packages/opentelemetry-web/test/WebTracerProvider.test.ts b/packages/opentelemetry-web/test/WebTracerProvider.test.ts index 01b1590d964..e10c80405ce 100644 --- a/packages/opentelemetry-web/test/WebTracerProvider.test.ts +++ b/packages/opentelemetry-web/test/WebTracerProvider.test.ts @@ -160,7 +160,7 @@ describe('WebTracerProvider', () => { assert.ok(span); assert.ok(span.resource instanceof Resource); assert.equal( - span.resource.labels[TELEMETRY_SDK_RESOURCE.LANGUAGE], + span.resource.attributes[TELEMETRY_SDK_RESOURCE.LANGUAGE], 'webjs' ); }); diff --git a/packages/opentelemetry-web/test/registration.test.ts b/packages/opentelemetry-web/test/registration.test.ts index 2639072bf4c..123b9667d1f 100644 --- a/packages/opentelemetry-web/test/registration.test.ts +++ b/packages/opentelemetry-web/test/registration.test.ts @@ -16,9 +16,10 @@ import { context, - NoopHttpTextPropagator, + NoopTextMapPropagator, propagation, trace, + ProxyTracerProvider, } from '@opentelemetry/api'; import { NoopContextManager } from '@opentelemetry/context-base'; import { CompositePropagator } from '@opentelemetry/core'; @@ -40,14 +41,16 @@ describe('API registration', () => { assert.ok( propagation['_getGlobalPropagator']() instanceof CompositePropagator ); - assert.ok(trace.getTracerProvider() === tracerProvider); + const apiTracerProvider = trace.getTracerProvider(); + assert.ok(apiTracerProvider instanceof ProxyTracerProvider); + assert.ok(apiTracerProvider.getDelegate() === tracerProvider); }); it('should register configured implementations', () => { const tracerProvider = new WebTracerProvider(); const contextManager = new NoopContextManager(); - const propagator = new NoopHttpTextPropagator(); + const propagator = new NoopTextMapPropagator(); tracerProvider.register({ contextManager, @@ -57,7 +60,9 @@ describe('API registration', () => { assert.ok(context['_getContextManager']() === contextManager); assert.ok(propagation['_getGlobalPropagator']() === propagator); - assert.ok(trace.getTracerProvider() === tracerProvider); + const apiTracerProvider = trace.getTracerProvider(); + assert.ok(apiTracerProvider instanceof ProxyTracerProvider); + assert.ok(apiTracerProvider.getDelegate() === tracerProvider); }); it('should skip null context manager', () => { @@ -71,7 +76,9 @@ describe('API registration', () => { assert.ok( propagation['_getGlobalPropagator']() instanceof CompositePropagator ); - assert.ok(trace.getTracerProvider() === tracerProvider); + const apiTracerProvider = trace.getTracerProvider(); + assert.ok(apiTracerProvider instanceof ProxyTracerProvider); + assert.ok(apiTracerProvider.getDelegate() === tracerProvider); }); it('should skip null propagator', () => { @@ -81,10 +88,12 @@ describe('API registration', () => { }); assert.ok( - propagation['_getGlobalPropagator']() instanceof NoopHttpTextPropagator + propagation['_getGlobalPropagator']() instanceof NoopTextMapPropagator ); assert.ok(context['_getContextManager']() instanceof StackContextManager); - assert.ok(trace.getTracerProvider() === tracerProvider); + const apiTracerProvider = trace.getTracerProvider(); + assert.ok(apiTracerProvider instanceof ProxyTracerProvider); + assert.ok(apiTracerProvider.getDelegate() === tracerProvider); }); }); diff --git a/renovate.json b/renovate.json index d2b949af999..d95f24142e9 100644 --- a/renovate.json +++ b/renovate.json @@ -12,8 +12,7 @@ "ignoreDeps": [ "gcp-metadata", "got", - "mocha", - "prom-client" + "mocha" ], "assignees": [ "@dyladan",