Skip to content

Commit

Permalink
feat: ESM support for instrumentation (#3698)
Browse files Browse the repository at this point in the history
Co-authored-by: Purvi Kanal <[email protected]>
Co-authored-by: Jamie Danielson <[email protected]>
Co-authored-by: Liz Fong-Jones <[email protected]>
Co-authored-by: Purvi Kanal <[email protected]>
  • Loading branch information
4 people authored May 15, 2023
1 parent bba09c0 commit 422a36a
Show file tree
Hide file tree
Showing 17 changed files with 432 additions and 12 deletions.
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use the latest and greatest features, and best practices.
| [grpc](grpc/) | gRPC Instrumentation to automatically collect trace data and export them to the backend of choice | Intermediate |
| [otlp-exporter-node](otlp-exporter-node/) | This example shows how to use `@opentelemetry/exporter-otlp-http` to instrument a simple Node.js application | Intermediate |
| [opentracing-shim](opentracing-shim/) | This is a simple example that demonstrates how existing OpenTracing instrumentation can be integrated with OpenTelemetry | Intermediate |
| [esm-http-ts](esm-http-ts/) | This is a simple example that demonstrates tracing HTTP request, with an app written in TypeScript and transpiled to ES Modules. | Intermediate |

Examples of experimental packages can be found at [experimental/examples](../experimental/examples).

Expand Down
25 changes: 25 additions & 0 deletions examples/esm-http-ts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Overview

This is a simple example that demonstrates tracing HTTP request, with an app written in TypeScript and transpiled to ES Modules.

## Installation

```sh
# from this directory
npm install
npm run build
npm start
```

In a separate terminal, `curl localhost:3000`.

See two spans in the console (one manual, one for http instrumentation)

## Useful links

- For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
- For more information on OpenTelemetry for Node.js, visit: <https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-node>

## LICENSE

Apache License 2.0
43 changes: 43 additions & 0 deletions examples/esm-http-ts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { trace, DiagConsoleLogger, DiagLogLevel, diag } from '@opentelemetry/api';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import {
ConsoleSpanExporter,
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-base';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import http from 'http';

diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
const tracerProvider = new NodeTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'esm-http-ts-example',
}),
});
const exporter = new ConsoleSpanExporter();
const processor = new SimpleSpanProcessor(exporter);
tracerProvider.addSpanProcessor(processor);
tracerProvider.register();

registerInstrumentations({
instrumentations: [new HttpInstrumentation()],
});

const hostname = '0.0.0.0';
const port = 3000;

const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
const tracer = trace.getTracer('esm-tracer');
tracer.startActiveSpan('manual', span => {
span.end();
});
res.end('Hello, World!\n');
});

server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
42 changes: 42 additions & 0 deletions examples/esm-http-ts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "esm-http-ts",
"private": true,
"version": "0.38.0",
"description": "Example of HTTP integration with OpenTelemetry using ESM and TypeScript",
"main": "build/index.js",
"type": "module",
"scripts": {
"build": "tsc --build",
"start": "node --experimental-loader=@opentelemetry/instrumentation/hook.mjs ./build/index.js"
},
"repository": {
"type": "git",
"url": "git+ssh://[email protected]/open-telemetry/opentelemetry-js.git"
},
"keywords": [
"opentelemetry",
"http",
"tracing",
"esm",
"typescript"
],
"engines": {
"node": ">=14"
},
"author": "OpenTelemetry Authors",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/open-telemetry/opentelemetry-js/issues"
},
"homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/examples/",
"dependencies": {
"@opentelemetry/api": "1.4.0",
"@opentelemetry/exporter-trace-otlp-proto": "0.38.0",
"@opentelemetry/instrumentation": "0.38.0",
"@opentelemetry/instrumentation-http": "0.38.0",
"@opentelemetry/resources": "1.9.1",
"@opentelemetry/sdk-trace-base": "1.9.1",
"@opentelemetry/sdk-trace-node": "1.9.1",
"@opentelemetry/semantic-conventions": "1.9.1"
}
}
25 changes: 25 additions & 0 deletions examples/esm-http-ts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"compilerOptions": {
/* Language and Environment */
"target": "ES2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,

/* Modules */
"module": "ESNext" /* Specify what module code is generated. */,
"rootDir": "." /* Specify the root folder within your source files. */,
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
"resolveJsonModule": true /* Enable importing .json files. */,

/* Emit */
"outDir": "build" /* Specify an output folder for all emitted files. */,

/* Interop Constraints */
"allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,

/* Completeness */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": ["**/*.ts", "**/*.js", "*.config.js"],
"exclude": ["node_modules"]
}
2 changes: 2 additions & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ All notable changes to experimental packages in this project will be documented

### :rocket: (Enhancement)

