Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions benchmark/otel/http.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use strict';

// Measures the per-request overhead of the built-in OpenTelemetry tracing
// subsystem on the http hot path. Requests are issued sequentially over a
// single keep-alive connection so that the difference between the traced
// and untraced configurations reflects per-request work (two spans per
// request: one SERVER, one CLIENT), not scheduling or connection noise.
//
// When tracing is enabled, spans are exported to an unreachable endpoint:
// the measurement covers span creation and serialization but not collector
// latency. Export failures are reported as throttled warnings.
//
// Run with: node benchmark/otel/http.js

const common = require('../common');

const bench = common.createBenchmark(main, {
tracing: [0, 1],
n: [1e5],
}, {
flags: ['--expose-internals'],
});

function main({ tracing, n }) {
if (tracing) {
const otel = require('internal/otel/core');
otel.start({ endpoint: 'http://127.0.0.1:1' });
}

const http = require('http');

const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('ok');
});

server.listen(0, '127.0.0.1', () => {
const port = server.address().port;
const agent = new http.Agent({ keepAlive: true, maxSockets: 1 });

let completed = 0;
let measured = false;
const kWarmup = 100;

function request(done) {
http.get({ host: '127.0.0.1', port, agent, path: '/bench' }, (res) => {
res.resume();
res.on('end', done);
});
}

request(function done() {
if (!measured) {
completed++;
if (completed < kWarmup) {
request(done);
return;
}
// Warmup is done; the measured requests start now.
measured = true;
completed = 0;
bench.start();
} else {
completed++;
}
if (completed >= n) {
bench.end(n);
server.close();
agent.destroy();
return;
}
request(done);
});
});
}
85 changes: 85 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1545,6 +1545,19 @@ added:
Enable experimental support for the network inspection with Chrome DevTools.

### `--experimental-otel`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
Enable the experimental built-in OpenTelemetry tracing subsystem. When
enabled, tracing is activated by setting the [`NODE_OTEL`][] or
[`NODE_OTEL_ENDPOINT`][] environment variables. See the [OpenTelemetry][]
documentation for details.

### `--experimental-package-map=<path>`

<!-- YAML
Expand Down Expand Up @@ -4254,6 +4267,7 @@ one is included in the list below.
* `--experimental-json-modules`
* `--experimental-loader`
* `--experimental-modules`
* `--experimental-otel`
* `--experimental-package-map`
* `--experimental-print-required-tla`
* `--experimental-quic`
Expand Down Expand Up @@ -4420,6 +4434,73 @@ V8 options that are allowed are:

<!-- node-options-others end -->

### `NODE_OTEL=value`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
When set to `1` while the [`--experimental-otel`][] flag is
enabled, activates the built-in OpenTelemetry tracing subsystem using the
default collector endpoint (`http://localhost:4318`). Values other than
`1` are ignored. If `NODE_OTEL_ENDPOINT` is also set, it takes precedence
for the endpoint. See the [OpenTelemetry][] documentation for details.

### `NODE_OTEL_ENDPOINT=url`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
When set to a non-empty value while the [`--experimental-otel`][] flag is
enabled, activates the built-in OpenTelemetry tracing subsystem and directs
spans to the specified OTLP/HTTP collector endpoint. The endpoint must be
the base URL of the collector: any path present in the endpoint is
replaced with `/v1/traces`, and an endpoint that already ends with
`/v1/traces` is used as is. When only `NODE_OTEL=1` is set, the default
collector endpoint (`http://localhost:4318`) is used. If `NODE_OTEL` is
also set, `NODE_OTEL_ENDPOINT` takes precedence for the endpoint. See the
[OpenTelemetry][] documentation for details.

### `NODE_OTEL_FILTER=module[,…]`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
Comma-separated list of core modules to instrument when OpenTelemetry tracing
is active. When not set, all supported modules are instrumented. Supported
values: `node:http`, `node:undici`, `node:fetch`. See the [OpenTelemetry][]
documentation for details.

