Repository navigation
lib: add built-in OpenTelemetry tracing #66587
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bengl
wants to merge
1
commit into
nodejs:main
Choose a base branch
from
bengl:bengl/otel-2
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+3,236
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| }); | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
|
||
| 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 | ||
| ``` | ||
|
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: | ||
|
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/ | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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:
With the caveats highlighted in this paragraph, I'm just not clear what this accomplishes.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
TracerProviderimplementation, so as to support@opentelemetry/apitracingin Rust, which could then be adopted as official by OTel (like .NET's prior art).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.
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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.