* feat(instrumentation): add ESM support for instrumentation. [#3698](https://github.com/open-telemetry/opentelemetry-js/pull/3698) @JamieDanielson, @pkanal, @vmarchaud, @lizthegrey, @bengl

### :bug: (Bug Fix)

### :books: (Refine Doc)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
export class HttpInstrumentation extends InstrumentationBase<Http> {
/** keep track on spans not ended */
private readonly _spanNotEnded: WeakSet<Span> = new WeakSet<Span>();
private readonly _version = process.versions.node;
private _headerCapture;
private _httpServerDurationHistogram!: Histogram;
private _httpClientDurationHistogram!: Histogram;
Expand Down Expand Up @@ -112,11 +111,12 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
}

private _getHttpInstrumentation() {
const version = process.versions.node;
return new InstrumentationNodeModuleDefinition<Http>(
'http',
['*'],
moduleExports => {
this._diag.debug(`Applying patch for http@${this._version}`);
this._diag.debug(`Applying patch for http@${version}`);
if (isWrapped(moduleExports.request)) {
this._unwrap(moduleExports, 'request');
}
Expand Down Expand Up @@ -145,7 +145,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
},
moduleExports => {
if (moduleExports === undefined) return;
this._diag.debug(`Removing patch for http@${this._version}`);
this._diag.debug(`Removing patch for http@${version}`);

this._unwrap(moduleExports, 'request');
this._unwrap(moduleExports, 'get');
Expand All @@ -155,11 +155,12 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
}

private _getHttpsInstrumentation() {
const version = process.versions.node;
return new InstrumentationNodeModuleDefinition<Https>(
'https',
['*'],
moduleExports => {
this._diag.debug(`Applying patch for https@${this._version}`);
this._diag.debug(`Applying patch for https@${version}`);
if (isWrapped(moduleExports.request)) {
this._unwrap(moduleExports, 'request');
}
Expand Down Expand Up @@ -188,7 +189,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
},
moduleExports => {
if (moduleExports === undefined) return;
this._diag.debug(`Removing patch for https@${this._version}`);
this._diag.debug(`Removing patch for https@${version}`);

this._unwrap(moduleExports, 'request');
this._unwrap(moduleExports, 'get');
Expand Down
12 changes: 9 additions & 3 deletions experimental/packages/opentelemetry-instrumentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,18 @@ If nothing is specified the global registered provider is used. Usually this is
There might be usecase where someone has the need for more providers within an application. Please note that special care must be takes in such setups
to avoid leaking information from one provider to the other because there are a lot places where e.g. the global `ContextManager` or `Propagator` is used.

## Instrumentation for ES Modules In NodeJS (experimental)

As the module loading mechanism for ESM is different than CJS, you need to select a custom loader so instrumentation can load hook on the esm module it want to patch. To do so, you must provide the `--experimental-loader=@opentelemetry/instrumentation/hook.mjs` flag to the `node` binary. Alternatively you can set the `NODE_OPTIONS` environment variable to `NODE_OPTIONS="--experimental-loader=@opentelemetry/instrumentation/hook.mjs"`.
As the ESM module loader from NodeJS is experimental, so is our support for it. Feel free to provide feedback or report issues about it.

**Note**: ESM Instrumentation is not yet supported for Node 20.

## Limitations

Instrumentations for external modules (e.g. express, mongodb,...) hooks the `require` call. Therefore following conditions need to be met that this mechanism can work:
Instrumentations for external modules (e.g. express, mongodb,...) hooks the `require` call or `import` statement. Therefore following conditions need to be met that this mechanism can work:

* `require` is used. ECMA script modules (using `import`) is not supported as of now
* Instrumentations are registered **before** the module to instrument is `require`ed
* Instrumentations are registered **before** the module to instrument is `require`ed (CJS only)
* modules are not included in a bundle. Tools like `esbuild`, `webpack`, ... usually have some mechanism to exclude specific modules from bundling

## License
Expand Down
28 changes: 28 additions & 0 deletions experimental/packages/opentelemetry-instrumentation/hook.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*!
* Copyright 2021 Datadog, Inc.
* 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.
*/

// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2.0 License.
//
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc.

import {
load,
resolve,
getFormat,
getSource,
} from 'import-in-the-middle/hook.mjs';
export { load, resolve, getFormat, getSource };
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"build/src/**/*.js",
"build/src/**/*.js.map",
"build/src/**/*.d.ts",
"hook.mjs",
"doc",
"LICENSE",
"README.md"
Expand All @@ -47,7 +48,9 @@
"tdd": "npm run tdd:node",
"tdd:node": "npm run test -- --watch-extensions ts --watch",
"tdd:browser": "karma start",
"test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts' --exclude 'test/browser/**/*.ts'",
"test:cjs": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts' --exclude 'test/browser/**/*.ts'",
"test:esm": "nyc node --experimental-loader=./hook.mjs ../../../node_modules/mocha/bin/mocha 'test/node/*.test.mjs' test/node/*.test.mjs",
"test": "npm run test:cjs && npm run test:esm",
"test:browser": "nyc karma start --single-run",
"version": "node ../../../scripts/version-update.js",
"watch": "tsc --build --watch tsconfig.json tsconfig.esm.json tsconfig.esnext.json",
Expand All @@ -68,6 +71,8 @@
"url": "https://github.com/open-telemetry/opentelemetry-js/issues"
},
"dependencies": {
"@types/shimmer": "^1.0.2",
"import-in-the-middle": "1.3.5",
"require-in-the-middle": "^7.1.0",
"semver": "^7.3.2",
"shimmer": "^1.2.1"
Expand All @@ -82,7 +87,6 @@
"@types/mocha": "10.0.0",
"@types/node": "18.6.5",
"@types/semver": "7.3.9",
"@types/shimmer": "1.0.2",
"@types/sinon": "10.0.13",
"@types/webpack-env": "1.16.3",
"babel-loader": "8.2.3",
Expand Down
Loading

0 comments on commit 422a36a

Please sign in to comment.