### `NODE_OTEL_FLUSH_INTERVAL=milliseconds`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
Interval in milliseconds between periodic flushes of buffered spans to the
collector. Must be a positive integer. **Default:** `10000`.

### `NODE_OTEL_MAX_BUFFER_SIZE=number`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
Maximum number of spans buffered in memory before an immediate flush to the
collector is triggered. Must be a positive integer. **Default:** `100`.

### `NODE_PATH=path[:…]`

<!-- YAML
Expand Down Expand Up @@ -4868,6 +4949,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[Navigator API]: globals.md#navigator
[Node.js issue tracker]: https://github.com/nodejs/node/issues
[OSSL_PROVIDER-legacy]: https://www.openssl.org/docs/man3.0/man7/OSSL_PROVIDER-legacy.html
[OpenTelemetry]: otel.md
[Package maps]: packages.md#package-maps
[Permission Model]: permissions.md#permission-model
[REPL]: repl.md
Expand Down Expand Up @@ -4897,6 +4979,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`--enable-fips`]: #--enable-fips
[`--env-file-if-exists`]: #--env-file-if-existsfile
[`--env-file`]: #--env-filefile
[`--experimental-otel`]: #--experimental-otel
[`--experimental-sea-config`]: single-executable-applications.md#1-generating-single-executable-preparation-blobs
[`--experimental-vfs`]: #--experimental-vfs
[`--heap-prof-dir`]: #--heap-prof-dir
Expand All @@ -4918,6 +5001,8 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`ERR_INVALID_TYPESCRIPT_SYNTAX`]: errors.md#err_invalid_typescript_syntax
[`ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`]: errors.md#err_unsupported_typescript_syntax
[`NODE_OPTIONS`]: #node_optionsoptions
[`NODE_OTEL_ENDPOINT`]: #node_otel_endpointurl
[`NODE_OTEL`]: #node_otelvalue
[`NODE_USE_ENV_PROXY=1`]: #node_use_env_proxy1
[`NODE_V8_COVERAGE=dir`]: #node_v8_coveragedir
[`NO_COLOR`]: https://no-color.org
Expand Down
1 change: 1 addition & 0 deletions doc/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
* [Modules: Packages](packages.md)
* [Modules: TypeScript](typescript.md)
* [Net](net.md)
* [OpenTelemetry](otel.md)
* [OS](os.md)
* [Path](path.md)
* [Performance hooks](perf_hooks.md)
Expand Down
187 changes: 187 additions & 0 deletions doc/api/otel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# OpenTelemetry

<!--introduced_in=REPLACEME-->

<!-- type=misc -->

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
Node.js includes an experimental built-in [OpenTelemetry][] tracing subsystem.
When activated, the subsystem automatically creates spans for HTTP server and
client operations and exports them using the [OTLP/HTTP JSON][] protocol.

The subsystem is experimental and must be enabled with the
`--experimental-otel` flag. It is activated only via environment
variables, listed below. There is currently no programmatic API for
activating or configuring the subsystem, and no support for custom
instrumentations; these may be added in the future.

## Limitations

The subsystem is independent of the OpenTelemetry JavaScript packages. It
is not interoperable with `@opentelemetry/api`, and spans created by one
are not visible to the other. Running both in the same process produces
duplicate trace and span IDs. The built-in instrumentation also
overwrites the `traceparent` (and, when present, `tracestate`) header on
outgoing HTTP requests, discarding whatever a userland propagator may
have set. Users should run either the built-in subsystem or a userland
OpenTelemetry SDK, not both.
Comment on lines +25 to +32

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I wonder what the purpose of this is. From my perspective, this feature should work in one of two ways:

  1. Provides an implementation of the API such that compliant instrumentations can register with it and it delivers the appropriate signals data to a collector.
  2. Registers with whatever OTEL implementation is present such that the signals data is inlined correctly.

