What you’ll learn
- The two ways to start the pipelines, and when each applies
- What to set per workload at deploy time
- How to add your own metrics and spans, and how to ship logs
- How to verify the telemetry and operate the kill switches
Prerequisites
- A Sherlock organization, and access to Settings → Collector
- A Node.js service on Node.js 20 or later, 20.6 or later for ES modules
- A secret store or a mounted secret for the bearer token in production
1. Get credentials
In Sherlock, open Settings and then Collector. Copy the Endpoint and reveal and copy the Bearer Token. Decide theenv value for each deployment of this service. Sherlock routes data into a source by OTEL_RESOURCE_ATTRIBUTES=env=<value>. The empty state of the Logs, Traces, and Metrics pages repeats these items until data arrives.
2. Install
- The package needs Node.js 20 or later. ES modules need 20.6 or later for the loader hook in step 3.
- It brings its own OpenTelemetry dependencies, pinned to exact versions. Do not add
@opentelemetry/*packages. - Import
trace,context,SpanKind, andSpanStatusCodefrom@sherlock-labs/otel. Never import them from@opentelemetry/api. Two copies of the API in one process means one of them sees no provider. - Type definitions are included. TypeScript needs no extra package.
- The SDK is in alpha. Until the package is on a public registry, Sherlock provides it as a tarball. Install it with a
file:dependency and commit the tarball next to your lockfile. In a Docker build, copy the tarball’s directory into the image before the install step, or the install cannot resolve it.
3. Register the SDK before your app loads
A patch wraps a library at the moment that library loads. A library that loads earlier gets no patch, and the SDK never traces it. Nothing warns you. How you register depends on your module system, so decide that first:- ES modules. The entry is
.mjs, orpackage.jsonhas"type": "module". - CommonJS. The entry is
.cjs, orpackage.jsonhas no"type"field. TypeScript withmodule: NodeNextand no"type"emits CommonJS.
- ES modules
- CommonJS
A line-1 import is not enough. Static imports are linked before any code runs, so a self-registering import arrives after your libraries loaded. The Put the same command in your
http built-in still gets patched, so you see GET spans and think it works, while Express, Postgres, Redis, and pino stay dark. Register the loader hook with --import.start script. @opentelemetry/instrumentation is installed with the SDK. In a container, set NODE_OPTIONS="--import ./otel.mjs" instead of changing the command.- Registering installs the auto-instrumentation patches. They record nothing until the pipelines start.
- A bundler can reorder imports. Check the built output.
4. Start the pipelines
There are two ways to start. Pick one.Zero-code
Set the token and the service name in the environment. The register import starts the pipelines on its own.From code
Usestart() when the token is not in the environment at process start. The token can come from a secret manager, a mounted file, or an async config loader.
start()always wins. If the environment also holds both variables and the auto-start ran first,start()takes the pipelines over with your options.start()never throws. A missing token or service name leaves telemetry off and logs one line. An endpoint that is not anhttporhttpsURL is replaced by the default, with a warning. A well-formed but wrong endpoint fails later, at export. See Troubleshooting.- Options you are likely to use:
5. Stop on shutdown
Callstop() in every shutdown handler, in every process type.
stop()flushes the last metric interval and the pending span batch.- Without it, every deploy loses the last 30 seconds of metrics and the last five seconds of spans.
- A background worker needs
stop()as much as a web server does.
6. Run locally and read the boot line
Start the service with no token. The SDK logs exactly one line and stays off. The app runs as normal.Unauthorized. OTEL_LOG_LEVEL=debug shows every export attempt.
Then send a few requests and open Sherlock:
- Traces shows your service within seconds.
- Metrics lists
http.server.request.durationafter one export interval, about a minute with the catalog refresh.
7. Deploy
Set these per workload.
Two things you do not need to configure:
/healthz,/livez, and/healthcheckget no span and nohttp.server.request.durationpoint. Add more paths withSHERLOCK_IGNORE_PATHSor theignorePathsoption. The Express and Koa adapter does not read that list: passignoreRequestto keep health checks out ofapp.http.server.*.- The SDK never traces or meters its own export requests. It skips them by host and port, so a collector on
localhostdoes not hide the other services on that host.
8. Add your own metrics
- Define at module scope. A definition resolves its instrument on the first record after
start(). - Never call
metrics.getMeter()from the OpenTelemetry API yourself. A meter created beforestart()is a permanent, silent no-op. - Durations are in seconds, with unit
s. Do not name a metric*_ms. - For Express or Koa,
applyMetricsMiddleware()addsapp.http.server.count,.errors, and.durationwith exemplars in one line.
9. Add spans for what auto-instrumentation misses
- A queue worker has no incoming request. Open one root span per job, or every database call inside the job is an orphan.
- A database client with no instrumentation needs a
CLIENTspan around each call. - A manual span around a route the HTTP instrumentation already traces makes a duplicate span. Reserve manual spans for the gaps.
10. Ship and correlate logs
The SDK does not ship logs. It stampstrace_id, span_id, and trace_flags on every pino log line written inside a request, and every exemplar carries the same three, so the join is one filter on trace_id. The lines still have to reach Sherlock. Two ways:
- pino transport
- Collector
Send log records straight from the process with The transport reads the standard OpenTelemetry variables. The transport brings its own copy of the OpenTelemetry logs SDK. That is expected and does not conflict with the Sherlock SDK.
pino-opentelemetry-transport. It reads the pino fields the SDK stamped and sets the trace id on each record.OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES are already set for the SDK. Add the logs endpoint and the header:trace_flagsis01when a trace exists and00when the ids exist but no trace was exported.- A log line written outside a request, such as a startup message, has no trace id. A line written inside a root span you open yourself does.
11. Verify
Run this after the first deploy and after every SDK upgrade.- Traces lists every service name you deploy, web and worker.
- A runtime metric such as
nodejs.eventloop.utilizationreports for every service name. This proves the SDK started in each process, independent of traffic. -
http.server.request.durationandhttp.client.request.durationarrive with unitsand the same 20-boundary list. - Each custom histogram arrives with unit
sand values in seconds. A 250 ms request lands in the0.25bucket. No metric name ends in_ms. - Exemplars attach to
http.server.request.durationand to each custom histogram, with a hextraceId, aspanId, andtrace_flags. Error series, 4xx and 5xx, carry them too. - Click an exemplar with
trace_flags01. The trace opens. Filter Logs by itstrace_id. The request’s log lines appear. - The server span name includes the route, for example
GET /work/:id, andhttp.server.request.durationcarries anhttp.routelabel. A bareGETmeans the app is under-instrumented. See step 3. - Spans come only from the instrumentations you enabled.
/healthz,/livez, and/healthcheckare absent from spans and HTTP metrics. - No client span and no
http.client.request.durationseries names the ingest host. - No drift warning in any process’s logs over two or more export intervals.
- Set
SHERLOCK_TRACES_ENABLED=falseon one instance. New traces from it stop. Custom exemplars still arrive withtrace_flags00. Set it back. - Set
SHERLOCK_METRICS_ENABLED=falseon one instance. Its metric series stop at the next export. Set it back. No double count on resume. - Look at span volume. The Express and Koa instrumentations emit one span per middleware layer per request. If that is noise, disable the framework instrumentation with
{ disabled: ['express'] }or accept it.
12. Operate
- Kill switches.
SHERLOCK_TRACES_ENABLEDandSHERLOCK_METRICS_ENABLEDset the state at start.setTelemetryEnabled({ traces, metrics })flips it at runtime. Wire it to your feature flags. Traces off finishes in-flight traces whole. Metrics off leaves a gap, never a double count, because temporality is delta. - Sampling.
OTEL_TRACES_SAMPLER_ARGortracing.sampleRatio, parent-based. - Upgrades. The telemetry shape is part of the SDK’s API. The SDK is on
0.xwhile in alpha, so a minor version can still change it. From 1.0, a change to metric names, attributes, units, or default boundaries is a major version, and a minor version is additive only. Every OpenTelemetry dependency is pinned exactly. Read the release notes, upgrade as a deliberate change, deploy one instance, and re-run step 11.
Related topics
Custom metrics
Define your own instruments with exemplars.
Custom spans
Trace queue jobs and database calls.
Troubleshooting
Symptom, cause, and fix.
SDK README
Every option and environment variable.

