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",