diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index f50009ef60..5cadb10953 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -84,7 +84,7 @@ body: "start": "node -r tracing.js app.js" }, "dependencies": { - "@opentelemetry/api": "^1.2.0", + "@opentelemetry/api": "^1.3.0", "@opentelemetry/sdk-trace-base": "~1.3.1", ... } diff --git a/.github/workflows/close-stale.yml b/.github/workflows/close-stale.yml index 122ab1c931..c26c21ca56 100644 --- a/.github/workflows/close-stale.yml +++ b/.github/workflows/close-stale.yml @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v5 + - uses: actions/stale@v6 with: days-before-stale: 60 days-before-close: 14 diff --git a/.github/workflows/peer-api.yml b/.github/workflows/peer-api.yml index 47daa6f20f..d3239068d5 100644 --- a/.github/workflows/peer-api.yml +++ b/.github/workflows/peer-api.yml @@ -10,7 +10,7 @@ jobs: peer-api-check: runs-on: ubuntu-latest container: - image: node:14 + image: node:18 steps: - name: Checkout uses: actions/checkout@v3 diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 21c99fbf5e..618e168e1e 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -47,16 +47,19 @@ jobs: - name: Build 🔧 run: | - npm run compile + npx lerna run compile - name: Unit tests - run: npm run test + run: | + # TODO(legendecas): webpack https://stackoverflow.com/questions/69692842/error-message-error0308010cdigital-envelope-routinesunsupported + if [ "${{ matrix.node_version }}" = "18" ]; then + export NODE_OPTIONS=--openssl-legacy-provider + fi + npm run test - name: Report Coverage run: npm run codecov if: ${{ matrix.node_version == '14' }} node-windows-tests: - strategy: - fail-fast: false runs-on: windows-latest env: NPM_CONFIG_UNSAFE_PERM: true @@ -90,22 +93,23 @@ jobs: - name: Build 🔧 run: | - npm run compile + npm config set script-shell "C:\\Program Files\\git\\bin\\bash.exe" + npx lerna run compile - name: Unit tests run: npm run test browser-tests: runs-on: ubuntu-latest - container: - image: circleci/node:16-browsers env: NPM_CONFIG_UNSAFE_PERM: true steps: - - name: Permission Setup - run: sudo chmod -R 777 /github /__w - name: Checkout uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 + - name: restore lerna id: cache uses: actions/cache@v3 @@ -126,8 +130,6 @@ jobs: - name: Build 🔧 run: | - npm run compile - # run additional compilation variants npx lerna run compile - name: Unit tests @@ -136,15 +138,14 @@ jobs: run: npm run codecov:browser webworker-tests: runs-on: ubuntu-latest - container: - image: circleci/node:16-browsers env: NPM_CONFIG_UNSAFE_PERM: true steps: - - name: Permission Setup - run: sudo chmod -R 777 /github /__w - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v3.0.2 + - uses: actions/setup-node@v3 + with: + node-version: 16 - name: restore lerna id: cache @@ -166,11 +167,37 @@ jobs: - name: Build 🔧 run: | - npm run compile - # run additional compilation variants npx lerna run compile - name: Unit tests run: npm run test:webworker - name: Report Coverage run: npm run codecov:webworker + api-eol-node-test: + strategy: + fail-fast: false + matrix: + node_version: + - "8" + - "10" + - "12" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node_version }} + + - name: Build + working-directory: ./api + run: | + npm install --ignore-scripts + npm install @types/mocha@^7 mocha@^7 ts-loader@^8 ts-mocha@^8 + node ../scripts/version-update.js + tsc --build tsconfig.json tsconfig.esm.json + + - name: Test + working-directory: ./api + run: npm test diff --git a/.github/workflows/w3c-integration-test.yml b/.github/workflows/w3c-integration-test.yml index c2be63b9df..971ea6ee7f 100644 --- a/.github/workflows/w3c-integration-test.yml +++ b/.github/workflows/w3c-integration-test.yml @@ -13,6 +13,10 @@ jobs: - name: Checkout 🛎️ uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '16' + - name: restore lock files uses: actions/cache@master # must use unreleased master to cache multiple paths id: cache diff --git a/CHANGELOG.md b/CHANGELOG.md index bc6d92008d..ab76cfe617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,25 +2,97 @@ All notable changes to this project will be documented in this file. +For API changes, see the [API CHANGELOG](api/CHANGELOG.md). +For experimental package changes, see the [experimental CHANGELOG](experimental/CHANGELOG.md). + ## Unreleased ### :boom: Breaking Change ### :rocket: (Enhancement) +* feat(api): add `getActiveBaggage` API [#3385](https://github.com/open-telemetry/opentelemetry-js/pull/3385) + ### :bug: (Bug Fix) -* fix(instrumentation-xhr): http.url attribute should be absolute [#3200](https://github.com/open-telemetry/opentelemetry-js/pull/3200) @t2t2 +* fix(sdk-metrics): use default Resource to comply with semantic conventions [#3411](https://github.com/open-telemetry/opentelemetry-js/pull/3411) @pichlermarc + * Metrics exported by the SDK now contain the following resource attributes by default: + * `service.name` + * `telemetry.sdk.name` + * `telemetry.sdk.language` + * `telemetry.sdk.version` ### :books: (Refine Doc) ### :house: (Internal) +## 1.8.0 + +* `@opentelemetry/sdk-metrics` has been promoted to stable +* `@opentelemetry/api-metrics` has been merged into `@opentelemetry/api` and deprecated + +### :boom: Breaking Change + +* feat(api): merge api-metrics into api [#3374](https://github.com/open-telemetry/opentelemetry-js/pull/3374) @legendecas + +### :rocket: (Enhancement) + +* feat(sdk-trace): re-export sdk-trace-base in sdk-trace-node and web [#3319](https://github.com/open-telemetry/opentelemetry-js/pull/3319) @legendecas +* feat: enable tree shaking [#3329](https://github.com/open-telemetry/opentelemetry-js/pull/3329) @pkanal + +### :bug: (Bug Fix) + +* fix(sdk-trace): enforce consistent span durations + [#3327](https://github.com/open-telemetry/opentelemetry-js/pull/3327) @dyladan +* fix(resources): fix EnvDetector throwing errors when attribute values contain spaces + [#3295](https://github.com/open-telemetry/opentelemetry-js/issues/3295) +* fix(trace): fix an issue which caused negative span durations in web based spans + [#3359](https://github.com/open-telemetry/opentelemetry-js/pull/3359) @dyladan +* fix(resources): strict OTEL_RESOURCE_ATTRIBUTES baggage octet decoding + [#3341](https://github.com/open-telemetry/opentelemetry-js/pull/3341) @legendecas + +### :books: (Refine Doc) + +* doc: Added Metrics documentation [#3360](https://github.com/open-telemetry/opentelemetry-js/pull/3360) @weyert +* docs(api): fix counter negative value wording [#3396](https://github.com/open-telemetry/opentelemetry-js/pull/3396) @legendecas + +### :house: (Internal) + +* ci: run browser tests without circle [#3328](https://github.com/open-telemetry/opentelemetry-js/pull/3328) @dyladan + +## Metrics API 1.0.0 + +Metrics API is now stable and generally available. +There are no changes between 1.0.0 and the previous 0.33.0 version. + +### :boom: Breaking Change + +* Add semver check to metrics API [#3357](https://github.com/open-telemetry/opentelemetry-js/pull/3357) @dyladan + * Previously API versions were only considered compatible if the API was exactly the same + +## 1.7.0 + +### :bug: (Bug Fix) + +* fix(sdk-trace-base): make span start times resistant to hrtime clock drift + [#3129](https://github.com/open-telemetry/opentelemetry-js/issues/3129) + +* fix(sdk-trace-base): validate maxExportBatchSize in BatchSpanProcessorBase + [#3232](https://github.com/open-telemetry/opentelemetry-js/issues/3232) + +### :books: (Refine Doc) + +* docs(metrics): add missing metrics packages to SDK reference documentation [#3239](https://github.com/open-telemetry/opentelemetry-js/pull/3239) @dyladan + +### :house: (Internal) + +* deps: update markdownlint-cli to 0.32.2 [#3253](https://github.com/open-telemetry/opentelemetry-js/pull/3253) @pichlermarc + ## 1.6.0 ### :rocket: (Enhancement) -* perf(opentelemetry-core): improve hexToBase64 performance [#3178](https://github.com/open-telemetry/opentelemetry-js/pull/3178) +* perf(opentelemetry-core): improve hexToBase64 performance [#3178](https://github.com/open-telemetry/opentelemetry-js/pull/3178) @seemk * feat(sdk-trace-base): move Sampler declaration into sdk-trace-base [#3088](https://github.com/open-telemetry/opentelemetry-js/pull/3088) @legendecas * fix(grpc-instrumentation): added grpc attributes in instrumentation [#3127](https://github.com/open-telemetry/opentelemetry-js/pull/3127) @andrewzenkov * feat: support latest `@opentelemetry/api` [#3177](https://github.com/open-telemetry/opentelemetry-js/pull/3177) @dyladan @@ -35,8 +107,6 @@ All notable changes to this project will be documented in this file. * chore: update trace-web example and rename it to opentelemetry-web [#3145](https://github.com/open-telemetry/opentelemetry-js/pull/3145) @pichlermarc * chore: update https example [#3152](https://github.com/open-telemetry/opentelemetry-js/pull/3152) @pichlermarc -### :house: (Internal) - ## 1.5.0 ### :rocket: (Enhancement) diff --git a/README.md b/README.md index f52b0a4ad2..c4561df9c0 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,7 @@ Getting Started   •   - API Reference -   •   - SDK Reference + API and SDK Reference

@@ -41,42 +39,15 @@ This is the JavaScript version of [OpenTelemetry](https://opentelemetry.io/), a framework for collecting traces and metrics from applications. -## Compatibility Matrix - -| API Version | Core version | Experimental Packages | -| ----------- | ------------ | --------------------- | -| 1.2.x | 1.6.x | 0.32.x | -| 1.1.x | 1.5.x | 0.31.x | -| 1.1.x | 1.4.x | 0.30.x | -| 1.1.x | 1.2.x, 1.3.x | 0.29.x | -| 1.1.x | 1.1.x | 0.28.x | -| 1.0.x | 1.0.x | 0.26.x, 0.27.x | -| 1.0.x | 0.26.x | ----- | -| 1.0.x | 0.25.x | ----- | -| 1.0.x | 0.24.x | ----- | -| 1.0.x | 0.23.x | ----- | -| 1.0.x | 0.22.x | ----- | -| 0.21.x | 0.21.x | ----- | -| 0.20.x | 0.20.x | ----- | -| v1.0.0-rc.3 | 0.19.x | ----- | -| 0.18.x | 0.18.x | ----- | -| | 0.17.x | ----- | -| | 0.16.x | ----- | -| | 0.15.x | ----- | -| | 0.14.x | ----- | -| | 0.13.x | ----- | -| | 0.12.x | ----- | -| | 0.11.x | ----- | - -## Versioning - -The current version for each package can be found in the respective `package.json` file for that module. For additional details see the [versioning and stability][spec-versioning] document in the specification. +## Quick Start -## Quick start +The following describes how to set up tracing for a basic web application. +For more detailed documentation, see the website at . -### Application Owner +### Installation -#### Install Dependencies +Dependencies with the `latest` tag on NPM should be compatible with each other. +See the [version compatibility matrix](#package-version-compatibility) below for more information. ```shell npm install --save @opentelemetry/api @@ -86,7 +57,7 @@ npm install --save @opentelemetry/auto-instrumentations-node **Note:** `auto-instrumentations-node` is a meta package from [opentelemetry-js-contrib](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/metapackages/auto-instrumentations-node) that provides a simple way to initialize multiple Node.js instrumentations. -#### Instantiate Tracing +### Set up Tracing ```js // tracing.js @@ -126,7 +97,7 @@ process.on('SIGTERM', () => { }); ``` -#### Run Your Application +### Run Your Application ```shell node -r ./tracing.js app.js @@ -134,7 +105,7 @@ node -r ./tracing.js app.js The above example will emit auto-instrumented telemetry about your Node.js application to the console. For a more in-depth example, see the [Getting Started Guide](https://opentelemetry.io/docs/js/getting-started/). For more information about automatic instrumentation see [@opentelemetry/sdk-trace-node][otel-node], which provides auto-instrumentation for Node.js applications. If the automatic instrumentation does not suit your needs, or you would like to create manual traces, see [@opentelemetry/sdk-trace-base][otel-tracing] -### Library Author +## Library Author If you are a library author looking to build OpenTelemetry into your library, please see [the documentation][docs]. As a library author, it is important that you only depend on properties and methods published on the public API. If you use any properties or methods from the SDK that are not officially a part of the public API, your library may break if an [Application Owner](#application-owner) uses a different SDK implementation. @@ -151,7 +122,7 @@ If you are a library author looking to build OpenTelemetry into your library, pl ### Node Support Only Node.js Active or Maintenance LTS versions are supported. -Previous versions of node _may_ work, but they are not tested by OpenTelemetry and they are not guaranteed to work. +Previous versions of node *may* work, but they are not tested by OpenTelemetry and they are not guaranteed to work. Please note that versions of Node.JS v8 prior to `v8.12.0` will NOT work, because OpenTelemetry Node depends on the `perf_hooks` module introduced in `v8.5.0` and `performance.timeOrigin` that is set correctly starting in `v8.12.0`. ### Browser Support @@ -159,13 +130,38 @@ Please note that versions of Node.JS v8 prior to `v8.12.0` will NOT work, becaus Automated browser tests are run in the latest version of Headless Chrome. There is currently no list of officially supported browsers, but OpenTelemetry is developed using standard web technologies with wide support and should work in currently supported versions of major browsers. +## Package Version Compatibility + +OpenTelemetry is released as a set of distinct packages in 3 categories: API, stable SDK, and experimental. +The API is located at [/api](/api/), the stable SDK packages are in the [/packages](/packages/) directory, and the experimental packages are listed in the [/experimental/packages](/experimental/packages/) directory. +There may also be API packages for experimental signals in the experimental directory. +All stable packages are released with the same version, and all experimental packages are released with the same version. +The below table describes which versions of each set of packages are expected to work together. + +| API | Stable Packages | Experimental Packages | +| ----- | --------------- | --------------------- | +| 1.3.x | 1.8.x | 0.34.x | +| 1.2.x | 1.7.x | 0.33.x | +| 1.2.x | 1.6.x | 0.32.x | +| 1.1.x | 1.5.x | 0.31.x | +| 1.1.x | 1.4.x | 0.30.x | +| 1.1.x | 1.3.x | 0.29.x | +| 1.1.x | 1.2.x | 0.29.x | +| 1.1.x | 1.1.x | 0.28.x | +| 1.0.x | 1.0.x | 0.27.x | +| 1.0.x | 1.0.x | 0.26.x | + +## Versioning + +The current version for each package can be found in the respective `package.json` file for that module. For additional details see the [versioning and stability][spec-versioning] document in the specification. + ## Feature Status -| Signal | API Status | SDK Status | -| ------- | ----------- | ----------------- | -| Tracing | Stable | Release Candidate | -| Metrics | Development | Development | -| Logs | Roadmap | Roadmap | +| Signal | API Status | SDK Status | +| ------- | ----------- | ----------- | +| Tracing | Stable | Stable | +| Metrics | Stable | Stable | +| Logs | Development | Development | For a more detailed breakdown of feature support see the [specification compliance matrix][compliance-matrix]. @@ -393,13 +389,13 @@ Collector exporter packages and types are renamed: [PR-1925](https://github.com/open-telemetry/opentelemetry-js/pull/1925) feat(diag-logger): part 2 - breaking changes - remove api.Logger, api.NoopLogger, core.LogLevel, core.ConsoleLogger -- These PR's remove the previous ```Logger``` and ```LogLevel``` implementations and change the way you should use the replacement ```DiagLogger``` and ```DiagLogLevel```, below are simple examples of how to change your existing usages. +- These PR's remove the previous `Logger` and `LogLevel` implementations and change the way you should use the replacement `DiagLogger` and `DiagLogLevel`, below are simple examples of how to change your existing usages. #### Setting the global diagnostic logger -The new global [```api.diag```](https://github.com/open-telemetry/opentelemetry-js-api/blob/main/src/api/diag.ts) provides the ability to set the global diagnostic logger ```setLogger()``` and logging level ```setLogLevel()```, it is also a ```DiagLogger``` implementation and should be directly to log diagnostic messages. +The new global [`api.diag`](https://github.com/open-telemetry/opentelemetry-js/blob/main/api/src/api/diag.ts) provides the ability to set the global diagnostic logger `setLogger()` and logging level `setLogLevel()`, it is also a `DiagLogger` implementation and should be directly to log diagnostic messages. -All included logger references have been removed in preference to using the global ```api.diag``` directly, so you no longer need to pass around the logger instance via function parameters or included as part of the configuration for a component. +All included logger references have been removed in preference to using the global `api.diag` directly, so you no longer need to pass around the logger instance via function parameters or included as part of the configuration for a component. ```javascript import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api"; @@ -553,9 +549,8 @@ Apache 2.0 - See [LICENSE][license-url] for more information. [otel-shim-opentracing]: https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-shim-opentracing [otel-tracing]: https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-base [otel-web]: https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-web -[otel-api]: https://github.com/open-telemetry/opentelemetry-js-api +[otel-api]: https://github.com/open-telemetry/opentelemetry-js/tree/main/api [otel-core]: https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-core [otel-propagator-b3]: https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-propagator-b3 -[generate-api-documentation]: https://github.com/open-telemetry/opentelemetry-js/blob/main/CONTRIBUTING.md#generating-api-documentation [spec-versioning]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md diff --git a/experimental/packages/opentelemetry-api-metrics/.eslintignore b/api/.eslintignore similarity index 100% rename from experimental/packages/opentelemetry-api-metrics/.eslintignore rename to api/.eslintignore diff --git a/api/.eslintrc.js b/api/.eslintrc.js new file mode 100644 index 0000000000..7d5c10c7f9 --- /dev/null +++ b/api/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + "env": { + "mocha": true, + "commonjs": true, + "shared-node-browser": true + }, + ...require('../eslint.config.js') +} diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md new file mode 100644 index 0000000000..3784f8abc8 --- /dev/null +++ b/api/CHANGELOG.md @@ -0,0 +1,217 @@ +# CHANGELOG + +All notable changes to this project will be documented in this file. + +## Unreleased + +* fix(api): deprecate MetricAttributes and MetricAttributeValue [#3406](https://github.com/open-telemetry/opentelemetry-js/pull/3406) @blumamir +* test(api): disable module concatenation in tree-shaking test [#3409](https://github.com/open-telemetry/opentelemetry-js/pull/3409) @legendecas + +## [1.3.0](https://www.github.com/open-telemetry/opentelemetry-js-api/compare/v1.2.0...v1.3.0) + +* feat(api): merge api-metrics into api [#3374](https://github.com/open-telemetry/opentelemetry-js/pull/3374) @legendecas +* Optionally suppress warning about logger being overwritten ([#3366](https://www.github.com/open-telemetry/opentelemetry-js-api/pull/3366)) + +## [1.2.0](https://www.github.com/open-telemetry/opentelemetry-js-api/compare/v1.1.0...v1.2.0) (2022-08-09) + +### Features + +* Add getActiveSpan to trace API ([#163](https://www.github.com/open-telemetry/opentelemetry-js-api/issues/163)) ([17ccb3a](https://www.github.com/open-telemetry/opentelemetry-js-api/commit/17ccb3a4e385bc5769ded6fc742c9782a93244a5)) +* deprecate Sampler ([#166](https://www.github.com/open-telemetry/opentelemetry-js-api/issues/166)) ([313b2e2](https://www.github.com/open-telemetry/opentelemetry-js-api/commit/313b2e2225f246a6a9518ec4da6942f7d61babce)) + +## [1.1.0](https://www.github.com/open-telemetry/opentelemetry-js-api/compare/v1.0.4...v1.1.0) (2022-01-25) + +### Features + +* add tracestate implementation to api ([#147](https://www.github.com/open-telemetry/opentelemetry-js-api/issues/147)) ([82842c7](https://www.github.com/open-telemetry/opentelemetry-js-api/commit/82842c7097614e6ece99e73838ac5e94ff5460b7)) +* define common attributes type ([#142](https://www.github.com/open-telemetry/opentelemetry-js-api/issues/142)) ([ae9bead](https://www.github.com/open-telemetry/opentelemetry-js-api/commit/ae9bead17750d35dec4b63cfae098087666abc85)) +* **trace:** add optional schema url to TracerProvider.getTracer ([#129](https://www.github.com/open-telemetry/opentelemetry-js-api/issues/129)) ([aa65fc6](https://www.github.com/open-telemetry/opentelemetry-js-api/commit/aa65fc66809d45090d6e4951c265386d17ccc6f6)) + +### Bug Fixes + +* export tracer options ([#154](https://www.github.com/open-telemetry/opentelemetry-js-api/issues/154)) ([b125324](https://www.github.com/open-telemetry/opentelemetry-js-api/commit/b125324438fb2f24eb80c7c6673afc8cfc99575e)) + +### [1.0.4](https://www.github.com/open-telemetry/opentelemetry-js-api/compare/v1.0.3...v1.0.4) (2021-12-18) + +### Bug Fixes + +* align globalThis fallbacks with otel-core ([#126](https://www.github.com/open-telemetry/opentelemetry-js-api/issues/126)) ([3507de7](https://www.github.com/open-telemetry/opentelemetry-js-api/commit/3507de7c3b95396696657c021953b0b24a63a029)) + +### [1.0.3](https://www.github.com/open-telemetry/opentelemetry-js-api/compare/v1.0.2...v1.0.3) (2021-08-30) + +### Bug Fixes + +* remove all circular dependencies ([#119](https://www.github.com/open-telemetry/opentelemetry-js-api/issues/119)) ([a8083e8](https://www.github.com/open-telemetry/opentelemetry-js-api/commit/a8083e84b23227828745da80fd5fe512357dd34b)) + +## 1.0.2 + +### :bug: Bug Fix + +* [#105](https://github.com/open-telemetry/opentelemetry-js-api/pull/105) fix: set delegate after successful registration ([@Flarna](https://github.com/Flarna)) +* [#94](https://github.com/open-telemetry/opentelemetry-js-api/pull/94) fix: enforce strict equality on prerelease versions ([@dyladan](https://github.com/dyladan)) + +### :memo: Documentation + +* [#106](https://github.com/open-telemetry/opentelemetry-js-api/pull/106) docs: fix crash in README example ([@trentm](https://github.com/trentm)) +* [#101](https://github.com/open-telemetry/opentelemetry-js-api/pull/101) docs: Format example for tracer.startActiveSpan ([@ad-m](https://github.com/ad-m)) +* [#99](https://github.com/open-telemetry/opentelemetry-js-api/pull/99) chore: fix link to API docs ([@dyladan](https://github.com/dyladan)) + +### :house: Internal + +* [#109](https://github.com/open-telemetry/opentelemetry-js-api/pull/109) internal: add missing approvers from core ([@dyladan](https://github.com/dyladan)) +* [#103](https://github.com/open-telemetry/opentelemetry-js-api/pull/103) chore: reuse NoopTracer in ProxyTracer ([@Flarna](https://github.com/Flarna)) + +### Committers: 4 + +* Adam Dobrawy ([@ad-m](https://github.com/ad-m)) +* Daniel Dyla ([@dyladan](https://github.com/dyladan)) +* Gerhard Stöbich ([@Flarna](https://github.com/Flarna)) +* Trent Mick ([@trentm](https://github.com/trentm)) + +## 1.0.1 + +### :bug: Bug Fix + +* [#96](https://github.com/open-telemetry/opentelemetry-js-api/pull/96) chore: remove circular dependency ([@dyladan](https://github.com/dyladan)) + +### Committers: 1 + +* Daniel Dyla ([@dyladan](https://github.com/dyladan)) + +## 1.0.0 + +### :memo: Documentation + +* [#89](https://github.com/open-telemetry/opentelemetry-js-api/pull/89) chore: update upgrade guidelines ([@dyladan](https://github.com/dyladan)) + +### :house: Internal + +* [#90](https://github.com/open-telemetry/opentelemetry-js-api/pull/90) chore: enable typescript 4.3 noImplicitOverride option ([@Flarna](https://github.com/Flarna)) + +### Committers: 2 + +* Daniel Dyla ([@dyladan](https://github.com/dyladan)) +* Gerhard Stöbich ([@Flarna](https://github.com/Flarna)) + +## 0.21.0 + +### :boom: Breaking Change + +* [#78](https://github.com/open-telemetry/opentelemetry-js-api/pull/78) feat: unify signatures of `with` and `bind` ([@Rauno56](https://github.com/Rauno56)) +* [#46](https://github.com/open-telemetry/opentelemetry-js-api/pull/46) chore: do not export singletons ([@dyladan](https://github.com/dyladan)) + +### :rocket: Enhancement + +* [#81](https://github.com/open-telemetry/opentelemetry-js-api/pull/81) chore: function overloads implementation of startActiveSpan in noop t… ([@naseemkullah](https://github.com/naseemkullah)) + +### :house: Internal + +* [#84](https://github.com/open-telemetry/opentelemetry-js-api/pull/84) chore: remove unused backwards compatibility folder ([@Flarna](https://github.com/Flarna)) +* [#85](https://github.com/open-telemetry/opentelemetry-js-api/pull/85) chore: add node:16 to the test matrix ([@Rauno56](https://github.com/Rauno56)) +* [#63](https://github.com/open-telemetry/opentelemetry-js-api/pull/63) feat: debug log global registrations and logger overwrites ([@Rauno56](https://github.com/Rauno56)) +* [#75](https://github.com/open-telemetry/opentelemetry-js-api/pull/75) Add CodeQL Security Scan ([@xukaren](https://github.com/xukaren)) +* [#79](https://github.com/open-telemetry/opentelemetry-js-api/pull/79) chore: fix eslint config ([@Rauno56](https://github.com/Rauno56)) + +### Committers: 5 + +* Daniel Dyla ([@dyladan](https://github.com/dyladan)) +* Gerhard Stöbich ([@Flarna](https://github.com/Flarna)) +* Karen Xu ([@xukaren](https://github.com/xukaren)) +* Naseem ([@naseemkullah](https://github.com/naseemkullah)) +* Rauno Viskus ([@Rauno56](https://github.com/Rauno56)) + +## 0.20.0 + +### :rocket: Enhancement + +* [#69](https://github.com/open-telemetry/opentelemetry-js-api/pull/69) feat(context): add utils method to remove keys from context ([@vmarchaud](https://github.com/vmarchaud)) +* [#71](https://github.com/open-telemetry/opentelemetry-js-api/pull/71) chore: export baggage ([@dyladan](https://github.com/dyladan)) + +### Committers: 2 + +* Daniel Dyla ([@dyladan](https://github.com/dyladan)) +* Valentin Marchaud ([@vmarchaud](https://github.com/vmarchaud)) + +## 0.19.0 + +### :boom: Breaking Change + +* [#55](https://github.com/open-telemetry/opentelemetry-js-api/pull/55) chore: move baggage methods in propagation namespace ([@vmarchaud](https://github.com/vmarchaud)) +* [#65](https://github.com/open-telemetry/opentelemetry-js-api/pull/65) chore: remove suppress instrumentation ([@dyladan](https://github.com/dyladan)) +* [#60](https://github.com/open-telemetry/opentelemetry-js-api/pull/60) chore: removing timed event ([@obecny](https://github.com/obecny)) +* [#58](https://github.com/open-telemetry/opentelemetry-js-api/pull/58) chore: use spancontext for link ([@dyladan](https://github.com/dyladan)) +* [#47](https://github.com/open-telemetry/opentelemetry-js-api/pull/47) chore: move span method for context in trace API #40 ([@vmarchaud](https://github.com/vmarchaud)) +* [#45](https://github.com/open-telemetry/opentelemetry-js-api/pull/45) chore: rename `span#context()` to `span#spanContext` ([@dyladan](https://github.com/dyladan)) +* [#43](https://github.com/open-telemetry/opentelemetry-js-api/pull/43) chore: renaming noop span to non recording span ([@obecny](https://github.com/obecny)) +* [#32](https://github.com/open-telemetry/opentelemetry-js-api/pull/32) feat!: return boolean success value from setGlobalXXX methods ([@dyladan](https://github.com/dyladan)) + +### :rocket: Enhancement + +* [#62](https://github.com/open-telemetry/opentelemetry-js-api/pull/62) chore: adding component logger ([@obecny](https://github.com/obecny)) +* [#54](https://github.com/open-telemetry/opentelemetry-js-api/pull/54) feat: add tracer.startActiveSpan() ([@naseemkullah](https://github.com/naseemkullah)) +* [#58](https://github.com/open-telemetry/opentelemetry-js-api/pull/58) chore: use spancontext for link ([@dyladan](https://github.com/dyladan)) +* [#51](https://github.com/open-telemetry/opentelemetry-js-api/pull/51) feat: add function to wrap SpanContext in NonRecordingSpan #49 ([@dyladan](https://github.com/dyladan)) + +### :memo: Documentation + +* [#64](https://github.com/open-telemetry/opentelemetry-js-api/pull/64) chore: document the reason for symbol.for ([@dyladan](https://github.com/dyladan)) +* [#44](https://github.com/open-telemetry/opentelemetry-js-api/pull/44) chore: updating readme headline and fixing links ([@obecny](https://github.com/obecny)) + +### Committers: 6 + +* Bartlomiej Obecny ([@obecny](https://github.com/obecny)) +* Daniel Dyla ([@dyladan](https://github.com/dyladan)) +* Gerhard Stöbich ([@Flarna](https://github.com/Flarna)) +* Naseem ([@naseemkullah](https://github.com/naseemkullah)) +* Valentin Marchaud ([@vmarchaud](https://github.com/vmarchaud)) +* t2t2 ([@t2t2](https://github.com/t2t2)) + +## 1.0.0-rc.0 + +### :memo: Documentation + +* [#20](https://github.com/open-telemetry/opentelemetry-js-api/pull/20) docs: document latest manual tracing ([@dyladan](https://github.com/dyladan)) +* [#18](https://github.com/open-telemetry/opentelemetry-js-api/pull/18) chore: deploy docs on a release ([@dyladan](https://github.com/dyladan)) +* [#19](https://github.com/open-telemetry/opentelemetry-js-api/pull/19) docs: fix readme links ([@dyladan](https://github.com/dyladan)) + +### Committers: 1 + +* Daniel Dyla ([@dyladan](https://github.com/dyladan)) + +## 0.18.1 + +### :bug: Bug Fix + +* [#16](https://github.com/open-telemetry/opentelemetry-js-api/pull/16) fix: Reverse the direction of the semver check ([@dyladan](https://github.com/dyladan)) + +### Committers: 1 + +* Daniel Dyla ([@dyladan](https://github.com/dyladan)) + +## v0.18.0 + +### :boom: Breaking Change + +* [#9](https://github.com/open-telemetry/opentelemetry-js-api/pull/9) chore: refactor diag logger ([@dyladan](https://github.com/dyladan)) + +### :rocket: Enhancement + +* [#10](https://github.com/open-telemetry/opentelemetry-js-api/pull/10) Use semver to determine API compatibility ([@dyladan](https://github.com/dyladan)) + +### :house: Internal + +* [#12](https://github.com/open-telemetry/opentelemetry-js-api/pull/12) chore: don't disable rule eqeqeq ([@Flarna](https://github.com/Flarna)) +* [#8](https://github.com/open-telemetry/opentelemetry-js-api/pull/8) chore: remove nycrc in favor of tsconfig reporting ([@dyladan](https://github.com/dyladan)) +* [#3](https://github.com/open-telemetry/opentelemetry-js-api/pull/3) chore: add test workflow ([@dyladan](https://github.com/dyladan)) +* [#4](https://github.com/open-telemetry/opentelemetry-js-api/pull/4) chore: remove package lock ([@dyladan](https://github.com/dyladan)) +* [#2](https://github.com/open-telemetry/opentelemetry-js-api/pull/2) chore: add lint workflow ([@dyladan](https://github.com/dyladan)) + +### Committers: 2 + +* Daniel Dyla ([@dyladan](https://github.com/dyladan)) +* Gerhard Stöbich ([@Flarna](https://github.com/Flarna)) + +## v0.17.0 + +Versions previous to `0.18.0` were developed in another repository. +To see previous changelog entries see the [CHANGELOG.md](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md). diff --git a/experimental/packages/opentelemetry-api-metrics/LICENSE b/api/LICENSE similarity index 100% rename from experimental/packages/opentelemetry-api-metrics/LICENSE rename to api/LICENSE diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000000..56dd23d177 --- /dev/null +++ b/api/README.md @@ -0,0 +1,117 @@ +# OpenTelemetry API for JavaScript + +

+ +API Reference +  •   +Documentation +
+ + NPM Release + +
+

+ +This package provides everything needed to interact with the OpenTelemetry API, including all TypeScript interfaces, enums, and no-op implementations. It is intended for use both on the server and in the browser. + +The methods in this package perform no operations by default. This means they can be safely called by a library or end-user application whether there is an SDK registered or not. In order to generate and export telemetry data, you will also need an SDK such as the [OpenTelemetry JS SDK][opentelemetry-js]. + +## Tracing Quick Start + +### You Will Need + +- An application you wish to instrument +- [OpenTelemetry JS SDK][opentelemetry-js] +- Node.js >=8.5.0 (14+ is preferred) or an ECMAScript 5+ compatible browser + +**Note:** ECMAScript 5+ compatibility is for this package only. Please refer to the documentation for the SDK you are using to determine its minimum ECMAScript version. + +**Note for library authors:** Only your end users will need an OpenTelemetry SDK. If you wish to support OpenTelemetry in your library, you only need to use the OpenTelemetry API. For more information, please read the [tracing documentation][docs-tracing]. + +### Install Dependencies + +```sh +npm install @opentelemetry/api @opentelemetry/sdk-trace-base +``` + +### Trace Your Application + +In order to get started with tracing, you will need to first register an SDK. The SDK you are using may provide a convenience method which calls the registration methods for you, but if you would like to call them directly they are documented here: [sdk registration methods][docs-sdk-registration]. + +Once you have registered an SDK, you can start and end spans. A simple example of basic SDK registration and tracing a simple operation is below. The example should export spans to the console once per second. For more information, see the [tracing documentation][docs-tracing]. + +```javascript +const { trace } = require("@opentelemetry/api"); +const { BasicTracerProvider, ConsoleSpanExporter, SimpleSpanProcessor } = require("@opentelemetry/sdk-trace-base"); + +// Create and register an SDK +const provider = new BasicTracerProvider(); +provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); +trace.setGlobalTracerProvider(provider); + +// Acquire a tracer from the global tracer provider which will be used to trace the application +const name = 'my-application-name'; +const version = '0.1.0'; +const tracer = trace.getTracer(name, version); + +// Trace your application by creating spans +async function operation() { + const span = tracer.startSpan("do operation"); + + // mock some work by sleeping 1 second + await new Promise((resolve, reject) => { + setTimeout(resolve, 1000); + }) + + span.end(); +} + +async function main() { + while (true) { + await operation(); + } +} + +main(); +``` + +## Version Compatibility + +Because the npm installer and node module resolution algorithm could potentially allow two or more copies of any given package to exist within the same `node_modules` structure, the OpenTelemetry API takes advantage of a variable on the `global` object to store the global API. When an API method in the API package is called, it checks if this `global` API exists and proxies calls to it if and only if it is a compatible API version. This means if a package has a dependency on an OpenTelemetry API version which is not compatible with the API used by the end user, the package will receive a no-op implementation of the API. + +## Upgrade Guidelines + +### 0.21.0 to 1.0.0 + +No breaking changes + +### 0.20.0 to 0.21.0 + +- [#78](https://github.com/open-telemetry/opentelemetry-js-api/issues/78) `api.context.bind` arguments reversed and `context` is now a required argument. +- [#46](https://github.com/open-telemetry/opentelemetry-js-api/issues/46) Noop classes and singletons are no longer exported. To create a noop span it is recommended to use `api.trace.wrapSpanContext` with `INVALID_SPAN_CONTEXT` instead of using the `NOOP_TRACER`. + +### 1.0.0-rc.3 to 0.20.0 + +- Removing `TimedEvent` which was not part of spec +- `HttpBaggage` renamed to `HttpBaggagePropagator` +- [#45](https://github.com/open-telemetry/opentelemetry-js-api/pull/45) `Span#context` renamed to `Span#spanContext` +- [#47](https://github.com/open-telemetry/opentelemetry-js-api/pull/47) `getSpan`/`setSpan`/`getSpanContext`/`setSpanContext` moved to `trace` namespace +- [#55](https://github.com/open-telemetry/opentelemetry-js-api/pull/55) `getBaggage`/`setBaggage`/`createBaggage` moved to `propagation` namespace + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us in [GitHub Discussions][discussions-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[opentelemetry-js]: https://github.com/open-telemetry/opentelemetry-js + +[discussions-url]: https://github.com/open-telemetry/opentelemetry-js/discussions +[license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/main/api/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[docs-tracing]: https://github.com/open-telemetry/opentelemetry-js/blob/main/doc/tracing.md +[docs-sdk-registration]: https://github.com/open-telemetry/opentelemetry-js/blob/main/doc/sdk-registration.md diff --git a/api/karma.conf.js b/api/karma.conf.js new file mode 100644 index 0000000000..5f7d9f888d --- /dev/null +++ b/api/karma.conf.js @@ -0,0 +1,24 @@ +/*! + * 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 + * + * 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. + */ + +const karmaWebpackConfig = require('../karma.webpack'); +const karmaBaseConfig = require('../karma.base'); + +module.exports = (config) => { + config.set(Object.assign({}, karmaBaseConfig, { + webpack: karmaWebpackConfig, + })) +}; diff --git a/api/karma.worker.js b/api/karma.worker.js new file mode 100644 index 0000000000..7917d5a000 --- /dev/null +++ b/api/karma.worker.js @@ -0,0 +1,24 @@ +/*! + * 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 + * + * 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. + */ + +const karmaWebpackConfig = require('../karma.webpack'); +const karmaBaseConfig = require('../karma.worker'); + +module.exports = (config) => { + config.set(Object.assign({}, karmaBaseConfig, { + webpack: karmaWebpackConfig, + })) +}; diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000000..50e3782e80 --- /dev/null +++ b/api/package.json @@ -0,0 +1,90 @@ +{ + "name": "@opentelemetry/api", + "version": "1.3.0", + "description": "Public API for OpenTelemetry", + "main": "build/src/index.js", + "module": "build/esm/index.js", + "types": "build/src/index.d.ts", + "browser": { + "./src/platform/index.ts": "./src/platform/browser/index.ts", + "./build/esm/platform/index.js": "./build/esm/platform/browser/index.js", + "./build/src/platform/index.js": "./build/src/platform/browser/index.js" + }, + "repository": "open-telemetry/opentelemetry-js", + "scripts": { + "clean": "tsc --build --clean", + "codecov:browser": "nyc report --reporter=json && codecov -f coverage/*.json -p ../", + "codecov:webworker": "nyc report --reporter=json && codecov -f coverage/*.json -p ../", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../", + "precompile": "lerna run version --scope $(npm pkg get name) --include-dependencies", + "compile": "tsc --build tsconfig.json tsconfig.esm.json", + "docs": "typedoc", + "docs:deploy": "gh-pages --dist docs/out", + "docs:test": "linkinator docs/out --silent && linkinator docs/*.md *.md --markdown --silent", + "lint:fix": "eslint . --ext .ts --fix", + "lint": "eslint . --ext .ts", + "test:browser": "nyc karma start --single-run", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", + "test:webworker": "nyc karma start karma.worker.js --single-run", + "cycle-check": "dpdm --exit-code circular:1 src/index.ts", + "version": "node ../scripts/version-update.js", + "prewatch": "npm run precompile", + "watch": "tsc --build --watch", + "peer-api-check": "node ../scripts/peer-api-check.js" + }, + "keywords": [ + "opentelemetry", + "nodejs", + "browser", + "tracing", + "profiling", + "stats", + "monitoring" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + }, + "files": [ + "build/esm/**/*.js", + "build/esm/**/*.js.map", + "build/esm/**/*.d.ts", + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@types/mocha": "10.0.0", + "@types/node": "18.6.5", + "@types/sinon": "10.0.13", + "@types/webpack": "4.41.26", + "@types/webpack-env": "1.16.3", + "codecov": "3.8.3", + "dpdm": "3.10.0", + "istanbul-instrumenter-loader": "3.0.1", + "karma": "6.3.16", + "karma-chrome-launcher": "3.1.0", + "karma-coverage-istanbul-reporter": "3.0.3", + "karma-mocha": "2.0.1", + "karma-mocha-webworker": "1.3.0", + "karma-spec-reporter": "0.0.32", + "karma-webpack": "4.0.2", + "memfs": "3.4.9", + "mocha": "10.0.0", + "nyc": "15.1.0", + "sinon": "14.0.0", + "ts-loader": "8.4.0", + "ts-mocha": "10.0.0", + "typescript": "4.4.4", + "unionfs": "4.4.0", + "webpack": "4.46.0" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/api", + "sideEffects": false +} diff --git a/api/src/api/context.ts b/api/src/api/context.ts new file mode 100644 index 0000000000..6631dea8f1 --- /dev/null +++ b/api/src/api/context.ts @@ -0,0 +1,99 @@ +/* + * 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 { NoopContextManager } from '../context/NoopContextManager'; +import { Context, ContextManager } from '../context/types'; +import { + getGlobal, + registerGlobal, + unregisterGlobal, +} from '../internal/global-utils'; +import { DiagAPI } from './diag'; + +const API_NAME = 'context'; +const NOOP_CONTEXT_MANAGER = new NoopContextManager(); + +/** + * Singleton object which represents the entry point to the OpenTelemetry Context API + */ +export class ContextAPI { + private static _instance?: ContextAPI; + + /** Empty private constructor prevents end users from constructing a new instance of the API */ + private constructor() {} + + /** Get the singleton instance of the Context API */ + public static getInstance(): ContextAPI { + if (!this._instance) { + this._instance = new ContextAPI(); + } + + return this._instance; + } + + /** + * Set the current context manager. + * + * @returns true if the context manager was successfully registered, else false + */ + public setGlobalContextManager(contextManager: ContextManager): boolean { + return registerGlobal(API_NAME, contextManager, DiagAPI.instance()); + } + + /** + * Get the currently active context + */ + public active(): Context { + return this._getContextManager().active(); + } + + /** + * Execute a function with an active context + * + * @param context context to be active during function execution + * @param fn function to execute in a context + * @param thisArg optional receiver to be used for calling fn + * @param args optional arguments forwarded to fn + */ + public with ReturnType>( + context: Context, + fn: F, + thisArg?: ThisParameterType, + ...args: A + ): ReturnType { + return this._getContextManager().with(context, fn, thisArg, ...args); + } + + /** + * Bind a context to a target function or event emitter + * + * @param context context to bind to the event emitter or function. Defaults to the currently active context + * @param target function or event emitter to bind + */ + public bind(context: Context, target: T): T { + return this._getContextManager().bind(context, target); + } + + private _getContextManager(): ContextManager { + return getGlobal(API_NAME) || NOOP_CONTEXT_MANAGER; + } + + /** Disable and remove the global context manager */ + public disable() { + this._getContextManager().disable(); + unregisterGlobal(API_NAME, DiagAPI.instance()); + } +} diff --git a/api/src/api/diag.ts b/api/src/api/diag.ts new file mode 100644 index 0000000000..516657d8c8 --- /dev/null +++ b/api/src/api/diag.ts @@ -0,0 +1,140 @@ +/* + * 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 { DiagComponentLogger } from '../diag/ComponentLogger'; +import { createLogLevelDiagLogger } from '../diag/internal/logLevelLogger'; +import { + ComponentLoggerOptions, + DiagLogFunction, + DiagLogger, + DiagLoggerApi, + DiagLogLevel, +} from '../diag/types'; +import { + getGlobal, + registerGlobal, + unregisterGlobal, +} from '../internal/global-utils'; + +const API_NAME = 'diag'; + +/** + * Singleton object which represents the entry point to the OpenTelemetry internal + * diagnostic API + */ +export class DiagAPI implements DiagLogger, DiagLoggerApi { + private static _instance?: DiagAPI; + + /** Get the singleton instance of the DiagAPI API */ + public static instance(): DiagAPI { + if (!this._instance) { + this._instance = new DiagAPI(); + } + + return this._instance; + } + + /** + * Private internal constructor + * @private + */ + private constructor() { + function _logProxy(funcName: keyof DiagLogger): DiagLogFunction { + return function (...args) { + const logger = getGlobal('diag'); + // shortcut if logger not set + if (!logger) return; + return logger[funcName](...args); + }; + } + + // Using self local variable for minification purposes as 'this' cannot be minified + const self = this; + + // DiagAPI specific functions + + const setLogger: DiagLoggerApi['setLogger'] = ( + logger, + optionsOrLogLevel = { logLevel: DiagLogLevel.INFO }, + ) => { + if (logger === self) { + // There isn't much we can do here. + // Logging to the console might break the user application. + // Try to log to self. If a logger was previously registered it will receive the log. + const err = new Error( + 'Cannot use diag as the logger for itself. Please use a DiagLogger implementation like ConsoleDiagLogger or a custom implementation' + ); + self.error(err.stack ?? err.message); + return false; + } + + if (typeof optionsOrLogLevel === 'number') { + optionsOrLogLevel = { + logLevel: optionsOrLogLevel, + }; + } + + const oldLogger = getGlobal('diag'); + const newLogger = createLogLevelDiagLogger(optionsOrLogLevel.logLevel ?? DiagLogLevel.INFO, logger); + // There already is an logger registered. We'll let it know before overwriting it. + if (oldLogger && !optionsOrLogLevel.suppressOverrideMessage) { + const stack = new Error().stack ?? ''; + oldLogger.warn(`Current logger will be overwritten from ${stack}`); + newLogger.warn( + `Current logger will overwrite one already registered from ${stack}` + ); + } + + return registerGlobal('diag', newLogger, self, true); + }; + + self.setLogger = setLogger; + + self.disable = () => { + unregisterGlobal(API_NAME, self); + }; + + self.createComponentLogger = (options: ComponentLoggerOptions) => { + return new DiagComponentLogger(options); + }; + + self.verbose = _logProxy('verbose'); + self.debug = _logProxy('debug'); + self.info = _logProxy('info'); + self.warn = _logProxy('warn'); + self.error = _logProxy('error'); + } + + public setLogger!: DiagLoggerApi['setLogger']; + /** + * + */ + public createComponentLogger!: ( + options: ComponentLoggerOptions + ) => DiagLogger; + + // DiagLogger implementation + public verbose!: DiagLogFunction; + public debug!: DiagLogFunction; + public info!: DiagLogFunction; + public warn!: DiagLogFunction; + public error!: DiagLogFunction; + + /** + * Unregister the global logger and return to Noop + */ + public disable!: () => void; +} diff --git a/experimental/packages/opentelemetry-api-metrics/src/api/metrics.ts b/api/src/api/metrics.ts similarity index 62% rename from experimental/packages/opentelemetry-api-metrics/src/api/metrics.ts rename to api/src/api/metrics.ts index 3e5fb6015a..5353d84b91 100644 --- a/experimental/packages/opentelemetry-api-metrics/src/api/metrics.ts +++ b/api/src/api/metrics.ts @@ -14,15 +14,13 @@ * limitations under the License. */ -import { Meter, MeterOptions } from '../types/Meter'; -import { MeterProvider } from '../types/MeterProvider'; -import { NOOP_METER_PROVIDER } from '../NoopMeterProvider'; -import { - API_BACKWARDS_COMPATIBILITY_VERSION, - GLOBAL_METRICS_API_KEY, - makeGetter, - _global, -} from './global-utils'; +import { Meter, MeterOptions } from '../metrics/Meter'; +import { MeterProvider } from '../metrics/MeterProvider'; +import { NOOP_METER_PROVIDER } from '../metrics/NoopMeterProvider'; +import { getGlobal, registerGlobal, unregisterGlobal } from '../internal/global-utils'; +import { DiagAPI } from './diag'; + +const API_NAME = 'metrics'; /** * Singleton object which represents the entry point to the OpenTelemetry Metrics API @@ -43,31 +41,18 @@ export class MetricsAPI { } /** - * Set the current global meter. Returns the initialized global meter provider. + * Set the current global meter provider. + * Returns true if the meter provider was successfully registered, else false. */ - public setGlobalMeterProvider(provider: MeterProvider): MeterProvider { - if (_global[GLOBAL_METRICS_API_KEY]) { - // global meter provider has already been set - return this.getMeterProvider(); - } - - _global[GLOBAL_METRICS_API_KEY] = makeGetter( - API_BACKWARDS_COMPATIBILITY_VERSION, - provider, - NOOP_METER_PROVIDER - ); - - return provider; + public setGlobalMeterProvider(provider: MeterProvider): boolean { + return registerGlobal(API_NAME, provider, DiagAPI.instance()); } /** * Returns the global meter provider. */ public getMeterProvider(): MeterProvider { - return ( - _global[GLOBAL_METRICS_API_KEY]?.(API_BACKWARDS_COMPATIBILITY_VERSION) ?? - NOOP_METER_PROVIDER - ); + return getGlobal(API_NAME) || NOOP_METER_PROVIDER; } /** @@ -79,6 +64,6 @@ export class MetricsAPI { /** Remove the global meter provider */ public disable(): void { - delete _global[GLOBAL_METRICS_API_KEY]; + unregisterGlobal(API_NAME, DiagAPI.instance()); } } diff --git a/api/src/api/propagation.ts b/api/src/api/propagation.ts new file mode 100644 index 0000000000..24c9c1f73f --- /dev/null +++ b/api/src/api/propagation.ts @@ -0,0 +1,125 @@ +/* + * 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 { Context } from '../context/types'; +import { + getGlobal, + registerGlobal, + unregisterGlobal, +} from '../internal/global-utils'; +import { NoopTextMapPropagator } from '../propagation/NoopTextMapPropagator'; +import { + defaultTextMapGetter, + defaultTextMapSetter, + TextMapGetter, + TextMapPropagator, + TextMapSetter, +} from '../propagation/TextMapPropagator'; +import { + getBaggage, + getActiveBaggage, + setBaggage, + deleteBaggage, +} from '../baggage/context-helpers'; +import { createBaggage } from '../baggage/utils'; +import { DiagAPI } from './diag'; + +const API_NAME = 'propagation'; +const NOOP_TEXT_MAP_PROPAGATOR = new NoopTextMapPropagator(); + +/** + * Singleton object which represents the entry point to the OpenTelemetry Propagation API + */ +export class PropagationAPI { + private static _instance?: PropagationAPI; + + /** Empty private constructor prevents end users from constructing a new instance of the API */ + private constructor() {} + + /** Get the singleton instance of the Propagator API */ + public static getInstance(): PropagationAPI { + if (!this._instance) { + this._instance = new PropagationAPI(); + } + + return this._instance; + } + + /** + * Set the current propagator. + * + * @returns true if the propagator was successfully registered, else false + */ + public setGlobalPropagator(propagator: TextMapPropagator): boolean { + return registerGlobal(API_NAME, propagator, DiagAPI.instance()); + } + + /** + * Inject context into a carrier to be propagated inter-process + * + * @param context Context carrying tracing data to inject + * @param carrier carrier to inject context into + * @param setter Function used to set values on the carrier + */ + public inject( + context: Context, + carrier: Carrier, + setter: TextMapSetter = defaultTextMapSetter + ): void { + return this._getGlobalPropagator().inject(context, carrier, setter); + } + + /** + * Extract context from a carrier + * + * @param context Context which the newly created context will inherit from + * @param carrier Carrier to extract context from + * @param getter Function used to extract keys from a carrier + */ + public extract( + context: Context, + carrier: Carrier, + getter: TextMapGetter = defaultTextMapGetter + ): Context { + return this._getGlobalPropagator().extract(context, carrier, getter); + } + + /** + * Return a list of all fields which may be used by the propagator. + */ + public fields(): string[] { + return this._getGlobalPropagator().fields(); + } + + /** Remove the global propagator */ + public disable() { + unregisterGlobal(API_NAME, DiagAPI.instance()); + } + + public createBaggage = createBaggage; + + public getBaggage = getBaggage; + + public getActiveBaggage = getActiveBaggage; + + public setBaggage = setBaggage; + + public deleteBaggage = deleteBaggage; + + private _getGlobalPropagator(): TextMapPropagator { + return getGlobal(API_NAME) || NOOP_TEXT_MAP_PROPAGATOR; + } +} diff --git a/api/src/api/trace.ts b/api/src/api/trace.ts new file mode 100644 index 0000000000..492c16b5ba --- /dev/null +++ b/api/src/api/trace.ts @@ -0,0 +1,113 @@ +/* + * 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 { + getGlobal, + registerGlobal, + unregisterGlobal, +} from '../internal/global-utils'; +import { ProxyTracerProvider } from '../trace/ProxyTracerProvider'; +import { + isSpanContextValid, + wrapSpanContext, +} from '../trace/spancontext-utils'; +import { Tracer } from '../trace/tracer'; +import { TracerProvider } from '../trace/tracer_provider'; +import { + deleteSpan, + getActiveSpan, + getSpan, + getSpanContext, + setSpan, + setSpanContext, +} from '../trace/context-utils'; +import { DiagAPI } from './diag'; + +const API_NAME = 'trace'; + +/** + * Singleton object which represents the entry point to the OpenTelemetry Tracing API + */ +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() {} + + /** Get the singleton instance of the Trace API */ + public static getInstance(): TraceAPI { + if (!this._instance) { + this._instance = new TraceAPI(); + } + + return this._instance; + } + + /** + * Set the current global tracer. + * + * @returns true if the tracer provider was successfully registered, else false + */ + public setGlobalTracerProvider(provider: TracerProvider): boolean { + const success = registerGlobal( + API_NAME, + this._proxyTracerProvider, + DiagAPI.instance() + ); + if (success) { + this._proxyTracerProvider.setDelegate(provider); + } + return success; + } + + /** + * Returns the global tracer provider. + */ + public getTracerProvider(): TracerProvider { + return getGlobal(API_NAME) || this._proxyTracerProvider; + } + + /** + * Returns a tracer from the global tracer provider. + */ + public getTracer(name: string, version?: string): Tracer { + return this.getTracerProvider().getTracer(name, version); + } + + /** Remove the global tracer provider */ + public disable() { + unregisterGlobal(API_NAME, DiagAPI.instance()); + this._proxyTracerProvider = new ProxyTracerProvider(); + } + + public wrapSpanContext = wrapSpanContext; + + public isSpanContextValid = isSpanContextValid; + + public deleteSpan = deleteSpan; + + public getSpan = getSpan; + + public getActiveSpan = getActiveSpan; + + public getSpanContext = getSpanContext; + + public setSpan = setSpan; + + public setSpanContext = setSpanContext; +} diff --git a/api/src/baggage/context-helpers.ts b/api/src/baggage/context-helpers.ts new file mode 100644 index 0000000000..93a5ffca83 --- /dev/null +++ b/api/src/baggage/context-helpers.ts @@ -0,0 +1,63 @@ +/* + * 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 { ContextAPI } from '../api/context'; +import { createContextKey } from '../context/context'; +import { Context } from '../context/types'; +import { Baggage } from './types'; + +/** + * Baggage key + */ +const BAGGAGE_KEY = createContextKey('OpenTelemetry Baggage Key'); + +/** + * Retrieve the current baggage from the given context + * + * @param {Context} Context that manage all context values + * @returns {Baggage} Extracted baggage from the context + */ +export function getBaggage(context: Context): Baggage | undefined { + return (context.getValue(BAGGAGE_KEY) as Baggage) || undefined; +} + +/** + * Retrieve the current baggage from the active/current context + * + * @returns {Baggage} Extracted baggage from the context + */ +export function getActiveBaggage(): Baggage | undefined { + return getBaggage(ContextAPI.getInstance().active()); +} + +/** + * Store a baggage in the given context + * + * @param {Context} Context that manage all context values + * @param {Baggage} baggage that will be set in the actual context + */ +export function setBaggage(context: Context, baggage: Baggage): Context { + return context.setValue(BAGGAGE_KEY, baggage); +} + +/** + * Delete the baggage stored in the given context + * + * @param {Context} Context that manage all context values + */ +export function deleteBaggage(context: Context): Context { + return context.deleteValue(BAGGAGE_KEY); +} diff --git a/api/src/baggage/internal/baggage-impl.ts b/api/src/baggage/internal/baggage-impl.ts new file mode 100644 index 0000000000..8bd45dd4b9 --- /dev/null +++ b/api/src/baggage/internal/baggage-impl.ts @@ -0,0 +1,62 @@ +/* + * 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 type { Baggage, BaggageEntry } from '../types'; + +export class BaggageImpl implements Baggage { + private _entries: Map; + + constructor(entries?: Map) { + this._entries = entries ? new Map(entries) : new Map(); + } + + getEntry(key: string): BaggageEntry | undefined { + const entry = this._entries.get(key); + if (!entry) { + return undefined; + } + + return Object.assign({}, entry); + } + + getAllEntries(): [string, BaggageEntry][] { + return Array.from(this._entries.entries()).map(([k, v]) => [k, v]); + } + + setEntry(key: string, entry: BaggageEntry): BaggageImpl { + const newBaggage = new BaggageImpl(this._entries); + newBaggage._entries.set(key, entry); + return newBaggage; + } + + removeEntry(key: string): BaggageImpl { + const newBaggage = new BaggageImpl(this._entries); + newBaggage._entries.delete(key); + return newBaggage; + } + + removeEntries(...keys: string[]): BaggageImpl { + const newBaggage = new BaggageImpl(this._entries); + for (const key of keys) { + newBaggage._entries.delete(key); + } + return newBaggage; + } + + clear(): BaggageImpl { + return new BaggageImpl(); + } +} diff --git a/api/src/baggage/internal/symbol.ts b/api/src/baggage/internal/symbol.ts new file mode 100644 index 0000000000..f4213926c9 --- /dev/null +++ b/api/src/baggage/internal/symbol.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. + */ + +/** + * Symbol used to make BaggageEntryMetadata an opaque type + */ +export const baggageEntryMetadataSymbol = Symbol('BaggageEntryMetadata'); diff --git a/api/src/baggage/types.ts b/api/src/baggage/types.ts new file mode 100644 index 0000000000..ab6d82e651 --- /dev/null +++ b/api/src/baggage/types.ts @@ -0,0 +1,97 @@ +/* + * 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 { baggageEntryMetadataSymbol } from './internal/symbol'; + +/* + * 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 interface BaggageEntry { + /** `String` value of the `BaggageEntry`. */ + value: string; + /** + * Metadata is an optional string property defined by the W3C baggage specification. + * It currently has no special meaning defined by the specification. + */ + metadata?: BaggageEntryMetadata; +} + +/** + * Serializable Metadata defined by the W3C baggage specification. + * It currently has no special meaning defined by the OpenTelemetry or W3C. + */ +export type BaggageEntryMetadata = { toString(): string } & { + __TYPE__: typeof baggageEntryMetadataSymbol; +}; + +/** + * Baggage represents collection of key-value pairs with optional metadata. + * Each key of Baggage is associated with exactly one value. + * Baggage may be used to annotate and enrich telemetry data. + */ +export interface Baggage { + /** + * Get an entry from Baggage if it exists + * + * @param key The key which identifies the BaggageEntry + */ + getEntry(key: string): BaggageEntry | undefined; + + /** + * Get a list of all entries in the Baggage + */ + getAllEntries(): [string, BaggageEntry][]; + + /** + * Returns a new baggage with the entries from the current bag and the specified entry + * + * @param key string which identifies the baggage entry + * @param entry BaggageEntry for the given key + */ + setEntry(key: string, entry: BaggageEntry): Baggage; + + /** + * Returns a new baggage with the entries from the current bag except the removed entry + * + * @param key key identifying the entry to be removed + */ + removeEntry(key: string): Baggage; + + /** + * Returns a new baggage with the entries from the current bag except the removed entries + * + * @param key keys identifying the entries to be removed + */ + removeEntries(...key: string[]): Baggage; + + /** + * Returns a new baggage with no entries + */ + clear(): Baggage; +} diff --git a/api/src/baggage/utils.ts b/api/src/baggage/utils.ts new file mode 100644 index 0000000000..80b87890f6 --- /dev/null +++ b/api/src/baggage/utils.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 { DiagAPI } from '../api/diag'; +import { BaggageImpl } from './internal/baggage-impl'; +import { baggageEntryMetadataSymbol } from './internal/symbol'; +import { Baggage, BaggageEntry, BaggageEntryMetadata } from './types'; + +const diag = DiagAPI.instance(); + +/** + * Create a new Baggage with optional entries + * + * @param entries An array of baggage entries the new baggage should contain + */ +export function createBaggage( + entries: Record = {} +): Baggage { + return new BaggageImpl(new Map(Object.entries(entries))); +} + +/** + * Create a serializable BaggageEntryMetadata object from a string. + * + * @param str string metadata. Format is currently not defined by the spec and has no special meaning. + * + */ +export function baggageEntryMetadataFromString( + str: string +): BaggageEntryMetadata { + if (typeof str !== 'string') { + diag.error( + `Cannot create baggage metadata from unknown type: ${typeof str}` + ); + str = ''; + } + + return { + __TYPE__: baggageEntryMetadataSymbol, + toString() { + return str; + }, + }; +} diff --git a/api/src/common/Attributes.ts b/api/src/common/Attributes.ts new file mode 100644 index 0000000000..f3bdfe461c --- /dev/null +++ b/api/src/common/Attributes.ts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +/** + * Attributes is a map from string to attribute values. + * + * Note: only the own enumerable keys are counted as valid attribute keys. + */ +export interface Attributes { + [attributeKey: string]: AttributeValue | undefined; +} + +/** + * Attribute values may be any non-nullish primitive value except an object. + * + * null or undefined attribute values are invalid and will result in undefined behavior. + */ +export type AttributeValue = + | string + | number + | boolean + | Array + | Array + | Array; diff --git a/api/src/common/Exception.ts b/api/src/common/Exception.ts new file mode 100644 index 0000000000..5539b7e9f3 --- /dev/null +++ b/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 | number; + name?: string; + message?: string; + stack?: string; +} + +interface ExceptionWithMessage { + code?: string | number; + message: string; + name?: string; + stack?: string; +} + +interface ExceptionWithName { + code?: string | number; + 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/api/src/common/Time.ts b/api/src/common/Time.ts new file mode 100644 index 0000000000..4ed60a1e70 --- /dev/null +++ b/api/src/common/Time.ts @@ -0,0 +1,35 @@ +/* + * 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. + */ +/** + * Defines High-Resolution Time. + * + * The first number, HrTime[0], is UNIX Epoch time in seconds since 00:00:00 UTC on 1 January 1970. + * The second number, HrTime[1], represents the partial second elapsed since Unix Epoch time represented by first number in nanoseconds. + * For example, 2021-01-01T12:30:10.150Z in UNIX Epoch time in milliseconds is represented as 1609504210150. + * The first number is calculated by converting and truncating the Epoch time in milliseconds to seconds: + * HrTime[0] = Math.trunc(1609504210150 / 1000) = 1609504210. + * The second number is calculated by converting the digits after the decimal point of the subtraction, (1609504210150 / 1000) - HrTime[0], to nanoseconds: + * HrTime[1] = Number((1609504210.150 - HrTime[0]).toFixed(9)) * 1e9 = 150000000. + * This is represented in HrTime format as [1609504210, 150000000]. + */ +export type HrTime = [number, number]; + +/** + * Defines TimeInput. + * + * hrtime, epoch milliseconds, performance.now() or Date + */ +export type TimeInput = HrTime | number | Date; diff --git a/api/src/context-api.ts b/api/src/context-api.ts new file mode 100644 index 0000000000..db474def26 --- /dev/null +++ b/api/src/context-api.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +// Split module-level variable definition into separate files to allow +// tree-shaking on each api instance. +import { ContextAPI } from './api/context'; +/** Entrypoint for context API */ +export const context = ContextAPI.getInstance(); diff --git a/api/src/context/NoopContextManager.ts b/api/src/context/NoopContextManager.ts new file mode 100644 index 0000000000..a62a89d33c --- /dev/null +++ b/api/src/context/NoopContextManager.ts @@ -0,0 +1,45 @@ +/* + * 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 { ROOT_CONTEXT } from './context'; +import * as types from './types'; + +export class NoopContextManager implements types.ContextManager { + active(): types.Context { + return ROOT_CONTEXT; + } + + with ReturnType>( + _context: types.Context, + fn: F, + thisArg?: ThisParameterType, + ...args: A + ): ReturnType { + return fn.call(thisArg, ...args); + } + + bind(_context: types.Context, target: T): T { + return target; + } + + enable(): this { + return this; + } + + disable(): this { + return this; + } +} diff --git a/api/src/context/context.ts b/api/src/context/context.ts new file mode 100644 index 0000000000..825726f36f --- /dev/null +++ b/api/src/context/context.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 { Context } from './types'; + +/** Get a key to uniquely identify a context value */ +export function createContextKey(description: string) { + // The specification states that for the same input, multiple calls should + // return different keys. Due to the nature of the JS dependency management + // system, this creates problems where multiple versions of some package + // could hold different keys for the same property. + // + // Therefore, we use Symbol.for which returns the same key for the same input. + return Symbol.for(description); +} + +class BaseContext implements Context { + private _currentContext!: Map; + + /** + * Construct a new context which inherits values from an optional parent context. + * + * @param parentContext a context from which to inherit values + */ + constructor(parentContext?: Map) { + // for minification + const self = this; + + self._currentContext = parentContext ? new Map(parentContext) : new Map(); + + self.getValue = (key: symbol) => self._currentContext.get(key); + + self.setValue = (key: symbol, value: unknown): Context => { + const context = new BaseContext(self._currentContext); + context._currentContext.set(key, value); + return context; + }; + + self.deleteValue = (key: symbol): Context => { + const context = new BaseContext(self._currentContext); + context._currentContext.delete(key); + return context; + }; + } + + /** + * Get a value from the context. + * + * @param key key which identifies a context value + */ + public getValue!: (key: symbol) => unknown; + + /** + * Create a new context which inherits from this context and has + * the given key set to the given value. + * + * @param key context key for which to set the value + * @param value value to set for the given key + */ + public setValue!: (key: symbol, value: unknown) => Context; + + /** + * Return a new context which inherits from this context but does + * not contain a value for the given key. + * + * @param key context key for which to clear a value + */ + public deleteValue!: (key: symbol) => Context; +} + +/** The root context is used as the default parent context when there is no active context */ +export const ROOT_CONTEXT: Context = new BaseContext(); diff --git a/api/src/context/types.ts b/api/src/context/types.ts new file mode 100644 index 0000000000..19ae6d9d65 --- /dev/null +++ b/api/src/context/types.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. + */ + +export interface Context { + /** + * Get a value from the context. + * + * @param key key which identifies a context value + */ + getValue(key: symbol): unknown; + + /** + * Create a new context which inherits from this context and has + * the given key set to the given value. + * + * @param key context key for which to set the value + * @param value value to set for the given key + */ + setValue(key: symbol, value: unknown): Context; + + /** + * Return a new context which inherits from this context but does + * not contain a value for the given key. + * + * @param key context key for which to clear a value + */ + deleteValue(key: symbol): Context; +} + +export interface ContextManager { + /** + * Get the current active context + */ + active(): Context; + + /** + * Run the fn callback with object set as the current active context + * @param context Any object to set as the current active context + * @param fn A callback to be immediately run within a specific context + * @param thisArg optional receiver to be used for calling fn + * @param args optional arguments forwarded to fn + */ + with ReturnType>( + context: Context, + fn: F, + thisArg?: ThisParameterType, + ...args: A + ): ReturnType; + + /** + * Bind an object as the current context (or a specific one) + * @param [context] Optionally specify the context which you want to assign + * @param target Any object to which a context need to be set + */ + bind(context: Context, target: T): T; + + /** + * Enable context management + */ + enable(): this; + + /** + * Disable context management + */ + disable(): this; +} diff --git a/api/src/diag-api.ts b/api/src/diag-api.ts new file mode 100644 index 0000000000..ecad84caf4 --- /dev/null +++ b/api/src/diag-api.ts @@ -0,0 +1,26 @@ +/* + * 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. + */ + +// Split module-level variable definition into separate files to allow +// tree-shaking on each api instance. +import { DiagAPI } from './api/diag'; +/** + * Entrypoint for Diag API. + * Defines Diagnostic handler used for internal diagnostic logging operations. + * The default provides a Noop DiagLogger implementation which may be changed via the + * diag.setLogger(logger: DiagLogger) function. + */ +export const diag = DiagAPI.instance(); diff --git a/api/src/diag/ComponentLogger.ts b/api/src/diag/ComponentLogger.ts new file mode 100644 index 0000000000..2253501863 --- /dev/null +++ b/api/src/diag/ComponentLogger.ts @@ -0,0 +1,70 @@ +/* + * 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 { getGlobal } from '../internal/global-utils'; +import { ComponentLoggerOptions, DiagLogger, DiagLogFunction } from './types'; + +/** + * Component Logger which is meant to be used as part of any component which + * will add automatically additional namespace in front of the log message. + * It will then forward all message to global diag logger + * @example + * const cLogger = diag.createComponentLogger({ namespace: '@opentelemetry/instrumentation-http' }); + * cLogger.debug('test'); + * // @opentelemetry/instrumentation-http test + */ +export class DiagComponentLogger implements DiagLogger { + private _namespace: string; + + constructor(props: ComponentLoggerOptions) { + this._namespace = props.namespace || 'DiagComponentLogger'; + } + + public debug(...args: any[]): void { + return logProxy('debug', this._namespace, args); + } + + public error(...args: any[]): void { + return logProxy('error', this._namespace, args); + } + + public info(...args: any[]): void { + return logProxy('info', this._namespace, args); + } + + public warn(...args: any[]): void { + return logProxy('warn', this._namespace, args); + } + + public verbose(...args: any[]): void { + return logProxy('verbose', this._namespace, args); + } +} + +function logProxy( + funcName: keyof DiagLogger, + namespace: string, + args: any +): void { + const logger = getGlobal('diag'); + // shortcut if logger not set + if (!logger) { + return; + } + + args.unshift(namespace); + return logger[funcName](...(args as Parameters)); +} diff --git a/api/src/diag/consoleLogger.ts b/api/src/diag/consoleLogger.ts new file mode 100644 index 0000000000..76e9855732 --- /dev/null +++ b/api/src/diag/consoleLogger.ts @@ -0,0 +1,92 @@ +/* + * 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 { DiagLogger, DiagLogFunction } from './types'; + +type ConsoleMapKeys = 'error' | 'warn' | 'info' | 'debug' | 'trace'; +const consoleMap: { n: keyof DiagLogger; c: ConsoleMapKeys }[] = [ + { n: 'error', c: 'error' }, + { n: 'warn', c: 'warn' }, + { n: 'info', c: 'info' }, + { n: 'debug', c: 'debug' }, + { n: 'verbose', c: 'trace' }, +]; + +/** + * A simple Immutable Console based diagnostic logger which will output any messages to the Console. + * If you want to limit the amount of logging to a specific level or lower use the + * {@link createLogLevelDiagLogger} + */ +export class DiagConsoleLogger implements DiagLogger { + constructor() { + function _consoleFunc(funcName: ConsoleMapKeys): DiagLogFunction { + return function (...args) { + if (console) { + // Some environments only expose the console when the F12 developer console is open + // eslint-disable-next-line no-console + let theFunc = console[funcName]; + if (typeof theFunc !== 'function') { + // Not all environments support all functions + // eslint-disable-next-line no-console + theFunc = console.log; + } + + // One last final check + if (typeof theFunc === 'function') { + return theFunc.apply(console, args); + } + } + }; + } + + for (let i = 0; i < consoleMap.length; i++) { + this[consoleMap[i].n] = _consoleFunc(consoleMap[i].c); + } + } + + /** Log an error scenario that was not expected and caused the requested operation to fail. */ + public error!: DiagLogFunction; + + /** + * Log a warning scenario to inform the developer of an issues that should be investigated. + * The requested operation may or may not have succeeded or completed. + */ + public warn!: DiagLogFunction; + + /** + * Log a general informational message, this should not affect functionality. + * This is also the default logging level so this should NOT be used for logging + * debugging level information. + */ + public info!: DiagLogFunction; + + /** + * Log a general debug message that can be useful for identifying a failure. + * Information logged at this level may include diagnostic details that would + * help identify a failure scenario. Useful scenarios would be to log the execution + * order of async operations + */ + public debug!: DiagLogFunction; + + /** + * Log a detailed (verbose) trace level logging that can be used to identify failures + * where debug level logging would be insufficient, this level of tracing can include + * input and output parameters and as such may include PII information passing through + * the API. As such it is recommended that this level of tracing should not be enabled + * in a production environment. + */ + public verbose!: DiagLogFunction; +} diff --git a/api/src/diag/internal/logLevelLogger.ts b/api/src/diag/internal/logLevelLogger.ts new file mode 100644 index 0000000000..e9205b3642 --- /dev/null +++ b/api/src/diag/internal/logLevelLogger.ts @@ -0,0 +1,51 @@ +/* + * 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 { DiagLogFunction, DiagLogger, DiagLogLevel } from '../types'; + +export function createLogLevelDiagLogger( + maxLevel: DiagLogLevel, + logger: DiagLogger +): DiagLogger { + if (maxLevel < DiagLogLevel.NONE) { + maxLevel = DiagLogLevel.NONE; + } else if (maxLevel > DiagLogLevel.ALL) { + maxLevel = DiagLogLevel.ALL; + } + + // In case the logger is null or undefined + logger = logger || {}; + + function _filterFunc( + funcName: keyof DiagLogger, + theLevel: DiagLogLevel + ): DiagLogFunction { + const theFunc = logger[funcName]; + + if (typeof theFunc === 'function' && maxLevel >= theLevel) { + return theFunc.bind(logger); + } + return function () {}; + } + + return { + error: _filterFunc('error', DiagLogLevel.ERROR), + warn: _filterFunc('warn', DiagLogLevel.WARN), + info: _filterFunc('info', DiagLogLevel.INFO), + debug: _filterFunc('debug', DiagLogLevel.DEBUG), + verbose: _filterFunc('verbose', DiagLogLevel.VERBOSE), + }; +} diff --git a/api/src/diag/internal/noopLogger.ts b/api/src/diag/internal/noopLogger.ts new file mode 100644 index 0000000000..0c4e7d13b0 --- /dev/null +++ b/api/src/diag/internal/noopLogger.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 { DiagLogger } from '../types'; + +function noopLogFunction() {} + +/** + * Returns a No-Op Diagnostic logger where all messages do nothing. + * @implements {@link DiagLogger} + * @returns {DiagLogger} + */ +export function createNoopDiagLogger(): DiagLogger { + return { + verbose: noopLogFunction, + debug: noopLogFunction, + info: noopLogFunction, + warn: noopLogFunction, + error: noopLogFunction, + }; +} diff --git a/api/src/diag/types.ts b/api/src/diag/types.ts new file mode 100644 index 0000000000..e2ec879f03 --- /dev/null +++ b/api/src/diag/types.ts @@ -0,0 +1,132 @@ +/* + * 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 type DiagLogFunction = (message: string, ...args: unknown[]) => void; + +/** + * Defines an internal diagnostic logger interface which is used to log internal diagnostic + * messages, you can set the default diagnostic logger via the {@link DiagAPI} setLogger function. + * API provided implementations include :- + * - a No-Op {@link createNoopDiagLogger} + * - a {@link DiagLogLevel} filtering wrapper {@link createLogLevelDiagLogger} + * - a general Console {@link DiagConsoleLogger} version. + */ +export interface DiagLogger { + /** Log an error scenario that was not expected and caused the requested operation to fail. */ + error: DiagLogFunction; + + /** + * Log a warning scenario to inform the developer of an issues that should be investigated. + * The requested operation may or may not have succeeded or completed. + */ + warn: DiagLogFunction; + + /** + * Log a general informational message, this should not affect functionality. + * This is also the default logging level so this should NOT be used for logging + * debugging level information. + */ + info: DiagLogFunction; + + /** + * Log a general debug message that can be useful for identifying a failure. + * Information logged at this level may include diagnostic details that would + * help identify a failure scenario. + * For example: Logging the order of execution of async operations. + */ + debug: DiagLogFunction; + + /** + * Log a detailed (verbose) trace level logging that can be used to identify failures + * where debug level logging would be insufficient, this level of tracing can include + * input and output parameters and as such may include PII information passing through + * the API. As such it is recommended that this level of tracing should not be enabled + * in a production environment. + */ + verbose: DiagLogFunction; +} + +/** + * Defines the available internal logging levels for the diagnostic logger, the numeric values + * of the levels are defined to match the original values from the initial LogLevel to avoid + * compatibility/migration issues for any implementation that assume the numeric ordering. + */ +export enum DiagLogLevel { + /** Diagnostic Logging level setting to disable all logging (except and forced logs) */ + NONE = 0, + + /** Identifies an error scenario */ + ERROR = 30, + + /** Identifies a warning scenario */ + WARN = 50, + + /** General informational log message */ + INFO = 60, + + /** General debug log message */ + DEBUG = 70, + + /** + * Detailed trace level logging should only be used for development, should only be set + * in a development environment. + */ + VERBOSE = 80, + + /** Used to set the logging level to include all logging */ + ALL = 9999, +} + +/** + * Defines options for ComponentLogger + */ +export interface ComponentLoggerOptions { + namespace: string; +} + +export interface LoggerOptions { + /** + * The {@link DiagLogLevel} used to filter logs sent to the logger. + * + * @defaultValue DiagLogLevel.INFO + */ + logLevel?: DiagLogLevel; + + /** + * Setting this value to `true` will suppress the warning message normally emitted when registering a logger when another logger is already registered. + */ + suppressOverrideMessage?: boolean; +} + +export interface DiagLoggerApi { + /** + * Set the global DiagLogger and DiagLogLevel. + * If a global diag logger is already set, this will override it. + * + * @param logger - The {@link DiagLogger} instance to set as the default logger. + * @param options - A {@link LoggerOptions} object. If not provided, default values will be set. + * @returns `true` if the logger was successfully registered, else `false` + */ + setLogger(logger: DiagLogger, options?: LoggerOptions): boolean; + + /** + * + * @param logger - The {@link DiagLogger} instance to set as the default logger. + * @param logLevel - The {@link DiagLogLevel} used to filter logs sent to the logger. If not provided it will default to {@link DiagLogLevel.INFO}. + * @returns `true` if the logger was successfully registered, else `false` + */ + setLogger(logger: DiagLogger, logLevel?: DiagLogLevel): boolean; +} diff --git a/api/src/index.ts b/api/src/index.ts new file mode 100644 index 0000000000..63325fa97e --- /dev/null +++ b/api/src/index.ts @@ -0,0 +1,145 @@ +/* + * 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 { + BaggageEntry, + BaggageEntryMetadata, + Baggage, +} from './baggage/types'; +export { baggageEntryMetadataFromString } from './baggage/utils'; +export { Exception } from './common/Exception'; +export { HrTime, TimeInput } from './common/Time'; +export { Attributes, AttributeValue } from './common/Attributes'; + +// Context APIs +export { + createContextKey, + ROOT_CONTEXT, +} from './context/context'; +export { + Context, + ContextManager, +} from './context/types'; +export type { ContextAPI } from './api/context'; + +// Diag APIs +export { DiagConsoleLogger } from './diag/consoleLogger'; +export { + DiagLogFunction, + DiagLogger, + DiagLogLevel, + ComponentLoggerOptions, +} from './diag/types'; +export type { DiagAPI } from './api/diag'; + +// Metrics APIs +export { + createNoopMeter, +} from './metrics/NoopMeter'; +export { + MeterOptions, + Meter, +} from './metrics/Meter'; +export { + MeterProvider, +} from './metrics/MeterProvider'; +export { + ValueType, + Counter, + Histogram, + MetricOptions, + Observable, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, + BatchObservableCallback, + MetricAttributes, + MetricAttributeValue, + ObservableCallback, +} from './metrics/Metric'; +export { + BatchObservableResult, + ObservableResult, +} from './metrics/ObservableResult'; + +// Propagation APIs +export { + TextMapPropagator, + TextMapSetter, + TextMapGetter, + defaultTextMapGetter, + defaultTextMapSetter, +} from './propagation/TextMapPropagator'; +export type { PropagationAPI } from './api/propagation'; + +// Trace APIs +export { + SpanAttributes, + SpanAttributeValue, +} from './trace/attributes'; +export { Link } from './trace/link'; +export { ProxyTracer, TracerDelegator } from './trace/ProxyTracer'; +export { ProxyTracerProvider } from './trace/ProxyTracerProvider'; +export { Sampler } from './trace/Sampler'; +export { SamplingDecision, SamplingResult } from './trace/SamplingResult'; +export { SpanContext } from './trace/span_context'; +export { SpanKind } from './trace/span_kind'; +export { Span } from './trace/span'; +export { SpanOptions } from './trace/SpanOptions'; +export { SpanStatus, SpanStatusCode } from './trace/status'; +export { TraceFlags } from './trace/trace_flags'; +export { TraceState } from './trace/trace_state'; +export { createTraceState } from './trace/internal/utils'; +export { TracerProvider } from './trace/tracer_provider'; +export { Tracer } from './trace/tracer'; +export { TracerOptions } from './trace/tracer_options'; +export { + isSpanContextValid, + isValidTraceId, + isValidSpanId, +} from './trace/spancontext-utils'; +export { + INVALID_SPANID, + INVALID_TRACEID, + INVALID_SPAN_CONTEXT, +} from './trace/invalid-span-constants'; +export type { TraceAPI } from './api/trace'; + +// Split module-level variable definition into separate files to allow +// tree-shaking on each api instance. +import { context } from './context-api'; +import { diag } from './diag-api'; +import { metrics } from './metrics-api'; +import { propagation } from './propagation-api'; +import { trace } from './trace-api'; + +// Named export. +export { + context, + diag, + metrics, + propagation, + trace, +}; +// Default export. +export default { + context, + diag, + metrics, + propagation, + trace, +}; diff --git a/api/src/internal/global-utils.ts b/api/src/internal/global-utils.ts new file mode 100644 index 0000000000..0753bc2775 --- /dev/null +++ b/api/src/internal/global-utils.ts @@ -0,0 +1,104 @@ +/* + * 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 { MeterProvider } from '../metrics/MeterProvider'; +import { ContextManager } from '../context/types'; +import { DiagLogger } from '../diag/types'; +import { _globalThis } from '../platform'; +import { TextMapPropagator } from '../propagation/TextMapPropagator'; +import type { TracerProvider } from '../trace/tracer_provider'; +import { VERSION } from '../version'; +import { isCompatible } from './semver'; + +const major = VERSION.split('.')[0]; +const GLOBAL_OPENTELEMETRY_API_KEY = Symbol.for( + `opentelemetry.js.api.${major}` +); + +const _global = _globalThis as OTelGlobal; + +export function registerGlobal( + type: Type, + instance: OTelGlobalAPI[Type], + diag: DiagLogger, + allowOverride = false +): boolean { + const api = (_global[GLOBAL_OPENTELEMETRY_API_KEY] = _global[ + GLOBAL_OPENTELEMETRY_API_KEY + ] ?? { + version: VERSION, + }); + + if (!allowOverride && api[type]) { + // already registered an API of this type + const err = new Error( + `@opentelemetry/api: Attempted duplicate registration of API: ${type}` + ); + diag.error(err.stack || err.message); + return false; + } + + if (api.version !== VERSION) { + // All registered APIs must be of the same version exactly + const err = new Error( + '@opentelemetry/api: All API registration versions must match' + ); + diag.error(err.stack || err.message); + return false; + } + + api[type] = instance; + diag.debug( + `@opentelemetry/api: Registered a global for ${type} v${VERSION}.` + ); + + return true; +} + +export function getGlobal( + type: Type +): OTelGlobalAPI[Type] | undefined { + const globalVersion = _global[GLOBAL_OPENTELEMETRY_API_KEY]?.version; + if (!globalVersion || !isCompatible(globalVersion)) { + return; + } + return _global[GLOBAL_OPENTELEMETRY_API_KEY]?.[type]; +} + +export function unregisterGlobal(type: keyof OTelGlobalAPI, diag: DiagLogger) { + diag.debug( + `@opentelemetry/api: Unregistering a global for ${type} v${VERSION}.` + ); + const api = _global[GLOBAL_OPENTELEMETRY_API_KEY]; + + if (api) { + delete api[type]; + } +} + +type OTelGlobal = { + [GLOBAL_OPENTELEMETRY_API_KEY]?: OTelGlobalAPI; +}; + +type OTelGlobalAPI = { + version: string; + + diag?: DiagLogger; + trace?: TracerProvider; + context?: ContextManager; + metrics?: MeterProvider; + propagation?: TextMapPropagator; +}; diff --git a/api/src/internal/semver.ts b/api/src/internal/semver.ts new file mode 100644 index 0000000000..076f9b9b51 --- /dev/null +++ b/api/src/internal/semver.ts @@ -0,0 +1,140 @@ +/* + * 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 { VERSION } from '../version'; + +const re = /^(\d+)\.(\d+)\.(\d+)(-(.+))?$/; + +/** + * Create a function to test an API version to see if it is compatible with the provided ownVersion. + * + * The returned function has the following semantics: + * - Exact match is always compatible + * - Major versions must match exactly + * - 1.x package cannot use global 2.x package + * - 2.x package cannot use global 1.x package + * - The minor version of the API module requesting access to the global API must be less than or equal to the minor version of this API + * - 1.3 package may use 1.4 global because the later global contains all functions 1.3 expects + * - 1.4 package may NOT use 1.3 global because it may try to call functions which don't exist on 1.3 + * - If the major version is 0, the minor version is treated as the major and the patch is treated as the minor + * - Patch and build tag differences are not considered at this time + * + * @param ownVersion version which should be checked against + */ +export function _makeCompatibilityCheck( + ownVersion: string +): (globalVersion: string) => boolean { + const acceptedVersions = new Set([ownVersion]); + const rejectedVersions = new Set(); + + const myVersionMatch = ownVersion.match(re); + if (!myVersionMatch) { + // we cannot guarantee compatibility so we always return noop + return () => false; + } + + const ownVersionParsed = { + major: +myVersionMatch[1], + minor: +myVersionMatch[2], + patch: +myVersionMatch[3], + prerelease: myVersionMatch[4], + }; + + // if ownVersion has a prerelease tag, versions must match exactly + if (ownVersionParsed.prerelease != null) { + return function isExactmatch(globalVersion: string): boolean { + return globalVersion === ownVersion; + }; + } + + function _reject(v: string) { + rejectedVersions.add(v); + return false; + } + + function _accept(v: string) { + acceptedVersions.add(v); + return true; + } + + return function isCompatible(globalVersion: string): boolean { + if (acceptedVersions.has(globalVersion)) { + return true; + } + + if (rejectedVersions.has(globalVersion)) { + return false; + } + + const globalVersionMatch = globalVersion.match(re); + if (!globalVersionMatch) { + // cannot parse other version + // we cannot guarantee compatibility so we always noop + return _reject(globalVersion); + } + + const globalVersionParsed = { + major: +globalVersionMatch[1], + minor: +globalVersionMatch[2], + patch: +globalVersionMatch[3], + prerelease: globalVersionMatch[4], + }; + + // if globalVersion has a prerelease tag, versions must match exactly + if (globalVersionParsed.prerelease != null) { + return _reject(globalVersion); + } + + // major versions must match + if (ownVersionParsed.major !== globalVersionParsed.major) { + return _reject(globalVersion); + } + + if (ownVersionParsed.major === 0) { + if ( + ownVersionParsed.minor === globalVersionParsed.minor && + ownVersionParsed.patch <= globalVersionParsed.patch + ) { + return _accept(globalVersion); + } + + return _reject(globalVersion); + } + + if (ownVersionParsed.minor <= globalVersionParsed.minor) { + return _accept(globalVersion); + } + + return _reject(globalVersion); + }; +} + +/** + * Test an API version to see if it is compatible with this API. + * + * - Exact match is always compatible + * - Major versions must match exactly + * - 1.x package cannot use global 2.x package + * - 2.x package cannot use global 1.x package + * - The minor version of the API module requesting access to the global API must be less than or equal to the minor version of this API + * - 1.3 package may use 1.4 global because the later global contains all functions 1.3 expects + * - 1.4 package may NOT use 1.3 global because it may try to call functions which don't exist on 1.3 + * - If the major version is 0, the minor version is treated as the major and the patch is treated as the minor + * - Patch and build tag differences are not considered at this time + * + * @param version version of the API requesting an instance of the global API + */ +export const isCompatible = _makeCompatibilityCheck(VERSION); diff --git a/experimental/packages/opentelemetry-api-metrics/src/index.ts b/api/src/metrics-api.ts similarity index 77% rename from experimental/packages/opentelemetry-api-metrics/src/index.ts rename to api/src/metrics-api.ts index e306e6c1ab..16f399a22e 100644 --- a/experimental/packages/opentelemetry-api-metrics/src/index.ts +++ b/api/src/metrics-api.ts @@ -14,13 +14,8 @@ * limitations under the License. */ -export * from './NoopMeter'; -export * from './NoopMeterProvider'; -export * from './types/Meter'; -export * from './types/MeterProvider'; -export * from './types/Metric'; -export * from './types/ObservableResult'; - +// Split module-level variable definition into separate files to allow +// tree-shaking on each api instance. import { MetricsAPI } from './api/metrics'; /** Entrypoint for metrics API */ export const metrics = MetricsAPI.getInstance(); diff --git a/experimental/packages/opentelemetry-api-metrics/src/types/Meter.ts b/api/src/metrics/Meter.ts similarity index 76% rename from experimental/packages/opentelemetry-api-metrics/src/types/Meter.ts rename to api/src/metrics/Meter.ts index 3931f01622..1904c48871 100644 --- a/experimental/packages/opentelemetry-api-metrics/src/types/Meter.ts +++ b/api/src/metrics/Meter.ts @@ -18,6 +18,7 @@ import { BatchObservableCallback, Counter, Histogram, + MetricAttributes, MetricOptions, Observable, ObservableCounter, @@ -49,7 +50,10 @@ export interface Meter { * @param name the name of the metric. * @param [options] the metric options. */ - createHistogram(name: string, options?: MetricOptions): Histogram; + createHistogram( + name: string, + options?: MetricOptions + ): Histogram; /** * Creates a new `Counter` metric. Generally, this kind of metric when the @@ -58,7 +62,10 @@ export interface Meter { * @param name the name of the metric. * @param [options] the metric options. */ - createCounter(name: string, options?: MetricOptions): Counter; + createCounter( + name: string, + options?: MetricOptions + ): Counter; /** * Creates a new `UpDownCounter` metric. UpDownCounter is a synchronous @@ -77,7 +84,7 @@ export interface Meter { * @param name the name of the metric. * @param [options] the metric options. */ - createUpDownCounter(name: string, options?: MetricOptions): UpDownCounter; + createUpDownCounter(name: string, options?: MetricOptions): UpDownCounter; /** * Creates a new `ObservableGauge` metric. @@ -87,10 +94,10 @@ export interface Meter { * @param name the name of the metric. * @param [options] the metric options. */ - createObservableGauge( + createObservableGauge( name: string, options?: MetricOptions - ): ObservableGauge; + ): ObservableGauge; /** * Creates a new `ObservableCounter` metric. @@ -100,10 +107,10 @@ export interface Meter { * @param name the name of the metric. * @param [options] the metric options. */ - createObservableCounter( + createObservableCounter( name: string, options?: MetricOptions - ): ObservableCounter; + ): ObservableCounter; /** * Creates a new `ObservableUpDownCounter` metric. @@ -113,10 +120,10 @@ export interface Meter { * @param name the name of the metric. * @param [options] the metric options. */ - createObservableUpDownCounter( + createObservableUpDownCounter( name: string, options?: MetricOptions - ): ObservableUpDownCounter; + ): ObservableUpDownCounter; /** * Sets up a function that will be called whenever a metric collection is @@ -132,7 +139,10 @@ export interface Meter { * @param callback the batch observable callback * @param observables the observables associated with this batch observable callback */ - addBatchObservableCallback(callback: BatchObservableCallback, observables: Observable[]): void; + addBatchObservableCallback( + callback: BatchObservableCallback, + observables: Observable[] + ): void; /** * Removes a callback previously registered with {@link Meter.addBatchObservableCallback}. @@ -143,5 +153,8 @@ export interface Meter { * @param callback the batch observable callback * @param observables the observables associated with this batch observable callback */ - removeBatchObservableCallback(callback: BatchObservableCallback, observables: Observable[]): void; + removeBatchObservableCallback( + callback: BatchObservableCallback, + observables: Observable[] + ): void; } diff --git a/experimental/packages/opentelemetry-api-metrics/src/types/MeterProvider.ts b/api/src/metrics/MeterProvider.ts similarity index 100% rename from experimental/packages/opentelemetry-api-metrics/src/types/MeterProvider.ts rename to api/src/metrics/MeterProvider.ts diff --git a/experimental/packages/opentelemetry-api-metrics/src/types/Metric.ts b/api/src/metrics/Metric.ts similarity index 56% rename from experimental/packages/opentelemetry-api-metrics/src/types/Metric.ts rename to api/src/metrics/Metric.ts index dcf779bc69..687369b92f 100644 --- a/experimental/packages/opentelemetry-api-metrics/src/types/Metric.ts +++ b/api/src/metrics/Metric.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Context, SpanAttributes, SpanAttributeValue } from '@opentelemetry/api'; +import { Attributes, AttributeValue } from '../common/Attributes'; +import { Context } from '../context/types'; import { BatchObservableResult, ObservableResult } from './ObservableResult'; /** @@ -61,67 +62,63 @@ export enum ValueType { *
  • count the number of 5xx errors.
  • *
      */ -export interface Counter { +export interface Counter { /** - * Increment value of counter by the input. Inputs may not be negative. + * Increment value of counter by the input. Inputs must not be negative. */ - add(value: number, attributes?: MetricAttributes, context?: Context): void; + add(value: number, attributes?: AttributesTypes, context?: Context): void; } -export interface UpDownCounter { +export interface UpDownCounter { /** * Increment value of counter by the input. Inputs may be negative. */ - add(value: number, attributes?: MetricAttributes, context?: Context): void; + add(value: number, attributes?: AttributesTypes, context?: Context): void; } -export interface Histogram { +export interface Histogram { /** * Records a measurement. Value of the measurement must not be negative. */ - record(value: number, attributes?: MetricAttributes, context?: Context): void; + record(value: number, attributes?: AttributesTypes, context?: Context): void; } -// api.SpanAttributes instead of api.Attributes is used here for api package backward compatibility. /** - * Attributes is a map from string to attribute values. - * - * Note: only the own enumerable keys are counted as valid attribute keys. + * @deprecated please use {@link Attributes} */ -export type MetricAttributes = SpanAttributes; +export type MetricAttributes = Attributes; -// api.SpanAttributeValue instead of api.AttributeValue is used here for api package backward compatibility. /** - * Attribute values may be any non-nullish primitive value except an object. - * - * null or undefined attribute values are invalid and will result in undefined behavior. + * @deprecated please use {@link AttributeValue} */ -export type MetricAttributeValue = SpanAttributeValue; +export type MetricAttributeValue = AttributeValue; /** * The observable callback for Observable instruments. */ -export type ObservableCallback = (observableResult: ObservableResult) => void | Promise; +export type ObservableCallback = + (observableResult: ObservableResult) => void | Promise; /** * The observable callback for a batch of Observable instruments. */ -export type BatchObservableCallback = (observableResult: BatchObservableResult) => void | Promise; +export type BatchObservableCallback = + (observableResult: BatchObservableResult) => void | Promise; -export interface Observable { +export interface Observable { /** * Sets up a function that will be called whenever a metric collection is initiated. * * If the function is already in the list of callbacks for this Observable, the function is not added a second time. */ - addCallback(callback: ObservableCallback): void; + addCallback(callback: ObservableCallback): void; /** * Removes a callback previously registered with {@link Observable.addCallback}. */ - removeCallback(callback: ObservableCallback): void; + removeCallback(callback: ObservableCallback): void; } -export type ObservableCounter = Observable; -export type ObservableUpDownCounter = Observable; -export type ObservableGauge = Observable; +export type ObservableCounter = Observable; +export type ObservableUpDownCounter = Observable; +export type ObservableGauge = Observable; diff --git a/experimental/packages/opentelemetry-api-metrics/src/NoopMeter.ts b/api/src/metrics/NoopMeter.ts similarity index 96% rename from experimental/packages/opentelemetry-api-metrics/src/NoopMeter.ts rename to api/src/metrics/NoopMeter.ts index 317431096f..de9a45eaac 100644 --- a/experimental/packages/opentelemetry-api-metrics/src/NoopMeter.ts +++ b/api/src/metrics/NoopMeter.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Meter } from './types/Meter'; +import { Meter } from './Meter'; import { BatchObservableCallback, Counter, @@ -27,7 +27,7 @@ import { UpDownCounter, MetricAttributes, Observable, -} from './types/Metric'; +} from './Metric'; /** * NoopMeter is a noop implementation of the {@link Meter} interface. It reuses @@ -114,11 +114,14 @@ export class NoopHistogramMetric extends NoopMetric implements Histogram { export class NoopObservableMetric { addCallback(_callback: ObservableCallback) {} + removeCallback(_callback: ObservableCallback) {} } export class NoopObservableCounterMetric extends NoopObservableMetric implements ObservableCounter {} + export class NoopObservableGaugeMetric extends NoopObservableMetric implements ObservableGauge {} + export class NoopObservableUpDownCounterMetric extends NoopObservableMetric implements ObservableUpDownCounter {} export const NOOP_METER = new NoopMeter(); @@ -132,3 +135,10 @@ export const NOOP_UP_DOWN_COUNTER_METRIC = new NoopUpDownCounterMetric(); export const NOOP_OBSERVABLE_COUNTER_METRIC = new NoopObservableCounterMetric(); export const NOOP_OBSERVABLE_GAUGE_METRIC = new NoopObservableGaugeMetric(); export const NOOP_OBSERVABLE_UP_DOWN_COUNTER_METRIC = new NoopObservableUpDownCounterMetric(); + +/** + * Create a no-op Meter + */ +export function createNoopMeter(): Meter { + return NOOP_METER; +} diff --git a/experimental/packages/opentelemetry-api-metrics/src/NoopMeterProvider.ts b/api/src/metrics/NoopMeterProvider.ts similarity index 90% rename from experimental/packages/opentelemetry-api-metrics/src/NoopMeterProvider.ts rename to api/src/metrics/NoopMeterProvider.ts index 97628d7122..7b6550ebac 100644 --- a/experimental/packages/opentelemetry-api-metrics/src/NoopMeterProvider.ts +++ b/api/src/metrics/NoopMeterProvider.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { Meter, MeterOptions } from './types/Meter'; -import { MeterProvider } from './types/MeterProvider'; +import { Meter, MeterOptions } from './Meter'; +import { MeterProvider } from './MeterProvider'; import { NOOP_METER } from './NoopMeter'; /** diff --git a/experimental/packages/opentelemetry-api-metrics/src/types/ObservableResult.ts b/api/src/metrics/ObservableResult.ts similarity index 81% rename from experimental/packages/opentelemetry-api-metrics/src/types/ObservableResult.ts rename to api/src/metrics/ObservableResult.ts index a38755982d..3bc4bc9c11 100644 --- a/experimental/packages/opentelemetry-api-metrics/src/types/ObservableResult.ts +++ b/api/src/metrics/ObservableResult.ts @@ -19,7 +19,7 @@ import { MetricAttributes, Observable } from './Metric'; /** * Interface that is being used in callback function for Observable Metric. */ -export interface ObservableResult { +export interface ObservableResult { /** * Observe a measurement of the value associated with the given attributes. * @@ -28,13 +28,13 @@ export interface ObservableResult { * one values associated with the same attributes values, SDK may pick the * last one or simply drop the entire observable result. */ - observe(value: number, attributes?: MetricAttributes): void; + observe(value: number, attributes?: AttributesTypes): void; } /** * Interface that is being used in batch observable callback function. */ -export interface BatchObservableResult { +export interface BatchObservableResult { /** * Observe a measurement of the value associated with the given attributes. * @@ -44,5 +44,5 @@ export interface BatchObservableResult { * one values associated with the same attributes values, SDK may pick the * last one or simply drop the entire observable result. */ - observe(metric: Observable, value: number, attributes?: MetricAttributes): void; + observe(metric: Observable, value: number, attributes?: AttributesTypes): void; } diff --git a/api/src/platform/browser/globalThis.ts b/api/src/platform/browser/globalThis.ts new file mode 100644 index 0000000000..76b640b668 --- /dev/null +++ b/api/src/platform/browser/globalThis.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. + */ + +// Updates to this file should also be replicated to @opentelemetry/core too. + +/** + * - globalThis (New standard) + * - self (Will return the current window instance for supported browsers) + * - window (fallback for older browser implementations) + * - global (NodeJS implementation) + * - (When all else fails) + */ + +/** only globals that common to node and browsers are allowed */ +// eslint-disable-next-line node/no-unsupported-features/es-builtins, no-undef +export const _globalThis: typeof globalThis = + typeof globalThis === 'object' ? globalThis : + typeof self === 'object' ? self : + typeof window === 'object' ? window : + typeof global === 'object' ? global : + {} as typeof globalThis; diff --git a/experimental/packages/opentelemetry-api-metrics/src/platform/browser/index.ts b/api/src/platform/browser/index.ts similarity index 100% rename from experimental/packages/opentelemetry-api-metrics/src/platform/browser/index.ts rename to api/src/platform/browser/index.ts diff --git a/experimental/packages/opentelemetry-api-metrics/src/platform/index.ts b/api/src/platform/index.ts similarity index 100% rename from experimental/packages/opentelemetry-api-metrics/src/platform/index.ts rename to api/src/platform/index.ts diff --git a/experimental/packages/opentelemetry-api-metrics/src/platform/node/globalThis.ts b/api/src/platform/node/globalThis.ts similarity index 100% rename from experimental/packages/opentelemetry-api-metrics/src/platform/node/globalThis.ts rename to api/src/platform/node/globalThis.ts diff --git a/experimental/packages/opentelemetry-api-metrics/src/platform/node/index.ts b/api/src/platform/node/index.ts similarity index 100% rename from experimental/packages/opentelemetry-api-metrics/src/platform/node/index.ts rename to api/src/platform/node/index.ts diff --git a/api/src/propagation-api.ts b/api/src/propagation-api.ts new file mode 100644 index 0000000000..94b65fce06 --- /dev/null +++ b/api/src/propagation-api.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +// Split module-level variable definition into separate files to allow +// tree-shaking on each api instance. +import { PropagationAPI } from './api/propagation'; +/** Entrypoint for propagation API */ +export const propagation = PropagationAPI.getInstance(); diff --git a/api/src/propagation/NoopTextMapPropagator.ts b/api/src/propagation/NoopTextMapPropagator.ts new file mode 100644 index 0000000000..16d90cf970 --- /dev/null +++ b/api/src/propagation/NoopTextMapPropagator.ts @@ -0,0 +1,33 @@ +/* + * 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 { Context } from '../context/types'; +import { TextMapPropagator } from './TextMapPropagator'; + +/** + * No-op implementations of {@link TextMapPropagator}. + */ +export class NoopTextMapPropagator implements TextMapPropagator { + /** Noop inject function does nothing */ + inject(_context: Context, _carrier: unknown): void {} + /** Noop extract function does nothing and returns the input context */ + extract(context: Context, _carrier: unknown): Context { + return context; + } + fields(): string[] { + return []; + } +} diff --git a/api/src/propagation/TextMapPropagator.ts b/api/src/propagation/TextMapPropagator.ts new file mode 100644 index 0000000000..7cb41432cb --- /dev/null +++ b/api/src/propagation/TextMapPropagator.ts @@ -0,0 +1,137 @@ +/* + * 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 { Context } from '../context/types'; + +/** + * Injects `Context` into and extracts it from carriers that travel + * in-band across process boundaries. Encoding is expected to conform to the + * HTTP Header Field semantics. Values are often encoded as RPC/HTTP request + * headers. + * + * The carrier of propagated data on both the client (injector) and server + * (extractor) side is usually an object such as http headers. Propagation is + * usually implemented via library-specific request interceptors, where the + * client-side injects values and the server-side extracts them. + */ +export interface TextMapPropagator { + /** + * Injects values from a given `Context` into a carrier. + * + * 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 + * the wire. + * @param carrier the carrier of propagation fields, such as http request + * headers. + * @param setter an optional {@link TextMapSetter}. If undefined, values will be + * set by direct object assignment. + */ + inject( + context: Context, + carrier: Carrier, + setter: TextMapSetter + ): void; + + /** + * Given a `Context` and a carrier, extract context values from a + * carrier and return a new context, created from the old context, with the + * extracted values. + * + * @param context the Context from which to extract values to transmit over + * the wire. + * @param carrier the carrier of propagation fields, such as http request + * headers. + * @param getter an optional {@link TextMapGetter}. If undefined, keys will be all + * own properties, and keys will be accessed by direct object access. + */ + extract( + context: Context, + carrier: Carrier, + getter: TextMapGetter + ): Context; + + /** + * Return a list of all fields which may be used by the propagator. + */ + fields(): string[]; +} + +/** + * A setter is specified by the caller to define a specific method + * to set key/value pairs on the carrier within a propagator. + */ +export interface TextMapSetter { + /** + * Callback used to set a key/value pair on an object. + * + * Should be called by the propagator each time a key/value pair + * should be set, and should set that key/value pair on the propagator. + * + * @param carrier object or class which carries key/value pairs + * @param key string key to modify + * @param value value to be set to the key on the carrier + */ + set(carrier: Carrier, key: string, value: string): void; +} + +/** + * A getter is specified by the caller to define a specific method + * to get the value of a key from a carrier. + */ +export interface TextMapGetter { + /** + * Get a list of all keys available on the carrier. + * + * @param carrier + */ + keys(carrier: Carrier): string[]; + + /** + * Get the value of a specific key from the carrier. + * + * @param carrier + * @param key + */ + get(carrier: Carrier, key: string): undefined | string | string[]; +} + +export const defaultTextMapGetter: TextMapGetter = { + get(carrier, key) { + if (carrier == null) { + return undefined; + } + return carrier[key]; + }, + + keys(carrier) { + if (carrier == null) { + return []; + } + return Object.keys(carrier); + }, +}; + +export const defaultTextMapSetter: TextMapSetter = { + set(carrier, key, value) { + if (carrier == null) { + return; + } + + carrier[key] = value; + }, +}; diff --git a/api/src/trace-api.ts b/api/src/trace-api.ts new file mode 100644 index 0000000000..f2b5919e87 --- /dev/null +++ b/api/src/trace-api.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +// Split module-level variable definition into separate files to allow +// tree-shaking on each api instance. +import { TraceAPI } from './api/trace'; +/** Entrypoint for trace API */ +export const trace = TraceAPI.getInstance(); diff --git a/api/src/trace/NonRecordingSpan.ts b/api/src/trace/NonRecordingSpan.ts new file mode 100644 index 0000000000..a9e5bcaf9b --- /dev/null +++ b/api/src/trace/NonRecordingSpan.ts @@ -0,0 +1,75 @@ +/* + * 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 { Exception } from '../common/Exception'; +import { TimeInput } from '../common/Time'; +import { SpanAttributes } from './attributes'; +import { INVALID_SPAN_CONTEXT } from './invalid-span-constants'; +import { Span } from './span'; +import { SpanContext } from './span_context'; +import { SpanStatus } from './status'; + +/** + * The NonRecordingSpan is the default {@link Span} that is used when no Span + * implementation is available. All operations are no-op including context + * propagation. + */ +export class NonRecordingSpan implements Span { + constructor( + private readonly _spanContext: SpanContext = INVALID_SPAN_CONTEXT + ) {} + + // Returns a SpanContext. + spanContext(): SpanContext { + return this._spanContext; + } + + // By default does nothing + setAttribute(_key: string, _value: unknown): this { + return this; + } + + // By default does nothing + setAttributes(_attributes: SpanAttributes): this { + return this; + } + + // By default does nothing + addEvent(_name: string, _attributes?: SpanAttributes): this { + return this; + } + + // By default does nothing + setStatus(_status: SpanStatus): this { + return this; + } + + // By default does nothing + updateName(_name: string): this { + return this; + } + + // By default does nothing + end(_endTime?: TimeInput): void {} + + // isRecording always returns false for NonRecordingSpan. + isRecording(): boolean { + return false; + } + + // By default does nothing + recordException(_exception: Exception, _time?: TimeInput): void {} +} diff --git a/api/src/trace/NoopTracer.ts b/api/src/trace/NoopTracer.ts new file mode 100644 index 0000000000..cf386c4f57 --- /dev/null +++ b/api/src/trace/NoopTracer.ts @@ -0,0 +1,105 @@ +/* + * 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 { ContextAPI } from '../api/context'; +import { Context } from '../context/types'; +import { getSpanContext, setSpan } from '../trace/context-utils'; +import { NonRecordingSpan } from './NonRecordingSpan'; +import { Span } from './span'; +import { isSpanContextValid } from './spancontext-utils'; +import { SpanOptions } from './SpanOptions'; +import { SpanContext } from './span_context'; +import { Tracer } from './tracer'; + +const contextApi = ContextAPI.getInstance(); + +/** + * No-op implementations of {@link Tracer}. + */ +export class NoopTracer implements Tracer { + // startSpan starts a noop span. + startSpan(name: string, options?: SpanOptions, context?: Context): Span { + const root = Boolean(options?.root); + if (root) { + return new NonRecordingSpan(); + } + + const parentFromContext = context && getSpanContext(context); + + if ( + isSpanContext(parentFromContext) && + isSpanContextValid(parentFromContext) + ) { + return new NonRecordingSpan(parentFromContext); + } else { + return new NonRecordingSpan(); + } + } + + startActiveSpan ReturnType>( + name: string, + fn: F + ): ReturnType; + startActiveSpan ReturnType>( + name: string, + opts: SpanOptions | undefined, + fn: F + ): ReturnType; + startActiveSpan ReturnType>( + name: string, + opts: SpanOptions | undefined, + ctx: Context | undefined, + fn: F + ): ReturnType; + startActiveSpan ReturnType>( + name: string, + arg2?: F | SpanOptions, + arg3?: F | Context, + arg4?: F + ): ReturnType | undefined { + let opts: SpanOptions | undefined; + let ctx: Context | undefined; + let fn: F; + + if (arguments.length < 2) { + return; + } else if (arguments.length === 2) { + fn = arg2 as F; + } else if (arguments.length === 3) { + opts = arg2 as SpanOptions | undefined; + fn = arg3 as F; + } else { + opts = arg2 as SpanOptions | undefined; + ctx = arg3 as Context | undefined; + fn = arg4 as F; + } + + const parentContext = ctx ?? contextApi.active(); + const span = this.startSpan(name, opts, parentContext); + const contextWithSpanSet = setSpan(parentContext, span); + + return contextApi.with(contextWithSpanSet, fn, undefined, span); + } +} + +function isSpanContext(spanContext: any): spanContext is SpanContext { + return ( + typeof spanContext === 'object' && + typeof spanContext['spanId'] === 'string' && + typeof spanContext['traceId'] === 'string' && + typeof spanContext['traceFlags'] === 'number' + ); +} diff --git a/api/src/trace/NoopTracerProvider.ts b/api/src/trace/NoopTracerProvider.ts new file mode 100644 index 0000000000..f4b31b9ac5 --- /dev/null +++ b/api/src/trace/NoopTracerProvider.ts @@ -0,0 +1,36 @@ +/* + * 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 { NoopTracer } from './NoopTracer'; +import { Tracer } from './tracer'; +import { TracerOptions } from './tracer_options'; +import { TracerProvider } from './tracer_provider'; + +/** + * An implementation of the {@link TracerProvider} which returns an impotent + * Tracer for all calls to `getTracer`. + * + * All operations are no-op. + */ +export class NoopTracerProvider implements TracerProvider { + getTracer( + _name?: string, + _version?: string, + _options?: TracerOptions + ): Tracer { + return new NoopTracer(); + } +} diff --git a/api/src/trace/ProxyTracer.ts b/api/src/trace/ProxyTracer.ts new file mode 100644 index 0000000000..a858bdc3ce --- /dev/null +++ b/api/src/trace/ProxyTracer.ts @@ -0,0 +1,76 @@ +/* + * 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 { Context } from '../context/types'; +import { NoopTracer } from './NoopTracer'; +import { Span } from './span'; +import { SpanOptions } from './SpanOptions'; +import { Tracer } from './tracer'; +import { TracerOptions } from './tracer_options'; + +const NOOP_TRACER = new NoopTracer(); + +/** + * 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: TracerDelegator, + public readonly name: string, + public readonly version?: string, + public readonly options?: TracerOptions + ) {} + + startSpan(name: string, options?: SpanOptions, context?: Context): Span { + return this._getTracer().startSpan(name, options, context); + } + + startActiveSpan unknown>( + _name: string, + _options: F | SpanOptions, + _context?: F | Context, + _fn?: F + ): ReturnType { + const tracer = this._getTracer(); + return Reflect.apply(tracer.startActiveSpan, tracer, arguments); + } + + /** + * 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, this.options); + + if (!tracer) { + return NOOP_TRACER; + } + + this._delegate = tracer; + return this._delegate; + } +} + +export interface TracerDelegator { + getDelegateTracer(name: string, version?: string, options?: TracerOptions): Tracer | undefined; +} diff --git a/api/src/trace/ProxyTracerProvider.ts b/api/src/trace/ProxyTracerProvider.ts new file mode 100644 index 0000000000..f7b9c475a2 --- /dev/null +++ b/api/src/trace/ProxyTracerProvider.ts @@ -0,0 +1,64 @@ +/* + * 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 { NoopTracerProvider } from './NoopTracerProvider'; +import { TracerOptions } from './tracer_options'; + +const NOOP_TRACER_PROVIDER = new 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, options?: TracerOptions): Tracer { + return ( + this.getDelegateTracer(name, version, options) ?? + new ProxyTracer(this, name, version, options) + ); + } + + getDelegate(): TracerProvider { + return this._delegate ?? NOOP_TRACER_PROVIDER; + } + + /** + * Set the delegate tracer provider + */ + setDelegate(delegate: TracerProvider) { + this._delegate = delegate; + } + + getDelegateTracer( + name: string, + version?: string, + options?: TracerOptions + ): Tracer | undefined { + return this._delegate?.getTracer(name, version, options); + } +} diff --git a/api/src/trace/Sampler.ts b/api/src/trace/Sampler.ts new file mode 100644 index 0000000000..b7a1d376ce --- /dev/null +++ b/api/src/trace/Sampler.ts @@ -0,0 +1,55 @@ +/* + * 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 { Context } from '../context/types'; +import { SpanAttributes } from './attributes'; +import { Link } from './link'; +import { SamplingResult } from './SamplingResult'; +import { SpanKind } from './span_kind'; + +/** + * @deprecated use the one declared in @opentelemetry/sdk-trace-base instead. + * This interface represent a sampler. Sampling is a mechanism to control the + * noise and overhead introduced by OpenTelemetry by reducing the number of + * samples of traces collected and sent to the backend. + */ +export interface Sampler { + /** + * Checks whether span needs to be created and tracked. + * + * @param context Parent Context which may contain a span. + * @param traceId of the span to be created. It can be different from the + * traceId in the {@link SpanContext}. Typically in situations when the + * span to be created starts a new trace. + * @param spanName of the span to be created. + * @param spanKind of the span to be created. + * @param attributes Initial set of SpanAttributes for the Span being constructed. + * @param links Collection of links that will be associated with the Span to + * be created. Typically useful for batch operations. + * @returns a {@link SamplingResult}. + */ + shouldSample( + context: Context, + traceId: string, + spanName: string, + spanKind: SpanKind, + attributes: SpanAttributes, + links: Link[] + ): SamplingResult; + + /** Returns the sampler name or short description with the configuration. */ + toString(): string; +} diff --git a/api/src/trace/SamplingResult.ts b/api/src/trace/SamplingResult.ts new file mode 100644 index 0000000000..3038eec077 --- /dev/null +++ b/api/src/trace/SamplingResult.ts @@ -0,0 +1,58 @@ +/* + * 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 { SpanAttributes } from './attributes'; + +/** + * @deprecated use the one declared in @opentelemetry/sdk-trace-base instead. + * A sampling decision that determines how a {@link Span} will be recorded + * and collected. + */ +export enum SamplingDecision { + /** + * `Span.isRecording() === false`, span will not be recorded and all events + * and attributes will be dropped. + */ + NOT_RECORD, + /** + * `Span.isRecording() === true`, but `Sampled` flag in {@link TraceFlags} + * MUST NOT be set. + */ + RECORD, + /** + * `Span.isRecording() === true` AND `Sampled` flag in {@link TraceFlags} + * MUST be set. + */ + RECORD_AND_SAMPLED, +} + +/** + * @deprecated use the one declared in @opentelemetry/sdk-trace-base instead. + * A sampling result contains a decision for a {@link Span} and additional + * attributes the sampler would like to added to the Span. + */ +export interface SamplingResult { + /** + * A sampling decision, refer to {@link SamplingDecision} for details. + */ + decision: SamplingDecision; + /** + * The list of attributes returned by SamplingResult MUST be immutable. + * Caller may call {@link Sampler}.shouldSample any number of times and + * can safely cache the returned value. + */ + attributes?: Readonly; +} diff --git a/api/src/trace/SpanOptions.ts b/api/src/trace/SpanOptions.ts new file mode 100644 index 0000000000..cf368ec287 --- /dev/null +++ b/api/src/trace/SpanOptions.ts @@ -0,0 +1,43 @@ +/* + * 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 { TimeInput } from '../common/Time'; +import { SpanAttributes } from './attributes'; +import { Link } from './link'; +import { SpanKind } from './span_kind'; + +/** + * Options needed for span creation + */ +export interface SpanOptions { + /** + * The SpanKind of a span + * @default {@link SpanKind.INTERNAL} + */ + kind?: SpanKind; + + /** A span's attributes */ + attributes?: SpanAttributes; + + /** {@link Link}s span to other spans */ + links?: Link[]; + + /** A manually specified start time for the created `Span` object. */ + startTime?: TimeInput; + + /** The new span should be a root span. (Ignore parent from context). */ + root?: boolean; +} diff --git a/api/src/trace/attributes.ts b/api/src/trace/attributes.ts new file mode 100644 index 0000000000..01feff0b0a --- /dev/null +++ b/api/src/trace/attributes.ts @@ -0,0 +1,27 @@ +/* + * 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 { Attributes, AttributeValue } from '../common/Attributes'; + +/** + * @deprecated please use {@link Attributes} + */ +export type SpanAttributes = Attributes; + +/** + * @deprecated please use {@link AttributeValue} + */ +export type SpanAttributeValue = AttributeValue; diff --git a/api/src/trace/context-utils.ts b/api/src/trace/context-utils.ts new file mode 100644 index 0000000000..83bd55184c --- /dev/null +++ b/api/src/trace/context-utils.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 { createContextKey } from '../context/context'; +import { Context } from '../context/types'; +import { Span } from './span'; +import { SpanContext } from './span_context'; +import { NonRecordingSpan } from './NonRecordingSpan'; +import { ContextAPI } from '../api/context'; + +/** + * span key + */ +const SPAN_KEY = createContextKey('OpenTelemetry Context Key SPAN'); + +/** + * Return the span if one exists + * + * @param context context to get span from + */ +export function getSpan(context: Context): Span | undefined { + return (context.getValue(SPAN_KEY) as Span) || undefined; +} + +/** + * Gets the span from the current context, if one exists. + */ +export function getActiveSpan(): Span | undefined { + return getSpan(ContextAPI.getInstance().active()); +} + +/** + * Set the span on a context + * + * @param context context to use as parent + * @param span span to set active + */ +export function setSpan(context: Context, span: Span): Context { + return context.setValue(SPAN_KEY, span); +} + +/** + * Remove current span stored in the context + * + * @param context context to delete span from + */ +export function deleteSpan(context: Context): Context { + return context.deleteValue(SPAN_KEY); +} + +/** + * Wrap span context in a NoopSpan and set as span in a new + * context + * + * @param context context to set active span on + * @param spanContext span context to be wrapped + */ +export function setSpanContext( + context: Context, + spanContext: SpanContext +): Context { + return setSpan(context, new NonRecordingSpan(spanContext)); +} + +/** + * Get the span context of the span if it exists. + * + * @param context context to get values from + */ +export function getSpanContext(context: Context): SpanContext | undefined { + return getSpan(context)?.spanContext(); +} diff --git a/api/src/trace/internal/tracestate-impl.ts b/api/src/trace/internal/tracestate-impl.ts new file mode 100644 index 0000000000..4c69a3a97a --- /dev/null +++ b/api/src/trace/internal/tracestate-impl.ts @@ -0,0 +1,110 @@ +/* + * 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 { TraceState } from '../trace_state'; +import { validateKey, validateValue } from './tracestate-validators'; + +const MAX_TRACE_STATE_ITEMS = 32; +const MAX_TRACE_STATE_LEN = 512; +const LIST_MEMBERS_SEPARATOR = ','; +const LIST_MEMBER_KEY_VALUE_SPLITTER = '='; + +/** + * TraceState must be a class and not a simple object type because of the spec + * requirement (https://www.w3.org/TR/trace-context/#tracestate-field). + * + * Here is the list of allowed mutations: + * - New key-value pair should be added into the beginning of the list + * - The value of any key can be updated. Modified keys MUST be moved to the + * beginning of the list. + */ +export class TraceStateImpl implements TraceState { + private _internalState: Map = new Map(); + + constructor(rawTraceState?: string) { + if (rawTraceState) this._parse(rawTraceState); + } + + set(key: string, value: string): TraceStateImpl { + // TODO: Benchmark the different approaches(map vs list) and + // use the faster one. + const traceState = this._clone(); + if (traceState._internalState.has(key)) { + traceState._internalState.delete(key); + } + traceState._internalState.set(key, value); + return traceState; + } + + unset(key: string): TraceStateImpl { + const traceState = this._clone(); + traceState._internalState.delete(key); + return traceState; + } + + get(key: string): string | undefined { + return this._internalState.get(key); + } + + serialize(): string { + return this._keys() + .reduce((agg: string[], key) => { + agg.push(key + LIST_MEMBER_KEY_VALUE_SPLITTER + this.get(key)); + return agg; + }, []) + .join(LIST_MEMBERS_SEPARATOR); + } + + private _parse(rawTraceState: string) { + if (rawTraceState.length > MAX_TRACE_STATE_LEN) return; + this._internalState = rawTraceState + .split(LIST_MEMBERS_SEPARATOR) + .reverse() // Store in reverse so new keys (.set(...)) will be placed at the beginning + .reduce((agg: Map, part: string) => { + const listMember = part.trim(); // Optional Whitespace (OWS) handling + const i = listMember.indexOf(LIST_MEMBER_KEY_VALUE_SPLITTER); + if (i !== -1) { + const key = listMember.slice(0, i); + const value = listMember.slice(i + 1, part.length); + if (validateKey(key) && validateValue(value)) { + agg.set(key, value); + } else { + // TODO: Consider to add warning log + } + } + return agg; + }, new Map()); + + // Because of the reverse() requirement, trunc must be done after map is created + if (this._internalState.size > MAX_TRACE_STATE_ITEMS) { + this._internalState = new Map( + Array.from(this._internalState.entries()) + .reverse() // Use reverse same as original tracestate parse chain + .slice(0, MAX_TRACE_STATE_ITEMS) + ); + } + } + + private _keys(): string[] { + return Array.from(this._internalState.keys()).reverse(); + } + + private _clone(): TraceStateImpl { + const traceState = new TraceStateImpl(); + traceState._internalState = new Map(this._internalState); + return traceState; + } +} diff --git a/api/src/trace/internal/tracestate-validators.ts b/api/src/trace/internal/tracestate-validators.ts new file mode 100644 index 0000000000..78fbee8234 --- /dev/null +++ b/api/src/trace/internal/tracestate-validators.ts @@ -0,0 +1,45 @@ +/* + * 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. + */ + +const VALID_KEY_CHAR_RANGE = '[_0-9a-z-*/]'; +const VALID_KEY = `[a-z]${VALID_KEY_CHAR_RANGE}{0,255}`; +const VALID_VENDOR_KEY = `[a-z0-9]${VALID_KEY_CHAR_RANGE}{0,240}@[a-z]${VALID_KEY_CHAR_RANGE}{0,13}`; +const VALID_KEY_REGEX = new RegExp(`^(?:${VALID_KEY}|${VALID_VENDOR_KEY})$`); +const VALID_VALUE_BASE_REGEX = /^[ -~]{0,255}[!-~]$/; +const INVALID_VALUE_COMMA_EQUAL_REGEX = /,|=/; + +/** + * Key is opaque string up to 256 characters printable. It MUST begin with a + * lowercase letter, and can only contain lowercase letters a-z, digits 0-9, + * underscores _, dashes -, asterisks *, and forward slashes /. + * For multi-tenant vendor scenarios, an at sign (@) can be used to prefix the + * vendor name. Vendors SHOULD set the tenant ID at the beginning of the key. + * see https://www.w3.org/TR/trace-context/#key + */ +export function validateKey(key: string): boolean { + return VALID_KEY_REGEX.test(key); +} + +/** + * Value is opaque string up to 256 characters printable ASCII RFC0020 + * characters (i.e., the range 0x20 to 0x7E) except comma , and =. + */ +export function validateValue(value: string): boolean { + return ( + VALID_VALUE_BASE_REGEX.test(value) && + !INVALID_VALUE_COMMA_EQUAL_REGEX.test(value) + ); +} diff --git a/api/src/trace/internal/utils.ts b/api/src/trace/internal/utils.ts new file mode 100644 index 0000000000..080be77848 --- /dev/null +++ b/api/src/trace/internal/utils.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. + */ + +import { TraceState } from '../trace_state'; +import { TraceStateImpl } from './tracestate-impl'; + + +export function createTraceState(rawTraceState?: string): TraceState { + return new TraceStateImpl(rawTraceState); +} diff --git a/api/src/trace/invalid-span-constants.ts b/api/src/trace/invalid-span-constants.ts new file mode 100644 index 0000000000..882b4fe58b --- /dev/null +++ b/api/src/trace/invalid-span-constants.ts @@ -0,0 +1,26 @@ +/* + * 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 { SpanContext } from './span_context'; +import { TraceFlags } from './trace_flags'; + +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, +}; diff --git a/api/src/trace/link.ts b/api/src/trace/link.ts new file mode 100644 index 0000000000..1d81d38d69 --- /dev/null +++ b/api/src/trace/link.ts @@ -0,0 +1,40 @@ +/* + * 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 { SpanAttributes } from './attributes'; +import { SpanContext } from './span_context'; + +/** + * A pointer from the current {@link Span} to another span in the same trace or + * in a different trace. + * Few examples of Link usage. + * 1. Batch Processing: A batch of elements may contain elements associated + * with one or more traces/spans. Since there can only be one parent + * SpanContext, Link is used to keep reference to SpanContext of all + * elements in the batch. + * 2. Public Endpoint: A SpanContext in incoming client request on a public + * endpoint is untrusted from service provider perspective. In such case it + * is advisable to start a new trace with appropriate sampling decision. + * However, it is desirable to associate incoming SpanContext to new trace + * initiated on service provider side so two traces (from Client and from + * Service Provider) can be correlated. + */ +export interface Link { + /** The {@link SpanContext} of a linked span. */ + context: SpanContext; + /** A set of {@link SpanAttributes} on the link. */ + attributes?: SpanAttributes; +} diff --git a/api/src/trace/span.ts b/api/src/trace/span.ts new file mode 100644 index 0000000000..d80b8c2626 --- /dev/null +++ b/api/src/trace/span.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 { Exception } from '../common/Exception'; +import { TimeInput } from '../common/Time'; +import { SpanAttributes, SpanAttributeValue } from './attributes'; +import { SpanContext } from './span_context'; +import { SpanStatus } from './status'; + +/** + * An interface that represents a span. A span represents a single operation + * within a trace. Examples of span might include remote procedure calls or a + * in-process function calls to sub-components. A Trace has a single, top-level + * "root" Span that in turn may have zero or more child Spans, which in turn + * may have children. + * + * Spans are created by the {@link Tracer.startSpan} method. + */ +export interface Span { + /** + * Returns the {@link SpanContext} object associated with this Span. + * + * Get an immutable, serializable identifier for this span that can be used + * to create new child spans. Returned SpanContext is usable even after the + * span ends. + * + * @returns the SpanContext object associated with this Span. + */ + spanContext(): SpanContext; + + /** + * Sets an attribute to the span. + * + * Sets a single Attribute with the key and value passed as arguments. + * + * @param key the key for this attribute. + * @param value the value for this attribute. Setting a value null or + * undefined is invalid and will result in undefined behavior. + */ + setAttribute(key: string, value: SpanAttributeValue): this; + + /** + * Sets attributes to the span. + * + * @param attributes the attributes that will be added. + * null or undefined attribute values + * are invalid and will result in undefined behavior. + */ + setAttributes(attributes: SpanAttributes): this; + + /** + * Adds an event to the Span. + * + * @param name the name of the event. + * @param [attributesOrStartTime] the attributes that will be added; these are + * associated with this event. Can be also a start time + * if type is {@type TimeInput} and 3rd param is undefined + * @param [startTime] start time of the event. + */ + addEvent( + name: string, + attributesOrStartTime?: SpanAttributes | TimeInput, + startTime?: TimeInput + ): this; + + /** + * Sets a status to the span. If used, this will override the default Span + * status. Default is {@link SpanStatusCode.UNSET}. SetStatus overrides the value + * of previous calls to SetStatus on the Span. + * + * @param status the SpanStatus to set. + */ + setStatus(status: SpanStatus): this; + + /** + * Updates the Span name. + * + * This will override the name provided via {@link Tracer.startSpan}. + * + * Upon this update, any sampling behavior based on Span name will depend on + * the implementation. + * + * @param name the Span name. + */ + updateName(name: string): this; + + /** + * Marks the end of Span execution. + * + * Call to End of a Span MUST not have any effects on child spans. Those may + * still be running and can be ended later. + * + * Do not return `this`. The Span generally should not be used after it + * is ended so chaining is not desired in this context. + * + * @param [endTime] the time to set as Span's end time. If not provided, + * use the current time as the span's end time. + */ + end(endTime?: TimeInput): void; + + /** + * Returns the flag whether this span will be recorded. + * + * @returns true if this Span is active and recording information like events + * 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/api/src/trace/span_context.ts b/api/src/trace/span_context.ts new file mode 100644 index 0000000000..613bdb8eb2 --- /dev/null +++ b/api/src/trace/span_context.ts @@ -0,0 +1,69 @@ +/* + * 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 { TraceState } from './trace_state'; + +/** + * A SpanContext represents the portion of a {@link Span} which must be + * serialized and propagated along side of a {@link Baggage}. + */ +export interface SpanContext { + /** + * The ID of the trace that this span belongs to. It is worldwide unique + * with practically sufficient probability by being made as 16 randomly + * generated bytes, encoded as a 32 lowercase hex characters corresponding to + * 128 bits. + */ + traceId: string; + /** + * The ID of the Span. It is globally unique with practically sufficient + * probability by being made as 8 randomly generated bytes, encoded as a 16 + * lowercase hex characters corresponding to 64 bits. + */ + spanId: string; + /** + * Only true if the SpanContext was propagated from a remote parent. + */ + isRemote?: boolean; + /** + * Trace flags to propagate. + * + * It is represented as 1 byte (bitmap). Bit to represent whether trace is + * sampled or not. When set, the least significant bit documents that the + * caller may have recorded trace data. A caller who does not record trace + * data out-of-band leaves this flag unset. + * + * see {@link TraceFlags} for valid flag values. + */ + traceFlags: number; + /** + * Tracing-system-specific info to propagate. + * + * The tracestate field value is a `list` as defined below. The `list` is a + * series of `list-members` separated by commas `,`, and a list-member is a + * key/value pair separated by an equals sign `=`. Spaces and horizontal tabs + * surrounding `list-members` are ignored. There can be a maximum of 32 + * `list-members` in a `list`. + * More Info: https://www.w3.org/TR/trace-context/#tracestate-field + * + * Examples: + * Single tracing system (generic format): + * tracestate: rojo=00f067aa0ba902b7 + * Multiple tracing systems (with different formatting): + * tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE + */ + traceState?: TraceState; +} diff --git a/api/src/trace/span_kind.ts b/api/src/trace/span_kind.ts new file mode 100644 index 0000000000..d0ed18123e --- /dev/null +++ b/api/src/trace/span_kind.ts @@ -0,0 +1,45 @@ +/* + * 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 enum SpanKind { + /** Default value. Indicates that the span is used internally. */ + INTERNAL = 0, + + /** + * Indicates that the span covers server-side handling of an RPC or other + * remote request. + */ + SERVER = 1, + + /** + * Indicates that the span covers the client-side wrapper around an RPC or + * other remote request. + */ + CLIENT = 2, + + /** + * Indicates that the span describes producer sending a message to a + * broker. Unlike client and server, there is no direct critical path latency + * relationship between producer and consumer spans. + */ + PRODUCER = 3, + + /** + * Indicates that the span describes consumer receiving a message from a + * broker. Unlike client and server, there is no direct critical path latency + * relationship between producer and consumer spans. + */ + CONSUMER = 4, +} diff --git a/api/src/trace/spancontext-utils.ts b/api/src/trace/spancontext-utils.ts new file mode 100644 index 0000000000..6bd54903ae --- /dev/null +++ b/api/src/trace/spancontext-utils.ts @@ -0,0 +1,50 @@ +/* + * 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 { INVALID_SPANID, INVALID_TRACEID } from './invalid-span-constants'; +import { NonRecordingSpan } from './NonRecordingSpan'; +import { Span } from './span'; +import { SpanContext } from './span_context'; + +const VALID_TRACEID_REGEX = /^([0-9a-f]{32})$/i; +const VALID_SPANID_REGEX = /^[0-9a-f]{16}$/i; + +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 isSpanContextValid(spanContext: SpanContext): boolean { + return ( + isValidTraceId(spanContext.traceId) && isValidSpanId(spanContext.spanId) + ); +} + +/** + * Wrap the given {@link SpanContext} in a new non-recording {@link Span} + * + * @param spanContext span context to be wrapped + * @returns a new non-recording {@link Span} with the provided context + */ +export function wrapSpanContext(spanContext: SpanContext): Span { + return new NonRecordingSpan(spanContext); +} diff --git a/api/src/trace/status.ts b/api/src/trace/status.ts new file mode 100644 index 0000000000..f5b7a9ba56 --- /dev/null +++ b/api/src/trace/status.ts @@ -0,0 +1,40 @@ +/* + * 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 interface SpanStatus { + /** The status code of this message. */ + code: SpanStatusCode; + /** A developer-facing error message. */ + message?: string; +} + +/** + * An enumeration of status codes. + */ +export enum SpanStatusCode { + /** + * The default status. + */ + UNSET = 0, + /** + * The operation has been validated by an Application developer or + * Operator to have completed successfully. + */ + OK = 1, + /** + * The operation contains an error. + */ + ERROR = 2, +} diff --git a/api/src/trace/trace_flags.ts b/api/src/trace/trace_flags.ts new file mode 100644 index 0000000000..fd282010c7 --- /dev/null +++ b/api/src/trace/trace_flags.ts @@ -0,0 +1,21 @@ +/* + * 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 enum TraceFlags { + /** Represents no flag set. */ + NONE = 0x0, + /** Bit to represent whether trace is sampled in trace flags. */ + SAMPLED = 0x1 << 0, +} diff --git a/api/src/trace/trace_state.ts b/api/src/trace/trace_state.ts new file mode 100644 index 0000000000..640d1578a9 --- /dev/null +++ b/api/src/trace/trace_state.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. + */ + +export interface TraceState { + /** + * Create a new TraceState which inherits from this TraceState and has the + * given key set. + * The new entry will always be added in the front of the list of states. + * + * @param key key of the TraceState entry. + * @param value value of the TraceState entry. + */ + set(key: string, value: string): TraceState; + + /** + * Return a new TraceState which inherits from this TraceState but does not + * contain the given key. + * + * @param key the key for the TraceState entry to be removed. + */ + unset(key: string): TraceState; + + /** + * Returns the value to which the specified key is mapped, or `undefined` if + * this map contains no mapping for the key. + * + * @param key with which the specified value is to be associated. + * @returns the value to which the specified key is mapped, or `undefined` if + * this map contains no mapping for the key. + */ + get(key: string): string | undefined; + + // TODO: Consider to add support for merging an object as well by also + // accepting a single internalTraceState argument similar to the constructor. + + /** + * Serializes the TraceState to a `list` as defined below. The `list` is a + * series of `list-members` separated by commas `,`, and a list-member is a + * key/value pair separated by an equals sign `=`. Spaces and horizontal tabs + * surrounding `list-members` are ignored. There can be a maximum of 32 + * `list-members` in a `list`. + * + * @returns the serialized string. + */ + serialize(): string; +} diff --git a/api/src/trace/tracer.ts b/api/src/trace/tracer.ts new file mode 100644 index 0000000000..3c76927b25 --- /dev/null +++ b/api/src/trace/tracer.ts @@ -0,0 +1,100 @@ +/* + * 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 { Context } from '../context/types'; +import { Span } from './span'; +import { SpanOptions } from './SpanOptions'; + +/** + * Tracer provides an interface for creating {@link Span}s. + */ +export interface Tracer { + /** + * Starts a new {@link Span}. Start the span without setting it on context. + * + * This method do NOT modify the current Context. + * + * @param name The name of the span + * @param [options] SpanOptions used for span creation + * @param [context] Context to use to extract parent + * @returns Span The newly created span + * @example + * const span = tracer.startSpan('op'); + * span.setAttribute('key', 'value'); + * span.end(); + */ + startSpan(name: string, options?: SpanOptions, context?: Context): Span; + + /** + * Starts a new {@link Span} and calls the given function passing it the + * created span as first argument. + * Additionally the new span gets set in context and this context is activated + * for the duration of the function call. + * + * @param name The name of the span + * @param [options] SpanOptions used for span creation + * @param [context] Context to use to extract parent + * @param fn function called in the context of the span and receives the newly created span as an argument + * @returns return value of fn + * @example + * const something = tracer.startActiveSpan('op', span => { + * try { + * do some work + * span.setStatus({code: SpanStatusCode.OK}); + * return something; + * } catch (err) { + * span.setStatus({ + * code: SpanStatusCode.ERROR, + * message: err.message, + * }); + * throw err; + * } finally { + * span.end(); + * } + * }); + * + * @example + * const span = tracer.startActiveSpan('op', span => { + * try { + * do some work + * return span; + * } catch (err) { + * span.setStatus({ + * code: SpanStatusCode.ERROR, + * message: err.message, + * }); + * throw err; + * } + * }); + * do some more work + * span.end(); + */ + startActiveSpan unknown>( + name: string, + fn: F + ): ReturnType; + startActiveSpan unknown>( + name: string, + options: SpanOptions, + fn: F + ): ReturnType; + startActiveSpan unknown>( + name: string, + options: SpanOptions, + context: Context, + fn: F + ): ReturnType; +} diff --git a/api/src/trace/tracer_options.ts b/api/src/trace/tracer_options.ts new file mode 100644 index 0000000000..44ef004ca7 --- /dev/null +++ b/api/src/trace/tracer_options.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** + * An interface describes additional metadata of a tracer. + */ +export interface TracerOptions { + /** + * The schemaUrl of the tracer or instrumentation library + */ + schemaUrl?: string; +} diff --git a/api/src/trace/tracer_provider.ts b/api/src/trace/tracer_provider.ts new file mode 100644 index 0000000000..0cde8fb664 --- /dev/null +++ b/api/src/trace/tracer_provider.ts @@ -0,0 +1,37 @@ +/* + * 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 { TracerOptions } from './tracer_options'; + +/** + * A registry for creating named {@link Tracer}s. + */ +export interface TracerProvider { + /** + * Returns a Tracer, creating one if one with the given name and version is + * not already created. + * + * This function may return different Tracer types (e.g. + * {@link NoopTracerProvider} vs. a functional tracer). + * + * @param name The name of the tracer or instrumentation library. + * @param version The version of the tracer or instrumentation library. + * @param options The options of the tracer or instrumentation library. + * @returns Tracer A Tracer with the given name and version + */ + getTracer(name: string, version?: string, options?: TracerOptions): Tracer; +} diff --git a/api/test/common/api/api.test.ts b/api/test/common/api/api.test.ts new file mode 100644 index 0000000000..3184827cc6 --- /dev/null +++ b/api/test/common/api/api.test.ts @@ -0,0 +1,284 @@ +/* + * 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 api, { + context, + Context, + defaultTextMapGetter, + defaultTextMapSetter, + diag, + metrics, + propagation, + ROOT_CONTEXT, + Span, + SpanOptions, + TextMapGetter, + TextMapPropagator, + TextMapSetter, + trace, + TraceFlags, +} from '../../../src'; +import { DiagAPI } from '../../../src/api/diag'; +import { NoopMeter } from '../../../src/metrics/NoopMeter'; +import { NoopMeterProvider } from '../../../src/metrics/NoopMeterProvider'; +import { NonRecordingSpan } from '../../../src/trace/NonRecordingSpan'; +import { NoopTracer } from '../../../src/trace/NoopTracer'; +import { NoopTracerProvider } from '../../../src/trace/NoopTracerProvider'; + +// DiagLogger implementation +const diagLoggerFunctions = [ + 'verbose', + 'debug', + 'info', + 'warn', + 'error', +] as const; + +describe('API', () => { + it('should expose a tracer provider via getTracerProvider', () => { + const tracer = api.trace.getTracerProvider(); + assert.ok(tracer); + assert.strictEqual(typeof tracer, 'object'); + }); + + it('getActiveSpan should get the current span', () => { + const span = new NonRecordingSpan(); + const ctx = trace.setSpan(ROOT_CONTEXT, span); + context.setGlobalContextManager({ active: () => ctx, disable: () => {} } as any); + + const active = trace.getActiveSpan(); + assert.strictEqual(active, span); + + context.disable(); + }); + + describe('Context', () => { + it('with should forward this, arguments and return value', () => { + function fnWithThis(this: string, a: string, b: number): string { + assert.strictEqual(this, 'that'); + assert.strictEqual(arguments.length, 2); + assert.strictEqual(a, 'one'); + assert.strictEqual(b, 2); + return 'done'; + } + + const res = context.with(ROOT_CONTEXT, fnWithThis, 'that', 'one', 2); + assert.strictEqual(res, 'done'); + + assert.strictEqual( + context.with(ROOT_CONTEXT, () => 3.14), + 3.14 + ); + }); + }); + + describe('GlobalTracerProvider', () => { + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.NONE, + }; + const dummySpan = new NonRecordingSpan(spanContext); + + beforeEach(() => { + context.disable(); + trace.disable(); + propagation.disable(); + }); + + it('should use the global tracer provider', () => { + api.trace.setGlobalTracerProvider(new TestTracerProvider()); + const tracer = api.trace.getTracerProvider().getTracer('name'); + const span = tracer.startSpan('test'); + assert.deepStrictEqual(span, dummySpan); + }); + + it('should set delegate only on success', () => { + assert.strictEqual( + api.trace.setGlobalTracerProvider(new TestTracerProvider()), + true + ); + assert.strictEqual( + api.trace.setGlobalTracerProvider(new NoopTracerProvider()), + false + ); + assert.ok(api.trace.getTracer('name') instanceof TestTracer); + }); + + class TestTracer extends NoopTracer { + override startSpan(name: string, options?: SpanOptions): Span { + return dummySpan; + } + } + + class TestTracerProvider extends NoopTracerProvider { + override getTracer(_name: string, version?: string) { + return new TestTracer(); + } + } + + describe('should use the global propagation', () => { + const testKey = Symbol('kTestKey'); + + interface Carrier { + context?: Context; + setter?: TextMapSetter; + } + + class TestTextMapPropagation implements TextMapPropagator { + inject( + context: Context, + carrier: Carrier, + setter: TextMapSetter + ): void { + carrier.context = context; + carrier.setter = setter; + } + + extract( + context: Context, + carrier: Carrier, + getter: TextMapGetter + ): Context { + return context.setValue(testKey, { + context, + carrier, + getter, + }); + } + + fields(): string[] { + return ['TestField']; + } + } + + it('inject', () => { + api.propagation.setGlobalPropagator(new TestTextMapPropagation()); + + const context = ROOT_CONTEXT.setValue(testKey, 15); + const carrier: Carrier = {}; + api.propagation.inject(context, carrier); + assert.strictEqual(carrier.context, context); + assert.strictEqual(carrier.setter, defaultTextMapSetter); + + const setter: TextMapSetter = { + set: () => {}, + }; + api.propagation.inject(context, carrier, setter); + assert.strictEqual(carrier.context, context); + assert.strictEqual(carrier.setter, setter); + }); + + it('extract', () => { + api.propagation.setGlobalPropagator(new TestTextMapPropagation()); + + const carrier: Carrier = {}; + let context = api.propagation.extract(ROOT_CONTEXT, carrier); + let data: any = context.getValue(testKey); + assert.ok(data != null); + assert.strictEqual(data.context, ROOT_CONTEXT); + assert.strictEqual(data.carrier, carrier); + assert.strictEqual(data.getter, defaultTextMapGetter); + + const getter: TextMapGetter = { + keys: () => [], + get: () => undefined, + }; + context = api.propagation.extract(ROOT_CONTEXT, carrier, getter); + data = context.getValue(testKey); + assert.ok(data != null); + assert.strictEqual(data.context, ROOT_CONTEXT); + assert.strictEqual(data.carrier, carrier); + assert.strictEqual(data.getter, getter); + }); + + it('fields', () => { + api.propagation.setGlobalPropagator(new TestTextMapPropagation()); + + const fields = api.propagation.fields(); + assert.deepStrictEqual(fields, ['TestField']); + }); + }); + }); + + describe('Global diag', () => { + it('initialization', () => { + const inst = DiagAPI.instance(); + + assert.deepStrictEqual(diag, inst); + }); + + diagLoggerFunctions.forEach(fName => { + it(`no argument logger ${fName} message doesn't throw`, () => { + //@ts-expect-error an undefined logger is not allowed + diag.setLogger(); + assert.doesNotThrow(() => { + diag[fName](`${fName} message`); + }); + }); + + it(`null logger ${fName} message doesn't throw`, () => { + diag.setLogger(null as any); + assert.doesNotThrow(() => { + diag[fName](`${fName} message`); + }); + }); + + it(`undefined logger ${fName} message doesn't throw`, () => { + diag.setLogger(undefined as any); + assert.doesNotThrow(() => { + diag[fName](`${fName} message`); + }); + }); + + it(`empty logger ${fName} message doesn't throw`, () => { + diag.setLogger({} as any); + assert.doesNotThrow(() => { + diag[fName](`${fName} message`); + }); + }); + }); + }); + + describe('Global metrics', () => { + it('should expose a meter provider via getMeterProvider', () => { + const meter = metrics.getMeterProvider(); + assert.ok(meter); + assert.strictEqual(typeof meter, 'object'); + }); + + describe('GlobalMeterProvider', () => { + const dummyMeter = new NoopMeter(); + + beforeEach(() => { + metrics.disable(); + }); + + it('should use the global meter provider', () => { + metrics.setGlobalMeterProvider(new TestMeterProvider()); + const meter = metrics.getMeterProvider().getMeter('name'); + assert.deepStrictEqual(meter, dummyMeter); + }); + + class TestMeterProvider extends NoopMeterProvider { + override getMeter() { + return dummyMeter; + } + } + }); + }); +}); diff --git a/api/test/common/baggage/Baggage.test.ts b/api/test/common/baggage/Baggage.test.ts new file mode 100644 index 0000000000..28401c0524 --- /dev/null +++ b/api/test/common/baggage/Baggage.test.ts @@ -0,0 +1,176 @@ +/* + * 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 { + context, + ROOT_CONTEXT, + propagation, + baggageEntryMetadataFromString, +} from '../../../src'; + +describe('Baggage', () => { + describe('create', () => { + it('should create an empty bag', () => { + const bag = propagation.createBaggage(); + + assert.deepStrictEqual(bag.getAllEntries(), []); + }); + + it('should create a bag with entries', () => { + const meta = baggageEntryMetadataFromString('opaque string'); + const bag = propagation.createBaggage({ + key1: { value: 'value1' }, + key2: { value: 'value2', metadata: meta }, + }); + + assert.deepStrictEqual(bag.getAllEntries(), [ + ['key1', { value: 'value1' }], + ['key2', { value: 'value2', metadata: meta }], + ]); + }); + }); + + describe('get', () => { + it('should not allow modification of returned entries', () => { + const bag = propagation + .createBaggage() + .setEntry('key', { value: 'value' }); + + const entry = bag.getEntry('key'); + assert.ok(entry); + entry.value = 'mutated'; + + assert.strictEqual(bag.getEntry('key')?.value, 'value'); + }); + }); + + describe('set', () => { + it('should create a new bag when an entry is added', () => { + const bag = propagation.createBaggage(); + + const bag2 = bag.setEntry('key', { value: 'value' }); + + assert.notStrictEqual(bag, bag2); + assert.deepStrictEqual(bag.getAllEntries(), []); + assert.deepStrictEqual(bag2.getAllEntries(), [ + ['key', { value: 'value' }], + ]); + }); + }); + + describe('remove', () => { + it('should create a new bag when an entry is removed', () => { + const bag = propagation.createBaggage({ + key: { value: 'value' }, + }); + + const bag2 = bag.removeEntry('key'); + + assert.deepStrictEqual(bag.getAllEntries(), [ + ['key', { value: 'value' }], + ]); + + assert.deepStrictEqual(bag2.getAllEntries(), []); + }); + + it('should create an empty bag multiple keys are removed', () => { + const bag = propagation.createBaggage({ + key: { value: 'value' }, + key1: { value: 'value1' }, + key2: { value: 'value2' }, + }); + + const bag2 = bag.removeEntries('key', 'key1'); + + assert.deepStrictEqual(bag.getAllEntries(), [ + ['key', { value: 'value' }], + ['key1', { value: 'value1' }], + ['key2', { value: 'value2' }], + ]); + + assert.deepStrictEqual(bag2.getAllEntries(), [ + ['key2', { value: 'value2' }], + ]); + }); + + it('should create an empty bag when it cleared', () => { + const bag = propagation.createBaggage({ + key: { value: 'value' }, + key1: { value: 'value1' }, + }); + + const bag2 = bag.clear(); + + assert.deepStrictEqual(bag.getAllEntries(), [ + ['key', { value: 'value' }], + ['key1', { value: 'value1' }], + ]); + + assert.deepStrictEqual(bag2.getAllEntries(), []); + }); + }); + + describe('context', () => { + it('should set and get a baggage from a context', () => { + const bag = propagation.createBaggage(); + + const ctx = propagation.setBaggage(ROOT_CONTEXT, bag); + + assert.strictEqual(bag, propagation.getBaggage(ctx)); + }); + + it('should get the current baggage', () => { + const entries = { + 'banana': {value: 'boats'} + }; + const bag = propagation.createBaggage(entries); + const ctx = propagation.setBaggage(ROOT_CONTEXT, bag); + + context.setGlobalContextManager({ active: () => ctx, disable: () => {} } as any); + + assert.strictEqual(bag, propagation.getActiveBaggage()); + + context.disable(); + }); + }); + + describe('metadata', () => { + it('should create an opaque object which returns the string unchanged', () => { + const meta = baggageEntryMetadataFromString('this is a string'); + + assert.strictEqual(meta.toString(), 'this is a string'); + }); + + it('should return an empty string if input is invalid', () => { + //@ts-expect-error only accepts string values + const meta = baggageEntryMetadataFromString(1); + + assert.strictEqual(meta.toString(), ''); + }); + + it('should retain metadata', () => { + const bag = propagation.createBaggage({ + key: { + value: 'value', + metadata: baggageEntryMetadataFromString('meta'), + }, + }); + + assert.deepStrictEqual(bag.getEntry('key')?.metadata?.toString(), 'meta'); + }); + }); +}); diff --git a/api/test/common/context/NoopContextManager.test.ts b/api/test/common/context/NoopContextManager.test.ts new file mode 100644 index 0000000000..8d257971b0 --- /dev/null +++ b/api/test/common/context/NoopContextManager.test.ts @@ -0,0 +1,138 @@ +/* + * 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 { createContextKey, ROOT_CONTEXT } from '../../../src/context/context'; +import { NoopContextManager } from '../../../src/context/NoopContextManager'; + +describe('NoopContextManager', () => { + let contextManager: NoopContextManager; + + describe('.enable()', () => { + it('should work', () => { + assert.doesNotThrow(() => { + contextManager = new NoopContextManager(); + assert( + contextManager.enable() === contextManager, + 'should return this' + ); + }); + }); + }); + + describe('.disable()', () => { + it('should work', () => { + assert.doesNotThrow(() => { + assert( + contextManager.disable() === contextManager, + 'should return this' + ); + }); + contextManager.enable(); + }); + }); + + describe('.with()', () => { + it('should run the callback (ROOT_CONTEXT as target)', done => { + contextManager.with(ROOT_CONTEXT, done); + }); + + it('should run the callback (object as target)', done => { + const key = createContextKey('test key 1'); + const test = ROOT_CONTEXT.setValue(key, 1); + contextManager.with(test, () => { + assert.strictEqual( + contextManager.active(), + ROOT_CONTEXT, + 'should not have context' + ); + return done(); + }); + }); + + it('should run the callback (when disabled)', done => { + contextManager.disable(); + contextManager.with(ROOT_CONTEXT, () => { + contextManager.enable(); + return done(); + }); + }); + + it('should forward this, arguments and return value', () => { + function fnWithThis(this: string, a: string, b: number): string { + assert.strictEqual(this, 'that'); + assert.strictEqual(arguments.length, 2); + assert.strictEqual(a, 'one'); + assert.strictEqual(b, 2); + return 'done'; + } + + const res = contextManager.with( + ROOT_CONTEXT, + fnWithThis, + 'that', + 'one', + 2 + ); + assert.strictEqual(res, 'done'); + + assert.strictEqual( + contextManager.with(ROOT_CONTEXT, () => 3.14), + 3.14 + ); + }); + }); + + describe('.active()', () => { + it('should always return ROOT_CONTEXT (when enabled)', () => { + assert.strictEqual( + contextManager.active(), + ROOT_CONTEXT, + 'should not have context' + ); + }); + + it('should always return ROOT_CONTEXT (when disabled)', () => { + contextManager.disable(); + assert.strictEqual( + contextManager.active(), + ROOT_CONTEXT, + 'should not have context' + ); + contextManager.enable(); + }); + }); + + describe('.bind()', () => { + it('should return the same target (when enabled)', () => { + const test = { a: 1 }; + assert.deepStrictEqual( + contextManager.bind(contextManager.active(), test), + test + ); + }); + + it('should return the same target (when disabled)', () => { + contextManager.disable(); + const test = { a: 1 }; + assert.deepStrictEqual( + contextManager.bind(contextManager.active(), test), + test + ); + contextManager.enable(); + }); + }); +}); diff --git a/api/test/common/diag/ComponentLogger.test.ts b/api/test/common/diag/ComponentLogger.test.ts new file mode 100644 index 0000000000..2a01cf2e98 --- /dev/null +++ b/api/test/common/diag/ComponentLogger.test.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 * as assert from 'assert'; +import * as sinon from 'sinon'; +import { diag, DiagLogger, DiagLogLevel } from '../../../src'; + +class SpyLogger implements DiagLogger { + debug() {} + error() {} + info() {} + warn() {} + verbose() {} +} + +const loggerFunctions = ['verbose', 'debug', 'info', 'warn', 'error']; + +describe('ComponentLogger', () => { + let logger: DiagLogger; + + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + logger = new SpyLogger(); + sandbox.spy(logger); + diag.setLogger(logger, DiagLogLevel.ALL); + // clean any messages up that might be there from the registration + sandbox.reset(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + loggerFunctions.forEach(name => { + const fName = name as keyof SpyLogger; + it(`should call global logger function "${name}" with namespace as first param`, () => { + const componentLogger = diag.createComponentLogger({ namespace: 'foo' }); + componentLogger[fName]('test'); + + assert.strictEqual((logger[fName] as sinon.SinonSpy).callCount, 1); + assert.deepStrictEqual((logger[fName] as sinon.SinonSpy).args[0], [ + 'foo', + 'test', + ]); + }); + }); +}); diff --git a/api/test/common/diag/consoleLogger.test.ts b/api/test/common/diag/consoleLogger.test.ts new file mode 100644 index 0000000000..781acd93b0 --- /dev/null +++ b/api/test/common/diag/consoleLogger.test.ts @@ -0,0 +1,220 @@ +/* + * 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. + */ + +/* eslint-disable no-console */ + +import * as assert from 'assert'; +import { DiagConsoleLogger } from '../../../src/diag/consoleLogger'; + +export const diagLoggerFunctions = [ + 'verbose', + 'debug', + 'info', + 'warn', + 'error', +] as const; + +const consoleFuncs = [ + 'debug', + 'info', + 'warn', + 'error', + 'log', + 'trace', +] as const; + +const expectedConsoleMap: { [n: string]: keyof Console } = { + error: 'error', + warn: 'warn', + info: 'info', + debug: 'debug', + verbose: 'trace', +}; + +describe('DiagConsoleLogger', () => { + const origConsole = console; + const orig: any = {}; + const calledArgs: any = {}; + + // Save original functions + consoleFuncs.forEach(fName => { + orig[fName] = console[fName]; + calledArgs[fName] = null; + }); + + let canMockConsole = true; + + try { + // eslint-disable-next-line no-global-assign + console = origConsole; + } catch (ex) { + // Not supported on CI pipeline (works locally) + canMockConsole = false; + } + + beforeEach(() => { + // mock Console + consoleFuncs.forEach(fName => { + console[fName] = (...args: unknown[]) => { + calledArgs[fName] = args; + }; + }); + }); + + afterEach(() => { + // restore + if (canMockConsole) { + try { + // eslint-disable-next-line no-global-assign + console = origConsole; + } catch (ex) { + // Not supported on CI pipeline + canMockConsole = false; + } + } + + consoleFuncs.forEach(fName => { + calledArgs[fName] = null; + console[fName] = orig[fName]; + }); + }); + + describe('constructor', () => { + diagLoggerFunctions.forEach(fName => { + it(`console logger should provide ${fName} function`, () => { + const consoleLogger: any = new DiagConsoleLogger(); + consoleLogger[fName](`${fName} called %s`, 'param1'); + assert.ok( + typeof consoleLogger[fName] === 'function', + `Must have a ${fName} function` + ); + }); + + it(`should log ${expectedConsoleMap[fName]} message with ${fName} call only`, () => { + const consoleLogger: any = new DiagConsoleLogger(); + consoleLogger[fName](`${fName} called %s`, 'param1'); + + // Make sure only gets logged once + let matches = 0; + consoleFuncs.forEach(cName => { + if (cName !== expectedConsoleMap[fName]) { + assert.deepStrictEqual(calledArgs[cName], null); + } else { + assert.deepStrictEqual(calledArgs[expectedConsoleMap[fName]], [ + `${fName} called %s`, + 'param1', + ]); + matches++; + } + }); + + assert.deepStrictEqual(calledArgs.log, null); + assert.strictEqual(matches, 1, 'should log at least once'); + }); + + consoleFuncs.forEach(cName => { + it(`should log ${fName} message even when console doesn't support ${cName} call before construction`, () => { + // @ts-expect-error removing a console property is not allowed by types + console[cName] = undefined; + const consoleLogger: any = new DiagConsoleLogger(); + consoleLogger[fName](`${fName} called %s`, 'param1'); + if (cName !== expectedConsoleMap[fName]) { + assert.deepStrictEqual(calledArgs[cName], null); + } else { + assert.deepStrictEqual(calledArgs.log, [ + `${fName} called %s`, + 'param1', + ]); + } + }); + + it(`should log ${fName} message even when console doesn't support ${cName} call after construction`, () => { + const consoleLogger: any = new DiagConsoleLogger(); + // @ts-expect-error removing a console property is not allowed by types + console[cName] = undefined; + consoleLogger[fName](`${fName} called %s`, 'param1'); + if (cName !== expectedConsoleMap[fName]) { + assert.deepStrictEqual(calledArgs[cName], null); + } else { + assert.deepStrictEqual(calledArgs.log, [ + `${fName} called %s`, + 'param1', + ]); + } + }); + }); + }); + + if (canMockConsole) { + diagLoggerFunctions.forEach(fName => { + const cName = expectedConsoleMap[fName]; + it(`should not throw even when console is not supported for ${fName} call`, () => { + // eslint-disable-next-line no-global-assign + (console as any) = undefined; + const consoleLogger: any = new DiagConsoleLogger(); + consoleLogger[fName](`${fName} called %s`, 'param1'); + assert.deepStrictEqual(calledArgs[cName], null); + assert.deepStrictEqual(calledArgs.log, null); + }); + + it(`should not throw even when console is disabled after construction for ${fName} call`, () => { + const consoleLogger: any = new DiagConsoleLogger(); + // eslint-disable-next-line no-global-assign + (console as any) = undefined; + consoleLogger[fName](`${fName} called %s`, 'param1'); + assert.deepStrictEqual(calledArgs[expectedConsoleMap[fName]], null); + assert.deepStrictEqual(calledArgs.log, null); + }); + + it(`should not throw even when console is invalid after construction for ${fName} call`, () => { + const invalidConsole = { + debug: 1, + warn: 2, + error: 3, + trace: 4, + info: 5, + log: 6, + }; + + const consoleLogger = new DiagConsoleLogger(); + // eslint-disable-next-line no-global-assign + (console as any) = invalidConsole; + consoleLogger[fName](`${fName} called %s`, 'param1'); + assert.deepStrictEqual(calledArgs[expectedConsoleMap[fName]], null); + assert.deepStrictEqual(calledArgs.log, null); + }); + + it(`should not throw even when console is invalid before construction for ${fName} call`, () => { + const invalidConsole = { + debug: 1, + warn: 2, + error: 3, + trace: 4, + info: 5, + log: 6, + }; + + // eslint-disable-next-line no-global-assign + (console as any) = invalidConsole; + const consoleLogger = new DiagConsoleLogger(); + consoleLogger[fName](`${fName} called %s`, 'param1'); + assert.deepStrictEqual(calledArgs[expectedConsoleMap[fName]], null); + assert.deepStrictEqual(calledArgs.log, null); + }); + }); + } + }); +}); diff --git a/api/test/common/diag/logLevel.test.ts b/api/test/common/diag/logLevel.test.ts new file mode 100644 index 0000000000..065e46d683 --- /dev/null +++ b/api/test/common/diag/logLevel.test.ts @@ -0,0 +1,236 @@ +/* + * 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 { diag } from '../../../src'; +import { createLogLevelDiagLogger } from '../../../src/diag/internal/logLevelLogger'; +import { createNoopDiagLogger } from '../../../src/diag/internal/noopLogger'; +import { DiagLogger, DiagLogLevel } from '../../../src/diag/types'; + +// Matches the previous Logger definition +const incompleteLoggerFuncs = ['debug', 'info', 'warn', 'error'] as const; + +export const diagLoggerFunctions = [ + 'verbose', + 'debug', + 'info', + 'warn', + 'error', +] as const; + +describe('LogLevelFilter DiagLogger', () => { + const calledArgs: any = { + error: null, + warn: null, + info: null, + debug: null, + verbose: null, + }; + + let dummyLogger: DiagLogger; + + /** Simulated Legacy logger */ + let incompleteLogger: DiagLogger; + + const restoreCallHistory = () => { + diagLoggerFunctions.forEach(fName => { + calledArgs[fName] = null; + }); + }; + + beforeEach(() => { + // Set no logger so that sinon doesn't complain about TypeError: Attempted to wrap xxxx which is already wrapped + diag.disable(); + + // mock + dummyLogger = {} as DiagLogger; + diagLoggerFunctions.forEach(fName => { + dummyLogger[fName] = (...args: unknown[]) => { + calledArgs[fName] = args; + }; + }); + + incompleteLogger = {} as DiagLogger; + incompleteLoggerFuncs.forEach(fName => { + incompleteLogger[fName] = (...args: unknown[]) => { + calledArgs[fName] = args; + }; + }); + }); + + afterEach(() => { + restoreCallHistory(); + }); + + const levelMap: Array<{ + message: string; + level: DiagLogLevel; + ignoreFuncs: Array; + }> = [ + { message: 'ALL', level: DiagLogLevel.ALL, ignoreFuncs: [] }, + { message: 'greater than ALL', level: 32768, ignoreFuncs: [] }, + { message: 'VERBOSE', level: DiagLogLevel.VERBOSE, ignoreFuncs: [] }, + { message: 'DEBUG', level: DiagLogLevel.DEBUG, ignoreFuncs: ['verbose'] }, + { + message: 'INFO', + level: DiagLogLevel.INFO, + ignoreFuncs: ['verbose', 'debug'], + }, + { + message: 'WARN', + level: DiagLogLevel.WARN, + ignoreFuncs: ['verbose', 'debug', 'info'], + }, + { + message: 'ERROR', + level: DiagLogLevel.ERROR, + ignoreFuncs: ['verbose', 'debug', 'info', 'warn'], + }, + { + message: 'between ERROR and NONE', + level: 1, + ignoreFuncs: ['verbose', 'debug', 'info', 'warn', 'error'], + }, + { + message: 'NONE', + level: DiagLogLevel.NONE, + ignoreFuncs: ['verbose', 'debug', 'info', 'warn', 'error'], + }, + { + message: 'less than NONE', + level: -1000, + ignoreFuncs: ['verbose', 'debug', 'info', 'warn', 'error'], + }, + ]; + + levelMap.forEach(map => { + describe(`with ${map.message} log level`, () => { + diagLoggerFunctions.forEach(fName => { + describe(`calling ${fName} message`, () => { + it('should log', () => { + const testLogger = createLogLevelDiagLogger(map.level, dummyLogger); + testLogger[fName](`${fName} called %s`, 'param1'); + diagLoggerFunctions.forEach(lName => { + if (fName === lName && map.ignoreFuncs.indexOf(lName) === -1) { + assert.deepStrictEqual(calledArgs[lName], [ + `${fName} called %s`, + 'param1', + ]); + } else { + assert.strictEqual(calledArgs[lName], null); + } + }); + }); + + it('should be noop for null with explicit noop Logger log', () => { + const testLogger = createLogLevelDiagLogger( + map.level, + createNoopDiagLogger() + ); + testLogger[fName](`${fName} called %s`, 'param1'); + diagLoggerFunctions.forEach(lName => { + assert.strictEqual(calledArgs[lName], null); + }); + }); + + it('should be noop and not throw for null and no default Logger log', () => { + // @ts-expect-error null logger is not allowed + const testLogger = createLogLevelDiagLogger(map.level, null); + testLogger[fName](`${fName} called %s`, 'param1'); + diagLoggerFunctions.forEach(lName => { + assert.strictEqual(calledArgs[lName], null); + }); + }); + + it('should be noop and not throw for undefined and no default Logger log', () => { + // @ts-expect-error undefined logger is not allowed + const testLogger = createLogLevelDiagLogger(map.level, undefined); + testLogger[fName](`${fName} called %s`, 'param1'); + diagLoggerFunctions.forEach(lName => { + assert.strictEqual(calledArgs[lName], null); + }); + }); + + levelMap.forEach(masterLevelMap => { + describe(`when diag logger is set to ${masterLevelMap.message}`, () => { + it('diag.setLogger level is ignored when using a specific logger', () => { + diag.setLogger(dummyLogger, masterLevelMap.level); + + const testLogger = createLogLevelDiagLogger( + map.level, + dummyLogger + ); + restoreCallHistory(); + testLogger[fName](`${fName} called %s`, 'param1'); + diagLoggerFunctions.forEach(lName => { + if ( + fName === lName && + map.ignoreFuncs.indexOf(lName) === -1 + ) { + assert.deepStrictEqual(calledArgs[lName], [ + `${fName} called %s`, + 'param1', + ]); + } else { + assert.strictEqual(calledArgs[lName], null); + } + }); + }); + }); + }); + + it('diag.setLogger level and logger should log', () => { + diag.setLogger(dummyLogger, map.level); + restoreCallHistory(); + diag[fName](`${fName} called %s`, 'param1'); + diagLoggerFunctions.forEach(lName => { + if (fName === lName && map.ignoreFuncs.indexOf(lName) === -1) { + assert.deepStrictEqual(calledArgs[lName], [ + `${fName} called %s`, + 'param1', + ]); + } else { + assert.strictEqual(calledArgs[lName], null); + } + }); + }); + + it('should not throw with an invalid DiagLogger', () => { + const invalidLogger = { + debug: 1, + warn: 2, + error: 3, + trace: 4, + info: 5, + log: 6, + }; + + const testLogger = createLogLevelDiagLogger( + map.level, + invalidLogger as any + ); + restoreCallHistory(); + + testLogger[fName](`${fName} called %s`, 'param1'); + diagLoggerFunctions.forEach(lName => { + assert.strictEqual(calledArgs[lName], null); + }); + }); + }); + }); + }); + }); +}); diff --git a/api/test/common/diag/logger.test.ts b/api/test/common/diag/logger.test.ts new file mode 100644 index 0000000000..1b6f0d57ba --- /dev/null +++ b/api/test/common/diag/logger.test.ts @@ -0,0 +1,126 @@ +/* + * 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 sinon = require('sinon'); +import { diag, DiagLogLevel } from '../../../src'; +import { createNoopDiagLogger } from '../../../src/diag/internal/noopLogger'; +import { DiagLogger } from '../../../src/diag/types'; + +export const diagLoggerFunctions = [ + 'verbose', + 'debug', + 'info', + 'warn', + 'error', +] as const; +describe('DiagLogger functions', () => { + const calledArgs: any = { + error: null, + warn: null, + info: null, + debug: null, + verbose: null, + }; + + let dummyLogger: DiagLogger; + + const restoreCallHistory = () => { + diagLoggerFunctions.forEach(fName => { + calledArgs[fName] = null; + }); + }; + + beforeEach(() => { + // mock + dummyLogger = {} as DiagLogger; + diagLoggerFunctions.forEach(fName => { + dummyLogger[fName] = (...args: unknown[]) => { + calledArgs[fName] = args; + }; + }); + }); + + afterEach(() => { + restoreCallHistory(); + diag.disable(); + }); + + describe('constructor', () => { + diagLoggerFunctions.forEach(fName => { + it(`should log with ${fName} message`, () => { + const testLogger = dummyLogger; + testLogger[fName](`${fName} called %s`, 'param1'); + diagLoggerFunctions.forEach(lName => { + if (fName === lName) { + assert.deepStrictEqual(calledArgs[fName], [ + `${fName} called %s`, + 'param1', + ]); + } else { + assert.strictEqual(calledArgs[lName], null); + } + }); + }); + + it(`diag should log with ${fName} message`, () => { + diag.setLogger(dummyLogger, DiagLogLevel.ALL); + restoreCallHistory(); + diag[fName](`${fName} called %s`, 'param1'); + diagLoggerFunctions.forEach(lName => { + if (fName === lName) { + assert.deepStrictEqual(calledArgs[fName], [ + `${fName} called %s`, + 'param1', + ]); + } else { + assert.strictEqual(calledArgs[lName], null); + } + }); + }); + + it(`NoopLogger should implement all functions and not throw when ${fName} called`, () => { + const testLogger = createNoopDiagLogger(); + + assert.ok(typeof testLogger[fName], 'function'); + testLogger[fName](`${fName} called %s`, 'param1'); + }); + }); + }); + + describe('diag is used as the diag logger in setLogger', () => { + it('should not throw', () => { + diag.setLogger(diag); + }); + + it('should use the previously registered logger to log the error', () => { + const error = sinon.stub(); + diag.setLogger({ + verbose: () => {}, + debug: () => {}, + info: () => {}, + warn: () => {}, + error, + }); + + sinon.assert.notCalled(error); + + diag.setLogger(diag); + + sinon.assert.calledOnce(error); + }); + }); +}); diff --git a/api/test/common/internal/global.test.ts b/api/test/common/internal/global.test.ts new file mode 100644 index 0000000000..291f707233 --- /dev/null +++ b/api/test/common/internal/global.test.ts @@ -0,0 +1,155 @@ +/* + * 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 { getGlobal } from '../../../src/internal/global-utils'; +import { _globalThis } from '../../../src/platform'; +import { NoopContextManager } from '../../../src/context/NoopContextManager'; +import { DiagLogLevel } from '../../../src/diag/types'; +import sinon = require('sinon'); + +const api1 = require('../../../src') as typeof import('../../../src'); + +// clear cache and load a second instance of the api +for (const key of Object.keys(require.cache)) { + delete require.cache[key]; +} +const api2 = require('../../../src') as typeof import('../../../src'); + +// This will need to be changed manually on major version changes. +// It is intentionally not autogenerated to ensure the author of the change is aware of what they are doing. +const GLOBAL_API_SYMBOL_KEY = 'opentelemetry.js.api.1'; + +const getMockLogger = () => ({ + verbose: sinon.spy(), + debug: sinon.spy(), + info: sinon.spy(), + warn: sinon.spy(), + error: sinon.spy(), +}); + +describe('Global Utils', () => { + // prove they are separate instances + assert.notEqual(api1, api2); + // that return separate noop instances to start + assert.notStrictEqual( + api1.context['_getContextManager'](), + api2.context['_getContextManager']() + ); + + beforeEach(() => { + api1.context.disable(); + api1.propagation.disable(); + api1.trace.disable(); + api1.diag.disable(); + // @ts-expect-error we are modifying internals for testing purposes here + delete _globalThis[Symbol.for(GLOBAL_API_SYMBOL_KEY)]; + }); + + it('should change the global context manager', () => { + const original = api1.context['_getContextManager'](); + const newContextManager = new NoopContextManager(); + api1.context.setGlobalContextManager(newContextManager); + assert.notStrictEqual(api1.context['_getContextManager'](), original); + assert.strictEqual(api1.context['_getContextManager'](), newContextManager); + }); + + it('should load an instance from one which was set in the other', () => { + api1.context.setGlobalContextManager(new NoopContextManager()); + assert.strictEqual( + api1.context['_getContextManager'](), + api2.context['_getContextManager']() + ); + }); + + it('should disable both if one is disabled', () => { + const manager = new NoopContextManager(); + api1.context.setGlobalContextManager(manager); + + assert.strictEqual(manager, api1.context['_getContextManager']()); + api2.context.disable(); + assert.notStrictEqual(manager, api1.context['_getContextManager']()); + }); + + it('should return the module NoOp implementation if the version is a mismatch', () => { + const newContextManager = new NoopContextManager(); + api1.context.setGlobalContextManager(newContextManager); + + // ensure new context manager is returned + assert.strictEqual(api1.context['_getContextManager'](), newContextManager); + + const globalInstance = getGlobal('context'); + assert.ok(globalInstance); + // @ts-expect-error we are modifying internals for testing purposes here + _globalThis[Symbol.for(GLOBAL_API_SYMBOL_KEY)].version = '0.0.1'; + + // ensure new context manager is not returned because version above is incompatible + assert.notStrictEqual( + api1.context['_getContextManager'](), + newContextManager + ); + }); + + it('should debug log registrations', () => { + const logger = getMockLogger(); + api1.diag.setLogger(logger, DiagLogLevel.DEBUG); + + const newContextManager = new NoopContextManager(); + api1.context.setGlobalContextManager(newContextManager); + + sinon.assert.calledWith(logger.debug, sinon.match(/global for context/)); + sinon.assert.calledWith(logger.debug, sinon.match(/global for diag/)); + sinon.assert.calledTwice(logger.debug); + }); + + it('should log an error if there is a duplicate registration', () => { + const logger = getMockLogger(); + api1.diag.setLogger(logger); + + api1.context.setGlobalContextManager(new NoopContextManager()); + api1.context.setGlobalContextManager(new NoopContextManager()); + + sinon.assert.calledOnce(logger.error); + assert.strictEqual(logger.error.firstCall.args.length, 1); + assert.ok( + logger.error.firstCall.args[0].startsWith( + 'Error: @opentelemetry/api: Attempted duplicate registration of API: context' + ) + ); + }); + + it('should allow duplicate registration of the diag logger', () => { + const logger1 = getMockLogger(); + const logger2 = getMockLogger(); + + api1.diag.setLogger(logger1); + api1.diag.setLogger(logger2); + + const MSG = '__log message__'; + api1.diag.info(MSG); + + sinon.assert.notCalled(logger1.error); + sinon.assert.notCalled(logger1.info); + sinon.assert.calledOnce(logger1.warn); + sinon.assert.calledWith(logger1.warn, sinon.match(/will be overwritten/i)); + + sinon.assert.notCalled(logger2.error); + sinon.assert.calledOnce(logger2.warn); + sinon.assert.calledWith(logger2.warn, sinon.match(/will overwrite/i)); + sinon.assert.calledOnce(logger2.info); + sinon.assert.calledWith(logger2.info, MSG); + }); +}); diff --git a/api/test/common/internal/semver.test.ts b/api/test/common/internal/semver.test.ts new file mode 100644 index 0000000000..acd164b542 --- /dev/null +++ b/api/test/common/internal/semver.test.ts @@ -0,0 +1,148 @@ +/* + * 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 { + isCompatible, + _makeCompatibilityCheck, +} from '../../../src/internal/semver'; +import { VERSION } from '../../../src/version'; + +describe('semver', () => { + it('should be compatible if versions are equal', () => { + assert.ok(isCompatible(VERSION)); + }); + + it('returns false if own version cannot be parsed', () => { + const check = _makeCompatibilityCheck('this is not semver'); + assert.ok(!check('1.0.0')); + }); + + it('incompatible if other version cannot be parsed', () => { + const check = _makeCompatibilityCheck('0.1.2'); + assert.ok(!check('this is not semver')); + }); + + describe('>= 1.0.0', () => { + const globalVersion = '5.5.5'; + const vers: [string, boolean][] = [ + // same major/minor run should be compatible + ['5.5.5', true], + ['5.5.6', true], + ['5.5.4', true], + + // prerelease version should not be compatible + ['5.5.5-rc.0', false], + + // if our version has a minor version increase, we may try to call methods which don't exist on the global + ['5.6.5', false], + ['5.6.6', false], + ['5.6.4', false], + + // if the global version is ahead of us by a minor revision, it has at least the methods we know about + ['5.4.5', true], + ['5.4.6', true], + ['5.4.4', true], + + // major version mismatch is always incompatible + ['6.5.5', false], + ['6.5.6', false], + ['6.5.4', false], + ['6.6.5', false], + ['6.6.6', false], + ['6.6.4', false], + ['6.4.5', false], + ['6.4.6', false], + ['6.4.4', false], + ['4.5.5', false], + ['4.5.6', false], + ['4.5.4', false], + ['4.6.5', false], + ['4.6.6', false], + ['4.6.4', false], + ['4.4.5', false], + ['4.4.6', false], + ['4.4.4', false], + ]; + + test(globalVersion, vers); + }); + + describe('< 1.0.0', () => { + const globalVersion = '0.5.5'; + const vers: [string, boolean][] = [ + // same minor/patch should be compatible + ['0.5.5', true], + + // prerelease version should not be compatible + ['0.5.5-rc.0', false], + + // if our version has a patch version increase, we may try to call methods which don't exist on the global + ['0.5.6', false], + + // if the global version is ahead of us by a patch revision, it has at least the methods we know about + ['0.5.4', true], + + // minor version mismatch is always incompatible + ['0.6.5', false], + ['0.6.6', false], + ['0.6.4', false], + ['0.4.5', false], + ['0.4.6', false], + ['0.4.4', false], + + // major version mismatch is always incompatible + ['1.5.5', false], + ['1.5.6', false], + ['1.5.4', false], + ['1.6.5', false], + ['1.6.6', false], + ['1.6.4', false], + ['1.4.5', false], + ['1.4.6', false], + ['1.4.4', false], + ]; + + test(globalVersion, vers); + }); + + describe('global version is prerelease', () => { + const globalVersion = '1.0.0-rc.3'; + const vers: [string, boolean][] = [ + // must match exactly + ['1.0.0', false], + ['1.0.0-rc.2', false], + ['1.0.0-rc.4', false], + + ['1.0.0-rc.3', true], + ]; + + test(globalVersion, vers); + }); +}); + +function test(globalVersion: string, vers: [string, boolean][]) { + describe(`global version is ${globalVersion}`, () => { + for (const [version, compatible] of vers) { + it(`API version ${version} ${ + compatible ? 'should' : 'should not' + } be able to access global`, () => { + const check = _makeCompatibilityCheck(version); + assert.strictEqual(check(globalVersion), compatible); + }); + } + }); +} diff --git a/api/test/common/internal/version.test.ts b/api/test/common/internal/version.test.ts new file mode 100644 index 0000000000..cfffeb20fa --- /dev/null +++ b/api/test/common/internal/version.test.ts @@ -0,0 +1,30 @@ +/* + * 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 { VERSION } from '../../../src/version'; + +describe('version', () => { + it('should have generated VERSION.ts', () => { + const pjson = require('../../../package.json'); + assert.strictEqual(pjson.version, VERSION); + }); + + it('prerelease tag versions are banned', () => { + // see https://github.com/open-telemetry/opentelemetry-js-api/issues/74 + assert.ok(VERSION.match(/^\d+\.\d+\.\d+$/)); + }); +}); diff --git a/api/test/common/metrics/Metric.test.ts b/api/test/common/metrics/Metric.test.ts new file mode 100644 index 0000000000..d7111d01f7 --- /dev/null +++ b/api/test/common/metrics/Metric.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { Counter, UpDownCounter, Histogram } from '../../../src'; + +describe('Metric', () => { + describe('Counter', () =>{ + it('enable not to define any type', () => { + const counter: Counter = { + add(_value: number, _attribute: unknown) {} + }; + counter.add(1, { 'some-attribute': 'value' }); + }); + + it('enable to use with type', () => { + type Attributes = { + 'some-attribute': string + }; + const counter: Counter = { + add(_value: number, _attribute: Attributes) {} + }; + counter.add(1, { 'some-attribute': 'value' }); + }); + + it('disable wrong attributes by typing', () => { + type Attributes = { + 'some-attribute': string + }; + const counter: Counter = { + add(_value: number, _attribute: Attributes) {} + }; + // @ts-expect-error Expacting the type of Attributes + counter.add(1, { 'another-attribute': 'value' }); + }); + }); + + describe('UpDownCounter', () =>{ + it('enable not to define any type', () => { + const counter: UpDownCounter = { + add(_value: number, _attribute: unknown) {} + }; + counter.add(1, { 'some-attribute': 'value' }); + }); + + it('enable to use with type', () => { + type Attributes = { + 'some-attribute': string + }; + const counter: UpDownCounter = { + add(_value: number, _attribute: Attributes) {} + }; + counter.add(1, { 'some-attribute': 'value' }); + }); + + it('disable wrong attributes by typing', () => { + type Attributes = { + 'some-attribute': string + }; + const counter: UpDownCounter = { + add(_value: number, _attribute: Attributes) {} + }; + // @ts-expect-error Expacting the type of Attributes + counter.add(1, { 'another-attribute': 'value' }); + }); + }); + + describe('Histogram', () =>{ + it('enable not to define any type', () => { + const counter: Histogram = { + record(_value: number, _attribute: unknown) {} + }; + counter.record(1, { 'some-attribute': 'value' }); + }); + + it('enable to use with type', () => { + type Attributes = { + 'some-attribute': string + }; + const counter: Histogram = { + record(_value: number, _attribute: Attributes) {} + }; + counter.record(1, { 'some-attribute': 'value' }); + }); + + it('disable wrong attributes by typing', () => { + type Attributes = { + 'some-attribute': string + }; + const counter: Histogram = { + record(_value: number, _attribute: Attributes) {} + }; + // @ts-expect-error Expacting the type of Attributes + counter.record(1, { 'another-attribute': 'value' }); + }); + }); +}); diff --git a/experimental/packages/opentelemetry-api-metrics/test/noop-implementations/noop-meter.test.ts b/api/test/common/noop-implementations/noop-meter.test.ts similarity index 94% rename from experimental/packages/opentelemetry-api-metrics/test/noop-implementations/noop-meter.test.ts rename to api/test/common/noop-implementations/noop-meter.test.ts index 82f0e8b6be..951bdb80e7 100644 --- a/experimental/packages/opentelemetry-api-metrics/test/noop-implementations/noop-meter.test.ts +++ b/api/test/common/noop-implementations/noop-meter.test.ts @@ -17,14 +17,15 @@ import * as assert from 'assert'; import { NoopMeter, - NoopMeterProvider, NOOP_COUNTER_METRIC, NOOP_HISTOGRAM_METRIC, NOOP_OBSERVABLE_COUNTER_METRIC, NOOP_OBSERVABLE_GAUGE_METRIC, NOOP_OBSERVABLE_UP_DOWN_COUNTER_METRIC, NOOP_UP_DOWN_COUNTER_METRIC, -} from '../../src'; + createNoopMeter, +} from '../../../src/metrics/NoopMeter'; +import { NoopMeterProvider } from '../../../src/metrics/NoopMeterProvider'; const attributes = {}; const options = { @@ -133,3 +134,9 @@ describe('NoopMeter', () => { meter.removeBatchObservableCallback(() => {}, []); }); }); + +describe('createNoopMeter', () => { + it('should return NoopMeter', () => { + assert.ok(createNoopMeter() instanceof NoopMeter); + }); +}); diff --git a/api/test/common/noop-implementations/noop-span.test.ts b/api/test/common/noop-implementations/noop-span.test.ts new file mode 100644 index 0000000000..5bc341f31a --- /dev/null +++ b/api/test/common/noop-implementations/noop-span.test.ts @@ -0,0 +1,54 @@ +/* + * 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 { + SpanStatusCode, + INVALID_SPANID, + INVALID_TRACEID, + TraceFlags, +} from '../../../src'; +import { NonRecordingSpan } from '../../../src/trace/NonRecordingSpan'; + +describe('NonRecordingSpan', () => { + it('do not crash', () => { + const span = new NonRecordingSpan(); + span.setAttribute('my_string_attribute', 'foo'); + span.setAttribute('my_number_attribute', 123); + span.setAttribute('my_boolean_attribute', false); + span.setAttribute('my_obj_attribute', { a: true }); + span.setAttribute('my_sym_attribute', Symbol('a')); + span.setAttributes({ + my_string_attribute: 'foo', + my_number_attribute: 123, + }); + + span.addEvent('sent'); + span.addEvent('sent', { id: '42', key: 'value' }); + + span.setStatus({ code: SpanStatusCode.ERROR }); + + span.updateName('my-span'); + + assert.ok(!span.isRecording()); + assert.deepStrictEqual(span.spanContext(), { + traceId: INVALID_TRACEID, + spanId: INVALID_SPANID, + traceFlags: TraceFlags.NONE, + }); + span.end(); + }); +}); diff --git a/api/test/common/noop-implementations/noop-tracer-provider.test.ts b/api/test/common/noop-implementations/noop-tracer-provider.test.ts new file mode 100644 index 0000000000..d9eda44ee9 --- /dev/null +++ b/api/test/common/noop-implementations/noop-tracer-provider.test.ts @@ -0,0 +1,31 @@ +/* + * 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 { NoopTracer } from '../../../src/trace/NoopTracer'; +import { NoopTracerProvider } from '../../../src/trace/NoopTracerProvider'; + +describe('NoopTracerProvider', () => { + it('should not crash', () => { + const tracerProvider = new NoopTracerProvider(); + + assert.ok(tracerProvider.getTracer('tracer-name') instanceof NoopTracer); + assert.ok(tracerProvider.getTracer('tracer-name', 'v1') instanceof NoopTracer); + assert.ok(tracerProvider.getTracer('tracer-name', 'v1', { + schemaUrl: 'https://opentelemetry.io/schemas/1.7.0' + }) instanceof NoopTracer); + }); +}); diff --git a/api/test/common/noop-implementations/noop-tracer.test.ts b/api/test/common/noop-implementations/noop-tracer.test.ts new file mode 100644 index 0000000000..429f6d481d --- /dev/null +++ b/api/test/common/noop-implementations/noop-tracer.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 { + context, + Span, + SpanContext, + SpanKind, + trace, + TraceFlags, +} from '../../../src'; +import { NonRecordingSpan } from '../../../src/trace/NonRecordingSpan'; +import { NoopTracer } from '../../../src/trace/NoopTracer'; + +describe('NoopTracer', () => { + it('should not crash', () => { + const tracer = new NoopTracer(); + + assert.ok(tracer.startSpan('span-name') instanceof NonRecordingSpan); + assert.ok( + tracer.startSpan('span-name1', { kind: SpanKind.CLIENT }) instanceof + NonRecordingSpan + ); + assert.ok( + tracer.startSpan('span-name2', { kind: SpanKind.CLIENT }) instanceof + NonRecordingSpan + ); + }); + + it('should propagate valid spanContext on the span (from context)', () => { + const tracer = new NoopTracer(); + const parent: SpanContext = { + traceId: 'd4cda95b652f4a1592b449dd92ffda3b', + spanId: '6e0c63ffe4e34c42', + traceFlags: TraceFlags.NONE, + }; + const span = tracer.startSpan( + 'test-1', + {}, + trace.setSpanContext(context.active(), parent) + ); + assert(span.spanContext().traceId === parent.traceId); + assert(span.spanContext().spanId === parent.spanId); + assert(span.spanContext().traceFlags === parent.traceFlags); + }); + + it('should accept 2 to 4 args and start an active span', () => { + const tracer = new NoopTracer(); + const name = 'span-name'; + const fn = (span: Span) => { + try { + return 1; + } finally { + span.end(); + } + }; + const opts = { attributes: { foo: 'bar' } }; + + assert.strictEqual((tracer as any).startActiveSpan(name), undefined); + + assert.strictEqual(tracer.startActiveSpan(name, fn), 1); + + assert.strictEqual(tracer.startActiveSpan(name, opts, fn), 1); + + assert.strictEqual( + tracer.startActiveSpan(name, opts, context.active(), fn), + 1 + ); + }); +}); diff --git a/api/test/common/proxy-implementations/proxy-tracer.test.ts b/api/test/common/proxy-implementations/proxy-tracer.test.ts new file mode 100644 index 0000000000..9bf7e94409 --- /dev/null +++ b/api/test/common/proxy-implementations/proxy-tracer.test.ts @@ -0,0 +1,189 @@ +/* + * 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 { + context, + ProxyTracer, + ProxyTracerProvider, + ROOT_CONTEXT, + Span, + SpanKind, + SpanOptions, + Tracer, + TracerProvider, +} from '../../../src'; +import { NonRecordingSpan } from '../../../src/trace/NonRecordingSpan'; +import { NoopTracer } from '../../../src/trace/NoopTracer'; + +describe('ProxyTracer', () => { + let provider: ProxyTracerProvider; + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + provider = new ProxyTracerProvider(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + 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.ok(tracer.startSpan('span-name') instanceof NonRecordingSpan); + assert.ok( + tracer.startSpan('span-name1', { kind: SpanKind.CLIENT }) instanceof + NonRecordingSpan + ); + assert.ok( + tracer.startSpan('span-name2', { kind: SpanKind.CLIENT }) instanceof + NonRecordingSpan + ); + }); + }); + + describe('when delegate is set before getTracer', () => { + let delegate: TracerProvider; + let getTracerStub: sinon.SinonStub; + + beforeEach(() => { + getTracerStub = sandbox.stub().returns(new NoopTracer()); + delegate = { + getTracer: getTracerStub, + }; + provider.setDelegate(delegate); + }); + + 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.deepStrictEqual(getTracerStub.firstCall.args, [ + 'test', + 'v0', + undefined, + ]); + }); + + it('should return tracers directly from the delegate with schema url', () => { + const tracer = provider.getTracer('test', 'v0', { + schemaUrl: 'https://opentelemetry.io/schemas/1.7.0', + }); + + sandbox.assert.calledOnce(getTracerStub); + assert.strictEqual(getTracerStub.firstCall.returnValue, tracer); + assert.deepStrictEqual(getTracerStub.firstCall.args, [ + 'test', + 'v0', + { schemaUrl: 'https://opentelemetry.io/schemas/1.7.0' }, + ]); + }); + }); + + describe('when delegate is set after getTracer', () => { + let tracer: Tracer; + let delegate: TracerProvider; + let delegateSpan: Span; + let delegateTracer: Tracer; + + beforeEach(() => { + delegateSpan = new NonRecordingSpan(); + delegateTracer = { + startSpan() { + return delegateSpan; + }, + + startActiveSpan() { + // stubbed + }, + }; + + 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); + }); + + it('should create active spans using the delegate tracer', () => { + // sinon types are broken with overloads, hence the any + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/36436 + const startActiveSpanStub = sinon.stub( + delegateTracer, + 'startActiveSpan' + ); + + const name = 'span-name'; + const fn = (span: Span) => { + try { + return 1; + } finally { + span.end(); + } + }; + const opts = { attributes: { foo: 'bar' } }; + const ctx = context.active(); + + startActiveSpanStub.withArgs(name, fn).returns(1); + startActiveSpanStub.withArgs(name, opts, fn).returns(2); + startActiveSpanStub.withArgs(name, opts, ctx, fn).returns(3); + + assert.strictEqual(tracer.startActiveSpan(name, fn), 1); + assert.strictEqual(tracer.startActiveSpan(name, opts, fn), 2); + assert.strictEqual(tracer.startActiveSpan(name, opts, ctx, fn), 3); + }); + + it('should pass original arguments to DelegateTracer#startSpan', () => { + const startSpanStub = sandbox.stub(delegateTracer, 'startSpan'); + + const name = 'name1'; + const options: SpanOptions = {}; + const ctx = ROOT_CONTEXT.setValue(Symbol('test'), 1); + tracer.startSpan(name, options, ctx); + + // Assert the proxy tracer has the full API of the NoopTracer + assert.strictEqual( + NoopTracer.prototype.startSpan.length, + ProxyTracer.prototype.startSpan.length + ); + assert.deepStrictEqual(Object.getOwnPropertyNames(NoopTracer.prototype), [ + 'constructor', + 'startSpan', + 'startActiveSpan', + ]); + sandbox.assert.calledOnceWithExactly(startSpanStub, name, options, ctx); + }); + }); +}); diff --git a/api/test/common/trace/spancontext-utils.test.ts b/api/test/common/trace/spancontext-utils.test.ts new file mode 100644 index 0000000000..e2d87f807d --- /dev/null +++ b/api/test/common/trace/spancontext-utils.test.ts @@ -0,0 +1,70 @@ +/* + * 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 context from '../../../src/trace/spancontext-utils'; +import { INVALID_SPANID, INVALID_TRACEID, TraceFlags } from '../../../src'; + +describe('spancontext-utils', () => { + it('should return true for valid spancontext', () => { + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.NONE, + }; + assert.ok(context.isSpanContextValid(spanContext)); + }); + + it('should return false when traceId is invalid', () => { + const spanContext = { + traceId: INVALID_TRACEID, + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.NONE, + }; + assert.ok(!context.isSpanContextValid(spanContext)); + }); + + it('should return false when spanId is invalid', () => { + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: INVALID_SPANID, + traceFlags: TraceFlags.NONE, + }; + assert.ok(!context.isSpanContextValid(spanContext)); + }); + + it('should return false when traceId & spanId is invalid', () => { + const spanContext = { + traceId: INVALID_TRACEID, + spanId: INVALID_SPANID, + traceFlags: TraceFlags.NONE, + }; + assert.ok(!context.isSpanContextValid(spanContext)); + }); + + it('should wrap a SpanContext in a non-recording span', () => { + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.NONE, + }; + + const span = context.wrapSpanContext(spanContext); + + assert.deepStrictEqual(span.spanContext(), spanContext); + assert.strictEqual(span.isRecording(), false); + }); +}); diff --git a/api/test/common/trace/tracestate-validators.test.ts b/api/test/common/trace/tracestate-validators.test.ts new file mode 100644 index 0000000000..15f49434eb --- /dev/null +++ b/api/test/common/trace/tracestate-validators.test.ts @@ -0,0 +1,92 @@ +/* + * 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 { validateKey, validateValue } from '../../../src/trace/internal/tracestate-validators'; + +describe('validators', () => { + describe('validateKey', () => { + const validKeysTestCases = [ + 'abcdefghijklmnopqrstuvwxyz0123456789-_*/', + 'baz-', + 'baz_', + 'baz*', + 'baz*bar', + 'baz/', + 'tracestate', + 'fw529a3039@dt', + '6cab5bb-29a@dt', + ]; + validKeysTestCases.forEach(testCase => + it(`returns true when key contains valid chars ${testCase}`, () => { + assert.ok(validateKey(testCase), `${testCase} should be valid`); + }) + ); + + const invalidKeysTestCases = [ + '1_key', + 'kEy_1', + 'k'.repeat(257), + 'key,', + 'TrAcEsTaTE', + 'TRACESTATE', + '', + '6num', + ]; + invalidKeysTestCases.forEach(testCase => + it(`returns true when key contains invalid chars ${testCase}`, () => { + assert.ok(!validateKey(testCase), `${testCase} should be invalid`); + }) + ); + }); + + describe('validateValue', () => { + const validValuesTestCases = [ + 'first second', + 'baz*', + 'baz$', + 'baz@', + 'first-second', + 'baz~bar', + 'test-v1:120', + '-second', + 'first.second', + 'TrAcEsTaTE', + 'TRACESTATE', + ]; + validValuesTestCases.forEach(testCase => + it(`returns true when value contains valid chars ${testCase}`, () => { + assert.ok(validateValue(testCase)); + }) + ); + + const invalidValuesTestCases = [ + 'my_value=5', + 'first,second', + 'first ', + 'k'.repeat(257), + ',baz', + 'baz,', + 'baz=', + '', + ]; + invalidValuesTestCases.forEach(testCase => + it(`returns true when value contains invalid chars ${testCase}`, () => { + assert.ok(!validateValue(testCase)); + }) + ); + }); +}); diff --git a/api/test/common/trace/tracestate.test.ts b/api/test/common/trace/tracestate.test.ts new file mode 100644 index 0000000000..6d20195b43 --- /dev/null +++ b/api/test/common/trace/tracestate.test.ts @@ -0,0 +1,139 @@ +/* + * 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 { createTraceState } from '../../../src/trace/internal/utils'; +import { TraceStateImpl } from '../../../src/trace/internal/tracestate-impl'; + +describe('TraceState', () => { + describe('.serialize()', () => { + it('returns serialize string', () => { + const state = createTraceState('a=1,b=2'); + assert.deepStrictEqual(state.serialize(), 'a=1,b=2'); + }); + + it('must create a createTraceState and move updated keys to the front', () => { + const orgState = createTraceState('a=1,b=2'); + const state = orgState.set('b', '3'); + assert.deepStrictEqual(orgState.serialize(), 'a=1,b=2'); + assert.deepStrictEqual(state.serialize(), 'b=3,a=1'); + }); + + it('must create a createTraceState and add new keys to the front', () => { + let state = createTraceState().set('vendorname1', 'opaqueValue1'); + assert.deepStrictEqual(state.serialize(), 'vendorname1=opaqueValue1'); + + state = state.set('vendorname2', 'opaqueValue2'); + assert.deepStrictEqual( + state.serialize(), + 'vendorname2=opaqueValue2,vendorname1=opaqueValue1' + ); + }); + + it('must create a createTraceState and unset the entries', () => { + const orgState = createTraceState('c=4,b=3,a=1'); + let state = orgState.unset('b'); + assert.deepStrictEqual(state.serialize(), 'c=4,a=1'); + state = state.unset('c').unset('A'); + assert.deepStrictEqual(state.serialize(), 'a=1'); + assert.strictEqual(orgState.serialize(), 'c=4,b=3,a=1'); + }); + }); + + describe('.parse()', () => { + it('must successfully parse valid state value', () => { + const state = createTraceState( + 'vendorname2=opaqueValue2,vendorname1=opaqueValue1' + ); + assert.deepStrictEqual(state.get('vendorname1'), 'opaqueValue1'); + assert.deepStrictEqual(state.get('vendorname2'), 'opaqueValue2'); + assert.deepStrictEqual( + state.serialize(), + 'vendorname2=opaqueValue2,vendorname1=opaqueValue1' + ); + }); + + it('must drop states when the items are too long', () => { + const state = createTraceState('a=' + 'b'.repeat(512)); + assert.deepStrictEqual(state.get('a'), undefined); + assert.deepStrictEqual(state.serialize(), ''); + }); + + it('must drop states which cannot be parsed', () => { + const state = createTraceState('a=1,b,c=3'); + assert.deepStrictEqual(state.get('a'), '1'); + assert.deepStrictEqual(state.get('b'), undefined); + assert.deepStrictEqual(state.get('c'), '3'); + assert.deepStrictEqual(state.serialize(), 'a=1,c=3'); + }); + + it('must skip states that only have a single value with an equal sign', () => { + const state = createTraceState('a=1='); + assert.deepStrictEqual(state.get('a'), undefined); + }); + + it('must successfully parse valid state keys', () => { + const state = createTraceState('a-b=1,c/d=2,p*q=3,x_y=4'); + assert.deepStrictEqual(state.get('a-b'), '1'); + assert.deepStrictEqual(state.get('c/d'), '2'); + assert.deepStrictEqual(state.get('p*q'), '3'); + assert.deepStrictEqual(state.get('x_y'), '4'); + }); + + it('must successfully parse valid state value with spaces in between', () => { + const state = createTraceState('a=1,foo=bar baz'); + assert.deepStrictEqual(state.get('foo'), 'bar baz'); + assert.deepStrictEqual(state.serialize(), 'a=1,foo=bar baz'); + }); + + it('must truncate states with too many items', () => { + const state = createTraceState( + new Array(33) + .fill(0) + .map((_: null, num: number) => `a${num}=${num}`) + .join(',') + ) as TraceStateImpl; + assert.deepStrictEqual(state['_keys']().length, 32); + assert.deepStrictEqual(state.get('a0'), '0'); + assert.deepStrictEqual(state.get('a31'), '31'); + assert.deepStrictEqual( + state.get('a32'), + undefined, + 'should truncate from the tail' + ); + }); + + it('should not count invalid items towards max limit', () => { + const tracestate = new Array(32) + .fill(0) + .map((_: null, num: number) => `a${num}=${num}`) + .concat('invalid.suffix.key=1'); // add invalid key to beginning + tracestate.unshift('invalid.prefix.key=1'); + tracestate.splice(15, 0, 'invalid.middle.key.a=1'); + tracestate.splice(15, 0, 'invalid.middle.key.b=2'); + tracestate.splice(15, 0, 'invalid.middle.key.c=3'); + + const state = createTraceState(tracestate.join(',')) as TraceStateImpl; + + assert.deepStrictEqual(state['_keys']().length, 32); + assert.deepStrictEqual(state.get('a0'), '0'); + assert.deepStrictEqual(state.get('a31'), '31'); + assert.deepStrictEqual(state.get('invalid.middle.key.a'), undefined); + assert.deepStrictEqual(state.get('invalid.middle.key.b'), undefined); + assert.deepStrictEqual(state.get('invalid.middle.key.c'), undefined); + }); + }); +}); diff --git a/api/test/index-webpack.ts b/api/test/index-webpack.ts new file mode 100644 index 0000000000..2d2ef32478 --- /dev/null +++ b/api/test/index-webpack.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ +{ + const testsContext = require.context('./common', true); + testsContext.keys().forEach(testsContext); +} diff --git a/api/test/index-webpack.worker.ts b/api/test/index-webpack.worker.ts new file mode 100644 index 0000000000..2d2ef32478 --- /dev/null +++ b/api/test/index-webpack.worker.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ +{ + const testsContext = require.context('./common', true); + testsContext.keys().forEach(testsContext); +} diff --git a/api/test/tree-shaking/tree-shaking.test.ts b/api/test/tree-shaking/tree-shaking.test.ts new file mode 100644 index 0000000000..5c75b8e9d5 --- /dev/null +++ b/api/test/tree-shaking/tree-shaking.test.ts @@ -0,0 +1,121 @@ +/* + * 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 webpack from 'webpack'; +import * as path from 'path'; +import { Union } from 'unionfs'; +import { fs as mfs } from 'memfs'; +import * as realFs from 'fs'; + +/** + * Verify that tree-shaking can be properly applied on the @opentelemetry/api package. + * Unused optional apis should be able to be removed from the final bundle. + */ +describe('tree-shaking', () => { + const allowedAPIs = ['ContextAPI', 'DiagAPI']; + const testAPIs = [ + { + name: 'MetricsAPI', + export: 'metrics', + }, + { + name: 'PropagationAPI', + export: 'propagation', + }, + { + name: 'TraceAPI', + export: 'trace' + }, + ]; + const APIMatcher = /(?:class|function) (\w+API)/g; + + const sourceCodePath = path.join(__dirname, 'test.js'); + const outputPath = path.join(__dirname, 'output'); + const outputFilename = path.join(outputPath, 'bundle.js'); + + afterEach(() => { + try { + mfs.unlinkSync(outputFilename); + } catch { + /** ignore */ + } + }); + + for (const testAPI of testAPIs) { + it(`verify ${testAPI.name}`, async () => { + const sourceCode = ` + import { ${testAPI.export} } from '../../'; + console.log(${testAPI.export}); + `; + mfs.mkdirpSync(path.dirname(sourceCodePath)); + mfs.writeFileSync(sourceCodePath, sourceCode, { encoding: 'utf8' }); + + const compiler = webpack({ + entry: sourceCodePath, + output: { + filename: 'bundle.js', + path: outputPath, + }, + mode: 'production', + optimization: { + // disable minimization so that we can inspect the output easily. + minimize: false, + // disable module concatenation so that variable names will not be mangled. + concatenateModules: false, + } + }); + + const fs = new Union(); + fs.use(mfs as any) + .use(realFs); + + //direct webpack to use unionfs for file input + compiler.inputFileSystem = fs; + //direct webpack to output to memoryfs rather than to disk + compiler.outputFileSystem = { + ...mfs, + join: path.join, + } as any; + + const stats = await new Promise((resolve, reject) => { + compiler.run((err, stats) => { + if (err) { + return reject(err); + } + resolve(stats); + }); + }); + assert.deepStrictEqual(stats.compilation.errors, []); + assert.deepStrictEqual(stats.compilation.warnings, []); + + const outputFile = mfs.readFileSync(outputFilename, 'utf8') as string; + const matches = new Set(); + let match; + do { + match = APIMatcher.exec(outputFile); + if (match) { + matches.add(match[1]); + } + } while (match); + + // Remove allowed apis from checking list. + allowedAPIs.forEach(it => matches.delete(it)); + + assert.deepStrictEqual(Array.from(matches), [testAPI.name]); + }); + } +}); diff --git a/api/tsconfig.all.json b/api/tsconfig.all.json new file mode 100644 index 0000000000..4aa747e89f --- /dev/null +++ b/api/tsconfig.all.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.base.json", + "files": [], + "references": [ + { "path": "./tsconfig.json" }, + { "path": "./tsconfig.esm.json" }, + { "path": "./tsconfig.esnext.json" } + ] +} diff --git a/api/tsconfig.esm.json b/api/tsconfig.esm.json new file mode 100644 index 0000000000..50611a86af --- /dev/null +++ b/api/tsconfig.esm.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base.esm.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build/esm", + "tsBuildInfoFile": "build/esm/tsconfig.esm.tsbuildinfo" + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/api/tsconfig.esnext.json b/api/tsconfig.esnext.json new file mode 100644 index 0000000000..0e3427cbc1 --- /dev/null +++ b/api/tsconfig.esnext.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base.esnext.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build/esnext", + "tsBuildInfoFile": "build/esnext/tsconfig.esnext.tsbuildinfo" + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/api/tsconfig.json b/api/tsconfig.json new file mode 100644 index 0000000000..088086278b --- /dev/null +++ b/api/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ], + "references": [ + ] +} diff --git a/doc/context.md b/doc/context.md new file mode 100644 index 0000000000..d49f8e44ec --- /dev/null +++ b/doc/context.md @@ -0,0 +1,234 @@ +# Context + +In order for OpenTelemetry to work, it must store and propagate important telemetry data. +For example, when a request is received and a span is started it must be available to a component which creates its child span. +To solve this problem, OpenTelemetry stores the span in the Context. +This document describes the OpenTelemetry context API for JavaScript and how it is used. + +_Context Specification: _ + +_Context API Reference: _ + +- [Context Manager](#context-manager) +- [Root Context](#root-context) +- [Context Keys](#context-keys) +- [Basic Operations](#basic-operations) + - [Get Entry](#get-entry) + - [Set Entry](#set-entry) + - [Delete Entry](#delete-entry) +- [Active Context](#active-context) + - [Get Active Context](#get-active-context) + - [Set Active Context](#set-active-context) + - [Example](#example) + +## Context Manager + +The context API depends on a context manager to work. +The examples in this document will assume you have already configured a context manager. +Typically the context manager is provided by your SDK, however it is possible to register one directly like this: + +```typescript +import * as api from "@opentelemetry/api"; +import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks"; + +const contextManager = new AsyncHooksContextManager(); +contextManager.enable(); +api.context.setGlobalContextManager(contextManager); +``` + +## Root Context + +The `ROOT_CONTEXT` is the empty context. +If no context is active, the `ROOT_CONTEXT` is active. +Active context is explained below [Active Context](#active-context). + +## Context Keys + +Context entries are key-value pairs. +Keys can be created by calling `api.createContextKey(description)`. + +```typescript +import * as api from "@opentelemetry/api"; + +const key1 = api.createContextKey("My first key"); +const key2 = api.createContextKey("My second key"); +``` + +## Basic Operations + +### Get Entry + +Entries are accessed using the `context.getValue(key)` method. + +```typescript +import * as api from "@opentelemetry/api"; + +const key = api.createContextKey("some key"); +// ROOT_CONTEXT is the empty context +const ctx = api.ROOT_CONTEXT; + +const value = ctx.getValue(key); +``` + +### Set Entry + +Entries are created by using the `context.setValue(key, value)` method. +Setting a context entry creates a new context with all the entries of the previous context, but with the new entry. +Setting a context entry does not modify the previous context. + +```typescript +import * as api from "@opentelemetry/api"; + +const key = api.createContextKey("some key"); +const ctx = api.ROOT_CONTEXT; + +// add a new entry +const ctx2 = ctx.setValue(key, "context 2"); + +// ctx2 contains the new entry +console.log(ctx2.getValue(key)) // "context 2" + +// ctx is unchanged +console.log(ctx.getValue(key)) // undefined +``` + +### Delete Entry + +Entries are removed by calling `context.deleteValue(key)`. +Deleting a context entry creates a new context with all the entries of the previous context, but without the entry identified by the key. +Deleting a context entry does not modify the previous context. + +```typescript +import * as api from "@opentelemetry/api"; + +const key = api.createContextKey("some key"); +const ctx = api.ROOT_CONTEXT; +const ctx2 = ctx.setValue(key, "context 2"); + +// remove the entry +const ctx3 = ctx.deleteValue(key); + +// ctx3 does not contain the entry +console.log(ctx3.getValue(key)) // undefined + +// ctx2 is unchanged +console.log(ctx2.getValue(key)) // "context 2" +// ctx is unchanged +console.log(ctx.getValue(key)) // undefined +``` + +## Active Context + +**IMPORTANT**: This assumes you have configured a Context Manager. +Without one, `api.context.active()` will _ALWAYS_ return the `ROOT_CONTEXT`. + +The active context is the context which is returned by `api.context.active()`. +The context object contains entries which allow tracing components which are tracing a single thread of execution to communicate with each other and ensure the trace is successfully created. +For example, when a span is created it may be added to the context. +Later, when another span is created it may use the span from the context as its parent span. +This is accomplished through the use of mechanisms like [async_hooks](https://nodejs.org/api/async_hooks.html) or [AsyncLocalStorage](https://nodejs.org/api/async_context.html#async_context_class_asynclocalstorage) in node, or [zone.js](https://github.com/angular/zone.js/) on the web in order to propagate the context through a single execution. +If no context is active, the `ROOT_CONTEXT` is returned, which is just the empty context object. + +### Get Active Context + +The active context is the context which is returned by `api.context.active()`. + +```typescript +import * as api from "@opentelemetry/api"; + +// Returns the active context +// If no context is active, the ROOT_CONTEXT is returned +const ctx = api.context.active(); +``` + +### Set Active Context + +A context can be made active by use of `api.context.with(ctx, callback)`. +During execution of the `callback`, the context passed to `with` will be returned by `context.active`. + +```typescript +import * as api from "@opentelemetry/api"; + +const key = api.createContextKey("Key to store a value"); +const ctx = api.context.active(); + +api.context.with(ctx.setValue(key, "context 2"), async () => { + // "context 2" is active + console.log(api.context.active().getValue(key)) // "context 2" +}); +``` + +The return value of `api.context.with(context, callback)` is the return value of the callback. +The callback is always called synchronously. + +```typescript +import * as api from "@opentelemetry/api"; + +const name = await api.context.with(api.context.active(), async () => { + const row = await db.getSomeValue(); + return row["name"]; +}); + +console.log(name); // name returned by the db +``` + +Active context executions may be nested. + +```typescript +import * as api from "@opentelemetry/api"; + +const key = api.createContextKey("Key to store a value"); +const ctx = api.context.active(); + +// No context is active +console.log(api.context.active().getValue(key)); // undefined + +api.context.with(ctx.setValue(key, "context 2"), () => { + // "context 2" is active + console.log(api.context.active().getValue(key)) // "context 2" + api.context.with(ctx.setValue(key, "context 3"), () => { + // "context 3" is active + console.log(api.context.active().getValue(key)) // "context 3" + }); + // "context 2" is active + console.log(api.context.active().getValue(key)) // "context 2" +}); + +// No context is active +console.log(api.context.active().getValue(key)); // undefined +``` + +### Example + +This more complex example illustrates how the context is not modified, but new context objects are created. + +```typescript +import * as api from "@opentelemetry/api"; + +const key = api.createContextKey("Key to store a value"); + +const ctx = api.context.active(); // Returns ROOT_CONTEXT when no context is active +const ctx2 = ctx.setValue(key, "context 2"); // does not modify ctx + +console.log(ctx.getValue(key)) //? undefined +console.log(ctx2.getValue(key)) //? "context 2" + +const ret = api.context.with(ctx2, () => { + const ctx3 = api.context.active().setValue(key, "context 3"); + + console.log(api.context.active().getValue(key)); //? "context 2" + console.log(ctx.getValue(key)) //? undefined + console.log(ctx2.getValue(key)) //? "context 2" + console.log(ctx3.getValue(key)) //? "context 3" + + api.context.with(ctx3, () => { + console.log(api.context.active().getValue(key)); //? "context 3" + }); + console.log(api.context.active().getValue(key)); //? "context 2" + + return "return value" +}); + +// The value returned by the callback is returned to the caller +console.log(ret); //? "return value" +``` diff --git a/doc/web-api.md b/doc/contributing/web-api.md similarity index 92% rename from doc/web-api.md rename to doc/contributing/web-api.md index 8d916e5847..2c3df6d65d 100644 --- a/doc/web-api.md +++ b/doc/contributing/web-api.md @@ -18,4 +18,4 @@ browsing context only. [`window`]: https://developer.mozilla.org/en-US/docs/Web/API/window [`document`]: https://developer.mozilla.org/en-US/docs/Web/API/Document -[`navigator]: https://developer.mozilla.org/en-US/docs/Web/API/Navigator +[`navigator`]: https://developer.mozilla.org/en-US/docs/Web/API/Navigator diff --git a/doc/exporter-guide.md b/doc/exporter-guide.md index 46dfe0d221..adeb090b43 100644 --- a/doc/exporter-guide.md +++ b/doc/exporter-guide.md @@ -12,13 +12,15 @@ opentelemetry-exporter-myexporter │ └── index.ts │ └── transform.ts │ └── types.ts - │ └── myexporter.ts + │ └── my-trace-exporter.ts + │ └── my-metric-exporter.ts └── test └── transform.test.ts - └── myexporter.test.ts + └── my-trace-exporter.test.ts + └── my-metric-exporter.test.ts ``` -## Tracing +## Traces The `SpanExporter` interface defines which methods the protocol-specific trace/span exporters must implement so that they can be plugged into OpenTelemetry SDK. Span exporters must follow these rules: @@ -29,7 +31,7 @@ The `SpanExporter` interface defines which methods the protocol-specific trace/s 5. Do not modify received spans. 6. Do not implement queuing or batching logic because this is handled by Span Processors. -The current `SpanExporter` interface (`0.2.0`) contains 2 methods: +The current `SpanExporter` interface contains 2 methods: - `export`: Exports a batch of spans. In this method, you’ll process and translate `ReadableSpan` Data into the data that your trace backend accepts, and send them to your tracing backend. @@ -39,16 +41,48 @@ Please refer to the [Zipkin Exporter][zipkin-exporter] or [Jaeger Exporter][jaeg ## Metrics -The `MetricExporter` defines the interface that protocol-specific exporters must implement so that they can be plugged into OpenTelemetry SDK and support sending of metrics data. +Metrics can be exported with two distinct patterns: -The current `MetricExporter` interface (`0.2.0`) defines 2 methods: +- Push model exporting, like periodically push metrics to the backend. +- Pull model exporting, like Prometheus pulling metrics from the application. -- `export`: Exports a batch of telemetry data. In this method you’ll process and translate `MetricRecord` Data into the data that your metric backend accepts. +### Push model exporting -- `shutdown`: Shuts down the exporter. This is an opportunity for exporter to do any cleanup required. `Shutdown` should be called only once for each Exporter instance. After the call to `Shutdown` subsequent calls to Export are not allowed and should return `FailedNotRetryable` error. +The `PushMetricExporter` defines the interface that protocol-specific exporters must implement so that they can be plugged into OpenTelemetry SDK and support sending of metrics data with `PeriodicMetricReader`. + +The current `PushMetricExporter` interface defines 3 required methods: + +- `export`: Exports a batch of telemetry data. In this method you’ll process and translate `ResourceMetrics` into the data that your metric backend accepts. + +- `shutdown`: Shuts down the exporter. This is an opportunity for exporter to do any cleanup required. `Shutdown` should be called only once for each Exporter instance. + +- `forceFlush`: Ensure that the export of any metrics the exporter has received is completed before the returned promise is settled. + +The `PushMetricExporter` interface can also implement following methods to provide a preference on metric configuration: + +- `selectAggregationTemporality`: Select the preferred `AggregationTemporality` for the given `InstrumentType` for this exporter. + +- `selectAggregation`: Select the preferred `Aggregation` for the given `InstrumentType` for this exporter. + +Please refer to the [OTLP Exporter][otlp-exporter] for more comprehensive examples. + +### Pull model exporting + +The pulling model exporting requires the export pipeline proactively initiate metric collections. Such exporting pipeline must be modeled as a `MetricReader`. + +The abstract class `MetricReader` defines the interface that protocol-specific readers must implement so that they can be plugged into OpenTelemetry SDK and support pulling of metrics data. + +The current `MetricReader` interface defines 2 required methods: + +- `onShutdown`: Shuts down the reader. This is an opportunity for reader to do any cleanup required. `Shutdown` should be called only once for each reader instance. + +- `onForceFlush`: Ensure that the export of any metrics the reader has received is completed before the returned promise is settled. + +A `MetricReader` can initiate a metric collection request with `MetricReader.collect()` method. The `MetricReader.collect()` returns a promise that settles with a `CollectionResult`, containing the `ResourceMetrics` record and a series of reasons for possibly failed asynchronous metric instrument collection. The `ResourceMetrics` record can be processed and translated into the data that your metric backend accepts. Please refer to the [Prometheus Exporter][prometheus-exporter] for more comprehensive examples. [zipkin-exporter]: https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-exporter-zipkin [jaeger-exporter]: https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-exporter-jaeger +[otlp-exporter]: https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc [prometheus-exporter]: https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-exporter-prometheus diff --git a/doc/library-author.md b/doc/library-author.md new file mode 100644 index 0000000000..44e5f1e97e --- /dev/null +++ b/doc/library-author.md @@ -0,0 +1,3 @@ +# OpenTelemetry for Library Authors + +TODO diff --git a/doc/metrics.md b/doc/metrics.md new file mode 100644 index 0000000000..7162f43638 --- /dev/null +++ b/doc/metrics.md @@ -0,0 +1,558 @@ +# Metrics + +This quick start is for end users of OpenTelemetry who wish to manually measure their applications. If you are a library author, please see the [Library Authors Guide](library-author.md). If you wish to automatically instrument your application, see the automatic instrumentation documentation for the SDK you wish to use. + +For a high-level overview of OpenTelemetry metrics in general and definitions of some common terms, you can refer to the [OpenTelemetry Specification Overview][spec-overview] + +_Metrics API Specification: _ + +_Metrics API Reference: _ + +- [Getting Started](#getting-started) +- [Acquiring a Meter](#acquiring-a-meter) +- [Create a metric instrument](#create-a-metric-instrument) +- [Describing a instrument measurement](#describing-a-instrument-measurement) + - [Metric Attributes](#metric-attributes) + - [Semantic Conventions](#semantic-conventions) +- [Configuring metric views](#configuring-metric-views) + - [Configuring explicit bucket sizes for the Histogram instrument](#configuring-explicit-bucket-sizes-for-the-histogram-instrument) + - [Dropping instrument from being exported](#dropping-instrument-from-being-exported) + - [Customizing the metric attributes of instrument](#customizing-the-metric-attributes-of-instrument) +- [Exporting measurements](#exporting-measurements) + - [Exporting measurements to Prometheus](#exporting-measurements-to-prometheus) + - [Exporting measurements to Opentelemetry Protocol](#exporting-measurements-to-opentelemetry-protocol) + +## Getting Started + +In this page, you'll learn how to setup OpenTelemetry JS to export metrics from an HTTP server with Fastify. If you're not using Fastify, that's fine -- this guide will also work with Express, etc. + +### Installation + +To begin, set up an environment in a new directory: + +```bash +mkdir otel-getting-started +cd otel-getting-started +npm init -y +``` + +Now install Fastify and OpenTelemetry: + +```bash +npm install fastify @opentelemetry/sdk-node @opentelemetry/exporter-prometheus @opentelemetry/auto-instrumentations-node +``` + +The `@opentelemetry/sdk-node` and `@opentelemetry/auto-instrumentations-node` will install all the +necessary packages to start with OpenTelemetry including instrumentation for a wide variety of popular +packages, such as `http`, `fetch` etc. The package `@opentelemetry/exporter-prometheus` is installed +to export our collected metrics to Prometheus. + +### Create the sample HTTP Server + +Create a file `app.js`: + +```javaScript +const api = require('@opentelemetry/api') +const opentelemetry = require("@opentelemetry/sdk-node"); +const { PrometheusExporter } = require("@opentelemetry/exporter-prometheus"); +const { + getNodeAutoInstrumentations, +} = require("@opentelemetry/auto-instrumentations-node"); + +const prometheusExporter = new PrometheusExporter({ startServer: true }); + +const sdk = new opentelemetry.NodeSDK({ + // Optional - If omitted, the metrics SDK will not be initialized + metricReader: prometheusExporter, + // Optional - you can use the metapackage or load each instrumentation individually + instrumentations: [getNodeAutoInstrumentations()], + // See the Configuration section below for additional configuration options +}); + +// You can optionally detect resources asynchronously from the environment. +// Detected resources are merged with the resources provided in the SDK configuration. +sdk.start().then(() => { + // Resources have been detected and SDK is started + console.log(`SDK started`) + +// Start the http server + const fastify = require('fastify')({ + logger: true + }) + + fastify.get('/', function (request, reply) { + reply.send({ hello: 'world' }) + }) + + fastify.listen({ port: 3000 }, function (err, address) { + if (err) { + fastify.log.error(err) + process.exit(1) + } + + console.log(`Server is now listening on ${address}`) + }) +}); + +// You can also use the shutdown method to gracefully shut down the SDK before process shutdown +// or on some operating system signal. +const process = require("process"); +process.on("SIGTERM", () => { + sdk + .shutdown() + .then( + () => console.log("SDK shut down successfully"), + (err) => console.log("Error shutting down SDK", err) + ) + .finally(() => process.exit(0)); +}); +``` + +In the above example we are initializing the Node SDK to enable the Metrics SDK +and configure it to export the metrics in Prometheus format by registering the +`PrometheusExporter`. + +You can now run the instrument application and it will run the HTTP server on +port 3000 with command: + +```bash +node app.js +``` + +Now when accessing the HTTP server via you will +see the following: + +```json +{"hello":"world"} +``` + +### Add manual instrumentation + +Automatic instrumentation is powerful but it doesn't capture what's going on in +your application. For that you'll need to write some manual instrumentation. Below +we will show you how you can count the number of times a HTTP endpoint has been +accessed. + +#### Counting number of incoming http requests + +First, modify `app.js` to include code that initializes a meter and uses it to +create a counter instrument which counts the number of times the `/` http endpoint +has been requested. + +```javaScript +const api = require('@opentelemetry/api') +const opentelemetry = require("@opentelemetry/sdk-node"); +const { PrometheusExporter } = require("@opentelemetry/exporter-prometheus"); +const { + getNodeAutoInstrumentations, +} = require("@opentelemetry/auto-instrumentations-node"); + +const prometheusExporter = new PrometheusExporter({ startServer: true }); + +const sdk = new opentelemetry.NodeSDK({ + // Optional - If omitted, the metrics SDK will not be initialized + metricReader: prometheusExporter, + // Optional - you can use the metapackage or load each instrumentation individually + instrumentations: [getNodeAutoInstrumentations()], + // See the Configuration section below for additional configuration options +}); + +// You can optionally detect resources asynchronously from the environment. +// Detected resources are merged with the resources provided in the SDK configuration. +sdk.start().then(() => { + // Resources have been detected and SDK is started + console.log(`SDK started`) + + // Create Meter with the name `http-server` + const appMeter = api.metrics.getMeter('http-server') + // Use the created Meter to create a counter instrument + const numberOfRequests = appMeter.createCounter('request-counter') + + // Start the http server + const fastify = require('fastify')({ + logger: true + }) + + fastify.get('/', function (request, reply) { + // Increase the counter by 1 each time the `/` endpoint is requested + numberOfRequests.add(1) + reply.send({ hello: 'world' }) + }) + + fastify.listen({ port: 3000 }, function (err, address) { + if (err) { + fastify.log.error(err) + process.exit(1) + } + + console.log(`Server is now listening on ${address}`) + }) +}); + +// You can also use the shutdown method to gracefully shut down the SDK before process shutdown +// or on some operating system signal. +const process = require("process"); +process.on("SIGTERM", () => { + sdk + .shutdown() + .then( + () => console.log("SDK shut down successfully"), + (err) => console.log("Error shutting down SDK", err) + ) + .finally(() => process.exit(0)); +}); +``` + +Now run the application again: + +```bash +node app.js +``` + +When you navigate to , the counter instrument will be increased +each time the page is accessed. If you want to see the exporter instruments, you +can access via the dedicates metrics endpoint for Prometheus by accessing: + the contents will look similar to: + +```text +# HELP request_counter_total description missing +# TYPE request_counter_total counter +request_counter_total 6 1666624810428 +``` + +In the above example output you can that one instrument is available with the +name `request_counter_total`: + +```text +request_counter_total 6 1666624810428 +``` + +The postfixed `_total` get automatically to the instrument name for each counter instrument +when the measurements are getting exported in the Prometheus format. In the above +example you see that we accessed our `/` endpoint six times. + +## Acquiring a Meter + +In OpenTelemetry, Instruments that allow for measurement operations are acquired through a _meter_. You can get a meter by calling [`getMeter`](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api.MeterProvider.html#getMeter) on the global meter provider. `getMeter` takes the name and version of the application or library acquiring the meter, and provides a meter which can be used to create instruments. + +```typescript +import { metrics } from '@opentelemetry/api'; + +const meter = metrics.getMeter("my-application", "0.1.0"); +``` + +## Create a metric instrument + +In OpenTelemetry, all _metrics_ are composed of [`Instruments`](https://open-telemetry.github.io/opentelemetry-js/enums/_opentelemetry_sdk_metrics.InstrumentType.html). An instrument is responsible for reporting measurements, +there are four types of instruments that can be created: + +- Counter, a synchronous instrument which supports non-negative increments +- Asynchronous Counter, a asynchronous instrument which supports non-negative increments +- Histogram, a synchronous instrument which supports arbitrary values that are statistically meaningful, such as histograms, summaries or percentile +- Asynchronous Gauge, an asynchronous instrument which supports non-additive values, such as room temperature +- UpDownCounter, a synchronous instrument which supports increments and decrements, such as number of active requests +- Asynchronous UpDownCounter, an asynchronous instrument which supports increments and decrements + +You can create a Counter instrument by calling [`Meter#createCounter`](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api.Meter.html#createCounter). The only required argument to `createCounter` is the _instrument name_, which should describe the item that is being measurement. + +```typescript +const counter = meter.createCounter("events.counter"); + +// increase the counter +counter.add(1); +``` + +Most of the time, instruments will be used to measure operations in your application. The following example shows what it might look like to manually measure a function's duration. + +```typescript +async function myTask() { + const histogram = meter.createHistogram("taks.duration"); + const startTime = new Date().getTime() + try { + // Wait for five seconds before continuing code execution + await setTimeout(5_000) + } catch (err) { + } finally { + const endTime = new Date().getTime() + const executionTime = endTime - startTime + + // Record the duration of the task operation + histogram.record(executionTime) + } +} + +await myTask() +``` + +## Describing a instrument measurement + +Using attributes, kind, and the related [semantic conventions](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/metrics/semantic_conventions), we can more accurately describe the measurement in a way our metrics backend will more easily understand. The following example uses these mechanisms, which are described below, for recording a measurement +of a HTTP request. + +Each metric instruments allows to associate a description, unit of measure, and the value type. +The description of a metric instrument can expose up in the metrics backend, the unit or value type +can be used to information about the record measurement itself. + +```typescript +async function myTask() { + const httpServerDuration = meter.createHistogram("http.server.duration", { + description: 'A http server duration', + unit: 'milliseconds', + valueType: ValueType.INT + }); + const startTime = new Date().getTime() + try { + // Wait for five seconds before continuing code execution + await setTimeout(5_000) + } catch (err) { + } finally { + const endTime = new Date().getTime() + const executionTime = endTime - startTime + + httpServerDuration.record(executionTime, { + [SemanticAttributes.HTTP_METHOD]: 'POST', + [SemanticAttributes.HTTP_STATUS_CODE]: '200', + [SemanticAttributes.HTTP_SCHEME]: 'https', + }) + } +} + +await myTask() +``` + +In the above example we are recording a measurement of roughly 5000ms and associate +three metric attributes with this measurement. Metrics backends can show these metric +attributes. In Prometheus the metric attributes would become labels and can be used +as part of queries, and allow search queries, such as what's the 90% percentile of +all successful POST requests. + +### Metric Attributes + +While name and measurement are the minimum required to record a metric measurement, +most of the time they will not be enough information on their own to effectively observe +an application. To solve this, OpenTelemetry uses _Metric Attributes_. Metric attributes are object with +string keys and string values which add more context to the measurement. + +For example, when you are measuring the number of inflight requests, you might want to be able to count +the number of POST, or GET requests. You can add the a metric attribute for `http.method` to allow more +flexibility when leveraging your metric measurement like in Grafana dashboards. + +### Semantic Conventions + +One problem with metrics names and attributes is recognizing, categorizing, and analyzing them in your metrics backend. Between different applications, libraries, and metrics backends there might be different names and expected values for various attributes. For example, your application may use `http.status` to describe the HTTP status code, but a library you use may use `http.status_code`. In order to solve this problem, OpenTelemetry uses a library of semantic conventions which describe the name and attributes which should be used for specific types of metrics. + +The use of semantic conventions is always recommended where applicable, but they are merely conventions. For example, you may find that some name other than the name suggested by the semantic conventions more accurately describes your metric, you may decide not to include a metric attribute which is suggested by semantic conventions for privacy reasons, or you may wish to add a custom attribute which isn't covered by semantic conventions. All of these cases are fine, but please keep in mind that if you stray from the semantic conventions, the categorization of metrics in your metrics backend may be affected. + +_See the current metrics semantic conventions in the OpenTelemetry Specification repository: _ + +[spec-overview]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md + +## Configuring metric views + +A [Metric View](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view) provides the ability to customize the metrics that are exposed by the +Metrics SDK. Metric Views allows you to do: + +- Customize which Metric Attributes are reported on metrics +- Customize which instruments get processed/ignored +- Change the aggregation of a metric +- Define explicit bucket sizes to Histogram instruments + +The Metric view requires the instrument selection query, and the configuration +for the resulting metric. The first step is select to the metrics to whom the View +is relevant, the second step is to configure the customizations for the the selected +metrics. + +A Metric View is a class that can be instantiated via: + +````typescript +const view = new View({ + name: 'metric-view', // optionally, give the view a unique name + // select instruments with a specific name + instrumentName: 'http.server.duration', +}); +```` + +In the above example a View is created which select instruments with the name `http.server.duration` other options to select instruments are: + +- By Instrument Type: use `instrumentType` to select instruments of the given type +- By Meter: use `meterName` to select meters with the given name +- By Meter Version: use `meterVersion` to select meters with the given version +- By Meter Schema URL: use `meterSchemaUrl` to select meters with given schema url + +The `instrumentName` has support for wildcards, so you can select all instruments +using `*` or select all instruments starting with 'http.' by using `http.*`. + +_Note_: The Views can only be defined when the `MeterProvider` instance gets +instantiated. A proposal is submitted to ease the ability to define Metrics Views: + + +### Configuring explicit bucket sizes for the Histogram instrument + +The Histogram instrument has a predefined set of bucket sizes defined which might not +suit all your needs. The bucket sizes can be overridden by configuring a different +aggregation for the Histogram instrument. The `ExplicitBucketHistogramAggregation` +should be used to define the bucket sizes for the Histogram instrument. + +Below an example is given how you can define explicit buckets for a histogram. + +```typescript +// Define view for the histogram metric +const histogramView = new View({ + aggregation: new ExplicitBucketHistogramAggregation([0, 1, 5, 10, 15, 20, 25, 30]), + instrumentName: 'http.server.duration', + instrumentType: InstrumentType.HISTOGRAM, +}); + +// Note, the instrumentName is the same as the name that has been passed for +// the Meter#createHistogram function + +// Create an instance of the metric provider +const meterProvider = new MeterProvider({ + views: [ + histogramView + ] +}); + +// Create histogram metric +const httpServerDuration = meter.createHistogram("http.server.duration", { + description: 'A http server duration', + unit: 'milliseconds', + valueType: ValueType.INT +}); + +// Record measurement for histogram +httpServerDuration.record(50, { + [SemanticAttributes.HTTP_METHOD]: 'POST', + [SemanticAttributes.HTTP_STATUS_CODE]: '200', + [SemanticAttributes.HTTP_SCHEME]: 'https', +}); +``` + +### Dropping instrument from being exported + +In some circumstances you don't want specific metrics to be exported by OpenTelemetry, +for example, you might be using custom instrumentations or third-party packages that +define their own metrics you are not interested in. + +In such cases you can define a view which drops the instruments you are +not interested in. For example, you can drop instruments of a specific meter or +instruments with a specific name: + +The following view drops all instruments that are associated with a meter named `pubsub`: + +```typescript +const dropView = new View({ + aggregation: new DropAggregation(), + meterName: 'pubsub', +}); +``` + +Alternatively, you can also drop instruments with a specific instrument name, +for example, all instruments of which the name starts with `http`: + +```typescript +const dropView = new View({ + aggregation: new DropAggregation(), + instrumentName: 'http*', +}); +``` + +### Customizing the metric attributes of instrument + +If you want to limit the Metric Attributes that get exported in measurements of +an instrument, you can create a Metric View which defines which attributes should +be exported. Attributes that are missing in the list will not be exported. + +In the example below will drop all attributes except attribute `environment` for +all instruments. + +```typescript +new View({ + // only export the attribute 'environment' + attributeKeys: ['environment'], + // apply the view to all instruments + instrumentName: '*', +}) +``` + +## Exporting measurements + +After you have instrumented your application with metrics, you also need to make +sure that the metrics get collected by your metrics backend. The most common formats +that are used are Prometheus and OLTP. + +The latter is the [OpenTelemetry protocol format](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md) +which is supported by the OpenTelemetry Collector. The former is based on the [OpenMetrics +format](https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md) can be consumed by Prometheus and Thanos or other OpenMetrics compatible +backends. + +_Note_: Both OpenTelemetry JavaScript and OpenTelemetry Collector support +exporters for different formats, such as [Cloud Monitoring](https://github.com/GoogleCloudPlatform/opentelemetry-operations-js/tree/master/packages/opentelemetry-cloud-monitoring-exporter). + +## Exporting measurements to Prometheus + +If you want to export your metrics to Prometheus you need to initialize OpenTelemetry +to use the Prometheus exporter `PrometheusExporter` which is included in the +`@opentelemetry/exporter-prometheus`-package. + +```typescript +const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus'); +const { MeterProvider } = require('@opentelemetry/sdk-metrics'); + +// Add your port and startServer to the Prometheus options +const options = { port: 9464, startServer: true }; +const exporter = new PrometheusExporter(options); + +// Creates MeterProvider and installs the exporter as a MetricReader +const meterProvider = new MeterProvider(); +meterProvider.addMetricReader(exporter); +const meter = meterProvider.getMeter('example-prometheus'); + +// Now, start recording data +const counter = meter.createCounter('metric_name', { + description: 'Example of a counter' +}); +counter.add(10, { pid: process.pid }); +``` + +In the above example the instantiated `PrometheusExporter` is configured to expose +a new http server on port 9464. You can now access the metrics at the endpoint +. This is the URL that can be scraped by Prometheus so it can consumed the metrics collected by OpenTelemetry in your application. + +More information about Prometheus and how to configure can be found at: +[https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config](Prometheus Scraping Config) + +For a fully functioning code example for using this exporter, please have a look +at: + +## Exporting measurements to OpenTelemetry Protocol + +OpenTelemetry JavaScript comes with three different kinds of exporters that export +the OTLP protocol, a) [over HTTP](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-metrics-otlp-http), b) [over GRPC](https://www.npmjs.com/package/@opentelemetry/exporter-metrics-otlp-grpc), c) [over Protofbuf](https://www.npmjs.com/package/@opentelemetry/exporter-metrics-otlp-proto). + +The example below shows how you can configure OpenTelemetry JavaScript to use +the OTLP exporter using http/protobuf. + +```typescript +const { MeterProvider, PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics'); +const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-proto'); +const collectorOptions = { + url: '', // url is optional and can be omitted - default is http://localhost:4318/v1/metrics + concurrencyLimit: 1, // an optional limit on pending requests +}; +const exporter = new OTLPMetricExporter(collectorOptions); +const meterProvider = new MeterProvider({}); + +meterProvider.addMetricReader(new PeriodicExportingMetricReader({ + exporter: metricExporter, + exportIntervalMillis: 1000, +})); + +// Now, start recording data +const meter = meterProvider.getMeter('example-meter'); +const counter = meter.createCounter('metric_name'); +counter.add(10, { 'key': 'value' }); +``` + +For a fully functioning code example for using this exporter, please have a look +at: diff --git a/doc/propagation.md b/doc/propagation.md new file mode 100644 index 0000000000..c3d246c0fc --- /dev/null +++ b/doc/propagation.md @@ -0,0 +1,92 @@ +# Propagation + +Span context fields like trace id, span id, trace flags, and baggages need to be send to the downstream services +in order to properly associate downstream created spans with the current span. + +This is commonly achieved with HTTP headers, RPC metadata, with well-known formats like: + +- [W3C Trace Context][]: Supported with [W3CTraceContextPropagator][]. +- [B3][]: Supported with [B3Propagator][]. +- Jaeger: Supported with [JaegerPropagator][]. + +If none of the above formats meet your needs, you can implement your own propagator. + +## Implement your own propagator + +To implement a propagator, you need to define a class implementing the `TextMapPropagator` interface. + +```ts +import { + Context, + isSpanContextValid, + isValidSpanId, + isValidTraceId, + TextMapGetter, + TextMapPropagator, + TextMapSetter, + TraceFlags, + trace, +} from '@opentelemetry/api'; +import { isTracingSuppressed } from '@opentelemetry/core'; + +// Example header, the content format can be `:` +const MyHeader = 'my-header'; + +export class MyPropagator implements TextMapPropagator { + // Inject the header to the outgoing request. + inject(context: Context, carrier: unknown, setter: TextMapSetter): void { + const spanContext = trace.getSpanContext(context); + // Skip if the current span context is not valid or suppressed. + if ( + !spanContext || + !isSpanContextValid(spanContext) || + isTracingSuppressed(context) + ) { + return; + } + + const value = `${spanContext.traceId}:${spanContext.spanId}`; + setter.set(carrier, MyHeader, value); + // You can set more header fields as you need. + } + + // Extract the header from the incoming request. + extract(context: Context, carrier: unknown, getter: TextMapGetter): Context { + const headers = getter.get(carrier, MyHeader); + const header = Array.isArray(headers) ? headers[0] : headers; + if (typeof header !== 'string') return context; + + const [traceId, spanId] = header.split(':'); + + // Skip if the traceId or spanId is invalid. + if (!isValidTraceId(traceId) || !isValidSpanId(spanId)) return context; + + return trace.setSpanContext(context, { + traceId, + spanId, + isRemote: true, + traceFlags: TraceFlags.SAMPLED, + }); + } + + fields(): string[] { + return [MyHeader]; + } +} +``` + +With the propagator defined, you can set it as the global propagator, so that all instrumentations +can make use of it. + +```ts +const api = require('@opentelemetry/api'); +const { MyPropagator } = require('./my-propagator'); + +api.propagation.setGlobalPropagator(new MyPropagator()); +``` + +[B3]: https://github.com/openzipkin/b3-propagation +[B3Propagator]: https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-propagator-b3 +[JaegerPropagator]: https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-propagator-jaeger +[W3C Trace Context]: https://www.w3.org/TR/trace-context/ +[W3CTraceContextPropagator]: https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-core#w3ctracecontextpropagator-propagator diff --git a/doc/sdk-registration.md b/doc/sdk-registration.md new file mode 100644 index 0000000000..a7a3bc72b7 --- /dev/null +++ b/doc/sdk-registration.md @@ -0,0 +1,28 @@ +# SDK Registration Methods + +These methods are used to register a compatible OpenTelemetry SDK. Some SDKs like the [OpenTelemetry JS SDK][opentelemetry-js] provide convenience methods which call these registration methods for you. + +- [Trace API Documentation][trace-api-docs] +- [Propagation API Documentation][propagation-api-docs] +- [Context API Documentation][context-api-docs] + +```javascript +const api = require("@opentelemetry/api"); + +/* Register a global TracerProvider */ +api.trace.setGlobalTracerProvider(tracerProvider); +/* returns tracerProvider (no-op if a working provider has not been initialized) */ +api.trace.getTracerProvider(); +/* returns a tracer from the registered global tracer provider (no-op if a working provider has not been initialized) */ +api.trace.getTracer(name, version); + +/* Register a global Propagator */ +api.propagation.setGlobalPropagator(httpTraceContextPropagator); + +/* Register a global Context Manager */ +api.context.setGlobalContextManager(asyncHooksContextManager); +``` + +[trace-api-docs]: https://open-telemetry.github.io/opentelemetry-js/classes/_opentelemetry_api.TraceAPI.html +[propagation-api-docs]: https://open-telemetry.github.io/opentelemetry-js/classes/_opentelemetry_api.PropagationAPI.html +[context-api-docs]: https://open-telemetry.github.io/opentelemetry-js/classes/_opentelemetry_api.ContextAPI.html diff --git a/doc/tracing.md b/doc/tracing.md new file mode 100644 index 0000000000..ea3e38284b --- /dev/null +++ b/doc/tracing.md @@ -0,0 +1,214 @@ +# Tracing + +This quick start is for end users of OpenTelemetry who wish to manually trace their applications. If you are a library author, please see the [Library Authors Guide](library-author.md). If you wish to automatically instrument your application, see the automatic instrumentation documentation for the SDK you wish to use. + +For a high-level overview of OpenTelemetry tracing in general and definitions of some common terms, you can refer to the [OpenTelemetry Specification Overview][spec-overview] + +_Trace API Specification: _ + +_Trace API Reference: _ + +- [Acquiring a Tracer](#acquiring-a-tracer) +- [Starting and Ending a Span](#starting-and-ending-a-span) +- [Describing a Span](#describing-a-span) + - [Span Relationships](#span-relationships) + - [Span Attributes](#span-attributes) + - [Span Kind](#span-kind) + - [Client](#client) + - [Server](#server) + - [Internal](#internal) + - [Producer](#producer) + - [Consumer](#consumer) + - [Semantic Conventions](#semantic-conventions) + +## Acquiring a Tracer + +In OpenTelemetry, tracing operations are performed using methods on a _tracer_. You can get a tracer by calling [`getTracer`](https://open-telemetry.github.io/opentelemetry-js/classes/_opentelemetry_api.TraceAPI.html#getTracer) on the global tracer provider. `getTracer` takes the name and version of the application or library acquiring the tracer, and provides a tracer which can be used to trace operations. + +```typescript +import { trace } from '@opentelemetry/api'; + +const tracer = trace.getTracer("my-application", "0.1.0"); +``` + +## Starting and Ending a Span + +In OpenTelemetry, all _traces_ are composed of [`Spans`](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api.Span.html). A span describes a single operation with a start time and and end time like a database request, outgoing remote request, or a function invocation. These spans are linked together by parent-child relationships to form a tree. The resultant tree is your trace, and the root of the tree is commonly called the _root span_. + +You can create a span by calling [`Tracer#startSpan`](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api.Tracer.html#startSpan). The only required argument to `startSpan` is the _span name_, which should describe the operation being performed with low cardinality. + +```typescript +const span = tracer.startSpan("my-span-name"); + +// do some work + +// When a span is ended, it will be exported to a tracing backend +// via the currently registered SDK. +span.end(); +``` + +Most of the time, spans will be used as part of a function which responds to some event like a web request. The following example shows what it might look like to manually trace a function which responds to a get request using an imaginary http server framework. + +```typescript +async function onGet(request, response) { + const span = tracer.startSpan("onGet"); + try { + // Do some work here + + response.send(); + + // If we get here and nothing has thrown, the request completed successfully + span.setStatus({ code: SpanStatusCode.OK }); + } catch (err) { + // When we catch an error, we want to show that an error occurred + span.setStatus({ + code: SpanStatusCode.ERROR, + message: err.message, + }); + } finally { + // Every span must be ended or it will not be exported + span.end(); + } +} + +server.on("GET", "/user/:id", onGet); +``` + +## Describing a Span + +Using span relationships, attributes, kind, and the related [semantic conventions](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/trace/semantic_conventions), we can more accurately describe the span in a way our tracing backend will more easily understand. The following example uses these mechanisms, which are described below. + +```typescript +import { NetTransportValues SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { trace, context, SpanKind, SpanStatusCode } from '@opentelemetry/api'; + +async function onGet(request, response) { + // HTTP semantic conventions determine the span name and attributes for this span + const span = tracer.startSpan(`GET /user/:id`, { + // attributes can be added when the span is started + attributes: { + // Attributes from the HTTP trace semantic conventions + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md + [SemanticAttributes.HTTP_METHOD]: "GET", + [SemanticAttributes.HTTP_FLAVOR]: "1.1", + [SemanticAttributes.HTTP_URL]: request.url, + [SemanticAttributes.NET_PEER_IP]: "192.0.2.5", + }, + // This span represents a remote incoming synchronous request + kind: SpanKind.SERVER + }); + + const userId = request.params.id; + + // Create a new context from the current context which has the span "active" + const ctx = trace.setSpan(context.active(), span); + + // Call getUser with the newly created context + // + // context.with calls a function with an associated "active" context. Within + // the function, calling context.active() returns the currently active context. + // If there is no active context, the ROOT_CONTEXT will be returned, which + // has no key-value pairs. + // + // context.with requires at least 2 arguments: a context and a function to be called. + // If a third argument is provided, it will be bound to `this` `this` inside the function. + // Any additional parameters are used as arguments when calling the function. + // + // Return value is the value returned from getUser + // | Context to be used as the "active" context + // | | Function to be called + // | | | Object assigned to this during function execution + // | | | | Passed as the first argument to getUser + // | | | | | + // V V V V V + const user = await context.with(ctx, getUser, undefined, userId); + + // Attributes may also be added after the span is started. + // http.status_code is required by the HTTP trace semantic conventions + span.setAttribute("http.status_code", 200); + + response.send(user.toJson()); + span.setStatus({ + code: SpanStatusCode.OK, + }); + span.end(); + + // Attributes MAY NOT be added after the span ends + span.setAttribute("my.attribute", false); // this does nothing +} + +async function getUser(userId) { + // when this span is created, it will automatically use the span from the context as its parent + const span = tracer.startSpan("SELECT ShopDb.Users", { + attributes: { + // Attributes from the database trace semantic conventions + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md + [SemanticAttributes.DB_SYSTEM]: "mysql", + [SemanticAttributes.DB_CONNECTION_STRING]: "Server=shopdb.example.com;Database=ShopDb;Uid=billing_user;TableCache=true;UseCompression=True;MinimumPoolSize=10;MaximumPoolSize=50;", + [SemanticAttributes.DB_USER]: "app_user", + [SemanticAttributes.NET_PEER_NAME]: "shopdb.example.com", + [SemanticAttributes.NET_PEER_IP]: "192.0.2.12", + [SemanticAttributes.NET_PEER_PORT]: 3306, + [SemanticAttributes.NET_TRANSPORT]: NetTransportValues.IP_TCP, + [SemanticAttributes.DB_NAME]: "ShopDb", + [SemanticAttributes.DB_STATEMENT]: `Select * from Users WHERE user_id = ${userId}`, + [SemanticAttributes.DB_OPERATION]: "SELECT", + [SemanticAttributes.DB_SQL_TABLE]: "Users", + }, + kind: SpanKind.CLIENT, + }); + const user = await db.select("Users", { id: userId }); + + span.setStatus({ + code: SpanStatusCode.OK, + }); + span.end(); + return user; +} + +server.on("GET", "/user/:id", onGet); +``` + +### Span Relationships + +One of the most important aspects of spans is their relationships to each other. For instance, if one span describes an incoming request which makes a database call, it is recommended to trace the database call as a separate span which is a child of the original request span. In order to do this, when we create a span we can tell OpenTelemetry which span to use as its parent using a mechanism called _Context_. + +Context is a very important part of the OpenTelemetry API which cannot be adequately explained in a single paragraph. To read more about context, see the [context documentation](context.md). + +### Span Attributes + +While name, start time, end time, and status are the minimum information required to trace an operation, most of the time they will not be enough information on their own to effectively observe an application. To solve this, OpenTelemetry uses _Span Attributes_. Span attributes are an object with string keys and string, number, or boolean values which describe the span. For example, we can use the span attributes to add route and http response code information to the example above. + +### Span Kind + +When a span is created, it is one of `Client`, `Server`, `Internal`, `Producer`, or `Consumer`. This span kind provides a hint to the tracing backend as to how the trace should be assembled. According to the OpenTelemetry specification, the parent of a server span is always a client span, and the child of a client span is always a server span. Similarly, the parent of a consumer span is always a producer and the child of a producer span is always a consumer. If not provided, the span kind is assumed to be internal. + +For more information regarding SpanKind, see . + +#### Client + +Client spans represent a synchronous outgoing remote call such as an outgoing HTTP request or database call. Note that in this context, "synchronous" does not refer to `async/await`, but to the fact that it is not queued for later processing. + +#### Server + +Server spans represent a synchronous incoming remote call such as an incoming HTTP request or remote procedure call. + +#### Internal + +Internal spans represent operations which do not cross a process boundary. Things like instrumenting a function call or an express middleware may use internal spans. + +#### Producer + +Producer spans represent the creation of a job which may be asynchronously processed later. It may be a remote job such as one inserted into a job queue or a local job handled by an event listener. + +#### Consumer + +Consumer spans represent the processing of a job created by a producer and may start long after the producer span has already ended. + +### Semantic Conventions + +One problem with span names and attributes is recognizing, categorizing, and analyzing them in your tracing backend. Between different applications, libraries, and tracing backends there might be different names and expected values for various attributes. For example, your application may use `http.status` to describe the HTTP status code, but a library you use may use `http.status_code`. In order to solve this problem, OpenTelemetry uses a library of semantic conventions which describe the name and attributes which should be used for specific types of spans. The use of semantic conventions is always recommended where applicable, but they are merely conventions. For example, you may find that some name other than the name suggested by the semantic conventions more accurately describes your span, you may decide not to include a span attribute which is suggested by semantic conventions for privacy reasons, or you may wish to add a custom attribute which isn't covered by semantic conventions. All of these cases are fine, but please keep in mind that if you stray from the semantic conventions, the categorization of spans in your tracing backend may be affected. + +_See the current trace semantic conventions in the OpenTelemetry Specification repository: _ + +[spec-overview]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md diff --git a/eslint.config.js b/eslint.config.js index 3c65700699..5e8f1b50e6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -13,7 +13,7 @@ module.exports = { "indent": ["error", 2, { "SwitchCase": 1 }], "no-trailing-spaces": "error", "eol-last": "error", - "quotes": [2, "single", { "avoidEscape": true }], + "quotes": ["error", "single", { "avoidEscape": true }], "brace-style": ["error", "1tbs"], "eqeqeq": [ "error", @@ -24,7 +24,7 @@ module.exports = { "no-shadow": "off", "arrow-parens": ["error", "as-needed"], "node/no-deprecated-api": ["warn"], - "header/header": [2, "block", [{ + "header/header": ["error", "block", [{ pattern: / \* Copyright The OpenTelemetry Authors[\r\n]+ \*[\r\n]+ \* Licensed under the Apache License, Version 2\.0 \(the \"License\"\);[\r\n]+ \* you may not use this file except in compliance with the License\.[\r\n]+ \* You may obtain a copy of the License at[\r\n]+ \*[\r\n]+ \* https:\/\/www\.apache\.org\/licenses\/LICENSE-2\.0[\r\n]+ \*[\r\n]+ \* Unless required by applicable law or agreed to in writing, software[\r\n]+ \* distributed under the License is distributed on an \"AS IS\" BASIS,[\r\n]+ \* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.[\r\n]+ \* See the License for the specific language governing permissions and[\r\n]+ \* limitations under the License\./gm, template: `\n * Copyright The OpenTelemetry Authors\n *\n * Licensed under the Apache License, Version 2.0 (the "License");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an "AS IS" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n ` @@ -34,7 +34,7 @@ module.exports = { { files: ['*.ts'], rules: { - "@typescript-eslint/no-floating-promises": 2, + "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-this-alias": "off", "@typescript-eslint/naming-convention": [ "error", @@ -45,7 +45,6 @@ module.exports = { "leadingUnderscore": "require" } ], - "@typescript-eslint/no-shadow": ["warn"], "@typescript-eslint/no-unused-vars": ["error", {"argsIgnorePattern": "^_", "args": "after-used"}], "@typescript-eslint/no-inferrable-types": ["error", { ignoreProperties: true }], "@typescript-eslint/no-empty-function": ["off"], @@ -65,7 +64,6 @@ module.exports = { "@typescript-eslint/ban-ts-ignore": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-floating-promises": 1, "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/no-shadow": ["off"], diff --git a/examples/basic-tracer-node/README.md b/examples/basic-tracer-node/README.md index 6db3fc7834..d7da08813f 100644 --- a/examples/basic-tracer-node/README.md +++ b/examples/basic-tracer-node/README.md @@ -40,5 +40,3 @@ Click on the trace to view its details. ## LICENSE Apache License 2.0 - -[opentelemetry-collector-url]: https://github.com/open-telemetry/opentelemetry-exporter-otlp-http diff --git a/examples/https/package.json b/examples/https/package.json index c6eee81462..e693df3b09 100644 --- a/examples/https/package.json +++ b/examples/https/package.json @@ -1,7 +1,7 @@ { "name": "https-example", "private": true, - "version": "0.32.0", + "version": "0.34.0", "description": "Example of HTTPs integration with OpenTelemetry", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -33,14 +33,14 @@ }, "dependencies": { "@opentelemetry/api": "^1.0.0", - "@opentelemetry/exporter-jaeger": "1.6.0", - "@opentelemetry/exporter-zipkin": "1.6.0", - "@opentelemetry/instrumentation": "0.32.0", - "@opentelemetry/instrumentation-http": "0.32.0", - "@opentelemetry/resources": "1.6.0", - "@opentelemetry/sdk-trace-base": "1.6.0", - "@opentelemetry/sdk-trace-node": "1.6.0", - "@opentelemetry/semantic-conventions": "1.6.0" + "@opentelemetry/exporter-jaeger": "1.8.0", + "@opentelemetry/exporter-zipkin": "1.8.0", + "@opentelemetry/instrumentation": "0.34.0", + "@opentelemetry/instrumentation-http": "0.34.0", + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0", + "@opentelemetry/sdk-trace-node": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0" }, "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/examples/https", "devDependencies": { diff --git a/examples/opentelemetry-web/examples/metrics/index.js b/examples/opentelemetry-web/examples/metrics/index.js index e036f1e8d2..c3f86cfbe5 100644 --- a/examples/opentelemetry-web/examples/metrics/index.js +++ b/examples/opentelemetry-web/examples/metrics/index.js @@ -1,5 +1,4 @@ -const { DiagConsoleLogger, DiagLogLevel, diag } = require('@opentelemetry/api'); -const { metrics } = require('@opentelemetry/api-metrics'); +const { DiagConsoleLogger, DiagLogLevel, diag, metrics } = require('@opentelemetry/api'); const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http'); const { MeterProvider, PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics'); diff --git a/examples/opentelemetry-web/package.json b/examples/opentelemetry-web/package.json index f1159103b5..827b2279c3 100644 --- a/examples/opentelemetry-web/package.json +++ b/examples/opentelemetry-web/package.json @@ -1,7 +1,7 @@ { "name": "web-opentelemetry-example", "private": true, - "version": "0.32.0", + "version": "0.34.0", "description": "Example of using @opentelemetry/sdk-trace-web and @opentelemetry/sdk-metrics in browser", "main": "index.js", "scripts": { @@ -42,21 +42,20 @@ "webpack-merge": "^5.8.0" }, "dependencies": { - "@opentelemetry/api": "^1.0.2", - "@opentelemetry/api-metrics": "0.32.0", - "@opentelemetry/context-zone": "1.6.0", - "@opentelemetry/core": "1.6.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.32.0", - "@opentelemetry/exporter-trace-otlp-http": "0.32.0", - "@opentelemetry/exporter-zipkin": "1.6.0", - "@opentelemetry/instrumentation": "0.32.0", - "@opentelemetry/instrumentation-fetch": "0.32.0", - "@opentelemetry/instrumentation-xml-http-request": "0.32.0", - "@opentelemetry/propagator-b3": "1.6.0", - "@opentelemetry/sdk-metrics": "0.32.0", - "@opentelemetry/sdk-trace-base": "1.6.0", - "@opentelemetry/sdk-trace-web": "1.6.0", - "@opentelemetry/semantic-conventions": "1.6.0" + "@opentelemetry/api": "^1.3.0", + "@opentelemetry/context-zone": "1.8.0", + "@opentelemetry/core": "1.8.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.34.0", + "@opentelemetry/exporter-trace-otlp-http": "0.34.0", + "@opentelemetry/exporter-zipkin": "1.8.0", + "@opentelemetry/instrumentation": "0.34.0", + "@opentelemetry/instrumentation-fetch": "0.34.0", + "@opentelemetry/instrumentation-xml-http-request": "0.34.0", + "@opentelemetry/propagator-b3": "1.8.0", + "@opentelemetry/sdk-metrics": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0", + "@opentelemetry/sdk-trace-web": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0" }, "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/examples/tracer-web" } diff --git a/examples/otlp-exporter-node/package.json b/examples/otlp-exporter-node/package.json index 21b27440d4..3468b247f0 100644 --- a/examples/otlp-exporter-node/package.json +++ b/examples/otlp-exporter-node/package.json @@ -1,7 +1,7 @@ { "name": "example-otlp-exporter-node", "private": true, - "version": "0.32.0", + "version": "0.34.0", "description": "Example of using @opentelemetry/collector-exporter in Node.js", "main": "index.js", "scripts": { @@ -28,19 +28,18 @@ "url": "https://github.com/open-telemetry/opentelemetry-js/issues" }, "dependencies": { - "@opentelemetry/api": "^1.1.0", - "@opentelemetry/api-metrics": "0.32.0", - "@opentelemetry/core": "1.6.0", - "@opentelemetry/exporter-metrics-otlp-grpc": "0.32.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.32.0", - "@opentelemetry/exporter-metrics-otlp-proto": "0.32.0", - "@opentelemetry/exporter-trace-otlp-grpc": "0.32.0", - "@opentelemetry/exporter-trace-otlp-http": "0.32.0", - "@opentelemetry/exporter-trace-otlp-proto": "0.32.0", - "@opentelemetry/resources": "1.6.0", - "@opentelemetry/sdk-metrics": "0.32.0", - "@opentelemetry/sdk-trace-base": "1.6.0", - "@opentelemetry/semantic-conventions": "1.6.0" + "@opentelemetry/api": "^1.3.0", + "@opentelemetry/core": "1.8.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.34.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.34.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.34.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.34.0", + "@opentelemetry/exporter-trace-otlp-http": "0.34.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.34.0", + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/sdk-metrics": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0" }, "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/examples/otlp-exporter-node" } diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 23c2624293..3ddc5c1f88 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -8,12 +8,103 @@ All notable changes to experimental packages in this project will be documented ### :rocket: (Enhancement) +* feat(instrumentation-http): monitor error events with events.errorMonitor [#3402](https://github.com/open-telemetry/opentelemetry-js/pull/3402) @legendecas +* feat(instrumentation): add new `_setMeterInstruments` protected method that update the meter instruments every meter provider update. + ### :bug: (Bug Fix) +* fix(instrumentation-xhr): http.url attribute should be absolute [#3200](https://github.com/open-telemetry/opentelemetry-js/pull/3200) @t2t2 + ### :books: (Refine Doc) ### :house: (Internal) +## 0.34.0 + +* `@opentelemetry/sdk-metrics` moved to [packages/sdk-metrics](../packages/sdk-metrics) +* `@opentelemetry/api-metrics` deprecated and merged into [api](../api) + +### :rocket: (Enhancement) + +* feat(metrics-sdk): Add tracing suppresing for Metrics Export [#3332](https://github.com/open-telemetry/opentelemetry-js/pull/3332) @hectorhdzg +* feat(instrumentation): implement `require-in-the-middle` singleton [#3161](https://github.com/open-telemetry/opentelemetry-js/pull/3161) @mhassan1 +* feat(sdk-node): configure trace exporter with environment variables [#3143](https://github.com/open-telemetry/opentelemetry-js/pull/3143) @svetlanabrennan +* feat: enable tree shaking [#3329](https://github.com/open-telemetry/opentelemetry-js/pull/3329) @pkanal +* feat(prometheus): serialize resource as target_info gauge [#3300](https://github.com/open-telemetry/opentelemetry-js/pull/3300) @pichlermarc +* feat(detectors): add browser detector module [#3292](https://github.com/open-telemetry/opentelemetry-js/pull/3292) @abinet18 +* deps: remove unused proto-loader dependencies and update grpc-js and proto-loader versions [#3337](https://github.com/open-telemetry/opentelemetry-js/pull/3337) @seemk +* feat(metrics-exporters): configure temporality via environment variable [#3305](https://github.com/open-telemetry/opentelemetry-js/pull/3305) @pichlermarc +* feat(console-metric-exporter): add temporality configuration [#3387](https://github.com/open-telemetry/opentelemetry-js/pull/3387) @pichlermarc + +### :bug: (Bug Fix) + +* fix(node-sdk): move `@opentelemetry/semantic-conventions` to `dependencies` [#3283](https://github.com/open-telemetry/opentelemetry-js/pull/3283) @mhassan1 +* fix(exporters): do not append trailing '/' when URL contains path [#3274](https://github.com/open-telemetry/opentelemetry-js/pull/3274) @pichlermarc +* fix(instrumentation): debug log on no modules defined for instrumentation [#3308](https://github.com/open-telemetry/opentelemetry-js/pull/3308) @legendecas + +### :books: (Refine Doc) + +* docs(metrics-exporters): fix wrong exporter const name in example [#3270](https://github.com/open-telemetry/opentelemetry-js/issues/3270) @pichlermarc + +### :house: (Internal) + +* ci(instrumentation-http): remove got devDependency + [#3347](https://github.com/open-telemetry/opentelemetry-js/issues/3347) @dyladan +* deps(instrumentation-http): move sdk-metrics to dev dependencies [#3380](https://github.com/open-telemetry/opentelemetry-js/issues/3380) @pichlermarc + +## 0.33.0 + +### :boom: Breaking Change + +* Add `resourceDetectors` option to `NodeSDK` [#3210](https://github.com/open-telemetry/opentelemetry-js/issues/3210) + * `NodeSDK.detectResources()` function is no longer able to receive config as a parameter. + Instead, the detectors are passed to the constructor. + +* chore(metrics-sdk): clean up exports [#3197](https://github.com/open-telemetry/opentelemetry-js/pull/3197) @pichlermarc + * removes export for: + * `AccumulationRecord` + * `Aggregator` + * `AggregatorKind` + * `Accumulation` + * `createInstrumentDescriptor` + * `createInstrumentDescriptorWithView` + * `isDescriptorCompatibleWith` +* chore(api-metrics): clean up exports [#3198](https://github.com/open-telemetry/opentelemetry-js/pull/3198) @pichlermarc + * removes export for: + * `NOOP_COUNTER_METRIC` + * `NOOP_HISTOGRAM_METRIC` + * `NOOP_METER_PROVIDER` + * `NOOP_OBSERVABLE_COUNTER_METRIC` + * `NOOP_OBSERVABLE_GAUGE_METRIC` + * `NOOP_OBSERVABLE_UP_DOWN_COUNTER_METRIC` + * `NOOP_UP_DOWN_COUNTER_METRIC` + * `NoopCounterMetric` + * `NoopHistogramMetric` + * `NoopMeter` + * `NoopMeterProvider` + * `NoopMetric` + * `NoopObservableCounterMetric` + * `NoopObservableGaugeMetric` + * `NoopObservableMetric` + * `NoopObservableUpDownCounterMetric` + * `NoopUpDownCounterMetric` +* feat(sdk-metrics): align MetricReader with specification and other language implementations [#3225](https://github.com/open-telemetry/opentelemetry-js/pull/3225) @pichlermarc +* chore(sdk-metrics): remove accidental export of the SDK `Meter` class [#3243](https://github.com/open-telemetry/opentelemetry-js/pull/3243) @pichlermarc + +### :rocket: (Enhancement) + +* Add `resourceDetectors` option to `NodeSDK` [#3210](https://github.com/open-telemetry/opentelemetry-js/issues/3210) +* feat: add Logs API @mkuba [#3117](https://github.com/open-telemetry/opentelemetry-js/pull/3117) + +### :books: (Refine Doc) + +* docs(sdk-metrics): fix typos and add missing parameter docs. [#3244](https://github.com/open-telemetry/opentelemetry-js/pull/3244) @pichlermarc + +### :house: (Internal) + +* ci(instrumentation-http): improve metrics test stability [#3242](https://github.com/open-telemetry/opentelemetry-js/pull/3242) @pichlermarc +* deps: remove unused protobufjs and update used ones to 7.1.1 #3251 [#3251](https://github.com/open-telemetry/opentelemetry-js/pull/3251) @pichlermarc + ## 0.32.0 ### :boom: Breaking Change @@ -22,10 +113,14 @@ All notable changes to experimental packages in this project will be documented ### :rocket: (Enhancement) +* feature(instrumentation-http): Add HTTP Server and Client duration Metrics in HTTP Node.js Instrumentation [#3149](https://github.com/open-telemetry/opentelemetry-js/pull/3149) @hectorhdzg * fix(add-views-to-node-sdk): added the ability to define meter views in `NodeSDK` [#3066](https://github.com/open-telemetry/opentelemetry-js/pull/3124) @weyert * feature(add-console-metrics-exporter): add ConsoleMetricExporter [#3120](https://github.com/open-telemetry/opentelemetry-js/pull/3120) @weyert * feature(prometheus-serialiser): export the unit block when unit is set in metric descriptor [#3066](https://github.com/open-telemetry/opentelemetry-js/pull/3041) @weyert * feat: support latest `@opentelemetry/api` [#3177](https://github.com/open-telemetry/opentelemetry-js/pull/3177) @dyladan +* feat(sdk-metrics-base): add per metric-reader aggregation support [#3153](https://github.com/open-telemetry/opentelemetry-js/pull/3153) @legendecas +* chore(deps): update prometheus example dependencies to 0.32 [#3126](https://github.com/open-telemetry/opentelemetry-js/pull/3216) @avzis +* feature(opentelemetry-api-metrics): Adding generics to `create{metricType}` [#3151](https://github.com/open-telemetry/opentelemetry-js/issues/3151) @tomerghelber-tm ### :bug: (Bug Fix) diff --git a/experimental/backwards-compatability/node14/package.json b/experimental/backwards-compatability/node14/package.json index a9db91d167..a8ff394187 100644 --- a/experimental/backwards-compatability/node14/package.json +++ b/experimental/backwards-compatability/node14/package.json @@ -1,6 +1,6 @@ { "name": "backcompat-node14", - "version": "0.32.0", + "version": "0.34.0", "private": true, "description": "Backwards compatability app for node 14 types and the OpenTelemetry Node.js SDK", "main": "index.js", @@ -9,8 +9,8 @@ "peer-api-check": "node ../../../scripts/peer-api-check.js" }, "dependencies": { - "@opentelemetry/sdk-node": "0.32.0", - "@opentelemetry/sdk-trace-base": "1.6.0" + "@opentelemetry/sdk-node": "0.34.0", + "@opentelemetry/sdk-trace-base": "1.8.0" }, "devDependencies": { "@types/node": "14.18.25", diff --git a/experimental/backwards-compatability/node16/package.json b/experimental/backwards-compatability/node16/package.json index f1ea4b4f77..03d284dbdb 100644 --- a/experimental/backwards-compatability/node16/package.json +++ b/experimental/backwards-compatability/node16/package.json @@ -1,6 +1,6 @@ { "name": "backcompat-node16", - "version": "0.32.0", + "version": "0.34.0", "private": true, "description": "Backwards compatability app for node 16 types and the OpenTelemetry Node.js SDK", "main": "index.js", @@ -9,8 +9,8 @@ "peer-api-check": "node ../../../scripts/peer-api-check.js" }, "dependencies": { - "@opentelemetry/sdk-node": "0.32.0", - "@opentelemetry/sdk-trace-base": "1.6.0" + "@opentelemetry/sdk-node": "0.34.0", + "@opentelemetry/sdk-trace-base": "1.8.0" }, "devDependencies": { "@types/node": "16.11.52", diff --git a/experimental/examples/prometheus/package.json b/experimental/examples/prometheus/package.json index a3c5dbca51..55cefc9d76 100644 --- a/experimental/examples/prometheus/package.json +++ b/experimental/examples/prometheus/package.json @@ -1,7 +1,8 @@ { "name": "prometheus-example", - "version": "0.28.0", - "description": "Example of using @opentelemetry/sdk-metrics-base and @opentelemetry/exporter-prometheus", + "version": "0.34.0", + "private": true, + "description": "Example of using @opentelemetry/sdk-metrics and @opentelemetry/exporter-prometheus", "main": "index.js", "scripts": { "start": "node index.js" @@ -9,8 +10,8 @@ "author": "OpenTelemetry Authors", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api": "^1.0.2", - "@opentelemetry/exporter-prometheus": "0.28.0", - "@opentelemetry/sdk-metrics-base": "0.28.0" + "@opentelemetry/api": "^1.3.0", + "@opentelemetry/exporter-prometheus": "0.34.0", + "@opentelemetry/sdk-metrics": "1.8.0" } } diff --git a/experimental/packages/opentelemetry-sdk-metrics/.eslintignore b/experimental/packages/api-logs/.eslintignore similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/.eslintignore rename to experimental/packages/api-logs/.eslintignore diff --git a/experimental/packages/api-logs/.eslintrc.js b/experimental/packages/api-logs/.eslintrc.js new file mode 100644 index 0000000000..7654abb6ac --- /dev/null +++ b/experimental/packages/api-logs/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + "env": { + "mocha": true, + "commonjs": true, + "shared-node-browser": true + }, + ...require('../../../eslint.config.js') +} diff --git a/experimental/packages/opentelemetry-sdk-metrics/LICENSE b/experimental/packages/api-logs/LICENSE similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/LICENSE rename to experimental/packages/api-logs/LICENSE diff --git a/experimental/packages/api-logs/README.md b/experimental/packages/api-logs/README.md new file mode 100644 index 0000000000..57d40a3211 --- /dev/null +++ b/experimental/packages/api-logs/README.md @@ -0,0 +1,63 @@ +# OpenTelemetry API for JavaScript + +[![NPM Published Version][npm-img]][npm-url] +[![Apache License][license-image]][license-image] + +This package provides everything needed to interact with the unstable OpenTelemetry Logs API, including all TypeScript interfaces, enums, and no-op implementations. It is intended for use both on the server and in the browser. + +## Beta Software - Use at your own risk + +The logs API is considered alpha software and there is no guarantee of stability or long-term support. When the API is stabilized, it will be made available and supported long-term in the `@opentelemetry/api` package and this package will be deprecated. + +## Quick Start + +Purposefully left blank until SDK is available. + +## Version Compatibility + +Because the npm installer and node module resolution algorithm could potentially allow two or more copies of any given package to exist within the same `node_modules` structure, the OpenTelemetry API takes advantage of a variable on the `global` object to store the global API. When an API method in the API package is called, it checks if this `global` API exists and proxies calls to it if and only if it is a compatible API version. This means if a package has a dependency on an OpenTelemetry API version which is not compatible with the API used by the end user, the package will receive a no-op implementation of the API. + +## Advanced Use + +### API Methods + +If you are writing an instrumentation library, or prefer to call the API methods directly rather than using the `register` method on the Tracer/Meter/Logger Provider, OpenTelemetry provides direct access to the underlying API methods through the `@opentelemetry/api-logs` package. API entry points are defined as global singleton objects `trace`, `metrics`, `logs`, `propagation`, and `context` which contain methods used to initialize SDK implementations and acquire resources from the API. + +- [Logs API Documentation][logs-api-docs] + +```javascript +const api = require("@opentelemetry/api-logs"); + +/* A specific implementation of LoggerProvider comes from an SDK */ +const loggerProvider = createLoggerProvider(); + +/* Initialize LoggerProvider */ +api.logs.setGlobalLoggerProvider(loggerProvider); +/* returns loggerProvider (no-op if a working provider has not been initialized) */ +api.logs.getLoggerProvider(); +/* returns a logger from the registered global logger provider (no-op if a working provider has not been initialized) */ +const logger = api.logs.getLogger(name, version); + +// logging an event in an instrumentation library +logger.emitEvent({ name: 'event-name', domain: 'event-domain' }); + +// logging an event in a log appender +logger.emitLogRecord({ severityNumber: 1, body: 'log data' }); +``` + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us in [GitHub Discussions][discussions-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[discussions-url]: https://github.com/open-telemetry/opentelemetry-js/discussions +[license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/main/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[npm-url]: https://www.npmjs.com/package/@opentelemetry/api-logs +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fapi-logs.svg +[logs-api-docs]: https://open-telemetry.github.io/opentelemetry-js/modules/_opentelemetry_api_logs.html diff --git a/experimental/packages/opentelemetry-api-metrics/karma.conf.js b/experimental/packages/api-logs/karma.conf.js similarity index 100% rename from experimental/packages/opentelemetry-api-metrics/karma.conf.js rename to experimental/packages/api-logs/karma.conf.js diff --git a/experimental/packages/opentelemetry-api-metrics/package.json b/experimental/packages/api-logs/package.json similarity index 91% rename from experimental/packages/opentelemetry-api-metrics/package.json rename to experimental/packages/api-logs/package.json index a901850ee7..952a107fcb 100644 --- a/experimental/packages/opentelemetry-api-metrics/package.json +++ b/experimental/packages/api-logs/package.json @@ -1,7 +1,7 @@ { - "name": "@opentelemetry/api-metrics", - "version": "0.32.0", - "description": "Public metrics API for OpenTelemetry", + "name": "@opentelemetry/api-logs", + "version": "0.34.0", + "description": "Public logs API for OpenTelemetry", "main": "build/src/index.js", "module": "build/esm/index.js", "esnext": "build/esnext/index.js", @@ -34,7 +34,8 @@ "nodejs", "browser", "profiling", - "metrics", + "logs", + "events", "stats", "monitoring" ], @@ -64,7 +65,7 @@ "@opentelemetry/api": "^1.0.0" }, "devDependencies": { - "@types/mocha": "9.1.1", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/webpack-env": "1.16.3", "codecov": "3.8.3", @@ -82,5 +83,6 @@ "typescript": "4.4.4", "webpack": "4.46.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-api-metrics" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/api-logs", + "sideEffects": false } diff --git a/experimental/packages/api-logs/src/NoopLogger.ts b/experimental/packages/api-logs/src/NoopLogger.ts new file mode 100644 index 0000000000..3f2227154c --- /dev/null +++ b/experimental/packages/api-logs/src/NoopLogger.ts @@ -0,0 +1,24 @@ +/* + * 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 { Logger } from './types/Logger'; +import { LogEvent } from './types/LogEvent'; +import { LogRecord } from './types/LogRecord'; + +export class NoopLogger implements Logger { + emitLogRecord(_logRecord: LogRecord): void {} + emitEvent(_event: LogEvent): void {} +} diff --git a/experimental/packages/api-logs/src/NoopLoggerProvider.ts b/experimental/packages/api-logs/src/NoopLoggerProvider.ts new file mode 100644 index 0000000000..5c69e5a7bd --- /dev/null +++ b/experimental/packages/api-logs/src/NoopLoggerProvider.ts @@ -0,0 +1,28 @@ +/* + * 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 { LoggerProvider } from './types/LoggerProvider'; +import { Logger } from './types/Logger'; +import { LoggerOptions } from './types/LoggerOptions'; +import { NoopLogger } from './NoopLogger'; + +export class NoopLoggerProvider implements LoggerProvider { + getLogger(_name: string, _version?: string | undefined, _options?: LoggerOptions | undefined): Logger { + return new NoopLogger(); + } +} + +export const NOOP_LOGGER_PROVIDER = new NoopLoggerProvider(); diff --git a/experimental/packages/api-logs/src/api/logs.ts b/experimental/packages/api-logs/src/api/logs.ts new file mode 100644 index 0000000000..0f9388648a --- /dev/null +++ b/experimental/packages/api-logs/src/api/logs.ts @@ -0,0 +1,80 @@ +/* + * 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 { + API_BACKWARDS_COMPATIBILITY_VERSION, + GLOBAL_LOGS_API_KEY, + _global, + makeGetter +} from '../internal/global-utils'; +import { LoggerProvider } from '../types/LoggerProvider'; +import { NOOP_LOGGER_PROVIDER } from '../NoopLoggerProvider'; +import { Logger } from '../types/Logger'; +import { LoggerOptions } from '../types/LoggerOptions'; + +export class LogsAPI { + private static _instance?: LogsAPI; + + private constructor() {} + + public static getInstance(): LogsAPI { + if (!this._instance) { + this._instance = new LogsAPI(); + } + + return this._instance; + } + + public setGlobalLoggerProvider(provider: LoggerProvider): LoggerProvider { + if (_global[GLOBAL_LOGS_API_KEY]) { + return this.getLoggerProvider(); + } + + _global[GLOBAL_LOGS_API_KEY] = makeGetter( + API_BACKWARDS_COMPATIBILITY_VERSION, + provider, + NOOP_LOGGER_PROVIDER + ); + + return provider; + } + + /** + * Returns the global logger provider. + * + * @returns LoggerProvider + */ + public getLoggerProvider(): LoggerProvider { + return ( + _global[GLOBAL_LOGS_API_KEY]?.(API_BACKWARDS_COMPATIBILITY_VERSION) ?? + NOOP_LOGGER_PROVIDER + ); + } + + /** + * Returns a logger from the global logger provider. + * + * @returns Logger + */ + public getLogger(name: string, version?: string, options?: LoggerOptions): Logger { + return this.getLoggerProvider().getLogger(name, version, options); + } + + /** Remove the global logger provider */ + public disable(): void { + delete _global[GLOBAL_LOGS_API_KEY]; + } +} diff --git a/experimental/packages/api-logs/src/index.ts b/experimental/packages/api-logs/src/index.ts new file mode 100644 index 0000000000..cf22a2e5eb --- /dev/null +++ b/experimental/packages/api-logs/src/index.ts @@ -0,0 +1,24 @@ +/* + * 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 './types/Logger'; +export * from './types/LoggerProvider'; +export * from './types/LogRecord'; +export * from './types/LogEvent'; +export * from './types/LoggerOptions'; + +import { LogsAPI } from './api/logs'; +export const logs = LogsAPI.getInstance(); diff --git a/experimental/packages/opentelemetry-api-metrics/src/api/global-utils.ts b/experimental/packages/api-logs/src/internal/global-utils.ts similarity index 88% rename from experimental/packages/opentelemetry-api-metrics/src/api/global-utils.ts rename to experimental/packages/api-logs/src/internal/global-utils.ts index e371d5165d..705139aeda 100644 --- a/experimental/packages/opentelemetry-api-metrics/src/api/global-utils.ts +++ b/experimental/packages/api-logs/src/internal/global-utils.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -import { MeterProvider } from '../types/MeterProvider'; +import { LoggerProvider } from '../types/LoggerProvider'; import { _globalThis } from '../platform'; -export const GLOBAL_METRICS_API_KEY = Symbol.for( - 'io.opentelemetry.js.api.metrics' +export const GLOBAL_LOGS_API_KEY = Symbol.for( + 'io.opentelemetry.js.api.logs' ); type Get = (version: number) => T; type OtelGlobal = Partial<{ - [GLOBAL_METRICS_API_KEY]: Get; + [GLOBAL_LOGS_API_KEY]: Get; }>; export const _global = _globalThis as OtelGlobal; @@ -52,4 +52,4 @@ export function makeGetter( * version. If the global API is not compatible with the API package * attempting to get it, a NOOP API implementation will be returned. */ -export const API_BACKWARDS_COMPATIBILITY_VERSION = 4; +export const API_BACKWARDS_COMPATIBILITY_VERSION = 1; diff --git a/experimental/packages/opentelemetry-api-metrics/src/platform/browser/globalThis.ts b/experimental/packages/api-logs/src/platform/browser/globalThis.ts similarity index 100% rename from experimental/packages/opentelemetry-api-metrics/src/platform/browser/globalThis.ts rename to experimental/packages/api-logs/src/platform/browser/globalThis.ts diff --git a/experimental/packages/api-logs/src/platform/browser/index.ts b/experimental/packages/api-logs/src/platform/browser/index.ts new file mode 100644 index 0000000000..e9d6ebed71 --- /dev/null +++ b/experimental/packages/api-logs/src/platform/browser/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 './globalThis'; diff --git a/experimental/packages/api-logs/src/platform/index.ts b/experimental/packages/api-logs/src/platform/index.ts new file mode 100644 index 0000000000..cdaf8858ce --- /dev/null +++ b/experimental/packages/api-logs/src/platform/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 './node'; diff --git a/experimental/packages/api-logs/src/platform/node/globalThis.ts b/experimental/packages/api-logs/src/platform/node/globalThis.ts new file mode 100644 index 0000000000..36e97e2732 --- /dev/null +++ b/experimental/packages/api-logs/src/platform/node/globalThis.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +/** only globals that common to node and browsers are allowed */ +// eslint-disable-next-line node/no-unsupported-features/es-builtins +export const _globalThis = typeof globalThis === 'object' ? globalThis : global; diff --git a/experimental/packages/api-logs/src/platform/node/index.ts b/experimental/packages/api-logs/src/platform/node/index.ts new file mode 100644 index 0000000000..e9d6ebed71 --- /dev/null +++ b/experimental/packages/api-logs/src/platform/node/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 './globalThis'; diff --git a/experimental/packages/api-logs/src/types/LogEvent.ts b/experimental/packages/api-logs/src/types/LogEvent.ts new file mode 100644 index 0000000000..7b37572c6c --- /dev/null +++ b/experimental/packages/api-logs/src/types/LogEvent.ts @@ -0,0 +1,54 @@ +/* + * 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 { Attributes } from '@opentelemetry/api'; + +export interface LogEvent { + /** + * The time when the event occurred as UNIX Epoch time in nanoseconds. + */ + timestamp?: number; + + /** + * The name of the event. + */ + name: string; + + /** + * The domain the event belongs to. + */ + domain?: string; + + /** + * Additional attributes that describe the event. + */ + attributes?: Attributes; + + /** + * 8 least significant bits are the trace flags as defined in W3C Trace Context specification. + */ + traceFlags?: number; + + /** + * A unique identifier for a trace. + */ + traceId?: string; + + /** + * A unique identifier for a span within a trace. + */ + spanId?: string; +} diff --git a/experimental/packages/api-logs/src/types/LogRecord.ts b/experimental/packages/api-logs/src/types/LogRecord.ts new file mode 100644 index 0000000000..132900c54a --- /dev/null +++ b/experimental/packages/api-logs/src/types/LogRecord.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 { Attributes } from '@opentelemetry/api'; + +export interface LogRecord { + /** + * The time when the log record occurred as UNIX Epoch time in nanoseconds. + */ + timestamp?: number; + + /** + * Numerical value of the severity. + */ + severityNumber?: number; + + /** + * The severity text. + */ + severityText?: string; + + /** + * A value containing the body of the log record. + */ + body?: string; + + /** + * Attributes that define the log record. + */ + attributes?: Attributes; + + /** + * 8 least significant bits are the trace flags as defined in W3C Trace Context specification. + */ + traceFlags?: number; + + /** + * A unique identifier for a trace. + */ + traceId?: string; + + /** + * A unique identifier for a span within a trace. + */ + spanId?: string; +} diff --git a/experimental/packages/api-logs/src/types/Logger.ts b/experimental/packages/api-logs/src/types/Logger.ts new file mode 100644 index 0000000000..b30f3aff93 --- /dev/null +++ b/experimental/packages/api-logs/src/types/Logger.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 { LogRecord } from './LogRecord'; +import { LogEvent } from './LogEvent'; + +export interface Logger { + /** + * Emit a log record. This method should only be used by log appenders. + * + * @param logRecord + */ + emitLogRecord(logRecord: LogRecord): void; + + /** + * Emit an event. This method should only be used by instrumentations emitting events. + * + * @param event + */ + emitEvent(event: LogEvent): void; +} diff --git a/experimental/packages/api-logs/src/types/LoggerOptions.ts b/experimental/packages/api-logs/src/types/LoggerOptions.ts new file mode 100644 index 0000000000..9b2fe06fc8 --- /dev/null +++ b/experimental/packages/api-logs/src/types/LoggerOptions.ts @@ -0,0 +1,42 @@ +/* + * 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 { Attributes } from '@opentelemetry/api'; + +export interface LoggerOptions { + /** + * The schemaUrl of the tracer or instrumentation library + * @default '' + */ + schemaUrl?: string; + + /** + * The default domain for events created by the Logger. + * + * The combination of event name and event domain uiquely identifies an event. + * By supplying an event domain, it is possible to use the same event name across + * different domains / use cases. + * + * The default domain can be overridden when emitting an individual event. + * @default '' + */ + eventDomain?: string; + + /** + * The instrumentation scope attributes to associate with emitted telemetry + */ + scopeAttributes?: Attributes; +} diff --git a/experimental/packages/api-logs/src/types/LoggerProvider.ts b/experimental/packages/api-logs/src/types/LoggerProvider.ts new file mode 100644 index 0000000000..e79aa1ce09 --- /dev/null +++ b/experimental/packages/api-logs/src/types/LoggerProvider.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 { Logger } from './Logger'; +import { LoggerOptions } from './LoggerOptions'; + +/** + * A registry for creating named {@link Logger}s. + */ +export interface LoggerProvider { + /** + * Returns a Logger, creating one if one with the given name, version, and + * schemaUrl pair is not already created. + * + * @param name The name of the logger or instrumentation library. + * @param version The version of the logger or instrumentation library. + * @param options The options of the logger or instrumentation library. + * @returns Logger A Logger with the given name and version + */ + getLogger(name: string, version?: string, options?: LoggerOptions): Logger; +} diff --git a/experimental/packages/api-logs/test/api/api.test.ts b/experimental/packages/api-logs/test/api/api.test.ts new file mode 100644 index 0000000000..3548e08184 --- /dev/null +++ b/experimental/packages/api-logs/test/api/api.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { Logger, logs } from '../../src'; +import { NoopLogger } from '../../src/NoopLogger'; +import { NoopLoggerProvider } from '../../src/NoopLoggerProvider'; + +describe('API', () => { + const dummyLogger = new NoopLogger(); + + it('should expose a logger provider via getLoggerProvider', () => { + const provider = logs.getLoggerProvider(); + assert.ok(provider); + assert.strictEqual(typeof provider, 'object'); + }); + + describe('GlobalLoggerProvider', () => { + beforeEach(() => { + logs.disable(); + }); + + it('should use the global logger provider', () => { + logs.setGlobalLoggerProvider(new TestLoggerProvider()); + const logger = logs.getLoggerProvider().getLogger('name'); + assert.deepStrictEqual(logger, dummyLogger); + }); + + it('should not allow overriding global provider if already set', () => { + const provider1 = new TestLoggerProvider(); + const provider2 = new TestLoggerProvider(); + logs.setGlobalLoggerProvider(provider1); + assert.equal(logs.getLoggerProvider(), provider1); + logs.setGlobalLoggerProvider(provider2); + assert.equal(logs.getLoggerProvider(), provider1); + }); + }); + + describe('getLogger', () => { + beforeEach(() => { + logs.disable(); + }); + + it('should return a logger instance from global provider', () => { + logs.setGlobalLoggerProvider(new TestLoggerProvider()); + const logger = logs.getLogger('myLogger'); + assert.deepStrictEqual(logger, dummyLogger); + }); + }); + + class TestLoggerProvider extends NoopLoggerProvider { + override getLogger(): Logger { + return dummyLogger; + } + } +}); diff --git a/experimental/packages/opentelemetry-api-metrics/test/index-webpack.ts b/experimental/packages/api-logs/test/index-webpack.ts similarity index 100% rename from experimental/packages/opentelemetry-api-metrics/test/index-webpack.ts rename to experimental/packages/api-logs/test/index-webpack.ts diff --git a/experimental/packages/opentelemetry-api-metrics/test/api/global.test.ts b/experimental/packages/api-logs/test/internal/global.test.ts similarity index 54% rename from experimental/packages/opentelemetry-api-metrics/test/api/global.test.ts rename to experimental/packages/api-logs/test/internal/global.test.ts index 485c68c41a..8ac9c48cf3 100644 --- a/experimental/packages/opentelemetry-api-metrics/test/api/global.test.ts +++ b/experimental/packages/api-logs/test/internal/global.test.ts @@ -15,8 +15,8 @@ */ import * as assert from 'assert'; -import { _global, GLOBAL_METRICS_API_KEY } from '../../src/api/global-utils'; -import { NoopMeterProvider } from '../../src'; +import { _global, GLOBAL_LOGS_API_KEY } from '../../src/internal/global-utils'; +import { NoopLoggerProvider } from '../../src/NoopLoggerProvider'; const api1 = require('../../src') as typeof import('../../src'); @@ -31,45 +31,45 @@ describe('Global Utils', () => { assert.notStrictEqual(api1, api2); // that return separate noop instances to start assert.notStrictEqual( - api1.metrics.getMeterProvider(), - api2.metrics.getMeterProvider() + api1.logs.getLoggerProvider(), + api2.logs.getLoggerProvider() ); beforeEach(() => { - api1.metrics.disable(); - api2.metrics.disable(); + api1.logs.disable(); + api2.logs.disable(); }); - it('should change the global meter provider', () => { - const original = api1.metrics.getMeterProvider(); - const newMeterProvider = new NoopMeterProvider(); - api1.metrics.setGlobalMeterProvider(newMeterProvider); - assert.notStrictEqual(api1.metrics.getMeterProvider(), original); - assert.strictEqual(api1.metrics.getMeterProvider(), newMeterProvider); + it('should change the global logger provider', () => { + const original = api1.logs.getLoggerProvider(); + const newLoggerProvider = new NoopLoggerProvider(); + api1.logs.setGlobalLoggerProvider(newLoggerProvider); + assert.notStrictEqual(api1.logs.getLoggerProvider(), original); + assert.strictEqual(api1.logs.getLoggerProvider(), newLoggerProvider); }); it('should load an instance from one which was set in the other', () => { - api1.metrics.setGlobalMeterProvider(new NoopMeterProvider()); + api1.logs.setGlobalLoggerProvider(new NoopLoggerProvider()); assert.strictEqual( - api1.metrics.getMeterProvider(), - api2.metrics.getMeterProvider() + api1.logs.getLoggerProvider(), + api2.logs.getLoggerProvider() ); }); it('should disable both if one is disabled', () => { - const original = api1.metrics.getMeterProvider(); + const original = api1.logs.getLoggerProvider(); - api1.metrics.setGlobalMeterProvider(new NoopMeterProvider()); + api1.logs.setGlobalLoggerProvider(new NoopLoggerProvider()); - assert.notStrictEqual(original, api1.metrics.getMeterProvider()); - api2.metrics.disable(); - assert.strictEqual(original, api1.metrics.getMeterProvider()); + assert.notStrictEqual(original, api1.logs.getLoggerProvider()); + api2.logs.disable(); + assert.strictEqual(original, api1.logs.getLoggerProvider()); }); it('should return the module NoOp implementation if the version is a mismatch', () => { - const original = api1.metrics.getMeterProvider(); - api1.metrics.setGlobalMeterProvider(new NoopMeterProvider()); - const afterSet = _global[GLOBAL_METRICS_API_KEY]!(-1); + const original = api1.logs.getLoggerProvider(); + api1.logs.setGlobalLoggerProvider(new NoopLoggerProvider()); + const afterSet = _global[GLOBAL_LOGS_API_KEY]!(-1); assert.strictEqual(original, afterSet); }); diff --git a/experimental/packages/api-logs/test/noop-implementations/noop-logger-provider.test.ts b/experimental/packages/api-logs/test/noop-implementations/noop-logger-provider.test.ts new file mode 100644 index 0000000000..e696c77b01 --- /dev/null +++ b/experimental/packages/api-logs/test/noop-implementations/noop-logger-provider.test.ts @@ -0,0 +1,31 @@ +/* + * 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 { NoopLogger } from '../../src/NoopLogger'; +import { NoopLoggerProvider } from '../../src/NoopLoggerProvider'; + +describe('NoopLoggerProvider', () => { + it('should not crash', () => { + const loggerProvider = new NoopLoggerProvider(); + + assert.ok(loggerProvider.getLogger('logger-name') instanceof NoopLogger); + assert.ok(loggerProvider.getLogger('logger-name', 'v1') instanceof NoopLogger); + assert.ok(loggerProvider.getLogger('logger-name', 'v1', { + schemaUrl: 'https://opentelemetry.io/schemas/1.7.0' + }) instanceof NoopLogger); + }); +}); diff --git a/experimental/packages/api-logs/test/noop-implementations/noop-logger.test.ts b/experimental/packages/api-logs/test/noop-implementations/noop-logger.test.ts new file mode 100644 index 0000000000..6360e7b7dd --- /dev/null +++ b/experimental/packages/api-logs/test/noop-implementations/noop-logger.test.ts @@ -0,0 +1,36 @@ +/* + * 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 { NoopLogger } from '../../src/NoopLogger'; +import { NoopLoggerProvider } from '../../src/NoopLoggerProvider'; + +describe('NoopLogger', () => { + it('constructor should not crash', () => { + const logger = new NoopLoggerProvider().getLogger('test-noop'); + assert(logger instanceof NoopLogger); + }); + + it('calling emitEvent should not crash', () => { + const logger = new NoopLoggerProvider().getLogger('test-noop'); + logger.emitEvent({ name: 'event-name', domain: 'event-domain' }); + }); + + it('calling emitLogRecord should not crash', () => { + const logger = new NoopLoggerProvider().getLogger('test-noop'); + logger.emitLogRecord({ severityNumber: 1, body: 'log body' }); + }); +}); diff --git a/experimental/packages/opentelemetry-api-metrics/tsconfig.all.json b/experimental/packages/api-logs/tsconfig.all.json similarity index 100% rename from experimental/packages/opentelemetry-api-metrics/tsconfig.all.json rename to experimental/packages/api-logs/tsconfig.all.json diff --git a/experimental/packages/opentelemetry-api-metrics/tsconfig.docs.json b/experimental/packages/api-logs/tsconfig.docs.json similarity index 79% rename from experimental/packages/opentelemetry-api-metrics/tsconfig.docs.json rename to experimental/packages/api-logs/tsconfig.docs.json index 627fa3ff3e..18c131e2b9 100644 --- a/experimental/packages/opentelemetry-api-metrics/tsconfig.docs.json +++ b/experimental/packages/api-logs/tsconfig.docs.json @@ -7,7 +7,7 @@ "src/**/*.ts" ], "typedocOptions": { - "name": "OpenTelemetry API for JavaScript", + "name": "OpenTelemetry Logs API for JavaScript", "out": "docs/out", "mode": "file", "hideGenerator": true diff --git a/experimental/packages/opentelemetry-api-metrics/tsconfig.esm.json b/experimental/packages/api-logs/tsconfig.esm.json similarity index 100% rename from experimental/packages/opentelemetry-api-metrics/tsconfig.esm.json rename to experimental/packages/api-logs/tsconfig.esm.json diff --git a/experimental/packages/opentelemetry-api-metrics/tsconfig.esnext.json b/experimental/packages/api-logs/tsconfig.esnext.json similarity index 100% rename from experimental/packages/opentelemetry-api-metrics/tsconfig.esnext.json rename to experimental/packages/api-logs/tsconfig.esnext.json diff --git a/experimental/packages/opentelemetry-api-metrics/tsconfig.json b/experimental/packages/api-logs/tsconfig.json similarity index 73% rename from experimental/packages/opentelemetry-api-metrics/tsconfig.json rename to experimental/packages/api-logs/tsconfig.json index ed9d0830bd..e22548584a 100644 --- a/experimental/packages/opentelemetry-api-metrics/tsconfig.json +++ b/experimental/packages/api-logs/tsconfig.json @@ -7,5 +7,10 @@ "include": [ "src/**/*.ts", "test/**/*.ts" + ], + "references": [ + { + "path": "../../../api" + } ] } diff --git a/experimental/packages/exporter-trace-otlp-grpc/README.md b/experimental/packages/exporter-trace-otlp-grpc/README.md index 3893a9f449..b03274b320 100644 --- a/experimental/packages/exporter-trace-otlp-grpc/README.md +++ b/experimental/packages/exporter-trace-otlp-grpc/README.md @@ -3,6 +3,8 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] +**Note: This is an experimental package under active development. New releases may include breaking changes.** + This module provides exporter for node to be used with OTLP (`grpc`) compatible receivers. Compatible with [opentelemetry-collector][opentelemetry-collector-url] versions `>=0.16 <=0.50`. diff --git a/experimental/packages/exporter-trace-otlp-grpc/package.json b/experimental/packages/exporter-trace-otlp-grpc/package.json index 7402f23557..4b7b41c973 100644 --- a/experimental/packages/exporter-trace-otlp-grpc/package.json +++ b/experimental/packages/exporter-trace-otlp-grpc/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/exporter-trace-otlp-grpc", - "version": "0.32.0", + "version": "0.34.0", "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", @@ -48,9 +48,10 @@ }, "devDependencies": { "@babel/core": "7.16.0", + "@grpc/proto-loader": "^0.7.3", "@opentelemetry/api": "^1.0.0", - "@opentelemetry/otlp-exporter-base": "0.32.0", - "@types/mocha": "9.1.1", + "@opentelemetry/otlp-exporter-base": "0.34.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "codecov": "3.8.3", @@ -67,13 +68,13 @@ "@opentelemetry/api": "^1.0.0" }, "dependencies": { - "@grpc/grpc-js": "^1.5.9", - "@grpc/proto-loader": "^0.6.9", - "@opentelemetry/core": "1.6.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.32.0", - "@opentelemetry/otlp-transformer": "0.32.0", - "@opentelemetry/resources": "1.6.0", - "@opentelemetry/sdk-trace-base": "1.6.0" + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.8.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.34.0", + "@opentelemetry/otlp-transformer": "0.34.0", + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/exporter-trace-otlp-grpc" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/exporter-trace-otlp-grpc", + "sideEffects": false } diff --git a/experimental/packages/exporter-trace-otlp-grpc/tsconfig.json b/experimental/packages/exporter-trace-otlp-grpc/tsconfig.json index 94d47b6e19..abde2bfde9 100644 --- a/experimental/packages/exporter-trace-otlp-grpc/tsconfig.json +++ b/experimental/packages/exporter-trace-otlp-grpc/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../../api" + }, { "path": "../../../packages/opentelemetry-core" }, diff --git a/experimental/packages/exporter-trace-otlp-http/README.md b/experimental/packages/exporter-trace-otlp-http/README.md index 448c8caf20..ef4259fcd1 100644 --- a/experimental/packages/exporter-trace-otlp-http/README.md +++ b/experimental/packages/exporter-trace-otlp-http/README.md @@ -3,6 +3,8 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] +**Note: This is an experimental package under active development. New releases may include breaking changes.** + This module provides exporter for web and node to be used with OTLP (`http/json`) compatible receivers. Compatible with [opentelemetry-collector][opentelemetry-collector-url] versions `>=0.48 <=0.50`. @@ -23,8 +25,10 @@ To see documentation and sample code for the metric exporter, see the [exporter- The OTLPTraceExporter in Web expects the endpoint to end in `/v1/traces`. ```js -import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'; +import { + BatchSpanProcessor, + WebTracerProvider, +} from '@opentelemetry/sdk-trace-web'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; const collectorOptions = { diff --git a/experimental/packages/exporter-trace-otlp-http/package.json b/experimental/packages/exporter-trace-otlp-http/package.json index 990f56cd42..bd17747f7f 100644 --- a/experimental/packages/exporter-trace-otlp-http/package.json +++ b/experimental/packages/exporter-trace-otlp-http/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/exporter-trace-otlp-http", - "version": "0.32.0", + "version": "0.34.0", "description": "OpenTelemetry Collector Trace Exporter allows user to send collected traces to the OpenTelemetry Collector", "main": "build/src/index.js", "module": "build/esm/index.js", @@ -65,7 +65,7 @@ "devDependencies": { "@babel/core": "7.16.0", "@opentelemetry/api": "^1.0.0", - "@types/mocha": "9.1.1", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "@types/webpack-env": "1.16.3", @@ -94,11 +94,12 @@ "@opentelemetry/api": "^1.0.0" }, "dependencies": { - "@opentelemetry/core": "1.6.0", - "@opentelemetry/otlp-exporter-base": "0.32.0", - "@opentelemetry/otlp-transformer": "0.32.0", - "@opentelemetry/resources": "1.6.0", - "@opentelemetry/sdk-trace-base": "1.6.0" + "@opentelemetry/core": "1.8.0", + "@opentelemetry/otlp-exporter-base": "0.34.0", + "@opentelemetry/otlp-transformer": "0.34.0", + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/exporter-trace-otlp-http" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/exporter-trace-otlp-http", + "sideEffects": false } diff --git a/experimental/packages/exporter-trace-otlp-http/src/platform/browser/OTLPTraceExporter.ts b/experimental/packages/exporter-trace-otlp-http/src/platform/browser/OTLPTraceExporter.ts index ff478ead7c..ec12def8f2 100644 --- a/experimental/packages/exporter-trace-otlp-http/src/platform/browser/OTLPTraceExporter.ts +++ b/experimental/packages/exporter-trace-otlp-http/src/platform/browser/OTLPTraceExporter.ts @@ -53,7 +53,7 @@ export class OTLPTraceExporter return typeof config.url === 'string' ? config.url : getEnv().OTEL_EXPORTER_OTLP_TRACES_ENDPOINT.length > 0 - ? appendRootPathToUrlIfNeeded(getEnv().OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, DEFAULT_COLLECTOR_RESOURCE_PATH) + ? appendRootPathToUrlIfNeeded(getEnv().OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) : getEnv().OTEL_EXPORTER_OTLP_ENDPOINT.length > 0 ? appendResourcePathToUrl(getEnv().OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_COLLECTOR_RESOURCE_PATH) : DEFAULT_COLLECTOR_URL; diff --git a/experimental/packages/exporter-trace-otlp-http/src/platform/node/OTLPTraceExporter.ts b/experimental/packages/exporter-trace-otlp-http/src/platform/node/OTLPTraceExporter.ts index aeec62259b..528a2735c3 100644 --- a/experimental/packages/exporter-trace-otlp-http/src/platform/node/OTLPTraceExporter.ts +++ b/experimental/packages/exporter-trace-otlp-http/src/platform/node/OTLPTraceExporter.ts @@ -52,7 +52,7 @@ export class OTLPTraceExporter return typeof config.url === 'string' ? config.url : getEnv().OTEL_EXPORTER_OTLP_TRACES_ENDPOINT.length > 0 - ? appendRootPathToUrlIfNeeded(getEnv().OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, DEFAULT_COLLECTOR_RESOURCE_PATH) + ? appendRootPathToUrlIfNeeded(getEnv().OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) : getEnv().OTEL_EXPORTER_OTLP_ENDPOINT.length > 0 ? appendResourcePathToUrl(getEnv().OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_COLLECTOR_RESOURCE_PATH) : DEFAULT_COLLECTOR_URL; diff --git a/experimental/packages/exporter-trace-otlp-http/tsconfig.json b/experimental/packages/exporter-trace-otlp-http/tsconfig.json index 087c804079..bb73fd2a34 100644 --- a/experimental/packages/exporter-trace-otlp-http/tsconfig.json +++ b/experimental/packages/exporter-trace-otlp-http/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../../api" + }, { "path": "../../../packages/opentelemetry-core" }, diff --git a/experimental/packages/exporter-trace-otlp-proto/README.md b/experimental/packages/exporter-trace-otlp-proto/README.md index 56551f9288..0338b4cd93 100644 --- a/experimental/packages/exporter-trace-otlp-proto/README.md +++ b/experimental/packages/exporter-trace-otlp-proto/README.md @@ -3,6 +3,8 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] +**Note: This is an experimental package under active development. New releases may include breaking changes.** + This module provides exporter for node to be used with OTLP (`http/protobuf`) compatible receivers. Compatible with [opentelemetry-collector][opentelemetry-collector-url] versions `>=0.32 <=0.50`. diff --git a/experimental/packages/exporter-trace-otlp-proto/package.json b/experimental/packages/exporter-trace-otlp-proto/package.json index 41aff440c7..1a44e0ea66 100644 --- a/experimental/packages/exporter-trace-otlp-proto/package.json +++ b/experimental/packages/exporter-trace-otlp-proto/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/exporter-trace-otlp-proto", - "version": "0.32.0", + "version": "0.34.0", "description": "OpenTelemetry Collector Exporter allows user to send collected traces to the OpenTelemetry Collector using protobuf over HTTP", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -49,7 +49,7 @@ "devDependencies": { "@babel/core": "7.16.0", "@opentelemetry/api": "^1.0.0", - "@types/mocha": "9.1.1", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "codecov": "3.8.3", @@ -66,14 +66,13 @@ "@opentelemetry/api": "^1.0.0" }, "dependencies": { - "@grpc/proto-loader": "^0.6.9", - "@opentelemetry/core": "1.6.0", - "@opentelemetry/otlp-exporter-base": "0.32.0", - "@opentelemetry/otlp-proto-exporter-base": "0.32.0", - "@opentelemetry/otlp-transformer": "0.32.0", - "@opentelemetry/resources": "1.6.0", - "@opentelemetry/sdk-trace-base": "1.6.0", - "protobufjs": "^6.9.0" + "@opentelemetry/core": "1.8.0", + "@opentelemetry/otlp-exporter-base": "0.34.0", + "@opentelemetry/otlp-proto-exporter-base": "0.34.0", + "@opentelemetry/otlp-transformer": "0.34.0", + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/exporter-trace-otlp-proto" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/exporter-trace-otlp-proto", + "sideEffects": false } diff --git a/experimental/packages/exporter-trace-otlp-proto/src/OTLPTraceExporter.ts b/experimental/packages/exporter-trace-otlp-proto/src/OTLPTraceExporter.ts index f523110024..d5c40a8214 100644 --- a/experimental/packages/exporter-trace-otlp-proto/src/OTLPTraceExporter.ts +++ b/experimental/packages/exporter-trace-otlp-proto/src/OTLPTraceExporter.ts @@ -54,7 +54,7 @@ export class OTLPTraceExporter return typeof config.url === 'string' ? config.url : getEnv().OTEL_EXPORTER_OTLP_TRACES_ENDPOINT.length > 0 - ? appendRootPathToUrlIfNeeded(getEnv().OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, DEFAULT_COLLECTOR_RESOURCE_PATH) + ? appendRootPathToUrlIfNeeded(getEnv().OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) : getEnv().OTEL_EXPORTER_OTLP_ENDPOINT.length > 0 ? appendResourcePathToUrl(getEnv().OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_COLLECTOR_RESOURCE_PATH) : DEFAULT_COLLECTOR_URL; diff --git a/experimental/packages/exporter-trace-otlp-proto/tsconfig.json b/experimental/packages/exporter-trace-otlp-proto/tsconfig.json index 3324a8a529..9b6c50baef 100644 --- a/experimental/packages/exporter-trace-otlp-proto/tsconfig.json +++ b/experimental/packages/exporter-trace-otlp-proto/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../../api" + }, { "path": "../../../packages/opentelemetry-core" }, diff --git a/experimental/packages/opentelemetry-api-metrics/README.md b/experimental/packages/opentelemetry-api-metrics/README.md deleted file mode 100644 index 838aca5226..0000000000 --- a/experimental/packages/opentelemetry-api-metrics/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# OpenTelemetry API for JavaScript - -[![NPM Published Version][npm-img]][npm-url] -[![Apache License][license-image]][license-image] - -This package provides everything needed to interact with the unstable OpenTelemetry Metrics API, including all TypeScript interfaces, enums, and no-op implementations. It is intended for use both on the server and in the browser. - -## Beta Software - Use at your own risk - -The metrics API is considered alpha software and there is no guarantee of stability or long-term support. When the API is stabilized, it will be made available and supported long-term in the `@opentelemetry/api` package and this package will be deprecated. - -## Quick Start - -To get started you need to install the SDK and instrumentations, create a MeterProvider, and register it with the API. - -### Install Dependencies - -```sh -$ # Install metrics dependencies -$ npm install \ - @opentelemetry/api-metrics \ - @opentelemetry/sdk-metrics \ - @opentelemetry/exporter-prometheus # add exporters as needed -``` - -> Note: this example is for node.js. See [examples/tracer-web](https://github.com/open-telemetry/opentelemetry-js/tree/main/examples/tracer-web) for a browser example. - -### Initialize the SDK - -Before any other module in your application is loaded, you must initialize the global tracer and meter providers. If you fail to initialize a provider, no-op implementations will be provided to any library which acquires them from the API. - -To collect traces and metrics, you will have to tell the SDK where to export telemetry data to. This example uses Jaeger and Prometheus, but exporters exist for [other tracing backends][other-tracing-backends]. If you're not sure if there is an exporter for your tracing backend, contact your tracing provider. - -#### Metrics - -```javascript -const api = require("@opentelemetry/api-metrics"); -const { MeterProvider } = require("@opentelemetry/sdk-metrics"); -const { PrometheusExporter } = require("@opentelemetry/exporter-prometheus"); - -// The Prometheus exporter runs an HTTP server which the Prometheus backend -// scrapes to collect metrics. -const exporter = new PrometheusExporter({ startServer: true }); -// Creates MeterProvider and installs the exporter as a MetricReader -const meterProvider = new MeterProvider(); -meterProvider.addMetricReader(exporter); - -/** - * Registering the provider with the API allows it to be discovered - * and used by instrumentation libraries. - */ -api.metrics.setGlobalMeterProvider(meterProvider); -``` - -## Version Compatibility - -Because the npm installer and node module resolution algorithm could potentially allow two or more copies of any given package to exist within the same `node_modules` structure, the OpenTelemetry API takes advantage of a variable on the `global` object to store the global API. When an API method in the API package is called, it checks if this `global` API exists and proxies calls to it if and only if it is a compatible API version. This means if a package has a dependency on an OpenTelemetry API version which is not compatible with the API used by the end user, the package will receive a no-op implementation of the API. - -## Advanced Use - -### API Methods - -If you are writing an instrumentation library, or prefer to call the API methods directly rather than using the `register` method on the Tracer/Meter Provider, OpenTelemetry provides direct access to the underlying API methods through the `@opentelemetry/api-metrics` package. API entry points are defined as global singleton objects `trace`, `metrics`, `propagation`, and `context` which contain methods used to initialize SDK implementations and acquire resources from the API. - -- [Metrics API Documentation][metrics-api-docs] - -```javascript -const api = require("@opentelemetry/api-metrics"); - -/* Initialize MeterProvider */ -api.metrics.setGlobalMeterProvider(meterProvider); -/* returns meterProvider (no-op if a working provider has not been initialized) */ -api.metrics.getMeterProvider(); -/* returns a meter from the registered global meter provider (no-op if a working provider has not been initialized) */ -api.metrics.getMeter(name, version); -``` - -## Useful links - -- For more information on OpenTelemetry, visit: -- For more about OpenTelemetry JavaScript: -- For help or feedback on this project, join us in [GitHub Discussions][discussions-url] - -## License - -Apache 2.0 - See [LICENSE][license-url] for more information. - -[discussions-url]: https://github.com/open-telemetry/opentelemetry-js/discussions -[license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/main/LICENSE -[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat -[npm-url]: https://www.npmjs.com/package/@opentelemetry/api-metrics -[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fapi-metrics.svg - -[metrics-api-docs]: https://open-telemetry.github.io/opentelemetry-js/modules/_opentelemetry_api_metrics.html - -[other-tracing-backends]: https://github.com/open-telemetry/opentelemetry-js#trace-exporters diff --git a/experimental/packages/opentelemetry-api-metrics/test/api/api.test.ts b/experimental/packages/opentelemetry-api-metrics/test/api/api.test.ts deleted file mode 100644 index e9a837230e..0000000000 --- a/experimental/packages/opentelemetry-api-metrics/test/api/api.test.ts +++ /dev/null @@ -1,46 +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 assert from 'assert'; -import { metrics, NoopMeter, NoopMeterProvider } from '../../src'; - -describe('API', () => { - it('should expose a meter provider via getMeterProvider', () => { - const meter = metrics.getMeterProvider(); - assert.ok(meter); - assert.strictEqual(typeof meter, 'object'); - }); - - describe('GlobalMeterProvider', () => { - const dummyMeter = new NoopMeter(); - - beforeEach(() => { - metrics.disable(); - }); - - it('should use the global meter provider', () => { - metrics.setGlobalMeterProvider(new TestMeterProvider()); - const meter = metrics.getMeterProvider().getMeter('name'); - assert.deepStrictEqual(meter, dummyMeter); - }); - - class TestMeterProvider extends NoopMeterProvider { - override getMeter() { - return dummyMeter; - } - } - }); -}); diff --git a/experimental/packages/opentelemetry-browser-detector/.eslintignore b/experimental/packages/opentelemetry-browser-detector/.eslintignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/experimental/packages/opentelemetry-browser-detector/.eslintignore @@ -0,0 +1 @@ +build diff --git a/experimental/packages/opentelemetry-api-metrics/.eslintrc.js b/experimental/packages/opentelemetry-browser-detector/.eslintrc.js similarity index 78% rename from experimental/packages/opentelemetry-api-metrics/.eslintrc.js rename to experimental/packages/opentelemetry-browser-detector/.eslintrc.js index b9004d2025..3ed0fbeba3 100644 --- a/experimental/packages/opentelemetry-api-metrics/.eslintrc.js +++ b/experimental/packages/opentelemetry-browser-detector/.eslintrc.js @@ -2,7 +2,7 @@ module.exports = { "env": { "mocha": true, "commonjs": true, - "shared-node-browser": true + "node": true, }, ...require('../../../eslint.config.js') } diff --git a/experimental/packages/opentelemetry-sdk-metrics/.npmignore b/experimental/packages/opentelemetry-browser-detector/.npmignore similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/.npmignore rename to experimental/packages/opentelemetry-browser-detector/.npmignore diff --git a/experimental/packages/opentelemetry-browser-detector/LICENSE b/experimental/packages/opentelemetry-browser-detector/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/experimental/packages/opentelemetry-browser-detector/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/experimental/packages/opentelemetry-browser-detector/README.md b/experimental/packages/opentelemetry-browser-detector/README.md new file mode 100644 index 0000000000..a1e9002ea9 --- /dev/null +++ b/experimental/packages/opentelemetry-browser-detector/README.md @@ -0,0 +1,54 @@ +# OpenTelemetry Browser Detector + +**Note: This is an experimental package under active development. New releases may include breaking changes.** + +This module provides detector for browser environments + +## Installation + +```bash +npm install --save @opentelemetry/opentelemetry-browser-detector +``` + +## Usage + +```js +import { Resource, detectResources } from '@opentelemetry/resources'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; +import { browserDetector } from '@opentelemetry/opentelemetry-browser-detector'; + +async function start(){ + let resource= new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: 'Test App Name', + }); + let detectedResources= await detectResources({detectors:[browserDetector]}); + resource=resource.merge(detectedResources); + const provider = new WebTracerProvider({ + resource + }); + + provider.addSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter( {url:CONF.url ,headers:{}}),{exportTimeoutMillis:CONF.timeOutMillis,scheduledDelayMillis:CONF.delayMillis})); + provider.register({ + // Changing default contextManager to use ZoneContextManager - supports asynchronous operations - optional + contextManager: new ZoneContextManager(), + }); + +// Registering instrumentations + + registerInstrumentations({ + instrumentations: [ + new DocumentLoadInstrumentation(), + new XMLHttpRequestInstrumentation(), + new FetchInstrumentation(), + ], + }); +} + + +start().then(()=> console.log("Instrumentation started")); + +``` + +The browser identification attributes will be added to the resource spans when traces are created. +These attributes include platform, brands, mobile, language if the browser supports +the userAgentData api, otherwise it will contain only the user_agent informations diff --git a/experimental/packages/opentelemetry-sdk-metrics/karma.conf.js b/experimental/packages/opentelemetry-browser-detector/karma.conf.js similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/karma.conf.js rename to experimental/packages/opentelemetry-browser-detector/karma.conf.js diff --git a/experimental/packages/opentelemetry-browser-detector/package.json b/experimental/packages/opentelemetry-browser-detector/package.json new file mode 100644 index 0000000000..9af38cea26 --- /dev/null +++ b/experimental/packages/opentelemetry-browser-detector/package.json @@ -0,0 +1,77 @@ +{ + "name": "@opentelemetry/opentelemetry-browser-detector", + "version": "0.34.0", + "description": "OpenTelemetry Resource Detector for Browser", + "main": "build/src/index.js", + "module": "build/esm/index.js", + "esnext": "build/esnext/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js", + "scripts": { + "prepublishOnly": "npm run compile", + "compile": "tsc --build tsconfig.all.json", + "clean": "tsc --build --clean tsconfig.all.json", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", + "test:browser": "nyc karma start --single-run", + "tdd": "npm run test -- --watch-extensions ts --watch", + "tdd:browser": "karma start", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../../", + "version": "node ../../../scripts/version-update.js", + "watch": "tsc --build --watch tsconfig.all.json", + "precompile": "lerna run version --scope $(npm pkg get name) --include-dependencies", + "prewatch": "npm run precompile", + "peer-api-check": "node ../../../scripts/peer-api-check.js" + }, + "keywords": [ + "opentelemetry", + "browser", + "resource", + "detector" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + }, + "files": [ + "build/esm/**/*.js", + "build/esm/**/*.js.map", + "build/esm/**/*.d.ts", + "build/esnext/**/*.js", + "build/esnext/**/*.js.map", + "build/esnext/**/*.d.ts", + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts", + "doc", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@babel/core": "7.16.0", + "@opentelemetry/api": "^1.0.0", + "@types/mocha": "9.1.1", + "@types/node": "18.6.5", + "@types/sinon": "10.0.13", + "codecov": "3.8.3", + "mocha": "10.0.0", + "nyc": "15.1.0", + "sinon": "14.0.0", + "ts-loader": "8.4.0", + "ts-mocha": "10.0.0", + "typescript": "4.4.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + }, + "dependencies": { + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/browser-detector" +} diff --git a/experimental/packages/opentelemetry-browser-detector/src/BrowserDetector.ts b/experimental/packages/opentelemetry-browser-detector/src/BrowserDetector.ts new file mode 100644 index 0000000000..d0e9dea278 --- /dev/null +++ b/experimental/packages/opentelemetry-browser-detector/src/BrowserDetector.ts @@ -0,0 +1,74 @@ +/* + * 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 { diag } from '@opentelemetry/api'; +import { Detector, Resource, ResourceDetectionConfig } from '@opentelemetry/resources'; +import { ResourceAttributes } from '@opentelemetry/resources'; +import { BROWSER_ATTRIBUTES, UserAgentData } from './types'; + +/** + * BrowserDetector will be used to detect the resources related to browser. + */ +class BrowserDetector implements Detector { + async detect(config?: ResourceDetectionConfig): Promise { + const isBrowser = typeof navigator !== 'undefined'; + if (!isBrowser) { + return Resource.empty(); + } + const browserResource: ResourceAttributes = getBrowserAttributes(); + return this._getResourceAttributes(browserResource, config); + } + /** + * Validates browser resource attribute map from browser variables + * + * @param browserResource The un-sanitized resource attributes from browser as key/value pairs. + * @param config: Config + * @returns The sanitized resource attributes. + */ + private _getResourceAttributes( + browserResource: ResourceAttributes, + _config?: ResourceDetectionConfig + ) { + if ( + !browserResource[BROWSER_ATTRIBUTES.USER_AGENT] && !browserResource[BROWSER_ATTRIBUTES.PLATFORM] + ) { + diag.debug( + 'BrowserDetector failed: Unable to find required browser resources. ' + ); + return Resource.empty(); + } else { + return new Resource(browserResource); + } + } +} + +// Add Browser related attributes to resources +function getBrowserAttributes(): ResourceAttributes { + const browserAttribs : ResourceAttributes = {}; + const userAgentData : UserAgentData | undefined = (navigator as any).userAgentData; + if (userAgentData) { + browserAttribs[BROWSER_ATTRIBUTES.PLATFORM] = userAgentData.platform; + browserAttribs[BROWSER_ATTRIBUTES.BRANDS] = userAgentData.brands.map(b => `${b.brand} ${b.version}`); + browserAttribs[BROWSER_ATTRIBUTES.MOBILE] = userAgentData.mobile; + } else { + browserAttribs[BROWSER_ATTRIBUTES.USER_AGENT] = navigator.userAgent; + } + browserAttribs[BROWSER_ATTRIBUTES.LANGUAGE]=navigator.language; + return browserAttribs; +} + + +export const browserDetector = new BrowserDetector(); diff --git a/experimental/packages/opentelemetry-browser-detector/src/index.ts b/experimental/packages/opentelemetry-browser-detector/src/index.ts new file mode 100644 index 0000000000..7209990763 --- /dev/null +++ b/experimental/packages/opentelemetry-browser-detector/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 './BrowserDetector'; diff --git a/experimental/packages/opentelemetry-browser-detector/src/types.ts b/experimental/packages/opentelemetry-browser-detector/src/types.ts new file mode 100644 index 0000000000..52d8a6610d --- /dev/null +++ b/experimental/packages/opentelemetry-browser-detector/src/types.ts @@ -0,0 +1,28 @@ +/* + * 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 type UserAgentData = { + brands: {brand:string,version:string}[], + platform: string, + mobile: boolean +}; + +export const BROWSER_ATTRIBUTES = { + PLATFORM: 'browser.platform', //TODO replace with SemantecConventions attribute when available + BRANDS: 'browser.brands', + MOBILE: 'browser.mobile', + LANGUAGE: 'browser.language', + USER_AGENT: 'browser.user_agent' +}; diff --git a/experimental/packages/opentelemetry-browser-detector/test/BrowserDetector.test.ts b/experimental/packages/opentelemetry-browser-detector/test/BrowserDetector.test.ts new file mode 100644 index 0000000000..07143c869b --- /dev/null +++ b/experimental/packages/opentelemetry-browser-detector/test/BrowserDetector.test.ts @@ -0,0 +1,88 @@ +/* + * 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 sinon from 'sinon'; +import { Resource } from '@opentelemetry/resources'; +import { browserDetector } from '../src/BrowserDetector'; +import { + describeBrowser, + assertResource, + assertEmptyResource +} from './util'; + +describeBrowser('browserDetector()', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should return browser information with userAgentData', async () => { + sinon.stub(globalThis, 'navigator').value({ + userAgent: 'dddd', + language: 'en-US', + userAgentData: { + platform: 'platform', + brands:[ + { + brand: 'Chromium', + version: '106' + }, + { + brand: 'Google Chrome', + version: '106' + }, + { + brand: 'Not;A=Brand', + version: '99' + } + ], + mobile: false + } + }); + + const resource: Resource = await browserDetector.detect(); + assertResource(resource, { + platform: 'platform', + brands: [ + 'Chromium 106', + 'Google Chrome 106', + 'Not;A=Brand 99' + ], + mobile: false, + language: 'en-US', + }); + }); + + it('should return browser information with userAgent', async () => { + sinon.stub(globalThis, 'navigator').value({ + userAgent: 'dddd', + language: 'en-US', + userAgentData: undefined, + }); + + const resource: Resource = await browserDetector.detect(); + assertResource(resource, { + language: 'en-US', + user_agent: 'dddd' + }); + }); + + it('should return empty resources if user agent is missing', async () => { + sinon.stub(globalThis, 'navigator').value({ + userAgent: '', + }); + const resource: Resource = await browserDetector.detect(); + assertEmptyResource(resource); + }); +}); diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/index-webpack.ts b/experimental/packages/opentelemetry-browser-detector/test/index-webpack.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/index-webpack.ts rename to experimental/packages/opentelemetry-browser-detector/test/index-webpack.ts diff --git a/experimental/packages/opentelemetry-browser-detector/test/util.ts b/experimental/packages/opentelemetry-browser-detector/test/util.ts new file mode 100644 index 0000000000..62be8b3c2e --- /dev/null +++ b/experimental/packages/opentelemetry-browser-detector/test/util.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 {Suite} from 'mocha'; +import * as assert from 'assert'; +import {BROWSER_ATTRIBUTES} from '../src/types'; +import {Resource} from '@opentelemetry/resources'; + +export function describeBrowser(title: string, fn: (this: Suite) => void) { + title = `Browser: ${title}`; + if (typeof process === 'object' && process.release?.name === 'node') { + return describe.skip(title, fn); + } + return describe(title, fn); +} + +export const assertResource = ( + resource: Resource, + validations: { + platform?: string, + brands?: string[], + mobile?: boolean, + language?: string, + user_agent?: string + } +) => { + if (validations.platform) { + assert.strictEqual( + resource.attributes[BROWSER_ATTRIBUTES.PLATFORM], + validations.platform + ); + } + if (validations.brands) { + assert.ok(Array.isArray(resource.attributes[BROWSER_ATTRIBUTES.BRANDS])); + assert.deepStrictEqual( + (resource.attributes[BROWSER_ATTRIBUTES.BRANDS] as string[]), + validations.brands + ); + } + if (validations.mobile) { + assert.strictEqual( + (resource.attributes[BROWSER_ATTRIBUTES.MOBILE]), + validations.mobile + ); + } + if (validations.language) { + assert.strictEqual( + (resource.attributes[BROWSER_ATTRIBUTES.LANGUAGE]), + validations.language + ); + } + if (validations.user_agent) { + assert.strictEqual( + (resource.attributes[BROWSER_ATTRIBUTES.USER_AGENT]), + validations.user_agent + ); + } +}; + +/** + * Test utility method to validate an empty resource + * + * @param resource the Resource to validate + */ +export const assertEmptyResource = (resource: Resource) => { + assert.strictEqual(Object.keys(resource.attributes).length, 0); +}; diff --git a/experimental/packages/opentelemetry-sdk-metrics/tsconfig.all.json b/experimental/packages/opentelemetry-browser-detector/tsconfig.all.json similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/tsconfig.all.json rename to experimental/packages/opentelemetry-browser-detector/tsconfig.all.json diff --git a/experimental/packages/opentelemetry-sdk-metrics/tsconfig.esm.json b/experimental/packages/opentelemetry-browser-detector/tsconfig.esm.json similarity index 69% rename from experimental/packages/opentelemetry-sdk-metrics/tsconfig.esm.json rename to experimental/packages/opentelemetry-browser-detector/tsconfig.esm.json index 53f9deefd0..379f547a46 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/tsconfig.esm.json +++ b/experimental/packages/opentelemetry-browser-detector/tsconfig.esm.json @@ -7,10 +7,5 @@ }, "include": [ "src/**/*.ts" - ], - "references": [ - { - "path": "../opentelemetry-api-metrics/tsconfig.esm.json" - } ] } diff --git a/experimental/packages/opentelemetry-sdk-metrics/tsconfig.esnext.json b/experimental/packages/opentelemetry-browser-detector/tsconfig.esnext.json similarity index 70% rename from experimental/packages/opentelemetry-sdk-metrics/tsconfig.esnext.json rename to experimental/packages/opentelemetry-browser-detector/tsconfig.esnext.json index 5b297a614a..cb78dd6ff3 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/tsconfig.esnext.json +++ b/experimental/packages/opentelemetry-browser-detector/tsconfig.esnext.json @@ -7,10 +7,5 @@ }, "include": [ "src/**/*.ts" - ], - "references": [ - { - "path": "../opentelemetry-api-metrics/tsconfig.esnext.json" - } ] } diff --git a/experimental/packages/opentelemetry-sdk-metrics/tsconfig.json b/experimental/packages/opentelemetry-browser-detector/tsconfig.json similarity index 75% rename from experimental/packages/opentelemetry-sdk-metrics/tsconfig.json rename to experimental/packages/opentelemetry-browser-detector/tsconfig.json index 53d44a09df..6294198a83 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/tsconfig.json +++ b/experimental/packages/opentelemetry-browser-detector/tsconfig.json @@ -10,13 +10,13 @@ ], "references": [ { - "path": "../../../packages/opentelemetry-core" + "path": "../../../api" }, { "path": "../../../packages/opentelemetry-resources" }, { - "path": "../opentelemetry-api-metrics" + "path": "../../../packages/opentelemetry-semantic-conventions" } ] } diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/README.md b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/README.md index 382e657272..3aee8d08a4 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/README.md +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/README.md @@ -3,6 +3,8 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] +**Note: This is an experimental package under active development. New releases may include breaking changes.** + This module provides exporter for node to be used with OTLP (`grpc`) compatible receivers. Compatible with [opentelemetry-collector][opentelemetry-collector-url] versions `>=0.16 <=0.53`. @@ -15,22 +17,25 @@ npm install --save @opentelemetry/exporter-metrics-otlp-grpc ## Service Name The OpenTelemetry Collector Exporter does not have a service name configuration. -In order to set the service name, use the `service.name` resource attribute as prescribed in the [OpenTelemetry Resource Semantic Conventions][semconv-resource-service-name]. -To see sample code and documentation for the traces exporter, as well as instructions for using TLS, visit the [Collector Trace Exporter for web and node][trace-exporter-url]. +In order to set the service name, use the `service.name` resource attribute as prescribed in +the [OpenTelemetry Resource Semantic Conventions][semconv-resource-service-name]. +To see sample code and documentation for the traces exporter, as well as instructions for using TLS, visit +the [Collector Trace Exporter for web and node][trace-exporter-url]. ## Metrics in Node - GRPC -The OTLPMetricsExporter 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. +The OTLPMetricsExporter 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, PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics'); -const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-grpc'); +const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-grpc'); const collectorOptions = { // url is optional and can be omitted - default is http://localhost:4317 url: 'http://:', }; -const exporter = new OTLPMetricExporter(collectorOptions); +const metricExporter = new OTLPMetricExporter(collectorOptions); const meterProvider = new MeterProvider({}); meterProvider.addMetricReader(new PeriodicExportingMetricReader({ @@ -50,20 +55,24 @@ counter.add(10, { 'key': 'value' }); ## Environment Variable Configuration - | Environment variable | Description | - |----------------------|-------------| - | OTEL_EXPORTER_OTLP_METRICS_COMPRESSION | The compression type to use on OTLP metric requests. Options include gzip. By default no compression will be used. | - | OTEL_EXPORTER_OTLP_COMPRESSION | The compression type to use on OTLP trace, metric, and log requests. Options include gzip. By default no compression will be used. | - | OTEL_EXPORTER_OTLP_METRICS_INSECURE | Whether to enable client transport security for the exporter's gRPC connection for metric requests. This option only applies to OTLP/gRPC when an endpoint is provided without the http or https scheme. Options include true or false. By default insecure is false which creates a secure connection. | - | OTEL_EXPORTER_OTLP_INSECURE | Whether to enable client transport security for the exporter's gRPC connection for trace, metric and log requests. This option only applies to OTLP/gRPC when an endpoint is provided without the http or https scheme. Options include true or false. By default insecure is false which creates a secure connection. | - | OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE | The path to the file containing trusted root certificate to use when verifying an OTLP metric server's TLS credentials. By default the host platform's trusted root certificate is used.| - | OTEL_EXPORTER_OTLP_CERTIFICATE | The path to the file containing trusted root certificate to use when verifying an OTLP trace, metric, or log server's TLS credentials. By default the host platform's trusted root certificate is used. | - | OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY | The path to the file containing private client key to use when verifying an OTLP metric client's TLS credentials. Must provide a client certificate/chain when providing a private client key. By default no client key file is used. | - | OTEL_EXPORTER_OTLP_CLIENT_KEY | The path to the file containing private client key to use when verifying an OTLP trace, metric or log client's TLS credentials. Must provide a client certificate/chain when providing a private client key. By default no client key file is used. | - | OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE | The path to the file containing trusted client certificate/chain for clients private key to use when verifying an OTLP metric server's TLS credentials. Must provide a private client key when providing a certificate/chain. By default no chain file is used. | - | OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE | The path to the file containing trusted client certificate/chain for clients private key to use when verifying an OTLP trace, metric and log server's TLS credentials. Must provide a private client key when providing a certificate/chain. By default no chain file is used. | - - > Settings configured programmatically take precedence over environment variables. Per-signal environment variables take precedence over non-per-signal environment variables. +In addition to settings passed to the constructor, the exporter also supports configuration via environment variables: + +| Environment variable | Description | +|----------------------|-------------| +| OTEL_EXPORTER_OTLP_ENDPOINT | The endpoint to send metrics to. This will also be used for the traces exporter if `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` is not configured. By default `localhost:4317` will be used. | +| OTEL_EXPORTER_OTLP_METRICS_ENDPOINT | The endpoint to send metrics to. By default `localhost:4317` will be used. | +| OTEL_EXPORTER_OTLP_COMPRESSION | The compression type to use on OTLP trace, metric, and log requests. Options include gzip. By default no compression will be used. | +| OTEL_EXPORTER_OTLP_METRICS_INSECURE | Whether to enable client transport security for the exporter's gRPC connection for metric requests. This option only applies to OTLP/gRPC when an endpoint is provided without the http or https scheme. Options include true or false. By default insecure is false which creates a secure connection. | +| OTEL_EXPORTER_OTLP_INSECURE | Whether to enable client transport security for the exporter's gRPC connection for trace, metric and log requests. This option only applies to OTLP/gRPC when an endpoint is provided without the http or https scheme. Options include true or false. By default insecure is false which creates a secure connection. | +| OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE | The path to the file containing trusted root certificate to use when verifying an OTLP metric server's TLS credentials. By default the host platform's trusted root certificate is used.| +| OTEL_EXPORTER_OTLP_CERTIFICATE | The path to the file containing trusted root certificate to use when verifying an OTLP trace, metric, or log server's TLS credentials. By default the host platform's trusted root certificate is used. | +| OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY | The path to the file containing private client key to use when verifying an OTLP metric client's TLS credentials. Must provide a client certificate/chain when providing a private client key. By default no client key file is used. | +| OTEL_EXPORTER_OTLP_CLIENT_KEY | The path to the file containing private client key to use when verifying an OTLP trace, metric or log client's TLS credentials. Must provide a client certificate/chain when providing a private client key. By default no client key file is used. | +| OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE | The path to the file containing trusted client certificate/chain for clients private key to use when verifying an OTLP metric server's TLS credentials. Must provide a private client key when providing a certificate/chain. By default no chain file is used. | +| OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE | The path to the file containing trusted client certificate/chain for clients private key to use when verifying an OTLP trace, metric and log server's TLS credentials. Must provide a private client key when providing a certificate/chain. By default no chain file is used. | +| OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE | The exporters aggregation temporality preference. Valid values are `cumulative`, and `delta`. `cumulative` selects cumulative temporality for all instrument kinds. `delta` selects delta aggregation temporality for Counter, Asynchronous Counter and Histogram instrument kinds, and selects cumulative aggregation for UpDownCounter and Asynchronous UpDownCounter instrument kinds. By default `cumulative` is used. | + +> Settings configured programmatically take precedence over environment variables. Per-signal environment variables take precedence over non-per-signal environment variables. ## Running opentelemetry-collector locally to see the metrics diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/package.json b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/package.json index 362fdae821..c56fa61892 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/package.json +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/exporter-metrics-otlp-grpc", - "version": "0.32.0", + "version": "0.34.0", "description": "OpenTelemetry Collector Metrics Exporter allows user to send collected metrics to the OpenTelemetry Collector", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -48,9 +48,9 @@ }, "devDependencies": { "@babel/core": "7.16.0", - "@opentelemetry/api": "^1.0.0", - "@opentelemetry/api-metrics": "0.32.0", - "@types/mocha": "9.1.1", + "@grpc/proto-loader": "^0.7.3", + "@opentelemetry/api": "^1.3.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "codecov": "3.8.3", @@ -64,17 +64,17 @@ "typescript": "4.4.4" }, "peerDependencies": { - "@opentelemetry/api": "^1.0.0" + "@opentelemetry/api": "^1.3.0" }, "dependencies": { - "@grpc/grpc-js": "^1.5.9", - "@grpc/proto-loader": "^0.6.9", - "@opentelemetry/core": "1.6.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.32.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.32.0", - "@opentelemetry/otlp-transformer": "0.32.0", - "@opentelemetry/resources": "1.6.0", - "@opentelemetry/sdk-metrics": "0.32.0" + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.8.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.34.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.34.0", + "@opentelemetry/otlp-transformer": "0.34.0", + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/sdk-metrics": "1.8.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc", + "sideEffects": false } diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/src/OTLPMetricExporter.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/src/OTLPMetricExporter.ts index 5f7d979fd3..3a512e3df4 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/src/OTLPMetricExporter.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/src/OTLPMetricExporter.ts @@ -15,7 +15,6 @@ */ import { - defaultOptions, OTLPMetricExporterBase, OTLPMetricExporterOptions } from '@opentelemetry/exporter-metrics-otlp-http'; @@ -33,7 +32,7 @@ import { createExportMetricsServiceRequest, IExportMetricsServiceRequest } from class OTLPMetricExporterProxy extends OTLPGRPCExporterNodeBase { - constructor(config: OTLPGRPCExporterConfigNode & OTLPMetricExporterOptions= defaultOptions) { + constructor(config?: OTLPGRPCExporterConfigNode & OTLPMetricExporterOptions) { super(config); const headers = baggageUtils.parseKeyPairsIntoRecord(getEnv().OTEL_EXPORTER_OTLP_METRICS_HEADERS); this.metadata ||= new Metadata(); @@ -73,7 +72,7 @@ class OTLPMetricExporterProxy extends OTLPGRPCExporterNodeBase{ - constructor(config: OTLPGRPCExporterConfigNode & OTLPMetricExporterOptions = defaultOptions) { + constructor(config?: OTLPGRPCExporterConfigNode & OTLPMetricExporterOptions) { super(new OTLPMetricExporterProxy(config), config); } } diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/OTLPMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/OTLPMetricExporter.test.ts index e6d4ced369..08a30aca3e 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/OTLPMetricExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/OTLPMetricExporter.test.ts @@ -214,13 +214,18 @@ const testOTLPMetricExporter = (params: TestParams) => assert.ok(exportedData, 'exportedData does not exist'); + // The order of the metrics is not guaranteed. + const counterIndex = exportedData[0].scopeMetrics[0].metrics.findIndex(it => it.name === 'int-counter'); + const observableIndex = exportedData[0].scopeMetrics[0].metrics.findIndex(it => it.name === 'double-observable-gauge'); + const histogramIndex = exportedData[0].scopeMetrics[0].metrics.findIndex(it => it.name === 'int-histogram'); + const resource = exportedData[0].resource; const counter = - exportedData[0].scopeMetrics[0].metrics[0]; + exportedData[0].scopeMetrics[0].metrics[counterIndex]; const observableGauge = - exportedData[0].scopeMetrics[0].metrics[1]; + exportedData[0].scopeMetrics[0].metrics[observableIndex]; const histogram = - exportedData[0].scopeMetrics[0].metrics[2]; + exportedData[0].scopeMetrics[0].metrics[histogramIndex]; ensureExportedCounterIsCorrect( counter, counter.sum?.dataPoints[0].timeUnixNano, diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/metricsHelper.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/metricsHelper.ts index 88d91a872b..9a11c8b6f2 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/metricsHelper.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/metricsHelper.ts @@ -14,13 +14,12 @@ * limitations under the License. */ -import { Counter, Histogram, ObservableGauge, ObservableResult, ValueType } from '@opentelemetry/api-metrics'; +import { Counter, Histogram, ObservableGauge, ObservableResult, ValueType } from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; import * as assert from 'assert'; import * as grpc from '@grpc/grpc-js'; import { VERSION } from '@opentelemetry/core'; import { - AggregationTemporality, ExplicitBucketHistogramAggregation, MeterProvider, MetricReader, @@ -29,10 +28,6 @@ import { import { IKeyValue, IMetric, IResource } from '@opentelemetry/otlp-transformer'; class TestMetricReader extends MetricReader { - selectAggregationTemporality() { - return AggregationTemporality.CUMULATIVE; - } - protected onForceFlush(): Promise { return Promise.resolve(undefined); } @@ -42,11 +37,11 @@ class TestMetricReader extends MetricReader { } } -const testResource = Resource.default().merge(new Resource({ +const testResource = new Resource({ service: 'ui', version: 1, cost: 112.12, -})); +}); let meterProvider = new MeterProvider({ resource: testResource }); diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/tsconfig.json b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/tsconfig.json index b6a4e45748..2ba3d74b8b 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/tsconfig.json +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../../api" + }, { "path": "../../../packages/opentelemetry-core" }, @@ -16,14 +19,11 @@ "path": "../../../packages/opentelemetry-resources" }, { - "path": "../opentelemetry-api-metrics" + "path": "../../../packages/sdk-metrics" }, { "path": "../opentelemetry-exporter-metrics-otlp-http" }, - { - "path": "../opentelemetry-sdk-metrics" - }, { "path": "../otlp-grpc-exporter-base" }, diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/README.md b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/README.md index 644a5b3449..4dcbda9dae 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/README.md +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/README.md @@ -3,6 +3,8 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] +**Note: This is an experimental package under active development. New releases may include breaking changes.** + This module provides exporter for web and node to be used with OTLP (`http/json`) compatible receivers. Compatible with [opentelemetry-collector][opentelemetry-collector-url] versions `>=0.52 <=0.53`. @@ -15,8 +17,10 @@ npm install --save @opentelemetry/exporter-metrics-otlp-http ## Service Name The OpenTelemetry Collector Metrics Exporter does not have a service name configuration. -In order to set the service name, use the `service.name` resource attribute as prescribed in the [OpenTelemetry Resource Semantic Conventions][semconv-resource-service-name]. -To see sample code and documentation for the traces exporter, visit the [Collector Trace Exporter for web and node][trace-exporter-url]. +In order to set the service name, use the `service.name` resource attribute as prescribed in +the [OpenTelemetry Resource Semantic Conventions][semconv-resource-service-name]. +To see sample code and documentation for the traces exporter, visit +the [Collector Trace Exporter for web and node][trace-exporter-url]. ## Metrics in Web @@ -25,12 +29,13 @@ The OTLPMetricExporter in Web expects the endpoint to end in `/v1/metrics`. ```js import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'; import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'; + const collectorOptions = { url: '', // url is optional and can be omitted - default is http://localhost:4318/v1/metrics headers: {}, // an optional object containing custom headers to be sent with each request concurrencyLimit: 1, // an optional limit on pending requests }; -const exporter = new OTLPMetricExporter(collectorOptions); +const metricExporter = new OTLPMetricExporter(collectorOptions); const meterProvider = new MeterProvider({}); meterProvider.addMetricReader(new PeriodicExportingMetricReader({ @@ -48,7 +53,7 @@ counter.add(10, { 'key': 'value' }); ```js const { MeterProvider, PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics'); -const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http'); +const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http'); const collectorOptions = { url: '', // url is optional and can be omitted - default is http://localhost:4318/v1/metrics concurrencyLimit: 1, // an optional limit on pending requests @@ -68,35 +73,18 @@ counter.add(10, { 'key': 'value' }); ``` -## GRPC - -For exporting metrics with GRPC please check [exporter-metrics-otlp-grpc][npm-url-grpc] - -## PROTOBUF - -For exporting metrics with PROTOBUF please check [exporter-metrics-otlp-proto][npm-url-proto] - -## Configuration options as environment variables - -Instead of providing options to `OTLPMetricExporter` and `OTLPTraceExporter` explicitly, environment variables may be provided instead. - -```sh -OTEL_EXPORTER_OTLP_ENDPOINT=https://localhost:4318 -# this will automatically append the version and signal path -# e.g. https://localhost:4318/v1/traces for `OTLPTraceExporter` and https://localhost:4318/v1/metrics for `OTLPMetricExporter` -``` - -If the trace and metric exporter endpoints have different providers, the env var for per-signal endpoints are available to use +## Environment Variable Configuration -```sh -OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://trace-service:4318/v1/traces -OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://metric-service:4318/v1/metrics -# version and signal needs to be explicit -``` +In addition to settings passed to the constructor, the exporter also supports configuration via environment variables: -> The per-signal endpoints take precedence and overrides `OTEL_EXPORTER_OTLP_ENDPOINT` +| Environment variable | Description | +|---------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| OTEL_EXPORTER_OTLP_ENDPOINT | The endpoint to send metrics to. This will also be used for the traces exporter if `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` is not configured. By default `http://localhost:4318` will be used. `/v1/metrics` will be automatically appended to configured values. | +| OTEL_EXPORTER_OTLP_METRICS_ENDPOINT | The endpoint to send metrics to. By default `https://localhost:4318/v1/metrics` will be used. `v1/metrics` will not be appended automatically and has to be added explicitly. | +| OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE | The exporters aggregation temporality preference. Valid values are `cumulative`, and `delta`. `cumulative` selects cumulative temporality for all instrument kinds. `delta` selects delta aggregation temporality for Counter, Asynchronous Counter and Histogram instrument kinds, and selects cumulative aggregation for UpDownCounter and Asynchronous UpDownCounter instrument kinds. By default `cumulative` is used. | -For more details, see [OpenTelemetry Specification on Protocol Exporter][opentelemetry-spec-protocol-exporter]. +> Settings configured programmatically take precedence over environment variables. Per-signal environment variables take +> precedence over non-per-signal environment variables. ## Running opentelemetry-collector locally to see the metrics @@ -108,6 +96,8 @@ For more details, see [OpenTelemetry Specification on Protocol Exporter][opentel - For more information on OpenTelemetry, visit: - For more about OpenTelemetry JavaScript: - For help or feedback on this project, join us in [GitHub Discussions][discussions-url] +- For exporting metrics via gRPC please check [exporter-metrics-otlp-grpc][npm-url-grpc] +- For exporting metrics via protobuf please check [exporter-metrics-otlp-proto][npm-url-proto] ## License diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/package.json b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/package.json index 91a50f0125..e17aa4f5e9 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/package.json +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/exporter-metrics-otlp-http", - "version": "0.32.0", + "version": "0.34.0", "description": "OpenTelemetry Collector Metrics Exporter allows user to send collected metrics to the OpenTelemetry Collector", "main": "build/src/index.js", "module": "build/esm/index.js", @@ -64,8 +64,8 @@ }, "devDependencies": { "@babel/core": "7.16.0", - "@opentelemetry/api": "^1.0.0", - "@types/mocha": "9.1.1", + "@opentelemetry/api": "^1.3.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "@types/webpack-env": "1.16.3", @@ -91,15 +91,15 @@ "webpack-merge": "5.8.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.0.0" + "@opentelemetry/api": "^1.3.0" }, "dependencies": { - "@opentelemetry/api-metrics": "0.32.0", - "@opentelemetry/core": "1.6.0", - "@opentelemetry/otlp-exporter-base": "0.32.0", - "@opentelemetry/otlp-transformer": "0.32.0", - "@opentelemetry/resources": "1.6.0", - "@opentelemetry/sdk-metrics": "0.32.0" + "@opentelemetry/core": "1.8.0", + "@opentelemetry/otlp-exporter-base": "0.34.0", + "@opentelemetry/otlp-transformer": "0.34.0", + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/sdk-metrics": "1.8.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-metrics-otlp-http" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-metrics-otlp-http", + "sideEffects": false } diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/OTLPMetricExporterBase.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/OTLPMetricExporterBase.ts index b970a150d0..fca5a54d25 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/OTLPMetricExporterBase.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/OTLPMetricExporterBase.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { ExportResult } from '@opentelemetry/core'; +import { + ExportResult, + getEnv +} from '@opentelemetry/core'; import { AggregationTemporality, AggregationTemporalitySelector, @@ -22,9 +25,12 @@ import { PushMetricExporter, ResourceMetrics } from '@opentelemetry/sdk-metrics'; -import { defaultOptions, OTLPMetricExporterOptions } from './OTLPMetricExporterOptions'; +import { + OTLPMetricExporterOptions +} from './OTLPMetricExporterOptions'; import { OTLPExporterBase } from '@opentelemetry/otlp-exporter-base'; import { IExportMetricsServiceRequest } from '@opentelemetry/otlp-transformer'; +import { diag } from '@opentelemetry/api'; export const CumulativeTemporalitySelector: AggregationTemporalitySelector = () => AggregationTemporality.CUMULATIVE; @@ -41,14 +47,33 @@ export const DeltaTemporalitySelector: AggregationTemporalitySelector = (instrum } }; -function chooseTemporalitySelector(temporalityPreference?: AggregationTemporality): AggregationTemporalitySelector { - if (temporalityPreference === AggregationTemporality.DELTA) { +function chooseTemporalitySelectorFromEnvironment() { + const env = getEnv(); + const configuredTemporality = env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE.trim().toLowerCase(); + + if (configuredTemporality === 'cumulative') { + return CumulativeTemporalitySelector; + } + if (configuredTemporality === 'delta') { return DeltaTemporalitySelector; } + diag.warn(`OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE is set to '${env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE}', but only 'cumulative' and 'delta' are allowed. Using default ('cumulative') instead.`); return CumulativeTemporalitySelector; } +function chooseTemporalitySelector(temporalityPreference?: AggregationTemporality): AggregationTemporalitySelector { + // Directly passed preference has priority. + if (temporalityPreference != null) { + if (temporalityPreference === AggregationTemporality.DELTA) { + return DeltaTemporalitySelector; + } + return CumulativeTemporalitySelector; + } + + return chooseTemporalitySelectorFromEnvironment(); +} + export class OTLPMetricExporterBase> @@ -57,9 +82,9 @@ implements PushMetricExporter { protected _aggregationTemporalitySelector: AggregationTemporalitySelector; constructor(exporter: T, - config: OTLPMetricExporterOptions = defaultOptions) { + config?: OTLPMetricExporterOptions) { this._otlpExporter = exporter; - this._aggregationTemporalitySelector = chooseTemporalitySelector(config.temporalityPreference); + this._aggregationTemporalitySelector = chooseTemporalitySelector(config?.temporalityPreference); } export(metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void): void { diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/OTLPMetricExporterOptions.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/OTLPMetricExporterOptions.ts index 986a85890b..324c4ea27f 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/OTLPMetricExporterOptions.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/OTLPMetricExporterOptions.ts @@ -20,5 +20,3 @@ import { OTLPExporterConfigBase } from '@opentelemetry/otlp-exporter-base'; export interface OTLPMetricExporterOptions extends OTLPExporterConfigBase { temporalityPreference?: AggregationTemporality } -export const defaultExporterTemporality = AggregationTemporality.CUMULATIVE; -export const defaultOptions = {temporalityPreference: defaultExporterTemporality}; diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/browser/OTLPMetricExporter.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/browser/OTLPMetricExporter.ts index 2a7b14d219..00c7c0c32c 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/browser/OTLPMetricExporter.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/browser/OTLPMetricExporter.ts @@ -16,7 +16,7 @@ import { ResourceMetrics } from '@opentelemetry/sdk-metrics'; import { baggageUtils, getEnv } from '@opentelemetry/core'; -import { defaultOptions, OTLPMetricExporterOptions } from '../../OTLPMetricExporterOptions'; +import { OTLPMetricExporterOptions } from '../../OTLPMetricExporterOptions'; import { OTLPMetricExporterBase } from '../../OTLPMetricExporterBase'; import { OTLPExporterBrowserBase, @@ -31,7 +31,7 @@ const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURC class OTLPExporterBrowserProxy extends OTLPExporterBrowserBase { - constructor(config: OTLPMetricExporterOptions & OTLPExporterConfigBase = defaultOptions) { + constructor(config?: OTLPMetricExporterOptions & OTLPExporterConfigBase) { super(config); this._headers = Object.assign( this._headers, @@ -45,7 +45,7 @@ class OTLPExporterBrowserProxy extends OTLPExporterBrowserBase 0 - ? appendRootPathToUrlIfNeeded(getEnv().OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, DEFAULT_COLLECTOR_RESOURCE_PATH) + ? appendRootPathToUrlIfNeeded(getEnv().OTEL_EXPORTER_OTLP_METRICS_ENDPOINT) : getEnv().OTEL_EXPORTER_OTLP_ENDPOINT.length > 0 ? appendResourcePathToUrl(getEnv().OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_COLLECTOR_RESOURCE_PATH) : DEFAULT_COLLECTOR_URL; @@ -60,7 +60,7 @@ class OTLPExporterBrowserProxy extends OTLPExporterBrowserBase { - constructor(config: OTLPExporterConfigBase & OTLPMetricExporterOptions = defaultOptions) { + constructor(config?: OTLPExporterConfigBase & OTLPMetricExporterOptions) { super(new OTLPExporterBrowserProxy(config), config); } } diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/node/OTLPMetricExporter.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/node/OTLPMetricExporter.ts index 2008b88662..d09693a253 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/node/OTLPMetricExporter.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/node/OTLPMetricExporter.ts @@ -16,7 +16,7 @@ import { ResourceMetrics } from '@opentelemetry/sdk-metrics'; import { getEnv, baggageUtils} from '@opentelemetry/core'; -import { defaultOptions, OTLPMetricExporterOptions } from '../../OTLPMetricExporterOptions'; +import { OTLPMetricExporterOptions } from '../../OTLPMetricExporterOptions'; import { OTLPMetricExporterBase } from '../../OTLPMetricExporterBase'; import { OTLPExporterNodeBase, @@ -31,7 +31,7 @@ const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURC class OTLPExporterNodeProxy extends OTLPExporterNodeBase { - constructor(config: OTLPExporterNodeConfigBase & OTLPMetricExporterOptions = defaultOptions) { + constructor(config?: OTLPExporterNodeConfigBase & OTLPMetricExporterOptions) { super(config); this.headers = Object.assign( this.headers, @@ -49,7 +49,7 @@ class OTLPExporterNodeProxy extends OTLPExporterNodeBase 0 - ? appendRootPathToUrlIfNeeded(getEnv().OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, DEFAULT_COLLECTOR_RESOURCE_PATH) + ? appendRootPathToUrlIfNeeded(getEnv().OTEL_EXPORTER_OTLP_METRICS_ENDPOINT) : getEnv().OTEL_EXPORTER_OTLP_ENDPOINT.length > 0 ? appendResourcePathToUrl(getEnv().OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_COLLECTOR_RESOURCE_PATH) : DEFAULT_COLLECTOR_URL; @@ -60,7 +60,7 @@ class OTLPExporterNodeProxy extends OTLPExporterNodeBase { - constructor(config: OTLPExporterNodeConfigBase & OTLPMetricExporterOptions = defaultOptions) { + constructor(config?: OTLPExporterNodeConfigBase & OTLPMetricExporterOptions) { super(new OTLPExporterNodeProxy(config), config); } } diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts index b0b95ce0be..c0cb9dc2ab 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { diag, DiagLogger, DiagLogLevel } from '@opentelemetry/api'; -import { Counter, Histogram, } from '@opentelemetry/api-metrics'; +import { diag, DiagLogger, DiagLogLevel, Counter, Histogram } from '@opentelemetry/api'; import { ExportResultCode, hrTimeToNanoseconds } from '@opentelemetry/core'; import { AggregationTemporality, ResourceMetrics, } from '@opentelemetry/sdk-metrics'; import * as assert from 'assert'; @@ -99,6 +98,7 @@ describe('OTLPMetricExporter - web', () => { temporalityPreference: AggregationTemporality.CUMULATIVE }); }); + it('should successfully send metrics using sendBeacon', done => { collectorExporter.export(metrics, () => { }); @@ -109,16 +109,22 @@ describe('OTLPMetricExporter - web', () => { const blob: Blob = args[1]; const body = await blob.text(); const json = JSON.parse(body) as IExportMetricsServiceRequest; - const metric1 = json.resourceMetrics[0].scopeMetrics[0].metrics[0]; - const metric2 = json.resourceMetrics[0].scopeMetrics[0].metrics[1]; - const metric3 = json.resourceMetrics[0].scopeMetrics[0].metrics[2]; + + // The order of the metrics is not guaranteed. + const counterIndex = metrics.scopeMetrics[0].metrics.findIndex(it => it.descriptor.name === 'int-counter'); + const observableIndex = metrics.scopeMetrics[0].metrics.findIndex(it => it.descriptor.name === 'double-observable-gauge2'); + const histogramIndex = metrics.scopeMetrics[0].metrics.findIndex(it => it.descriptor.name === 'int-histogram'); + + const metric1 = json.resourceMetrics[0].scopeMetrics[0].metrics[counterIndex]; + const metric2 = json.resourceMetrics[0].scopeMetrics[0].metrics[observableIndex]; + const metric3 = json.resourceMetrics[0].scopeMetrics[0].metrics[histogramIndex]; assert.ok(typeof metric1 !== 'undefined', "metric doesn't exist"); ensureCounterIsCorrect( metric1, - hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[0].dataPoints[0].endTime), - hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[0].dataPoints[0].startTime) + hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0].endTime), + hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0].startTime) ); @@ -128,8 +134,8 @@ describe('OTLPMetricExporter - web', () => { ); ensureObservableGaugeIsCorrect( metric2, - hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[1].dataPoints[0].endTime), - hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[1].dataPoints[0].startTime), + hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0].endTime), + hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0].startTime), 6, 'double-observable-gauge2' ); @@ -140,8 +146,8 @@ describe('OTLPMetricExporter - web', () => { ); ensureHistogramIsCorrect( metric3, - hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[2].dataPoints[0].endTime), - hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[2].dataPoints[0].startTime), + hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0].endTime), + hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0].startTime), [0, 100], [0, 2, 0] ); @@ -216,15 +222,20 @@ describe('OTLPMetricExporter - web', () => { const body = request.requestBody; const json = JSON.parse(body) as IExportMetricsServiceRequest; - const metric1 = json.resourceMetrics[0].scopeMetrics[0].metrics[0]; - const metric2 = json.resourceMetrics[0].scopeMetrics[0].metrics[1]; - const metric3 = json.resourceMetrics[0].scopeMetrics[0].metrics[2]; + // The order of the metrics is not guaranteed. + const counterIndex = metrics.scopeMetrics[0].metrics.findIndex(it => it.descriptor.name === 'int-counter'); + const observableIndex = metrics.scopeMetrics[0].metrics.findIndex(it => it.descriptor.name === 'double-observable-gauge2'); + const histogramIndex = metrics.scopeMetrics[0].metrics.findIndex(it => it.descriptor.name === 'int-histogram'); + + const metric1 = json.resourceMetrics[0].scopeMetrics[0].metrics[counterIndex]; + const metric2 = json.resourceMetrics[0].scopeMetrics[0].metrics[observableIndex]; + const metric3 = json.resourceMetrics[0].scopeMetrics[0].metrics[histogramIndex]; assert.ok(typeof metric1 !== 'undefined', "metric doesn't exist"); ensureCounterIsCorrect( metric1, - hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[0].dataPoints[0].endTime), - hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[0].dataPoints[0].startTime) + hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0].endTime), + hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0].startTime) ); assert.ok( @@ -233,8 +244,8 @@ describe('OTLPMetricExporter - web', () => { ); ensureObservableGaugeIsCorrect( metric2, - hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[1].dataPoints[0].endTime), - hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[1].dataPoints[0].startTime), + hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0].endTime), + hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0].startTime), 6, 'double-observable-gauge2' ); @@ -245,8 +256,8 @@ describe('OTLPMetricExporter - web', () => { ); ensureHistogramIsCorrect( metric3, - hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[2].dataPoints[0].endTime), - hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[2].dataPoints[0].startTime), + hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0].endTime), + hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0].startTime), [0, 100], [0, 2, 0] ); diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/common/CollectorMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/common/CollectorMetricExporter.test.ts index 38c0b3bcc7..406e20f612 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/common/CollectorMetricExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/common/CollectorMetricExporter.test.ts @@ -20,22 +20,34 @@ import { } from '@opentelemetry/sdk-metrics'; import * as assert from 'assert'; import * as sinon from 'sinon'; -import { collect, mockCounter, mockObservableGauge, setUp, shutdown } from '../metricsHelper'; -import { OTLPExporterBase, OTLPExporterConfigBase } from '@opentelemetry/otlp-exporter-base'; +import { + collect, + mockCounter, + mockObservableGauge, + setUp, + shutdown +} from '../metricsHelper'; +import { + OTLPExporterBase, + OTLPExporterConfigBase +} from '@opentelemetry/otlp-exporter-base'; import { IExportMetricsServiceRequest } from '@opentelemetry/otlp-transformer'; type CollectorExporterConfig = OTLPExporterConfigBase; -class OTLPMetricExporter extends OTLPExporterBase< - CollectorExporterConfig, + +class OTLPMetricExporter extends OTLPExporterBase { + IExportMetricsServiceRequest> { onInit() {} + onShutdown() {} + send() {} + getDefaultUrl(config: CollectorExporterConfig) { return config.url || ''; } + convert(metrics: ResourceMetrics[]): IExportMetricsServiceRequest { return { resourceMetrics: [] }; } @@ -125,7 +137,7 @@ describe('OTLPMetricExporter - common', () => { describe('when exporter is shutdown', () => { it( 'should not export anything but return callback with code' + - ' "FailedNotRetryable"', + ' "FailedNotRetryable"', async () => { await collectorExporter.shutdown(); spySend.resetHistory(); diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/metricsHelper.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/metricsHelper.ts index 40a08775bf..5989d870f6 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/metricsHelper.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/metricsHelper.ts @@ -22,12 +22,11 @@ import { ObservableCounter, ObservableGauge, ObservableUpDownCounter, -} from '@opentelemetry/api-metrics'; +} from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; import * as assert from 'assert'; import { InstrumentationScope, VERSION } from '@opentelemetry/core'; import { - AggregationTemporality, ExplicitBucketHistogramAggregation, MeterProvider, MetricReader, @@ -56,10 +55,6 @@ class TestMetricReader extends MetricReader { protected onShutdown(): Promise { return Promise.resolve(undefined); } - - selectAggregationTemporality() { - return AggregationTemporality.CUMULATIVE; - } } export const HISTOGRAM_AGGREGATION_VIEW = new View({ diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/CollectorMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/CollectorMetricExporter.test.ts index 9cdbaed7e0..68dd569d89 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/CollectorMetricExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/CollectorMetricExporter.test.ts @@ -15,12 +15,17 @@ */ -import { diag, DiagLogger } from '@opentelemetry/api'; +import { + diag, + DiagLogger +} from '@opentelemetry/api'; import * as core from '@opentelemetry/core'; import * as assert from 'assert'; import * as http from 'http'; import * as sinon from 'sinon'; import { + CumulativeTemporalitySelector, + DeltaTemporalitySelector, OTLPMetricExporterOptions } from '../../src'; @@ -28,22 +33,31 @@ import { OTLPMetricExporter } from '../../src/platform/node'; import { + collect, ensureCounterIsCorrect, ensureExportMetricsServiceRequestIsSet, - ensureObservableGaugeIsCorrect, ensureHistogramIsCorrect, + ensureObservableGaugeIsCorrect, + HISTOGRAM_AGGREGATION_VIEW, mockCounter, - mockObservableGauge, mockHistogram, - collect, - shutdown, + mockObservableGauge, setUp, - HISTOGRAM_AGGREGATION_VIEW, + shutdown, } from '../metricsHelper'; import { MockedResponse } from './nodeHelpers'; -import { AggregationTemporality, ResourceMetrics } from '@opentelemetry/sdk-metrics'; -import { Stream, PassThrough } from 'stream'; -import { OTLPExporterError, OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base'; +import { + AggregationTemporality, + ResourceMetrics +} from '@opentelemetry/sdk-metrics'; +import { + PassThrough, + Stream +} from 'stream'; +import { + OTLPExporterError, + OTLPExporterNodeConfigBase +} from '@opentelemetry/otlp-exporter-base'; import { IExportMetricsServiceRequest } from '@opentelemetry/otlp-transformer'; let fakeRequest: PassThrough; @@ -190,6 +204,34 @@ describe('OTLPMetricExporter - node with json over http', () => { envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = ''; envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); + it('should use delta temporality defined via env', () => { + for (const envValue of ['delta', 'DELTA', 'DeLTa', 'delta ']) { + envSource.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = envValue; + const exporter = new OTLPMetricExporter(); + assert.strictEqual(exporter['_aggregationTemporalitySelector'], DeltaTemporalitySelector); + } + }); + it('should use cumulative temporality defined via env', () => { + for (const envValue of ['cumulative', 'CUMULATIVE', 'CuMULaTIvE', 'cumulative ']) { + envSource.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = envValue; + const exporter = new OTLPMetricExporter(); + assert.strictEqual(exporter['_aggregationTemporalitySelector'], CumulativeTemporalitySelector); + } + }); + it('should configure cumulative temporality with invalid value in env', () => { + for (const envValue of ['invalid', ' ']) { + envSource.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = envValue; + const exporter = new OTLPMetricExporter(); + assert.strictEqual(exporter['_aggregationTemporalitySelector'], CumulativeTemporalitySelector); + } + }); + it('should respect explicit config over environment variable', () => { + envSource.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = 'cumulative'; + const exporter = new OTLPMetricExporter({ + temporalityPreference: AggregationTemporality.DELTA + }); + assert.strictEqual(exporter['_aggregationTemporalitySelector'], DeltaTemporalitySelector); + }); }); describe('export', () => { @@ -288,29 +330,34 @@ describe('OTLPMetricExporter - node with json over http', () => { const responseBody = buff.toString(); const json = JSON.parse(responseBody) as IExportMetricsServiceRequest; - const metric1 = json.resourceMetrics[0].scopeMetrics[0].metrics[0]; - const metric2 = json.resourceMetrics[0].scopeMetrics[0].metrics[1]; - const metric3 = json.resourceMetrics[0].scopeMetrics[0].metrics[2]; + // The order of the metrics is not guaranteed. + const counterIndex = metrics.scopeMetrics[0].metrics.findIndex(it => it.descriptor.name === 'int-counter'); + const observableIndex = metrics.scopeMetrics[0].metrics.findIndex(it => it.descriptor.name === 'double-observable-gauge2'); + const histogramIndex = metrics.scopeMetrics[0].metrics.findIndex(it => it.descriptor.name === 'int-histogram'); + + const metric1 = json.resourceMetrics[0].scopeMetrics[0].metrics[counterIndex]; + const metric2 = json.resourceMetrics[0].scopeMetrics[0].metrics[observableIndex]; + const metric3 = json.resourceMetrics[0].scopeMetrics[0].metrics[histogramIndex]; assert.ok(typeof metric1 !== 'undefined', "counter doesn't exist"); ensureCounterIsCorrect( metric1, - core.hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[0].dataPoints[0].endTime), - core.hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[0].dataPoints[0].startTime) + core.hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0].endTime), + core.hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0].startTime) ); assert.ok(typeof metric2 !== 'undefined', "observable gauge doesn't exist"); ensureObservableGaugeIsCorrect( metric2, - core.hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[1].dataPoints[0].endTime), - core.hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[1].dataPoints[0].startTime), + core.hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0].endTime), + core.hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0].startTime), 6, 'double-observable-gauge2' ); assert.ok(typeof metric3 !== 'undefined', "histogram doesn't exist"); ensureHistogramIsCorrect( metric3, - core.hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[2].dataPoints[0].endTime), - core.hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[2].dataPoints[0].startTime), + core.hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0].endTime), + core.hrTimeToNanoseconds(metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0].startTime), [0, 100], [0, 2, 0] ); diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/tsconfig.json b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/tsconfig.json index 132af2033a..7ae0cb4939 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/tsconfig.json +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/tsconfig.json @@ -10,16 +10,16 @@ ], "references": [ { - "path": "../../../packages/opentelemetry-core" + "path": "../../../api" }, { - "path": "../../../packages/opentelemetry-resources" + "path": "../../../packages/opentelemetry-core" }, { - "path": "../opentelemetry-api-metrics" + "path": "../../../packages/opentelemetry-resources" }, { - "path": "../opentelemetry-sdk-metrics" + "path": "../../../packages/sdk-metrics" }, { "path": "../otlp-exporter-base" diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/README.md b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/README.md index da8746fb5b..6a2e160bac 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/README.md +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/README.md @@ -3,6 +3,8 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] +**Note: This is an experimental package under active development. New releases may include breaking changes.** + This module provides exporter for node to be used with OTLP (`http/protobuf`) compatible receivers. Compatible with [opentelemetry-collector][opentelemetry-collector-url] versions `>=0.32 <=0.53`. @@ -26,7 +28,7 @@ const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-pr const collectorOptions = { url: '', // url is optional and can be omitted - default is http://localhost:4318/v1/metrics }; -const exporter = new OTLPMetricExporter(collectorOptions); +const metricExporter = new OTLPMetricExporter(collectorOptions); const meterProvider = new MeterProvider({}); meterProvider.addMetricReader(new PeriodicExportingMetricReader({ @@ -41,6 +43,19 @@ counter.add(10, { 'key': 'value' }); ``` +## Environment Variable Configuration + +In addition to settings passed to the constructor, the exporter also supports configuration via environment variables: + +| Environment variable | Description | +|---------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| OTEL_EXPORTER_OTLP_ENDPOINT | The endpoint to send metrics to. This will also be used for the traces exporter if `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` is not configured. By default `http://localhost:4318` will be used. `/v1/metrics` will be automatically appended to configured values. | +| OTEL_EXPORTER_OTLP_METRICS_ENDPOINT | The endpoint to send metrics to. By default `https://localhost:4318/v1/metrics` will be used. `v1/metrics` will not be appended automatically and has to be added explicitly. | +| OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE | The exporters aggregation temporality preference. Valid values are `cumulative`, and `delta`. `cumulative` selects cumulative temporality for all instrument kinds. `delta` selects delta aggregation temporality for Counter, Asynchronous Counter and Histogram instrument kinds, and selects cumulative aggregation for UpDownCounter and Asynchronous UpDownCounter instrument kinds. By default `cumulative` is used. | + +> Settings configured programmatically take precedence over environment variables. Per-signal environment variables take +> precedence over non-per-signal environment variables. + ## Running opentelemetry-collector locally to see the metrics 1. Go to `examples/otlp-exporter-node` diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/package.json b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/package.json index 9723a58a55..4305efa91f 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/package.json +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/exporter-metrics-otlp-proto", - "version": "0.32.0", + "version": "0.34.0", "description": "OpenTelemetry Collector Metrics Exporter allows user to send collected metrics to the OpenTelemetry Collector using protobuf over HTTP", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -48,9 +48,8 @@ }, "devDependencies": { "@babel/core": "7.16.0", - "@opentelemetry/api": "^1.0.0", - "@opentelemetry/api-metrics": "0.32.0", - "@types/mocha": "9.1.1", + "@opentelemetry/api": "^1.3.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "codecov": "3.8.3", @@ -64,18 +63,17 @@ "typescript": "4.4.4" }, "peerDependencies": { - "@opentelemetry/api": "^1.0.0" + "@opentelemetry/api": "^1.3.0" }, "dependencies": { - "@grpc/proto-loader": "0.6.9", - "@opentelemetry/core": "1.6.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.32.0", - "@opentelemetry/otlp-exporter-base": "0.32.0", - "@opentelemetry/otlp-proto-exporter-base": "0.32.0", - "@opentelemetry/otlp-transformer": "0.32.0", - "@opentelemetry/resources": "1.6.0", - "@opentelemetry/sdk-metrics": "0.32.0", - "protobufjs": "^6.9.0" + "@opentelemetry/core": "1.8.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.34.0", + "@opentelemetry/otlp-exporter-base": "0.34.0", + "@opentelemetry/otlp-proto-exporter-base": "0.34.0", + "@opentelemetry/otlp-transformer": "0.34.0", + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/sdk-metrics": "1.8.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-metrics-otlp-proto" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-metrics-otlp-proto", + "sideEffects": false } diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/src/OTLPMetricExporter.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/src/OTLPMetricExporter.ts index dec361e522..e62a8e0ece 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/src/OTLPMetricExporter.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/src/OTLPMetricExporter.ts @@ -15,7 +15,6 @@ */ import { - defaultOptions, OTLPMetricExporterOptions } from '@opentelemetry/exporter-metrics-otlp-http'; import { ServiceClientType, OTLPProtoExporterNodeBase } from '@opentelemetry/otlp-proto-exporter-base'; @@ -34,7 +33,7 @@ const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURC class OTLPMetricExporterNodeProxy extends OTLPProtoExporterNodeBase { - constructor(config: OTLPExporterNodeConfigBase & OTLPMetricExporterOptions = defaultOptions) { + constructor(config?: OTLPExporterNodeConfigBase & OTLPMetricExporterOptions) { super(config); this.headers = Object.assign( this.headers, @@ -52,7 +51,7 @@ class OTLPMetricExporterNodeProxy extends OTLPProtoExporterNodeBase 0 - ? appendRootPathToUrlIfNeeded(getEnv().OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, DEFAULT_COLLECTOR_RESOURCE_PATH) + ? appendRootPathToUrlIfNeeded(getEnv().OTEL_EXPORTER_OTLP_METRICS_ENDPOINT) : getEnv().OTEL_EXPORTER_OTLP_ENDPOINT.length > 0 ? appendResourcePathToUrl(getEnv().OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_COLLECTOR_RESOURCE_PATH) : DEFAULT_COLLECTOR_URL; @@ -64,7 +63,7 @@ class OTLPMetricExporterNodeProxy extends OTLPProtoExporterNodeBase { - constructor(config: OTLPExporterNodeConfigBase & OTLPMetricExporterOptions = defaultOptions) { + constructor(config?: OTLPExporterNodeConfigBase & OTLPMetricExporterOptions) { super(new OTLPMetricExporterNodeProxy(config), config); } } diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/OTLPMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/OTLPMetricExporter.test.ts index 38ea5db918..8ff43fed7c 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/OTLPMetricExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/OTLPMetricExporter.test.ts @@ -240,9 +240,14 @@ describe('OTLPMetricExporter - node with proto over http', () => { const data = ExportTraceServiceRequestProto.decode(buff); const json = data?.toJSON() as IExportMetricsServiceRequest; - const metric1 = json.resourceMetrics[0].scopeMetrics[0].metrics[0]; - const metric2 = json.resourceMetrics[0].scopeMetrics[0].metrics[1]; - const metric3 = json.resourceMetrics[0].scopeMetrics[0].metrics[2]; + // The order of the metrics is not guaranteed. + const counterIndex = metrics.scopeMetrics[0].metrics.findIndex(it => it.descriptor.name === 'int-counter'); + const observableIndex = metrics.scopeMetrics[0].metrics.findIndex(it => it.descriptor.name === 'double-observable-gauge'); + const histogramIndex = metrics.scopeMetrics[0].metrics.findIndex(it => it.descriptor.name === 'int-histogram'); + + const metric1 = json.resourceMetrics[0].scopeMetrics[0].metrics[counterIndex]; + const metric2 = json.resourceMetrics[0].scopeMetrics[0].metrics[observableIndex]; + const metric3 = json.resourceMetrics[0].scopeMetrics[0].metrics[histogramIndex]; assert.ok(typeof metric1 !== 'undefined', "counter doesn't exist"); ensureExportedCounterIsCorrect( diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/metricsHelper.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/metricsHelper.ts index 0beb770ed8..d86775d332 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/metricsHelper.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/metricsHelper.ts @@ -20,11 +20,10 @@ import { Histogram, ValueType, ObservableGauge, -} from '@opentelemetry/api-metrics'; +} from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; import * as assert from 'assert'; import { - AggregationTemporality, ExplicitBucketHistogramAggregation, MeterProvider, MetricReader, @@ -34,10 +33,6 @@ import { IExportMetricsServiceRequest, IKeyValue, IMetric } from '@opentelemetry import { Stream } from 'stream'; export class TestMetricReader extends MetricReader { - selectAggregationTemporality() { - return AggregationTemporality.CUMULATIVE; - } - protected onForceFlush(): Promise { return Promise.resolve(undefined); } @@ -47,11 +42,11 @@ export class TestMetricReader extends MetricReader { } } -const testResource = Resource.default().merge(new Resource({ +const testResource = new Resource({ service: 'ui', version: 1, cost: 112.12, -})); +}); let meterProvider = new MeterProvider({ resource: testResource }); diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/tsconfig.json b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/tsconfig.json index be2921b2fd..1539111d42 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/tsconfig.json +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../../api" + }, { "path": "../../../packages/opentelemetry-core" }, @@ -16,14 +19,11 @@ "path": "../../../packages/opentelemetry-resources" }, { - "path": "../opentelemetry-api-metrics" + "path": "../../../packages/sdk-metrics" }, { "path": "../opentelemetry-exporter-metrics-otlp-http" }, - { - "path": "../opentelemetry-sdk-metrics" - }, { "path": "../otlp-exporter-base" }, diff --git a/experimental/packages/opentelemetry-exporter-prometheus/README.md b/experimental/packages/opentelemetry-exporter-prometheus/README.md index f0ce7ff97d..97ea063d3c 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/README.md +++ b/experimental/packages/opentelemetry-exporter-prometheus/README.md @@ -3,6 +3,8 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] +**Note: This is an experimental package under active development. New releases may include breaking changes.** + The OpenTelemetry Prometheus Metrics Exporter allows the user to send collected [OpenTelemetry Metrics](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-sdk-metrics) to Prometheus. [Prometheus](https://prometheus.io/) is a monitoring system that collects metrics, by scraping exposed endpoints at regular intervals, evaluating rule expressions. It can also trigger alerts if certain conditions are met. For assistance setting up Prometheus, [Click here](https://opencensus.io/codelabs/prometheus/#0) for a guided codelab. diff --git a/experimental/packages/opentelemetry-exporter-prometheus/package.json b/experimental/packages/opentelemetry-exporter-prometheus/package.json index 69e9dc9694..0a36b920e4 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/package.json +++ b/experimental/packages/opentelemetry-exporter-prometheus/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/exporter-prometheus", - "version": "0.32.0", + "version": "0.34.0", "description": "OpenTelemetry Exporter Prometheus provides a metrics endpoint for Prometheus", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -43,8 +43,9 @@ "access": "public" }, "devDependencies": { - "@opentelemetry/api": "^1.0.0", - "@types/mocha": "9.1.1", + "@opentelemetry/api": "^1.3.0", + "@opentelemetry/semantic-conventions": "1.8.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "codecov": "3.8.3", @@ -56,12 +57,13 @@ "typescript": "4.4.4" }, "peerDependencies": { - "@opentelemetry/api": "^1.0.0" + "@opentelemetry/api": "^1.3.0" }, "dependencies": { - "@opentelemetry/api-metrics": "0.32.0", - "@opentelemetry/core": "1.6.0", - "@opentelemetry/sdk-metrics": "0.32.0" + "@opentelemetry/core": "1.8.0", + "@opentelemetry/sdk-metrics": "1.8.0", + "@opentelemetry/resources": "1.8.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-prometheus" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-prometheus", + "sideEffects": false } diff --git a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts index c548b906dc..4f333eec55 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts @@ -18,15 +18,22 @@ import { diag } from '@opentelemetry/api'; import { globalErrorHandler, } from '@opentelemetry/core'; -import { AggregationTemporality, MetricReader } from '@opentelemetry/sdk-metrics'; -import { createServer, IncomingMessage, Server, ServerResponse } from 'http'; +import { + Aggregation, + AggregationTemporality, + MetricReader +} from '@opentelemetry/sdk-metrics'; +import { + createServer, + IncomingMessage, + Server, + ServerResponse +} from 'http'; import { ExporterConfig } from './export/types'; import { PrometheusSerializer } from './PrometheusSerializer'; /** Node.js v8.x compat */ import { URL } from 'url'; -const NO_REGISTERED_METRICS = '# no registered metrics'; - export class PrometheusExporter extends MetricReader { static readonly DEFAULT_OPTIONS = { host: undefined, @@ -55,7 +62,10 @@ export class PrometheusExporter extends MetricReader { * @param callback Callback to be called after a server was started */ constructor(config: ExporterConfig = {}, callback?: () => void) { - super(); + super({ + aggregationSelector: _instrumentType => Aggregation.Default(), + aggregationTemporalitySelector: _instrumentType => AggregationTemporality.CUMULATIVE + }); this._host = config.host || process.env.OTEL_EXPORTER_PROMETHEUS_HOST || @@ -90,10 +100,6 @@ export class PrometheusExporter extends MetricReader { } } - selectAggregationTemporality(): AggregationTemporality { - return AggregationTemporality.CUMULATIVE; - } - override async onForceFlush(): Promise { /** do nothing */ } @@ -155,7 +161,7 @@ export class PrometheusExporter extends MetricReader { /** * Request handler that responds with the current state of metrics - * @param request Incoming HTTP request of server instance + * @param _request Incoming HTTP request of server instance * @param response HTTP response objet used to response to request */ public getMetricsRequestHandler( @@ -196,11 +202,7 @@ export class PrometheusExporter extends MetricReader { if (errors.length) { diag.error('PrometheusExporter: metrics collection errors', ...errors); } - let result = this._serializer.serialize(resourceMetrics); - if (result === '') { - result = NO_REGISTERED_METRICS; - } - response.end(result); + response.end(this._serializer.serialize(resourceMetrics)); }, err => { response.end(`# failed to export metrics: ${err}`); diff --git a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts index 94144732ac..0879f16e87 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { diag } from '@opentelemetry/api'; +import { + diag, + MetricAttributes, + MetricAttributeValue, +} from '@opentelemetry/api'; import { ResourceMetrics, InstrumentType, @@ -24,11 +28,8 @@ import { DataPoint, Histogram, } from '@opentelemetry/sdk-metrics'; -import type { - MetricAttributes, - MetricAttributeValue -} from '@opentelemetry/api-metrics'; import { hrTimeToMilliseconds } from '@opentelemetry/core'; +import { Resource } from '@opentelemetry/resources'; type PrometheusDataTypeLiteral = | 'counter' @@ -167,6 +168,8 @@ function stringify( }\n`; } +const NO_REGISTERED_METRICS = '# no registered metrics'; + export class PrometheusSerializer { private _prefix: string | undefined; private _appendTimestamp: boolean; @@ -180,10 +183,16 @@ export class PrometheusSerializer { serialize(resourceMetrics: ResourceMetrics): string { let str = ''; + for (const scopeMetrics of resourceMetrics.scopeMetrics) { str += this._serializeScopeMetrics(scopeMetrics); } - return str; + + if (str === '') { + str += NO_REGISTERED_METRICS; + } + + return this._serializeResource(resourceMetrics.resource) + str; } private _serializeScopeMetrics(scopeMetrics: ScopeMetrics) { @@ -311,4 +320,13 @@ export class PrometheusSerializer { return results; } + + protected _serializeResource(resource: Resource): string { + const name = 'target_info'; + const help = `# HELP ${name} Target metadata`; + const type = `# TYPE ${name} gauge`; + + const results = stringify(name, resource.attributes, 1).trim(); + return `${help}\n${type}\n${results}\n`; + } } diff --git a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts index 2a722e573a..31137bd156 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts @@ -14,17 +14,33 @@ * limitations under the License. */ -import { Meter, ObservableResult } from '@opentelemetry/api-metrics'; import { - MeterProvider, -} from '@opentelemetry/sdk-metrics'; + Counter, + Meter, + ObservableResult +} from '@opentelemetry/api'; +import { MeterProvider } from '@opentelemetry/sdk-metrics'; import * as assert from 'assert'; import * as sinon from 'sinon'; import * as http from 'http'; import { PrometheusExporter } from '../src'; -import { mockedHrTimeMs, mockHrTime } from './util'; +import { + mockedHrTimeMs, + mockHrTime, + sdkLanguage, + sdkName, + sdkVersion, + serviceName +} from './util'; import { SinonStubbedInstance } from 'sinon'; -import { Counter } from '@opentelemetry/api-metrics'; + +const infoLine = `target_info{service_name="${serviceName}",telemetry_sdk_language="${sdkLanguage}",telemetry_sdk_name="${sdkName}",telemetry_sdk_version="${sdkVersion}"} 1`; + +const serializedDefaultResourceLines = [ + '# HELP target_info Target metadata', + '# TYPE target_info gauge', + infoLine +]; describe('PrometheusExporter', () => { beforeEach(() => { @@ -251,11 +267,12 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.strictEqual( - lines[0], + lines[serializedDefaultResourceLines.length], '# HELP counter_total a test description' ); assert.deepStrictEqual(lines, [ + ...serializedDefaultResourceLines, '# HELP counter_total a test description', '# TYPE counter_total counter', `counter_total{key1="attributeValue1"} 10 ${mockedHrTimeMs}`, @@ -285,6 +302,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedDefaultResourceLines, '# HELP metric_observable_gauge a test description', '# TYPE metric_observable_gauge gauge', `metric_observable_gauge{pid="123",core="1"} 0.999 ${mockedHrTimeMs}`, @@ -304,6 +322,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedDefaultResourceLines, '# HELP counter_total a test description', '# TYPE counter_total counter', `counter_total{counterKey1="attributeValue1"} 10 ${mockedHrTimeMs}`, @@ -333,11 +352,14 @@ describe('PrometheusExporter', () => { }); }); - it('should export a comment if no metrics are registered', async () => { + it('should export resource even if no metrics are registered', async () => { const body = await request('http://localhost:9464/metrics'); const lines = body.split('\n'); - assert.deepStrictEqual(lines, ['# no registered metrics']); + assert.deepStrictEqual(lines, [ + ...serializedDefaultResourceLines, + '# no registered metrics' + ]); }); it('should add a description if missing', async () => { @@ -349,6 +371,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedDefaultResourceLines, '# HELP counter_total description missing', '# TYPE counter_total counter', `counter_total{key1="attributeValue1"} 10 ${mockedHrTimeMs}`, @@ -365,6 +388,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedDefaultResourceLines, '# HELP counter_bad_name_total description missing', '# TYPE counter_bad_name_total counter', `counter_bad_name_total{key1="attributeValue1"} 10 ${mockedHrTimeMs}`, @@ -382,6 +406,7 @@ describe('PrometheusExporter', () => { const body = await request('http://localhost:9464/metrics'); const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedDefaultResourceLines, '# HELP counter a test description', '# TYPE counter gauge', `counter{key1="attributeValue1"} 20 ${mockedHrTimeMs}`, @@ -389,7 +414,7 @@ describe('PrometheusExporter', () => { ]); }); - it('should export an ObservableCounter as a counter', async() => { + it('should export an ObservableCounter as a counter', async () => { function getValue() { return 20; } @@ -410,6 +435,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedDefaultResourceLines, '# HELP metric_observable_counter a test description', '# TYPE metric_observable_counter counter', `metric_observable_counter{key1="attributeValue1"} 20 ${mockedHrTimeMs}`, @@ -438,6 +464,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedDefaultResourceLines, '# HELP metric_observable_up_down_counter a test description', '# TYPE metric_observable_up_down_counter gauge', `metric_observable_up_down_counter{key1="attributeValue1"} 20 ${mockedHrTimeMs}`, @@ -445,7 +472,7 @@ describe('PrometheusExporter', () => { ]); }); - it('should export a Histogram as a summary', async() => { + it('should export a Histogram as a summary', async () => { const histogram = meter.createHistogram('test_histogram', { description: 'a test description', }); @@ -456,6 +483,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedDefaultResourceLines, '# HELP test_histogram a test description', '# TYPE test_histogram histogram', `test_histogram_count{key1="attributeValue1"} 1 ${mockedHrTimeMs}`, @@ -480,22 +508,19 @@ describe('PrometheusExporter', () => { let meter: Meter; let meterProvider: MeterProvider; let counter: Counter; - let exporter: PrometheusExporter | undefined; + let exporter: PrometheusExporter; - beforeEach(() => { + function setup(reader: PrometheusExporter) { meterProvider = new MeterProvider(); + meterProvider.addMetricReader(reader); + meter = meterProvider.getMeter('test-prometheus'); counter = meter.createCounter('counter'); counter.add(10, { key1: 'attributeValue1' }); - }); + } - afterEach(done => { - if (exporter) { - exporter.shutdown().then(done); - exporter = undefined; - } else { - done(); - } + afterEach(async () => { + await exporter.shutdown(); }); it('should use a configured name prefix', done => { @@ -504,7 +529,7 @@ describe('PrometheusExporter', () => { prefix: 'test_prefix', }, async () => { - meterProvider.addMetricReader(exporter!); + setup(exporter); http .get('http://localhost:9464/metrics', res => { res.on('data', chunk => { @@ -512,6 +537,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedDefaultResourceLines, '# HELP test_prefix_counter_total description missing', '# TYPE test_prefix_counter_total counter', `test_prefix_counter_total{key1="attributeValue1"} 10 ${mockedHrTimeMs}`, @@ -532,7 +558,7 @@ describe('PrometheusExporter', () => { port: 8080, }, async () => { - meterProvider.addMetricReader(exporter!); + setup(exporter); http .get('http://localhost:8080/metrics', res => { res.on('data', chunk => { @@ -540,6 +566,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedDefaultResourceLines, '# HELP counter_total description missing', '# TYPE counter_total counter', `counter_total{key1="attributeValue1"} 10 ${mockedHrTimeMs}`, @@ -560,7 +587,7 @@ describe('PrometheusExporter', () => { endpoint: '/test', }, async () => { - meterProvider.addMetricReader(exporter!); + setup(exporter); http .get('http://localhost:9464/test', res => { res.on('data', chunk => { @@ -568,6 +595,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedDefaultResourceLines, '# HELP counter_total description missing', '# TYPE counter_total counter', `counter_total{key1="attributeValue1"} 10 ${mockedHrTimeMs}`, @@ -588,7 +616,7 @@ describe('PrometheusExporter', () => { appendTimestamp: false, }, async () => { - meterProvider.addMetricReader(exporter!); + setup(exporter); http .get('http://localhost:9464/metrics', res => { res.on('data', chunk => { @@ -596,6 +624,7 @@ describe('PrometheusExporter', () => { const lines = body.split('\n'); assert.deepStrictEqual(lines, [ + ...serializedDefaultResourceLines, '# HELP counter_total description missing', '# TYPE counter_total counter', 'counter_total{key1="attributeValue1"} 10', diff --git a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts index c05f94f81c..a4792eef54 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts @@ -15,8 +15,12 @@ */ import * as assert from 'assert'; -import { MetricAttributes, UpDownCounter } from '@opentelemetry/api-metrics'; import { + MetricAttributes, + UpDownCounter +} from '@opentelemetry/api'; +import { + Aggregation, AggregationTemporality, DataPoint, DataPointType, @@ -30,20 +34,32 @@ import { } from '@opentelemetry/sdk-metrics'; import * as sinon from 'sinon'; import { PrometheusSerializer } from '../src'; -import { mockedHrTimeMs, mockHrTime } from './util'; +import { + mockedHrTimeMs, + mockHrTime, + sdkLanguage, + sdkName, + sdkVersion, + serviceName +} from './util'; +import { Resource } from '@opentelemetry/resources'; const attributes = { foo1: 'bar1', foo2: 'bar2', }; +const serializedDefaultResource = + '# HELP target_info Target metadata\n' + + '# TYPE target_info gauge\n' + + `target_info{service_name="${serviceName}",telemetry_sdk_language="${sdkLanguage}",telemetry_sdk_name="${sdkName}",telemetry_sdk_version="${sdkVersion}"} 1\n`; + class TestMetricReader extends MetricReader { constructor() { - super(); - } - - selectAggregationTemporality() { - return AggregationTemporality.CUMULATIVE; + super({ + aggregationTemporalitySelector: _instrumentType => AggregationTemporality.CUMULATIVE, + aggregationSelector: _instrumentType => Aggregation.Default() + }); } async onForceFlush() {} @@ -278,7 +294,7 @@ describe('PrometheusSerializer', () => { const reader = new TestMetricReader(); const meterProvider = new MeterProvider({ views: [ - new View({aggregation: new LastValueAggregation(), instrumentName: '*' }) + new View({ aggregation: new LastValueAggregation(), instrumentName: '*' }) ] }); meterProvider.addMetricReader(reader); @@ -361,7 +377,7 @@ describe('PrometheusSerializer', () => { assert.strictEqual( result, '# HELP test foobar\n' + - '# TYPE test histogram\n' + + '# TYPE test histogram\n' + `test_count{val="1"} 3 ${mockedHrTimeMs}\n` + `test_sum{val="1"} 175 ${mockedHrTimeMs}\n` + `test_bucket{val="1",le="1"} 0 ${mockedHrTimeMs}\n` + @@ -465,6 +481,7 @@ describe('PrometheusSerializer', () => { const result = await getCounterResult('test', serializer, { unit: unitOfMetric, exportAll: true }); assert.strictEqual( result, + serializedDefaultResource + '# HELP test_total description missing\n' + `# UNIT test_total ${unitOfMetric}\n` + '# TYPE test_total counter\n' + @@ -478,6 +495,7 @@ describe('PrometheusSerializer', () => { const result = await getCounterResult('test', serializer, { exportAll: true }); assert.strictEqual( result, + serializedDefaultResource + '# HELP test_total description missing\n' + '# TYPE test_total counter\n' + `test_total 1 ${mockedHrTimeMs}\n` @@ -607,7 +625,7 @@ describe('PrometheusSerializer', () => { const serializer = new PrometheusSerializer(); const result = await testSerializer(serializer, 'test_total', counter => { - // if you try to use a attribute name like account-id prometheus will complain + // if you try to use an attribute name like account-id prometheus will complain // with an error like: // error while linting: text format parsing error in line 282: expected '=' after label name, found '-' counter.add(1, ({ @@ -621,4 +639,24 @@ describe('PrometheusSerializer', () => { ); }); }); + + describe('_serializeResource', () => { + it('should serialize resource', () => { + const serializer = new PrometheusSerializer(undefined, true); + const result = serializer['_serializeResource'](new Resource({ + env: 'prod', + hostname: 'myhost', + datacenter: 'sdc', + region: 'europe', + owner: 'frontend' + })); + + assert.strictEqual( + result, + '# HELP target_info Target metadata\n' + + '# TYPE target_info gauge\n' + + 'target_info{env="prod",hostname="myhost",datacenter="sdc",region="europe",owner="frontend"} 1\n' + ); + }); + }); }); diff --git a/experimental/packages/opentelemetry-exporter-prometheus/test/util.ts b/experimental/packages/opentelemetry-exporter-prometheus/test/util.ts index 49536a5a35..8caa7795cd 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/test/util.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/test/util.ts @@ -16,6 +16,8 @@ import * as sinon from 'sinon'; import * as perf_hooks from 'perf_hooks'; +import { Resource } from '@opentelemetry/resources'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; export const mockedHrTimeMs = 1586347902211; @@ -25,3 +27,12 @@ export function mockHrTime() { sinon.stub(perf_hooks.performance, 'timeOrigin').value(0); sinon.stub(perf_hooks.performance, 'now').returns(mockedHrTimeMs); } + +export const serviceName = Resource.default().attributes[SemanticResourceAttributes.SERVICE_NAME]?.toString() + .replace(/\\/g, '\\\\').replace(/\n/g, '\\n'); +export const sdkLanguage = Resource.default().attributes[SemanticResourceAttributes.TELEMETRY_SDK_LANGUAGE]?.toString() + .replace(/\\/g, '\\\\').replace(/\n/g, '\\n'); +export const sdkName = Resource.default().attributes[SemanticResourceAttributes.TELEMETRY_SDK_NAME]?.toString() + .replace(/\\/g, '\\\\').replace(/\n/g, '\\n'); +export const sdkVersion = Resource.default().attributes[SemanticResourceAttributes.TELEMETRY_SDK_VERSION]?.toString() + .replace(/\\/g, '\\\\').replace(/\n/g, '\\n'); diff --git a/experimental/packages/opentelemetry-exporter-prometheus/tsconfig.json b/experimental/packages/opentelemetry-exporter-prometheus/tsconfig.json index b9a0327885..cd21ec8274 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/tsconfig.json +++ b/experimental/packages/opentelemetry-exporter-prometheus/tsconfig.json @@ -9,14 +9,20 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../../api" + }, { "path": "../../../packages/opentelemetry-core" }, { - "path": "../opentelemetry-api-metrics" + "path": "../../../packages/opentelemetry-resources" + }, + { + "path": "../../../packages/opentelemetry-semantic-conventions" }, { - "path": "../opentelemetry-sdk-metrics" + "path": "../../../packages/sdk-metrics" } ] } diff --git a/experimental/packages/opentelemetry-instrumentation-fetch/README.md b/experimental/packages/opentelemetry-instrumentation-fetch/README.md index bf6719731c..2b5336be0b 100644 --- a/experimental/packages/opentelemetry-instrumentation-fetch/README.md +++ b/experimental/packages/opentelemetry-instrumentation-fetch/README.md @@ -3,6 +3,8 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] +**Note: This is an experimental package under active development. New releases may include breaking changes.** + This module provides auto instrumentation for web using fetch. ## Installation @@ -14,9 +16,11 @@ npm install --save @opentelemetry/instrumentation-fetch ## Usage ```js -'use strict'; -import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'; +import { + ConsoleSpanExporter, + SimpleSpanProcessor, + WebTracerProvider, +} from '@opentelemetry/sdk-trace-web'; import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch'; import { ZoneContextManager } from '@opentelemetry/context-zone'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; diff --git a/experimental/packages/opentelemetry-instrumentation-fetch/package.json b/experimental/packages/opentelemetry-instrumentation-fetch/package.json index d7e22f7f07..cebc08ada0 100644 --- a/experimental/packages/opentelemetry-instrumentation-fetch/package.json +++ b/experimental/packages/opentelemetry-instrumentation-fetch/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/instrumentation-fetch", - "version": "0.32.0", + "version": "0.34.0", "description": "OpenTelemetry fetch automatic instrumentation package.", "main": "build/src/index.js", "module": "build/esm/index.js", @@ -56,10 +56,10 @@ "devDependencies": { "@babel/core": "7.16.0", "@opentelemetry/api": "^1.0.0", - "@opentelemetry/context-zone": "1.6.0", - "@opentelemetry/propagator-b3": "1.6.0", - "@opentelemetry/sdk-trace-base": "1.6.0", - "@types/mocha": "9.1.1", + "@opentelemetry/context-zone": "1.8.0", + "@opentelemetry/propagator-b3": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "@types/webpack-env": "1.16.3", @@ -87,10 +87,11 @@ "@opentelemetry/api": "^1.0.0" }, "dependencies": { - "@opentelemetry/core": "1.6.0", - "@opentelemetry/instrumentation": "0.32.0", - "@opentelemetry/sdk-trace-web": "1.6.0", - "@opentelemetry/semantic-conventions": "1.6.0" + "@opentelemetry/core": "1.8.0", + "@opentelemetry/instrumentation": "0.34.0", + "@opentelemetry/sdk-trace-web": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-fetch" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-fetch", + "sideEffects": false } diff --git a/experimental/packages/opentelemetry-instrumentation-fetch/tsconfig.json b/experimental/packages/opentelemetry-instrumentation-fetch/tsconfig.json index b228a2080a..9b921df4d9 100644 --- a/experimental/packages/opentelemetry-instrumentation-fetch/tsconfig.json +++ b/experimental/packages/opentelemetry-instrumentation-fetch/tsconfig.json @@ -10,6 +10,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../../api" + }, { "path": "../../../packages/opentelemetry-context-zone" }, diff --git a/experimental/packages/opentelemetry-instrumentation-grpc/README.md b/experimental/packages/opentelemetry-instrumentation-grpc/README.md index 44d598fe14..16a5b22eae 100644 --- a/experimental/packages/opentelemetry-instrumentation-grpc/README.md +++ b/experimental/packages/opentelemetry-instrumentation-grpc/README.md @@ -3,6 +3,8 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] +**Note: This is an experimental package under active development. New releases may include breaking changes.** + This module provides automatic instrumentation for [`grpc`](https://grpc.github.io/grpc/node/) and [`@grpc/grpc-js`](https://grpc.io/blog/grpc-js-1.0/). Currently, version [`1.x`](https://www.npmjs.com/package/grpc?activeTab=versions) of `grpc` and version [`1.x`](https://www.npmjs.com/package/@grpc/grpc-js?activeTab=versions) of `@grpc/grpc-js` is supported. For automatic instrumentation see the diff --git a/experimental/packages/opentelemetry-instrumentation-grpc/package.json b/experimental/packages/opentelemetry-instrumentation-grpc/package.json index 31675724b9..ec8781464d 100644 --- a/experimental/packages/opentelemetry-instrumentation-grpc/package.json +++ b/experimental/packages/opentelemetry-instrumentation-grpc/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/instrumentation-grpc", - "version": "0.32.0", + "version": "0.34.0", "description": "OpenTelemetry grpc automatic instrumentation package.", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -45,14 +45,14 @@ "access": "public" }, "devDependencies": { - "@grpc/grpc-js": "1.5.9", - "@grpc/proto-loader": "0.6.9", - "@opentelemetry/api": "^1.0.0", - "@opentelemetry/context-async-hooks": "1.6.0", - "@opentelemetry/core": "1.6.0", - "@opentelemetry/sdk-trace-base": "1.6.0", - "@opentelemetry/sdk-trace-node": "1.6.0", - "@types/mocha": "9.1.1", + "@grpc/grpc-js": "^1.7.1", + "@grpc/proto-loader": "^0.7.3", + "@opentelemetry/api": "^1.3.0", + "@opentelemetry/context-async-hooks": "1.8.0", + "@opentelemetry/core": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0", + "@opentelemetry/sdk-trace-node": "1.8.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/semver": "7.3.9", "@types/sinon": "10.0.13", @@ -68,12 +68,12 @@ "typescript": "4.4.4" }, "peerDependencies": { - "@opentelemetry/api": "^1.0.0" + "@opentelemetry/api": "^1.3.0" }, "dependencies": { - "@opentelemetry/api-metrics": "0.32.0", - "@opentelemetry/instrumentation": "0.32.0", - "@opentelemetry/semantic-conventions": "1.6.0" + "@opentelemetry/instrumentation": "0.34.0", + "@opentelemetry/semantic-conventions": "1.8.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-grpc" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-grpc", + "sideEffects": false } diff --git a/experimental/packages/opentelemetry-instrumentation-grpc/src/instrumentation.ts b/experimental/packages/opentelemetry-instrumentation-grpc/src/instrumentation.ts index 5ac237038a..d6f782684b 100644 --- a/experimental/packages/opentelemetry-instrumentation-grpc/src/instrumentation.ts +++ b/experimental/packages/opentelemetry-instrumentation-grpc/src/instrumentation.ts @@ -19,7 +19,6 @@ import { VERSION } from './version'; import { GrpcNativeInstrumentation } from './grpc'; import { GrpcJsInstrumentation } from './grpc-js'; import * as api from '@opentelemetry/api'; -import { MeterProvider } from '@opentelemetry/api-metrics'; /** The metadata key under which span context is stored as a binary value. */ export const GRPC_TRACE_KEY = 'grpc-trace-bin'; @@ -81,7 +80,7 @@ export class GrpcInstrumentation { * Sets MeterProvider to this plugin * @param meterProvider */ - public setMeterProvider(meterProvider: MeterProvider) { + public setMeterProvider(meterProvider: api.MeterProvider) { this._grpcJsInstrumentation.setMeterProvider(meterProvider); this._grpcNativeInstrumentation.setMeterProvider(meterProvider); } diff --git a/experimental/packages/opentelemetry-instrumentation-grpc/tsconfig.json b/experimental/packages/opentelemetry-instrumentation-grpc/tsconfig.json index 8b4ba562f1..0170a84c3c 100644 --- a/experimental/packages/opentelemetry-instrumentation-grpc/tsconfig.json +++ b/experimental/packages/opentelemetry-instrumentation-grpc/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../../api" + }, { "path": "../../../packages/opentelemetry-context-async-hooks" }, @@ -24,9 +27,6 @@ { "path": "../../../packages/opentelemetry-semantic-conventions" }, - { - "path": "../opentelemetry-api-metrics" - }, { "path": "../opentelemetry-instrumentation" } diff --git a/experimental/packages/opentelemetry-instrumentation-http/README.md b/experimental/packages/opentelemetry-instrumentation-http/README.md index 4684adaa4c..312eb73bd7 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/README.md +++ b/experimental/packages/opentelemetry-instrumentation-http/README.md @@ -3,6 +3,8 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] +**Note: This is an experimental package under active development. New releases may include breaking changes.** + This module provides automatic instrumentation for [`http`](https://nodejs.org/api/http.html) and [`https`](https://nodejs.org/api/https.html). For automatic instrumentation see the @@ -22,8 +24,11 @@ To load a specific instrumentation (HTTP in this case), specify it in the Node T ```js const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); -const { ConsoleSpanExporter, SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base'); -const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); +const { + ConsoleSpanExporter, + NodeTracerProvider, + SimpleSpanProcessor, +} = require('@opentelemetry/sdk-trace-node'); const { registerInstrumentations } = require('@opentelemetry/instrumentation'); const provider = new NodeTracerProvider(); diff --git a/experimental/packages/opentelemetry-instrumentation-http/package.json b/experimental/packages/opentelemetry-instrumentation-http/package.json index a248b031ea..b62aafe8c4 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/package.json +++ b/experimental/packages/opentelemetry-instrumentation-http/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/instrumentation-http", - "version": "0.32.0", + "version": "0.34.0", "description": "OpenTelemetry http/https automatic instrumentation package.", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -45,12 +45,12 @@ "access": "public" }, "devDependencies": { - "@opentelemetry/api": "^1.0.0", - "@opentelemetry/context-async-hooks": "1.6.0", - "@opentelemetry/sdk-trace-base": "1.6.0", - "@opentelemetry/sdk-trace-node": "1.6.0", - "@types/got": "9.6.12", - "@types/mocha": "9.1.1", + "@opentelemetry/api": "^1.3.0", + "@opentelemetry/context-async-hooks": "1.8.0", + "@opentelemetry/sdk-metrics": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0", + "@opentelemetry/sdk-trace-node": "1.8.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/request-promise-native": "1.0.18", "@types/semver": "7.3.9", @@ -58,7 +58,6 @@ "@types/superagent": "4.1.13", "axios": "0.24.0", "codecov": "3.8.3", - "got": "9.6.0", "mocha": "10.0.0", "nock": "13.0.11", "nyc": "15.1.0", @@ -71,13 +70,14 @@ "typescript": "4.4.4" }, "peerDependencies": { - "@opentelemetry/api": "^1.0.0" + "@opentelemetry/api": "^1.3.0" }, "dependencies": { - "@opentelemetry/core": "1.6.0", - "@opentelemetry/instrumentation": "0.32.0", - "@opentelemetry/semantic-conventions": "1.6.0", + "@opentelemetry/core": "1.8.0", + "@opentelemetry/instrumentation": "0.34.0", + "@opentelemetry/semantic-conventions": "1.8.0", "semver": "^7.3.5" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-http" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-http", + "sideEffects": false } diff --git a/experimental/packages/opentelemetry-instrumentation-http/src/http.ts b/experimental/packages/opentelemetry-instrumentation-http/src/http.ts index 5459e1d26f..0f19ffd867 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/src/http.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/src/http.ts @@ -15,6 +15,7 @@ */ import { context, + HrTime, INVALID_SPAN_CONTEXT, propagation, ROOT_CONTEXT, @@ -24,8 +25,11 @@ import { SpanStatus, SpanStatusCode, trace, + Histogram, + MetricAttributes, + ValueType } from '@opentelemetry/api'; -import { suppressTracing } from '@opentelemetry/core'; +import { hrTime, hrTimeDuration, hrTimeToMilliseconds, suppressTracing } from '@opentelemetry/core'; import type * as http from 'http'; import type * as https from 'https'; import { Socket } from 'net'; @@ -49,6 +53,7 @@ import { safeExecuteInTheMiddle, } from '@opentelemetry/instrumentation'; import { RPCMetadata, RPCType, setRPCMetadata } from '@opentelemetry/core'; +import { errorMonitor } from 'events'; /** * Http instrumentation instrumentation for Opentelemetry @@ -58,6 +63,8 @@ export class HttpInstrumentation extends InstrumentationBase { private readonly _spanNotEnded: WeakSet = new WeakSet(); private readonly _version = process.versions.node; private _headerCapture; + private _httpServerDurationHistogram!: Histogram; + private _httpClientDurationHistogram!: Histogram; constructor(config?: HttpInstrumentationConfig) { super( @@ -65,10 +72,22 @@ export class HttpInstrumentation extends InstrumentationBase { VERSION, config ); - this._headerCapture = this._createHeaderCapture(); } + protected override _updateMetricInstruments() { + this._httpServerDurationHistogram = this.meter.createHistogram('http.server.duration', { + description: 'measures the duration of the inbound HTTP requests', + unit: 'ms', + valueType: ValueType.DOUBLE + }); + this._httpClientDurationHistogram = this.meter.createHistogram('http.client.duration', { + description: 'measures the duration of the outbound HTTP requests', + unit: 'ms', + valueType: ValueType.DOUBLE + }); + } + private _getConfig(): HttpInstrumentationConfig { return this._config; } @@ -272,11 +291,15 @@ export class HttpInstrumentation extends InstrumentationBase { * @param request The original request object. * @param options The arguments to the original function. * @param span representing the current operation + * @param startTime representing the start time of the request to calculate duration in Metric + * @param metricAttributes metric attributes */ private _traceClientRequest( request: http.ClientRequest, hostname: string, - span: Span + span: Span, + startTime: HrTime, + metricAttributes: MetricAttributes ): http.ClientRequest { if (this._getConfig().requestHook) { this._callRequestHook(span, request); @@ -294,6 +317,8 @@ export class HttpInstrumentation extends InstrumentationBase { response, ); span.setAttributes(responseAttributes); + metricAttributes = Object.assign(metricAttributes, utils.getOutgoingRequestMetricAttributesOnResponse(responseAttributes)); + if (this._getConfig().responseHook) { this._callResponseHook(span, response); } @@ -323,32 +348,32 @@ export class HttpInstrumentation extends InstrumentationBase { request, response ), - () => {}, + () => { }, true ); } - this._closeHttpSpan(span); + this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes); }); - response.on('error', (error: Err) => { + response.on(errorMonitor, (error: Err) => { this._diag.debug('outgoingRequest on error()', error); utils.setSpanWithError(span, error); const code = utils.parseResponseStatus(SpanKind.CLIENT, response.statusCode); span.setStatus({ code, message: error.message }); - this._closeHttpSpan(span); + this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes); }); } ); request.on('close', () => { this._diag.debug('outgoingRequest on request close()'); if (!request.aborted) { - this._closeHttpSpan(span); + this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes); } }); - request.on('error', (error: Err) => { + request.on(errorMonitor, (error: Err) => { this._diag.debug('outgoingRequest on request error()', error); utils.setSpanWithError(span, error); - this._closeHttpSpan(span); + this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes); }); this._diag.debug('http.ClientRequest return request'); @@ -404,18 +429,23 @@ export class HttpInstrumentation extends InstrumentationBase { const headers = request.headers; + const spanAttributes = utils.getIncomingRequestAttributes(request, { + component: component, + serverName: instrumentation._getConfig().serverName, + hookAttributes: instrumentation._callStartSpanHook( + request, + instrumentation._getConfig().startIncomingSpanHook + ), + }); + const spanOptions: SpanOptions = { kind: SpanKind.SERVER, - attributes: utils.getIncomingRequestAttributes(request, { - component: component, - serverName: instrumentation._getConfig().serverName, - hookAttributes: instrumentation._callStartSpanHook( - request, - instrumentation._getConfig().startIncomingSpanHook - ), - }), + attributes: spanAttributes, }; + const startTime = hrTime(); + let metricAttributes: MetricAttributes = utils.getIncomingRequestMetricAttributes(spanAttributes); + const ctx = propagation.extract(ROOT_CONTEXT, headers); const span = instrumentation._startHttpSpan( `${component.toLocaleUpperCase()} ${method}`, @@ -456,7 +486,7 @@ export class HttpInstrumentation extends InstrumentationBase { error => { if (error) { utils.setSpanWithError(span, error); - instrumentation._closeHttpSpan(span); + instrumentation._closeHttpSpan(span, SpanKind.SERVER, startTime, metricAttributes); throw error; } } @@ -466,6 +496,7 @@ export class HttpInstrumentation extends InstrumentationBase { request, response ); + metricAttributes = Object.assign(metricAttributes, utils.getIncomingRequestMetricAttributesOnResponse(attributes)); instrumentation._headerCapture.server.captureResponseHeaders(span, header => response.getHeader(header)); @@ -481,12 +512,12 @@ export class HttpInstrumentation extends InstrumentationBase { request, response ), - () => {}, + () => { }, true ); } - instrumentation._closeHttpSpan(span); + instrumentation._closeHttpSpan(span, SpanKind.SERVER, startTime, metricAttributes); return returned; }; @@ -495,7 +526,7 @@ export class HttpInstrumentation extends InstrumentationBase { error => { if (error) { utils.setSpanWithError(span, error); - instrumentation._closeHttpSpan(span); + instrumentation._closeHttpSpan(span, SpanKind.SERVER, startTime, metricAttributes); throw error; } } @@ -520,7 +551,7 @@ export class HttpInstrumentation extends InstrumentationBase { } const extraOptions = typeof args[0] === 'object' && - (typeof options === 'string' || options instanceof url.URL) + (typeof options === 'string' || options instanceof url.URL) ? (args.shift() as http.RequestOptions) : undefined; const { origin, pathname, method, optionsParsed } = utils.getRequestInfo( @@ -572,6 +603,9 @@ export class HttpInstrumentation extends InstrumentationBase { ), }); + const startTime = hrTime(); + const metricAttributes: MetricAttributes = utils.getOutgoingRequestMetricAttributes(attributes); + const spanOptions: SpanOptions = { kind: SpanKind.CLIENT, attributes, @@ -601,7 +635,7 @@ export class HttpInstrumentation extends InstrumentationBase { error => { if (error) { utils.setSpanWithError(span, error); - instrumentation._closeHttpSpan(span); + instrumentation._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes); throw error; } } @@ -612,7 +646,9 @@ export class HttpInstrumentation extends InstrumentationBase { return instrumentation._traceClientRequest( request, hostname, - span + span, + startTime, + metricAttributes ); }); }; @@ -646,13 +682,21 @@ export class HttpInstrumentation extends InstrumentationBase { return span; } - private _closeHttpSpan(span: Span) { + private _closeHttpSpan(span: Span, spanKind: SpanKind, startTime: HrTime, metricAttributes: MetricAttributes) { if (!this._spanNotEnded.has(span)) { return; } span.end(); this._spanNotEnded.delete(span); + + // Record metrics + const duration = hrTimeToMilliseconds(hrTimeDuration(startTime, hrTime())); + if (spanKind === SpanKind.SERVER) { + this._httpServerDurationHistogram.record(duration, metricAttributes); + } else if (spanKind === SpanKind.CLIENT) { + this._httpClientDurationHistogram.record(duration, metricAttributes); + } } private _callResponseHook( @@ -661,7 +705,7 @@ export class HttpInstrumentation extends InstrumentationBase { ) { safeExecuteInTheMiddle( () => this._getConfig().responseHook!(span, response), - () => {}, + () => { }, true ); } @@ -672,7 +716,7 @@ export class HttpInstrumentation extends InstrumentationBase { ) { safeExecuteInTheMiddle( () => this._getConfig().requestHook!(span, request), - () => {}, + () => { }, true ); } @@ -681,7 +725,7 @@ export class HttpInstrumentation extends InstrumentationBase { request: http.IncomingMessage | http.RequestOptions, hookFunc: Function | undefined, ) { - if(typeof hookFunc === 'function'){ + if (typeof hookFunc === 'function') { return safeExecuteInTheMiddle( () => hookFunc(request), () => { }, diff --git a/experimental/packages/opentelemetry-instrumentation-http/src/utils.ts b/experimental/packages/opentelemetry-instrumentation-http/src/utils.ts index f42f9955b3..4094586493 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/src/utils.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/src/utils.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { + MetricAttributes, SpanAttributes, SpanStatusCode, Span, @@ -299,7 +300,7 @@ export const extractHostnameAndPort = ( requestOptions: Pick ): { hostname: string, port: number | string } => { if (requestOptions.hostname && requestOptions.port) { - return {hostname: requestOptions.hostname, port: requestOptions.port}; + return { hostname: requestOptions.hostname, port: requestOptions.port }; } const matches = requestOptions.host?.match(/^([^:/ ]+)(:\d{1,5})?/) || null; const hostname = requestOptions.hostname || (matches === null ? 'localhost' : matches[1]); @@ -312,7 +313,7 @@ export const extractHostnameAndPort = ( port = requestOptions.protocol === 'https:' ? '443' : '80'; } } - return {hostname, port}; + return { hostname, port }; }; /** @@ -348,6 +349,20 @@ export const getOutgoingRequestAttributes = ( return Object.assign(attributes, options.hookAttributes); }; +/** + * Returns outgoing request Metric attributes scoped to the request data + * @param {SpanAttributes} spanAttributes the span attributes + */ +export const getOutgoingRequestMetricAttributes = ( + spanAttributes: SpanAttributes +): MetricAttributes => { + const metricAttributes: MetricAttributes = {}; + metricAttributes[SemanticAttributes.HTTP_METHOD] = spanAttributes[SemanticAttributes.HTTP_METHOD]; + metricAttributes[SemanticAttributes.NET_PEER_NAME] = spanAttributes[SemanticAttributes.NET_PEER_NAME]; + //TODO: http.url attribute, it should susbtitute any parameters to avoid high cardinality. + return metricAttributes; +}; + /** * Returns attributes related to the kind of HTTP protocol used * @param {string} [kind] Kind of HTTP protocol used: "1.0", "1.1", "2", "SPDY" or "QUIC". @@ -392,6 +407,20 @@ export const getOutgoingRequestAttributesOnResponse = ( return Object.assign(attributes, httpKindAttributes); }; +/** + * Returns outgoing request Metric attributes scoped to the response data + * @param {SpanAttributes} spanAttributes the span attributes + */ +export const getOutgoingRequestMetricAttributesOnResponse = ( + spanAttributes: SpanAttributes +): MetricAttributes => { + const metricAttributes: MetricAttributes = {}; + metricAttributes[SemanticAttributes.NET_PEER_PORT] = spanAttributes[SemanticAttributes.NET_PEER_PORT]; + metricAttributes[SemanticAttributes.HTTP_STATUS_CODE] = spanAttributes[SemanticAttributes.HTTP_STATUS_CODE]; + metricAttributes[SemanticAttributes.HTTP_FLAVOR] = spanAttributes[SemanticAttributes.HTTP_FLAVOR]; + return metricAttributes; +}; + /** * Returns incoming request attributes scoped to the request data * @param {IncomingMessage} request the request object @@ -422,6 +451,7 @@ export const getIncomingRequestAttributes = ( [SemanticAttributes.HTTP_HOST]: host, [SemanticAttributes.NET_HOST_NAME]: hostname, [SemanticAttributes.HTTP_METHOD]: method, + [SemanticAttributes.HTTP_SCHEME]: options.component, }; if (typeof ips === 'string') { @@ -445,6 +475,23 @@ export const getIncomingRequestAttributes = ( return Object.assign(attributes, httpKindAttributes, options.hookAttributes); }; +/** + * Returns incoming request Metric attributes scoped to the request data + * @param {SpanAttributes} spanAttributes the span attributes + * @param {{ component: string }} options used to pass data needed to create attributes + */ +export const getIncomingRequestMetricAttributes = ( + spanAttributes: SpanAttributes +): MetricAttributes => { + const metricAttributes: MetricAttributes = {}; + metricAttributes[SemanticAttributes.HTTP_SCHEME] = spanAttributes[SemanticAttributes.HTTP_SCHEME]; + metricAttributes[SemanticAttributes.HTTP_METHOD] = spanAttributes[SemanticAttributes.HTTP_METHOD]; + metricAttributes[SemanticAttributes.NET_HOST_NAME] = spanAttributes[SemanticAttributes.NET_HOST_NAME]; + metricAttributes[SemanticAttributes.HTTP_FLAVOR] = spanAttributes[SemanticAttributes.HTTP_FLAVOR]; + //TODO: http.target attribute, it should susbtitute any parameters to avoid high cardinality. + return metricAttributes; +}; + /** * Returns incoming request attributes scoped to the response data * @param {(ServerResponse & { socket: Socket; })} response the response object @@ -475,6 +522,19 @@ export const getIncomingRequestAttributesOnResponse = ( return attributes; }; +/** + * Returns incoming request Metric attributes scoped to the request data + * @param {SpanAttributes} spanAttributes the span attributes + */ +export const getIncomingRequestMetricAttributesOnResponse = ( + spanAttributes: SpanAttributes +): MetricAttributes => { + const metricAttributes: MetricAttributes = {}; + metricAttributes[SemanticAttributes.HTTP_STATUS_CODE] = spanAttributes[SemanticAttributes.HTTP_STATUS_CODE]; + metricAttributes[SemanticAttributes.NET_HOST_PORT] = spanAttributes[SemanticAttributes.NET_HOST_PORT]; + return metricAttributes; +}; + export function headerCapture(type: 'request' | 'response', headers: string[]) { const normalizedHeaders = new Map(headers.map(header => [header.toLowerCase(), header.toLowerCase().replace(/-/g, '_')])); diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts index bee473ed3d..ecf5191427 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts @@ -670,21 +670,21 @@ describe('HttpInstrumentation', () => { ); req.setTimeout(10, () => { req.abort(); - reject('timeout'); + }); + // Instrumentation should not swallow error event. + assert.strictEqual(req.listeners('error').length, 0); + req.on('error', err => { + reject(err); }); return req.end(); }); - try { - await promiseRequest; - assert.fail(); - } catch (error) { - const spans = memoryExporter.getFinishedSpans(); - const [span] = spans; - assert.strictEqual(spans.length, 1); - assert.strictEqual(span.status.code, SpanStatusCode.ERROR); - assert.ok(Object.keys(span.attributes).length >= 6); - } + await assert.rejects(promiseRequest, /Error: socket hang up/); + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.status.code, SpanStatusCode.ERROR); + assert.ok(Object.keys(span.attributes).length >= 6); }); it('should have 1 ended span when request is aborted after receiving response', async () => { @@ -701,7 +701,7 @@ describe('HttpInstrumentation', () => { (resp: http.IncomingMessage) => { let data = ''; resp.on('data', chunk => { - req.destroy(Error()); + req.destroy(Error('request destroyed')); data += chunk; }); resp.on('end', () => { @@ -709,20 +709,21 @@ describe('HttpInstrumentation', () => { }); } ); + // Instrumentation should not swallow error event. + assert.strictEqual(req.listeners('error').length, 0); + req.on('error', err => { + reject(err); + }); return req.end(); }); - try { - await promiseRequest; - assert.fail(); - } catch (error) { - const spans = memoryExporter.getFinishedSpans(); - const [span] = spans; - assert.strictEqual(spans.length, 1); - assert.strictEqual(span.status.code, SpanStatusCode.ERROR); - assert.ok(Object.keys(span.attributes).length > 7); - } + await assert.rejects(promiseRequest, /Error: request destroyed/); + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.status.code, SpanStatusCode.ERROR); + assert.ok(Object.keys(span.attributes).length > 7); }); it("should have 1 ended client span when request doesn't listening response", done => { diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-metrics.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-metrics.test.ts new file mode 100644 index 0000000000..3d80bb950d --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-metrics.test.ts @@ -0,0 +1,104 @@ +/* + * 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 { + AggregationTemporality, + DataPointType, + InMemoryMetricExporter, + MeterProvider, +} from '@opentelemetry/sdk-metrics'; +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import * as assert from 'assert'; +import { HttpInstrumentation } from '../../src/http'; +import { httpRequest } from '../utils/httpRequest'; +import { TestMetricReader } from '../utils/TestMetricReader'; + +const instrumentation = new HttpInstrumentation(); +instrumentation.enable(); +instrumentation.disable(); + +import * as http from 'http'; + +let server: http.Server; +const serverPort = 22346; +const protocol = 'http'; +const hostname = 'localhost'; +const pathname = '/test'; +const tracerProvider = new NodeTracerProvider(); +const meterProvider = new MeterProvider(); +const metricsMemoryExporter = new InMemoryMetricExporter(AggregationTemporality.DELTA); +const metricReader = new TestMetricReader(metricsMemoryExporter); + +meterProvider.addMetricReader(metricReader); +instrumentation.setTracerProvider(tracerProvider); +instrumentation.setMeterProvider(meterProvider); + + +describe('metrics', () => { + beforeEach(() => { + metricsMemoryExporter.reset(); + }); + + before(() => { + instrumentation.enable(); + server = http.createServer((request, response) => { + response.end('Test Server Response'); + }); + server.listen(serverPort); + }); + + after(() => { + server.close(); + instrumentation.disable(); + }); + + it('should add server/client duration metrics', async () => { + const requestCount = 3; + for (let i = 0; i < requestCount; i++) { + await httpRequest.get(`${protocol}://${hostname}:${serverPort}${pathname}`); + } + await metricReader.collectAndExport(); + const resourceMetrics = metricsMemoryExporter.getMetrics(); + const scopeMetrics = resourceMetrics[0].scopeMetrics; + assert.strictEqual(scopeMetrics.length, 1, 'scopeMetrics count'); + const metrics = scopeMetrics[0].metrics; + assert.strictEqual(metrics.length, 2, 'metrics count'); + assert.strictEqual(metrics[0].dataPointType, DataPointType.HISTOGRAM); + assert.strictEqual(metrics[0].descriptor.description, 'measures the duration of the inbound HTTP requests'); + assert.strictEqual(metrics[0].descriptor.name, 'http.server.duration'); + assert.strictEqual(metrics[0].descriptor.unit, 'ms'); + assert.strictEqual(metrics[0].dataPoints.length, 1); + assert.strictEqual((metrics[0].dataPoints[0].value as any).count, requestCount); + assert.strictEqual(metrics[0].dataPoints[0].attributes[SemanticAttributes.HTTP_SCHEME], 'http'); + assert.strictEqual(metrics[0].dataPoints[0].attributes[SemanticAttributes.HTTP_METHOD], 'GET'); + assert.strictEqual(metrics[0].dataPoints[0].attributes[SemanticAttributes.HTTP_FLAVOR], '1.1'); + assert.strictEqual(metrics[0].dataPoints[0].attributes[SemanticAttributes.NET_HOST_NAME], 'localhost'); + assert.strictEqual(metrics[0].dataPoints[0].attributes[SemanticAttributes.HTTP_STATUS_CODE], 200); + assert.strictEqual(metrics[0].dataPoints[0].attributes[SemanticAttributes.NET_HOST_PORT], 22346); + + assert.strictEqual(metrics[1].dataPointType, DataPointType.HISTOGRAM); + assert.strictEqual(metrics[1].descriptor.description, 'measures the duration of the outbound HTTP requests'); + assert.strictEqual(metrics[1].descriptor.name, 'http.client.duration'); + assert.strictEqual(metrics[1].descriptor.unit, 'ms'); + assert.strictEqual(metrics[1].dataPoints.length, 1); + assert.strictEqual((metrics[1].dataPoints[0].value as any).count, requestCount); + assert.strictEqual(metrics[1].dataPoints[0].attributes[SemanticAttributes.HTTP_METHOD], 'GET'); + assert.strictEqual(metrics[1].dataPoints[0].attributes[SemanticAttributes.NET_PEER_NAME], 'localhost'); + assert.strictEqual(metrics[1].dataPoints[0].attributes[SemanticAttributes.NET_PEER_PORT], 22346); + assert.strictEqual(metrics[1].dataPoints[0].attributes[SemanticAttributes.HTTP_STATUS_CODE], 200); + assert.strictEqual(metrics[1].dataPoints[0].attributes[SemanticAttributes.HTTP_FLAVOR], '1.1'); + }); +}); diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-package.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-package.test.ts index 8da3577ddb..51b15dd6f3 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-package.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-package.test.ts @@ -35,7 +35,8 @@ instrumentation.disable(); import * as http from 'http'; import * as request from 'request-promise-native'; import * as superagent from 'superagent'; -import * as got from 'got'; +// Temporarily removed. See https://github.com/open-telemetry/opentelemetry-js/issues/3344 +// import * as got from 'got'; import * as nock from 'nock'; import axios, { AxiosResponse } from 'axios'; @@ -80,7 +81,7 @@ describe('Packages', () => { [ { name: 'axios', httpPackage: axios }, //keep first { name: 'superagent', httpPackage: superagent }, - { name: 'got', httpPackage: { get: (url: string) => got(url) } }, + // { name: 'got', httpPackage: { get: (url: string) => got(url) } }, { name: 'request', httpPackage: { get: (url: string) => request(url) }, diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-enable.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-enable.test.ts index 2611642b4c..5cfe4ac953 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-enable.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-enable.test.ts @@ -639,21 +639,21 @@ describe('HttpsInstrumentation', () => { ); req.setTimeout(10, () => { req.abort(); - reject('timeout'); + }); + // Instrumentation should not swallow error event. + assert.strictEqual(req.listeners('error').length, 0); + req.on('error', err => { + reject(err); }); return req.end(); }); - try { - await promiseRequest; - assert.fail(); - } catch (error) { - const spans = memoryExporter.getFinishedSpans(); - const [span] = spans; - assert.strictEqual(spans.length, 1); - assert.strictEqual(span.status.code, SpanStatusCode.ERROR); - assert.ok(Object.keys(span.attributes).length >= 6); - } + await assert.rejects(promiseRequest, /Error: socket hang up/); + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.status.code, SpanStatusCode.ERROR); + assert.ok(Object.keys(span.attributes).length >= 6); }); it('should have 1 ended span when request is aborted after receiving response', async () => { @@ -670,7 +670,7 @@ describe('HttpsInstrumentation', () => { (resp: http.IncomingMessage) => { let data = ''; resp.on('data', chunk => { - req.destroy(Error()); + req.destroy(Error('request destroyed')); data += chunk; }); resp.on('end', () => { @@ -678,20 +678,21 @@ describe('HttpsInstrumentation', () => { }); } ); + // Instrumentation should not swallow error event. + assert.strictEqual(req.listeners('error').length, 0); + req.on('error', err => { + reject(err); + }); return req.end(); }); - try { - await promiseRequest; - assert.fail(); - } catch (error) { - const spans = memoryExporter.getFinishedSpans(); - const [span] = spans; - assert.strictEqual(spans.length, 1); - assert.strictEqual(span.status.code, SpanStatusCode.ERROR); - assert.ok(Object.keys(span.attributes).length > 7); - } + await assert.rejects(promiseRequest, /Error: request destroyed/); + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.status.code, SpanStatusCode.ERROR); + assert.ok(Object.keys(span.attributes).length > 7); }); it("should have 1 ended span when response is listened by using req.on('response')", done => { diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-package.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-package.test.ts index 5f1c11d4a7..3fd164c243 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-package.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-package.test.ts @@ -35,7 +35,8 @@ instrumentation.disable(); import * as http from 'http'; import * as request from 'request-promise-native'; import * as superagent from 'superagent'; -import * as got from 'got'; +// Temporarily removed. See https://github.com/open-telemetry/opentelemetry-js/issues/3344 +// import * as got from 'got'; import * as nock from 'nock'; import axios, { AxiosResponse } from 'axios'; @@ -80,7 +81,7 @@ describe('Packages', () => { [ { name: 'axios', httpPackage: axios }, //keep first { name: 'superagent', httpPackage: superagent }, - { name: 'got', httpPackage: { get: (url: string) => got(url) } }, + // { name: 'got', httpPackage: { get: (url: string) => got(url) } }, { name: 'request', httpPackage: { get: (url: string) => request(url) }, diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/utils/TestMetricReader.ts b/experimental/packages/opentelemetry-instrumentation-http/test/utils/TestMetricReader.ts new file mode 100644 index 0000000000..53ea687d64 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-http/test/utils/TestMetricReader.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 { + MetricReader, + PushMetricExporter +} from '@opentelemetry/sdk-metrics'; + +export class TestMetricReader extends MetricReader { + constructor(private _exporter: PushMetricExporter) { + super({ + aggregationTemporalitySelector: _exporter.selectAggregationTemporality?.bind(_exporter), + }); + } + + protected onForceFlush(): Promise { + return Promise.resolve(undefined); + } + + protected onShutdown(): Promise { + return Promise.resolve(undefined); + } + + public async collectAndExport(): Promise { + const result = await this.collect(); + await new Promise((resolve, reject) => { + this._exporter.export(result.resourceMetrics, result => { + if (result.error != null) { + reject(result.error); + } else { + resolve(); + } + }); + }); + } +} diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/utils/assertSpan.ts b/experimental/packages/opentelemetry-instrumentation-http/test/utils/assertSpan.ts index db0d0d1ea0..3b9b1a402d 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/utils/assertSpan.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/utils/assertSpan.ts @@ -125,7 +125,7 @@ export const assertSpan = ( validations.hostname, 'must be consistent (PEER_NAME and hostname)' ); - if(!validations.noNetPeer) { + if (!validations.noNetPeer) { assert.ok( span.attributes[SemanticAttributes.NET_PEER_IP], 'must have PEER_IP' @@ -178,6 +178,11 @@ export const assertSpan = ( 'must have HOST_IP' ); } + assert.strictEqual( + span.attributes[SemanticAttributes.HTTP_SCHEME], + validations.component, + ' must have http.scheme attribute' + ); assert.ok(typeof span.parentSpanId === 'string'); assert.ok(isValidSpanId(span.parentSpanId)); } else if (validations.reqHeaders) { diff --git a/experimental/packages/opentelemetry-instrumentation-http/tsconfig.json b/experimental/packages/opentelemetry-instrumentation-http/tsconfig.json index b883031b0f..ca127b85dc 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/tsconfig.json +++ b/experimental/packages/opentelemetry-instrumentation-http/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../../api" + }, { "path": "../../../packages/opentelemetry-context-async-hooks" }, @@ -24,6 +27,9 @@ { "path": "../../../packages/opentelemetry-semantic-conventions" }, + { + "path": "../../../packages/sdk-metrics" + }, { "path": "../opentelemetry-instrumentation" } diff --git a/experimental/packages/opentelemetry-instrumentation-xml-http-request/README.md b/experimental/packages/opentelemetry-instrumentation-xml-http-request/README.md index bdd343d46c..8c96869460 100644 --- a/experimental/packages/opentelemetry-instrumentation-xml-http-request/README.md +++ b/experimental/packages/opentelemetry-instrumentation-xml-http-request/README.md @@ -3,6 +3,8 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] +**Note: This is an experimental package under active development. New releases may include breaking changes.** + This module provides auto instrumentation for web using XMLHttpRequest . ## Installation @@ -14,8 +16,11 @@ npm install --save @opentelemetry/instrumentation-xml-http-request ## Usage ```js -import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'; +import { + ConsoleSpanExporter, + SimpleSpanProcessor, + WebTracerProvider, +} from '@opentelemetry/sdk-trace-web'; import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request'; import { ZoneContextManager } from '@opentelemetry/context-zone'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; diff --git a/experimental/packages/opentelemetry-instrumentation-xml-http-request/package.json b/experimental/packages/opentelemetry-instrumentation-xml-http-request/package.json index 78b43fda9e..1f1b480ecc 100644 --- a/experimental/packages/opentelemetry-instrumentation-xml-http-request/package.json +++ b/experimental/packages/opentelemetry-instrumentation-xml-http-request/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/instrumentation-xml-http-request", - "version": "0.32.0", + "version": "0.34.0", "description": "OpenTelemetry XMLHttpRequest automatic instrumentation package.", "main": "build/src/index.js", "module": "build/esm/index.js", @@ -56,10 +56,10 @@ "devDependencies": { "@babel/core": "7.16.0", "@opentelemetry/api": "^1.0.0", - "@opentelemetry/context-zone": "1.6.0", - "@opentelemetry/propagator-b3": "1.6.0", - "@opentelemetry/sdk-trace-base": "1.6.0", - "@types/mocha": "9.1.1", + "@opentelemetry/context-zone": "1.8.0", + "@opentelemetry/propagator-b3": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "@types/webpack-env": "1.16.3", @@ -87,10 +87,11 @@ "@opentelemetry/api": "^1.0.0" }, "dependencies": { - "@opentelemetry/core": "1.6.0", - "@opentelemetry/instrumentation": "0.32.0", - "@opentelemetry/sdk-trace-web": "1.6.0", - "@opentelemetry/semantic-conventions": "1.6.0" + "@opentelemetry/core": "1.8.0", + "@opentelemetry/instrumentation": "0.34.0", + "@opentelemetry/sdk-trace-web": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-xml-http-request" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-xml-http-request", + "sideEffects": false } diff --git a/experimental/packages/opentelemetry-instrumentation-xml-http-request/tsconfig.json b/experimental/packages/opentelemetry-instrumentation-xml-http-request/tsconfig.json index b228a2080a..9b921df4d9 100644 --- a/experimental/packages/opentelemetry-instrumentation-xml-http-request/tsconfig.json +++ b/experimental/packages/opentelemetry-instrumentation-xml-http-request/tsconfig.json @@ -10,6 +10,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../../api" + }, { "path": "../../../packages/opentelemetry-context-zone" }, diff --git a/experimental/packages/opentelemetry-instrumentation/README.md b/experimental/packages/opentelemetry-instrumentation/README.md index 834b5adc87..26e9f8c696 100644 --- a/experimental/packages/opentelemetry-instrumentation/README.md +++ b/experimental/packages/opentelemetry-instrumentation/README.md @@ -3,6 +3,8 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] +**Note: This is an experimental package under active development. New releases may include breaking changes.** + ## Installation ```bash diff --git a/experimental/packages/opentelemetry-instrumentation/package.json b/experimental/packages/opentelemetry-instrumentation/package.json index 6fc1409a1f..8dc3dfb9d2 100644 --- a/experimental/packages/opentelemetry-instrumentation/package.json +++ b/experimental/packages/opentelemetry-instrumentation/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/instrumentation", - "version": "0.32.0", + "version": "0.34.0", "description": "Base class for node which OpenTelemetry instrumentation modules extend", "author": "OpenTelemetry Authors", "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation", @@ -68,18 +68,18 @@ "url": "https://github.com/open-telemetry/opentelemetry-js/issues" }, "dependencies": { - "@opentelemetry/api-metrics": "0.32.0", "require-in-the-middle": "^5.0.3", "semver": "^7.3.2", "shimmer": "^1.2.1" }, "peerDependencies": { - "@opentelemetry/api": "^1.0.0" + "@opentelemetry/api": "^1.3.0" }, "devDependencies": { + "@opentelemetry/sdk-metrics": "^1.8.0", "@babel/core": "7.16.0", - "@opentelemetry/api": "^1.0.0", - "@types/mocha": "9.1.1", + "@opentelemetry/api": "^1.3.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/semver": "7.3.9", "@types/shimmer": "1.0.2", @@ -108,5 +108,6 @@ }, "engines": { "node": ">=14" - } + }, + "sideEffects": false } diff --git a/experimental/packages/opentelemetry-instrumentation/src/autoLoader.ts b/experimental/packages/opentelemetry-instrumentation/src/autoLoader.ts index 862ba111e0..c6a052c6c9 100644 --- a/experimental/packages/opentelemetry-instrumentation/src/autoLoader.ts +++ b/experimental/packages/opentelemetry-instrumentation/src/autoLoader.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { trace } from '@opentelemetry/api'; -import { metrics } from '@opentelemetry/api-metrics'; +import { trace, metrics } from '@opentelemetry/api'; import { disableInstrumentations, enableInstrumentations, diff --git a/experimental/packages/opentelemetry-instrumentation/src/autoLoaderUtils.ts b/experimental/packages/opentelemetry-instrumentation/src/autoLoaderUtils.ts index 962c6de128..a6d63de472 100644 --- a/experimental/packages/opentelemetry-instrumentation/src/autoLoaderUtils.ts +++ b/experimental/packages/opentelemetry-instrumentation/src/autoLoaderUtils.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { TracerProvider } from '@opentelemetry/api'; -import { MeterProvider } from '@opentelemetry/api-metrics'; +import { TracerProvider, MeterProvider } from '@opentelemetry/api'; import { Instrumentation } from './types'; import { AutoLoaderResult, InstrumentationOption } from './types_internal'; diff --git a/experimental/packages/opentelemetry-instrumentation/src/instrumentation.ts b/experimental/packages/opentelemetry-instrumentation/src/instrumentation.ts index fb9f53fc70..d562ed9056 100644 --- a/experimental/packages/opentelemetry-instrumentation/src/instrumentation.ts +++ b/experimental/packages/opentelemetry-instrumentation/src/instrumentation.ts @@ -17,11 +17,13 @@ import { diag, DiagLogger, + metrics, + Meter, + MeterProvider, trace, Tracer, TracerProvider, } from '@opentelemetry/api'; -import { Meter, MeterProvider, metrics } from '@opentelemetry/api-metrics'; import * as shimmer from 'shimmer'; import { InstrumentationModuleDefinition } from './platform/node'; import * as types from './types'; @@ -54,6 +56,7 @@ implements types.Instrumentation { this._tracer = trace.getTracer(instrumentationName, instrumentationVersion); this._meter = metrics.getMeter(instrumentationName, instrumentationVersion); + this._updateMetricInstruments(); } /* Api to wrap instrumented method */ @@ -79,6 +82,15 @@ implements types.Instrumentation { this.instrumentationName, this.instrumentationVersion ); + + this._updateMetricInstruments(); + } + + /** + * Sets the new metric instruments with the current Meter. + */ + protected _updateMetricInstruments(): void { + return; } /* Returns InstrumentationConfig */ diff --git a/experimental/packages/opentelemetry-instrumentation/src/platform/node/ModuleNameTrie.ts b/experimental/packages/opentelemetry-instrumentation/src/platform/node/ModuleNameTrie.ts new file mode 100644 index 0000000000..3230fea99c --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation/src/platform/node/ModuleNameTrie.ts @@ -0,0 +1,86 @@ +/* + * 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 type { Hooked } from './RequireInTheMiddleSingleton'; + +export const ModuleNameSeparator = '/'; + +/** + * Node in a `ModuleNameTrie` + */ +class ModuleNameTrieNode { + hooks: Array<{ hook: Hooked, insertedId: number }> = []; + children: Map = new Map(); +} + +/** + * Trie containing nodes that represent a part of a module name (i.e. the parts separated by forward slash) + */ +export class ModuleNameTrie { + private _trie: ModuleNameTrieNode = new ModuleNameTrieNode(); + private _counter: number = 0; + + /** + * Insert a module hook into the trie + * + * @param {Hooked} hook Hook + */ + insert(hook: Hooked) { + let trieNode = this._trie; + + for (const moduleNamePart of hook.moduleName.split(ModuleNameSeparator)) { + let nextNode = trieNode.children.get(moduleNamePart); + if (!nextNode) { + nextNode = new ModuleNameTrieNode(); + trieNode.children.set(moduleNamePart, nextNode); + } + trieNode = nextNode; + } + trieNode.hooks.push({ hook, insertedId: this._counter++ }); + } + + /** + * Search for matching hooks in the trie + * + * @param {string} moduleName Module name + * @param {boolean} maintainInsertionOrder Whether to return the results in insertion order + * @returns {Hooked[]} Matching hooks + */ + search(moduleName: string, { maintainInsertionOrder }: { maintainInsertionOrder?: boolean } = {}): Hooked[] { + let trieNode = this._trie; + const results: ModuleNameTrieNode['hooks'] = []; + + for (const moduleNamePart of moduleName.split(ModuleNameSeparator)) { + const nextNode = trieNode.children.get(moduleNamePart); + if (!nextNode) { + break; + } + results.push(...nextNode.hooks); + trieNode = nextNode; + } + + if (results.length === 0) { + return []; + } + if (results.length === 1) { + return [results[0].hook]; + } + if (maintainInsertionOrder) { + results.sort((a, b) => a.insertedId - b.insertedId); + } + return results.map(({ hook }) => hook); + } +} diff --git a/experimental/packages/opentelemetry-instrumentation/src/platform/node/RequireInTheMiddleSingleton.ts b/experimental/packages/opentelemetry-instrumentation/src/platform/node/RequireInTheMiddleSingleton.ts new file mode 100644 index 0000000000..812db52b68 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation/src/platform/node/RequireInTheMiddleSingleton.ts @@ -0,0 +1,111 @@ +/* + * 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 RequireInTheMiddle from 'require-in-the-middle'; +import * as path from 'path'; +import { ModuleNameTrie, ModuleNameSeparator } from './ModuleNameTrie'; + +export type Hooked = { + moduleName: string + onRequire: RequireInTheMiddle.OnRequireFn +}; + +/** + * Whether Mocha is running in this process + * Inspired by https://github.com/AndreasPizsa/detect-mocha + * + * @type {boolean} + */ +const isMocha = ['afterEach','after','beforeEach','before','describe','it'].every(fn => { + // @ts-expect-error TS7053: Element implicitly has an 'any' type + return typeof global[fn] === 'function'; +}); + +/** + * Singleton class for `require-in-the-middle` + * Allows instrumentation plugins to patch modules with only a single `require` patch + * WARNING: Because this class will create its own `require-in-the-middle` (RITM) instance, + * we should minimize the number of new instances of this class. + * Multiple instances of `@opentelemetry/instrumentation` (e.g. multiple versions) in a single process + * will result in multiple instances of RITM, which will have an impact + * on the performance of instrumentation hooks being applied. + */ +export class RequireInTheMiddleSingleton { + private _moduleNameTrie: ModuleNameTrie = new ModuleNameTrie(); + private static _instance?: RequireInTheMiddleSingleton; + + private constructor() { + this._initialize(); + } + + private _initialize() { + RequireInTheMiddle( + // Intercept all `require` calls; we will filter the matching ones below + null, + { internals: true }, + (exports, name, basedir) => { + // For internal files on Windows, `name` will use backslash as the path separator + const normalizedModuleName = normalizePathSeparators(name); + + const matches = this._moduleNameTrie.search(normalizedModuleName, { maintainInsertionOrder: true }); + + for (const { onRequire } of matches) { + exports = onRequire(exports, name, basedir); + } + + return exports; + } + ); + } + + /** + * Register a hook with `require-in-the-middle` + * + * @param {string} moduleName Module name + * @param {RequireInTheMiddle.OnRequireFn} onRequire Hook function + * @returns {Hooked} Registered hook + */ + register(moduleName: string, onRequire: RequireInTheMiddle.OnRequireFn): Hooked { + const hooked = { moduleName, onRequire }; + this._moduleNameTrie.insert(hooked); + return hooked; + } + + /** + * Get the `RequireInTheMiddleSingleton` singleton + * + * @returns {RequireInTheMiddleSingleton} Singleton of `RequireInTheMiddleSingleton` + */ + static getInstance(): RequireInTheMiddleSingleton { + // Mocha runs all test suites in the same process + // This prevents test suites from sharing a singleton + if (isMocha) return new RequireInTheMiddleSingleton(); + + return this._instance = this._instance ?? new RequireInTheMiddleSingleton(); + } +} + +/** + * Normalize the path separators to forward slash in a module name or path + * + * @param {string} moduleNameOrPath Module name or path + * @returns {string} Normalized module name or path + */ +function normalizePathSeparators(moduleNameOrPath: string): string { + return path.sep !== ModuleNameSeparator + ? moduleNameOrPath.split(path.sep).join(ModuleNameSeparator) + : moduleNameOrPath; +} diff --git a/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts b/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts index 70dac85332..d80985431c 100644 --- a/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts +++ b/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts @@ -16,9 +16,9 @@ import * as types from '../../types'; import * as path from 'path'; -import * as RequireInTheMiddle from 'require-in-the-middle'; import { satisfies } from 'semver'; import { InstrumentationAbstract } from '../../instrumentation'; +import { RequireInTheMiddleSingleton, Hooked } from './RequireInTheMiddleSingleton'; import { InstrumentationModuleDefinition } from './types'; import { diag } from '@opentelemetry/api'; @@ -29,7 +29,8 @@ export abstract class InstrumentationBase extends InstrumentationAbstract implements types.Instrumentation { private _modules: InstrumentationModuleDefinition[]; - private _hooks: RequireInTheMiddle.Hooked[] = []; + private _hooks: Hooked[] = []; + private _requireInTheMiddleSingleton: RequireInTheMiddleSingleton = RequireInTheMiddleSingleton.getInstance(); private _enabled = false; constructor( @@ -48,9 +49,10 @@ export abstract class InstrumentationBase this._modules = (modules as InstrumentationModuleDefinition[]) || []; if (this._modules.length === 0) { - diag.warn( - 'No modules instrumentation has been defined,' + - ' nothing will be patched' + diag.debug( + 'No modules instrumentation has been defined for ' + + `'${this.instrumentationName}@${this.instrumentationVersion}'` + + ', nothing will be patched' ); } @@ -159,9 +161,8 @@ export abstract class InstrumentationBase this._warnOnPreloadedModules(); for (const module of this._modules) { this._hooks.push( - RequireInTheMiddle( - [module.name], - { internals: true }, + this._requireInTheMiddleSingleton.register( + module.name, (exports, name, baseDir) => { return this._onRequire( (module as unknown) as InstrumentationModuleDefinition< diff --git a/experimental/packages/opentelemetry-instrumentation/src/types.ts b/experimental/packages/opentelemetry-instrumentation/src/types.ts index 9edd6368ec..837f096792 100644 --- a/experimental/packages/opentelemetry-instrumentation/src/types.ts +++ b/experimental/packages/opentelemetry-instrumentation/src/types.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { TracerProvider } from '@opentelemetry/api'; -import { MeterProvider } from '@opentelemetry/api-metrics'; +import { TracerProvider, MeterProvider } from '@opentelemetry/api'; /** Interface Instrumentation to apply patch. */ export interface Instrumentation { diff --git a/experimental/packages/opentelemetry-instrumentation/src/types_internal.ts b/experimental/packages/opentelemetry-instrumentation/src/types_internal.ts index 6383b6cd17..016be0ad36 100644 --- a/experimental/packages/opentelemetry-instrumentation/src/types_internal.ts +++ b/experimental/packages/opentelemetry-instrumentation/src/types_internal.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { TracerProvider } from '@opentelemetry/api'; -import { MeterProvider } from '@opentelemetry/api-metrics'; +import { TracerProvider, MeterProvider } from '@opentelemetry/api'; import { InstrumentationBase } from './platform'; import { Instrumentation } from './types'; diff --git a/experimental/packages/opentelemetry-instrumentation/test/common/Instrumentation.test.ts b/experimental/packages/opentelemetry-instrumentation/test/common/Instrumentation.test.ts index ac0c5b0a90..be53d1ca59 100644 --- a/experimental/packages/opentelemetry-instrumentation/test/common/Instrumentation.test.ts +++ b/experimental/packages/opentelemetry-instrumentation/test/common/Instrumentation.test.ts @@ -21,6 +21,8 @@ import { InstrumentationConfig, } from '../../src'; +import { MeterProvider } from '@opentelemetry/sdk-metrics'; + interface TestInstrumentationConfig extends InstrumentationConfig { isActive?: boolean; } @@ -54,13 +56,36 @@ describe('BaseInstrumentation', () => { describe('constructor', () => { it('should enable instrumentation by default', () => { - let called = false; + let enableCalled = false; + let updateMetricInstrumentsCalled = false; class TestInstrumentation2 extends TestInstrumentation { override enable() { + enableCalled = true; + } + override _updateMetricInstruments() { + updateMetricInstrumentsCalled = true; + } + } + instrumentation = new TestInstrumentation2(); + assert.strictEqual(enableCalled, true); + assert.strictEqual(updateMetricInstrumentsCalled, true); + }); + }); + + describe('setMeterProvider', () => { + let otelTestingMeterProvider: MeterProvider; + beforeEach(() => { + otelTestingMeterProvider = new MeterProvider(); + }); + it('should call _updateMetricInstruments', () => { + let called = true; + class TestInstrumentation2 extends TestInstrumentation { + override _updateMetricInstruments() { called = true; } } instrumentation = new TestInstrumentation2(); + instrumentation.setMeterProvider(otelTestingMeterProvider); assert.strictEqual(called, true); }); }); diff --git a/experimental/packages/opentelemetry-instrumentation/test/common/autoLoader.test.ts b/experimental/packages/opentelemetry-instrumentation/test/common/autoLoader.test.ts index ff7971f66a..c7bebf204c 100644 --- a/experimental/packages/opentelemetry-instrumentation/test/common/autoLoader.test.ts +++ b/experimental/packages/opentelemetry-instrumentation/test/common/autoLoader.test.ts @@ -14,8 +14,13 @@ * limitations under the License. */ -import { Tracer, TracerProvider } from '@opentelemetry/api'; -import { NOOP_METER_PROVIDER } from '@opentelemetry/api-metrics'; +import { + Tracer, + TracerProvider, + Meter, + MeterOptions, + MeterProvider, +} from '@opentelemetry/api'; import * as assert from 'assert'; import * as sinon from 'sinon'; import { InstrumentationBase, registerInstrumentations } from '../../src'; @@ -25,6 +30,13 @@ class DummyTracerProvider implements TracerProvider { throw new Error('not implemented'); } } + +class DummyMeterProvider implements MeterProvider { + getMeter(name: string, version?: string, options?: MeterOptions): Meter { + throw new Error('not implemented'); + } +} + class FooInstrumentation extends InstrumentationBase { init() { return []; @@ -50,14 +62,14 @@ describe('autoLoader', () => { let instrumentation: InstrumentationBase; let enableSpy: sinon.SinonSpy; let setTracerProviderSpy: sinon.SinonSpy; - let setsetMeterProvider: sinon.SinonSpy; + let setMeterProviderSpy: sinon.SinonSpy; const tracerProvider = new DummyTracerProvider(); - const meterProvider = NOOP_METER_PROVIDER; + const meterProvider = new DummyMeterProvider(); beforeEach(() => { instrumentation = new FooInstrumentation('foo', '1', {}); enableSpy = sinon.spy(instrumentation, 'enable'); setTracerProviderSpy = sinon.stub(instrumentation, 'setTracerProvider'); - setsetMeterProvider = sinon.stub(instrumentation, 'setMeterProvider'); + setMeterProviderSpy = sinon.stub(instrumentation, 'setMeterProvider'); unload = registerInstrumentations({ instrumentations: [instrumentation], tracerProvider, @@ -85,6 +97,7 @@ describe('autoLoader', () => { ); enableSpy = sinon.spy(instrumentation, 'enable'); setTracerProviderSpy = sinon.stub(instrumentation, 'setTracerProvider'); + setMeterProviderSpy = sinon.stub(instrumentation, 'setMeterProvider'); unload = registerInstrumentations({ instrumentations: [instrumentation], tracerProvider, @@ -104,9 +117,9 @@ describe('autoLoader', () => { }); it('should set MeterProvider', () => { - assert.strictEqual(setsetMeterProvider.callCount, 1); - assert.ok(setsetMeterProvider.lastCall.args[0] === meterProvider); - assert.strictEqual(setsetMeterProvider.lastCall.args.length, 1); + assert.strictEqual(setMeterProviderSpy.callCount, 1); + assert.ok(setMeterProviderSpy.lastCall.args[0] === meterProvider); + assert.strictEqual(setMeterProviderSpy.lastCall.args.length, 1); }); }); }); diff --git a/experimental/packages/opentelemetry-instrumentation/test/node/ModuleNameTrie.test.ts b/experimental/packages/opentelemetry-instrumentation/test/node/ModuleNameTrie.test.ts new file mode 100644 index 0000000000..c3d72c89d7 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation/test/node/ModuleNameTrie.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 * as assert from 'assert'; +import { Hooked } from '../../src/platform/node/RequireInTheMiddleSingleton'; +import { ModuleNameTrie } from '../../src/platform/node/ModuleNameTrie'; + +describe('ModuleNameTrie', () => { + describe('search', () => { + const trie = new ModuleNameTrie(); + const inserts = [ + { moduleName: 'a', onRequire: () => {} }, + { moduleName: 'a/b', onRequire: () => {} }, + { moduleName: 'a', onRequire: () => {} }, + { moduleName: 'a/c', onRequire: () => {} }, + { moduleName: 'd', onRequire: () => {} } + ] as Hooked[]; + inserts.forEach(trie.insert.bind(trie)); + + it('should return a list of exact matches (no results)', () => { + assert.deepEqual(trie.search('e'), []); + }); + + it('should return a list of exact matches (one result)', () => { + assert.deepEqual(trie.search('d'), [inserts[4]]); + }); + + it('should return a list of exact matches (more than one result)', () => { + assert.deepEqual(trie.search('a'), [ + inserts[0], + inserts[2] + ]); + }); + + describe('maintainInsertionOrder = false', () => { + it('should return a list of matches in prefix order', () => { + assert.deepEqual(trie.search('a/b'), [ + inserts[0], + inserts[2], + inserts[1] + ]); + }); + }); + + describe('maintainInsertionOrder = true', () => { + it('should return a list of matches in insertion order', () => { + assert.deepEqual(trie.search('a/b', { maintainInsertionOrder: true }), [ + inserts[0], + inserts[1], + inserts[2] + ]); + }); + }); + }); +}); diff --git a/experimental/packages/opentelemetry-instrumentation/test/node/RequireInTheMiddleSingleton.test.ts b/experimental/packages/opentelemetry-instrumentation/test/node/RequireInTheMiddleSingleton.test.ts new file mode 100644 index 0000000000..724dced720 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation/test/node/RequireInTheMiddleSingleton.test.ts @@ -0,0 +1,126 @@ +/* + * 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 * as path from 'path'; +import * as RequireInTheMiddle from 'require-in-the-middle'; +import { RequireInTheMiddleSingleton } from '../../src/platform/node/RequireInTheMiddleSingleton'; + +const requireInTheMiddleSingleton = RequireInTheMiddleSingleton.getInstance(); + +type AugmentedExports = { + __ritmOnRequires?: string[] +}; + +const makeOnRequiresStub = (label: string): sinon.SinonStub => sinon.stub().callsFake(((exports: AugmentedExports) => { + exports.__ritmOnRequires ??= []; + exports.__ritmOnRequires.push(label); + return exports; +}) as RequireInTheMiddle.OnRequireFn); + +describe('RequireInTheMiddleSingleton', () => { + describe('register', () => { + const onRequireFsStub = makeOnRequiresStub('fs'); + const onRequireFsPromisesStub = makeOnRequiresStub('fs-promises'); + const onRequireCodecovStub = makeOnRequiresStub('codecov'); + const onRequireCodecovLibStub = makeOnRequiresStub('codecov-lib'); + const onRequireCpxStub = makeOnRequiresStub('cpx'); + const onRequireCpxLibStub = makeOnRequiresStub('cpx-lib'); + + before(() => { + requireInTheMiddleSingleton.register('fs', onRequireFsStub); + requireInTheMiddleSingleton.register('fs/promises', onRequireFsPromisesStub); + requireInTheMiddleSingleton.register('codecov', onRequireCodecovStub); + requireInTheMiddleSingleton.register('codecov/lib/codecov.js', onRequireCodecovLibStub); + requireInTheMiddleSingleton.register('cpx', onRequireCpxStub); + requireInTheMiddleSingleton.register('cpx/lib/copy-sync.js', onRequireCpxLibStub); + }); + + beforeEach(() => { + onRequireFsStub.resetHistory(); + onRequireFsPromisesStub.resetHistory(); + onRequireCodecovStub.resetHistory(); + onRequireCodecovLibStub.resetHistory(); + onRequireCpxStub.resetHistory(); + onRequireCpxLibStub.resetHistory(); + }); + + it('should return a hooked object', () => { + const moduleName = 'm'; + const onRequire = makeOnRequiresStub('m'); + const hooked = requireInTheMiddleSingleton.register(moduleName, onRequire); + assert.deepStrictEqual(hooked, { moduleName, onRequire }); + }); + + describe('core module', () => { + describe('AND module name matches', () => { + it('should call `onRequire`', () => { + const exports = require('fs'); + assert.deepStrictEqual(exports.__ritmOnRequires, ['fs']); + sinon.assert.calledOnceWithExactly(onRequireFsStub, exports, 'fs', undefined); + sinon.assert.notCalled(onRequireFsPromisesStub); + }); + }); + describe('AND module name does not match', () => { + it('should not call `onRequire`', () => { + const exports = require('crypto'); + assert.equal(exports.__ritmOnRequires, undefined); + sinon.assert.notCalled(onRequireFsStub); + }); + }); + }); + + describe('core module with sub-path', () => { + describe('AND module name matches', () => { + it('should call `onRequire`', () => { + const exports = require('fs/promises'); + assert.deepStrictEqual(exports.__ritmOnRequires, ['fs', 'fs-promises']); + sinon.assert.calledOnceWithExactly(onRequireFsPromisesStub, exports, 'fs/promises', undefined); + sinon.assert.calledOnceWithMatch(onRequireFsStub, { __ritmOnRequires: ['fs', 'fs-promises'] }, 'fs/promises', undefined); + }); + }); + }); + + describe('non-core module', () => { + describe('AND module name matches', () => { + const baseDir = path.dirname(require.resolve('codecov')); + const modulePath = path.join('codecov', 'lib', 'codecov.js'); + it('should call `onRequire`', () => { + const exports = require('codecov'); + assert.deepStrictEqual(exports.__ritmOnRequires, ['codecov']); + sinon.assert.calledWithExactly(onRequireCodecovStub, exports, 'codecov', baseDir); + sinon.assert.calledWithMatch(onRequireCodecovStub, { __ritmOnRequires: ['codecov', 'codecov-lib'] }, modulePath, baseDir); + sinon.assert.calledWithMatch(onRequireCodecovLibStub, { __ritmOnRequires: ['codecov', 'codecov-lib'] }, modulePath, baseDir); + }).timeout(30000); + }); + }); + + describe('non-core module with sub-path', () => { + describe('AND module name matches', () => { + const baseDir = path.resolve(path.dirname(require.resolve('cpx')), '..'); + const modulePath = path.join('cpx', 'lib', 'copy-sync.js'); + it('should call `onRequire`', () => { + const exports = require('cpx/lib/copy-sync'); + assert.deepStrictEqual(exports.__ritmOnRequires, ['cpx', 'cpx-lib']); + sinon.assert.calledWithMatch(onRequireCpxStub, { __ritmOnRequires: ['cpx', 'cpx-lib'] }, modulePath, baseDir); + sinon.assert.calledWithExactly(onRequireCpxStub, exports, modulePath, baseDir); + sinon.assert.calledWithExactly(onRequireCpxLibStub, exports, modulePath, baseDir); + }); + }); + }); + }); +}); diff --git a/experimental/packages/opentelemetry-instrumentation/tsconfig.esnext.json b/experimental/packages/opentelemetry-instrumentation/tsconfig.esnext.json index 5b297a614a..cb78dd6ff3 100644 --- a/experimental/packages/opentelemetry-instrumentation/tsconfig.esnext.json +++ b/experimental/packages/opentelemetry-instrumentation/tsconfig.esnext.json @@ -7,10 +7,5 @@ }, "include": [ "src/**/*.ts" - ], - "references": [ - { - "path": "../opentelemetry-api-metrics/tsconfig.esnext.json" - } ] } diff --git a/experimental/packages/opentelemetry-instrumentation/tsconfig.json b/experimental/packages/opentelemetry-instrumentation/tsconfig.json index 948abef3ce..e22548584a 100644 --- a/experimental/packages/opentelemetry-instrumentation/tsconfig.json +++ b/experimental/packages/opentelemetry-instrumentation/tsconfig.json @@ -10,7 +10,7 @@ ], "references": [ { - "path": "../opentelemetry-api-metrics" + "path": "../../../api" } ] } diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/index.ts b/experimental/packages/opentelemetry-sdk-metrics/src/index.ts deleted file mode 100644 index e9ad1ef4b9..0000000000 --- a/experimental/packages/opentelemetry-sdk-metrics/src/index.ts +++ /dev/null @@ -1,32 +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. - */ - -export { Sum, LastValue, Histogram } from './aggregator/types'; -export * from './export/AggregationTemporality'; -export * from './export/MetricData'; -export * from './export/MetricExporter'; -export * from './export/MetricProducer'; -export * from './export/MetricReader'; -export * from './export/PeriodicExportingMetricReader'; -export * from './export/InMemoryMetricExporter'; -export * from './export/ConsoleMetricExporter'; -export { InstrumentDescriptor, InstrumentType } from './InstrumentDescriptor'; -export * from './Meter'; -export * from './MeterProvider'; -export * from './ObservableResult'; -export { TimeoutError } from './utils'; -export * from './view/Aggregation'; -export * from './view/View'; diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/export/ConsoleMetricExporter.test.ts b/experimental/packages/opentelemetry-sdk-metrics/test/export/ConsoleMetricExporter.test.ts deleted file mode 100644 index 39edda36f1..0000000000 --- a/experimental/packages/opentelemetry-sdk-metrics/test/export/ConsoleMetricExporter.test.ts +++ /dev/null @@ -1,108 +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 metrics from '@opentelemetry/api-metrics'; -import { ExportResult } from '@opentelemetry/core'; -import { ConsoleMetricExporter } from '../../src/export/ConsoleMetricExporter'; -import { PeriodicExportingMetricReader } from '../../src/export/PeriodicExportingMetricReader'; -import { ResourceMetrics } from '../../src/export/MetricData'; -import { MeterProvider } from '../../src/MeterProvider'; -import { defaultResource } from '../util'; -import * as assert from 'assert'; -import * as sinon from 'sinon'; - - -async function waitForNumberOfExports(exporter: sinon.SinonSpy<[metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void], void>, numberOfExports: number): Promise { - if (numberOfExports <= 0) { - throw new Error('numberOfExports must be greater than or equal to 0'); - } - - let totalExports = 0; - while (totalExports < numberOfExports) { - await new Promise(resolve => setTimeout(resolve, 20)); - totalExports = exporter.callCount; - } -} - -/* eslint-disable no-console */ -describe('ConsoleMetricExporter', () => { - let previousConsoleDir: any; - let exporter: ConsoleMetricExporter; - let meterProvider: MeterProvider; - let meterReader: PeriodicExportingMetricReader; - let meter: metrics.Meter; - - beforeEach(() => { - previousConsoleDir = console.dir; - console.dir = () => {}; - - exporter = new ConsoleMetricExporter(); - meterProvider = new MeterProvider({ resource: defaultResource }); - meter = meterProvider.getMeter('ConsoleMetricExporter', '1.0.0'); - meterReader = new PeriodicExportingMetricReader({ - exporter: exporter, - exportIntervalMillis: 100, - exportTimeoutMillis: 100 - }); - meterProvider.addMetricReader(meterReader); - }); - - afterEach(async () => { - console.dir = previousConsoleDir; - - await meterReader.shutdown(); - }); - - it('should export information about metric', async () => { - const counter = meter.createCounter('counter_total', { - description: 'a test description', - }); - const counterAttribute = { key1: 'attributeValue1' }; - counter.add(10, counterAttribute); - counter.add(10, counterAttribute); - - const histogram = meter.createHistogram('histogram', { description: 'a histogram' }); - histogram.record(10); - histogram.record(100); - histogram.record(1000); - - const spyConsole = sinon.spy(console, 'dir'); - const spyExport = sinon.spy(exporter, 'export'); - - await waitForNumberOfExports(spyExport, 1); - const resourceMetrics = spyExport.args[0]; - const firstResourceMetric = resourceMetrics[0]; - const consoleArgs = spyConsole.args[0]; - const consoleMetric = consoleArgs[0]; - const keys = Object.keys(consoleMetric).sort().join(','); - - const expectedKeys = [ - 'dataPointType', - 'dataPoints', - 'descriptor', - ].join(','); - - assert.ok(firstResourceMetric.resource.attributes.resourceKey === 'my-resource', 'resourceKey'); - assert.ok(keys === expectedKeys, 'expectedKeys'); - assert.ok(consoleMetric.descriptor.name === 'counter_total', 'name'); - assert.ok(consoleMetric.descriptor.description === 'a test description', 'description'); - assert.ok(consoleMetric.descriptor.type === 'COUNTER', 'type'); - assert.ok(consoleMetric.descriptor.unit === '', 'unit'); - assert.ok(consoleMetric.descriptor.valueType === 1, 'valueType'); - assert.ok(consoleMetric.dataPoints[0].attributes.key1 === 'attributeValue1', 'ensure metric attributes exists'); - - assert.ok(spyExport.calledOnce); - }); -}); diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/export/MetricReader.test.ts b/experimental/packages/opentelemetry-sdk-metrics/test/export/MetricReader.test.ts deleted file mode 100644 index 1ee88df5d1..0000000000 --- a/experimental/packages/opentelemetry-sdk-metrics/test/export/MetricReader.test.ts +++ /dev/null @@ -1,34 +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 assert from 'assert'; -import { MeterProvider } from '../../src/MeterProvider'; -import { TestMetricReader } from './TestMetricReader'; - - -describe('MetricReader', () => { - describe('setMetricProducer', () => { - it('The SDK MUST NOT allow a MetricReader instance to be registered on more than one MeterProvider instance', () => { - const reader = new TestMetricReader(); - const meterProvider1 = new MeterProvider(); - const meterProvider2 = new MeterProvider(); - - meterProvider1.addMetricReader(reader); - assert.throws(() => meterProvider1.addMetricReader(reader), /MetricReader can not be bound to a MeterProvider again/); - assert.throws(() => meterProvider2.addMetricReader(reader), /MetricReader can not be bound to a MeterProvider again/); - }); - }); -}); diff --git a/experimental/packages/opentelemetry-sdk-node/README.md b/experimental/packages/opentelemetry-sdk-node/README.md index 5b288ba1f3..0d4f3c0f7a 100644 --- a/experimental/packages/opentelemetry-sdk-node/README.md +++ b/experimental/packages/opentelemetry-sdk-node/README.md @@ -3,6 +3,8 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] +**Note: This is an experimental package under active development. New releases may include breaking changes.** + This package provides the full OpenTelemetry SDK for Node.js including tracing and metrics. ## Quick Start @@ -18,7 +20,7 @@ $ npm install @opentelemetry/sdk-node $ # Install exporters and plugins $ npm install \ @opentelemetry/exporter-jaeger \ # add tracing exporters as needed - @opentelemetry/exporter-prometheus # add metrics exporters as needed + @opentelemetry/exporter-prometheus \ # add metrics exporters as needed @opentelemetry/instrumentation-http # add instrumentations as needed $ # or install all officially supported core and contrib plugins @@ -26,7 +28,7 @@ $ npm install @opentelemetry/auto-instrumentations-node ``` -> Note: this example is for Node.js. See [examples/tracer-web](https://github.com/open-telemetry/opentelemetry-js/tree/main/examples/tracer-web) for a browser example. +> Note: this example is for Node.js. See [examples/opentelemetry-web](https://github.com/open-telemetry/opentelemetry-js/tree/main/examples/opentelemetry-web) for a browser example. ### Initialize the SDK @@ -50,7 +52,7 @@ const sdk = new opentelemetry.NodeSDK({ // Optional - if omitted, the tracing SDK will not be initialized traceExporter: jaegerExporter, // Optional - If omitted, the metrics SDK will not be initialized - metricExporter: prometheusExporter, + metricReader: prometheusExporter, // Optional - you can use the metapackage or load each instrumentation individually instrumentations: [getNodeAutoInstrumentations()], // See the Configuration section below for additional configuration options @@ -92,17 +94,17 @@ Use a custom context manager. Default: [AsyncHooksContextManager](../../../packa Use a custom propagator. Default: [CompositePropagator](../../../packages/opentelemetry-core/src/propagation/composite.ts) using [W3C Trace Context](../../../packages/opentelemetry-core/README.md#w3ctracecontextpropagator-propagator) and [Baggage](../../../packages/opentelemetry-core/README.md#baggage-propagator) -### metricProcessor - -Use a custom processor for metrics. Default: UngroupedProcessor - -### metricExporter +### metricReader -Configure a metric exporter. If an exporter is not configured, the metrics SDK will not be initialized and registered. +Add a [MetricReader](../opentelemetry-sdk-metrics/src/export/MetricReader.ts) +that will be passed to the `MeterProvider`. If `metricReader` is not configured, +the metrics SDK will not be initialized and registered. -### metricInterval +### views -Configure an interval for metrics export in ms. Default: 60,000 (60 seconds) +A list of views to be passed to the `MeterProvider`. +Accepts an array of [View](../opentelemetry-sdk-metrics/src/view/View.ts)-instances. +This parameter can be used to configure explicit bucket sizes of histogram metrics. ### instrumentations @@ -114,28 +116,53 @@ or configure each instrumentation individually. Configure a resource. Resources may also be detected by using the `autoDetectResources` method of the SDK. +### resourceDetectors + +Configure resource detectors. By default, the resource detectors are [envDetector, processDetector]. +NOTE: In order to enable the detection, the parameter `autoDetectResources` has to be `true`. + ### sampler -Configure a custom sampler. By default all traces will be sampled. +Configure a custom sampler. By default, all traces will be sampled. ### spanProcessor ### traceExporter -Configure a trace exporter. If an exporter OR span processor is not configured, the tracing SDK will not be initialized and registered. If an exporter is configured, it will be used with a [BatchSpanProcessor](../../../packages/opentelemetry-sdk-trace-base/src/platform/node/export/BatchSpanProcessor.ts). +Configure a trace exporter. If an exporter is configured, it will be used with a [BatchSpanProcessor](../../../packages/opentelemetry-sdk-trace-base/src/platform/node/export/BatchSpanProcessor.ts). If an exporter OR span processor is not configured programatically, this package will auto setup the default `otlp` exporter with `http/protobuf` protocol with a `BatchSpanProcessor`. ### spanLimits Configure tracing parameters. These are the same trace parameters used to [configure a tracer](../../../packages/opentelemetry-sdk-trace-base/src/types.ts#L71). -### views - -Configure views of your instruments and accepts an array of [View](../opentelemetry-sdk-metrics-base/src/view/View.ts)-instances. The parameter can be used to configure the explicit bucket sizes of histogram metrics. - ### serviceName Configure the [service name](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/README.md#service). +## Configure Trace Exporter from Environment + +This is an alternative to programmatically configuring an exporter or span processor. This package will auto setup the default `otlp` exporter with `http/protobuf` protocol if `traceExporter` or `spanProcessor` hasn't been passed into the `NodeSDK` constructor. + +### Exporters + +| Environment variable | Description | +|----------------------|-------------| +| OTEL_TRACES_EXPORTER | List of exporters to be used for tracing, separated by commas. Options include `otlp`, `jaeger`, `zipkin`, and `none`. Default is `otlp`. `none` means no autoconfigured exporter. + +### OTLP Exporter + +| Environment variable | Description | +|----------------------|-------------| +| OTEL_EXPORTER_OTLP_PROTOCOL | The transport protocol to use on OTLP trace, metric, and log requests. Options include `grpc`, `http/protobuf`, and `http/json`. Default is `http/protobuf`. | +| OTEL_EXPORTER_OTLP_TRACES_PROTOCOL | The transport protocol to use on OTLP trace requests. Options include `grpc`, `http/protobuf`, and `http/json`. Default is `http/protobuf`. | +| OTEL_EXPORTER_OTLP_METRICS_PROTOCOL | The transport protocol to use on OTLP metric requests. Options include `grpc`, `http/protobuf`, and `http/json`. Default is `http/protobuf`. | + +Additionally, you can specify other applicable environment variables that apply to each exporter such as the following: + +- [OTLP exporter environment configuration](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#configuration-options) +- [Zipkin exporter environment configuration](https://github.com/open-telemetry/opentelemetry-specification/blob/6ce62202e5407518e19c56c445c13682ef51a51d/specification/sdk-environment-variables.md#zipkin-exporter) +- [Jaeger exporter environment configuration](https://github.com/open-telemetry/opentelemetry-specification/blob/6ce62202e5407518e19c56c445c13682ef51a51d/specification/sdk-environment-variables.md#jaeger-exporter) + ## Useful links - For more information on OpenTelemetry, visit: diff --git a/experimental/packages/opentelemetry-sdk-node/package.json b/experimental/packages/opentelemetry-sdk-node/package.json index 584183a5dd..e37dc098c7 100644 --- a/experimental/packages/opentelemetry-sdk-node/package.json +++ b/experimental/packages/opentelemetry-sdk-node/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/sdk-node", - "version": "0.32.0", + "version": "0.34.0", "description": "OpenTelemetry SDK for Node.js", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -44,22 +44,26 @@ "access": "public" }, "dependencies": { - "@opentelemetry/api-metrics": "0.32.0", - "@opentelemetry/core": "1.6.0", - "@opentelemetry/instrumentation": "0.32.0", - "@opentelemetry/resources": "1.6.0", - "@opentelemetry/sdk-metrics": "0.32.0", - "@opentelemetry/sdk-trace-base": "1.6.0", - "@opentelemetry/sdk-trace-node": "1.6.0" + "@opentelemetry/exporter-jaeger": "1.8.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.34.0", + "@opentelemetry/exporter-trace-otlp-http": "0.34.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.34.0", + "@opentelemetry/exporter-zipkin": "1.8.0", + "@opentelemetry/core": "1.8.0", + "@opentelemetry/instrumentation": "0.34.0", + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/sdk-metrics": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0", + "@opentelemetry/sdk-trace-node": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0" + "@opentelemetry/api": ">=1.3.0 <1.4.0" }, "devDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0", - "@opentelemetry/context-async-hooks": "1.6.0", - "@opentelemetry/semantic-conventions": "1.6.0", - "@types/mocha": "9.1.1", + "@opentelemetry/api": ">=1.3.0 <1.4.0", + "@opentelemetry/context-async-hooks": "1.8.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/semver": "7.3.9", "@types/sinon": "10.0.13", @@ -73,5 +77,6 @@ "ts-mocha": "10.0.0", "typescript": "4.4.4" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-sdk-node" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-sdk-node", + "sideEffects": false } diff --git a/experimental/packages/opentelemetry-sdk-node/src/TracerProviderWithEnvExporter.ts b/experimental/packages/opentelemetry-sdk-node/src/TracerProviderWithEnvExporter.ts new file mode 100644 index 0000000000..f4abb593ab --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-node/src/TracerProviderWithEnvExporter.ts @@ -0,0 +1,124 @@ +/* + * 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 { diag } from '@opentelemetry/api'; +import { getEnv, getEnvWithoutDefaults } from '@opentelemetry/core'; +import { ConsoleSpanExporter, SpanExporter, BatchSpanProcessor, SimpleSpanProcessor, SDKRegistrationConfig, SpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { NodeTracerConfig, NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; +import { OTLPTraceExporter as OTLPProtoTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'; +import { OTLPTraceExporter as OTLPHttpTraceExporter} from '@opentelemetry/exporter-trace-otlp-http'; +import { OTLPTraceExporter as OTLPGrpcTraceExporter} from '@opentelemetry/exporter-trace-otlp-grpc'; +import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'; +import { JaegerExporter } from '@opentelemetry/exporter-jaeger'; + +export class TracerProviderWithEnvExporters extends NodeTracerProvider { + private _configuredExporters: SpanExporter[] = []; + private _spanProcessors: SpanProcessor[] | undefined; + private _hasSpanProcessors: boolean = false; + + static configureOtlp(): SpanExporter { + const protocol = this.getOtlpProtocol(); + + switch (protocol) { + case 'grpc': + return new OTLPGrpcTraceExporter; + case 'http/json': + return new OTLPHttpTraceExporter; + case 'http/protobuf': + return new OTLPProtoTraceExporter; + default: + diag.warn(`Unsupported OTLP traces protocol: ${protocol}. Using http/protobuf.`); + return new OTLPProtoTraceExporter; + } + } + + static getOtlpProtocol(): string { + const parsedEnvValues = getEnvWithoutDefaults(); + + return parsedEnvValues.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL ?? + parsedEnvValues.OTEL_EXPORTER_OTLP_PROTOCOL ?? + getEnv().OTEL_EXPORTER_OTLP_TRACES_PROTOCOL ?? + getEnv().OTEL_EXPORTER_OTLP_PROTOCOL; + } + + protected static override _registeredExporters = new Map< + string, + () => SpanExporter + >([ + ['otlp', () => this.configureOtlp()], + ['zipkin', () => new ZipkinExporter], + ['jaeger', () => new JaegerExporter], + ['console', () => new ConsoleSpanExporter] + ]); + + public constructor(config: NodeTracerConfig = {}) { + super(config); + let traceExportersList = this.filterBlanksAndNulls(Array.from(new Set(getEnv().OTEL_TRACES_EXPORTER.split(',')))); + + if (traceExportersList.length === 0 || traceExportersList[0] === 'none') { + diag.warn('OTEL_TRACES_EXPORTER contains "none" or is empty. SDK will not be initialized.'); + } else { + if (traceExportersList.length > 1 && traceExportersList.includes('none')) { + diag.warn('OTEL_TRACES_EXPORTER contains "none" along with other exporters. Using default otlp exporter.'); + traceExportersList = ['otlp']; + } + + traceExportersList.forEach(exporterName => { + const exporter = this._getSpanExporter(exporterName); + if (exporter) { + this._configuredExporters.push(exporter); + } else { + diag.warn(`Unrecognized OTEL_TRACES_EXPORTER value: ${exporterName}.`); + } + }); + + if (this._configuredExporters.length > 0) { + this._spanProcessors = this.configureSpanProcessors(this._configuredExporters); + this._spanProcessors.forEach(processor => { + this.addSpanProcessor(processor); + }); + } else { + diag.warn('Unable to set up trace exporter(s) due to invalid exporter and/or protocol values.'); + } + } + } + + override addSpanProcessor(spanProcessor: SpanProcessor) { + super.addSpanProcessor(spanProcessor); + this._hasSpanProcessors = true; + } + + override register(config?: SDKRegistrationConfig) { + if (this._hasSpanProcessors) { + super.register(config); + } + } + + private configureSpanProcessors(exporters: SpanExporter[]): (BatchSpanProcessor | SimpleSpanProcessor)[] { + return exporters.map(exporter => { + if (exporter instanceof ConsoleSpanExporter) { + return new SimpleSpanProcessor(exporter); + } else { + return new BatchSpanProcessor(exporter); + } + }); + } + + private filterBlanksAndNulls(list: string[]): string[] { + return list.map(item => item.trim()) + .filter(s => s !== 'null' && s !== ''); + } +} diff --git a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts index 2c6005806d..38f85b87f2 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ContextManager, TextMapPropagator } from '@opentelemetry/api'; -import { metrics } from '@opentelemetry/api-metrics'; +import { ContextManager, TextMapPropagator, metrics } from '@opentelemetry/api'; import { InstrumentationOption, registerInstrumentations } from '@opentelemetry/instrumentation'; import { + Detector, detectResources, envDetector, processDetector, @@ -30,11 +30,12 @@ import { import { MeterProvider, MetricReader, View } from '@opentelemetry/sdk-metrics'; import { BatchSpanProcessor, - SpanProcessor + SpanProcessor, } from '@opentelemetry/sdk-trace-base'; import { NodeTracerConfig, NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { NodeSDKConfiguration } from './types'; +import { TracerProviderWithEnvExporters } from './TracerProviderWithEnvExporter'; /** This class represents everything needed to register a fully configured OpenTelemetry Node.js SDK */ @@ -44,10 +45,7 @@ export type MeterProviderConfig = { */ reader?: MetricReader /** - * Lists the views that should be passed when meterProvider - * - * Note: This is only getting used when NodeSDK is responsible for - * instantiated an instance of MeterProvider + * List of {@link View}s that should be passed to the MeterProvider */ views?: View[] }; @@ -62,10 +60,11 @@ export class NodeSDK { private _instrumentations: InstrumentationOption[]; private _resource: Resource; + private _resourceDetectors: Detector[]; private _autoDetectResources: boolean; - private _tracerProvider?: NodeTracerProvider; + private _tracerProvider?: NodeTracerProvider | TracerProviderWithEnvExporters; private _meterProvider?: MeterProvider; private _serviceName?: string; @@ -74,6 +73,7 @@ export class NodeSDK { */ public constructor(configuration: Partial = {}) { this._resource = configuration.resource ?? new Resource({}); + this._resourceDetectors = configuration.resourceDetectors ?? [envDetector, processDetector]; this._serviceName = configuration.serviceName; @@ -166,12 +166,9 @@ export class NodeSDK { } /** Detect resource attributes */ - public async detectResources( - config?: ResourceDetectionConfig - ): Promise { + public async detectResources(): Promise { const internalConfig: ResourceDetectionConfig = { - detectors: [envDetector, processDetector], - ...config, + detectors: this._resourceDetectors, }; this.addResource(await detectResources(internalConfig)); @@ -196,21 +193,25 @@ export class NodeSDK { { [SemanticResourceAttributes.SERVICE_NAME]: this._serviceName } )); - if (this._tracerProviderConfig) { - const tracerProvider = new NodeTracerProvider({ - ...this._tracerProviderConfig.tracerConfig, - resource: this._resource, - }); + const Provider = + this._tracerProviderConfig ? NodeTracerProvider : TracerProviderWithEnvExporters; + + const tracerProvider = new Provider ({ + ...this._tracerProviderConfig?.tracerConfig, + resource: this._resource, + }); - this._tracerProvider = tracerProvider; + this._tracerProvider = tracerProvider; + if (this._tracerProviderConfig) { tracerProvider.addSpanProcessor(this._tracerProviderConfig.spanProcessor); - tracerProvider.register({ - contextManager: this._tracerProviderConfig.contextManager, - propagator: this._tracerProviderConfig.textMapPropagator, - }); } + tracerProvider.register({ + contextManager: this._tracerProviderConfig?.contextManager, + propagator: this._tracerProviderConfig?.textMapPropagator, + }); + if (this._meterProviderConfig) { const meterProvider = new MeterProvider({ resource: this._resource, diff --git a/experimental/packages/opentelemetry-sdk-node/src/types.ts b/experimental/packages/opentelemetry-sdk-node/src/types.ts index 425b375bac..722201f03f 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/types.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/types.ts @@ -17,7 +17,7 @@ import type { ContextManager, SpanAttributes } from '@opentelemetry/api'; import { TextMapPropagator } from '@opentelemetry/api'; import { InstrumentationOption } from '@opentelemetry/instrumentation'; -import { Resource } from '@opentelemetry/resources'; +import { Detector, Resource } from '@opentelemetry/resources'; import { MetricReader, View } from '@opentelemetry/sdk-metrics'; import { Sampler, @@ -35,6 +35,7 @@ export interface NodeSDKConfiguration { views: View[] instrumentations: InstrumentationOption[]; resource: Resource; + resourceDetectors: Detector[]; sampler: Sampler; serviceName?: string; spanProcessor: SpanProcessor; diff --git a/experimental/packages/opentelemetry-sdk-node/test/TracerProviderWithEnvExporter.test.ts b/experimental/packages/opentelemetry-sdk-node/test/TracerProviderWithEnvExporter.test.ts new file mode 100644 index 0000000000..2ba4d0394a --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-node/test/TracerProviderWithEnvExporter.test.ts @@ -0,0 +1,283 @@ +/* + * 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 { + diag, +} from '@opentelemetry/api'; +import { + ConsoleSpanExporter, + SimpleSpanProcessor, + BatchSpanProcessor, +} from '@opentelemetry/sdk-trace-base'; +import * as assert from 'assert'; +import * as Sinon from 'sinon'; +import { env } from 'process'; +import { OTLPTraceExporter as OTLPProtoTraceExporter, OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'; +import { OTLPTraceExporter as OTLPHttpTraceExporter} from '@opentelemetry/exporter-trace-otlp-http'; +import { OTLPTraceExporter as OTLPGrpcTraceExporter} from '@opentelemetry/exporter-trace-otlp-grpc'; +import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'; +import { JaegerExporter } from '@opentelemetry/exporter-jaeger'; +import { TracerProviderWithEnvExporters } from '../src/TracerProviderWithEnvExporter'; + +describe('set up trace exporter with env exporters', () => { + let spyGetOtlpProtocol: Sinon.SinonSpy; + let stubLoggerError: Sinon.SinonStub; + + beforeEach(() => { + spyGetOtlpProtocol = Sinon.spy(TracerProviderWithEnvExporters, 'getOtlpProtocol'); + stubLoggerError = Sinon.stub(diag, 'warn'); + }); + afterEach(() => { + spyGetOtlpProtocol.restore(); + stubLoggerError.restore(); + }); + describe('setup otlp exporter from env', () => { + it('set up default exporter when user does not define otel trace exporter', async () => { + const sdk = new TracerProviderWithEnvExporters(); + const listOfProcessors = sdk['_spanProcessors']!; + const listOfExporters = sdk['_configuredExporters']; + + assert(spyGetOtlpProtocol.returned('http/protobuf')); + assert(listOfExporters.length === 1); + assert(listOfExporters[0] instanceof OTLPProtoTraceExporter); + assert(listOfProcessors.length === 1); + assert(listOfProcessors[0] instanceof BatchSpanProcessor); + }); + it('use otlp exporter and grpc exporter protocol env value', async () => { + env.OTEL_TRACES_EXPORTER = 'otlp'; + env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'grpc'; + + const sdk = new TracerProviderWithEnvExporters(); + const listOfProcessors = sdk['_spanProcessors']!; + const listOfExporters = sdk['_configuredExporters']; + + assert(spyGetOtlpProtocol.returned('grpc')); + assert(listOfExporters.length === 1); + assert(listOfExporters[0] instanceof OTLPGrpcTraceExporter); + assert(listOfProcessors.length === 1); + assert(listOfProcessors[0] instanceof BatchSpanProcessor); + + delete env.OTEL_TRACES_EXPORTER; + delete env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL; + }); + it('sdk will ignore protocol defined with no-signal env and use signal specific protocol instead', async () => { + env.OTEL_TRACES_EXPORTER = 'otlp'; + env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'http/protobuf'; + env.OTEL_EXPORTER_OTLP_PROTOCOL = 'grpc'; + + const sdk = new TracerProviderWithEnvExporters(); + const listOfProcessors = sdk['_spanProcessors']!; + const listOfExporters = sdk['_configuredExporters']; + + assert(spyGetOtlpProtocol.returned('http/protobuf')); + assert(listOfExporters.length === 1); + assert(listOfExporters[0] instanceof OTLPTraceExporter); + assert(listOfProcessors.length === 1); + assert(listOfProcessors[0] instanceof BatchSpanProcessor); + + delete env.OTEL_TRACES_EXPORTER; + delete env.OTEL_EXPORTER_OTLP_PROTOCOL; + delete env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL; + }); + it('do not use any exporters when empty value is provided for exporter', async () => { + env.OTEL_TRACES_EXPORTER = ''; + const sdk = new TracerProviderWithEnvExporters(); + const listOfProcessors = sdk['_spanProcessors']; + const listOfExporters = sdk['_configuredExporters']; + + assert(spyGetOtlpProtocol.notCalled); + assert(listOfExporters.length === 0); + assert(listOfProcessors === undefined); + env.OTEL_TRACES_EXPORTER = ''; + }); + it('do not use any exporters when none value is only provided', async () => { + env.OTEL_TRACES_EXPORTER = 'none'; + const sdk = new TracerProviderWithEnvExporters(); + const listOfProcessors = sdk['_spanProcessors']; + const listOfExporters = sdk['_configuredExporters']; + + assert(spyGetOtlpProtocol.notCalled); + assert(listOfExporters.length === 0); + assert(listOfProcessors === undefined); + delete env.OTEL_TRACES_EXPORTER; + }); + it('log warning that sdk will not be initalized when exporter is set to none', async () => { + env.OTEL_TRACES_EXPORTER = 'none'; + new TracerProviderWithEnvExporters(); + + assert.strictEqual(stubLoggerError.args[0][0], 'OTEL_TRACES_EXPORTER contains "none" or is empty. SDK will not be initialized.'); + delete env.OTEL_TRACES_EXPORTER; + }); + it('use default exporter when none value is provided with other exports', async () => { + env.OTEL_TRACES_EXPORTER = 'otlp,zipkin,none'; + const sdk = new TracerProviderWithEnvExporters(); + const listOfProcessors = sdk['_spanProcessors']!; + const listOfExporters = sdk['_configuredExporters']; + + assert(listOfExporters[0] instanceof OTLPProtoTraceExporter); + assert(listOfExporters.length === 1); + assert(listOfExporters[0] instanceof OTLPHttpTraceExporter === false); + assert(listOfExporters[0] instanceof ZipkinExporter === false); + assert(listOfProcessors.length === 1); + assert(listOfProcessors[0] instanceof BatchSpanProcessor); + delete env.OTEL_TRACES_EXPORTER; + }); + it('log warning that default exporter will be used since exporter list contains none with other exports ', async () => { + env.OTEL_TRACES_EXPORTER = 'otlp,zipkin,none'; + new TracerProviderWithEnvExporters(); + + assert.strictEqual( + stubLoggerError.args[0][0], 'OTEL_TRACES_EXPORTER contains "none" along with other exporters. Using default otlp exporter.' + ); + delete env.OTEL_TRACES_EXPORTER; + }); + it('should warn that exporter is unrecognized and not able to be set up', async () => { + env.OTEL_TRACES_EXPORTER = 'invalid'; + new TracerProviderWithEnvExporters(); + + assert.strictEqual( + stubLoggerError.args[0][0], 'Unrecognized OTEL_TRACES_EXPORTER value: invalid.' + ); + + assert.strictEqual( + stubLoggerError.args[1][0], 'Unable to set up trace exporter(s) due to invalid exporter and/or protocol values.' + ); + + delete env.OTEL_TRACES_EXPORTER; + }); + it('should log warning when provided protocol name is not valid', async () => { + env.OTEL_EXPORTER_OTLP_PROTOCOL = 'invalid'; + new TracerProviderWithEnvExporters(); + + assert.strictEqual( + stubLoggerError.args[0][0], 'Unsupported OTLP traces protocol: invalid. Using http/protobuf.' + ); + delete env.OTEL_EXPORTER_OTLP_PROTOCOL; + }); + }); + describe('setup zipkin exporter from env', () => { + it('use the zipkin exporter', async () => { + env.OTEL_TRACES_EXPORTER = 'zipkin'; + const sdk = new TracerProviderWithEnvExporters(); + const listOfProcessors = sdk['_spanProcessors']!; + const listOfExporters = sdk['_configuredExporters']; + + assert(listOfExporters.length === 1); + assert(listOfExporters[0] instanceof ZipkinExporter); + assert(listOfProcessors.length === 1); + assert(listOfProcessors[0] instanceof BatchSpanProcessor); + delete env.OTEL_TRACES_EXPORTER; + }); + it('setup zipkin exporter and otlp exporter', async () => { + env.OTEL_TRACES_EXPORTER = 'zipkin, otlp'; + env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'grpc'; + + const sdk = new TracerProviderWithEnvExporters(); + const listOfProcessors = sdk['_spanProcessors']!; + const listOfExporters = sdk['_configuredExporters']; + + assert(spyGetOtlpProtocol.returned('grpc')); + assert(listOfExporters.length === 2); + assert(listOfExporters[0] instanceof ZipkinExporter); + assert(listOfExporters[1] instanceof OTLPGrpcTraceExporter); + assert(listOfProcessors[0] instanceof BatchSpanProcessor); + assert(listOfProcessors[1] instanceof BatchSpanProcessor); + + delete env.OTEL_TRACES_EXPORTER; + delete env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL; + }); + }); + describe('setup jaeger exporter from env', () => { + it('use the jaeger exporter', async () => { + env.OTEL_TRACES_EXPORTER = 'jaeger'; + const sdk = new TracerProviderWithEnvExporters(); + const listOfProcessors = sdk['_spanProcessors']!; + const listOfExporters = sdk['_configuredExporters']; + + assert(listOfExporters.length === 1); + assert(listOfExporters[0] instanceof JaegerExporter); + assert(listOfProcessors.length === 1); + assert(listOfProcessors[0] instanceof BatchSpanProcessor); + delete env.OTEL_TRACES_EXPORTER; + }); + it('setup jaeger exporter and otlp exporter', async () => { + env.OTEL_TRACES_EXPORTER = 'jaeger, otlp'; + env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'http/json'; + const sdk = new TracerProviderWithEnvExporters(); + const listOfProcessors = sdk['_spanProcessors']!; + const listOfExporters = sdk['_configuredExporters']; + + assert(spyGetOtlpProtocol.returned('http/json')); + assert(listOfExporters.length === 2); + assert(listOfExporters[0] instanceof JaegerExporter); + assert(listOfExporters[1] instanceof OTLPHttpTraceExporter); + assert(listOfProcessors.length === 2); + assert(listOfProcessors[0] instanceof BatchSpanProcessor); + assert(listOfProcessors[1] instanceof BatchSpanProcessor); + + delete env.OTEL_TRACES_EXPORTER; + delete env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL; + }); + }); + describe('setup console exporter from env', () => { + it('use the console exporter', async () => { + env.OTEL_TRACES_EXPORTER = 'console'; + const sdk = new TracerProviderWithEnvExporters(); + const listOfProcessors = sdk['_spanProcessors']!; + const listOfExporters = sdk['_configuredExporters']; + + assert(listOfExporters.length === 1); + assert(listOfExporters[0] instanceof ConsoleSpanExporter); + assert(listOfProcessors.length === 1); + assert(listOfProcessors[0] instanceof SimpleSpanProcessor); + delete env.OTEL_TRACES_EXPORTER; + }); + it('ignores the protocol', async () => { + env.OTEL_TRACES_EXPORTER = 'console'; + env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'grpc'; + const sdk = new TracerProviderWithEnvExporters(); + const listOfProcessors = sdk['_spanProcessors']!; + const listOfExporters = sdk['_configuredExporters']; + + assert(spyGetOtlpProtocol.notCalled); + assert(listOfExporters.length === 1); + assert(listOfExporters[0] instanceof ConsoleSpanExporter); + assert(listOfProcessors.length === 1); + assert(listOfProcessors[0] instanceof SimpleSpanProcessor); + + delete env.OTEL_TRACES_EXPORTER; + delete env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL; + }); + it('setup console exporter and otlp exporter', async () => { + env.OTEL_TRACES_EXPORTER = 'console, otlp'; + env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'grpc'; + const sdk = new TracerProviderWithEnvExporters(); + const listOfProcessors = sdk['_spanProcessors']!; + const listOfExporters = sdk['_configuredExporters']; + + assert(spyGetOtlpProtocol.returned('grpc')); + assert(listOfExporters.length === 2); + assert(listOfExporters[0] instanceof ConsoleSpanExporter); + assert(listOfExporters[1] instanceof OTLPGrpcTraceExporter); + assert(listOfProcessors.length === 2); + assert(listOfProcessors[0] instanceof SimpleSpanProcessor); + assert(listOfProcessors[1] instanceof BatchSpanProcessor); + + delete env.OTEL_TRACES_EXPORTER; + delete env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL; + }); + }); +}); diff --git a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts index edad60c5ec..6d853e4902 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts @@ -21,14 +21,22 @@ import { trace, diag, DiagLogLevel, + metrics, } from '@opentelemetry/api'; -import { metrics, NoopMeterProvider } from '@opentelemetry/api-metrics'; import { AsyncHooksContextManager, AsyncLocalStorageContextManager, } from '@opentelemetry/context-async-hooks'; import { CompositePropagator } from '@opentelemetry/core'; -import { AggregationTemporality, ConsoleMetricExporter, InMemoryMetricExporter, InstrumentType, MeterProvider, PeriodicExportingMetricReader, View } from '@opentelemetry/sdk-metrics'; +import { + AggregationTemporality, + ConsoleMetricExporter, + InMemoryMetricExporter, + InstrumentType, + MeterProvider, + PeriodicExportingMetricReader, + View, +} from '@opentelemetry/sdk-metrics'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { assertServiceResource, @@ -36,13 +44,21 @@ import { import { ConsoleSpanExporter, SimpleSpanProcessor, + BatchSpanProcessor, + NoopSpanProcessor, } from '@opentelemetry/sdk-trace-base'; import * as assert from 'assert'; import * as semver from 'semver'; import * as Sinon from 'sinon'; import { NodeSDK } from '../src'; -import { envDetector, processDetector } from '@opentelemetry/resources'; - +import { env } from 'process'; +import { TracerProviderWithEnvExporters } from '../src/TracerProviderWithEnvExporter'; +import { + envDetector, + processDetector, + Resource +} from '@opentelemetry/resources'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; const DefaultContextManager = semver.gte(process.version, '14.8.0') ? AsyncLocalStorageContextManager @@ -66,6 +82,9 @@ describe('Node SDK', () => { describe('Basic Registration', () => { it('should not register any unconfigured SDK components', async () => { + // need to set OTEL_TRACES_EXPORTER to none since default value is otlp + // which sets up an exporter and affects the context manager + env.OTEL_TRACES_EXPORTER = 'none'; const sdk = new NodeSDK({ autoDetectResources: false, }); @@ -75,8 +94,8 @@ describe('Node SDK', () => { assert.strictEqual(context['_getContextManager'](), ctxManager, 'context manager should not change'); assert.strictEqual(propagation['_getGlobalPropagator'](), propagator, 'propagator should not change'); assert.strictEqual((trace.getTracerProvider() as ProxyTracerProvider).getDelegate(), delegate, 'tracer provider should not have changed'); - - assert.ok(metrics.getMeterProvider() instanceof NoopMeterProvider); + assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider)); + delete env.OTEL_TRACES_EXPORTER; }); it('should register a tracer provider if an exporter is provided', async () => { @@ -87,7 +106,7 @@ describe('Node SDK', () => { await sdk.start(); - assert.ok(metrics.getMeterProvider() instanceof NoopMeterProvider); + assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider)); assert.ok( context['_getContextManager']().constructor.name === DefaultContextManager.name @@ -110,7 +129,7 @@ describe('Node SDK', () => { await sdk.start(); - assert.ok(metrics.getMeterProvider() instanceof NoopMeterProvider); + assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider)); assert.ok( context['_getContextManager']().constructor.name === DefaultContextManager.name @@ -123,6 +142,9 @@ describe('Node SDK', () => { }); it('should register a meter provider if a reader is provided', async () => { + // need to set OTEL_TRACES_EXPORTER to none since default value is otlp + // which sets up an exporter and affects the context manager + env.OTEL_TRACES_EXPORTER = 'none'; const exporter = new ConsoleMetricExporter(); const metricReader = new PeriodicExportingMetricReader({ exporter: exporter, @@ -144,6 +166,7 @@ describe('Node SDK', () => { assert.ok(metrics.getMeterProvider() instanceof MeterProvider); await sdk.shutdown(); + delete env.OTEL_TRACES_EXPORTER; }); }); @@ -161,6 +184,9 @@ describe('Node SDK', () => { } it('should register meter views when provided', async () => { + // need to set OTEL_TRACES_EXPORTER to none since default value is otlp + // which sets up an exporter and affects the context manager + env.OTEL_TRACES_EXPORTER = 'none'; const exporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE); const metricReader = new PeriodicExportingMetricReader({ exporter: exporter, @@ -207,6 +233,7 @@ describe('Node SDK', () => { assert.ok(firstMetricRecord.descriptor.name === 'test-view', 'should have renamed counter metric'); await sdk.shutdown(); + delete env.OTEL_TRACES_EXPORTER; }); it('should throw error when calling configureMeterProvider when views are already configured', () => { @@ -284,19 +311,47 @@ describe('Node SDK', () => { delete process.env.OTEL_RESOURCE_ATTRIBUTES; }); - describe('with a buggy detector', () => { + describe('with a custom resource', () => { it('returns a merged resource', async () => { const sdk = new NodeSDK({ autoDetectResources: true, + resourceDetectors: [processDetector, { + async detect(): Promise { + return new Resource({'customAttr': 'someValue'}); + } + }, + envDetector] + }); + await sdk.detectResources(); + const resource = sdk['_resource']; + + assert.strictEqual( + resource.attributes['customAttr'], + 'someValue' + ); + + assertServiceResource(resource, { + instanceId: '627cc493', + name: 'my-service', + namespace: 'default', + version: '0.0.1', }); - await sdk.detectResources({ - detectors: [processDetector, { + }); + }); + + describe('with a buggy detector', () => { + it('returns a merged resource', async () => { + const sdk = new NodeSDK({ + autoDetectResources: true, + resourceDetectors: [processDetector, { detect() { throw new Error('Buggy detector'); } }, envDetector] }); + + await sdk.detectResources(); const resource = sdk['_resource']; assertServiceResource(resource, { @@ -361,7 +416,7 @@ describe('Node SDK', () => { describe('with a faulty environment variable', () => { beforeEach(() => { - process.env.OTEL_RESOURCE_ATTRIBUTES = 'bad=~attribute'; + process.env.OTEL_RESOURCE_ATTRIBUTES = 'bad=\\attribute'; }); it('prints correct error messages when EnvDetector has an invalid variable', async () => { @@ -431,7 +486,6 @@ describe('Node SDK', () => { delete process.env.OTEL_SERVICE_NAME; }); - it('should configure service name via OTEL_RESOURCE_ATTRIBUTES env var', async () => { process.env.OTEL_RESOURCE_ATTRIBUTES = 'service.name=resource-env-set-name'; const sdk = new NodeSDK(); @@ -461,3 +515,178 @@ describe('Node SDK', () => { }); }); }); + +describe('setup exporter from env', () => { + let spyGetOtlpProtocol: Sinon.SinonSpy; + let stubLoggerError: Sinon.SinonStub; + + beforeEach(() => { + spyGetOtlpProtocol = Sinon.spy(TracerProviderWithEnvExporters, 'getOtlpProtocol'); + stubLoggerError = Sinon.stub(diag, 'warn'); + }); + afterEach(() => { + spyGetOtlpProtocol.restore(); + stubLoggerError.restore(); + }); + it('use default exporter TracerProviderWithEnvExporters when user does not provide span processor or trace exporter to sdk config', async () => { + const sdk = new NodeSDK(); + await sdk.start(); + const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; + + assert(sdk['_tracerProvider'] instanceof TracerProviderWithEnvExporters); + assert(listOfProcessors.length === 1); + assert(listOfProcessors[0] instanceof BatchSpanProcessor); + }); + it('ignore env exporter when user provides exporter to sdk config', async () => { + const traceExporter = new ConsoleSpanExporter(); + const sdk = new NodeSDK({ + traceExporter + }); + await sdk.start(); + const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; + + assert(sdk['_tracerProvider'] instanceof TracerProviderWithEnvExporters === false); + assert(listOfProcessors.length === 1); + assert(listOfProcessors[0] instanceof SimpleSpanProcessor === false); + assert(listOfProcessors[0] instanceof BatchSpanProcessor); + }); + it('ignores default env exporter when user provides span processor to sdk config', async () => { + const traceExporter = new ConsoleSpanExporter(); + const spanProcessor = new SimpleSpanProcessor(traceExporter); + const sdk = new NodeSDK({ + spanProcessor + }); + await sdk.start(); + const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; + + assert(sdk['_tracerProvider'] instanceof TracerProviderWithEnvExporters === false); + assert(listOfProcessors.length === 1); + assert(listOfProcessors[0] instanceof SimpleSpanProcessor); + assert(listOfProcessors[0] instanceof BatchSpanProcessor === false); + }); + it('ignores env exporter when user provides tracer exporter to sdk config and sets exporter via env', async () => { + env.OTEL_TRACES_EXPORTER = 'console'; + const traceExporter = new OTLPTraceExporter(); + const sdk = new NodeSDK({ + traceExporter + }); + await sdk.start(); + const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; + + assert(sdk['_tracerProvider'] instanceof TracerProviderWithEnvExporters === false); + assert(listOfProcessors.length === 1); + assert(listOfProcessors[0] instanceof SimpleSpanProcessor === false); + assert(listOfProcessors[0] instanceof BatchSpanProcessor); + delete env.OTEL_TRACES_EXPORTER; + }); + it('use otlp exporter and defined exporter protocol env value', async () => { + env.OTEL_TRACES_EXPORTER = 'otlp'; + env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'grpc'; + const sdk = new NodeSDK(); + await sdk.start(); + + const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; + assert(sdk['_tracerProvider'] instanceof TracerProviderWithEnvExporters); + assert(listOfProcessors.length === 1); + assert(listOfProcessors[0] instanceof BatchSpanProcessor); + delete env.OTEL_TRACES_EXPORTER; + delete env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL; + }); + it('use noop span processor when user sets env exporter to none', async () => { + env.OTEL_TRACES_EXPORTER = 'none'; + const sdk = new NodeSDK(); + await sdk.start(); + + const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; + const activeProcessor = sdk['_tracerProvider']?.getActiveSpanProcessor(); + + assert(listOfProcessors.length === 0); + assert(activeProcessor instanceof NoopSpanProcessor); + delete env.OTEL_TRACES_EXPORTER; + }); + it('log warning that sdk will not be initalized when exporter is set to none', async () => { + env.OTEL_TRACES_EXPORTER = 'none'; + const sdk = new NodeSDK(); + await sdk.start(); + + assert.strictEqual(stubLoggerError.args[0][0], 'OTEL_TRACES_EXPORTER contains "none" or is empty. SDK will not be initialized.'); + delete env.OTEL_TRACES_EXPORTER; + }); + it('do not use any exporters when empty value is provided for exporter', async () => { + env.OTEL_TRACES_EXPORTER = ''; + const sdk = new NodeSDK(); + await sdk.start(); + + const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; + const activeProcessor = sdk['_tracerProvider']?.getActiveSpanProcessor(); + + assert(listOfProcessors.length === 0); + assert(activeProcessor instanceof NoopSpanProcessor); + env.OTEL_TRACES_EXPORTER = ''; + }); + + it('use only default exporter when none value is provided with other exporters', async () => { + env.OTEL_TRACES_EXPORTER = 'otlp,zipkin,none'; + const sdk = new NodeSDK(); + await sdk.start(); + + const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; + assert(sdk['_tracerProvider'] instanceof TracerProviderWithEnvExporters); + assert(listOfProcessors.length === 1); + assert(listOfProcessors[0] instanceof BatchSpanProcessor); + + delete env.OTEL_TRACES_EXPORTER; + }); + it('log warning that only default exporter will be used since exporter list contains none with other exports ', async () => { + env.OTEL_TRACES_EXPORTER = 'otlp,zipkin,none'; + const sdk = new NodeSDK(); + await sdk.start(); + + assert.strictEqual( + stubLoggerError.args[0][0], 'OTEL_TRACES_EXPORTER contains "none" along with other exporters. Using default otlp exporter.' + ); + delete env.OTEL_TRACES_EXPORTER; + }); + it('should warn that provided exporter value is unrecognized and not able to be set up', async () => { + env.OTEL_TRACES_EXPORTER = 'invalid'; + const sdk = new NodeSDK(); + await sdk.start(); + + assert.strictEqual( + stubLoggerError.args[0][0], 'Unrecognized OTEL_TRACES_EXPORTER value: invalid.' + ); + + assert.strictEqual( + stubLoggerError.args[1][0], 'Unable to set up trace exporter(s) due to invalid exporter and/or protocol values.' + ); + + delete env.OTEL_TRACES_EXPORTER; + }); + it('setup zipkin, jaeger and otlp exporters', async () => { + env.OTEL_TRACES_EXPORTER = 'zipkin, otlp, jaeger'; + env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'grpc'; + const sdk = new NodeSDK(); + await sdk.start(); + + const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; + assert(sdk['_tracerProvider'] instanceof TracerProviderWithEnvExporters); + assert(listOfProcessors.length === 3); + assert(listOfProcessors[0] instanceof BatchSpanProcessor); + assert(listOfProcessors[1] instanceof BatchSpanProcessor); + assert(listOfProcessors[2] instanceof BatchSpanProcessor); + + delete env.OTEL_TRACES_EXPORTER; + delete env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL; + }); + it('use the console exporter', async () => { + env.OTEL_TRACES_EXPORTER = 'console, otlp'; + const sdk = new NodeSDK(); + await sdk.start(); + + const listOfProcessors = sdk['_tracerProvider']!['_registeredSpanProcessors']!; + assert(listOfProcessors.length === 2); + assert(listOfProcessors[0] instanceof SimpleSpanProcessor); + assert(listOfProcessors[1] instanceof BatchSpanProcessor); + delete env.OTEL_TRACES_EXPORTER; + }); +}); diff --git a/experimental/packages/opentelemetry-sdk-node/tsconfig.json b/experimental/packages/opentelemetry-sdk-node/tsconfig.json index 8249dd1ce6..3b5a3efd79 100644 --- a/experimental/packages/opentelemetry-sdk-node/tsconfig.json +++ b/experimental/packages/opentelemetry-sdk-node/tsconfig.json @@ -9,12 +9,21 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../../api" + }, { "path": "../../../packages/opentelemetry-context-async-hooks" }, { "path": "../../../packages/opentelemetry-core" }, + { + "path": "../../../packages/opentelemetry-exporter-jaeger" + }, + { + "path": "../../../packages/opentelemetry-exporter-zipkin" + }, { "path": "../../../packages/opentelemetry-resources" }, @@ -28,13 +37,19 @@ "path": "../../../packages/opentelemetry-semantic-conventions" }, { - "path": "../opentelemetry-api-metrics" + "path": "../../../packages/sdk-metrics" }, { - "path": "../opentelemetry-instrumentation" + "path": "../exporter-trace-otlp-grpc" + }, + { + "path": "../exporter-trace-otlp-http" }, { - "path": "../opentelemetry-sdk-metrics" + "path": "../exporter-trace-otlp-proto" + }, + { + "path": "../opentelemetry-instrumentation" } ] } diff --git a/experimental/packages/otlp-exporter-base/README.md b/experimental/packages/otlp-exporter-base/README.md index cdb8a26cdc..85f96aecd8 100644 --- a/experimental/packages/otlp-exporter-base/README.md +++ b/experimental/packages/otlp-exporter-base/README.md @@ -3,6 +3,8 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] +**Note: This is an experimental package under active development. New releases may include breaking changes.** + This module provides a base exporter for web and node to be used with [opentelemetry-collector][opentelemetry-collector-url]. ## Installation @@ -37,6 +39,3 @@ Apache 2.0 - See [LICENSE][license-url] for more information. [npm-url-proto]: https://www.npmjs.com/package/@opentelemetry/otlp-proto-exporter-base [npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fotlp-exporter-base.svg [opentelemetry-collector-url]: https://github.com/open-telemetry/opentelemetry-collector -[opentelemetry-spec-protocol-exporter]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#configuration-options -[semconv-resource-service-name]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/README.md#service -[metrics-exporter-url]: https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-metrics-otlp-http diff --git a/experimental/packages/otlp-exporter-base/package.json b/experimental/packages/otlp-exporter-base/package.json index 3cc6e7e75e..712a269d62 100644 --- a/experimental/packages/otlp-exporter-base/package.json +++ b/experimental/packages/otlp-exporter-base/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/otlp-exporter-base", - "version": "0.32.0", + "version": "0.34.0", "description": "OpenTelemetry OTLP Exporter base (for internal use only)", "main": "build/src/index.js", "module": "build/esm/index.js", @@ -61,11 +61,11 @@ "access": "public" }, "dependencies": { - "@opentelemetry/core": "1.6.0" + "@opentelemetry/core": "1.8.0" }, "devDependencies": { "@opentelemetry/api": "^1.0.0", - "@types/mocha": "9.1.1", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "codecov": "3.8.3", @@ -81,5 +81,6 @@ "peerDependencies": { "@opentelemetry/api": "^1.0.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/otlp-exporter-base" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/otlp-exporter-base", + "sideEffects": false } diff --git a/experimental/packages/otlp-exporter-base/src/util.ts b/experimental/packages/otlp-exporter-base/src/util.ts index bfe9209758..6290153bcb 100644 --- a/experimental/packages/otlp-exporter-base/src/util.ts +++ b/experimental/packages/otlp-exporter-base/src/util.ts @@ -53,14 +53,19 @@ export function appendResourcePathToUrl(url: string, path: string): string { /** * Adds root path to signal specific endpoint when endpoint contains no path part and no root path * @param url - * @param path * @returns url */ -export function appendRootPathToUrlIfNeeded(url: string, path: string): string { - if (!url.includes(path) && !url.endsWith('/')) { - url = url + '/'; +export function appendRootPathToUrlIfNeeded(url: string): string { + try { + const parsedUrl = new URL(url); + if (parsedUrl.pathname === '') { + parsedUrl.pathname = parsedUrl.pathname + '/'; + } + return parsedUrl.toString(); + } catch { + diag.warn(`Could not parse export URL: '${url}'`); + return url; } - return url; } /** @@ -83,7 +88,7 @@ export function configureExporterTimeout(timeoutMillis: number | undefined): num function getExporterTimeoutFromEnv(): number { const definedTimeout = Number(getEnv().OTEL_EXPORTER_OTLP_TRACES_TIMEOUT ?? - getEnv().OTEL_EXPORTER_OTLP_TIMEOUT); + getEnv().OTEL_EXPORTER_OTLP_TIMEOUT); if (definedTimeout <= 0) { // OTLP exporter configured timeout - using default value of 10000ms diff --git a/experimental/packages/otlp-exporter-base/test/common/util.test.ts b/experimental/packages/otlp-exporter-base/test/common/util.test.ts index cc29de641d..d78b719faa 100644 --- a/experimental/packages/otlp-exporter-base/test/common/util.test.ts +++ b/experimental/packages/otlp-exporter-base/test/common/util.test.ts @@ -20,7 +20,7 @@ import { diag } from '@opentelemetry/api'; import { parseHeaders, appendResourcePathToUrl, - appendRootPathToUrlIfNeeded + appendRootPathToUrlIfNeeded, } from '../../src/util'; describe('utils', () => { @@ -84,23 +84,35 @@ describe('utils', () => { describe('appendRootPathToUrlIfNeeded - specifc signal http endpoint', () => { it('should append root path when missing', () => { const url = 'http://foo.bar'; - const resourcePath = 'v1/traces'; - const finalUrl = appendRootPathToUrlIfNeeded(url, resourcePath); + const finalUrl = appendRootPathToUrlIfNeeded(url); assert.strictEqual(finalUrl, url + '/'); }); it('should not append root path and return same url', () => { const url = 'http://foo.bar/'; - const resourcePath = 'v1/traces'; - const finalUrl = appendRootPathToUrlIfNeeded(url, resourcePath); + const finalUrl = appendRootPathToUrlIfNeeded(url); assert.strictEqual(finalUrl, url); }); - it('should append root path when url contains resource path', () => { - const url = 'http://foo.bar/v1/traces'; - const resourcePath = 'v1/traces'; + it('should not append root path when url contains resource path', () => { + { + const url = 'http://foo.bar/v1/traces'; + + const finalUrl = appendRootPathToUrlIfNeeded(url); + assert.strictEqual(finalUrl, url); + } + { + const url = 'https://endpoint/something'; + + const finalUrl = appendRootPathToUrlIfNeeded(url); + assert.strictEqual(finalUrl, url); + } + }); + + it('should not change string when url is not parseable', () => { + const url = 'this is not a URL'; - const finalUrl = appendRootPathToUrlIfNeeded(url, resourcePath); + const finalUrl = appendRootPathToUrlIfNeeded(url); assert.strictEqual(finalUrl, url); }); }); diff --git a/experimental/packages/otlp-exporter-base/tsconfig.json b/experimental/packages/otlp-exporter-base/tsconfig.json index 3e284c16fc..fbb799ec14 100644 --- a/experimental/packages/otlp-exporter-base/tsconfig.json +++ b/experimental/packages/otlp-exporter-base/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../../api" + }, { "path": "../../../packages/opentelemetry-core" } diff --git a/experimental/packages/otlp-grpc-exporter-base/README.md b/experimental/packages/otlp-grpc-exporter-base/README.md index c55d7d2e2a..d49ece2a36 100644 --- a/experimental/packages/otlp-grpc-exporter-base/README.md +++ b/experimental/packages/otlp-grpc-exporter-base/README.md @@ -3,6 +3,8 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] +**Note: This is an experimental package under active development. New releases may include breaking changes.** + This module provides a gRPC exporter base for Node.js (browsers not supported) to be used with [opentelemetry-collector][opentelemetry-collector-url]. ## Installation @@ -27,5 +29,3 @@ Apache 2.0 - See [LICENSE][license-url] for more information. [npm-url]: https://www.npmjs.com/package/@opentelemetry/otlp-grpc-exporter-base [npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fotlp-grpc-exporter-base.svg [opentelemetry-collector-url]: https://github.com/open-telemetry/opentelemetry-collector -[semconv-resource-service-name]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/README.md#service -[metrics-exporter-url]: https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc diff --git a/experimental/packages/otlp-grpc-exporter-base/package.json b/experimental/packages/otlp-grpc-exporter-base/package.json index 12ede6c932..e90632e22c 100644 --- a/experimental/packages/otlp-grpc-exporter-base/package.json +++ b/experimental/packages/otlp-grpc-exporter-base/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/otlp-grpc-exporter-base", - "version": "0.32.0", + "version": "0.34.0", "description": "OpenTelemetry OTLP-gRPC Exporter base (for internal use only)", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -51,10 +51,10 @@ "devDependencies": { "@babel/core": "7.16.0", "@opentelemetry/api": "^1.0.0", - "@opentelemetry/otlp-transformer": "0.32.0", - "@opentelemetry/resources": "1.6.0", - "@opentelemetry/sdk-trace-base": "1.6.0", - "@types/mocha": "9.1.1", + "@opentelemetry/otlp-transformer": "0.34.0", + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "codecov": "3.8.3", @@ -71,10 +71,11 @@ "@opentelemetry/api": "^1.0.0" }, "dependencies": { - "@grpc/grpc-js": "^1.5.9", - "@grpc/proto-loader": "^0.6.9", - "@opentelemetry/core": "1.6.0", - "@opentelemetry/otlp-exporter-base": "0.32.0" + "@grpc/grpc-js": "^1.7.1", + "@grpc/proto-loader": "^0.7.3", + "@opentelemetry/core": "1.8.0", + "@opentelemetry/otlp-exporter-base": "0.34.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/otlp-grpc-exporter-base" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/otlp-grpc-exporter-base", + "sideEffects": false } diff --git a/experimental/packages/otlp-grpc-exporter-base/tsconfig.json b/experimental/packages/otlp-grpc-exporter-base/tsconfig.json index 087c804079..bb73fd2a34 100644 --- a/experimental/packages/otlp-grpc-exporter-base/tsconfig.json +++ b/experimental/packages/otlp-grpc-exporter-base/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../../api" + }, { "path": "../../../packages/opentelemetry-core" }, diff --git a/experimental/packages/otlp-proto-exporter-base/README.md b/experimental/packages/otlp-proto-exporter-base/README.md index adeffe39f4..84cd880dcd 100644 --- a/experimental/packages/otlp-proto-exporter-base/README.md +++ b/experimental/packages/otlp-proto-exporter-base/README.md @@ -3,6 +3,8 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] +**Note: This is an experimental package under active development. New releases may include breaking changes.** + This module provides a OTLP-http/protobuf exporter base for Node.js (browsers not supported) to be used with [opentelemetry-collector][opentelemetry-collector-url]. ## Installation @@ -27,5 +29,3 @@ Apache 2.0 - See [LICENSE][license-url] for more information. [npm-url]: https://www.npmjs.com/package/@opentelemetry/otlp-proto-exporter-base [npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fotlp-proto-exporter-base.svg [opentelemetry-collector-url]: https://github.com/open-telemetry/opentelemetry-collector -[semconv-resource-service-name]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/README.md#service -[metrics-exporter-url]: https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-metrics-otlp-proto diff --git a/experimental/packages/otlp-proto-exporter-base/package.json b/experimental/packages/otlp-proto-exporter-base/package.json index a9ae89dcec..71653bd0d6 100644 --- a/experimental/packages/otlp-proto-exporter-base/package.json +++ b/experimental/packages/otlp-proto-exporter-base/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/otlp-proto-exporter-base", - "version": "0.32.0", + "version": "0.34.0", "description": "OpenTelemetry OTLP-HTTP-protobuf Exporter base (for internal use only)", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -46,13 +46,13 @@ "devDependencies": { "@babel/core": "7.16.0", "@opentelemetry/api": "^1.0.0", - "@types/mocha": "9.1.1", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "codecov": "3.8.3", "mocha": "10.0.0", "nyc": "15.1.0", - "protobufjs": "6.11.3", + "protobufjs-cli": "1.0.2", "rimraf": "3.0.2", "sinon": "14.0.0", "ts-loader": "8.4.0", @@ -63,9 +63,10 @@ "@opentelemetry/api": "^1.0.0" }, "dependencies": { - "@grpc/proto-loader": "^0.6.9", - "@opentelemetry/core": "1.6.0", - "@opentelemetry/otlp-exporter-base": "0.32.0" + "@opentelemetry/core": "1.8.0", + "@opentelemetry/otlp-exporter-base": "0.34.0", + "protobufjs": "7.1.1" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/otlp-proto-exporter-base" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/otlp-proto-exporter-base", + "sideEffects": false } diff --git a/experimental/packages/otlp-proto-exporter-base/tsconfig.json b/experimental/packages/otlp-proto-exporter-base/tsconfig.json index 3e05eef24c..b0fd3b0b55 100644 --- a/experimental/packages/otlp-proto-exporter-base/tsconfig.json +++ b/experimental/packages/otlp-proto-exporter-base/tsconfig.json @@ -11,6 +11,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../../api" + }, { "path": "../../../packages/opentelemetry-core" }, diff --git a/experimental/packages/otlp-transformer/README.md b/experimental/packages/otlp-transformer/README.md index fc5c13af9e..e069aab559 100644 --- a/experimental/packages/otlp-transformer/README.md +++ b/experimental/packages/otlp-transformer/README.md @@ -3,7 +3,9 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] -**NOTE: This package is intended for internal use only.** +**Note: This package is intended for internal use only.** + +**Note: This is an experimental package under active development. New releases may include breaking changes.** This package provides everything needed to serialize [OpenTelemetry SDK][sdk] traces and metrics into the [OpenTelemetry Protocol][otlp] format. diff --git a/experimental/packages/otlp-transformer/package.json b/experimental/packages/otlp-transformer/package.json index b15be78d13..4d0f59f88e 100644 --- a/experimental/packages/otlp-transformer/package.json +++ b/experimental/packages/otlp-transformer/package.json @@ -4,7 +4,7 @@ "publishConfig": { "access": "public" }, - "version": "0.32.0", + "version": "0.34.0", "description": "Transform OpenTelemetry SDK data into OTLP", "module": "build/esm/index.js", "esnext": "build/esnext/index.js", @@ -52,11 +52,11 @@ "README.md" ], "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0" + "@opentelemetry/api": ">=1.3.0 <1.4.0" }, "devDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0", - "@types/mocha": "9.1.1", + "@opentelemetry/api": ">=1.3.0 <1.4.0", + "@types/mocha": "10.0.0", "@types/webpack-env": "1.16.3", "codecov": "3.8.3", "istanbul-instrumenter-loader": "3.0.1", @@ -69,7 +69,6 @@ "mkdirp": "1.0.4", "mocha": "10.0.0", "nyc": "15.1.0", - "protobufjs": "6.11.3", "rimraf": "3.0.2", "ts-loader": "8.4.0", "ts-mocha": "10.0.0", @@ -77,11 +76,11 @@ "webpack": "4.46.0" }, "dependencies": { - "@opentelemetry/api-metrics": "0.32.0", - "@opentelemetry/core": "1.6.0", - "@opentelemetry/resources": "1.6.0", - "@opentelemetry/sdk-metrics": "0.32.0", - "@opentelemetry/sdk-trace-base": "1.6.0" + "@opentelemetry/core": "1.8.0", + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/sdk-metrics": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/otlp-transformer" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/otlp-transformer", + "sideEffects": false } diff --git a/experimental/packages/otlp-transformer/src/metrics/internal.ts b/experimental/packages/otlp-transformer/src/metrics/internal.ts index bc17b84976..7aa9c82acd 100644 --- a/experimental/packages/otlp-transformer/src/metrics/internal.ts +++ b/experimental/packages/otlp-transformer/src/metrics/internal.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ValueType } from '@opentelemetry/api-metrics'; +import { ValueType } from '@opentelemetry/api'; import { hrTimeToNanoseconds } from '@opentelemetry/core'; import { AggregationTemporality, diff --git a/experimental/packages/otlp-transformer/test/metrics.test.ts b/experimental/packages/otlp-transformer/test/metrics.test.ts index afb65d264a..df88389c5e 100644 --- a/experimental/packages/otlp-transformer/test/metrics.test.ts +++ b/experimental/packages/otlp-transformer/test/metrics.test.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ValueType } from '@opentelemetry/api-metrics'; +import { ValueType } from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; import { AggregationTemporality, diff --git a/experimental/packages/otlp-transformer/tsconfig.json b/experimental/packages/otlp-transformer/tsconfig.json index a396bcb48f..cbcb2b9bc6 100644 --- a/experimental/packages/otlp-transformer/tsconfig.json +++ b/experimental/packages/otlp-transformer/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../../api" + }, { "path": "../../../packages/opentelemetry-core" }, @@ -19,10 +22,7 @@ "path": "../../../packages/opentelemetry-sdk-trace-base" }, { - "path": "../opentelemetry-api-metrics" - }, - { - "path": "../opentelemetry-sdk-metrics" + "path": "../../../packages/sdk-metrics" } ] } diff --git a/integration-tests/propagation-validation-server/package.json b/integration-tests/propagation-validation-server/package.json index d937b351af..78383d4673 100644 --- a/integration-tests/propagation-validation-server/package.json +++ b/integration-tests/propagation-validation-server/package.json @@ -1,6 +1,6 @@ { "name": "propagation-validation-server", - "version": "1.6.0", + "version": "1.8.0", "description": "server for w3c tests", "main": "validation_server.js", "private": true, @@ -12,9 +12,9 @@ }, "dependencies": { "@opentelemetry/api": "^1.0.0", - "@opentelemetry/context-async-hooks": "1.6.0", - "@opentelemetry/core": "1.6.0", - "@opentelemetry/sdk-trace-base": "1.6.0", + "@opentelemetry/context-async-hooks": "1.8.0", + "@opentelemetry/core": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0", "axios": "0.24.0", "body-parser": "1.19.0", "express": "4.17.1" diff --git a/integration-tests/propagation-validation-server/tsconfig.json b/integration-tests/propagation-validation-server/tsconfig.json index eb74c6b4c6..7b374cea7b 100644 --- a/integration-tests/propagation-validation-server/tsconfig.json +++ b/integration-tests/propagation-validation-server/tsconfig.json @@ -6,6 +6,9 @@ }, "include": [], "references": [ + { + "path": "../../api" + }, { "path": "../../packages/opentelemetry-context-async-hooks" }, diff --git a/lerna.json b/lerna.json index 04c90d0ef0..d97ab02a62 100644 --- a/lerna.json +++ b/lerna.json @@ -2,8 +2,10 @@ "version": "independent", "npmClient": "npm", "packages": [ + "api", "packages/*", "experimental/packages/*", + "experimental/examples/*", "experimental/backwards-compatability/*", "integration-tests/*", "selenium-tests", diff --git a/package.json b/package.json index e8bf21849a..716db784bb 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,9 @@ "codecov:browser": "lerna run codecov:browser", "codecov:webworker": "lerna run codecov:webworker", "predocs-test": "npm run docs", - "docs": "typedoc && touch docs/.nojekyll", + "docs": "typedoc --readme none && touch docs/.nojekyll", "docs-deploy": "gh-pages --dotfiles --dist docs", - "docs:test": "linkinator docs --silent --retry && linkinator doc/*.md --silent --retry", + "docs:test": "linkinator docs --silent --retry && linkinator doc/*.md --skip http://localhost:3000 --skip http://localhost:9464 --silent --retry", "lint": "lerna run lint", "lint:changed": "lerna run --concurrency 1 --stream lint --since HEAD --exclude-dependents", "lint:fix": "lerna run lint:fix", @@ -54,10 +54,10 @@ "eslint-plugin-header": "3.1.1", "eslint-plugin-node": "11.1.0", "gh-pages": "4.0.0", - "lerna": "5.4.3", + "lerna": "6.0.3", "lerna-changelog": "2.2.0", - "linkinator": "4.0.2", - "markdownlint-cli": "0.29.0", + "linkinator": "4.0.3", + "markdownlint-cli": "0.32.2", "semver": "7.3.5", "typedoc": "0.22.10", "typescript": "4.4.4", diff --git a/packages/opentelemetry-context-async-hooks/README.md b/packages/opentelemetry-context-async-hooks/README.md index 553156d7c7..6817826f68 100644 --- a/packages/opentelemetry-context-async-hooks/README.md +++ b/packages/opentelemetry-context-async-hooks/README.md @@ -3,9 +3,9 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] -This package provides two [`ContextManager`](https://open-telemetry.github.io/opentelemetry-js-api/interfaces/contextmanager.html) implementations built on APIs from Node.js's [`async_hooks`][async-hooks-doc] module. If you're looking for a `ContextManager` to use in browser environments, consider [opentelemetry-context-zone](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-context-zone) or [opentelemetry-context-zone-peer-dep](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-context-zone-peer-dep). +This package provides two [`ContextManager`](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api.ContextManager.html) implementations built on APIs from Node.js's [`async_hooks`][async-hooks-doc] module. If you're looking for a `ContextManager` to use in browser environments, consider [opentelemetry-context-zone](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-context-zone) or [opentelemetry-context-zone-peer-dep](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-context-zone-peer-dep). -The definition of the `ContextManager` interface and the problem it solves can be found [here](def-context-manager). +The definition of the `ContextManager` interface and the problem it solves can be found [here][def-context-manager]. ## API diff --git a/packages/opentelemetry-context-async-hooks/package.json b/packages/opentelemetry-context-async-hooks/package.json index cc06b47ca7..cf1dc3636e 100644 --- a/packages/opentelemetry-context-async-hooks/package.json +++ b/packages/opentelemetry-context-async-hooks/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/context-async-hooks", - "version": "1.6.0", + "version": "1.8.0", "description": "OpenTelemetry AsyncHooks-based Context Manager", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -44,8 +44,8 @@ "access": "public" }, "devDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0", - "@types/mocha": "9.1.1", + "@opentelemetry/api": ">=1.0.0 <1.4.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "codecov": "3.8.3", "mocha": "10.0.0", @@ -55,7 +55,8 @@ "typescript": "4.4.4" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.4.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-context-async-hooks" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-context-async-hooks", + "sideEffects": false } diff --git a/packages/opentelemetry-context-async-hooks/tsconfig.json b/packages/opentelemetry-context-async-hooks/tsconfig.json index bdc94d2213..b1ea0838e6 100644 --- a/packages/opentelemetry-context-async-hooks/tsconfig.json +++ b/packages/opentelemetry-context-async-hooks/tsconfig.json @@ -7,5 +7,10 @@ "include": [ "src/**/*.ts", "test/**/*.ts" + ], + "references": [ + { + "path": "../../api" + } ] } diff --git a/packages/opentelemetry-context-zone-peer-dep/README.md b/packages/opentelemetry-context-zone-peer-dep/README.md index e8c08b74c9..a505b9fbdd 100644 --- a/packages/opentelemetry-context-zone-peer-dep/README.md +++ b/packages/opentelemetry-context-zone-peer-dep/README.md @@ -20,8 +20,11 @@ npm install --save @opentelemetry/context-zone-peer-dep ```js import { context, trace } from '@opentelemetry/api'; -import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'; +import { + ConsoleSpanExporter, + SimpleSpanProcessor, + WebTracerProvider, +} from '@opentelemetry/sdk-trace-web'; import { ZoneContextManager } from '@opentelemetry/context-zone-peer-dep'; const providerWithZone = new WebTracerProvider(); diff --git a/packages/opentelemetry-context-zone-peer-dep/package.json b/packages/opentelemetry-context-zone-peer-dep/package.json index e2fd07fe4b..153d158d37 100644 --- a/packages/opentelemetry-context-zone-peer-dep/package.json +++ b/packages/opentelemetry-context-zone-peer-dep/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/context-zone-peer-dep", - "version": "1.6.0", + "version": "1.8.0", "description": "OpenTelemetry Context Zone with peer dependency for zone.js", "main": "build/src/index.js", "module": "build/esm/index.js", @@ -54,8 +54,8 @@ }, "devDependencies": { "@babel/core": "7.16.0", - "@opentelemetry/api": ">=1.0.0 <1.3.0", - "@types/mocha": "9.1.1", + "@opentelemetry/api": ">=1.0.0 <1.4.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "@types/webpack-env": "1.16.3", @@ -81,7 +81,7 @@ "zone.js": "0.11.4" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0", + "@opentelemetry/api": ">=1.0.0 <1.4.0", "zone.js": "^0.10.2 || ^0.11.0" }, "sideEffects": false, diff --git a/packages/opentelemetry-context-zone-peer-dep/tsconfig.json b/packages/opentelemetry-context-zone-peer-dep/tsconfig.json index 479d4bc0d2..da6d2b6486 100644 --- a/packages/opentelemetry-context-zone-peer-dep/tsconfig.json +++ b/packages/opentelemetry-context-zone-peer-dep/tsconfig.json @@ -11,5 +11,10 @@ "include": [ "src/**/*.ts", "test/**/*.ts" + ], + "references": [ + { + "path": "../../api" + } ] } diff --git a/packages/opentelemetry-context-zone/README.md b/packages/opentelemetry-context-zone/README.md index 19ba530901..0162f9645d 100644 --- a/packages/opentelemetry-context-zone/README.md +++ b/packages/opentelemetry-context-zone/README.md @@ -17,8 +17,11 @@ npm install --save @opentelemetry/context-zone ```js import { context, trace } from '@opentelemetry/api'; -import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'; +import { + ConsoleSpanExporter, + SimpleSpanProcessor, + WebTracerProvider, +} from '@opentelemetry/sdk-trace-web'; import { ZoneContextManager } from '@opentelemetry/context-zone'; const providerWithZone = new WebTracerProvider(); diff --git a/packages/opentelemetry-context-zone/package.json b/packages/opentelemetry-context-zone/package.json index d9d38cd4e5..14f36b16fb 100644 --- a/packages/opentelemetry-context-zone/package.json +++ b/packages/opentelemetry-context-zone/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/context-zone", - "version": "1.6.0", + "version": "1.8.0", "description": "OpenTelemetry Context Zone", "main": "build/src/index.js", "module": "build/esm/index.js", @@ -51,7 +51,7 @@ }, "devDependencies": { "@babel/core": "7.16.0", - "@types/mocha": "9.1.1", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "@types/webpack-env": "1.16.3", @@ -74,7 +74,7 @@ "webpack-merge": "5.8.0" }, "dependencies": { - "@opentelemetry/context-zone-peer-dep": "1.6.0", + "@opentelemetry/context-zone-peer-dep": "1.8.0", "zone.js": "^0.11.0" }, "sideEffects": true, diff --git a/packages/opentelemetry-core/package.json b/packages/opentelemetry-core/package.json index a5fab5ea21..7fa8b1d418 100644 --- a/packages/opentelemetry-core/package.json +++ b/packages/opentelemetry-core/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/core", - "version": "1.6.0", + "version": "1.8.0", "description": "OpenTelemetry Core provides constants and utilities shared by all OpenTelemetry SDK packages.", "main": "build/src/index.js", "module": "build/esm/index.js", @@ -64,8 +64,8 @@ "access": "public" }, "devDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0", - "@types/mocha": "9.1.1", + "@opentelemetry/api": ">=1.0.0 <1.4.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "@types/webpack-env": "1.16.3", @@ -77,7 +77,7 @@ "karma-mocha": "2.0.1", "karma-spec-reporter": "0.0.32", "karma-webpack": "4.0.2", - "lerna": "5.4.3", + "lerna": "6.0.3", "mocha": "10.0.0", "nyc": "15.1.0", "rimraf": "3.0.2", @@ -88,10 +88,11 @@ "webpack": "4.46.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.4.0" }, "dependencies": { - "@opentelemetry/semantic-conventions": "1.6.0" + "@opentelemetry/semantic-conventions": "1.8.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-core" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-core", + "sideEffects": false } diff --git a/packages/opentelemetry-core/src/common/anchored-clock.ts b/packages/opentelemetry-core/src/common/anchored-clock.ts new file mode 100644 index 0000000000..a47fe796b7 --- /dev/null +++ b/packages/opentelemetry-core/src/common/anchored-clock.ts @@ -0,0 +1,67 @@ +/* + * 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 interface Clock { + /** + * Return the current time in milliseconds from some epoch such as the Unix epoch or process start + */ + now(): number; +} + + +/** + * A utility for returning wall times anchored to a given point in time. Wall time measurements will + * not be taken from the system, but instead are computed by adding a monotonic clock time + * to the anchor point. + * + * This is needed because the system time can change and result in unexpected situations like + * spans ending before they are started. Creating an anchored clock for each local root span + * ensures that span timings and durations are accurate while preventing span times from drifting + * too far from the system clock. + * + * Only creating an anchored clock once per local trace ensures span times are correct relative + * to each other. For example, a child span will never have a start time before its parent even + * if the system clock is corrected during the local trace. + * + * Heavily inspired by the OTel Java anchored clock + * https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/AnchoredClock.java + */ +export class AnchoredClock implements Clock { + private _monotonicClock: Clock; + private _epochMillis: number; + private _performanceMillis: number; + + /** + * Create a new AnchoredClock anchored to the current time returned by systemClock. + * + * @param systemClock should be a clock that returns the number of milliseconds since January 1 1970 such as Date + * @param monotonicClock should be a clock that counts milliseconds monotonically such as window.performance or perf_hooks.performance + */ + public constructor(systemClock: Clock, monotonicClock: Clock) { + this._monotonicClock = monotonicClock; + this._epochMillis = systemClock.now(); + this._performanceMillis = monotonicClock.now(); + } + + /** + * Returns the current time by adding the number of milliseconds since the + * AnchoredClock was created to the creation epoch time + */ + public now(): number { + const delta = this._monotonicClock.now() - this._performanceMillis; + return this._epochMillis + delta; + } +} diff --git a/packages/opentelemetry-core/src/index.ts b/packages/opentelemetry-core/src/index.ts index 9316774752..6c0834fe0f 100644 --- a/packages/opentelemetry-core/src/index.ts +++ b/packages/opentelemetry-core/src/index.ts @@ -15,6 +15,7 @@ */ export * from './baggage/propagation/W3CBaggagePropagator'; +export * from './common/anchored-clock'; export * from './common/attributes'; export * from './common/global-error-handler'; export * from './common/logging-error-handler'; @@ -41,3 +42,7 @@ export * from './utils/url'; export * from './utils/wrap'; export * from './utils/callback'; export * from './version'; +import { _export } from './internal/exporter'; +export const internal = { + _export +}; diff --git a/packages/opentelemetry-core/src/internal/exporter.ts b/packages/opentelemetry-core/src/internal/exporter.ts new file mode 100644 index 0000000000..a489b35eac --- /dev/null +++ b/packages/opentelemetry-core/src/internal/exporter.ts @@ -0,0 +1,38 @@ +/* + * 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 { context } from '@opentelemetry/api'; +import { ExportResult } from '../ExportResult'; +import { suppressTracing } from '../trace/suppress-tracing'; + +export interface Exporter { + export(arg: T, resultCallback: (result: ExportResult) => void): void; +} + +/** +* @internal +* Shared functionality used by Exporters while exporting data, including suppresion of Traces. +*/ +export function _export(exporter: Exporter, arg: T): Promise { + return new Promise(resolve => { + // prevent downstream exporter calls from generating spans + context.with(suppressTracing(context.active()), () => { + exporter.export(arg, (result: ExportResult) => { + resolve(result); + }); + }); + }); +} diff --git a/packages/opentelemetry-core/src/platform/browser/globalThis.ts b/packages/opentelemetry-core/src/platform/browser/globalThis.ts index aa3c2fec6e..c438f2895f 100644 --- a/packages/opentelemetry-core/src/platform/browser/globalThis.ts +++ b/packages/opentelemetry-core/src/platform/browser/globalThis.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -// Updates to this file should also be replicated to @opentelemetry/api and -// @opentelemetry/api-metrics too. +// Updates to this file should also be replicated to @opentelemetry/api too. /** * - globalThis (New standard) diff --git a/packages/opentelemetry-core/src/utils/environment.ts b/packages/opentelemetry-core/src/utils/environment.ts index 05ba058f0f..fcb0614c61 100644 --- a/packages/opentelemetry-core/src/utils/environment.ts +++ b/packages/opentelemetry-core/src/utils/environment.ts @@ -102,7 +102,11 @@ export type ENVIRONMENT = { OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY?: string, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE?: string, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE?: string, - OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE?: string + OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE?: string, + OTEL_EXPORTER_OTLP_PROTOCOL?: string, + OTEL_EXPORTER_OTLP_TRACES_PROTOCOL?: string, + OTEL_EXPORTER_OTLP_METRICS_PROTOCOL?: string, + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE?: string } & ENVIRONMENT_NUMBERS & ENVIRONMENT_LISTS; @@ -154,7 +158,7 @@ export const DEFAULT_ENVIRONMENT: Required = { OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: DEFAULT_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT: 128, OTEL_SPAN_LINK_COUNT_LIMIT: 128, - OTEL_TRACES_EXPORTER: 'none', + OTEL_TRACES_EXPORTER: 'otlp', OTEL_TRACES_SAMPLER: TracesSamplerValues.ParentBasedAlwaysOn, OTEL_TRACES_SAMPLER_ARG: '', OTEL_EXPORTER_OTLP_INSECURE: '', @@ -171,7 +175,11 @@ export const DEFAULT_ENVIRONMENT: Required = { OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY: '', OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: '', OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE: '', - OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE: '' + OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE: '', + OTEL_EXPORTER_OTLP_PROTOCOL: 'http/protobuf', + OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: 'http/protobuf', + OTEL_EXPORTER_OTLP_METRICS_PROTOCOL: 'http/protobuf', + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: 'cumulative' }; /** diff --git a/packages/opentelemetry-core/test/common/anchored-clock.test.ts b/packages/opentelemetry-core/test/common/anchored-clock.test.ts new file mode 100644 index 0000000000..44f45be35c --- /dev/null +++ b/packages/opentelemetry-core/test/common/anchored-clock.test.ts @@ -0,0 +1,29 @@ +/* + * 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 assert = require('assert'); +import { AnchoredClock, otperformance } from '../../src'; + +describe('AnchoredClock', () => { + it('should keep time', done => { + const clock = new AnchoredClock(Date, otperformance); + setTimeout(() => { + // after about 100ms, the clocks are within 10ms of each other + assert.ok(Math.abs(Date.now() - clock.now()) < 10); + done(); + }, 100); + }); +}); diff --git a/packages/opentelemetry-core/test/internal/exporter.test.ts b/packages/opentelemetry-core/test/internal/exporter.test.ts new file mode 100644 index 0000000000..1ca256d659 --- /dev/null +++ b/packages/opentelemetry-core/test/internal/exporter.test.ts @@ -0,0 +1,43 @@ +/* + * 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 { ExportResult, ExportResultCode } from '../../src'; +import * as suppress from '../../src/trace/suppress-tracing'; +import { _export } from '../../src/internal/exporter'; + +describe('exporter', () => { + const sandbox = sinon.createSandbox(); + + afterEach(() => { + sandbox.restore(); + }); + + class TestExporter { + export(arg: any, resultCallback: (result: ExportResult) => void) { + resultCallback({ code: ExportResultCode.SUCCESS }); + } + } + + it('_export should suppress tracing', async () => { + const suppressSpy = sandbox.spy(suppress, 'suppressTracing'); + const exporter = new TestExporter(); + const result = await _export(exporter, ['Test1']); + assert.strictEqual(result.code, ExportResultCode.SUCCESS); + assert.ok(suppressSpy.calledOnce); + }); +}); diff --git a/packages/opentelemetry-core/tsconfig.json b/packages/opentelemetry-core/tsconfig.json index 0717beecbf..101baf4195 100644 --- a/packages/opentelemetry-core/tsconfig.json +++ b/packages/opentelemetry-core/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../api" + }, { "path": "../opentelemetry-semantic-conventions" } diff --git a/packages/opentelemetry-exporter-jaeger/package.json b/packages/opentelemetry-exporter-jaeger/package.json index 15064eba8f..38371fcf76 100644 --- a/packages/opentelemetry-exporter-jaeger/package.json +++ b/packages/opentelemetry-exporter-jaeger/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/exporter-jaeger", - "version": "1.6.0", + "version": "1.8.0", "description": "OpenTelemetry Exporter Jaeger allows user to send collected traces to Jaeger", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -45,8 +45,8 @@ }, "devDependencies": { "@opentelemetry/api": "^1.0.0", - "@opentelemetry/resources": "1.6.0", - "@types/mocha": "9.1.1", + "@opentelemetry/resources": "1.8.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "codecov": "3.8.3", @@ -62,10 +62,11 @@ "@opentelemetry/api": "^1.0.0" }, "dependencies": { - "@opentelemetry/core": "1.6.0", - "@opentelemetry/sdk-trace-base": "1.6.0", - "@opentelemetry/semantic-conventions": "1.6.0", + "@opentelemetry/core": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0", "jaeger-client": "^3.15.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-exporter-jaeger" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-exporter-jaeger", + "sideEffects": false } diff --git a/packages/opentelemetry-exporter-jaeger/tsconfig.json b/packages/opentelemetry-exporter-jaeger/tsconfig.json index 8b45d214a9..ce2661c050 100644 --- a/packages/opentelemetry-exporter-jaeger/tsconfig.json +++ b/packages/opentelemetry-exporter-jaeger/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../api" + }, { "path": "../opentelemetry-core" }, diff --git a/packages/opentelemetry-exporter-zipkin/package.json b/packages/opentelemetry-exporter-zipkin/package.json index ed04d165b1..7d56b47a1b 100644 --- a/packages/opentelemetry-exporter-zipkin/package.json +++ b/packages/opentelemetry-exporter-zipkin/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/exporter-zipkin", - "version": "1.6.0", + "version": "1.8.0", "description": "OpenTelemetry Zipkin Exporter allows the user to send collected traces to Zipkin.", "main": "build/src/index.js", "module": "build/esm/index.js", @@ -62,7 +62,7 @@ "devDependencies": { "@babel/core": "7.16.0", "@opentelemetry/api": "^1.0.0", - "@types/mocha": "9.1.1", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "@types/webpack-env": "1.16.3", @@ -91,10 +91,11 @@ "@opentelemetry/api": "^1.0.0" }, "dependencies": { - "@opentelemetry/core": "1.6.0", - "@opentelemetry/resources": "1.6.0", - "@opentelemetry/sdk-trace-base": "1.6.0", - "@opentelemetry/semantic-conventions": "1.6.0" + "@opentelemetry/core": "1.8.0", + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-exporter-zipkin" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-exporter-zipkin", + "sideEffects": false } diff --git a/packages/opentelemetry-exporter-zipkin/tsconfig.json b/packages/opentelemetry-exporter-zipkin/tsconfig.json index 8b45d214a9..ce2661c050 100644 --- a/packages/opentelemetry-exporter-zipkin/tsconfig.json +++ b/packages/opentelemetry-exporter-zipkin/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../api" + }, { "path": "../opentelemetry-core" }, diff --git a/packages/opentelemetry-propagator-b3/package.json b/packages/opentelemetry-propagator-b3/package.json index 6b78e271fe..dd2ab2756e 100644 --- a/packages/opentelemetry-propagator-b3/package.json +++ b/packages/opentelemetry-propagator-b3/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/propagator-b3", - "version": "1.6.0", + "version": "1.8.0", "description": "OpenTelemetry B3 propagator provides context propagation for systems that are using the B3 header format", "main": "build/src/index.js", "module": "build/esm/index.js", @@ -51,14 +51,14 @@ "access": "public" }, "dependencies": { - "@opentelemetry/core": "1.6.0" + "@opentelemetry/core": "1.8.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.4.0" }, "devDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0", - "@types/mocha": "9.1.1", + "@opentelemetry/api": ">=1.0.0 <1.4.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "codecov": "3.8.3", "istanbul-instrumenter-loader": "3.0.1", @@ -69,5 +69,6 @@ "ts-mocha": "10.0.0", "typescript": "4.4.4" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-propagator-b3" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-propagator-b3", + "sideEffects": false } diff --git a/packages/opentelemetry-propagator-b3/tsconfig.json b/packages/opentelemetry-propagator-b3/tsconfig.json index 789e61fc86..f60209f8c4 100644 --- a/packages/opentelemetry-propagator-b3/tsconfig.json +++ b/packages/opentelemetry-propagator-b3/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../api" + }, { "path": "../opentelemetry-core" } diff --git a/packages/opentelemetry-propagator-jaeger/package.json b/packages/opentelemetry-propagator-jaeger/package.json index 54b24fcf69..e44d3c77f7 100644 --- a/packages/opentelemetry-propagator-jaeger/package.json +++ b/packages/opentelemetry-propagator-jaeger/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/propagator-jaeger", - "version": "1.6.0", + "version": "1.8.0", "description": "OpenTelemetry Jaeger propagator provides HTTP header propagation for systems that are using Jaeger HTTP header format.", "main": "build/src/index.js", "module": "build/esm/index.js", @@ -54,8 +54,8 @@ "access": "public" }, "devDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0", - "@types/mocha": "9.1.1", + "@opentelemetry/api": ">=1.0.0 <1.4.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "@types/webpack-env": "1.16.3", @@ -77,10 +77,11 @@ "webpack": "4.46.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.4.0" }, "dependencies": { - "@opentelemetry/core": "1.6.0" + "@opentelemetry/core": "1.8.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-propagator-jaeger" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-propagator-jaeger", + "sideEffects": false } diff --git a/packages/opentelemetry-propagator-jaeger/tsconfig.json b/packages/opentelemetry-propagator-jaeger/tsconfig.json index a6c0bb2617..a942b2b6aa 100644 --- a/packages/opentelemetry-propagator-jaeger/tsconfig.json +++ b/packages/opentelemetry-propagator-jaeger/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../api" + }, { "path": "../opentelemetry-core" } diff --git a/packages/opentelemetry-resources/package.json b/packages/opentelemetry-resources/package.json index 0e4d276c30..a65084826c 100644 --- a/packages/opentelemetry-resources/package.json +++ b/packages/opentelemetry-resources/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/resources", - "version": "1.6.0", + "version": "1.8.0", "description": "OpenTelemetry SDK resources", "main": "build/src/index.js", "module": "build/esm/index.js", @@ -61,8 +61,8 @@ "access": "public" }, "devDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0", - "@types/mocha": "9.1.1", + "@opentelemetry/api": ">=1.0.0 <1.4.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "@types/webpack-env": "1.16.3", @@ -86,11 +86,12 @@ "webpack-merge": "5.8.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.4.0" }, "dependencies": { - "@opentelemetry/core": "1.6.0", - "@opentelemetry/semantic-conventions": "1.6.0" + "@opentelemetry/core": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-resources" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-resources", + "sideEffects": false } diff --git a/packages/opentelemetry-resources/src/detectors/EnvDetector.ts b/packages/opentelemetry-resources/src/detectors/EnvDetector.ts index 782f664a1a..b7a98ff4e1 100644 --- a/packages/opentelemetry-resources/src/detectors/EnvDetector.ts +++ b/packages/opentelemetry-resources/src/detectors/EnvDetector.ts @@ -110,14 +110,14 @@ class EnvDetector implements Detector { let [key, value] = keyValuePair; // Leading and trailing whitespaces are trimmed. key = key.trim(); - value = value.trim().split('^"|"$').join(''); + value = value.trim().split(/^"|"$/).join(''); if (!this._isValidAndNotEmpty(key)) { throw new Error(`Attribute key ${this._ERROR_MESSAGE_INVALID_CHARS}`); } if (!this._isValid(value)) { throw new Error(`Attribute value ${this._ERROR_MESSAGE_INVALID_VALUE}`); } - attributes[key] = value; + attributes[key] = decodeURIComponent(value); } return attributes; } @@ -130,13 +130,14 @@ class EnvDetector implements Detector { * @returns Whether the String is valid. */ private _isValid(name: string): boolean { - return name.length <= this._MAX_LENGTH && this._isPrintableString(name); + return name.length <= this._MAX_LENGTH && this._isBaggageOctetString(name); } - private _isPrintableString(str: string): boolean { + // https://www.w3.org/TR/baggage/#definition + private _isBaggageOctetString(str: string): boolean { for (let i = 0; i < str.length; i++) { - const ch: string = str.charAt(i); - if (ch <= ' ' || ch >= '~') { + const ch = str.charCodeAt(i); + if (ch < 0x21 || ch === 0x2C || ch === 0x3B || ch === 0x5C || ch > 0x7E) { return false; } } diff --git a/packages/opentelemetry-resources/test/detectors/browser/EnvDetector.test.ts b/packages/opentelemetry-resources/test/detectors/browser/EnvDetector.test.ts index 940b5b07c0..da4354f89e 100644 --- a/packages/opentelemetry-resources/test/detectors/browser/EnvDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/browser/EnvDetector.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import * as assert from 'assert'; import { RAW_ENVIRONMENT } from '@opentelemetry/core'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { envDetector, Resource } from '../../../src'; @@ -27,7 +28,7 @@ describeBrowser('envDetector() on web browser', () => { describe('with valid env', () => { before(() => { (globalThis as typeof globalThis & RAW_ENVIRONMENT).OTEL_RESOURCE_ATTRIBUTES = - 'webengine.name="chromium",webengine.version="99",webengine.description="Chromium"'; + 'webengine.name="chromium",webengine.version="99",webengine.description="Chromium",custom.key="custom%20value"'; }); after(() => { @@ -41,6 +42,38 @@ describeBrowser('envDetector() on web browser', () => { [SemanticResourceAttributes.WEBENGINE_VERSION]: '99', [SemanticResourceAttributes.WEBENGINE_DESCRIPTION]: 'Chromium', }); + assert.strictEqual(resource.attributes['custom.key'], 'custom value'); + }); + }); + + + describe('with invalid env', () => { + const values = [ + 'webengine.description="with spaces"', + ]; + + for (const value of values) { + describe(`value: '${value}'`, () => { + before(() => { + (globalThis as typeof globalThis & RAW_ENVIRONMENT).OTEL_RESOURCE_ATTRIBUTES = value; + }); + + after(() => { + delete (globalThis as typeof globalThis & RAW_ENVIRONMENT).OTEL_RESOURCE_ATTRIBUTES; + }); + + it('should return empty resource', async () => { + const resource: Resource = await envDetector.detect(); + assertEmptyResource(resource); + }); + }); + } + }); + + describe('with empty env', () => { + it('should return empty resource', async () => { + const resource: Resource = await envDetector.detect(); + assertEmptyResource(resource); }); }); diff --git a/packages/opentelemetry-resources/test/detectors/node/EnvDetector.test.ts b/packages/opentelemetry-resources/test/detectors/node/EnvDetector.test.ts index 2a2746e14b..3f6b63a1f1 100644 --- a/packages/opentelemetry-resources/test/detectors/node/EnvDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/node/EnvDetector.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { envDetector, Resource } from '../../../src'; import { assertK8sResource, @@ -26,7 +25,7 @@ describeNode('envDetector() on Node.js', () => { describe('with valid env', () => { before(() => { process.env.OTEL_RESOURCE_ATTRIBUTES = - 'k8s.pod.name="pod-xyz-123",k8s.cluster.name="c1",k8s.namespace.name="default"'; + 'k8s.pod.name="pod-xyz-123",k8s.cluster.name="c1",k8s.namespace.name="default",k8s.deployment.name="deployment%20name"'; }); after(() => { @@ -36,13 +35,37 @@ describeNode('envDetector() on Node.js', () => { it('should return resource information from environment variable', async () => { const resource: Resource = await envDetector.detect(); assertK8sResource(resource, { - [SemanticResourceAttributes.K8S_POD_NAME]: 'pod-xyz-123', - [SemanticResourceAttributes.K8S_CLUSTER_NAME]: 'c1', - [SemanticResourceAttributes.K8S_NAMESPACE_NAME]: 'default', + podName: 'pod-xyz-123', + clusterName: 'c1', + namespaceName: 'default', + deploymentName: 'deployment name' }); }); }); + describe('with invalid env', () => { + const values = [ + 'k8s.deployment.name="with spaces"', + ]; + + for (const value of values) { + describe(`value: '${value}'`, () => { + before(() => { + process.env.OTEL_RESOURCE_ATTRIBUTES = value; + }); + + after(() => { + delete process.env.OTEL_RESOURCE_ATTRIBUTES; + }); + + it('should return empty resource', async () => { + const resource: Resource = await envDetector.detect(); + assertEmptyResource(resource); + }); + }); + } + }); + describe('with empty env', () => { it('should return empty resource', async () => { const resource: Resource = await envDetector.detect(); diff --git a/packages/opentelemetry-resources/tsconfig.json b/packages/opentelemetry-resources/tsconfig.json index 51dcc3ed88..bbd7dbbe43 100644 --- a/packages/opentelemetry-resources/tsconfig.json +++ b/packages/opentelemetry-resources/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../api" + }, { "path": "../opentelemetry-core" }, diff --git a/packages/opentelemetry-sdk-trace-base/README.md b/packages/opentelemetry-sdk-trace-base/README.md index c4a40cc6f5..6d7b655894 100644 --- a/packages/opentelemetry-sdk-trace-base/README.md +++ b/packages/opentelemetry-sdk-trace-base/README.md @@ -57,10 +57,12 @@ Samples every trace regardless of upstream sampling decisions. > This is used as a default Sampler ```js -const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node"); -const { AlwaysOnSampler } = require("@opentelemetry/core"); +const { + AlwaysOnSampler, + BasicTracerProvider, +} = require("@opentelemetry/sdk-trace-base"); -const tracerProvider = new NodeTracerProvider({ +const tracerProvider = new BasicTracerProvider({ sampler: new AlwaysOnSampler() }); ``` @@ -70,10 +72,12 @@ const tracerProvider = new NodeTracerProvider({ Doesn't sample any trace, regardless of upstream sampling decisions. ```js -const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node"); -const { AlwaysOffSampler } = require("@opentelemetry/core"); +const { + AlwaysOffSampler, + BasicTracerProvider, +} = require("@opentelemetry/sdk-trace-base"); -const tracerProvider = new NodeTracerProvider({ +const tracerProvider = new BasicTracerProvider({ sampler: new AlwaysOffSampler() }); ``` @@ -86,10 +90,12 @@ Any trace that would be sampled at a given percentage will also be sampled at an The `TraceIDRatioSampler` may be used with the `ParentBasedSampler` to respect the sampled flag of an incoming trace. ```js -const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node"); -const { TraceIdRatioBasedSampler } = require("@opentelemetry/core"); +const { + BasicTracerProvider, + TraceIdRatioBasedSampler, +} = require("@opentelemetry/sdk-trace-base"); -const tracerProvider = new NodeTracerProvider({ +const tracerProvider = new BasicTracerProvider({ // See details of ParentBasedSampler below sampler: new ParentBasedSampler({ // Trace ID Ratio Sampler accepts a positional argument @@ -130,10 +136,14 @@ Optional parameters: |present|false|false|`localParentNotSampled()`| ```js -const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node"); -const { ParentBasedSampler, AlwaysOffSampler, TraceIdRatioBasedSampler } = require("@opentelemetry/core"); - -const tracerProvider = new NodeTracerProvider({ +const { + AlwaysOffSampler, + BasicTracerProvider, + ParentBasedSampler, + TraceIdRatioBasedSampler, +} = require("@opentelemetry/sdk-trace-base"); + +const tracerProvider = new BasicTracerProvider({ sampler: new ParentBasedSampler({ // By default, the ParentBasedSampler will respect the parent span's sampling // decision. This is configurable by providing a different sampler to use diff --git a/packages/opentelemetry-sdk-trace-base/package.json b/packages/opentelemetry-sdk-trace-base/package.json index 088904cc49..4d8d98cf3d 100644 --- a/packages/opentelemetry-sdk-trace-base/package.json +++ b/packages/opentelemetry-sdk-trace-base/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/sdk-trace-base", - "version": "1.6.0", + "version": "1.8.0", "description": "OpenTelemetry Tracing", "main": "build/src/index.js", "module": "build/esm/index.js", @@ -64,8 +64,8 @@ "access": "public" }, "devDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0", - "@types/mocha": "9.1.1", + "@opentelemetry/api": ">=1.0.0 <1.4.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "@types/webpack-env": "1.16.3", @@ -88,12 +88,13 @@ "webpack": "4.46.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.4.0" }, "dependencies": { - "@opentelemetry/core": "1.6.0", - "@opentelemetry/resources": "1.6.0", - "@opentelemetry/semantic-conventions": "1.6.0" + "@opentelemetry/core": "1.8.0", + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-base" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-base", + "sideEffects": false } diff --git a/packages/opentelemetry-sdk-trace-base/src/Span.ts b/packages/opentelemetry-sdk-trace-base/src/Span.ts index 00859d7d55..058507b7f7 100644 --- a/packages/opentelemetry-sdk-trace-base/src/Span.ts +++ b/packages/opentelemetry-sdk-trace-base/src/Span.ts @@ -15,24 +15,25 @@ */ import * as api from '@opentelemetry/api'; +import { Context, HrTime, SpanAttributeValue } from '@opentelemetry/api'; import { - isAttributeValue, - hrTime, + Clock, hrTimeDuration, InstrumentationLibrary, + isAttributeValue, isTimeInput, - timeInputToHrTime, + otperformance, sanitizeAttributes, + timeInputToHrTime } from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { ExceptionEventName } from './enums'; import { ReadableSpan } from './export/ReadableSpan'; +import { SpanProcessor } from './SpanProcessor'; import { TimedEvent } from './TimedEvent'; import { Tracer } from './Tracer'; -import { SpanProcessor } from './SpanProcessor'; import { SpanLimits } from './types'; -import { SpanAttributeValue, Context } from '@opentelemetry/api'; -import { ExceptionEventName } from './enums'; /** * This class represents a span. @@ -59,8 +60,13 @@ export class Span implements api.Span, ReadableSpan { private readonly _spanProcessor: SpanProcessor; private readonly _spanLimits: SpanLimits; private readonly _attributeValueLengthLimit: number; + private readonly _clock: Clock; - /** Constructs a new Span instance. */ + /** + * Constructs a new Span instance. + * + * @deprecated calling Span constructor directly is not supported. Please use tracer.startSpan. + * */ constructor( parentTracer: Tracer, context: Context, @@ -69,14 +75,16 @@ export class Span implements api.Span, ReadableSpan { kind: api.SpanKind, parentSpanId?: string, links: api.Link[] = [], - startTime: api.TimeInput = hrTime() + startTime?: api.TimeInput, + clock: Clock = otperformance, ) { + this._clock = clock; this.name = spanName; this._spanContext = spanContext; this.parentSpanId = parentSpanId; this.kind = kind; this.links = links; - this.startTime = timeInputToHrTime(startTime); + this.startTime = timeInputToHrTime(startTime ?? clock.now()); this.resource = parentTracer.resource; this.instrumentationLibrary = parentTracer.instrumentationLibrary; this._spanLimits = parentTracer.getSpanLimits(); @@ -103,7 +111,7 @@ export class Span implements api.Span, ReadableSpan { if ( Object.keys(this.attributes).length >= - this._spanLimits.attributeCountLimit! && + this._spanLimits.attributeCountLimit! && !Object.prototype.hasOwnProperty.call(this.attributes, key) ) { return this; @@ -147,7 +155,7 @@ export class Span implements api.Span, ReadableSpan { attributesOrStartTime = undefined; } if (typeof startTime === 'undefined') { - startTime = hrTime(); + startTime = this._clock.now(); } const attributes = sanitizeAttributes(attributesOrStartTime); @@ -171,21 +179,24 @@ export class Span implements api.Span, ReadableSpan { return this; } - end(endTime: api.TimeInput = hrTime()): void { + end(endTime?: api.TimeInput): void { if (this._isSpanEnded()) { api.diag.error('You can only call end() on a span once.'); return; } this._ended = true; - this.endTime = timeInputToHrTime(endTime); + this.endTime = timeInputToHrTime(endTime ?? this._clock.now()); this._duration = hrTimeDuration(this.startTime, this.endTime); + if (this._duration[0] < 0) { api.diag.warn( - 'Inconsistent start and end time, startTime > endTime', + 'Inconsistent start and end time, startTime > endTime. Setting span duration to 0ms.', this.startTime, this.endTime ); + this.endTime = this.startTime.slice() as HrTime; + this._duration = [0, 0]; } this._spanProcessor.onEnd(this); @@ -195,7 +206,7 @@ export class Span implements api.Span, ReadableSpan { return this._ended === false; } - recordException(exception: api.Exception, time: api.TimeInput = hrTime()): void { + recordException(exception: api.Exception, time: api.TimeInput = this._clock.now()): void { const attributes: api.SpanAttributes = {}; if (typeof exception === 'string') { attributes[SemanticAttributes.EXCEPTION_MESSAGE] = exception; diff --git a/packages/opentelemetry-sdk-trace-base/src/Tracer.ts b/packages/opentelemetry-sdk-trace-base/src/Tracer.ts index b77b5346b5..23d0f47895 100644 --- a/packages/opentelemetry-sdk-trace-base/src/Tracer.ts +++ b/packages/opentelemetry-sdk-trace-base/src/Tracer.ts @@ -67,17 +67,19 @@ export class Tracer implements api.Tracer { options: api.SpanOptions = {}, context = api.context.active() ): api.Span { - if (isTracingSuppressed(context)) { - api.diag.debug('Instrumentation suppressed, returning Noop Span'); - return api.trace.wrapSpanContext(api.INVALID_SPAN_CONTEXT); - } - // remove span from context in case a root span is requested via options if (options.root) { context = api.trace.deleteSpan(context); } + const parentSpan = api.trace.getSpan(context); + + if (isTracingSuppressed(context)) { + api.diag.debug('Instrumentation suppressed, returning Noop Span'); + const nonRecordingSpan = api.trace.wrapSpanContext(api.INVALID_SPAN_CONTEXT); + return nonRecordingSpan; + } - const parentSpanContext = api.trace.getSpanContext(context); + const parentSpanContext = parentSpan?.spanContext(); const spanId = this._idGenerator.generateSpanId(); let traceId; let traceState; @@ -117,7 +119,8 @@ export class Tracer implements api.Tracer { const spanContext = { traceId, spanId, traceFlags, traceState }; if (samplingResult.decision === api.SamplingDecision.NOT_RECORD) { api.diag.debug('Recording is off, propagating context in a non-recording span'); - return api.trace.wrapSpanContext(spanContext); + const nonRecordingSpan = api.trace.wrapSpanContext(spanContext); + return nonRecordingSpan; } const span = new Span( @@ -128,7 +131,7 @@ export class Tracer implements api.Tracer { spanKind, parentSpanId, links, - options.startTime + options.startTime, ); // Set initial span attributes. The attributes object may have been mutated // by the sampler, so we sanitize the merged attributes before setting them. diff --git a/packages/opentelemetry-sdk-trace-base/src/export/BatchSpanProcessorBase.ts b/packages/opentelemetry-sdk-trace-base/src/export/BatchSpanProcessorBase.ts index e6417d84af..8978fc7d79 100644 --- a/packages/opentelemetry-sdk-trace-base/src/export/BatchSpanProcessorBase.ts +++ b/packages/opentelemetry-sdk-trace-base/src/export/BatchSpanProcessorBase.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { context, Context, TraceFlags } from '@opentelemetry/api'; +import {context, Context, diag, TraceFlags} from '@opentelemetry/api'; import { BindOnceFuture, ExportResultCode, getEnv, globalErrorHandler, suppressTracing, - unrefTimer, + unrefTimer } from '@opentelemetry/core'; import { Span } from '../Span'; import { SpanProcessor } from '../SpanProcessor'; @@ -63,6 +63,11 @@ export abstract class BatchSpanProcessorBase implements : env.OTEL_BSP_EXPORT_TIMEOUT; this._shutdownOnce = new BindOnceFuture(this._shutdown, this); + + if (this._maxExportBatchSize > this._maxQueueSize) { + diag.warn('BatchSpanProcessor: maxExportBatchSize must be smaller or equal to maxQueueSize, setting maxExportBatchSize to match maxQueueSize'); + this._maxExportBatchSize = this._maxQueueSize; + } } forceFlush(): Promise { diff --git a/packages/opentelemetry-sdk-trace-base/src/export/SimpleSpanProcessor.ts b/packages/opentelemetry-sdk-trace-base/src/export/SimpleSpanProcessor.ts index c775bdf6d4..a510ad02a6 100644 --- a/packages/opentelemetry-sdk-trace-base/src/export/SimpleSpanProcessor.ts +++ b/packages/opentelemetry-sdk-trace-base/src/export/SimpleSpanProcessor.ts @@ -14,12 +14,13 @@ * limitations under the License. */ -import { context, Context, TraceFlags } from '@opentelemetry/api'; +import { Context, TraceFlags } from '@opentelemetry/api'; import { + internal, ExportResultCode, globalErrorHandler, - suppressTracing, BindOnceFuture, + ExportResult } from '@opentelemetry/core'; import { Span } from '../Span'; import { SpanProcessor } from '../SpanProcessor'; @@ -45,7 +46,7 @@ export class SimpleSpanProcessor implements SpanProcessor { } // does nothing. - onStart(_span: Span, _parentContext: Context): void {} + onStart(_span: Span, _parentContext: Context): void { } onEnd(span: ReadableSpan): void { if (this._shutdownOnce.isCalled) { @@ -56,18 +57,17 @@ export class SimpleSpanProcessor implements SpanProcessor { return; } - // prevent downstream exporter calls from generating spans - context.with(suppressTracing(context.active()), () => { - this._exporter.export([span], result => { - if (result.code !== ExportResultCode.SUCCESS) { - globalErrorHandler( - result.error ?? - new Error( - `SimpleSpanProcessor: span export failed (status ${result})` - ) - ); - } - }); + internal._export(this._exporter, [span]).then((result: ExportResult) => { + if (result.code !== ExportResultCode.SUCCESS) { + globalErrorHandler( + result.error ?? + new Error( + `SimpleSpanProcessor: span export failed (status ${result})` + ) + ); + } + }).catch(error => { + globalErrorHandler(error); }); } diff --git a/packages/opentelemetry-sdk-trace-base/test/common/Span.test.ts b/packages/opentelemetry-sdk-trace-base/test/common/Span.test.ts index 4573efbbf4..792c1f88df 100644 --- a/packages/opentelemetry-sdk-trace-base/test/common/Span.test.ts +++ b/packages/opentelemetry-sdk-trace-base/test/common/Span.test.ts @@ -128,6 +128,18 @@ describe('Span', () => { assert.ok(hrTimeToNanoseconds(span.duration) >= 0); }); + it('should ensure duration is never negative even if provided with inconsistent times', () => { + const span = new Span( + tracer, + ROOT_CONTEXT, + name, + spanContext, + SpanKind.SERVER + ); + span.end(hrTimeToMilliseconds(span.startTime) - 1); + assert.ok(hrTimeToNanoseconds(span.duration) >= 0); + }); + it('should have valid event.time', () => { const span = new Span( tracer, diff --git a/packages/opentelemetry-sdk-trace-base/test/common/export/BatchSpanProcessorBase.test.ts b/packages/opentelemetry-sdk-trace-base/test/common/export/BatchSpanProcessorBase.test.ts index 8d8de40d32..229e2bdb3c 100644 --- a/packages/opentelemetry-sdk-trace-base/test/common/export/BatchSpanProcessorBase.test.ts +++ b/packages/opentelemetry-sdk-trace-base/test/common/export/BatchSpanProcessorBase.test.ts @@ -18,7 +18,7 @@ import { diag, ROOT_CONTEXT } from '@opentelemetry/api'; import { ExportResultCode, loggingErrorHandler, - setGlobalErrorHandler, + setGlobalErrorHandler } from '@opentelemetry/core'; import * as assert from 'assert'; import * as sinon from 'sinon'; @@ -435,4 +435,21 @@ describe('BatchSpanProcessorBase', () => { }); }); }); + + describe('maxExportBatchSize', () => { + let processor: BatchSpanProcessor; + + describe('when "maxExportBatchSize" is greater than "maxQueueSize"', () => { + beforeEach(() => { + processor = new BatchSpanProcessor( + exporter,{ + maxExportBatchSize: 7, + maxQueueSize: 6, + }); + }); + it('should match maxQueueSize', () => { + assert.equal(processor['_maxExportBatchSize'], processor['_maxQueueSize']); + }); + }); + }); }); diff --git a/packages/opentelemetry-sdk-trace-base/tsconfig.json b/packages/opentelemetry-sdk-trace-base/tsconfig.json index b16c7037b4..fa14714abd 100644 --- a/packages/opentelemetry-sdk-trace-base/tsconfig.json +++ b/packages/opentelemetry-sdk-trace-base/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../api" + }, { "path": "../opentelemetry-core" }, diff --git a/packages/opentelemetry-sdk-trace-node/README.md b/packages/opentelemetry-sdk-trace-node/README.md index 417a380405..c54d11e04a 100644 --- a/packages/opentelemetry-sdk-trace-node/README.md +++ b/packages/opentelemetry-sdk-trace-node/README.md @@ -117,6 +117,6 @@ Apache 2.0 - See [LICENSE][license-url] for more information. [discussions-url]: https://github.com/open-telemetry/opentelemetry-js/discussions [license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/main/LICENSE [license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat -[def-context]: https://github.com/open-telemetry/opentelemetry-js-api/blob/main/docs/context.md +[def-context]: https://github.com/open-telemetry/opentelemetry-js/blob/main/doc/context.md [npm-url]: https://www.npmjs.com/package/@opentelemetry/sdk-trace-node [npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fsdk-trace-node.svg diff --git a/packages/opentelemetry-sdk-trace-node/package.json b/packages/opentelemetry-sdk-trace-node/package.json index 051fad0f9d..4889fab422 100644 --- a/packages/opentelemetry-sdk-trace-node/package.json +++ b/packages/opentelemetry-sdk-trace-node/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/sdk-trace-node", - "version": "1.6.0", + "version": "1.8.0", "description": "OpenTelemetry Node SDK provides automatic telemetry (tracing, metrics, etc) for Node.js applications", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -45,10 +45,10 @@ "access": "public" }, "devDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0", - "@opentelemetry/resources": "1.6.0", - "@opentelemetry/semantic-conventions": "1.6.0", - "@types/mocha": "9.1.1", + "@opentelemetry/api": ">=1.0.0 <1.4.0", + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/semver": "7.3.9", "@types/sinon": "10.0.13", @@ -61,15 +61,16 @@ "typescript": "4.4.4" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.4.0" }, "dependencies": { - "@opentelemetry/context-async-hooks": "1.6.0", - "@opentelemetry/core": "1.6.0", - "@opentelemetry/propagator-b3": "1.6.0", - "@opentelemetry/propagator-jaeger": "1.6.0", - "@opentelemetry/sdk-trace-base": "1.6.0", + "@opentelemetry/context-async-hooks": "1.8.0", + "@opentelemetry/core": "1.8.0", + "@opentelemetry/propagator-b3": "1.8.0", + "@opentelemetry/propagator-jaeger": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0", "semver": "^7.3.5" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-node" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-node", + "sideEffects": false } diff --git a/packages/opentelemetry-sdk-trace-node/src/index.ts b/packages/opentelemetry-sdk-trace-node/src/index.ts index 44e347c09e..8f6c5b9311 100644 --- a/packages/opentelemetry-sdk-trace-node/src/index.ts +++ b/packages/opentelemetry-sdk-trace-node/src/index.ts @@ -16,3 +16,4 @@ export { NodeTracerConfig } from './config'; export * from './NodeTracerProvider'; +export * from '@opentelemetry/sdk-trace-base'; diff --git a/packages/opentelemetry-sdk-trace-node/tsconfig.json b/packages/opentelemetry-sdk-trace-node/tsconfig.json index 5894551b3d..837d89d421 100644 --- a/packages/opentelemetry-sdk-trace-node/tsconfig.json +++ b/packages/opentelemetry-sdk-trace-node/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../api" + }, { "path": "../opentelemetry-context-async-hooks" }, diff --git a/packages/opentelemetry-sdk-trace-web/README.md b/packages/opentelemetry-sdk-trace-web/README.md index cd7e448e67..8eccf7fc13 100644 --- a/packages/opentelemetry-sdk-trace-web/README.md +++ b/packages/opentelemetry-sdk-trace-web/README.md @@ -31,8 +31,11 @@ npm install --save @opentelemetry/sdk-trace-web ## Usage ```js -import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'; +import { + ConsoleSpanExporter, + SimpleSpanProcessor, + WebTracerProvider, +} from '@opentelemetry/sdk-trace-web'; import { DocumentLoad } from '@opentelemetry/plugin-document-load'; import { ZoneContextManager } from '@opentelemetry/context-zone'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; diff --git a/packages/opentelemetry-sdk-trace-web/package.json b/packages/opentelemetry-sdk-trace-web/package.json index ade633dcf7..70f3c21256 100644 --- a/packages/opentelemetry-sdk-trace-web/package.json +++ b/packages/opentelemetry-sdk-trace-web/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/sdk-trace-web", - "version": "1.6.0", + "version": "1.8.0", "description": "OpenTelemetry Web Tracer", "main": "build/src/index.js", "module": "build/esm/index.js", @@ -56,12 +56,12 @@ }, "devDependencies": { "@babel/core": "7.16.0", - "@opentelemetry/api": ">=1.0.0 <1.3.0", - "@opentelemetry/context-zone": "1.6.0", - "@opentelemetry/propagator-b3": "1.6.0", - "@opentelemetry/resources": "1.6.0", + "@opentelemetry/api": ">=1.0.0 <1.4.0", + "@opentelemetry/context-zone": "1.8.0", + "@opentelemetry/propagator-b3": "1.8.0", + "@opentelemetry/resources": "1.8.0", "@types/jquery": "3.5.8", - "@types/mocha": "9.1.1", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "@types/webpack-env": "1.16.3", @@ -88,12 +88,13 @@ "webpack-merge": "5.8.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.4.0" }, "dependencies": { - "@opentelemetry/core": "1.6.0", - "@opentelemetry/sdk-trace-base": "1.6.0", - "@opentelemetry/semantic-conventions": "1.6.0" + "@opentelemetry/core": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-web" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-web", + "sideEffects": false } diff --git a/packages/opentelemetry-sdk-trace-web/src/index.ts b/packages/opentelemetry-sdk-trace-web/src/index.ts index a506be00ce..d7bfeaf382 100644 --- a/packages/opentelemetry-sdk-trace-web/src/index.ts +++ b/packages/opentelemetry-sdk-trace-web/src/index.ts @@ -19,3 +19,4 @@ export * from './StackContextManager'; export * from './enums/PerformanceTimingNames'; export * from './types'; export * from './utils'; +export * from '@opentelemetry/sdk-trace-base'; diff --git a/packages/opentelemetry-sdk-trace-web/tsconfig.json b/packages/opentelemetry-sdk-trace-web/tsconfig.json index f317cb38d1..09ac19a176 100644 --- a/packages/opentelemetry-sdk-trace-web/tsconfig.json +++ b/packages/opentelemetry-sdk-trace-web/tsconfig.json @@ -10,6 +10,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../api" + }, { "path": "../opentelemetry-context-zone" }, diff --git a/packages/opentelemetry-semantic-conventions/package.json b/packages/opentelemetry-semantic-conventions/package.json index 8d4953452a..a801f2b02e 100644 --- a/packages/opentelemetry-semantic-conventions/package.json +++ b/packages/opentelemetry-semantic-conventions/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/semantic-conventions", - "version": "1.6.0", + "version": "1.8.0", "description": "OpenTelemetry semantic conventions", "main": "build/src/index.js", "module": "build/esm/index.js", @@ -49,7 +49,7 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "9.1.1", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "codecov": "3.8.3", @@ -61,5 +61,6 @@ "ts-mocha": "10.0.0", "typescript": "4.4.4" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-semantic-conventions" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-semantic-conventions", + "sideEffects": false } diff --git a/packages/opentelemetry-shim-opentracing/package.json b/packages/opentelemetry-shim-opentracing/package.json index 81ac9c3a66..f60f9e9e35 100644 --- a/packages/opentelemetry-shim-opentracing/package.json +++ b/packages/opentelemetry-shim-opentracing/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/shim-opentracing", - "version": "1.6.0", + "version": "1.8.0", "description": "OpenTracing to OpenTelemetry shim", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -42,11 +42,11 @@ "access": "public" }, "devDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0", - "@opentelemetry/propagator-b3": "1.6.0", - "@opentelemetry/propagator-jaeger": "1.6.0", - "@opentelemetry/sdk-trace-base": "1.6.0", - "@types/mocha": "9.1.1", + "@opentelemetry/api": ">=1.0.0 <1.4.0", + "@opentelemetry/propagator-b3": "1.8.0", + "@opentelemetry/propagator-jaeger": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "codecov": "3.8.3", "mocha": "10.0.0", @@ -56,12 +56,13 @@ "typescript": "4.4.4" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.4.0" }, "dependencies": { - "@opentelemetry/core": "1.6.0", - "@opentelemetry/semantic-conventions": "1.6.0", + "@opentelemetry/core": "1.8.0", + "@opentelemetry/semantic-conventions": "1.8.0", "opentracing": "^0.14.4" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-shim-opentracing" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-shim-opentracing", + "sideEffects": false } diff --git a/packages/opentelemetry-shim-opentracing/tsconfig.json b/packages/opentelemetry-shim-opentracing/tsconfig.json index a256765295..0e01f63184 100644 --- a/packages/opentelemetry-shim-opentracing/tsconfig.json +++ b/packages/opentelemetry-shim-opentracing/tsconfig.json @@ -9,6 +9,9 @@ "test/**/*.ts" ], "references": [ + { + "path": "../../api" + }, { "path": "../opentelemetry-core" }, diff --git a/packages/sdk-metrics/.eslintignore b/packages/sdk-metrics/.eslintignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/packages/sdk-metrics/.eslintignore @@ -0,0 +1 @@ +build diff --git a/experimental/packages/opentelemetry-sdk-metrics/.eslintrc.js b/packages/sdk-metrics/.eslintrc.js similarity index 65% rename from experimental/packages/opentelemetry-sdk-metrics/.eslintrc.js rename to packages/sdk-metrics/.eslintrc.js index f756f4488b..f726f3becb 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/.eslintrc.js +++ b/packages/sdk-metrics/.eslintrc.js @@ -3,5 +3,5 @@ module.exports = { "mocha": true, "node": true }, - ...require('../../../eslint.config.js') + ...require('../../eslint.config.js') } diff --git a/packages/sdk-metrics/.npmignore b/packages/sdk-metrics/.npmignore new file mode 100644 index 0000000000..9505ba9450 --- /dev/null +++ b/packages/sdk-metrics/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/packages/sdk-metrics/LICENSE b/packages/sdk-metrics/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/packages/sdk-metrics/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/experimental/packages/opentelemetry-sdk-metrics/README.md b/packages/sdk-metrics/README.md similarity index 97% rename from experimental/packages/opentelemetry-sdk-metrics/README.md rename to packages/sdk-metrics/README.md index 39b4f5f3c7..78bd3c000d 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/README.md +++ b/packages/sdk-metrics/README.md @@ -12,7 +12,7 @@ It does **not** provide automated instrumentation of known libraries or host env ## Installation ```bash -npm install --save @opentelemetry/api-metrics +npm install --save @opentelemetry/api npm install --save @opentelemetry/sdk-metrics ``` @@ -21,7 +21,7 @@ npm install --save @opentelemetry/sdk-metrics The basic setup of the SDK can be seen as followings: ```js -const opentelemetry = require('@opentelemetry/api-metrics'); +const opentelemetry = require('@opentelemetry/api'); const { MeterProvider } = require('@opentelemetry/sdk-metrics'); // To create an instrument, you first need to initialize the Meter provider. diff --git a/packages/sdk-metrics/karma.conf.js b/packages/sdk-metrics/karma.conf.js new file mode 100644 index 0000000000..3019564a15 --- /dev/null +++ b/packages/sdk-metrics/karma.conf.js @@ -0,0 +1,24 @@ +/*! + * 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 + * + * 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. + */ + +const karmaWebpackConfig = require('../../karma.webpack'); +const karmaBaseConfig = require('../../karma.base'); + +module.exports = (config) => { + config.set(Object.assign({}, karmaBaseConfig, { + webpack: karmaWebpackConfig + })) +}; diff --git a/experimental/packages/opentelemetry-sdk-metrics/package.json b/packages/sdk-metrics/package.json similarity index 78% rename from experimental/packages/opentelemetry-sdk-metrics/package.json rename to packages/sdk-metrics/package.json index 3d8cbb9a23..f799b62b78 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/package.json +++ b/packages/sdk-metrics/package.json @@ -1,7 +1,7 @@ { "name": "@opentelemetry/sdk-metrics", - "version": "0.32.0", - "description": "Work in progress OpenTelemetry metrics SDK", + "version": "1.8.0", + "description": "OpenTelemetry metrics SDK", "main": "build/src/index.js", "module": "build/esm/index.js", "esnext": "build/esnext/index.js", @@ -15,14 +15,14 @@ "test:browser": "nyc karma start --single-run", "tdd": "npm run test -- --watch-extensions ts --watch", "tdd:browser": "karma start", - "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../../", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", "lint": "eslint . --ext .ts", "lint:fix": "eslint . --ext .ts --fix", - "version": "node ../../../scripts/version-update.js", + "version": "node ../../scripts/version-update.js", "watch": "tsc --build --watch tsconfig.all.json", "precompile": "lerna run version --scope $(npm pkg get name) --include-dependencies", - "prewatch": "node ../../../scripts/version-update.js", - "peer-api-check": "node ../../../scripts/peer-api-check.js" + "prewatch": "node ../../scripts/version-update.js", + "peer-api-check": "node ../../scripts/peer-api-check.js" }, "keywords": [ "opentelemetry", @@ -54,9 +54,9 @@ "access": "public" }, "devDependencies": { - "@opentelemetry/api": "^1.0.0", + "@opentelemetry/api": ">=1.3.0 <1.4.0", "@types/lodash.merge": "4.6.6", - "@types/mocha": "9.1.1", + "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "codecov": "3.8.3", @@ -74,13 +74,13 @@ "typescript": "4.4.4" }, "peerDependencies": { - "@opentelemetry/api": "^1.0.0" + "@opentelemetry/api": ">=1.3.0 <1.4.0" }, "dependencies": { - "@opentelemetry/api-metrics": "0.32.0", - "@opentelemetry/core": "1.6.0", - "@opentelemetry/resources": "1.6.0", + "@opentelemetry/core": "1.8.0", + "@opentelemetry/resources": "1.8.0", "lodash.merge": "4.6.2" }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-sdk-metrics" + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/sdk-metrics", + "sideEffects": false } diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/InstrumentDescriptor.ts b/packages/sdk-metrics/src/InstrumentDescriptor.ts similarity index 96% rename from experimental/packages/opentelemetry-sdk-metrics/src/InstrumentDescriptor.ts rename to packages/sdk-metrics/src/InstrumentDescriptor.ts index e511cd8dd3..4008bc3de2 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/InstrumentDescriptor.ts +++ b/packages/sdk-metrics/src/InstrumentDescriptor.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { MetricOptions, ValueType } from '@opentelemetry/api-metrics'; +import { MetricOptions, ValueType } from '@opentelemetry/api'; import { View } from './view/View'; /** diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/Instruments.ts b/packages/sdk-metrics/src/Instruments.ts similarity index 67% rename from experimental/packages/opentelemetry-sdk-metrics/src/Instruments.ts rename to packages/sdk-metrics/src/Instruments.ts index 5706059691..8b70c06f70 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/Instruments.ts +++ b/packages/sdk-metrics/src/Instruments.ts @@ -14,9 +14,21 @@ * limitations under the License. */ -import * as api from '@opentelemetry/api'; -import * as metrics from '@opentelemetry/api-metrics'; -import { ObservableCallback } from '@opentelemetry/api-metrics'; +import { + context as contextApi, + diag, + Context, + MetricAttributes, + ValueType, + UpDownCounter, + Counter, + Histogram, + Observable, + ObservableCallback, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, +} from '@opentelemetry/api'; import { hrTime } from '@opentelemetry/core'; import { InstrumentDescriptor } from './InstrumentDescriptor'; import { ObservableRegistry } from './state/ObservableRegistry'; @@ -25,9 +37,9 @@ import { AsyncWritableMetricStorage, WritableMetricStorage } from './state/Writa export class SyncInstrument { constructor(private _writableMetricStorage: WritableMetricStorage, protected _descriptor: InstrumentDescriptor) {} - protected _record(value: number, attributes: metrics.MetricAttributes = {}, context: api.Context = api.context.active()) { - if (this._descriptor.valueType === metrics.ValueType.INT && !Number.isInteger(value)) { - api.diag.warn( + protected _record(value: number, attributes: MetricAttributes = {}, context: Context = contextApi.active()) { + if (this._descriptor.valueType === ValueType.INT && !Number.isInteger(value)) { + diag.warn( `INT value type cannot accept a floating-point value for ${this._descriptor.name}, ignoring the fractional digits.` ); value = Math.trunc(value); @@ -37,27 +49,27 @@ export class SyncInstrument { } /** - * The class implements {@link metrics.UpDownCounter} interface. + * The class implements {@link UpDownCounter} interface. */ -export class UpDownCounterInstrument extends SyncInstrument implements metrics.UpDownCounter { +export class UpDownCounterInstrument extends SyncInstrument implements UpDownCounter { /** * Increment value of counter by the input. Inputs may be negative. */ - add(value: number, attributes?: metrics.MetricAttributes, ctx?: api.Context): void { + add(value: number, attributes?: MetricAttributes, ctx?: Context): void { this._record(value, attributes, ctx); } } /** - * The class implements {@link metrics.Counter} interface. + * The class implements {@link Counter} interface. */ -export class CounterInstrument extends SyncInstrument implements metrics.Counter { +export class CounterInstrument extends SyncInstrument implements Counter { /** * Increment value of counter by the input. Inputs may not be negative. */ - add(value: number, attributes?: metrics.MetricAttributes, ctx?: api.Context): void { + add(value: number, attributes?: MetricAttributes, ctx?: Context): void { if (value < 0) { - api.diag.warn(`negative value provided to counter ${this._descriptor.name}: ${value}`); + diag.warn(`negative value provided to counter ${this._descriptor.name}: ${value}`); return; } @@ -66,22 +78,22 @@ export class CounterInstrument extends SyncInstrument implements metrics.Counter } /** - * The class implements {@link metrics.Histogram} interface. + * The class implements {@link Histogram} interface. */ -export class HistogramInstrument extends SyncInstrument implements metrics.Histogram { +export class HistogramInstrument extends SyncInstrument implements Histogram { /** * Records a measurement. Value of the measurement must not be negative. */ - record(value: number, attributes?: metrics.MetricAttributes, ctx?: api.Context): void { + record(value: number, attributes?: MetricAttributes, ctx?: Context): void { if (value < 0) { - api.diag.warn(`negative value provided to histogram ${this._descriptor.name}: ${value}`); + diag.warn(`negative value provided to histogram ${this._descriptor.name}: ${value}`); return; } this._record(value, attributes, ctx); } } -export class ObservableInstrument implements metrics.Observable { +export class ObservableInstrument implements Observable { /** @internal */ _metricStorages: AsyncWritableMetricStorage[]; /** @internal */ @@ -93,23 +105,23 @@ export class ObservableInstrument implements metrics.Observable { } /** - * @see {metrics.Observable.addCallback} + * @see {Observable.addCallback} */ addCallback(callback: ObservableCallback) { this._observableRegistry.addCallback(callback, this); } /** - * @see {metrics.Observable.removeCallback} + * @see {Observable.removeCallback} */ removeCallback(callback: ObservableCallback) { this._observableRegistry.removeCallback(callback, this); } } -export class ObservableCounterInstrument extends ObservableInstrument implements metrics.ObservableCounter {} -export class ObservableGaugeInstrument extends ObservableInstrument implements metrics.ObservableGauge {} -export class ObservableUpDownCounterInstrument extends ObservableInstrument implements metrics.ObservableUpDownCounter {} +export class ObservableCounterInstrument extends ObservableInstrument implements ObservableCounter {} +export class ObservableGaugeInstrument extends ObservableInstrument implements ObservableGauge {} +export class ObservableUpDownCounterInstrument extends ObservableInstrument implements ObservableUpDownCounter {} export function isObservableInstrument(it: unknown): it is ObservableInstrument { return it instanceof ObservableInstrument; diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/Meter.ts b/packages/sdk-metrics/src/Meter.ts similarity index 69% rename from experimental/packages/opentelemetry-sdk-metrics/src/Meter.ts rename to packages/sdk-metrics/src/Meter.ts index d8a7f00a21..bd58dcbac1 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/Meter.ts +++ b/packages/sdk-metrics/src/Meter.ts @@ -14,7 +14,18 @@ * limitations under the License. */ -import * as metrics from '@opentelemetry/api-metrics'; +import { + Meter as IMeter, + MetricOptions, + Histogram, + Counter, + UpDownCounter, + ObservableGauge, + ObservableCounter, + ObservableUpDownCounter, + BatchObservableCallback, + Observable, +} from '@opentelemetry/api'; import { createInstrumentDescriptor, InstrumentType } from './InstrumentDescriptor'; import { CounterInstrument, @@ -27,85 +38,85 @@ import { import { MeterSharedState } from './state/MeterSharedState'; /** - * This class implements the {@link metrics.Meter} interface. + * This class implements the {@link IMeter} interface. */ -export class Meter implements metrics.Meter { +export class Meter implements IMeter { constructor(private _meterSharedState: MeterSharedState) {} /** - * Create a {@link metrics.Histogram} instrument. + * Create a {@link Histogram} instrument. */ - createHistogram(name: string, options?: metrics.MetricOptions): metrics.Histogram { + createHistogram(name: string, options?: MetricOptions): Histogram { const descriptor = createInstrumentDescriptor(name, InstrumentType.HISTOGRAM, options); const storage = this._meterSharedState.registerMetricStorage(descriptor); return new HistogramInstrument(storage, descriptor); } /** - * Create a {@link metrics.Counter} instrument. + * Create a {@link Counter} instrument. */ - createCounter(name: string, options?: metrics.MetricOptions): metrics.Counter { + createCounter(name: string, options?: MetricOptions): Counter { const descriptor = createInstrumentDescriptor(name, InstrumentType.COUNTER, options); const storage = this._meterSharedState.registerMetricStorage(descriptor); return new CounterInstrument(storage, descriptor); } /** - * Create a {@link metrics.UpDownCounter} instrument. + * Create a {@link UpDownCounter} instrument. */ - createUpDownCounter(name: string, options?: metrics.MetricOptions): metrics.UpDownCounter { + createUpDownCounter(name: string, options?: MetricOptions): UpDownCounter { const descriptor = createInstrumentDescriptor(name, InstrumentType.UP_DOWN_COUNTER, options); const storage = this._meterSharedState.registerMetricStorage(descriptor); return new UpDownCounterInstrument(storage, descriptor); } /** - * Create a {@link metrics.ObservableGauge} instrument. + * Create a {@link ObservableGauge} instrument. */ createObservableGauge( name: string, - options?: metrics.MetricOptions, - ): metrics.ObservableGauge { + options?: MetricOptions, + ): ObservableGauge { const descriptor = createInstrumentDescriptor(name, InstrumentType.OBSERVABLE_GAUGE, options); const storages = this._meterSharedState.registerAsyncMetricStorage(descriptor); return new ObservableGaugeInstrument(descriptor, storages, this._meterSharedState.observableRegistry); } /** - * Create a {@link metrics.ObservableCounter} instrument. + * Create a {@link ObservableCounter} instrument. */ createObservableCounter( name: string, - options?: metrics.MetricOptions, - ): metrics.ObservableCounter { + options?: MetricOptions, + ): ObservableCounter { const descriptor = createInstrumentDescriptor(name, InstrumentType.OBSERVABLE_COUNTER, options); const storages = this._meterSharedState.registerAsyncMetricStorage(descriptor); return new ObservableCounterInstrument(descriptor, storages, this._meterSharedState.observableRegistry); } /** - * Create a {@link metrics.ObservableUpDownCounter} instrument. + * Create a {@link ObservableUpDownCounter} instrument. */ createObservableUpDownCounter( name: string, - options?: metrics.MetricOptions, - ): metrics.ObservableUpDownCounter { + options?: MetricOptions, + ): ObservableUpDownCounter { const descriptor = createInstrumentDescriptor(name, InstrumentType.OBSERVABLE_UP_DOWN_COUNTER, options); const storages = this._meterSharedState.registerAsyncMetricStorage(descriptor); return new ObservableUpDownCounterInstrument(descriptor, storages, this._meterSharedState.observableRegistry); } /** - * @see {@link metrics.Meter.addBatchObservableCallback} + * @see {@link Meter.addBatchObservableCallback} */ - addBatchObservableCallback(callback: metrics.BatchObservableCallback, observables: metrics.Observable[]) { + addBatchObservableCallback(callback: BatchObservableCallback, observables: Observable[]) { this._meterSharedState.observableRegistry.addBatchCallback(callback, observables); } /** - * @see {@link metrics.Meter.removeBatchObservableCallback} + * @see {@link Meter.removeBatchObservableCallback} */ - removeBatchObservableCallback(callback: metrics.BatchObservableCallback, observables: metrics.Observable[]) { + removeBatchObservableCallback(callback: BatchObservableCallback, observables: Observable[]) { this._meterSharedState.observableRegistry.removeBatchCallback(callback, observables); } } diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/MeterProvider.ts b/packages/sdk-metrics/src/MeterProvider.ts similarity index 81% rename from experimental/packages/opentelemetry-sdk-metrics/src/MeterProvider.ts rename to packages/sdk-metrics/src/MeterProvider.ts index 003bb41a6b..13b17fb564 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/MeterProvider.ts +++ b/packages/sdk-metrics/src/MeterProvider.ts @@ -14,8 +14,13 @@ * limitations under the License. */ -import * as api from '@opentelemetry/api'; -import * as metrics from '@opentelemetry/api-metrics'; +import { + diag, + MeterProvider as IMeterProvider, + Meter as IMeter, + MeterOptions, + createNoopMeter, +} from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; import { MetricReader } from './export/MetricReader'; import { MeterProviderSharedState } from './state/MeterProviderSharedState'; @@ -33,14 +38,15 @@ export interface MeterProviderOptions { } /** - * This class implements the {@link metrics.MeterProvider} interface. + * This class implements the {@link MeterProvider} interface. */ -export class MeterProvider implements metrics.MeterProvider { +export class MeterProvider implements IMeterProvider { private _sharedState: MeterProviderSharedState; private _shutdown = false; constructor(options?: MeterProviderOptions) { - this._sharedState = new MeterProviderSharedState(options?.resource ?? Resource.empty()); + const resource = Resource.default().merge(options?.resource ?? Resource.empty()); + this._sharedState = new MeterProviderSharedState(resource); if(options?.views != null && options.views.length > 0){ for(const view of options.views){ this._sharedState.viewRegistry.addView(view); @@ -51,11 +57,11 @@ export class MeterProvider implements metrics.MeterProvider { /** * Get a meter with the configuration of the MeterProvider. */ - getMeter(name: string, version = '', options: metrics.MeterOptions = {}): metrics.Meter { + getMeter(name: string, version = '', options: MeterOptions = {}): IMeter { // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#meter-creation if (this._shutdown) { - api.diag.warn('A shutdown MeterProvider cannot provide a Meter'); - return metrics.NOOP_METER; + diag.warn('A shutdown MeterProvider cannot provide a Meter'); + return createNoopMeter(); } return this._sharedState @@ -83,7 +89,7 @@ export class MeterProvider implements metrics.MeterProvider { */ async shutdown(options?: ShutdownOptions): Promise { if (this._shutdown) { - api.diag.warn('shutdown may only be called once per MeterProvider'); + diag.warn('shutdown may only be called once per MeterProvider'); return; } @@ -102,7 +108,7 @@ export class MeterProvider implements metrics.MeterProvider { async forceFlush(options?: ForceFlushOptions): Promise { // do not flush after shutdown if (this._shutdown) { - api.diag.warn('invalid attempt to force flush after MeterProvider shutdown'); + diag.warn('invalid attempt to force flush after MeterProvider shutdown'); return; } diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/ObservableResult.ts b/packages/sdk-metrics/src/ObservableResult.ts similarity index 69% rename from experimental/packages/opentelemetry-sdk-metrics/src/ObservableResult.ts rename to packages/sdk-metrics/src/ObservableResult.ts index 19ba32846c..e3a46f1c4d 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/ObservableResult.ts +++ b/packages/sdk-metrics/src/ObservableResult.ts @@ -14,16 +14,22 @@ * limitations under the License. */ -import * as api from '@opentelemetry/api'; -import * as metrics from '@opentelemetry/api-metrics'; +import { + diag, + ObservableResult, + MetricAttributes, + ValueType, + BatchObservableResult, + Observable, +} from '@opentelemetry/api'; import { AttributeHashMap } from './state/HashMap'; import { isObservableInstrument, ObservableInstrument } from './Instruments'; import { InstrumentDescriptor } from '.'; /** - * The class implements {@link metrics.ObservableResult} interface. + * The class implements {@link ObservableResult} interface. */ -export class ObservableResultImpl implements metrics.ObservableResult { +export class ObservableResultImpl implements ObservableResult { /** * @internal */ @@ -34,9 +40,9 @@ export class ObservableResultImpl implements metrics.ObservableResult { /** * Observe a measurement of the value associated with the given attributes. */ - observe(value: number, attributes: metrics.MetricAttributes = {}): void { - if (this._descriptor.valueType === metrics.ValueType.INT && !Number.isInteger(value)) { - api.diag.warn( + observe(value: number, attributes: MetricAttributes = {}): void { + if (this._descriptor.valueType === ValueType.INT && !Number.isInteger(value)) { + diag.warn( `INT value type cannot accept a floating-point value for ${this._descriptor.name}, ignoring the fractional digits.` ); value = Math.trunc(value); @@ -46,9 +52,9 @@ export class ObservableResultImpl implements metrics.ObservableResult { } /** - * The class implements {@link metrics.BatchObservableCallback} interface. + * The class implements {@link BatchObservableCallback} interface. */ -export class BatchObservableResultImpl implements metrics.BatchObservableResult { +export class BatchObservableResultImpl implements BatchObservableResult { /** * @internal */ @@ -57,7 +63,7 @@ export class BatchObservableResultImpl implements metrics.BatchObservableResult /** * Observe a measurement of the value associated with the given attributes. */ - observe(metric: metrics.Observable, value: number, attributes: metrics.MetricAttributes = {}): void { + observe(metric: Observable, value: number, attributes: MetricAttributes = {}): void { if (!isObservableInstrument(metric)) { return; } @@ -66,8 +72,8 @@ export class BatchObservableResultImpl implements metrics.BatchObservableResult map = new AttributeHashMap(); this._buffer.set(metric, map); } - if (metric._descriptor.valueType === metrics.ValueType.INT && !Number.isInteger(value)) { - api.diag.warn( + if (metric._descriptor.valueType === ValueType.INT && !Number.isInteger(value)) { + diag.warn( `INT value type cannot accept a floating-point value for ${metric._descriptor.name}, ignoring the fractional digits.` ); value = Math.trunc(value); diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/aggregator/Drop.ts b/packages/sdk-metrics/src/aggregator/Drop.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/src/aggregator/Drop.ts rename to packages/sdk-metrics/src/aggregator/Drop.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/aggregator/Histogram.ts b/packages/sdk-metrics/src/aggregator/Histogram.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/src/aggregator/Histogram.ts rename to packages/sdk-metrics/src/aggregator/Histogram.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/aggregator/LastValue.ts b/packages/sdk-metrics/src/aggregator/LastValue.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/src/aggregator/LastValue.ts rename to packages/sdk-metrics/src/aggregator/LastValue.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/aggregator/Sum.ts b/packages/sdk-metrics/src/aggregator/Sum.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/src/aggregator/Sum.ts rename to packages/sdk-metrics/src/aggregator/Sum.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/aggregator/index.ts b/packages/sdk-metrics/src/aggregator/index.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/src/aggregator/index.ts rename to packages/sdk-metrics/src/aggregator/index.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/aggregator/types.ts b/packages/sdk-metrics/src/aggregator/types.ts similarity index 96% rename from experimental/packages/opentelemetry-sdk-metrics/src/aggregator/types.ts rename to packages/sdk-metrics/src/aggregator/types.ts index 8f4f2e1acc..cc6a2a4976 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/aggregator/types.ts +++ b/packages/sdk-metrics/src/aggregator/types.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { HrTime } from '@opentelemetry/api'; -import { MetricAttributes } from '@opentelemetry/api-metrics'; +import { HrTime, MetricAttributes } from '@opentelemetry/api'; import { AggregationTemporality } from '../export/AggregationTemporality'; import { MetricData } from '../export/MetricData'; import { InstrumentDescriptor } from '../InstrumentDescriptor'; @@ -111,6 +110,7 @@ export interface Aggregator { * Returns the {@link MetricData} that this {@link Aggregator} will produce. * * @param descriptor the metric instrument descriptor. + * @param aggregationTemporality the temporality of the resulting {@link MetricData} * @param accumulationByAttributes the array of attributes and accumulation pairs. * @param endTime the end time of the metric data. * @return the {@link MetricData} that this {@link Aggregator} will produce. diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/exemplar/AlignedHistogramBucketExemplarReservoir.ts b/packages/sdk-metrics/src/exemplar/AlignedHistogramBucketExemplarReservoir.ts similarity index 93% rename from experimental/packages/opentelemetry-sdk-metrics/src/exemplar/AlignedHistogramBucketExemplarReservoir.ts rename to packages/sdk-metrics/src/exemplar/AlignedHistogramBucketExemplarReservoir.ts index 96060d16ae..0148ead72b 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/exemplar/AlignedHistogramBucketExemplarReservoir.ts +++ b/packages/sdk-metrics/src/exemplar/AlignedHistogramBucketExemplarReservoir.ts @@ -15,8 +15,7 @@ */ -import { Context, HrTime } from '@opentelemetry/api'; -import { MetricAttributes } from '@opentelemetry/api-metrics'; +import { Context, HrTime, MetricAttributes } from '@opentelemetry/api'; import { FixedSizeExemplarReservoirBase } from './ExemplarReservoir'; diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/exemplar/AlwaysSampleExemplarFilter.ts b/packages/sdk-metrics/src/exemplar/AlwaysSampleExemplarFilter.ts similarity index 88% rename from experimental/packages/opentelemetry-sdk-metrics/src/exemplar/AlwaysSampleExemplarFilter.ts rename to packages/sdk-metrics/src/exemplar/AlwaysSampleExemplarFilter.ts index d2c52cdc0e..01c4a5dbfb 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/exemplar/AlwaysSampleExemplarFilter.ts +++ b/packages/sdk-metrics/src/exemplar/AlwaysSampleExemplarFilter.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { MetricAttributes } from '@opentelemetry/api-metrics'; -import { Context, HrTime } from '@opentelemetry/api'; +import { Context, HrTime, MetricAttributes } from '@opentelemetry/api'; import { ExemplarFilter } from './ExemplarFilter'; diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/exemplar/Exemplar.ts b/packages/sdk-metrics/src/exemplar/Exemplar.ts similarity index 93% rename from experimental/packages/opentelemetry-sdk-metrics/src/exemplar/Exemplar.ts rename to packages/sdk-metrics/src/exemplar/Exemplar.ts index 28628ff04f..d98246dc49 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/exemplar/Exemplar.ts +++ b/packages/sdk-metrics/src/exemplar/Exemplar.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { HrTime } from '@opentelemetry/api'; -import { MetricAttributes } from '@opentelemetry/api-metrics'; +import { HrTime, MetricAttributes } from '@opentelemetry/api'; /** * A representation of an exemplar, which is a sample input measurement. diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/exemplar/ExemplarFilter.ts b/packages/sdk-metrics/src/exemplar/ExemplarFilter.ts similarity index 88% rename from experimental/packages/opentelemetry-sdk-metrics/src/exemplar/ExemplarFilter.ts rename to packages/sdk-metrics/src/exemplar/ExemplarFilter.ts index 956025f253..78b8ca0f39 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/exemplar/ExemplarFilter.ts +++ b/packages/sdk-metrics/src/exemplar/ExemplarFilter.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { MetricAttributes } from '@opentelemetry/api-metrics'; -import { Context, HrTime } from '@opentelemetry/api'; +import { Context, HrTime, MetricAttributes } from '@opentelemetry/api'; /** * This interface represents a ExemplarFilter. Exemplar filters are @@ -29,7 +28,7 @@ export interface ExemplarFilter { * @param value The value of the measurement * @param timestamp A timestamp that best represents when the measurement was taken * @param attributes The complete set of MetricAttributes of the measurement - * @param context The Context of the measurement + * @param ctx The Context of the measurement */ shouldSample( value: number, diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/exemplar/ExemplarReservoir.ts b/packages/sdk-metrics/src/exemplar/ExemplarReservoir.ts similarity index 96% rename from experimental/packages/opentelemetry-sdk-metrics/src/exemplar/ExemplarReservoir.ts rename to packages/sdk-metrics/src/exemplar/ExemplarReservoir.ts index 190976502e..fb2e0b8adb 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/exemplar/ExemplarReservoir.ts +++ b/packages/sdk-metrics/src/exemplar/ExemplarReservoir.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { MetricAttributes } from '@opentelemetry/api-metrics'; -import { Context, HrTime, isSpanContextValid, trace } from '@opentelemetry/api'; +import { Context, HrTime, isSpanContextValid, trace, MetricAttributes } from '@opentelemetry/api'; import { Exemplar } from './Exemplar'; diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/exemplar/NeverSampleExemplarFilter.ts b/packages/sdk-metrics/src/exemplar/NeverSampleExemplarFilter.ts similarity index 88% rename from experimental/packages/opentelemetry-sdk-metrics/src/exemplar/NeverSampleExemplarFilter.ts rename to packages/sdk-metrics/src/exemplar/NeverSampleExemplarFilter.ts index 374f35e52f..c51adda410 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/exemplar/NeverSampleExemplarFilter.ts +++ b/packages/sdk-metrics/src/exemplar/NeverSampleExemplarFilter.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { MetricAttributes } from '@opentelemetry/api-metrics'; -import { Context, HrTime } from '@opentelemetry/api'; +import { Context, HrTime, MetricAttributes } from '@opentelemetry/api'; import { ExemplarFilter } from './ExemplarFilter'; export class NeverSampleExemplarFilter implements ExemplarFilter { diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/exemplar/SimpleFixedSizeExemplarReservoir.ts b/packages/sdk-metrics/src/exemplar/SimpleFixedSizeExemplarReservoir.ts similarity index 93% rename from experimental/packages/opentelemetry-sdk-metrics/src/exemplar/SimpleFixedSizeExemplarReservoir.ts rename to packages/sdk-metrics/src/exemplar/SimpleFixedSizeExemplarReservoir.ts index 31b0369d87..892f0605dc 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/exemplar/SimpleFixedSizeExemplarReservoir.ts +++ b/packages/sdk-metrics/src/exemplar/SimpleFixedSizeExemplarReservoir.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { Context, HrTime } from '@opentelemetry/api'; -import { MetricAttributes } from '@opentelemetry/api-metrics'; +import { Context, HrTime, MetricAttributes } from '@opentelemetry/api'; import { FixedSizeExemplarReservoirBase } from './ExemplarReservoir'; /** diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/exemplar/WithTraceExemplarFilter.ts b/packages/sdk-metrics/src/exemplar/WithTraceExemplarFilter.ts similarity index 87% rename from experimental/packages/opentelemetry-sdk-metrics/src/exemplar/WithTraceExemplarFilter.ts rename to packages/sdk-metrics/src/exemplar/WithTraceExemplarFilter.ts index 1ca0439c0d..6251a36a05 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/exemplar/WithTraceExemplarFilter.ts +++ b/packages/sdk-metrics/src/exemplar/WithTraceExemplarFilter.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { MetricAttributes } from '@opentelemetry/api-metrics'; -import { Context, HrTime, isSpanContextValid, trace, TraceFlags } from '@opentelemetry/api'; +import { Context, HrTime, isSpanContextValid, trace, TraceFlags, MetricAttributes } from '@opentelemetry/api'; import { ExemplarFilter } from './ExemplarFilter'; export class WithTraceExemplarFilter implements ExemplarFilter { diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/exemplar/index.ts b/packages/sdk-metrics/src/exemplar/index.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/src/exemplar/index.ts rename to packages/sdk-metrics/src/exemplar/index.ts diff --git a/packages/sdk-metrics/src/export/AggregationSelector.ts b/packages/sdk-metrics/src/export/AggregationSelector.ts new file mode 100644 index 0000000000..b0ef5f36ad --- /dev/null +++ b/packages/sdk-metrics/src/export/AggregationSelector.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 { InstrumentType } from '../InstrumentDescriptor'; +import { Aggregation } from '../view/Aggregation'; +import { AggregationTemporality } from './AggregationTemporality'; + +/** + * Aggregation selector based on metric instrument types. + */ +export type AggregationSelector = (instrumentType: InstrumentType) => Aggregation; + +/** + * Aggregation temporality selector based on metric instrument types. + */ +export type AggregationTemporalitySelector = (instrumentType: InstrumentType) => AggregationTemporality; + +export const DEFAULT_AGGREGATION_SELECTOR: AggregationSelector = _instrumentType => Aggregation.Default(); +export const DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR: AggregationTemporalitySelector = _instrumentType => AggregationTemporality.CUMULATIVE; diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/export/AggregationTemporality.ts b/packages/sdk-metrics/src/export/AggregationTemporality.ts similarity index 82% rename from experimental/packages/opentelemetry-sdk-metrics/src/export/AggregationTemporality.ts rename to packages/sdk-metrics/src/export/AggregationTemporality.ts index 0b93671472..6cc6d1231b 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/export/AggregationTemporality.ts +++ b/packages/sdk-metrics/src/export/AggregationTemporality.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { InstrumentType } from '../InstrumentDescriptor'; - /** * AggregationTemporality indicates the way additive quantities are expressed. */ @@ -23,5 +21,3 @@ export enum AggregationTemporality { DELTA, CUMULATIVE, } - -export type AggregationTemporalitySelector = (instrumentType: InstrumentType) => AggregationTemporality; diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/export/ConsoleMetricExporter.ts b/packages/sdk-metrics/src/export/ConsoleMetricExporter.ts similarity index 77% rename from experimental/packages/opentelemetry-sdk-metrics/src/export/ConsoleMetricExporter.ts rename to packages/sdk-metrics/src/export/ConsoleMetricExporter.ts index 39b268f193..0b990dfe5e 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/export/ConsoleMetricExporter.ts +++ b/packages/sdk-metrics/src/export/ConsoleMetricExporter.ts @@ -13,15 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ExportResult, ExportResultCode } from '@opentelemetry/core'; +import { + ExportResult, + ExportResultCode +} from '@opentelemetry/core'; import { InstrumentType } from '../InstrumentDescriptor'; import { AggregationTemporality } from './AggregationTemporality'; import { ResourceMetrics } from './MetricData'; import { PushMetricExporter } from './MetricExporter'; +import { + AggregationTemporalitySelector, + DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR, +} from './AggregationSelector'; + +interface ConsoleMetricExporterOptions { + temporalitySelector?: AggregationTemporalitySelector +} /* eslint-disable no-console */ export class ConsoleMetricExporter implements PushMetricExporter { protected _shutdown = false; + protected _temporalitySelector: AggregationTemporalitySelector; + + constructor(options?: ConsoleMetricExporterOptions) { + this._temporalitySelector = options?.temporalitySelector ?? DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR; + } export(metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void): void { if (this._shutdown) { @@ -38,7 +54,7 @@ export class ConsoleMetricExporter implements PushMetricExporter { } selectAggregationTemporality(_instrumentType: InstrumentType): AggregationTemporality { - return AggregationTemporality.CUMULATIVE; + return this._temporalitySelector(_instrumentType); } shutdown(): Promise { diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/export/InMemoryMetricExporter.ts b/packages/sdk-metrics/src/export/InMemoryMetricExporter.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/src/export/InMemoryMetricExporter.ts rename to packages/sdk-metrics/src/export/InMemoryMetricExporter.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/export/MetricData.ts b/packages/sdk-metrics/src/export/MetricData.ts similarity index 96% rename from experimental/packages/opentelemetry-sdk-metrics/src/export/MetricData.ts rename to packages/sdk-metrics/src/export/MetricData.ts index 36e031e2d3..b15da1d9b9 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/export/MetricData.ts +++ b/packages/sdk-metrics/src/export/MetricData.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { HrTime } from '@opentelemetry/api'; -import { MetricAttributes } from '@opentelemetry/api-metrics'; +import { HrTime, MetricAttributes } from '@opentelemetry/api'; import { InstrumentationScope } from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; import { InstrumentDescriptor } from '../InstrumentDescriptor'; @@ -25,7 +24,7 @@ import { Histogram } from '../aggregator/types'; /** * Basic metric data fields. */ -export interface BaseMetricData { +interface BaseMetricData { readonly descriptor: InstrumentDescriptor; readonly aggregationTemporality: AggregationTemporality; /** diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/export/MetricExporter.ts b/packages/sdk-metrics/src/export/MetricExporter.ts similarity index 82% rename from experimental/packages/opentelemetry-sdk-metrics/src/export/MetricExporter.ts rename to packages/sdk-metrics/src/export/MetricExporter.ts index 782a39ccb8..1421ddb71f 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/export/MetricExporter.ts +++ b/packages/sdk-metrics/src/export/MetricExporter.ts @@ -20,6 +20,7 @@ import { ExportResult, } from '@opentelemetry/core'; import { InstrumentType } from '../InstrumentDescriptor'; +import { Aggregation } from '../view/Aggregation'; /** * An interface that allows different metric services to export recorded data @@ -31,6 +32,7 @@ export interface PushMetricExporter { /** * Called to export sampled {@link ResourceMetrics}. * @param metrics the metric data to be exported. + * @param resultCallback callback for when the export has completed */ export(metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void): void; @@ -44,7 +46,13 @@ export interface PushMetricExporter { * Select the {@link AggregationTemporality} for the given * {@link InstrumentType} for this exporter. */ - selectAggregationTemporality(instrumentType: InstrumentType): AggregationTemporality; + selectAggregationTemporality?(instrumentType: InstrumentType): AggregationTemporality; + + /** + * Select the {@link Aggregation} for the given + * {@link InstrumentType} for this exporter. + */ + selectAggregation?(instrumentType: InstrumentType): Aggregation; /** * Returns a promise which resolves when the last exportation is completed. diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/export/MetricProducer.ts b/packages/sdk-metrics/src/export/MetricProducer.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/src/export/MetricProducer.ts rename to packages/sdk-metrics/src/export/MetricProducer.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/export/MetricReader.ts b/packages/sdk-metrics/src/export/MetricReader.ts similarity index 73% rename from experimental/packages/opentelemetry-sdk-metrics/src/export/MetricReader.ts rename to packages/sdk-metrics/src/export/MetricReader.ts index 35d86c5d94..acf22d46f0 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/export/MetricReader.ts +++ b/packages/sdk-metrics/src/export/MetricReader.ts @@ -20,7 +20,32 @@ import { MetricProducer } from './MetricProducer'; import { CollectionResult } from './MetricData'; import { callWithTimeout } from '../utils'; import { InstrumentType } from '../InstrumentDescriptor'; -import { CollectionOptions, ForceFlushOptions, ShutdownOptions } from '../types'; +import { + CollectionOptions, + ForceFlushOptions, + ShutdownOptions +} from '../types'; +import { Aggregation } from '../view/Aggregation'; +import { + AggregationSelector, + AggregationTemporalitySelector, + DEFAULT_AGGREGATION_SELECTOR, + DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR +} from './AggregationSelector'; + +export interface MetricReaderOptions { + /** + * Aggregation selector based on metric instrument types. If no views are + * configured for a metric instrument, a per-metric-reader aggregation is + * selected with this selector. + */ + aggregationSelector?: AggregationSelector; + /** + * Aggregation temporality selector based on metric instrument types. If + * not configured, cumulative is used for all instruments. + */ + aggregationTemporalitySelector?: AggregationTemporalitySelector; +} /** * A registered reader of metrics that, when linked to a {@link MetricProducer}, offers global @@ -32,6 +57,15 @@ export abstract class MetricReader { private _shutdown = false; // MetricProducer used by this instance. private _metricProducer?: MetricProducer; + private readonly _aggregationTemporalitySelector: AggregationTemporalitySelector; + private readonly _aggregationSelector: AggregationSelector; + + constructor(options?: MetricReaderOptions) { + this._aggregationSelector = options?.aggregationSelector ?? + DEFAULT_AGGREGATION_SELECTOR; + this._aggregationTemporalitySelector = options?.aggregationTemporalitySelector ?? + DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR; + } /** * Set the {@link MetricProducer} used by this instance. @@ -46,11 +80,21 @@ export abstract class MetricReader { this.onInitialized(); } + /** + * Select the {@link Aggregation} for the given {@link InstrumentType} for this + * reader. + */ + selectAggregation(instrumentType: InstrumentType): Aggregation { + return this._aggregationSelector(instrumentType); + } + /** * Select the {@link AggregationTemporality} for the given * {@link InstrumentType} for this reader. */ - abstract selectAggregationTemporality(instrumentType: InstrumentType): AggregationTemporality; + selectAggregationTemporality(instrumentType: InstrumentType): AggregationTemporality { + return this._aggregationTemporalitySelector(instrumentType); + } /** * Handle once the SDK has initialized this {@link MetricReader} diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/export/PeriodicExportingMetricReader.ts b/packages/sdk-metrics/src/export/PeriodicExportingMetricReader.ts similarity index 77% rename from experimental/packages/opentelemetry-sdk-metrics/src/export/PeriodicExportingMetricReader.ts rename to packages/sdk-metrics/src/export/PeriodicExportingMetricReader.ts index 3e188c0293..2a686181cc 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/export/PeriodicExportingMetricReader.ts +++ b/packages/sdk-metrics/src/export/PeriodicExportingMetricReader.ts @@ -16,37 +16,49 @@ import * as api from '@opentelemetry/api'; import { + internal, ExportResultCode, globalErrorHandler, unrefTimer } from '@opentelemetry/core'; import { MetricReader } from './MetricReader'; -import { AggregationTemporality } from './AggregationTemporality'; -import { InstrumentType } from '../InstrumentDescriptor'; import { PushMetricExporter } from './MetricExporter'; -import { callWithTimeout, TimeoutError } from '../utils'; +import { + callWithTimeout, + TimeoutError +} from '../utils'; export type PeriodicExportingMetricReaderOptions = { - exporter: PushMetricExporter - exportIntervalMillis?: number, - exportTimeoutMillis?: number + /** + * The backing exporter for the metric reader. + */ + exporter: PushMetricExporter; + /** + * An internal milliseconds for the metric reader to initiate metric + * collection. + */ + exportIntervalMillis?: number; + /** + * Milliseconds for the async observable callback to timeout. + */ + exportTimeoutMillis?: number; }; /** * {@link MetricReader} which collects metrics based on a user-configurable time interval, and passes the metrics to - * the configured {@link MetricExporter} + * the configured {@link PushMetricExporter} */ export class PeriodicExportingMetricReader extends MetricReader { private _interval?: ReturnType; - private _exporter: PushMetricExporter; - private readonly _exportInterval: number; - private readonly _exportTimeout: number; constructor(options: PeriodicExportingMetricReaderOptions) { - super(); + super({ + aggregationSelector: options.exporter.selectAggregation?.bind(options.exporter), + aggregationTemporalitySelector: options.exporter.selectAggregationTemporality?.bind(options.exporter) + }); if (options.exportIntervalMillis !== undefined && options.exportIntervalMillis <= 0) { throw Error('exportIntervalMillis must be greater than 0'); @@ -74,20 +86,12 @@ export class PeriodicExportingMetricReader extends MetricReader { api.diag.error('PeriodicExportingMetricReader: metrics collection errors', ...errors); } - return new Promise((resolve, reject) => { - this._exporter.export(resourceMetrics, result => { - if (result.code !== ExportResultCode.SUCCESS) { - reject( - result.error ?? - new Error( - `PeriodicExportingMetricReader: metrics export failed (error ${result.error})` - ) - ); - } else { - resolve(); - } - }); - }); + const result = await internal._export(this._exporter, resourceMetrics); + if (result.code !== ExportResultCode.SUCCESS) { + throw new Error( + `PeriodicExportingMetricReader: metrics export failed (error ${result.error})` + ); + } } protected override onInitialized(): void { @@ -118,11 +122,4 @@ export class PeriodicExportingMetricReader extends MetricReader { await this._exporter.shutdown(); } - - /** - * @inheritdoc - */ - selectAggregationTemporality(instrumentType: InstrumentType): AggregationTemporality { - return this._exporter.selectAggregationTemporality(instrumentType); - } } diff --git a/packages/sdk-metrics/src/index.ts b/packages/sdk-metrics/src/index.ts new file mode 100644 index 0000000000..0d82d420a5 --- /dev/null +++ b/packages/sdk-metrics/src/index.ts @@ -0,0 +1,93 @@ +/* + * 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 { + Sum, + LastValue, + Histogram, +} from './aggregator/types'; + +export { + AggregationSelector, + AggregationTemporalitySelector, +} from './export/AggregationSelector'; + +export { + AggregationTemporality, +} from './export/AggregationTemporality'; + +export { + DataPoint, + DataPointType, + SumMetricData, + GaugeMetricData, + HistogramMetricData, + ResourceMetrics, + ScopeMetrics, + MetricData, + CollectionResult, +} from './export/MetricData'; + +export { + PushMetricExporter, +} from './export/MetricExporter'; + +export { + MetricReader, + MetricReaderOptions +} from './export/MetricReader'; + +export { + PeriodicExportingMetricReader, + PeriodicExportingMetricReaderOptions, +} from './export/PeriodicExportingMetricReader'; + +export { + InMemoryMetricExporter, +} from './export/InMemoryMetricExporter'; + +export { + ConsoleMetricExporter, +} from './export/ConsoleMetricExporter'; + +export { + InstrumentDescriptor, + InstrumentType, +} from './InstrumentDescriptor'; + +export { + MeterProvider, + MeterProviderOptions, +} from './MeterProvider'; + +export { + DefaultAggregation, + ExplicitBucketHistogramAggregation, + DropAggregation, + HistogramAggregation, + LastValueAggregation, + SumAggregation, + Aggregation +} from './view/Aggregation'; + +export { + View, + ViewOptions, +} from './view/View'; + +export { + TimeoutError +} from './utils'; diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/state/AsyncMetricStorage.ts b/packages/sdk-metrics/src/state/AsyncMetricStorage.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/src/state/AsyncMetricStorage.ts rename to packages/sdk-metrics/src/state/AsyncMetricStorage.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/state/DeltaMetricProcessor.ts b/packages/sdk-metrics/src/state/DeltaMetricProcessor.ts similarity index 95% rename from experimental/packages/opentelemetry-sdk-metrics/src/state/DeltaMetricProcessor.ts rename to packages/sdk-metrics/src/state/DeltaMetricProcessor.ts index 22c25edab0..999db3c282 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/state/DeltaMetricProcessor.ts +++ b/packages/sdk-metrics/src/state/DeltaMetricProcessor.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { Context, HrTime } from '@opentelemetry/api'; -import { MetricAttributes } from '@opentelemetry/api-metrics'; +import { Context, HrTime, MetricAttributes } from '@opentelemetry/api'; import { Maybe } from '../utils'; import { Accumulation, Aggregator } from '../aggregator/types'; import { AttributeHashMap } from './HashMap'; diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/state/HashMap.ts b/packages/sdk-metrics/src/state/HashMap.ts similarity index 97% rename from experimental/packages/opentelemetry-sdk-metrics/src/state/HashMap.ts rename to packages/sdk-metrics/src/state/HashMap.ts index 0f8efd3794..681d4dedee 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/state/HashMap.ts +++ b/packages/sdk-metrics/src/state/HashMap.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { MetricAttributes } from '@opentelemetry/api-metrics'; +import { MetricAttributes } from '@opentelemetry/api'; import { hashAttributes } from '../utils'; export interface Hash { diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/state/MeterProviderSharedState.ts b/packages/sdk-metrics/src/state/MeterProviderSharedState.ts similarity index 79% rename from experimental/packages/opentelemetry-sdk-metrics/src/state/MeterProviderSharedState.ts rename to packages/sdk-metrics/src/state/MeterProviderSharedState.ts index 87fe540ee5..a63f53d51d 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/state/MeterProviderSharedState.ts +++ b/packages/sdk-metrics/src/state/MeterProviderSharedState.ts @@ -16,10 +16,11 @@ import { InstrumentationScope } from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; +import { Aggregation, InstrumentType } from '..'; import { instrumentationScopeId } from '../utils'; import { ViewRegistry } from '../view/ViewRegistry'; import { MeterSharedState } from './MeterSharedState'; -import { MetricCollector } from './MetricCollector'; +import { MetricCollector, MetricCollectorHandle } from './MetricCollector'; /** * An internal record for shared meter provider states. @@ -42,4 +43,12 @@ export class MeterProviderSharedState { } return meterSharedState; } + + selectAggregations(instrumentType: InstrumentType) { + const result: [MetricCollectorHandle, Aggregation][] = []; + for (const collector of this.metricCollectors) { + result.push([collector, collector.selectAggregation(instrumentType)]); + } + return result; + } } diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/state/MeterSharedState.ts b/packages/sdk-metrics/src/state/MeterSharedState.ts similarity index 58% rename from experimental/packages/opentelemetry-sdk-metrics/src/state/MeterSharedState.ts rename to packages/sdk-metrics/src/state/MeterSharedState.ts index ed96aca444..65ea6edde7 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/state/MeterSharedState.ts +++ b/packages/sdk-metrics/src/state/MeterSharedState.ts @@ -20,7 +20,7 @@ import { MetricCollectOptions } from '../export/MetricProducer'; import { ScopeMetrics } from '../export/MetricData'; import { createInstrumentDescriptorWithView, InstrumentDescriptor } from '../InstrumentDescriptor'; import { Meter } from '../Meter'; -import { isNotNullish } from '../utils'; +import { isNotNullish, Maybe } from '../utils'; import { AsyncMetricStorage } from './AsyncMetricStorage'; import { MeterProviderSharedState } from './MeterProviderSharedState'; import { MetricCollectorHandle } from './MetricCollector'; @@ -28,12 +28,15 @@ import { MetricStorageRegistry } from './MetricStorageRegistry'; import { MultiMetricStorage } from './MultiWritableMetricStorage'; import { ObservableRegistry } from './ObservableRegistry'; import { SyncMetricStorage } from './SyncMetricStorage'; +import { Accumulation, Aggregator } from '../aggregator/types'; +import { AttributesProcessor } from '../view/AttributesProcessor'; +import { MetricStorage } from './MetricStorage'; /** * An internal record for shared meter provider states. */ export class MeterSharedState { - private _metricStorageRegistry = new MetricStorageRegistry(); + metricStorageRegistry = new MetricStorageRegistry(); observableRegistry = new ObservableRegistry(); meter: Meter; @@ -42,15 +45,8 @@ export class MeterSharedState { } registerMetricStorage(descriptor: InstrumentDescriptor) { - const views = this._meterProviderSharedState.viewRegistry.findViews(descriptor, this._instrumentationScope); - const storages = views - .map(view => { - const viewDescriptor = createInstrumentDescriptorWithView(view, descriptor); - const aggregator = view.aggregation.createAggregator(viewDescriptor); - const storage = new SyncMetricStorage(viewDescriptor, aggregator, view.attributesProcessor); - return this._metricStorageRegistry.register(storage); - }) - .filter(isNotNullish); + const storages = this._registerMetricStorage(descriptor, SyncMetricStorage); + if (storages.length === 1) { return storages[0]; } @@ -58,21 +54,15 @@ export class MeterSharedState { } registerAsyncMetricStorage(descriptor: InstrumentDescriptor) { - const views = this._meterProviderSharedState.viewRegistry.findViews(descriptor, this._instrumentationScope); - const storages = views - .map(view => { - const viewDescriptor = createInstrumentDescriptorWithView(view, descriptor); - const aggregator = view.aggregation.createAggregator(viewDescriptor); - const viewStorage = new AsyncMetricStorage(viewDescriptor, aggregator, view.attributesProcessor); - return this._metricStorageRegistry.register(viewStorage); - }) - .filter(isNotNullish); + const storages = this._registerMetricStorage(descriptor, AsyncMetricStorage); + return storages; } /** * @param collector opaque handle of {@link MetricCollector} which initiated the collection. * @param collectionTime the HrTime at which the collection was initiated. + * @param options options for collection. * @returns the list of metric data collected. */ async collect(collector: MetricCollectorHandle, collectionTime: HrTime, options?: MetricCollectOptions): Promise { @@ -81,7 +71,7 @@ export class MeterSharedState { * 2. Collect metric result for the collector. */ const errors = await this.observableRegistry.observe(collectionTime, options?.timeoutMillis); - const metricDataList = Array.from(this._metricStorageRegistry.getStorages()) + const metricDataList = Array.from(this.metricStorageRegistry.getStorages(collector)) .map(metricStorage => { return metricStorage.collect( collector, @@ -98,9 +88,49 @@ export class MeterSharedState { errors, }; } + + private _registerMetricStorage>(descriptor: InstrumentDescriptor, MetricStorageType: MetricStorageType): R[] { + const views = this._meterProviderSharedState.viewRegistry.findViews(descriptor, this._instrumentationScope); + let storages = views + .map(view => { + const viewDescriptor = createInstrumentDescriptorWithView(view, descriptor); + const compatibleStorage = this.metricStorageRegistry.findOrUpdateCompatibleStorage(viewDescriptor); + if (compatibleStorage != null) { + return compatibleStorage; + } + const aggregator = view.aggregation.createAggregator(viewDescriptor); + const viewStorage = new MetricStorageType(viewDescriptor, aggregator, view.attributesProcessor) as R; + this.metricStorageRegistry.register(viewStorage); + return viewStorage; + }); + + // Fallback to the per-collector aggregations if no view is configured for the instrument. + if (storages.length === 0) { + const perCollectorAggregations = this._meterProviderSharedState.selectAggregations(descriptor.type); + const collectorStorages = perCollectorAggregations.map(([collector, aggregation]) => { + const compatibleStorage = this.metricStorageRegistry.findOrUpdateCompatibleCollectorStorage(collector, descriptor); + if (compatibleStorage != null) { + return compatibleStorage; + } + const aggregator = aggregation.createAggregator(descriptor); + const storage = new MetricStorageType(descriptor, aggregator, AttributesProcessor.Noop()) as R; + this.metricStorageRegistry.registerForCollector(collector, storage); + return storage; + }); + storages = storages.concat(collectorStorages); + } + + return storages; + } } interface ScopeMetricsResult { scopeMetrics: ScopeMetrics; errors: unknown[]; } + +interface MetricStorageConstructor { + new (instrumentDescriptor: InstrumentDescriptor, + aggregator: Aggregator>, + attributesProcessor: AttributesProcessor): MetricStorage; +} diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/state/MetricCollector.ts b/packages/sdk-metrics/src/state/MetricCollector.ts similarity index 95% rename from experimental/packages/opentelemetry-sdk-metrics/src/state/MetricCollector.ts rename to packages/sdk-metrics/src/state/MetricCollector.ts index 066106e47d..fbffc2f060 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/state/MetricCollector.ts +++ b/packages/sdk-metrics/src/state/MetricCollector.ts @@ -15,7 +15,7 @@ */ import { hrTime } from '@opentelemetry/core'; -import { AggregationTemporalitySelector } from '../export/AggregationTemporality'; +import { AggregationTemporalitySelector } from '../export/AggregationSelector'; import { CollectionResult } from '../export/MetricData'; import { MetricProducer, MetricCollectOptions } from '../export/MetricProducer'; import { MetricReader } from '../export/MetricReader'; @@ -65,6 +65,10 @@ export class MetricCollector implements MetricProducer { selectAggregationTemporality(instrumentType: InstrumentType) { return this._metricReader.selectAggregationTemporality(instrumentType); } + + selectAggregation(instrumentType: InstrumentType) { + return this._metricReader.selectAggregation(instrumentType); + } } /** diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/state/MetricStorage.ts b/packages/sdk-metrics/src/state/MetricStorage.ts similarity index 96% rename from experimental/packages/opentelemetry-sdk-metrics/src/state/MetricStorage.ts rename to packages/sdk-metrics/src/state/MetricStorage.ts index 8e24da668a..f69a00daa4 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/state/MetricStorage.ts +++ b/packages/sdk-metrics/src/state/MetricStorage.ts @@ -41,7 +41,7 @@ export abstract class MetricStorage { collectionTime: HrTime, ): Maybe; - getInstrumentDescriptor(): InstrumentDescriptor{ + getInstrumentDescriptor(): Readonly { return this._instrumentDescriptor; } diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/state/MetricStorageRegistry.ts b/packages/sdk-metrics/src/state/MetricStorageRegistry.ts similarity index 51% rename from experimental/packages/opentelemetry-sdk-metrics/src/state/MetricStorageRegistry.ts rename to packages/sdk-metrics/src/state/MetricStorageRegistry.ts index 1a930cacb0..c65a1dd7b2 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/state/MetricStorageRegistry.ts +++ b/packages/sdk-metrics/src/state/MetricStorageRegistry.ts @@ -15,40 +15,93 @@ */ import { MetricStorage } from './MetricStorage'; -import { isDescriptorCompatibleWith } from '../InstrumentDescriptor'; +import { InstrumentDescriptor, isDescriptorCompatibleWith } from '../InstrumentDescriptor'; import * as api from '@opentelemetry/api'; -import { Maybe } from '../utils'; import { getConflictResolutionRecipe, getIncompatibilityDetails } from '../view/RegistrationConflicts'; +import { MetricCollectorHandle } from './MetricCollector'; + +type StorageMap = Map; /** * Internal class for storing {@link MetricStorage} */ export class MetricStorageRegistry { - private readonly _metricStorageRegistry = new Map(); + private readonly _sharedRegistry: StorageMap = new Map(); + private readonly _perCollectorRegistry = new Map(); static create(){ return new MetricStorageRegistry(); } - getStorages(): MetricStorage[] { + getStorages(collector: MetricCollectorHandle): MetricStorage[] { let storages: MetricStorage[] = []; - for (const metricStorages of this._metricStorageRegistry.values()) { + for (const metricStorages of this._sharedRegistry.values()) { storages = storages.concat(metricStorages); } + const perCollectorStorages = this._perCollectorRegistry.get(collector); + if (perCollectorStorages != null) { + for (const metricStorages of perCollectorStorages.values()) { + storages = storages.concat(metricStorages); + } + } + return storages; } - register(storage: T): Maybe { - const expectedDescriptor = storage.getInstrumentDescriptor(); - const existingStorages = this._metricStorageRegistry.get(expectedDescriptor.name); + register(storage: MetricStorage) { + this._registerStorage(storage, this._sharedRegistry); + } + + registerForCollector(collector: MetricCollectorHandle, storage: MetricStorage) { + let storageMap = this._perCollectorRegistry.get(collector); + if (storageMap == null) { + storageMap = new Map(); + this._perCollectorRegistry.set(collector, storageMap); + } + this._registerStorage(storage, storageMap); + } + + findOrUpdateCompatibleStorage(expectedDescriptor: InstrumentDescriptor): T | null { + const storages = this._sharedRegistry.get(expectedDescriptor.name); + if (storages === undefined) { + return null; + } + + // If the descriptor is compatible, the type of their metric storage + // (either SyncMetricStorage or AsyncMetricStorage) must be compatible. + return this._findOrUpdateCompatibleStorage(expectedDescriptor, storages); + } - // Add storage if it does not exist. - if (existingStorages === undefined) { - this._metricStorageRegistry.set(expectedDescriptor.name, [storage]); - return storage; + findOrUpdateCompatibleCollectorStorage(collector: MetricCollectorHandle, expectedDescriptor: InstrumentDescriptor): T | null { + const storageMap = this._perCollectorRegistry.get(collector); + if (storageMap === undefined) { + return null; } + const storages = this._sharedRegistry.get(expectedDescriptor.name); + if (storages === undefined) { + return null; + } + + // If the descriptor is compatible, the type of their metric storage + // (either SyncMetricStorage or AsyncMetricStorage) must be compatible. + return this._findOrUpdateCompatibleStorage(expectedDescriptor, storages); + } + + private _registerStorage(storage: MetricStorage, storageMap: StorageMap) { + const descriptor = storage.getInstrumentDescriptor(); + const storages = storageMap.get(descriptor.name); + + if (storages === undefined) { + storageMap.set(descriptor.name, [storage]); + return; + } + + storages.push(storage); + } + + private _findOrUpdateCompatibleStorage(expectedDescriptor: InstrumentDescriptor, existingStorages: MetricStorage[]): T | null { let compatibleStorage = null; for (const existingStorage of existingStorages) { @@ -84,12 +137,6 @@ export class MetricStorageRegistry { } } - if (compatibleStorage != null) { - return compatibleStorage; - } - - // None of the storages were compatible, add the current one to the list. - existingStorages.push(storage); - return storage; + return compatibleStorage; } } diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/state/MultiWritableMetricStorage.ts b/packages/sdk-metrics/src/state/MultiWritableMetricStorage.ts similarity index 90% rename from experimental/packages/opentelemetry-sdk-metrics/src/state/MultiWritableMetricStorage.ts rename to packages/sdk-metrics/src/state/MultiWritableMetricStorage.ts index 323ef83947..64694793ed 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/state/MultiWritableMetricStorage.ts +++ b/packages/sdk-metrics/src/state/MultiWritableMetricStorage.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { Context, HrTime } from '@opentelemetry/api'; -import { MetricAttributes } from '@opentelemetry/api-metrics'; +import { Context, HrTime, MetricAttributes } from '@opentelemetry/api'; import { WritableMetricStorage } from './WritableMetricStorage'; /** diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/state/ObservableRegistry.ts b/packages/sdk-metrics/src/state/ObservableRegistry.ts similarity index 94% rename from experimental/packages/opentelemetry-sdk-metrics/src/state/ObservableRegistry.ts rename to packages/sdk-metrics/src/state/ObservableRegistry.ts index c74b9f9ab9..bd3e3fc6a7 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/state/ObservableRegistry.ts +++ b/packages/sdk-metrics/src/state/ObservableRegistry.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -import * as api from '@opentelemetry/api'; -import { HrTime } from '@opentelemetry/api'; -import { BatchObservableCallback, Observable, ObservableCallback } from '@opentelemetry/api-metrics'; +import { diag, HrTime, BatchObservableCallback, Observable, ObservableCallback } from '@opentelemetry/api'; import { isObservableInstrument, ObservableInstrument } from '../Instruments'; import { BatchObservableResultImpl, ObservableResultImpl } from '../ObservableResult'; import { callWithTimeout, PromiseAllSettled, isPromiseAllSettledRejectionResult, setEquals } from '../utils'; @@ -67,7 +65,7 @@ export class ObservableRegistry { // Create a set of unique instruments. const observableInstruments = new Set(instruments.filter(isObservableInstrument)); if (observableInstruments.size === 0) { - api.diag.error('BatchObservableCallback is not associated with valid instruments', instruments); + diag.error('BatchObservableCallback is not associated with valid instruments', instruments); return; } const idx = this._findBatchCallback(callback, observableInstruments); diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/state/SyncMetricStorage.ts b/packages/sdk-metrics/src/state/SyncMetricStorage.ts similarity index 95% rename from experimental/packages/opentelemetry-sdk-metrics/src/state/SyncMetricStorage.ts rename to packages/sdk-metrics/src/state/SyncMetricStorage.ts index 26d5fec1ab..7bfd5967b2 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/state/SyncMetricStorage.ts +++ b/packages/sdk-metrics/src/state/SyncMetricStorage.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { Context, HrTime } from '@opentelemetry/api'; -import { MetricAttributes } from '@opentelemetry/api-metrics'; +import { Context, HrTime, MetricAttributes } from '@opentelemetry/api'; import { WritableMetricStorage } from './WritableMetricStorage'; import { Accumulation, Aggregator } from '../aggregator/types'; import { InstrumentDescriptor } from '../InstrumentDescriptor'; diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/state/TemporalMetricProcessor.ts b/packages/sdk-metrics/src/state/TemporalMetricProcessor.ts similarity index 96% rename from experimental/packages/opentelemetry-sdk-metrics/src/state/TemporalMetricProcessor.ts rename to packages/sdk-metrics/src/state/TemporalMetricProcessor.ts index 6c648ac117..7e0000d775 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/state/TemporalMetricProcessor.ts +++ b/packages/sdk-metrics/src/state/TemporalMetricProcessor.ts @@ -44,7 +44,7 @@ interface LastReportedHistory> { /** * Internal interface. * - * Provides unique reporting for each collectors. Allows synchronous collection + * Provides unique reporting for each collector. Allows synchronous collection * of metrics and reports given temporality values. */ export class TemporalMetricProcessor> { @@ -57,8 +57,6 @@ export class TemporalMetricProcessor> { * Builds the {@link MetricData} streams to report against a specific MetricCollector. * @param collector The information of the MetricCollector. * @param collectors The registered collectors. - * @param resource The resource to attach these metrics against. - * @param instrumentationScope The instrumentation scope that generated these metrics. * @param instrumentDescriptor The instrumentation descriptor that these metrics generated with. * @param currentAccumulations The current accumulation of metric data from instruments. * @param collectionTime The current collection timestamp. diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/state/WritableMetricStorage.ts b/packages/sdk-metrics/src/state/WritableMetricStorage.ts similarity index 92% rename from experimental/packages/opentelemetry-sdk-metrics/src/state/WritableMetricStorage.ts rename to packages/sdk-metrics/src/state/WritableMetricStorage.ts index 8bfdbcfde1..72945cd01d 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/state/WritableMetricStorage.ts +++ b/packages/sdk-metrics/src/state/WritableMetricStorage.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { Context, HrTime } from '@opentelemetry/api'; -import { MetricAttributes } from '@opentelemetry/api-metrics'; +import { Context, HrTime, MetricAttributes } from '@opentelemetry/api'; import { AttributeHashMap } from './HashMap'; /** diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/types.ts b/packages/sdk-metrics/src/types.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/src/types.ts rename to packages/sdk-metrics/src/types.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/utils.ts b/packages/sdk-metrics/src/utils.ts similarity index 98% rename from experimental/packages/opentelemetry-sdk-metrics/src/utils.ts rename to packages/sdk-metrics/src/utils.ts index 33e198b3ba..532e05d227 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/utils.ts +++ b/packages/sdk-metrics/src/utils.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { MetricAttributes } from '@opentelemetry/api-metrics'; +import { MetricAttributes } from '@opentelemetry/api'; import { InstrumentationScope } from '@opentelemetry/core'; export type Maybe = T | undefined; diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/view/Aggregation.ts b/packages/sdk-metrics/src/view/Aggregation.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/src/view/Aggregation.ts rename to packages/sdk-metrics/src/view/Aggregation.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/view/AttributesProcessor.ts b/packages/sdk-metrics/src/view/AttributesProcessor.ts similarity index 94% rename from experimental/packages/opentelemetry-sdk-metrics/src/view/AttributesProcessor.ts rename to packages/sdk-metrics/src/view/AttributesProcessor.ts index 9900daf82f..96858c6428 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/view/AttributesProcessor.ts +++ b/packages/sdk-metrics/src/view/AttributesProcessor.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { Context } from '@opentelemetry/api'; -import { MetricAttributes } from '@opentelemetry/api-metrics'; +import { Context, MetricAttributes } from '@opentelemetry/api'; /** * The {@link AttributesProcessor} is responsible for customizing which diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/view/InstrumentSelector.ts b/packages/sdk-metrics/src/view/InstrumentSelector.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/src/view/InstrumentSelector.ts rename to packages/sdk-metrics/src/view/InstrumentSelector.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/view/MeterSelector.ts b/packages/sdk-metrics/src/view/MeterSelector.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/src/view/MeterSelector.ts rename to packages/sdk-metrics/src/view/MeterSelector.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/view/Predicate.ts b/packages/sdk-metrics/src/view/Predicate.ts similarity index 93% rename from experimental/packages/opentelemetry-sdk-metrics/src/view/Predicate.ts rename to packages/sdk-metrics/src/view/Predicate.ts index 605737b379..c12c32bbb2 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/view/Predicate.ts +++ b/packages/sdk-metrics/src/view/Predicate.ts @@ -16,7 +16,7 @@ // https://tc39.es/proposal-regex-escaping // escape ^ $ \ . + ? ( ) [ ] { } | -// do not need to escape * as we are interpret it as wildcard +// do not need to escape * as we interpret it as wildcard const ESCAPE = /[\^$\\.+?()[\]{}|]/g; export interface Predicate { @@ -24,7 +24,7 @@ export interface Predicate { } /** - * Wildcard pattern predicate, support patterns like `*`, `foo*`, `*bar`. + * Wildcard pattern predicate, supports patterns like `*`, `foo*`, `*bar`. */ export class PatternPredicate implements Predicate { private _matchAll: boolean; diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/view/RegistrationConflicts.ts b/packages/sdk-metrics/src/view/RegistrationConflicts.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/src/view/RegistrationConflicts.ts rename to packages/sdk-metrics/src/view/RegistrationConflicts.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/view/View.ts b/packages/sdk-metrics/src/view/View.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/src/view/View.ts rename to packages/sdk-metrics/src/view/View.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/src/view/ViewRegistry.ts b/packages/sdk-metrics/src/view/ViewRegistry.ts similarity index 92% rename from experimental/packages/opentelemetry-sdk-metrics/src/view/ViewRegistry.ts rename to packages/sdk-metrics/src/view/ViewRegistry.ts index 1a26387d10..1dcaf7d22f 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/src/view/ViewRegistry.ts +++ b/packages/sdk-metrics/src/view/ViewRegistry.ts @@ -21,9 +21,6 @@ import { MeterSelector } from './MeterSelector'; import { View } from './View'; export class ViewRegistry { - private static DEFAULT_VIEW = new View({ - instrumentName: '*' - }); private _registeredViews: View[] = []; addView(view: View) { @@ -37,9 +34,6 @@ export class ViewRegistry { this._matchMeter(registeredView.meterSelector, meter); }); - if (views.length === 0) { - return [ViewRegistry.DEFAULT_VIEW]; - } return views; } diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/ExemplarFilter.test.ts b/packages/sdk-metrics/test/ExemplarFilter.test.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/ExemplarFilter.test.ts rename to packages/sdk-metrics/test/ExemplarFilter.test.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/ExemplarReservoir.test.ts b/packages/sdk-metrics/test/ExemplarReservoir.test.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/ExemplarReservoir.test.ts rename to packages/sdk-metrics/test/ExemplarReservoir.test.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/InstrumentDescriptor.test.ts b/packages/sdk-metrics/test/InstrumentDescriptor.test.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/InstrumentDescriptor.test.ts rename to packages/sdk-metrics/test/InstrumentDescriptor.test.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/Instruments.test.ts b/packages/sdk-metrics/test/Instruments.test.ts similarity index 98% rename from experimental/packages/opentelemetry-sdk-metrics/test/Instruments.test.ts rename to packages/sdk-metrics/test/Instruments.test.ts index 2bb47be3b2..1faa5b64a1 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/test/Instruments.test.ts +++ b/packages/sdk-metrics/test/Instruments.test.ts @@ -19,7 +19,6 @@ import * as sinon from 'sinon'; import { InstrumentationScope } from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; import { - AggregationTemporality, InstrumentDescriptor, InstrumentType, MeterProvider, @@ -28,7 +27,7 @@ import { DataPointType, Histogram } from '../src'; -import { TestMetricReader } from './export/TestMetricReader'; +import { TestDeltaMetricReader, TestMetricReader } from './export/TestMetricReader'; import { assertMetricData, assertDataPoint, @@ -37,7 +36,7 @@ import { defaultResource, defaultInstrumentationScope } from './util'; -import { ObservableResult, ValueType } from '@opentelemetry/api-metrics'; +import { ObservableResult, ValueType } from '@opentelemetry/api'; describe('Instruments', () => { describe('Counter', () => { @@ -654,9 +653,9 @@ function setup() { const meter = meterProvider.getMeter(defaultInstrumentationScope.name, defaultInstrumentationScope.version, { schemaUrl: defaultInstrumentationScope.schemaUrl, }); - const deltaReader = new TestMetricReader(() => AggregationTemporality.DELTA); + const deltaReader = new TestDeltaMetricReader(); meterProvider.addMetricReader(deltaReader); - const cumulativeReader = new TestMetricReader(() => AggregationTemporality.CUMULATIVE); + const cumulativeReader = new TestMetricReader(); meterProvider.addMetricReader(cumulativeReader); return { diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/Meter.test.ts b/packages/sdk-metrics/test/Meter.test.ts similarity index 99% rename from experimental/packages/opentelemetry-sdk-metrics/test/Meter.test.ts rename to packages/sdk-metrics/test/Meter.test.ts index ada1234d58..c51c0baa52 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/test/Meter.test.ts +++ b/packages/sdk-metrics/test/Meter.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Observable } from '@opentelemetry/api-metrics'; +import { Observable } from '@opentelemetry/api'; import * as assert from 'assert'; import { CounterInstrument, diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/MeterProvider.test.ts b/packages/sdk-metrics/test/MeterProvider.test.ts similarity index 98% rename from experimental/packages/opentelemetry-sdk-metrics/test/MeterProvider.test.ts rename to packages/sdk-metrics/test/MeterProvider.test.ts index 3d7dc68c6a..a7e6ac12fb 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/test/MeterProvider.test.ts +++ b/packages/sdk-metrics/test/MeterProvider.test.ts @@ -15,8 +15,7 @@ */ import * as assert from 'assert'; -import { NOOP_METER } from '@opentelemetry/api-metrics'; -import { Meter, MeterProvider, InstrumentType, DataPointType } from '../src'; +import { MeterProvider, InstrumentType, DataPointType } from '../src'; import { assertScopeMetrics, assertMetricData, @@ -26,6 +25,7 @@ import { import { TestMetricReader } from './export/TestMetricReader'; import * as sinon from 'sinon'; import { View } from '../src/view/View'; +import { Meter } from '../src/Meter'; describe('MeterProvider', () => { afterEach(() => { @@ -62,7 +62,8 @@ describe('MeterProvider', () => { const meterProvider = new MeterProvider(); meterProvider.shutdown(); const meter = meterProvider.getMeter('meter1', '1.0.0'); - assert.strictEqual(meter, NOOP_METER); + // returned tracer should be no-op, not instance of Meter (from SDK) + assert.ok(!(meter instanceof Meter)); }); it('get meter with same identity', async () => { diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/ObservableResult.test.ts b/packages/sdk-metrics/test/ObservableResult.test.ts similarity index 95% rename from experimental/packages/opentelemetry-sdk-metrics/test/ObservableResult.test.ts rename to packages/sdk-metrics/test/ObservableResult.test.ts index f6a38ce443..c425eafec7 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/test/ObservableResult.test.ts +++ b/packages/sdk-metrics/test/ObservableResult.test.ts @@ -14,11 +14,14 @@ * limitations under the License. */ -import { ValueType } from '@opentelemetry/api-metrics'; +import { ValueType } from '@opentelemetry/api'; import * as assert from 'assert'; -import { BatchObservableResultImpl, InstrumentType } from '../src'; +import { InstrumentType } from '../src'; import { ObservableInstrument } from '../src/Instruments'; -import { ObservableResultImpl } from '../src/ObservableResult'; +import { + BatchObservableResultImpl, + ObservableResultImpl +} from '../src/ObservableResult'; import { ObservableRegistry } from '../src/state/ObservableRegistry'; import { commonAttributes, commonValues, defaultInstrumentDescriptor } from './util'; diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/aggregator/Drop.test.ts b/packages/sdk-metrics/test/aggregator/Drop.test.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/aggregator/Drop.test.ts rename to packages/sdk-metrics/test/aggregator/Drop.test.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/aggregator/Histogram.test.ts b/packages/sdk-metrics/test/aggregator/Histogram.test.ts similarity index 98% rename from experimental/packages/opentelemetry-sdk-metrics/test/aggregator/Histogram.test.ts rename to packages/sdk-metrics/test/aggregator/Histogram.test.ts index f5d5ea7e33..3dadaf8b1c 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/test/aggregator/Histogram.test.ts +++ b/packages/sdk-metrics/test/aggregator/Histogram.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { HrTime } from '@opentelemetry/api'; +import { HrTime, ValueType } from '@opentelemetry/api'; import * as assert from 'assert'; import { AggregationTemporality, @@ -30,7 +30,6 @@ import { commonValues, defaultInstrumentDescriptor } from '../util'; -import { ValueType } from '@opentelemetry/api-metrics'; describe('HistogramAggregator', () => { describe('createAccumulation', () => { diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/aggregator/LastValue.test.ts b/packages/sdk-metrics/test/aggregator/LastValue.test.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/aggregator/LastValue.test.ts rename to packages/sdk-metrics/test/aggregator/LastValue.test.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/aggregator/Sum.test.ts b/packages/sdk-metrics/test/aggregator/Sum.test.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/aggregator/Sum.test.ts rename to packages/sdk-metrics/test/aggregator/Sum.test.ts diff --git a/packages/sdk-metrics/test/export/ConsoleMetricExporter.test.ts b/packages/sdk-metrics/test/export/ConsoleMetricExporter.test.ts new file mode 100644 index 0000000000..7cd8048bf9 --- /dev/null +++ b/packages/sdk-metrics/test/export/ConsoleMetricExporter.test.ts @@ -0,0 +1,153 @@ +/* + * 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 metrics from '@opentelemetry/api'; +import { ExportResult } from '@opentelemetry/core'; +import { ConsoleMetricExporter } from '../../src/export/ConsoleMetricExporter'; +import { PeriodicExportingMetricReader } from '../../src/export/PeriodicExportingMetricReader'; +import { ResourceMetrics } from '../../src/export/MetricData'; +import { MeterProvider } from '../../src/MeterProvider'; +import { defaultResource } from '../util'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { assertAggregationTemporalitySelector } from './utils'; +import { + DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR +} from '../../src/export/AggregationSelector'; +import { + AggregationTemporality, + InstrumentType +} from '../../src'; + + +async function waitForNumberOfExports(exporter: sinon.SinonSpy<[metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void], void>, numberOfExports: number): Promise { + if (numberOfExports <= 0) { + throw new Error('numberOfExports must be greater than or equal to 0'); + } + + let totalExports = 0; + while (totalExports < numberOfExports) { + await new Promise(resolve => setTimeout(resolve, 20)); + totalExports = exporter.callCount; + } +} + +/* eslint-disable no-console */ +describe('ConsoleMetricExporter', () => { + describe('export', () => { + let previousConsoleDir: any; + let exporter: ConsoleMetricExporter; + let meterProvider: MeterProvider; + let meterReader: PeriodicExportingMetricReader; + let meter: metrics.Meter; + + beforeEach(() => { + previousConsoleDir = console.dir; + console.dir = () => {}; + + exporter = new ConsoleMetricExporter(); + meterProvider = new MeterProvider({ resource: defaultResource }); + meter = meterProvider.getMeter('ConsoleMetricExporter', '1.0.0'); + meterReader = new PeriodicExportingMetricReader({ + exporter: exporter, + exportIntervalMillis: 100, + exportTimeoutMillis: 100 + }); + meterProvider.addMetricReader(meterReader); + }); + + afterEach(async () => { + console.dir = previousConsoleDir; + + await meterReader.shutdown(); + }); + + it('should export information about metric', async () => { + const counter = meter.createCounter('counter_total', { + description: 'a test description', + }); + const counterAttribute = { key1: 'attributeValue1' }; + counter.add(10, counterAttribute); + counter.add(10, counterAttribute); + + const histogram = meter.createHistogram('histogram', { description: 'a histogram' }); + histogram.record(10); + histogram.record(100); + histogram.record(1000); + + const spyConsole = sinon.spy(console, 'dir'); + const spyExport = sinon.spy(exporter, 'export'); + + await waitForNumberOfExports(spyExport, 1); + const resourceMetrics = spyExport.args[0]; + const firstResourceMetric = resourceMetrics[0]; + const consoleArgs = spyConsole.args[0]; + const consoleMetric = consoleArgs[0]; + const keys = Object.keys(consoleMetric).sort().join(','); + + const expectedKeys = [ + 'dataPointType', + 'dataPoints', + 'descriptor', + ].join(','); + + assert.ok(firstResourceMetric.resource.attributes.resourceKey === 'my-resource', 'resourceKey'); + assert.ok(keys === expectedKeys, 'expectedKeys'); + assert.ok(consoleMetric.descriptor.name === 'counter_total', 'name'); + assert.ok(consoleMetric.descriptor.description === 'a test description', 'description'); + assert.ok(consoleMetric.descriptor.type === 'COUNTER', 'type'); + assert.ok(consoleMetric.descriptor.unit === '', 'unit'); + assert.ok(consoleMetric.descriptor.valueType === 1, 'valueType'); + assert.ok(consoleMetric.dataPoints[0].attributes.key1 === 'attributeValue1', 'ensure metric attributes exists'); + + assert.ok(spyExport.calledOnce); + }); + }); + + describe('constructor', () => { + it('with no arguments should select cumulative temporality', () => { + const exporter = new ConsoleMetricExporter(); + assertAggregationTemporalitySelector(exporter, DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR); + }); + + it('with empty options should select cumulative temporality', () => { + const exporter = new ConsoleMetricExporter({}); + assertAggregationTemporalitySelector(exporter, DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR); + }); + + it('with cumulative preference should select cumulative temporality', () => { + const exporter = new ConsoleMetricExporter({ temporalitySelector: _ => AggregationTemporality.CUMULATIVE }); + assertAggregationTemporalitySelector(exporter, _ => AggregationTemporality.CUMULATIVE); + }); + + it('with mixed preference should select matching temporality', () => { + // use delta-ish example as a representation of a commonly used "mixed" preference. + const selector = (instrumentType: InstrumentType) => { + switch (instrumentType) { + case InstrumentType.COUNTER: + case InstrumentType.OBSERVABLE_COUNTER: + case InstrumentType.HISTOGRAM: + case InstrumentType.OBSERVABLE_GAUGE: + return AggregationTemporality.DELTA; + case InstrumentType.UP_DOWN_COUNTER: + case InstrumentType.OBSERVABLE_UP_DOWN_COUNTER: + return AggregationTemporality.CUMULATIVE; + } + }; + const exporter = new ConsoleMetricExporter({ temporalitySelector: selector }); + assertAggregationTemporalitySelector(exporter, selector); + }); + }); +}); diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/export/InMemoryMetricExporter.test.ts b/packages/sdk-metrics/test/export/InMemoryMetricExporter.test.ts similarity index 98% rename from experimental/packages/opentelemetry-sdk-metrics/test/export/InMemoryMetricExporter.test.ts rename to packages/sdk-metrics/test/export/InMemoryMetricExporter.test.ts index 7afb3299a4..cab538fd2d 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/test/export/InMemoryMetricExporter.test.ts +++ b/packages/sdk-metrics/test/export/InMemoryMetricExporter.test.ts @@ -15,7 +15,7 @@ */ import { ExportResultCode } from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; -import * as metrics from '@opentelemetry/api-metrics'; +import * as metrics from '@opentelemetry/api'; import assert = require('assert'); import { AggregationTemporality } from '../../src/export/AggregationTemporality'; import { InMemoryMetricExporter } from '../../src/export/InMemoryMetricExporter'; diff --git a/packages/sdk-metrics/test/export/MetricReader.test.ts b/packages/sdk-metrics/test/export/MetricReader.test.ts new file mode 100644 index 0000000000..32c00c24d4 --- /dev/null +++ b/packages/sdk-metrics/test/export/MetricReader.test.ts @@ -0,0 +1,128 @@ +/* + * 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 { MeterProvider } from '../../src/MeterProvider'; +import { assertRejects } from '../test-utils'; +import { + emptyResourceMetrics, + TestMetricProducer +} from './TestMetricProducer'; +import { TestMetricReader } from './TestMetricReader'; +import { + Aggregation, + AggregationTemporality +} from '../../src'; +import { + DEFAULT_AGGREGATION_SELECTOR, + DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR, +} from '../../src/export/AggregationSelector'; +import { + assertAggregationSelector, + assertAggregationTemporalitySelector +} from './utils'; + +describe('MetricReader', () => { + describe('setMetricProducer', () => { + it('The SDK MUST NOT allow a MetricReader instance to be registered on more than one MeterProvider instance', () => { + const reader = new TestMetricReader(); + const meterProvider1 = new MeterProvider(); + const meterProvider2 = new MeterProvider(); + + meterProvider1.addMetricReader(reader); + assert.throws(() => meterProvider1.addMetricReader(reader), /MetricReader can not be bound to a MeterProvider again/); + assert.throws(() => meterProvider2.addMetricReader(reader), /MetricReader can not be bound to a MeterProvider again/); + }); + }); + + describe('setMetricProducer', () => { + it('should initialize the metric reader', async () => { + const reader = new TestMetricReader(); + + reader.setMetricProducer(new TestMetricProducer()); + const result = await reader.collect(); + + assert.deepStrictEqual(result, { + resourceMetrics: emptyResourceMetrics, + errors: [], + }); + await reader.shutdown(); + }); + }); + + describe('collect', () => { + it('should throw on non-initialized instance', async () => { + const reader = new TestMetricReader(); + + await assertRejects(() => reader.collect(), /MetricReader is not bound to a MetricProducer/); + }); + + it('should return empty on shut-down instance', async () => { + const reader = new TestMetricReader(); + + reader.setMetricProducer(new TestMetricProducer()); + + await reader.shutdown(); + assertRejects(reader.collect(), /MetricReader is shutdown/); + }); + + it('should call MetricProduce.collect with timeout', async () => { + const reader = new TestMetricReader(); + const producer = new TestMetricProducer(); + reader.setMetricProducer(producer); + + const collectStub = sinon.stub(producer, 'collect'); + + await reader.collect({ timeoutMillis: 20 }); + assert(collectStub.calledOnce); + const args = collectStub.args[0]; + assert.deepStrictEqual(args, [{ timeoutMillis: 20 }]); + + await reader.shutdown(); + }); + }); + + describe('selectAggregation', () => { + it('should override default when not provided with a selector', () => { + assertAggregationSelector(new TestMetricReader(), DEFAULT_AGGREGATION_SELECTOR); + assertAggregationSelector(new TestMetricReader({}), DEFAULT_AGGREGATION_SELECTOR); + }); + + it('should override default when provided with a selector', () => { + const reader = new TestMetricReader({ + aggregationSelector: _instrumentType => Aggregation.Sum() + }); + assertAggregationSelector(reader, _instrumentType => Aggregation.Sum()); + reader.shutdown(); + }); + }); + + describe('selectAggregationTemporality', () => { + it('should override default when not provided with a selector', () => { + assertAggregationTemporalitySelector(new TestMetricReader(), DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR); + assertAggregationTemporalitySelector(new TestMetricReader({}), DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR); + }); + + it('should override default when provided with a selector', () => { + const reader = new TestMetricReader({ + aggregationTemporalitySelector: _instrumentType => AggregationTemporality.DELTA + }); + assertAggregationTemporalitySelector(reader, _instrumentType => AggregationTemporality.DELTA); + reader.shutdown(); + }); + }); +}); diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/export/PeriodicExportingMetricReader.test.ts b/packages/sdk-metrics/test/export/PeriodicExportingMetricReader.test.ts similarity index 84% rename from experimental/packages/opentelemetry-sdk-metrics/test/export/PeriodicExportingMetricReader.test.ts rename to packages/sdk-metrics/test/export/PeriodicExportingMetricReader.test.ts index 21890024bf..9e3ac30826 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/test/export/PeriodicExportingMetricReader.test.ts +++ b/packages/sdk-metrics/test/export/PeriodicExportingMetricReader.test.ts @@ -16,15 +16,32 @@ import { PeriodicExportingMetricReader } from '../../src/export/PeriodicExportingMetricReader'; import { AggregationTemporality } from '../../src/export/AggregationTemporality'; -import { InstrumentType, PushMetricExporter } from '../../src'; -import { CollectionResult, ResourceMetrics } from '../../src/export/MetricData'; +import { + Aggregation, + InstrumentType, + PushMetricExporter +} from '../../src'; +import { ResourceMetrics } from '../../src/export/MetricData'; import * as assert from 'assert'; import * as sinon from 'sinon'; -import { MetricProducer } from '../../src/export/MetricProducer'; import { TimeoutError } from '../../src/utils'; -import { ExportResult, ExportResultCode } from '@opentelemetry/core'; +import { + ExportResult, + ExportResultCode +} from '@opentelemetry/core'; import { assertRejects } from '../test-utils'; -import { defaultResource } from '../util'; +import { + emptyResourceMetrics, + TestMetricProducer +} from './TestMetricProducer'; +import { + assertAggregationSelector, + assertAggregationTemporalitySelector +} from './utils'; +import { + DEFAULT_AGGREGATION_SELECTOR, + DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR +} from '../../src/export/AggregationSelector'; const MAX_32_BIT_INT = 2 ** 31 - 1; @@ -44,9 +61,9 @@ class TestMetricExporter implements PushMetricExporter { } setTimeout(() => { if (this.failureResult) { - resultCallback({code: ExportResultCode.FAILED, error: new Error('some error') }); + resultCallback({ code: ExportResultCode.FAILED, error: new Error('some error') }); } else { - resultCallback({code: ExportResultCode.SUCCESS }); + resultCallback({ code: ExportResultCode.SUCCESS }); } }, this.exportTime); } @@ -76,26 +93,17 @@ class TestMetricExporter implements PushMetricExporter { } return this._batches.slice(0, numberOfExports); } - - selectAggregationTemporality(_instrumentType: InstrumentType): AggregationTemporality { - return AggregationTemporality.CUMULATIVE; - } } class TestDeltaMetricExporter extends TestMetricExporter { - override selectAggregationTemporality(_instrumentType: InstrumentType): AggregationTemporality { + selectAggregationTemporality(_instrumentType: InstrumentType): AggregationTemporality { return AggregationTemporality.DELTA; } } -const emptyResourceMetrics = { resource: defaultResource, scopeMetrics: [] }; - -class TestMetricProducer implements MetricProducer { - async collect(): Promise { - return { - resourceMetrics: { resource: defaultResource, scopeMetrics: [] }, - errors: [], - }; +class TestDropMetricExporter extends TestMetricExporter { + selectAggregation(_instrumentType: InstrumentType): Aggregation { + return Aggregation.Drop(); } } @@ -299,6 +307,60 @@ describe('PeriodicExportingMetricReader', () => { }); }); + describe('selectAggregationTemporality', () => { + it('should default to Cumulative with no exporter preference', () => { + // Adding exporter without preference. + const exporter = new TestMetricExporter(); + const reader = new PeriodicExportingMetricReader({ + exporter: exporter, + exportIntervalMillis: MAX_32_BIT_INT, + }); + + assertAggregationTemporalitySelector(reader, DEFAULT_AGGREGATION_TEMPORALITY_SELECTOR); + reader.shutdown(); + }); + + it('should default to exporter preference', () => { + // Adding exporter with DELTA preference. + const exporter = new TestDeltaMetricExporter(); + const reader = new PeriodicExportingMetricReader({ + exporter: exporter, + exportIntervalMillis: MAX_32_BIT_INT, + }); + + assertAggregationTemporalitySelector(reader, exporter.selectAggregationTemporality); + reader.shutdown(); + }); + }); + + describe('selectAggregation', () => { + it('should use default aggregation with no exporter preference', () => { + // Adding exporter without preference. + const exporter = new TestMetricExporter(); + const reader = new PeriodicExportingMetricReader({ + exporter: exporter, + exportIntervalMillis: MAX_32_BIT_INT, + }); + + // check if the default selector is used. + assertAggregationSelector(reader, DEFAULT_AGGREGATION_SELECTOR); + reader.shutdown(); + }); + + it('should default to exporter preference', () => { + // Adding exporter with Drop Aggregation preference. + const exporter = new TestDropMetricExporter(); + const reader = new PeriodicExportingMetricReader({ + exporter: exporter, + exportIntervalMillis: MAX_32_BIT_INT, + }); + + // check if the exporter's selector is used. + assertAggregationSelector(reader, exporter.selectAggregation); + reader.shutdown(); + }); + }); + describe('shutdown', () => { afterEach(() => { sinon.restore(); @@ -364,53 +426,5 @@ describe('PeriodicExportingMetricReader', () => { await assertRejects(() => reader.shutdown(), /Error during forceFlush/); }); - }) - ; - - describe('collect', () => { - it('should throw on non-initialized instance', async () => { - const exporter = new TestMetricExporter(); - const reader = new PeriodicExportingMetricReader({ - exporter: exporter, - exportIntervalMillis: MAX_32_BIT_INT, - exportTimeoutMillis: 80, - }); - - await assertRejects(() => reader.collect(), /MetricReader is not bound to a MetricProducer/); - }); - - it('should return empty on shut-down instance', async () => { - const exporter = new TestMetricExporter(); - const reader = new PeriodicExportingMetricReader({ - exporter: exporter, - exportIntervalMillis: MAX_32_BIT_INT, - exportTimeoutMillis: 80, - }); - - reader.setMetricProducer(new TestMetricProducer()); - - await reader.shutdown(); - assertRejects(reader.collect(), /MetricReader is shutdown/); - }); - - it('should call MetricProduce.collect with timeout', async () => { - const exporter = new TestMetricExporter(); - const reader = new PeriodicExportingMetricReader({ - exporter: exporter, - exportIntervalMillis: MAX_32_BIT_INT, - exportTimeoutMillis: 80, - }); - const producer = new TestMetricProducer(); - reader.setMetricProducer(producer); - - const collectStub = sinon.stub(producer, 'collect'); - - await reader.collect({ timeoutMillis: 20 }); - assert(collectStub.calledOnce); - const args = collectStub.args[0]; - assert.deepStrictEqual(args, [{ timeoutMillis: 20 }]); - - await reader.shutdown(); - }); }); }); diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/export/TestMetricExporter.ts b/packages/sdk-metrics/test/export/TestMetricExporter.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/export/TestMetricExporter.ts rename to packages/sdk-metrics/test/export/TestMetricExporter.ts diff --git a/packages/sdk-metrics/test/export/TestMetricProducer.ts b/packages/sdk-metrics/test/export/TestMetricProducer.ts new file mode 100644 index 0000000000..cb1247ed00 --- /dev/null +++ b/packages/sdk-metrics/test/export/TestMetricProducer.ts @@ -0,0 +1,30 @@ +/* + * 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 { CollectionResult } from '../../src/export/MetricData'; +import { MetricProducer } from '../../src/export/MetricProducer'; +import { defaultResource } from '../util'; + +export const emptyResourceMetrics = { resource: defaultResource, scopeMetrics: [] }; + +export class TestMetricProducer implements MetricProducer { + async collect(): Promise { + return { + resourceMetrics: { resource: defaultResource, scopeMetrics: [] }, + errors: [], + }; + } +} diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/export/TestMetricReader.ts b/packages/sdk-metrics/test/export/TestMetricReader.ts similarity index 71% rename from experimental/packages/opentelemetry-sdk-metrics/test/export/TestMetricReader.ts rename to packages/sdk-metrics/test/export/TestMetricReader.ts index 4b02562c94..40ae90e153 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/test/export/TestMetricReader.ts +++ b/packages/sdk-metrics/test/export/TestMetricReader.ts @@ -16,22 +16,15 @@ import { AggregationTemporality, - AggregationTemporalitySelector, - InstrumentType, MetricReader, } from '../../src'; import { MetricCollector } from '../../src/state/MetricCollector'; +import { MetricReaderOptions } from '../../src/export/MetricReader'; /** * A test metric reader that implements no-op onForceFlush() and onShutdown() handlers. */ export class TestMetricReader extends MetricReader { - private _aggregationTemporalitySelector: AggregationTemporalitySelector; - - constructor(aggregationTemporalitySelector?: AggregationTemporalitySelector) { - super(); - this._aggregationTemporalitySelector = aggregationTemporalitySelector ?? (() => AggregationTemporality.CUMULATIVE); - } protected onForceFlush(): Promise { return Promise.resolve(undefined); @@ -41,11 +34,16 @@ export class TestMetricReader extends MetricReader { return Promise.resolve(undefined); } - selectAggregationTemporality(instrumentType: InstrumentType) { - return this._aggregationTemporalitySelector(instrumentType); - } - getMetricCollector(): MetricCollector { return this['_metricProducer'] as MetricCollector; } } + +export class TestDeltaMetricReader extends TestMetricReader { + constructor(options: MetricReaderOptions = {}) { + super({ + ...options, + aggregationTemporalitySelector: () => AggregationTemporality.DELTA, + }); + } +} diff --git a/packages/sdk-metrics/test/export/utils.ts b/packages/sdk-metrics/test/export/utils.ts new file mode 100644 index 0000000000..3ad8543be7 --- /dev/null +++ b/packages/sdk-metrics/test/export/utils.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 { + AggregationSelector, + AggregationTemporalitySelector, + InstrumentType, + MetricReader, + PushMetricExporter +} from '../../src'; +import * as assert from 'assert'; + +const instrumentTypes = [ + InstrumentType.COUNTER, + InstrumentType.OBSERVABLE_COUNTER, + InstrumentType.UP_DOWN_COUNTER, + InstrumentType.OBSERVABLE_UP_DOWN_COUNTER, + InstrumentType.HISTOGRAM, + InstrumentType.OBSERVABLE_GAUGE +]; + +/** + * Check if AggregationSelectors behave in the same way. + * @param reader + * @param expectedSelector + */ +export function assertAggregationSelector(reader: MetricReader | PushMetricExporter, expectedSelector: AggregationSelector) { + for (const instrumentType of instrumentTypes) { + assert.strictEqual(reader.selectAggregation?.(instrumentType), + expectedSelector(instrumentType), + `incorrect aggregation selection for ${InstrumentType[instrumentType]}`); + } +} + +/** + * Check if AggregationTemporalitySelectors behave in the same way. + * @param reader + * @param expectedSelector + */ +export function assertAggregationTemporalitySelector(reader: MetricReader | PushMetricExporter, expectedSelector: AggregationTemporalitySelector) { + for (const instrumentType of instrumentTypes) { + assert.strictEqual(reader.selectAggregationTemporality?.(instrumentType), + expectedSelector(instrumentType), + `incorrect aggregation temporality selection for ${InstrumentType[instrumentType]}`); + } +} diff --git a/packages/sdk-metrics/test/index-webpack.ts b/packages/sdk-metrics/test/index-webpack.ts new file mode 100644 index 0000000000..9cfc10baa1 --- /dev/null +++ b/packages/sdk-metrics/test/index-webpack.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +const testsContext = require.context('.', true, /test$/); +testsContext.keys().forEach(testsContext); + +const srcContext = require.context('.', true, /src$/); +srcContext.keys().forEach(srcContext); diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/state/AsyncMetricStorage.test.ts b/packages/sdk-metrics/test/state/AsyncMetricStorage.test.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/state/AsyncMetricStorage.test.ts rename to packages/sdk-metrics/test/state/AsyncMetricStorage.test.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/state/DeltaMetricProcessor.test.ts b/packages/sdk-metrics/test/state/DeltaMetricProcessor.test.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/state/DeltaMetricProcessor.test.ts rename to packages/sdk-metrics/test/state/DeltaMetricProcessor.test.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/state/HashMap.test.ts b/packages/sdk-metrics/test/state/HashMap.test.ts similarity index 97% rename from experimental/packages/opentelemetry-sdk-metrics/test/state/HashMap.test.ts rename to packages/sdk-metrics/test/state/HashMap.test.ts index d41c24e9f4..8ae772989b 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/test/state/HashMap.test.ts +++ b/packages/sdk-metrics/test/state/HashMap.test.ts @@ -15,7 +15,7 @@ */ import * as assert from 'assert'; -import { MetricAttributes } from '@opentelemetry/api-metrics'; +import { MetricAttributes } from '@opentelemetry/api'; import { HashMap } from '../../src/state/HashMap'; import { hashAttributes } from '../../src/utils'; diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/state/MeterSharedState.test.ts b/packages/sdk-metrics/test/state/MeterSharedState.test.ts similarity index 60% rename from experimental/packages/opentelemetry-sdk-metrics/test/state/MeterSharedState.test.ts rename to packages/sdk-metrics/test/state/MeterSharedState.test.ts index bacbe87e3f..21f2dce7fd 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/test/state/MeterSharedState.test.ts +++ b/packages/sdk-metrics/test/state/MeterSharedState.test.ts @@ -17,31 +17,140 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { - AggregationTemporality, - Meter, MeterProvider, DataPointType, - CollectionResult, - View + View, + Aggregation, + MetricReader, + InstrumentType } from '../../src'; import { assertMetricData, defaultInstrumentationScope, defaultResource, sleep } from '../util'; -import { TestMetricReader } from '../export/TestMetricReader'; +import { TestDeltaMetricReader, TestMetricReader } from '../export/TestMetricReader'; import { MeterSharedState } from '../../src/state/MeterSharedState'; +import { CollectionResult } from '../../src/export/MetricData'; +import { Meter } from '../../src/Meter'; describe('MeterSharedState', () => { afterEach(() => { sinon.restore(); }); + describe('registerMetricStorage', () => { + function setupMeter(views?: View[], readers?: MetricReader[]) { + const meterProvider = new MeterProvider({ + resource: defaultResource, + views, + }); + readers?.forEach(reader => meterProvider.addMetricReader(reader)); + + const meter = meterProvider.getMeter('test-meter'); + + return { + meter, + meterSharedState: meterProvider['_sharedState'].getMeterSharedState({ name: 'test-meter' }), + collectors: Array.from(meterProvider['_sharedState'].metricCollectors), + }; + } + + it('should register metric storages with views', () => { + const reader = new TestMetricReader({ + aggregationSelector: () => { + throw new Error('should not be called'); + }, + }); + const { meter, meterSharedState, collectors } = setupMeter( + [ new View({ instrumentName: 'test-counter' }) ], + [reader], + ); + + meter.createCounter('test-counter'); + const metricStorages = meterSharedState.metricStorageRegistry.getStorages(collectors[0]); + + assert.strictEqual(metricStorages.length, 1); + assert.strictEqual(metricStorages[0].getInstrumentDescriptor().name, 'test-counter'); + }); + + it('should register metric storages with views', () => { + const reader = new TestMetricReader({ + aggregationSelector: () => { + throw new Error('should not be called'); + }, + }); + const { meter, meterSharedState, collectors } = setupMeter( + [ new View({ instrumentName: 'test-counter' }) ], + [reader], + ); + + meter.createCounter('test-counter'); + const metricStorages = meterSharedState.metricStorageRegistry.getStorages(collectors[0]); + + assert.strictEqual(metricStorages.length, 1); + assert.strictEqual(metricStorages[0].getInstrumentDescriptor().name, 'test-counter'); + }); + + it('should register metric storages with the collector', () => { + const reader = new TestMetricReader({ + aggregationSelector: (instrumentType: InstrumentType) => { + return Aggregation.Drop(); + }, + }); + const readerAggregationSelectorSpy = sinon.spy(reader, 'selectAggregation'); + + const { meter, meterSharedState, collectors } = setupMeter( + [], /** no views registered */ + [reader], + ); + + meter.createCounter('test-counter'); + const metricStorages = meterSharedState.metricStorageRegistry.getStorages(collectors[0]); + + // Should select aggregation with the metric reader. + assert.strictEqual(readerAggregationSelectorSpy.callCount, 1); + assert.strictEqual(metricStorages.length, 1); + assert.strictEqual(metricStorages[0].getInstrumentDescriptor().name, 'test-counter'); + }); + + it('should register metric storages with collectors', () => { + const reader = new TestMetricReader({ + aggregationSelector: (instrumentType: InstrumentType) => { + return Aggregation.Drop(); + }, + }); + const reader2 = new TestMetricReader({ + aggregationSelector: (instrumentType: InstrumentType) => { + return Aggregation.LastValue(); + }, + }); + + const { meter, meterSharedState, collectors } = setupMeter( + [], /** no views registered */ + [reader, reader2], + ); + + meter.createCounter('test-counter'); + const metricStorages = meterSharedState.metricStorageRegistry.getStorages(collectors[0]); + const metricStorages2 = meterSharedState.metricStorageRegistry.getStorages(collectors[1]); + + // Should select aggregation with the metric reader. + assert.strictEqual(metricStorages.length, 1); + assert.strictEqual(metricStorages[0].getInstrumentDescriptor().name, 'test-counter'); + + assert.strictEqual(metricStorages2.length, 1); + assert.strictEqual(metricStorages2[0].getInstrumentDescriptor().name, 'test-counter'); + + assert.notStrictEqual(metricStorages[0], metricStorages2[0], 'should create a distinct metric storage for each metric reader'); + }); + }); + describe('collect', () => { function setupInstruments(views?: View[]) { const meterProvider = new MeterProvider({ resource: defaultResource, views: views }); - const cumulativeReader = new TestMetricReader(() => AggregationTemporality.CUMULATIVE); + const cumulativeReader = new TestMetricReader(); meterProvider.addMetricReader(cumulativeReader); const cumulativeCollector = cumulativeReader.getMetricCollector(); - const deltaReader = new TestMetricReader(() => AggregationTemporality.DELTA); + const deltaReader = new TestDeltaMetricReader(); meterProvider.addMetricReader(deltaReader); const deltaCollector = deltaReader.getMetricCollector(); diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/state/MetricCollector.test.ts b/packages/sdk-metrics/test/state/MetricCollector.test.ts similarity index 91% rename from experimental/packages/opentelemetry-sdk-metrics/test/state/MetricCollector.test.ts rename to packages/sdk-metrics/test/state/MetricCollector.test.ts index 77047d9f52..6f316a11ed 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/test/state/MetricCollector.test.ts +++ b/packages/sdk-metrics/test/state/MetricCollector.test.ts @@ -16,9 +16,9 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { MeterProvider, TimeoutError } from '../../src'; +import { MeterProvider } from '../../src'; +import { TimeoutError } from '../../src/utils'; import { DataPointType } from '../../src/export/MetricData'; -import { PushMetricExporter } from '../../src/export/MetricExporter'; import { MeterProviderSharedState } from '../../src/state/MeterProviderSharedState'; import { MetricCollector } from '../../src/state/MetricCollector'; import { @@ -29,8 +29,7 @@ import { ObservableCallbackDelegate, BatchObservableCallbackDelegate, } from '../util'; -import { TestMetricReader } from '../export/TestMetricReader'; -import { TestDeltaMetricExporter, TestMetricExporter } from '../export/TestMetricExporter'; +import { TestDeltaMetricReader, TestMetricReader } from '../export/TestMetricReader'; describe('MetricCollector', () => { afterEach(() => { @@ -40,20 +39,18 @@ describe('MetricCollector', () => { describe('constructor', () => { it('should construct MetricCollector without exceptions', () => { const meterProviderSharedState = new MeterProviderSharedState(defaultResource); - const exporters = [ new TestMetricExporter(), new TestDeltaMetricExporter() ]; - for (const exporter of exporters) { - const reader = new TestMetricReader(exporter.selectAggregationTemporality); + const readers = [ new TestMetricReader(), new TestDeltaMetricReader() ]; + for (const reader of readers) { assert.doesNotThrow(() => new MetricCollector(meterProviderSharedState, reader)); } }); }); describe('collect', () => { - - function setupInstruments(exporter: PushMetricExporter) { + function setupInstruments() { const meterProvider = new MeterProvider({ resource: defaultResource }); - const reader = new TestMetricReader(exporter.selectAggregationTemporality); + const reader = new TestMetricReader(); meterProvider.addMetricReader(reader); const metricCollector = reader.getMetricCollector(); @@ -66,8 +63,7 @@ describe('MetricCollector', () => { it('should collect sync metrics', async () => { /** preparing test instrumentations */ - const exporter = new TestMetricExporter(); - const { metricCollector, meter } = setupInstruments(exporter); + const { metricCollector, meter } = setupInstruments(); /** creating metric events */ const counter = meter.createCounter('counter1'); @@ -104,8 +100,7 @@ describe('MetricCollector', () => { it('should collect async metrics', async () => { /** preparing test instrumentations */ - const exporter = new TestMetricExporter(); - const { metricCollector, meter } = setupInstruments(exporter); + const { metricCollector, meter } = setupInstruments(); /** creating metric events */ /** observable */ @@ -163,8 +158,7 @@ describe('MetricCollector', () => { it('should collect observer metrics with timeout', async () => { sinon.useFakeTimers(); /** preparing test instrumentations */ - const exporter = new TestMetricExporter(); - const { metricCollector, meter } = setupInstruments(exporter); + const { metricCollector, meter } = setupInstruments(); /** creating metric events */ @@ -246,8 +240,7 @@ describe('MetricCollector', () => { it('should collect with throwing observable callbacks', async () => { /** preparing test instrumentations */ - const exporter = new TestMetricExporter(); - const { metricCollector, meter } = setupInstruments(exporter); + const { metricCollector, meter } = setupInstruments(); /** creating metric events */ const counter = meter.createCounter('counter1'); @@ -283,8 +276,7 @@ describe('MetricCollector', () => { it('should collect batch observer metrics with timeout', async () => { sinon.useFakeTimers(); /** preparing test instrumentations */ - const exporter = new TestMetricExporter(); - const { metricCollector, meter } = setupInstruments(exporter); + const { metricCollector, meter } = setupInstruments(); /** creating metric events */ @@ -366,8 +358,7 @@ describe('MetricCollector', () => { it('should collect with throwing batch observable callbacks', async () => { /** preparing test instrumentations */ - const exporter = new TestMetricExporter(); - const { metricCollector, meter } = setupInstruments(exporter); + const { metricCollector, meter } = setupInstruments(); /** creating metric events */ const counter = meter.createCounter('counter1'); diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/state/MetricStorageRegistry.test.ts b/packages/sdk-metrics/test/state/MetricStorageRegistry.test.ts similarity index 57% rename from experimental/packages/opentelemetry-sdk-metrics/test/state/MetricStorageRegistry.test.ts rename to packages/sdk-metrics/test/state/MetricStorageRegistry.test.ts index e3defd2c92..53ffa5a75a 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/test/state/MetricStorageRegistry.test.ts +++ b/packages/sdk-metrics/test/state/MetricStorageRegistry.test.ts @@ -15,13 +15,12 @@ */ import { MetricStorageRegistry } from '../../src/state/MetricStorageRegistry'; -import { ValueType } from '@opentelemetry/api-metrics'; +import { diag, ValueType } from '@opentelemetry/api'; import { MetricStorage } from '../../src/state/MetricStorage'; import { HrTime } from '@opentelemetry/api'; import { MetricCollectorHandle } from '../../src/state/MetricCollector'; import { MetricData, InstrumentDescriptor, InstrumentType } from '../../src'; import { Maybe } from '../../src/utils'; -import * as api from '@opentelemetry/api'; import * as assert from 'assert'; import * as sinon from 'sinon'; import { @@ -31,7 +30,8 @@ import { } from '../../src/view/RegistrationConflicts'; class TestMetricStorage extends MetricStorage { - collect(collector: MetricCollectorHandle, + collect( + collector: MetricCollectorHandle, collectors: MetricCollectorHandle[], collectionTime: HrTime, ): Maybe { @@ -43,15 +43,26 @@ describe('MetricStorageRegistry', () => { let spyLoggerWarn: sinon.SinonStub<[message: string, ...args: unknown[]], void>; beforeEach(() => { - spyLoggerWarn = sinon.stub(api.diag, 'warn'); + spyLoggerWarn = sinon.stub(diag, 'warn'); }); afterEach(() => { sinon.restore(); }); + const collectorHandle: MetricCollectorHandle = { + selectAggregationTemporality: () => { + throw new Error('should not be invoked'); + }, + }; + const collectorHandle2: MetricCollectorHandle = { + selectAggregationTemporality: () => { + throw new Error('should not be invoked'); + }, + }; + describe('register', () => { - it('should register MetricStorage if it does not exist', () => { + it('should register MetricStorage', () => { const registry = new MetricStorageRegistry(); const storage = new TestMetricStorage({ name: 'instrument', @@ -61,34 +72,65 @@ describe('MetricStorageRegistry', () => { valueType: ValueType.DOUBLE }); - const registeredStorage = registry.register(storage); - const registeredStorages = registry.getStorages(); + registry.register(storage); + const registeredStorages = registry.getStorages(collectorHandle); - // returned the same storage - assert.strictEqual(registeredStorage, storage); - // registered the actual storage + // registered the storage. assert.deepStrictEqual([storage], registeredStorages); - // no warning logs written - assert.strictEqual(spyLoggerWarn.args.length, 0); }); + }); - function testConflictingRegistration(existingDescriptor: InstrumentDescriptor, + describe('registerForCollector', () => { + it('should register MetricStorage for each collector', () => { + const registry = new MetricStorageRegistry(); + const storage = new TestMetricStorage({ + name: 'instrument', + type: InstrumentType.COUNTER, + description: 'description', + unit: '1', + valueType: ValueType.DOUBLE + }); + const storage2 = new TestMetricStorage({ + name: 'instrument2', + type: InstrumentType.COUNTER, + description: 'description', + unit: '1', + valueType: ValueType.DOUBLE + }); + + registry.registerForCollector(collectorHandle, storage); + registry.registerForCollector(collectorHandle2, storage); + registry.registerForCollector(collectorHandle2, storage2); + + assert.deepStrictEqual(registry.getStorages(collectorHandle), [storage]); + assert.deepStrictEqual(registry.getStorages(collectorHandle2), [storage, storage2]); + }); + }); + + describe('findOrUpdateCompatibleStorage', () => { + function testConflictingRegistration( + existingDescriptor: InstrumentDescriptor, otherDescriptor: InstrumentDescriptor, - expectedLog: string) { + expectedLog: string + ) { const registry = new MetricStorageRegistry(); const storage = new TestMetricStorage(existingDescriptor); const otherStorage = new TestMetricStorage(otherDescriptor); - assert.strictEqual(registry.register(storage), storage); - assert.strictEqual(registry.register(otherStorage), otherStorage); - const registeredStorages = registry.getStorages(); + assert.strictEqual(registry.findOrUpdateCompatibleStorage(existingDescriptor), null); + registry.register(storage); + assertLogNotCalled(); - // registered both storages - assert.deepStrictEqual([storage, otherStorage], registeredStorages); + assert.strictEqual(registry.findOrUpdateCompatibleStorage(otherDescriptor), null); // warned assertLogCalledOnce(); assertFirstLogContains(expectedLog); + registry.register(otherStorage); + + // registered both storages + const registeredStorages = registry.getStorages(collectorHandle); + assert.deepStrictEqual([storage, otherStorage], registeredStorages); } it('warn when instrument with same name and different type is already registered', () => { @@ -179,17 +221,14 @@ describe('MetricStorageRegistry', () => { const storage = new TestMetricStorage(existingDescriptor); const otherStorage = new TestMetricStorage(otherDescriptor); - // returns the first registered storage. - assert.strictEqual(registry.register(storage), storage); - // returns the original storage - assert.strictEqual(registry.register(otherStorage), storage); + // register the first storage. + assert.strictEqual(registry.findOrUpdateCompatibleStorage(existingDescriptor), null); + registry.register(storage); + // register the second storage. + assert.strictEqual(registry.findOrUpdateCompatibleStorage(otherDescriptor), storage); // original storage now has the updated (longer) description. assert.strictEqual(otherStorage.getInstrumentDescriptor().description, otherDescriptor.description); - const registeredStorages = registry.getStorages(); - - // only the original storage has been added - assert.deepStrictEqual([storage], registeredStorages); // log called exactly once assertLogCalledOnce(); // added resolution recipe to the log @@ -207,14 +246,9 @@ describe('MetricStorageRegistry', () => { }; const storage = new TestMetricStorage(descriptor); - const otherStorage = new TestMetricStorage(descriptor); - assert.strictEqual(registry.register(storage), storage); - assert.strictEqual(registry.register(otherStorage), storage); - const registeredStorages = registry.getStorages(); - - // registered the actual storage, but not more than that. - assert.deepStrictEqual([storage], registeredStorages); + registry.register(storage); + assert.strictEqual(registry.findOrUpdateCompatibleStorage(descriptor), storage); }); it('should return the existing instrument if a compatible sync instrument is already registered', () => { @@ -228,18 +262,15 @@ describe('MetricStorageRegistry', () => { }; const storage = new TestMetricStorage(descriptor); - const otherStorage = new TestMetricStorage(descriptor); registry.register(storage); - const previouslyRegisteredStorage = registry.register(otherStorage); - const registeredStorages = registry.getStorages(); - - // returned undefined - assert.strictEqual(previouslyRegisteredStorage, storage); - // registered the actual storage, but not more than that. - assert.deepStrictEqual([storage], registeredStorages); + assert.strictEqual(registry.findOrUpdateCompatibleStorage(descriptor), storage); }); + function assertLogNotCalled() { + assert.strictEqual(spyLoggerWarn.args.length, 0); + } + function assertLogCalledOnce() { assert.strictEqual(spyLoggerWarn.args.length, 1); } @@ -248,4 +279,63 @@ describe('MetricStorageRegistry', () => { assert.ok(spyLoggerWarn.args[0].includes(expectedString), 'Logs did not include: ' + expectedString); } }); + + describe('findOrUpdateCompatibleCollectorStorage', () => { + it('register conflicting metric storages for collector', () => { + const existingDescriptor = { + name: 'instrument', + type: InstrumentType.COUNTER, + description: 'description', + unit: '1', + valueType: ValueType.DOUBLE + }; + + const otherDescriptor = { + name: 'instrument', + type: InstrumentType.UP_DOWN_COUNTER, + description: 'description', + unit: '1', + valueType: ValueType.DOUBLE + }; + + const registry = new MetricStorageRegistry(); + + const storage = new TestMetricStorage(existingDescriptor); + const otherStorage = new TestMetricStorage(otherDescriptor); + + assert.strictEqual(registry.findOrUpdateCompatibleCollectorStorage(collectorHandle, existingDescriptor), null); + registry.registerForCollector(collectorHandle, storage); + + // Should not return an existing metric storage. + assert.strictEqual(registry.findOrUpdateCompatibleCollectorStorage(collectorHandle, otherDescriptor), null); + registry.registerForCollector(collectorHandle, otherStorage); + + // registered both storages + const registeredStorages = registry.getStorages(collectorHandle); + assert.deepStrictEqual([storage, otherStorage], registeredStorages); + }); + + it('register the same metric storage for each collector', () => { + const descriptor = { + name: 'instrument', + type: InstrumentType.COUNTER, + description: 'description', + unit: '1', + valueType: ValueType.DOUBLE + }; + const registry = new MetricStorageRegistry(); + + const storage = new TestMetricStorage(descriptor); + + assert.strictEqual(registry.findOrUpdateCompatibleCollectorStorage(collectorHandle, descriptor), null); + registry.registerForCollector(collectorHandle, storage); + + assert.strictEqual(registry.findOrUpdateCompatibleCollectorStorage(collectorHandle2, descriptor), null); + registry.registerForCollector(collectorHandle2, storage); + + // registered the storage for each collector + assert.deepStrictEqual(registry.getStorages(collectorHandle), [storage]); + assert.deepStrictEqual(registry.getStorages(collectorHandle2), [storage]); + }); + }); }); diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/state/MultiWritableMetricStorage.test.ts b/packages/sdk-metrics/test/state/MultiWritableMetricStorage.test.ts similarity index 97% rename from experimental/packages/opentelemetry-sdk-metrics/test/state/MultiWritableMetricStorage.test.ts rename to packages/sdk-metrics/test/state/MultiWritableMetricStorage.test.ts index e91c952e69..1ccae07723 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/test/state/MultiWritableMetricStorage.test.ts +++ b/packages/sdk-metrics/test/state/MultiWritableMetricStorage.test.ts @@ -15,7 +15,7 @@ */ import * as api from '@opentelemetry/api'; -import { MetricAttributes } from '@opentelemetry/api-metrics'; +import { MetricAttributes } from '@opentelemetry/api'; import { hrTime } from '@opentelemetry/core'; import * as assert from 'assert'; import { MultiMetricStorage } from '../../src/state/MultiWritableMetricStorage'; diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/state/ObservableRegistry.test.ts b/packages/sdk-metrics/test/state/ObservableRegistry.test.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/state/ObservableRegistry.test.ts rename to packages/sdk-metrics/test/state/ObservableRegistry.test.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/state/SyncMetricStorage.test.ts b/packages/sdk-metrics/test/state/SyncMetricStorage.test.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/state/SyncMetricStorage.test.ts rename to packages/sdk-metrics/test/state/SyncMetricStorage.test.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/state/TemporalMetricProcessor.test.ts b/packages/sdk-metrics/test/state/TemporalMetricProcessor.test.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/state/TemporalMetricProcessor.test.ts rename to packages/sdk-metrics/test/state/TemporalMetricProcessor.test.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/test-utils.ts b/packages/sdk-metrics/test/test-utils.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/test-utils.ts rename to packages/sdk-metrics/test/test-utils.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/util.ts b/packages/sdk-metrics/test/util.ts similarity index 97% rename from experimental/packages/opentelemetry-sdk-metrics/test/util.ts rename to packages/sdk-metrics/test/util.ts index 87c6184e37..7227698a93 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/test/util.ts +++ b/packages/sdk-metrics/test/util.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import * as api from '@opentelemetry/api'; import { + Context, BatchObservableCallback, MetricAttributes, ObservableCallback, ValueType, -} from '@opentelemetry/api-metrics'; +} from '@opentelemetry/api'; import { InstrumentationScope } from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; import * as assert from 'assert'; @@ -40,12 +40,12 @@ export type Measurement = { value: number; // TODO: use common attributes attributes: MetricAttributes - context?: api.Context; + context?: Context; }; -export const defaultResource = new Resource({ +export const defaultResource = Resource.default().merge(new Resource({ resourceKey: 'my-resource', -}); +})); export const defaultInstrumentDescriptor: InstrumentDescriptor = { name: 'default_metric', diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/utils.test.ts b/packages/sdk-metrics/test/utils.test.ts similarity index 97% rename from experimental/packages/opentelemetry-sdk-metrics/test/utils.test.ts rename to packages/sdk-metrics/test/utils.test.ts index 07731a5376..2bf818088a 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/test/utils.test.ts +++ b/packages/sdk-metrics/test/utils.test.ts @@ -18,7 +18,7 @@ import * as sinon from 'sinon'; import * as assert from 'assert'; import { callWithTimeout, hashAttributes, TimeoutError } from '../src/utils'; import { assertRejects } from './test-utils'; -import { MetricAttributes } from '@opentelemetry/api-metrics'; +import { MetricAttributes } from '@opentelemetry/api'; describe('utils', () => { afterEach(() => { diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/view/Aggregation.test.ts b/packages/sdk-metrics/test/view/Aggregation.test.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/view/Aggregation.test.ts rename to packages/sdk-metrics/test/view/Aggregation.test.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/view/AttributesProcessor.test.ts b/packages/sdk-metrics/test/view/AttributesProcessor.test.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/view/AttributesProcessor.test.ts rename to packages/sdk-metrics/test/view/AttributesProcessor.test.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/view/Predicate.test.ts b/packages/sdk-metrics/test/view/Predicate.test.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/view/Predicate.test.ts rename to packages/sdk-metrics/test/view/Predicate.test.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/view/View.test.ts b/packages/sdk-metrics/test/view/View.test.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-metrics/test/view/View.test.ts rename to packages/sdk-metrics/test/view/View.test.ts diff --git a/experimental/packages/opentelemetry-sdk-metrics/test/view/ViewRegistry.test.ts b/packages/sdk-metrics/test/view/ViewRegistry.test.ts similarity index 92% rename from experimental/packages/opentelemetry-sdk-metrics/test/view/ViewRegistry.test.ts rename to packages/sdk-metrics/test/view/ViewRegistry.test.ts index a56d7b65e0..68f9f27072 100644 --- a/experimental/packages/opentelemetry-sdk-metrics/test/view/ViewRegistry.test.ts +++ b/packages/sdk-metrics/test/view/ViewRegistry.test.ts @@ -23,13 +23,6 @@ import { View } from '../../src'; describe('ViewRegistry', () => { describe('findViews', () => { - it('should return default view if no view registered', () => { - const registry = new ViewRegistry(); - const views = registry.findViews(defaultInstrumentDescriptor, defaultInstrumentationScope); - assert.strictEqual(views.length, 1); - assert.strictEqual(views[0], ViewRegistry['DEFAULT_VIEW']); - }); - describe('InstrumentSelector', () => { it('should match view with instrument name', () => { const registry = new ViewRegistry(); diff --git a/packages/sdk-metrics/tsconfig.all.json b/packages/sdk-metrics/tsconfig.all.json new file mode 100644 index 0000000000..4d28a993d3 --- /dev/null +++ b/packages/sdk-metrics/tsconfig.all.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "references": [ + { "path": "./tsconfig.json" }, + { "path": "./tsconfig.esm.json" }, + { "path": "./tsconfig.esnext.json" } + ] +} diff --git a/packages/sdk-metrics/tsconfig.esm.json b/packages/sdk-metrics/tsconfig.esm.json new file mode 100644 index 0000000000..a94adff6aa --- /dev/null +++ b/packages/sdk-metrics/tsconfig.esm.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.esm.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build/esm", + "tsBuildInfoFile": "build/esm/tsconfig.esm.tsbuildinfo" + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/sdk-metrics/tsconfig.esnext.json b/packages/sdk-metrics/tsconfig.esnext.json new file mode 100644 index 0000000000..65a918cf6b --- /dev/null +++ b/packages/sdk-metrics/tsconfig.esnext.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.esnext.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build/esnext", + "tsBuildInfoFile": "build/esnext/tsconfig.esnext.tsbuildinfo" + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/sdk-metrics/tsconfig.json b/packages/sdk-metrics/tsconfig.json new file mode 100644 index 0000000000..9b42c9bb79 --- /dev/null +++ b/packages/sdk-metrics/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ], + "references": [ + { + "path": "../../api" + }, + { + "path": "../opentelemetry-core" + }, + { + "path": "../opentelemetry-resources" + } + ] +} diff --git a/packages/template/package.json b/packages/template/package.json index e34c60e575..6342f2876c 100644 --- a/packages/template/package.json +++ b/packages/template/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/template", - "version": "1.6.0", + "version": "1.8.0", "private": true, "publishConfig": { "access": "restricted" @@ -68,6 +68,7 @@ "LICENSE", "README.md" ], + "sideEffects": false, "Add these to files if browser is supported": [ "build/esm/**/*.js", "build/esm/**/*.js.map", diff --git a/renovate.json b/renovate.json index f284cae23d..0e91ce6425 100644 --- a/renovate.json +++ b/renovate.json @@ -6,17 +6,13 @@ "groupSlug": "all-minor-patch", "matchUpdateTypes": ["patch", "minor"] }, - { - "matchPackageNames": ["@opentelemetry/api"], - "rangeStrategy": "bump" - }, { "matchPaths": ["backwards-compatibility"], "matchPackageNames": ["@types/node"], "enabled": false } ], - "ignoreDeps": ["gcp-metadata", "got", "mocha", "husky", "karma-webpack"], + "ignoreDeps": ["gcp-metadata", "got", "mocha", "husky", "karma-webpack", "@opentelemetry/api"], "assignees": ["@blumamir", "@dyladan", "@legendecas", "@Rauno56", "@vmarchaud"], "schedule": ["before 3am on Friday"], "labels": ["dependencies"] diff --git a/selenium-tests/package.json b/selenium-tests/package.json index a276f0e84c..7ede549bee 100644 --- a/selenium-tests/package.json +++ b/selenium-tests/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/selenium-tests", - "version": "1.6.0", + "version": "1.8.0", "private": true, "description": "OpenTelemetry Selenium Tests", "main": "index.js", @@ -40,7 +40,7 @@ "babel-loader": "8.2.3", "babel-polyfill": "6.26.0", "browserstack-local": "1.4.8", - "chromedriver": "104.0.0", + "chromedriver": "107.0.3", "dotenv": "16.0.0", "fast-safe-stringify": "2.1.1", "geckodriver": "3.0.1", @@ -56,16 +56,16 @@ "@opentelemetry/api": "^1.0.0" }, "dependencies": { - "@opentelemetry/context-zone-peer-dep": "1.6.0", - "@opentelemetry/core": "1.6.0", - "@opentelemetry/exporter-trace-otlp-http": "0.32.0", - "@opentelemetry/exporter-zipkin": "1.6.0", - "@opentelemetry/instrumentation": "0.32.0", - "@opentelemetry/instrumentation-fetch": "0.32.0", - "@opentelemetry/instrumentation-xml-http-request": "0.32.0", - "@opentelemetry/sdk-metrics": "0.32.0", - "@opentelemetry/sdk-trace-base": "1.6.0", - "@opentelemetry/sdk-trace-web": "1.6.0", + "@opentelemetry/context-zone-peer-dep": "1.8.0", + "@opentelemetry/core": "1.8.0", + "@opentelemetry/exporter-trace-otlp-http": "0.34.0", + "@opentelemetry/exporter-zipkin": "1.8.0", + "@opentelemetry/instrumentation": "0.34.0", + "@opentelemetry/instrumentation-fetch": "0.34.0", + "@opentelemetry/instrumentation-xml-http-request": "0.34.0", + "@opentelemetry/sdk-metrics": "1.8.0", + "@opentelemetry/sdk-trace-base": "1.8.0", + "@opentelemetry/sdk-trace-web": "1.8.0", "zone.js": "0.11.4" } } diff --git a/tsconfig.base.json b/tsconfig.base.json index ca44078430..cbafb67678 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -16,6 +16,7 @@ "noImplicitReturns": true, "noUnusedLocals": true, "pretty": true, + "skipLibCheck": true, "sourceMap": true, "strict": true, "strictNullChecks": true, diff --git a/tsconfig.json b/tsconfig.json index cdbf4c4272..09ccfeb07f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,22 +4,29 @@ "typedocOptions": { "entryPointStrategy": "packages", "entryPoints": [ + "api", + "experimental/packages/api-logs", + "experimental/packages/exporter-trace-otlp-grpc", + "experimental/packages/exporter-trace-otlp-http", + "experimental/packages/exporter-trace-otlp-proto", + "experimental/packages/opentelemetry-browser-detector", "experimental/packages/opentelemetry-exporter-metrics-otlp-grpc", "experimental/packages/opentelemetry-exporter-metrics-otlp-http", "experimental/packages/opentelemetry-exporter-metrics-otlp-proto", "experimental/packages/opentelemetry-exporter-prometheus", - "experimental/packages/opentelemetry-instrumentation", "experimental/packages/opentelemetry-instrumentation-fetch", "experimental/packages/opentelemetry-instrumentation-grpc", "experimental/packages/opentelemetry-instrumentation-http", "experimental/packages/opentelemetry-instrumentation-xml-http-request", + "experimental/packages/opentelemetry-instrumentation", "experimental/packages/opentelemetry-sdk-node", - "packages/exporter-trace-otlp-grpc", - "packages/exporter-trace-otlp-http", - "packages/exporter-trace-otlp-proto", + "experimental/packages/otlp-exporter-base", + "experimental/packages/otlp-grpc-exporter-base", + "experimental/packages/otlp-proto-exporter-base", + "experimental/packages/otlp-transformer", "packages/opentelemetry-context-async-hooks", - "packages/opentelemetry-context-zone", "packages/opentelemetry-context-zone-peer-dep", + "packages/opentelemetry-context-zone", "packages/opentelemetry-core", "packages/opentelemetry-exporter-jaeger", "packages/opentelemetry-exporter-zipkin", @@ -30,7 +37,8 @@ "packages/opentelemetry-sdk-trace-node", "packages/opentelemetry-sdk-trace-web", "packages/opentelemetry-semantic-conventions", - "packages/opentelemetry-shim-opentracing" + "packages/opentelemetry-shim-opentracing", + "packages/sdk-metrics" ], "out": "docs", "exclude": [ @@ -43,6 +51,9 @@ "excludePrivate": true }, "references": [ + { + "path": "api" + }, { "path": "packages/opentelemetry-context-async-hooks" }, @@ -85,9 +96,15 @@ { "path": "packages/opentelemetry-shim-opentracing" }, + { + "path": "packages/sdk-metrics" + }, { "path": "packages/template" }, + { + "path": "experimental/packages/api-logs" + }, { "path": "experimental/packages/exporter-trace-otlp-grpc" }, @@ -98,7 +115,7 @@ "path": "experimental/packages/exporter-trace-otlp-proto" }, { - "path": "experimental/packages/opentelemetry-api-metrics" + "path": "experimental/packages/opentelemetry-browser-detector" }, { "path": "experimental/packages/opentelemetry-exporter-metrics-otlp-grpc" @@ -127,9 +144,6 @@ { "path": "experimental/packages/opentelemetry-instrumentation" }, - { - "path": "experimental/packages/opentelemetry-sdk-metrics" - }, { "path": "experimental/packages/opentelemetry-sdk-node" },