OpenTelemetry Observability
This page covers VextJS OpenTelemetry integration: confirm the plugin works,
verify Traces, Metrics, and Logs with local files, then connect a Collector.
Start from a TypeScript API project in Quick Start.
The published package checked on 2026-09-25 was
@devcodex/opentelemetry@2.1.17, with a Vext peer range of >=0.2.5.
These versions describe the checked scope; the install command does not pin
the framework version.
For access instructions to other frameworks (Egg.js/Koa/Express/Hono/Fastify), please check the GitHub repository directly:
devcodex-labs/opentelemetry
Directory overview (VextJS-only)
- Quick start (VextJS framework)
- VextJS configuration and initialization
- Local testing without Docker
/_otel/statusstatus endpoint- VextJS configuration
- Declarative capture
- 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
@devcodex/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 the@devcodex/opentelemetry/vextjssubpath (VextJS specific). The main entrance@devcodex/opentelemetryonly exports framework-independent tools (createWithSpan,getOtelStatus).
3. Add a verifiable route and start
Merge this config into the project:
This route performs one local demo operation. It does not contact a payment service or database and assumes this page's plugin remains enabled:
For production, stop dev first, then run npm run build -- --typecheck and
npm start. Do not run both servers on the same port simultaneously.
The CLI discovers vext.preload from installed dependencies and injects the
instrumentation entry. The current package's default preload prepares that
entry; SDK initialization may wait until plugin setup. With no export target
and no forced SDK preload, the SDK is not initialized. Set preloadSdk: true
as described below when auto-instrumentation must start before app modules.
4. Verify the default state
With no other OTel environment settings:
Expect sdk: "noop" and exportMode: "none" in status; the business route
still returns 200. No telemetry is exported, so this does not prove a
Collector received data. Use the local file workflow below to verify output.
Understand VextJS configuration and initialization order
There are three configuration locations. Keep SDK lifecycle and request observation responsibilities separate:
Recommended order
- Set
serviceNameand the default export target inpackage.json. SetpreloadSdk: trueif early auto-instrumentation is needed. - Add request observation behavior in the plugin. Keep its export target consistent with package config.
- An already configured exporter is not replaced by a later call.
Current
attachExporterToSdkfills only an unconfigured delegate, even though the environment variables displayed by status may change later. The status endpoint is therefore not complete evidence of the actual delivery target. Restart after changing target or sampling, then inspect the output file or Collector. - In default delayed mode, plugin exporter options resolve in order:
options →
app.config.otel→ package. They do not reread every OTel environment variable. If relying only on environment variables, enable early SDK initialization explicitly and verify actual output.
endpoint and protocol quick reference
The current package has two gRPC paths. Early SDK Trace/Metrics use a gRPC
exporter, while Logs still construct an HTTP exporter. When the plugin
attaches an exporter, insecure: true uses h2c and false uses TLS; the
h2c branch does not forward configured headers. For all three signals or
authenticated delivery, verify the OTLP/HTTP path on this page. Do not
infer successful delivery from protocol: "grpc" or status alone.
What happens without an export address?
By default, nothing is sent to a Collector or local file. Setting none
is not a reliable runtime off switch if another entry already initialized
an exporter. Coordinate config and restart.
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.
Option 1: Export to local files (recommended)
Set the export address in the project's package.json. The SDK
initialization entry reads it to choose the actual export target:
package.json vext.otel.endpoint is the recommended preload source in
VextJS mode so startup and runtime agree from the beginning. Merge this
fragment into an existing package.json; keep scripts and dependencies.
A plugin can only fill exporters that have not already been configured.
Relative paths resolve from process.cwd(), so start in the app root.
Keep the plugin's service name aligned:
After changing package.json, stop and restart the service so the early
SDK reads the new config:
The plugin creates the directory. To avoid multiple workers writing the same file concurrently, the implementation uses per-process files:
traces.<pid>.jsonlmetrics.<pid>.jsonllogs.<pid>.jsonl
Wait for batching and the metric cycle (15 seconds by default). Enabling
the plugin without business requests does not guarantee records in all three
files. In PowerShell, use Get-Content ./otel-data/traces.*.jsonl, then
inspect metrics and logs; on Unix use cat. Confirm the demo.work span,
HTTP metrics, and otel demo completed log. These files are for debugging;
the application owns rotation and retention.
Actual file structure:
- Traces: one span per line with
traceId,spanId,name, andattributes. Time and duration use SDK high-resolution arrays, not the old example'sidand microsecondtimestamp. - Metrics: each line contains a
timestampand SDKResourceMetrics; metrics live undermetrics.scopeMetrics[].metrics, not a top-level array. - Logs: each line serializes an SDK LogRecord. Field and Resource shape follow the installed SDK; debug JSONL is not a fixed OTLP wire protocol.
- Optional parent span and resource fields may be absent. Build a reader from the installed version's actual output, and inspect all three files rather than relying only on status.
Option 2: Local Jaeger (when Docker is available)
Use the official Jaeger docs to start a version appropriate service with an OTLP/HTTP receiver and map port 4318 locally. Jaeger primarily verifies Traces; Metrics and Logs need their own receiving backend.
Configure local Jaeger in project package.json:
Choose either this Jaeger endpoint or the file endpoint above. Keep
serviceName and preloadSdk: true from the file setup. Query
demo.work under that service in the Jaeger UI. The plugin stays minimal:
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:
README.mdchangelogs/
/_otel/status Status check interface
Used to verify the current running status of OTel SDK:
When the plugin is enabled, the adapter registers GET /_otel/status
directly before ordinary global middleware. Do not rely on ordinary route
auth or later middleware to protect it. With this page's native default
response wrapper, the fields above are under data; custom or disabled
wrapping changes that shape. Status variables do not prove the backend
received anything. The current package displays samplingRatio: 1 when
the value is zero, so this field alone cannot prove zero sampling.
Production environment It is recommended to restrict intranet access at the gateway layer.
Reported data content
Traces (link tracing)
HTTP auto-instrumentation creates request spans and the plugin adds attributes when the SDK is enabled, the library is supported, and sampling allows recording. Attributes may include:
The package already depends on auto-instrumentations-node; do not install
it again solely for the default integration. Early initialization, module
load order, specific library versions, and sampling determine whether child
spans appear. Installation alone does not prove them.
Metrics (metric monitoring)
ignorePathssuppresses this plugin's span attribute handling and HTTP metrics on matching paths. It does not remove a span already created by HTTP auto-instrumentation or skip lifecycle callbacks. Configure the underlying instrumentation/exporter for full filtering. Currentrequest.sizeuses rawreq.pathas a label, while other metrics prefer a matched route; assess high-cardinality paths separately.
Node.js runtime metrics come from the bundled runtime-node
instrumentation and names may vary by version. The current package includes
definitions such as nodejs.eventloop.delay.*,
nodejs.eventloop.utilization, and v8js.memory.heap.used. Do not infer
CPU, RSS, or GC metric names from an older example; check the current local
metrics file or Collector.
Logs (log correlation)
Framework logs can include trace_id and span_id after the plugin writes
a sampled, recording active span to requestContext. An inactive context,
ignored path, or unsampled request does not guarantee these fields:
Logs and links can be correlated in Grafana Loki / ELK via trace_id.
Structured logs (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 @devcodex/opentelemetry/log:
createStructuredLogFormatter— Schema A structured JSON formatter (fixed field order)createOtelLogBridge— Schema B OTel LogRecord bridge through the current OTel Logs API provider.
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 app
code through vext.preload) reads the default export config. The CLI
delays SDK startup by default; preloadSdk: true starts it before app
modules. The plugin can only fill exporters not already configured and
cannot replace an existing target.
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 plugin owns runtime tracer, meter, and logger behavior such as
ignorePaths, metric buckets, log bridging, and adding exporters not yet
configured during setup. The option snippets here and in capture replace
the same plugin's options from Quick Start; do not register multiple
copies.
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.- The current version also supports explicit full mode for headers and body;
neither is collected by default, and this example uses allowlists. Use
fields,exclude,sensitiveKeys,maxValueLength,maxDepth,maxItems, andoutputto bound, redact, and snapshot values. Body capture reads parsed data and does not consume the request stream again. 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
These variables have different readers in the package and SDK. For VextJS,
prefer package.json vext.otel for a stable export configuration.
Additional plugin options: enabled defaults on. insecure only applies
when the plugin configures a gRPC exporter. resourceAttributes is currently
a compatibility placeholder, and the package reader does not read a same-name
field; use supported OTEL_RESOURCE_ATTRIBUTES for SDK Resource attributes
and verify actual output. statusEndpoint cannot set a custom path.
Tracing/metrics default on, ignorePaths defaults empty, and
logs.bridgeAppLogger defaults on when the endpoint is not none.
Lifecycle callbacks should finish synchronously. Exceptions warn and
continue, so they are not authorization or transaction hooks. An exception
path is observed as 500 and may differ from the HTTP status produced by
later business error conversion. metrics.labels applies only to
duration/total; capture only adds span attributes.
Connect to a backend
Local development
Cloud vendors
These are example address shapes. Confirm the actual region, tenant endpoint, receiver protocol, and authentication fields in the vendor console and official integration documentation. This table does not imply that these remote services have been verified here.
Supply cloud vendor tokens through environment variables, such as Kubernetes Secrets, rather than embedding them in application code.
Auto-Instrumentation
@devcodex/opentelemetry includes @opentelemetry/auto-instrumentations-node for common libraries. Whether database queries, outgoing HTTP calls, and message queues produce spans depends on SDK initialization order, library compatibility, enabled instrumentations, and sampling.
Installation
For automatic instrumentation, set vext.otel.preloadSdk: true in package.json and start with vext dev or vext start. Confirm that the SDK initializes before the business libraries you want to instrument are loaded. Starting it only in the plugin phase cannot reliably patch libraries that are already loaded.
If application code directly imports getNodeAutoInstrumentations from @opentelemetry/auto-instrumentations-node for deeper customization, declare that package as an application direct dependency.
Supported libraries
See @opentelemetry/auto-instrumentations-node for the full list.
Example result
A GET /users/:id request might produce this span tree in Jaeger:
This illustrates a possible business call chain. The route must actually call these dependencies and their instrumentations must be active. The /otel-demo route does not create database or Redis operations.
Disable selected instrumentations
Prefer the environment variable supported by auto-instrumentations-node; do not create a second NodeSDK for this. Set it before starting the process, for example in PowerShell:
Use names without package prefixes. This package already disables fs by default, and this setting also triggers early SDK initialization. See the OpenTelemetry configuration guide and check the supported range of the installed instrumentation versions.
Behavior when auto-instrumentation is unavailable
If @opentelemetry/auto-instrumentations-node is unavailable:
- The console prints a warning.
- Manual
withSpanoperations and SDK metrics can still work. The plugin can only enrich an existing active span; without HTTP instrumentation, request spans and log trace correlation are not guaranteed. - Automatically generated request, database, and outgoing HTTP spans are absent. Whether the application continues to run also depends on its own code.
Advanced usage
Manually track business operations (withSpan)
withSpan() tracks custom business operations. It wraps tracer.startActiveSpan() with try/catch/finally and handles span.end(), span.recordException() and span.setStatus().
VextJS plugin (via app.otel.withSpan)
This route fragment requires the OTel plugin setup described above, a business-owned src/services/payment.ts service exposing process(id), and typegen. Use a payment-service test double for verification. Each request executes the payment operation exactly once.
Behavioral Description:
Underlying API (custom SpanKind, Processor, and other advanced scenarios)
This advanced fragment also belongs in src/routes/index.ts. First implement findById(id) in src/services/user.ts and run typegen. Install @opentelemetry/api as a direct dependency when importing it. startSpan does not make the new span the active context for child calls; prefer withSpan when context propagation matters.
Custom business metrics
Sampling (reduce overhead)
Option 1: package.json configuration (recommended)
The instrumentation reads vext.otel.sampling.ratio when the SDK initializes. When a valid ratio is below 1, it uses ParentBasedSampler(TraceIdRatioBasedSampler(ratio)); root spans without a sampled parent are sampled at this ratio. Restart after changing it:
Option 2: Environment variable when package sampling is absent
Cluster processes
Custom instrumentation
Project src/preload/ entries and direct dependency packages' vext.preload entries run together. An application's own package.json vext.preload is not a project script entry point and does not replace a dependency package's entry point. Do not create an uncoordinated second NodeSDK alongside the default integration.
If you need to own the SDK yourself, read the preload guide. Explicitly disable or exclude the built-in startup entry, and define initialization order, exporters, and shutdown ownership before implementing the upstream custom SDK instructions. This page's default example uses one plugin-managed SDK.
Log field planning
VextJS and @devcodex/opentelemetry support two complementary log outputs:
- A. Application logs (stdout / file JSON): readable business fields for investigation and aggregation in ELK or Loki.
- B. OTel Logs (LogRecord → Collector): lightweight records linked to traces by
trace_id.
A. Application log fields (stdout / file JSON)
Add stable business fields with config.logger.mixin. A logger mixin is not the same as an SDK Resource configuration. You can replace the earlier logger configuration with this example; it needs neither top-level await nor the non-public Span.name field:
An active recording span in the request context supplies trace_id and span_id. Log a business span name explicitly when needed.
Example output fragment:
The framework's built-in provider automatically injects
requestId, andtraceId/spanIdwritten torequestContext, asrequestId,trace_id, andspan_id. Do not duplicate them in the user mixin.
Field reference
The default request message looks like GET /users/123 200 8ms | IP. Aggregate metrics by route template so each user ID does not become a separate label. Record explicit fields in your own route middleware. This file does not replace the OTel initialization above:
Add route-metrics to the config.middlewares whitelist and reference it in the target route's middlewares; see Middleware registration and use. A parameter route's endpoint should be /users/:id, rather than /users/123. This example uses req.onClose() to measure response completion or premature connection closure. A close event does not guarantee the client received the complete response and should not automatically count as a successful request.
B. OTel Logs (LogRecord → Collector)
The default Vext logger does not depend on a third-party logger, so logger-specific auto-instrumentation does not automatically capture app.logger. To export OTel Logs, use the app.setLogger() bridge provided by @devcodex/opentelemetry or wrap the current logger in a custom plugin:
trace_id/span_id: derived fromrequestContextor an active span for the LogRecord.severity_text: mapped from the Vext logger level.body: the log message.service.name: from the SDK Resource configured ininstrumentation.ts.attributes: structured log arguments mapped to LogRecord attributes.
The current bridge reads the arguments passed to the logger and then calls the original logger. Fields added later by the original logger's mixin do not automatically enter the LogRecord. Pass fields needed in both outputs explicitly as log arguments, or set OTel logs.globalAttributes. The bridge is enabled by default when endpoint is not none; it wraps info, warn, error, debug, and fatal. Child loggers and trace are not bridged automatically, and nested object fields are not fully passed through.
Avoid copying every application log field into LogRecord attributes. Use trace_id to connect the log to a trace and inspect the richer context there. Keeping LogRecords small helps control Collector traffic.
C. Deeper fields in child spans
This is an illustration using older semantic names. Actual fields depend on the installed instrumentation, target library, configuration, and sampling. Newer versions may use url.full or db.query.text; do not treat this table as a guarantee for every request.
Follow trace_id in Jaeger or Grafana Tempo to inspect the complete call chain.
Production best practices
- Configure an export endpoint. Without one, no data is exported; this is the safe default.
- Budget for shutdown. The plugin calls SDK shutdown in
onClose. Setshutdown.timeoutin seconds based on actual batching and network delay, then verify it. A larger timeout cannot guarantee Collector receipt. - Restrict
/_otel/status. The VextJS adapter registers this route automatically. In production, restrict it to internal access at the gateway. - Exclude sensitive data from spans. Avoid passwords, tokens, and identity numbers.
- Set sampling deliberately. Use one package sampling configuration or verified environment configuration, restart, and inspect the actual output volume.
- Use a Collector where appropriate. Application → Collector → backend provides decoupling and buffering.
FAQ
Q: /_otel/status returns "sdk": "noop"
Without an endpoint, noop may be expected. To export data, check the direct dependency, plugin enablement, package endpoint and preloadSdk, and OTEL_SDK_DISABLED. Disabling the plugin entirely makes this endpoint return 404.
Q: The endpoint shows localhost, but I configured another address
Check package.json vext.otel.endpoint, keep the plugin's endpoint, protocol, and headers aligned with package config, and confirm that you start with vext start or vext dev.
Q: Logs have no trace_id
Check the SDK, early auto-instrumentation, plugin registration, and sampling. requestContext must be enabled, and the log must occur inside a request context with a recording span. An initialized status alone does not prove that this request has an active span.
Q: The backend receives no data
Check exportMode and exportTarget first. Local file export can distinguish “no data produced” from “network export failed.” Inspect actual backend records, authentication, protocol, and connectivity; allow for the configured batching and metrics intervals. The package does not guarantee a SUCCESS log for each batch, and gRPC failure or recovery logs do not prove delivery of every signal.
Q: [otel] ... export FAILED: grpcSend timeout
The server cannot complete an h2c gRPC connection to the Collector. Check the address and port, Collector health, network rules, and service DNS inside Docker or Kubernetes; localhost there refers to the current container or pod.
Q: I start with node dist/server.js; why is the SDK inactive?
The zero-configuration VextJS integration depends on the CLI discovering dependency packages' vext.preload entries and injecting --import before startup.
- Recommended: use
vext devorvext startthrough project npm scripts. - Custom Node command: only if you have actually built a complete application entry point, add
--import @devcodex/opentelemetry/instrumentationyourself. A standard Vext build does not createdist/server.jsautomatically.
Q: How do I disable the integration in tests?
Alternatively set OTEL_SDK_DISABLED=true before startup. Also disable routes that depend on app.otel. Setting only endpoint: "none" stops export; it does not disable the entire integration.
Related documentation
- Preload: project and dependency entries, development and production lifecycle.
- Plugins: setup, dependency order, and shutdown.
- Logger and access log: output fields, context, and response completion timing.
- Deployment: startup, processes, and shutdown budget.