OpenTelemetry Observability
This document only introduces how to access vextjs-opentelemetry in the VextJS scenario.
For access instructions to other frameworks (Egg.js/Koa/Express/Hono/Fastify), please check the GitHub repository directly:
vextjs/vextjs-plugins
Directory overview (VextJS-only)
- [Quick Start (VextJS Framework)](#Quick Start vextjs-framework)
- [Understand first: VextJS dual-entry priority](#Understand vextjs-dual-entry priority first)
- [Local testing (without Docker)](#Local testing without-docker)
- [
/_otel/statusstatus check interface](#_otelstatus-status check interface) - [Configuration method (VextJS)](#Configuration method vextjs)
- [Declarative capture (
capture)](#Declarative capture capture) - [Complete Configuration Reference](#Complete Configuration Reference)
- Production Best Practices
- FAQ
This page only retains the official access path of VextJS; if you are checking Egg.js / Koa / Express / Hono / Fastify, please jump directly to the GitHub README to get instructions for the corresponding framework version.
Quick start (VextJS framework)
1. Installation
vextjs-opentelemetryhas built-in@opentelemetry/api,@opentelemetry/sdk-node, commonly used OTLP exporters and automatic detection dependencies; For VextJS default access, there is no need to repeatedly install these packages. Only when your application code needs to directly import an OTel package, it is recommended to declare it as a direct dependency of the application itself.
2. Create plug-in
Note:
opentelemetryPluginis imported through thevextjs-opentelemetry/vextjssubpath (VextJS specific). The main entrancevextjs-opentelemetryonly exports framework-independent tools (createWithSpan,getOtelStatus).
3. Start
vext start/vext devautomatically runs the OTel SDK initialization script (vextjs-opentelemetryhas declared"vext.preload": "./dist/instrumentation.js"in itspackage.json, VextJS CLI automatically scans and injects it with--import, no manual configuration is required). Not reported by default - The SDK initialization script reads thevext.otel.endpointfield of the project's ownpackage.jsonat startup to determine the reporting address. When not configured, it is a safe noop (the data is discarded and will not be sent to any address).
4. Verification
Done. All telemetry features are automatically enabled.
First understand: VextJS dual entry priority
OpenTelemetry configuration under VextJS is divided into two formal entrances, but with different responsibilities:
Recommended order
- First solidify the default export target in
package.json vext.otel: Make CLI preload, startup log and/_otel/statusconsistent from the beginning. - Add request-side behavior in
opentelemetryPlugin(): such asignorePaths,capture, log bridging and runtime additional tags. - If
endpoint/protocol/headersis written in both places, try to keep it consistent: avoid the cognitive bias of "one address at startup and another address at runtime".
endpoint / protocol quick check
What will happen if the reporting address is not configured?
✅ Safe Default - When the endpoint is not configured, data will not be sent to any address and will not be written to local files. To enable escalation, you can:
- Configure in
package.jsonvext.otel.*(recommended, it will take effect during the preloading phase)- or configured in
opentelemetryPlugin({...})(setup phase appends/overwrites the exporter)
Local testing (no Docker required)
Don’t want to install Jaeger/Collector? You can export data to local files and view the original data format directly.
Solution 1: Export to local file (recommended)
Configure the reporting address in the project package.json (read by the SDK initialization script to control the actual export):
package.json vext.otel.endpointis the recommended preloading configuration source in VextJS mode, which can make the startup phase and running phase consistent from the beginning. Ifendpoint / protocol / headersis passed in again inopentelemetryPlugin({...}), the exported configuration will continue to be appended or overwritten in the setup phase. Relative paths are still resolved based onprocess.cwd().
Create a plug-in (just keep serviceName consistent with package.json):
The plug-in automatically creates a directory; in order to avoid cluster/multiple worker processes writing the same file concurrently, the current implementation will write files according to process.pid:
traces.<pid>.jsonlmetrics.<pid>.jsonllogs.<pid>.jsonl
Each row is a JSON record that can be viewed via glob merging.
traces.<pid>.jsonl example (one span per line):
metrics.<pid>.jsonl example (batch of metrics per line):
Option 2: Local Jaeger (when Docker is available)
Configure local Jaeger in project package.json:
Just keep the plugin as simple as possible:
Access to other frameworks
The Vext official website only retains access instructions for the VextJS scenario.
If you need to check out the following:
- Access methods for Egg.js / Koa / Express / Hono / Fastify
- CJS preloading mode for
initOtel() - Multi-framework
HttpOtelOptions/startAttributes/endAttributes/metrics.labels/createEggMiddlewareDescription - Complete release history and version differences
Please check the GitHub repository directly:
It is recommended to read the following in the warehouse first:
vextjs-opentelemetry/README.mdvextjs-opentelemetry/changelogs/
/_otel/status Status check interface
Used to verify the current running status of OTel SDK:
VextJS: Automatically registered after startup, no manual configuration required.
Production environment It is recommended to restrict intranet access at the gateway layer.
Reported data content
Traces (link tracing)
Each HTTP request generates a Span, containing:| Properties | Example values | Description |
| ------------------ | ---------------------------------- | ---------------------------------- |
| http.method | "GET" | HTTP method |
| http.route | "/users/:id" | Route template (low cardinality, safe for metric aggregation) |
| http.status_code | 200 | Response status code |
| http.request_id | "my-app-a1b2c3d4" | vext request ID |
| vext.service | "my-app" | Service name |
| http.url | "http://localhost:3000/users/42" | Full request URL |
| net.peer.ip | "127.0.0.1" | Client IP |
After installing @opentelemetry/auto-instrumentations-node, database operations, HTTP external calls, etc. will automatically generate sub-Spans.
Metrics (metric monitoring)
ignorePathssuppresses both Trace and Metrics - Ignored paths (such as/health) will not produce any span or metric data and will not cause noise in the monitoring panel.
Node.js Runtime indicators (automatically reported through @opentelemetry/instrumentation-runtime-node):
Logs (log correlation)
Each request log is automatically injected with trace_id + span_id:
Logs and links can be correlated in Grafana Loki / ELK via trace_id.
**Structured Log (Schema A + Schema B) **
When the log needs to be landed (Schema A) and reported to the OTLP Collector (Schema B) at the same time, use the two factory functions provided by vextjs-opentelemetry/log:
createStructuredLogFormatter— Schema A structured JSON formatter (fixed field order)createOtelLogBridge— Schema B OTel LogRecord bridge (viaglobalThis._otelLogger)
Schema A — Implementation log JSON (complete fields)
VextJS recommended writing method
In VextJS, there is usually no need to copy the logger formatter / middleware assembly methods of other frameworks. More recommended:
- Enable
logs.bridgeAppLoggerinopentelemetryPlugin() - Add stable fields in
config.logger.mixin
If you need the log bridging method for Egg.js / Koa / Express / Hono / Fastify, please check the GitHub README directly; the multi-framework branch will no longer be expanded here on the official website.
Configuration method (VextJS)
VextJS's OTel configuration is divided into two layers with different purposes:
First layer: Default export configuration during preloading phase (package.json, recommended)
The SDK initialization script (instrumentation.ts, executed before application code through vext.preload) is read first and determines the default export configuration when the process starts.
If endpoint / protocol / headers is subsequently passed in opentelemetryPlugin({...}), the plugin phase will continue to append or overwrite the exporter.
Configure read priority (high → low):
package.jsonvext.otel.*- OpenTelemetry standard environment variables (such as
OTEL_SERVICE_NAME,OTEL_EXPORTER_OTLP_ENDPOINT) - Project
package.json.name(only forserviceNamefallback) - Built-in default values (
serviceName: "vext-app",protocol: "http",endpoint: "none")
Second layer: runtime plug-in behavior (src/plugins/otel.ts)
The plug-in layer is responsible for runtime tracer / meter / logger behavior, such as ignorePaths, indicator buckets, log bridging, and appending/overwriting exporters in the setup phase.
It is recommended that the
endpoint/protocol/headersof the plug-in layer be consistent withpackage.json vext.otelto facilitate the unification of/_otel/statuswith the actual export target.
Declarative capture (capture)
If you only want to add a small number of headers / query / params / body fields and don’t want to hand-write the startAttributes / endAttributes resolver for each field, you can use capture directly:
The generated attribute prefix is fixed to:
http.request.header.*http.request.query.*http.request.param.*http.request.body.*
Key constraints:
query: true/params: truemeans explicitly enable full mode; by default, full mode will not be automatically taken.headers/bodyIt is still recommended to only collect whitelists and not provide the default full mode to avoid accidentally collecting sensitive fields such asauthorization,cookie, passwords, and mobile phone numbers.capturegenerates Span attributes and will not automatically go intometrics.labels; metric dimensions should still be provided separately throughmetrics.labelsand keep the cardinality low.
Complete configuration reference
opentelemetryPlugin() options
The current unified public model is
startAttributes / endAttributes / metrics.labels / lifecycle. Therawparameter of the VextJS adapter isreq; other frameworks will transparently transmit their own original context (such as Express's{ req, res }, Koa/Egg'sctx).
package.json vext.otel
Environment variables
The following environment variables are natively supported by OpenTelemetry SDK, but in VextJS scenarios it is recommended to solidify and export the configuration through
package.json vext.otel.
Access backend
Local development
Cloud vendors
Cloud vendor token is recommended to be injected through environment variables (K8s Secret) instead of hard-coded into the code.
Auto-Instrumentation
Under default access, vextjs-opentelemetry already comes with @opentelemetry/auto-instrumentations-node, and the SDK will automatically patch common libraries, and you can obtain link tracking for database queries, HTTP external calls, message queues, etc. without modifying any business code.
Installation
By default, vext start / vext dev is used to take effect automatically after startup.
If your application code needs to directly import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node" for in-depth customization, then declare it as a direct dependency of the application itself.
Supported libraries
See @opentelemetry/auto-instrumentations-node for a complete list.
Effect example
After installation, a GET /users/:id request may produce the following Span tree in Jaeger:
No code changes required - The SDK automatically patches mongodb, ioredis, http and other modules when the process is started (--import).
Disable specific detection
If a certain automatic detection causes problems or is not needed, the instrumentation.ts configuration can be overridden in the plugin:
Then point to the custom instrumentation in package.json:
Behavior when not installed
If @opentelemetry/auto-instrumentations-node is not installed:
- The console outputs a line of warning prompts
- HTTP middleware layer tracking (Span attribute annotation, indicator statistics, log correlation) still normal
- Only deep spans such as database/external HTTP are missing (does not affect application operation)
Advanced usage
Manually track business operations (withSpan)withSpan() is the recommended way to track custom business operations. It encapsulates tracer.startActiveSpan() with try/catch/finally and automatically handles span.end(), span.recordException() and span.setStatus(), the three most easily missed things.
VextJS plugin (via app.otel.withSpan)
Behavioral Description:
Underlying API (customized SpanKind / Processor and other advanced scenarios)
When you need fine control over the span type or custom processing, you can use tracer directly:
Custom business indicators
Sampling (reduces overhead)
Method 1: package.json code-level configuration (recommended)
instrumentation reads vext.otel.sampling.ratio during SDK initialization,
Automatically use ParentBasedSampler(TraceIdRatioBasedSampler(ratio)):
Method 2: Environment variables (runtime override)
Cluster multi-process
Custom instrumentation
Completely replaces built-in SDK initialization:
Log field planning
VextJS + vextjs-opentelemetry supports two levels of log output, each with its own emphasis:
- A. Implementation log (stdout/file JSON): Business fields are clear and readable, which facilitates manual troubleshooting and log aggregation (ELK/Loki)
- B. OTel Logs (LogRecord → Collector): lightweight, associated with complete links through
trace_id
A. Implementation log field (stdout/file JSON)
Inject Resource-level and Span-level contexts via config.logger.mixin:
Output example:
requestId, andtraceId/spanIdwritten inrequestContextwill be automatically injected asrequestId,trace_id,span_idby the framework’s built-in provider; there is no need to repeat configuration in the user mixin.
Field comparison table
B. OTel Logs (LogRecord → Collector)
Vext's default logger does not rely on third-party loggers, so logger-specific auto instrumentation will not automatically capture app.logger. If you need to output OTel Logs, you can bridge it through app.setLogger() of vextjs-opentelemetry, or wrap the current logger with a custom plug-in:
trace_id/span_id: Write LogRecord fromrequestContextor active spanseverity_text: mapped from Vext logger levelbody: Log message contentservice.name: from Resource (configured in instrumentation.ts)attributes: Structured log fields are mapped to LogRecord attributes
Fields injected by user mixin (such as service_name, host, span) will automatically appear in LogRecord.attributes**.
Avoid putting all landing log fields in LogRecord attributes. OTel Logs associates Trace with trace_id to see the complete context of endpoint, latency_ms, user.id, etc. Keeping LogRecord lightweight helps control Collector traffic.
C. Deep fields (automatically appear in child Span)
The following fields are automatically collected by @opentelemetry/auto-instrumentations-node and do not require manual configuration:
Correlate these deep fields by viewing the complete call chain in Jaeger / Grafana Tempo via
trace_id.
Production Best Practices
- Configure reporting address - will not be reported if not configured (safe default value), but it also means no observability data
shutdown.timeout: 60— Make sure the SDK has enough time to flush data- Restriction
/_otel/status— The current VextJS adapter will automatically register this route. In the production environment, please restrict access to the intranet at the gateway layer. - Do not record sensitive information in Span — passwords, tokens, ID numbers, etc.
- Sampling — Use
OTEL_TRACES_SAMPLER=traceidratiofor high-concurrency services - Deploy Collector — Application → Collector → Backend, decoupling + buffering
FAQ
Q: /_otel/status returns "sdk": "noop"
① Use vext start/dev to start ② vextjs-opentelemetry in dependencies ③ The SDK package has been installed ④ package.json vext.otel configuration has taken effect.
Q: The endpoint shows localhost but I assigned another address.
① Check package.json vext.otel.endpoint ② Confirm that endpoint/protocol/headers in the plug-in is consistent with package.json ③ Confirm to start with vext start/dev
Q: The log has no trace_id
First confirm that /_otel/status returns "sdk": "initialized". In the VextJS scenario, trace_id relies on SDK initialization in the preload stage + normal plug-in access, both of which are indispensable.
Q: The backend cannot receive data
① /_otel/status Confirm that sdk: "initialized" + endpoint is correct ② Confirm in the service log [otel] ... export SUCCESS (grpc-status:0) ③ Wait for 30 seconds (batch reporting delay) ④ Use Jaeger/LGTM local debugging to confirm the data format
Q: [otel] ... export FAILED: grpcSend timeout
The h2c gRPC connection from the server to the collector is blocked. Check: ① The collector address and port are reachable ② The collector service is running normally ③ Network firewall/security group rules ④ If in Docker/K8s, use Service DNS instead of localhost
Q: I start directly with node dist/server.js, why does the SDK not take effect?
Because VextJS's "zero configuration access" relies on the CLI to automatically scan vext.preload in the dependency package and inject --import before starting.
Optional practices:
- Recommended: Continue to use
vext dev/vext start - Customized Node startup command: Manually add
--import vextjs-opentelemetry/instrumentation
Q: How to disable the test environment
Or the plug-in is not loaded in the test environment.