With the caveats highlighted in this paragraph, I'm just not clear what this accomplishes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to start somewhere. I can see this eventually working with userland sdk's before it comes out of experimental but in order to make progress at all we should work incrementally here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. This is a step 1 (or maybe a step 0). It serves two purposes:

  1. Provide OOTB (limited) tracing with no dependencies or code changes.
  2. Provide a baseline implementation of the data model and egress pipeline in core. This can be followed up in future PRs with all kinds of ideas, like:
    • A TracerProvider implementation, so as to support @opentelemetry/api
    • Prototyping a much more minimal interface, like tracing in Rust, which could then be adopted as official by OTel (like .NET's prior art).
    • Providing a default core for OTel SDK.
    • All kinds of other options!

Basically this PR exists to eliminate the more contentious parts of my previous one, so that we can avoid blocking this core functionality while we deliberate on that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might miss some contexts here about the goal of OpenTelemetry support. OpenTelemetry specification defines a baseline data model and egress pipeline. Would the goal of this built-in support comply with the https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would the goal of this built-in support comply with the open-telemetry/opentelemetry-specification@main/specification/trace/sdk.md ?

Eventually, yes, that's an option for future PRs, as indicated by the first bullet point in the purpose number two in my previous comment. This PR only creates OTLP tracing data based on inbound and outbound HTTP calls, and ships it off to a collector. All other concerns/discussions are left for future PRs, and the feature should probably stay experimental until those are figured out.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for clarifying! I'm trying to understand the goal of this PR and the future path on a built-in Observability support. So I'd like to make sure the goal is clear even if this is an initial experimental PR.

The PR might be good on its own, but a future work that may create incompat or create preferential/minimal API towards particular observability data backend vendors must be a non-starter. I'd be wary of future paths like "I don't use xxx so let's not implement xxx in OpenTelemetry", or "I need xxx but it's not in OpenTelemetry, let's skip OpenTelemetry and do xxx".

OpenTelemetry has been the de-facto standard and a collaborative project between observability backend vendors, so I'd appreciate that a vendor neutral path is the goal of Node.js built-in observability support.


The subsystem runs in the main thread only. Worker threads
(`node:worker_threads`) are not traced, even though they inherit the
environment variables.

Buffered spans are exported periodically, when the internal buffer fills,
and when the event loop is about to drain. Spans that are still buffered
when `process.exit()` is called explicitly are lost, because explicit
exits do not run the exit-time flush.

## Environment variables

### `NODE_OTEL`

When set to `1`, activates the tracing subsystem using the default
collector endpoint (`http://localhost:4318`). Values other than `1` are
ignored. If `NODE_OTEL_ENDPOINT` is also set, it takes precedence for the
endpoint.

```bash
node --experimental-otel app.js # NODE_OTEL=1 set in the environment
```

### `NODE_OTEL_ENDPOINT`

When set to a non-empty value, activates the tracing subsystem and directs
spans to the specified OTLP collector endpoint. The endpoint should be the
base URL of an OTLP/HTTP collector (e.g. `http://localhost:4318`) without
a path: any path present in the endpoint is replaced with `/v1/traces`,
and an endpoint that already ends with `/v1/traces` is used as is. When
only `NODE_OTEL=1` is set, the default collector endpoint
(`http://localhost:4318`) is used.

```bash
NODE_OTEL_ENDPOINT=http://collector.example.com:4318 \
node --experimental-otel app.js
```
Comment thread
atlowChemi marked this conversation as resolved.

### `NODE_OTEL_FILTER`

Accepts a comma-separated list of core modules to instrument. When not set, all
supported modules are instrumented. For example, setting
`NODE_OTEL_FILTER=node:http` would enable tracing only for the `node:http`
module.

Supported module filter values:

* `node:http` — HTTP server and client operations
* `node:undici` — Undici HTTP client operations
* `node:fetch` — Fetch API operations (alias for undici)

### `NODE_OTEL_MAX_BUFFER_SIZE`

Maximum number of spans buffered in memory before an immediate flush to the
collector is triggered. Must be a positive integer. **Default:** `100`.

### `NODE_OTEL_FLUSH_INTERVAL`

Interval in milliseconds between periodic flushes of buffered spans to the
collector. Must be a positive integer. **Default:** `10000`.

### `OTEL_SERVICE_NAME`

Standard OpenTelemetry environment variable used to set the service name in
exported resource attributes. Defaults to `unknown_service:node`, per the
OpenTelemetry [semantic conventions][] for low-cardinality service names.

## Instrumented operations

When the subsystem is active, spans are automatically created for the
following operations. Per the OpenTelemetry [semantic conventions][], span
names must be low cardinality; Node.js core has no route concept, so spans
are named `{method}` and the request details live in the attributes.

### HTTP server

A span with kind `SERVER` is created for each incoming HTTP request. The span
starts when the request is received and ends when the response finishes. If the
client disconnects before the response completes, the span ends with an error
status.

Server spans receive error status (`STATUS_ERROR`) for 5xx response codes. 4xx
responses are not treated as server errors per OpenTelemetry semantic
conventions.

Attributes set on server spans:
Comment thread
legendecas marked this conversation as resolved.

| Attribute | Description | Condition |
| --------------------------- | -------------------------------- | ----------------------------- |
| `http.request.method` | HTTP method (e.g. `GET`, `POST`) | Always |
| `url.path` | Request URL path (without query) | Always |
| `url.query` | Query string (without `?`) | When query string is present |
| `url.scheme` | `http` or `https` | Always |
| `server.address` | Host header value | When `Host` header is present |
| `network.protocol.version` | HTTP version (e.g. `1.1`) | Always |
| `http.response.status_code` | Response status code | When response finishes |
| `error.type` | HTTP status code as string | On 5xx responses |

### HTTP client

A span with kind `CLIENT` is created for each outgoing HTTP request made via
`node:http`. The span starts when the request is created and ends when the
response body completes or an error occurs.

Client spans receive error status (`STATUS_ERROR`) for 4xx and 5xx response
codes. On connection errors, an `exception` event is added to the span with
`exception.type`, `exception.message`, and `exception.stacktrace` attributes.

Attributes set on client spans:

| Attribute | Description | Condition |
| --------------------------- | ------------------------- | ------------------------- |
| `http.request.method` | HTTP method | Always |
| `url.full` | Full request URL | Always |
| `server.address` | Target host | Always |
| `server.port` | Target port | When available |
| `http.response.status_code` | Response status code | When response is received |
| `network.protocol.version` | HTTP version | When response is received |
| `error.type` | Status code or error name | On 4xx/5xx or errors |

### Undici/Fetch client

A span with kind `CLIENT` is created for each outgoing request made via
`fetch()` or undici's `request()`. Error status, `exception` event behavior,
and span end timing are the same as for HTTP client spans above.

Attributes set on undici/fetch client spans:

| Attribute | Description | Condition |
| --------------------------- | ------------------------- | ------------------------- |
| `http.request.method` | HTTP method | Always |
| `url.full` | Full request URL | Always |
| `server.address` | Target origin | Always |
| `http.response.status_code` | Response status code | When response is received |
| `error.type` | Status code or error name | On 4xx/5xx or errors |

## W3C Trace Context propagation

The tracing subsystem automatically propagates [W3C Trace Context][] across HTTP
boundaries:

* **Incoming requests**: The `traceparent` header is read from incoming HTTP
requests, and child spans created during request processing inherit the
trace ID. `traceparent` values using a version other than `00` are
rejected, and a fresh trace is started instead. The `tracestate` header,
when present, is attached to the span and forwarded verbatim to outgoing
requests. It is never parsed or modified.
* **Outgoing requests**: The `traceparent` header is injected into outgoing
HTTP and undici/fetch requests, together with the stored `tracestate`
when one is present, enabling distributed tracing across services.

[OTLP/HTTP JSON]: https://opentelemetry.io/docs/specs/otlp/#otlphttp
[W3C Trace Context]: https://www.w3.org/TR/trace-context/
[OpenTelemetry]: https://opentelemetry.io/
[semantic conventions]: https://opentelemetry.io/docs/specs/semconv/http/http-spans/
Loading
Loading