Node.js SDK
@watchtowerobservability/node (0.7.0 at the time of writing; install @latest) is a thin layer over the OpenTelemetry Node SDK: endpoint and key handling, offline buffer, error capture, logs, database and queue integrations and Next.js support. If you prefer plain OpenTelemetry with no Watchtower package, see Node.js (plain OpenTelemetry).
npm install @watchtowerobservability/node@latest @opentelemetry/context-async-hooks @opentelemetry/core \
@opentelemetry/exporter-logs-otlp-http @opentelemetry/exporter-metrics-otlp-http @opentelemetry/exporter-trace-otlp-http \
@opentelemetry/otlp-transformer @opentelemetry/resources @opentelemetry/sdk-logs @opentelemetry/sdk-metrics @opentelemetry/sdk-traceimport * as wt from "@watchtowerobservability/node";
wt.init({ apiKey: process.env.WT_INGEST_KEY, service: "api", environment: "production", release: process.env.WT_RELEASE });
wt.captureException(new Error("oops"));
wt.log("info", "started", { port: 3000 });The ingest key (wti_) needs ingest:traces, ingest:logs, ingest:metrics and ingest:errors; see Keys and scopes. init reads WT_INGEST_KEY, WT_SERVICE, WT_ENVIRONMENT, WT_RELEASE and WT_OTLP_ENDPOINT when options are left out.
Resource attributes
Every signal carries service.name, service.version (your release), deployment.environment.name and the standard telemetry.sdk.name, telemetry.sdk.language and telemetry.sdk.version, so the instrumentation coverage report can tell which SDK a service uses.
Secrets in URLs
By default the SDK scrubs http.target, url.full, url.path, url.query and http.url: the query string is dropped and path segments that look like secrets become :redacted (/api/feed/<token>/csv?since=1 is stored as /api/feed/:redacted/csv). http.route is kept. Opt out with init({ urlScrubbing: false }). Details and cleanup of older data: Privacy.
Auto-detect database drivers
init({ instrument: "auto" }) instruments whichever of pg, postgres.js, better-sqlite3, mysql2 and ioredis is installed (plus http and fetch), and logs one line naming what it enabled. Without it, init patches node:http, fetch, pg and ioredis for the OpenTelemetry packages you have installed; instrument: false or WT_INSTRUMENT=0 turns it off. Integrations for other drivers are explicit calls, for example instrumentPostgres() from @watchtowerobservability/node/postgres.
Queries need a parent span
The datastore integrations take requireParent (DbOptions.requireParent, default on; set WATCHTOWER_DB_REQUIRE_PARENT=0 or pass { requireParent: false } to turn it off): a query is recorded only under an active span. Polling loops, schedulers and start-up migrations then add no root spans.
Cron and scheduler check-ins
await wt.checkin("nightly-report", async () => { await buildReport(); });wt.checkin(monitorSlug, fn) posts in_progress, then ok or error with the duration, to the heartbeat monitor with that slug (the key needs ingest:checkins; the edge takes 6 check-ins a minute per monitor). It returns what fn returns and rethrows its error.
Flush on shutdown
init flushes buffered telemetry on SIGTERM and SIGINT (at most 2 seconds). When your app, or a framework such as next start, has its own signal handler, that handler decides how the process ends and the SDK only starts the flush. When the SDK's is the only handler it re-sends the signal after flushing, so the default exit still happens. This applies to makeRegister in Next.js as well; call await wt.shutdown() yourself if you exit another way.
Next.js
// instrumentation.ts
import { makeRegister } from "@watchtowerobservability/node/next";
export const register = makeRegister({ nextInternals: "sample" }); // plus any init option
export { onRequestError } from "@watchtowerobservability/node/next";nextInternals controls the spans Next.js emits about itself (load route module, prepare route module, resolve page components, start response, middleware, generateMetadata):
Value | Effect |
|---|---|
| keeps a share ( |
| keeps none |
| keeps all |
Request, route handler, render, server action and fetch spans, every SERVER and CLIENT span and every errored span are always kept, and a dropped span's children stay in the trace.
Source maps. Wrap the config, then upload after the build:
// next.config.ts
import { withWatchtower } from "@watchtowerobservability/node/next";
export default withWatchtower(nextConfig, { sourcemaps: true }); // turns on productionBrowserSourceMaps{ "scripts": { "build": "next build && wt-next-sourcemaps" } }wt-next-sourcemaps uploads the client maps under .next/static and deletes the .map files. It reads WT_API_KEY (a service account key with ci:write, which covers the artifact upload), WT_PROJECT and WT_RELEASE, which must equal the release the browser SDK reports. This works with webpack and Turbopack.
Browser tunnel. Ad blockers block third-party telemetry hosts. Forward the browser SDK through your own origin; see Next.js (browser).
Version
Package versions: latest on npm. The SDK matrix lists the other languages.