Login V2: getNodeAutoInstrumentations() loads 45 OpenTelemetry packages on the cold start path
Summary
apps/login/src/instrumentation.node.ts builds its instrumentation list with
getNodeAutoInstrumentations(). That call require()s 40 instrumentation
packages and 5 cloud resource detectors at module load, and Next.js awaits
register() before serving the first request, so the whole set sits on the cold
start path between the port binding and the server answering.
The login app's own dependency surface is much narrower than that set, so most of the loaded instrumentations can never match anything.
Measurement
v4.16.2 login image, container start to first HTTP response, interleaved A/B
between the unchanged image and one importing only the instrumentations the app
uses. Two CPU shapes:
| CPU | Unchanged | Narrowed | Delta |
|---|---|---|---|
| 0.5 | 1.64s mean (n=7) | 1.46s mean (n=7) | 0.18s, narrowed faster in 7 of 7 paired rounds |
| 0.1 (throttled) | 21.0s mean (n=3) | 17.7s mean (n=3) | ~3s |
For reference, OTEL_SDK_DISABLED=true on the same 0.1 CPU harness measured
13.3s, so the SDK as a whole is roughly 8s there and this accounts for a little
under half of it.
The 0.5 figure is the honest one for a normally-scheduled container; 0.1 is a deliberately throttled worst case we use to reproduce probe failures.
Why the environment variables do not solve it
OTEL_NODE_ENABLED_INSTRUMENTATIONS and OTEL_NODE_DISABLED_INSTRUMENTATIONS
look like the intended escape hatch, but in
@opentelemetry/[email protected] they are read inside
getNodeAutoInstrumentations(), after build/src/utils.js has already
require()d every instrumentation package and every resource detector at module
top level. They therefore skip patching, never loading, so an operator who
sets them still pays the full module-load cost. That may be worth documenting
regardless of what you decide here.
What the login app appears to use
From apps/login's dependencies and call sites:
- http - Next.js serves over it, and
src/lib/zitadel.tsreaches the ZITADEL API viacreateConnectTransport({ httpVersion: "1.1" }), i.e. Connect over HTTP/1.1, whichinstrumentation-httpalready covers. (nice-grpcis in the manifest but has no call site.) - undici - global
fetch, used directly insrc/lib/server/security-settings.tsand internally by Next.js. - winston - the existing config already sets
disableLogSending: false. - runtime-node - Node event-loop and heap metrics.
We also found that instrumentation-dns and instrumentation-net are worth
calling out separately: unlike the rest of the bundle they patch Node built-ins
rather than app dependencies, so they do emit here. Narrowing loses their
socket-level spans, which is a real if small trade rather than a no-op.
Possible directions
- Import the four (or five, with dns/net) instrumentations directly instead of the auto bundle.
- Keep
getNodeAutoInstrumentations()but document that theOTEL_NODE_*_INSTRUMENTATIONSvariables cannot recover the load cost. - Leave as is, if broad coverage for self-hosters who extend the login app is the deliberate intent.
Happy to open a PR for (1) if that is a direction you would take. We are currently carrying (1) as a local change in our fork, and would rather converge than diverge if you are open to it.
Environment
- ZITADEL
v4.16.2, Login V2, deployed on Kubernetes (EKS and k3d) @opentelemetry/[email protected],@opentelemetry/[email protected]
Source: zitadel/zitadel