Ollygarden's recommended pattern for setting up OpenTelemetry in Node.js services. Covers project structure, which auto-instrumentations to disable, the entry-point ordering, the programmatic NodeSDK fallback, and the setup checklist (no query strings in telemetry, startup/non-request span hygiene, declarative YAML config, standard OTEL_* env vars honored, lean resource with service.instance.id) closed by a required verification report. Use when adding OTel to a Node.js project, structuring telemetry code, or reviewing an existing setup. Triggers on "node otel setup", "NodeSDK pattern", "auto instrumentation node", "url.query", "query parameter PII", "service.instance.id".
The @opentelemetry/configuration package is experimental. Default to declarative config
for new projects. Use the programmatic NodeSDK fallback below if stability is critical or
the project needs runtime configuration that YAML cannot express.
Setup Checklist — verify every item before you finish
Setup is not done when the SDK boots. Each unchecked item below produces a specific
telemetry-quality finding in production; work through all of them.
Do not export query strings or other user input. Telemetry must not capture data
that can carry user input by default, and the query string is exactly that — yet the Node
HTTP instrumentation (@opentelemetry/instrumentation-http, active through
auto-instrumentations-node) exports it out of the box: url.query on server spans and
folded inside url.full on client spans under the stable HTTP semantics. Check which
semantic-convention mode your installed instrumentation actually emits — many contrib
versions still default to the old HTTP attributes, where the query rides in http.target
on server spans and inside http.url on client spans; redacting only the stable
url.query/url.full keys then leaves the marker exposed. Only userinfo credentials are
redacted by default; everything else — GET /owners?lastName=Smith, search terms, tokens
pasted into links — goes out verbatim (Critical PII Leakage finding). Strip it
in-process, before it leaves the application — overwrite or delete whichever attributes
carry it in your mode (url.query/url.full, and/or http.target/http.url) in the HTTP
instrumentation's own attribute hook (applyCustomAttributesOnSpan /
startIncomingSpanHook) or in a span-processor / exporter wrapper that rewrites the
attribute before export. A Collector transform/redaction processor is only
defense-in-depth, never the sole control: the raw value has already crossed the process
boundary and may sit in in-transit buffers, debug logs, or an alternate export path that
skips the Collector. The route template (http.route,
url.path) already answers "which endpoint"; if a specific parameter is genuinely needed as
telemetry, capture it deliberately as a bounded, named attribute — never by keeping the raw
query string. Leave the opt-in header-capture knobs
(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_*) off for the same reason. Verify by sending a
request with a known marker value in a query parameter and inspecting the exported span: the
marker must not appear anywhere.
Keep startup and other non-request work from polluting trace shapes. Auto
instrumentation patches http, database drivers, and clients process-wide, so any outbound
call made during boot — a migration, a warm-up HTTP request, a connection-pool probe — is
emitted as a parentless CLIENT root span because no request context is active yet
(Root Client Span finding). Two obligations: (1) either wrap deliberate startup work in an
explicit application-startup span so those child spans have a parent, or suppress
instrumentation for it (run it before startNodeSDK, or drop init-phase spans with a
sampler / Collector rule) — do not ship detached CLIENT roots by default; (2) never build
span names from unbounded input. Custom spans named with an id, a URL, or any per-request
value (e.g. tracer.startSpan('job ' + jobId)) create unbounded span-name cardinality; name
the span for the operation and put the varying value in a bounded attribute. Verify by
booting the app and inspecting the first exported traces: no parentless CLIENT roots, and no
per-request/per-boot identifiers anywhere in span names.
Configure the SDK declaratively — one YAML document, not option sprawl. A scatter of
OTEL_* flags plus hand-wired new NodeSDK({...}) exporter/processor code is the
anti-pattern: operators cannot review or change the telemetry pipeline as a single document
without a redeploy. Prefer the declarative model — startNodeSDK() from
@opentelemetry/sdk-node loads the file named by OTEL_CONFIG_FILE through the experimental
@opentelemetry/configuration package (mechanics: otel-js skill,
references/declarative-setup.md; YAML conventions: ollygarden-otel-declarative-config).
Keep the programmatic NodeSDK fallback only when stability is critical or the config needs
runtime values YAML cannot express (see the status decision above). The file replaces
option/env-based configuration — not code. Components that must run in code — the
query-string redaction from the first item, the startup-span policy from the second, the
disabled fs/dns/net instrumentations — stay in place and are passed to
startNodeSDK(); going declarative does not discharge those items, and every earlier item's
verification must be re-run after the switch. The file must also preserve the standard
OTEL_* contract via ${ENV} substitution — the next item spells that out. Verify by
changing a config value (e.g. the sampler argument or the exporter endpoint) in the YAML and
confirming behavior changes with no code change and no rebuild.
Honor the standard OTEL_* environment variables end-to-end.OTEL_EXPORTER_OTLP_*, OTEL_SERVICE_NAME, and OTEL_RESOURCE_ATTRIBUTES must all take
effect at runtime. Do not invent custom variables (SERVICE_VERSION,
DEPLOYMENT_ENVIRONMENT, ...) for values the standard variables already express, and never
let a hardcoded default clobber an operator-supplied value. The programmatic NodeSDK reads
these variables directly, but a declarative YAML loaded via OTEL_CONFIG_FILE does not —
when the config file is set the SDK stops falling back to the environment, so each standard
variable must be threaded in explicitly with substitution:
resource:
attributes:
- name: service.name
value: "${OTEL_SERVICE_NAME:-my-node-service}"
type: string
# standard deploy-time attributes (service.version,
# deployment.environment.name, ...) arrive through the STANDARD variable:
attributes_list: "${OTEL_RESOURCE_ATTRIBUTES}"
Do not also hardcode a deploy-varying key (e.g. deployment.environment.name) under
attributes, or the literal silently wins and misfiles every signal. Point the exporter at
${OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}. Verify by booting with all three
standard variables set to non-default values: OTEL_SERVICE_NAME and
OTEL_RESOURCE_ATTRIBUTES must land on the exported resource, while the overridden
OTEL_EXPORTER_OTLP_ENDPOINT — a destination, not a telemetry attribute — is confirmed at
the receiving collector, not on the spans.
Keep the resource lean and add service.instance.id. The Node SDK's default
detector set (envDetector, processDetector, hostDetector) does not include
service.instance.id — the serviceinstance detector that would mint one is experimental
and off by default — so unless you act, every process of a replicated service is
indistinguishable (Missing service.instance.id finding). Add a per-process UUID
(crypto.randomUUID()) alongside service.version: programmatically via
resourceFromAttributes (the string key is service.instance.id, or ATTR_SERVICE_INSTANCE_ID
from @opentelemetry/semantic-conventions/incubating), or in declarative YAML as a resource
attribute fed a per-process value. Keep the rest of the resource minimal — service.name,
service.version, service.instance.id, deployment.environment.name is the full set. Do
not enable host/process/OS resource detectors that stamp discouraged process.* / os.*
attributes (Discouraged Resource Attribute finding). Verify by inspecting the exported
resource: service.instance.id is present and differs between two separate process starts,
and no process.*/os.* attributes are attached.
Required: Verification Report
Setup is not complete until you produce this report. It is a table with one row per
checklist item above. Fill each row with artifacts from THIS run — the marker value you
sent, an excerpt of the exported span dump, a trace id, the config value you changed.
Never a restatement of the requirement, never a bare "done".
The table below is an illustrative example, not a report you can submit: every value in
it is a placeholder showing the expected shape of evidence. Replace every cell with your
own run's artifacts. If you did not run a check, write GAP — not run in that row and
leave it visible — a missing or hand-waved row is itself a finding.
Example (illustrative values — replace every cell with your own run's evidence):
Item
Check performed
Observed evidence
No query strings exported
GET /owners?lastName=MARKER_7f3a → inspected exported span (check which semconv mode: url.query/url.full vs legacy http.target/http.url)
query attribute absent; MARKER_7f3a nowhere in span dump (trace 4bf9...)
Startup / non-request span hygiene
Booted app, inspected first exported traces
No parentless CLIENT roots; startup DB call is child of app.startup span; no ids in span names
SDK configured declaratively
Changed sampler arg in configs/otel.yaml, restarted without rebuild
Sampling ratio changed on exported spans; no code edit needed
Standard OTEL_* honored
Booted with OTEL_SERVICE_NAME/OTEL_RESOURCE_ATTRIBUTES set to non-defaults, and OTEL_EXPORTER_OTLP_ENDPOINT pointed at a marker collector
service.name/deployment.environment.name carry the supplied values on the exported resource; the marker collector's log / receipt confirms telemetry arrived at the overridden endpoint (the endpoint is a destination, evidenced there, not on the spans)
Lean resource + service.instance.id
Dumped resource from two separate process starts
service.instance.id present and differs across starts; no process.*/os.* attrs
A row you cannot fill with observed evidence is a visible gap — that item is not done.
Do not delete the row, copy these example values, or write "N/A" to hide it; go run the
check and record what you actually saw.
import { startNodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
// startNodeSDK loads OTEL_CONFIG_FILE and registers the configured providers.
// new NodeSDK(...) is the separate programmatic path and does not load the file.
export const sdk = startNodeSDK({
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false },
'@opentelemetry/instrumentation-dns': { enabled: false },
'@opentelemetry/instrumentation-net': { enabled: false },
}),
],
});
Entry Point (src/index.ts)
// IMPORTANT: Import and start telemetry BEFORE any other imports
import { sdk } from './telemetry/setup';
import { app } from './app';
const PORT = process.env.PORT ?? 3000;
const server = app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
process.on('SIGTERM', () => {
server.close(async () => {
await sdk.shutdown();
process.exit(0);
});
});
Fallback: Programmatic NodeSDK Setup
If declarative config is not suitable (e.g., need dynamic runtime config or older SDK version),
use programmatic setup:
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { resourceFromAttributes, envDetector } from '@opentelemetry/resources';
import { ATTR_SERVICE_INSTANCE_ID } from '@opentelemetry/semantic-conventions/incubating';
import { randomUUID } from 'node:crypto';
// service.name / service.version / deployment.environment.name arrive through the standard
// OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES variables (applied by envDetector below);
// do not invent custom vars for them. Mint only the per-process service.instance.id that no
// default detector provides.
const resource = resourceFromAttributes({
[ATTR_SERVICE_INSTANCE_ID]: randomUUID(),
});
export const sdk = new NodeSDK({
resource,
// Only envDetector: honor the OTEL_* contract without stamping discouraged
// process.* / os.* attributes from the default process/host detectors.
resourceDetectors: [envDetector],
traceExporter: new OTLPTraceExporter(),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter(),
exportIntervalMillis: 30_000,
}),
logRecordProcessors: [
new BatchLogRecordProcessor(new OTLPLogExporter()),
],
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false },
'@opentelemetry/instrumentation-dns': { enabled: false },
'@opentelemetry/instrumentation-net': { enabled: false },
}),
],
});
Key Details
Disable noisy instrumentations: fs, dns, and net instrumentations generate high volumes of low-value spans. Disable them unless specifically needed.
SIGTERM handler: Always register a shutdown handler to flush buffered telemetry before the process exits.
Cross-References
Reference: otel-js skill — references/declarative-setup.md for the package version fetch table, activation, ESM/CJS rules, v2.0 migration facts.
General conventions: ollygarden-otel-declarative-config — anti-patterns and common YAML patterns.
1---2name: ollygarden-otel-js-setup3description: Ollygarden's recommended pattern for setting up OpenTelemetry in Node.js services. Covers project structure, which auto-instrumentations to disable, the entry-point ordering, the programmatic NodeSDK fallback, and the setup checklist (no query strings in telemetry, startup/non-request span hygiene, declarative YAML config, standard OTEL_* env vars honored, lean resource with service.instance.id) closed by a required verification report. Use when adding OTel to a Node.js project, structuring telemetry code, or reviewing an existing setup. Triggers on "node otel setup", "NodeSDK pattern", "auto instrumentation node", "url.query", "query parameter PII", "service.instance.id".4---56# JS/Node.js SDK Setup Conventions78## Status decision: declarative vs programmatic910The `@opentelemetry/configuration` package is experimental. Default to declarative config11for new projects. Use the programmatic NodeSDK fallback below if stability is critical or12the project needs runtime configuration that YAML cannot express.1314## Setup Checklist — verify every item before you finish1516Setup is not done when the SDK boots. Each unchecked item below produces a specific17telemetry-quality finding in production; work through all of them.1819- [ ] **Do not export query strings or other user input.** Telemetry must not capture data20 that can carry user input by default, and the query string is exactly that — yet the Node21 HTTP instrumentation (`@opentelemetry/instrumentation-http`, active through22 `auto-instrumentations-node`) exports it out of the box: `url.query` on server spans and23 folded inside `url.full` on client spans under the stable HTTP semantics. **Check which24 semantic-convention mode your installed instrumentation actually emits** — many contrib25 versions still default to the *old* HTTP attributes, where the query rides in `http.target`26 on server spans and inside `http.url` on client spans; redacting only the stable27 `url.query`/`url.full` keys then leaves the marker exposed. Only userinfo credentials are28 redacted by default; everything else — `GET /owners?lastName=Smith`, search terms, tokens29 pasted into links — goes out verbatim (Critical *PII Leakage* finding). Strip it30 **in-process, before it leaves the application** — overwrite or delete whichever attributes31 carry it in your mode (`url.query`/`url.full`, and/or `http.target`/`http.url`) in the HTTP32 instrumentation's own attribute hook (`applyCustomAttributesOnSpan` /33 `startIncomingSpanHook`) or in a span-processor / exporter wrapper that rewrites the34 attribute before export. A Collector `transform`/`redaction` processor is only35 defense-in-depth, never the sole control: the raw value has already crossed the process36 boundary and may sit in in-transit buffers, debug logs, or an alternate export path that37 skips the Collector. The route template (`http.route`,38 `url.path`) already answers "which endpoint"; if a specific parameter is genuinely needed as39 telemetry, capture it deliberately as a bounded, named attribute — never by keeping the raw40 query string. Leave the opt-in header-capture knobs41 (`OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_*`) off for the same reason. Verify by sending a42 request with a known marker value in a query parameter and inspecting the exported span: the43 marker must not appear anywhere.4445- [ ] **Keep startup and other non-request work from polluting trace shapes.** Auto46 instrumentation patches `http`, database drivers, and clients process-wide, so any outbound47 call made during boot — a migration, a warm-up HTTP request, a connection-pool probe — is48 emitted as a parentless CLIENT **root** span because no request context is active yet49 (*Root Client Span* finding). Two obligations: (1) either wrap deliberate startup work in an50 explicit application-startup span so those child spans have a parent, or suppress51 instrumentation for it (run it before `startNodeSDK`, or drop init-phase spans with a52 sampler / Collector rule) — do not ship detached CLIENT roots by default; (2) never build53 span names from unbounded input. Custom spans named with an id, a URL, or any per-request54 value (e.g. `tracer.startSpan('job ' + jobId)`) create unbounded span-name cardinality; name55 the span for the operation and put the varying value in a bounded attribute. Verify by56 booting the app and inspecting the first exported traces: no parentless CLIENT roots, and no57 per-request/per-boot identifiers anywhere in span names.5859- [ ] **Configure the SDK declaratively — one YAML document, not option sprawl.** A scatter of60 `OTEL_*` flags plus hand-wired `new NodeSDK({...})` exporter/processor code is the61 anti-pattern: operators cannot review or change the telemetry pipeline as a single document62 without a redeploy. Prefer the declarative model — `startNodeSDK()` from63 `@opentelemetry/sdk-node` loads the file named by `OTEL_CONFIG_FILE` through the experimental64 `@opentelemetry/configuration` package (mechanics: `otel-js` skill,65 `references/declarative-setup.md`; YAML conventions: `ollygarden-otel-declarative-config`).66 Keep the programmatic `NodeSDK` fallback only when stability is critical or the config needs67 runtime values YAML cannot express (see the status decision above). **The file replaces68 option/env-based configuration — not code.** Components that must run in code — the69 query-string redaction from the first item, the startup-span policy from the second, the70 disabled `fs`/`dns`/`net` instrumentations — stay in place and are passed to71 `startNodeSDK()`; going declarative does not discharge those items, and every earlier item's72 verification must be re-run after the switch. The file must also preserve the standard73 `OTEL_*` contract via `${ENV}` substitution — the next item spells that out. Verify by74 changing a config value (e.g. the sampler argument or the exporter endpoint) in the YAML and75 confirming behavior changes with no code change and no rebuild.7677- [ ] **Honor the standard `OTEL_*` environment variables end-to-end.**78 `OTEL_EXPORTER_OTLP_*`, `OTEL_SERVICE_NAME`, and `OTEL_RESOURCE_ATTRIBUTES` must all take79 effect at runtime. Do not invent custom variables (`SERVICE_VERSION`,80 `DEPLOYMENT_ENVIRONMENT`, ...) for values the standard variables already express, and never81 let a hardcoded default clobber an operator-supplied value. The programmatic `NodeSDK` reads82 these variables directly, but **a declarative YAML loaded via `OTEL_CONFIG_FILE` does not** —83 when the config file is set the SDK stops falling back to the environment, so each standard84 variable must be threaded in explicitly with substitution:8586 ```yaml87 resource:88 attributes:89 - name: service.name90 value: "${OTEL_SERVICE_NAME:-my-node-service}"91 type: string92 # standard deploy-time attributes (service.version,93 # deployment.environment.name, ...) arrive through the STANDARD variable:94 attributes_list: "${OTEL_RESOURCE_ATTRIBUTES}"95 ```9697 Do not also hardcode a deploy-varying key (e.g. `deployment.environment.name`) under98 `attributes`, or the literal silently wins and misfiles every signal. Point the exporter at99 `${OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}`. Verify by booting with all three100 standard variables set to non-default values: `OTEL_SERVICE_NAME` and101 `OTEL_RESOURCE_ATTRIBUTES` must land on the exported resource, while the overridden102 `OTEL_EXPORTER_OTLP_ENDPOINT` — a destination, not a telemetry attribute — is confirmed at103 the receiving collector, not on the spans.104105- [ ] **Keep the resource lean and add `service.instance.id`.** The Node SDK's default106 detector set (`envDetector`, `processDetector`, `hostDetector`) does **not** include107 `service.instance.id` — the `serviceinstance` detector that would mint one is experimental108 and off by default — so unless you act, every process of a replicated service is109 indistinguishable (*Missing service.instance.id* finding). Add a per-process UUID110 (`crypto.randomUUID()`) alongside `service.version`: programmatically via111 `resourceFromAttributes` (the string key is `service.instance.id`, or `ATTR_SERVICE_INSTANCE_ID`112 from `@opentelemetry/semantic-conventions/incubating`), or in declarative YAML as a resource113 attribute fed a per-process value. Keep the rest of the resource minimal — `service.name`,114 `service.version`, `service.instance.id`, `deployment.environment.name` is the full set. Do115 not enable host/process/OS resource detectors that stamp discouraged `process.*` / `os.*`116 attributes (*Discouraged Resource Attribute* finding). Verify by inspecting the exported117 resource: `service.instance.id` is present and differs between two separate process starts,118 and no `process.*`/`os.*` attributes are attached.119120## Required: Verification Report121122Setup is not complete until you produce this report. It is a table with one row per123checklist item above. Fill each row with artifacts from THIS run — the marker value you124sent, an excerpt of the exported span dump, a trace id, the config value you changed.125Never a restatement of the requirement, never a bare "done".126127The table below is an **illustrative example, not a report you can submit**: every value in128it is a placeholder showing the expected *shape* of evidence. Replace every cell with your129own run's artifacts. If you did not run a check, write `GAP — not run` in that row and130leave it visible — a missing or hand-waved row is itself a finding.131132Example (illustrative values — replace every cell with your own run's evidence):133134| Item | Check performed | Observed evidence |135| -- | -- | -- |136| No query strings exported | `GET /owners?lastName=MARKER_7f3a` → inspected exported span (check which semconv mode: `url.query`/`url.full` vs legacy `http.target`/`http.url`) | query attribute absent; `MARKER_7f3a` nowhere in span dump (trace `4bf9...`) |137| Startup / non-request span hygiene | Booted app, inspected first exported traces | No parentless CLIENT roots; startup DB call is child of `app.startup` span; no ids in span names |138| SDK configured declaratively | Changed sampler arg in `configs/otel.yaml`, restarted without rebuild | Sampling ratio changed on exported spans; no code edit needed |139| Standard `OTEL_*` honored | Booted with `OTEL_SERVICE_NAME`/`OTEL_RESOURCE_ATTRIBUTES` set to non-defaults, and `OTEL_EXPORTER_OTLP_ENDPOINT` pointed at a marker collector | `service.name`/`deployment.environment.name` carry the supplied values on the exported resource; the marker collector's log / receipt confirms telemetry arrived at the overridden endpoint (the endpoint is a destination, evidenced there, not on the spans) |140| Lean resource + `service.instance.id` | Dumped resource from two separate process starts | `service.instance.id` present and differs across starts; no `process.*`/`os.*` attrs |141142A row you cannot fill with observed evidence is a visible gap — that item is not done.143Do not delete the row, copy these example values, or write "N/A" to hide it; go run the144check and record what you actually saw.145146## Dependencies147148```bash149npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/configuration150npm install @opentelemetry/auto-instrumentations-node151npm install @opentelemetry/semantic-conventions152```153154Fetch the latest versions of these packages (see the `otel-js` skill's Sources of Truth)155and pin them in `package.json` as appropriate for your project.156157## Project Structure158159```160src/161├── telemetry/162│ ├── constants.ts # Service scope and telemetry constants163│ ├── setup.ts # SDK initialization164│ └── index.ts # Re-exports165├── index.ts # App entry point (imports telemetry first)166configs/167└── otel.yaml # Declarative configuration168```169170## Instrumentation File (`src/telemetry/setup.ts`)171172```typescript173import { startNodeSDK } from '@opentelemetry/sdk-node';174import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';175176// startNodeSDK loads OTEL_CONFIG_FILE and registers the configured providers.177// new NodeSDK(...) is the separate programmatic path and does not load the file.178export const sdk = startNodeSDK({179 instrumentations: [180 getNodeAutoInstrumentations({181 '@opentelemetry/instrumentation-fs': { enabled: false },182 '@opentelemetry/instrumentation-dns': { enabled: false },183 '@opentelemetry/instrumentation-net': { enabled: false },184 }),185 ],186});187```188189## Entry Point (`src/index.ts`)190191```typescript192// IMPORTANT: Import and start telemetry BEFORE any other imports193import { sdk } from './telemetry/setup';194195import { app } from './app';196197const PORT = process.env.PORT ?? 3000;198const server = app.listen(PORT, () => {199 console.log(`Server listening on port ${PORT}`);200});201202process.on('SIGTERM', () => {203 server.close(async () => {204 await sdk.shutdown();205 process.exit(0);206 });207});208```209210## Fallback: Programmatic NodeSDK Setup211212If declarative config is not suitable (e.g., need dynamic runtime config or older SDK version),213use programmatic setup:214215```typescript216import { NodeSDK } from '@opentelemetry/sdk-node';217import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';218import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';219import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';220import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';221import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';222import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';223import { resourceFromAttributes, envDetector } from '@opentelemetry/resources';224import { ATTR_SERVICE_INSTANCE_ID } from '@opentelemetry/semantic-conventions/incubating';225import { randomUUID } from 'node:crypto';226227// service.name / service.version / deployment.environment.name arrive through the standard228// OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES variables (applied by envDetector below);229// do not invent custom vars for them. Mint only the per-process service.instance.id that no230// default detector provides.231const resource = resourceFromAttributes({232 [ATTR_SERVICE_INSTANCE_ID]: randomUUID(),233});234235export const sdk = new NodeSDK({236 resource,237 // Only envDetector: honor the OTEL_* contract without stamping discouraged238 // process.* / os.* attributes from the default process/host detectors.239 resourceDetectors: [envDetector],240 traceExporter: new OTLPTraceExporter(),241 metricReader: new PeriodicExportingMetricReader({242 exporter: new OTLPMetricExporter(),243 exportIntervalMillis: 30_000,244 }),245 logRecordProcessors: [246 new BatchLogRecordProcessor(new OTLPLogExporter()),247 ],248 instrumentations: [249 getNodeAutoInstrumentations({250 '@opentelemetry/instrumentation-fs': { enabled: false },251 '@opentelemetry/instrumentation-dns': { enabled: false },252 '@opentelemetry/instrumentation-net': { enabled: false },253 }),254 ],255});256```257258## Key Details259260- **Disable noisy instrumentations**: `fs`, `dns`, and `net` instrumentations generate high volumes of low-value spans. Disable them unless specifically needed.261- **SIGTERM handler**: Always register a shutdown handler to flush buffered telemetry before the process exits.262263## Cross-References264265- Reference: `otel-js` skill — `references/declarative-setup.md` for the package version fetch table, activation, ESM/CJS rules, v2.0 migration facts.266- General conventions: `ollygarden-otel-declarative-config` — anti-patterns and common YAML patterns.
Run npx skillmds@latest add ollygarden/ollygarden-otel-js-setup in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Ollygarden's recommended pattern for setting up OpenTelemetry in Node.js services. Covers project structure, which auto-instrumentations to disable, the entry-point ordering, the programmatic NodeSDK fallback, and the setup checklist (no query strings in telemetry, startup/non-request span hygiene, declarative YAML config, standard OTEL_* env vars honored, lean resource with service.instance.id) closed by a required verification report. Use when adding OTel to a Node.js project, structuring telemetry code, or reviewing an existing setup. Triggers on "node otel setup", "NodeSDK pattern", "auto instrumentation node", "url.query", "query parameter PII", "service.instance.id". It is listed under Docs & Writing on SkillMD.
This skill has not completed SkillMD's automated safety review yet. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
ollygarden (@ollygarden) published this skill. Their other Agent Skills are listed on their SkillMD profile.