Ollygarden otel js setup
OllyGarden's opinionated AI agent skills for OpenTelemetry and observability workflows
npx -y skills add ollygarden/skills --skill ollygarden-otel-js-setupAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
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".
SKILL.md
15.3 KB, ~3.4k tokens by cl100k_base, as published. Nobody here has run it
JS/Node.js SDK Setup Conventions
Status decision: declarative vs programmatic
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 throughauto-instrumentations-node) exports it out of the box:url.queryon server spans and folded insideurl.fullon 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 inhttp.targeton server spans and insidehttp.urlon client spans; redacting only the stableurl.query/url.fullkeys 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/orhttp.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 Collectortransform/redactionprocessor 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 beforestartNodeSDK, 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-wirednew 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-nodeloads the file named byOTEL_CONFIG_FILEthrough the experimental@opentelemetry/configurationpackage (mechanics:otel-jsskill,references/declarative-setup.md; YAML conventions:ollygarden-otel-declarative-config). Keep the programmaticNodeSDKfallback 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 disabledfs/dns/netinstrumentations — stay in place and are passed tostartNodeSDK(); 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 standardOTEL_*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, andOTEL_RESOURCE_ATTRIBUTESmust 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 programmaticNodeSDKreads these variables directly, but a declarative YAML loaded viaOTEL_CONFIG_FILEdoes 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) underattributes, 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_NAMEandOTEL_RESOURCE_ATTRIBUTESmust land on the exported resource, while the overriddenOTEL_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 includeservice.instance.id— theserviceinstancedetector 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()) alongsideservice.version: programmatically viaresourceFromAttributes(the string key isservice.instance.id, orATTR_SERVICE_INSTANCE_IDfrom@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.nameis the full set. Do not enable host/process/OS resource detectors that stamp discouragedprocess.*/os.*attributes (Discouraged Resource Attribute finding). Verify by inspecting the exported resource:service.instance.idis present and differs between two separate process starts, and noprocess.*/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.
Dependencies
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/configuration
npm install @opentelemetry/auto-instrumentations-node
npm install @opentelemetry/semantic-conventions
Fetch the latest versions of these packages (see the otel-js skill's Sources of Truth)
and pin them in package.json as appropriate for your project.
Project Structure
src/
├── telemetry/
│ ├── constants.ts # Service scope and telemetry constants
│ ├── setup.ts # SDK initialization
│ └── index.ts # Re-exports
├── index.ts # App entry point (imports telemetry first)
configs/
└── otel.yaml # Declarative configuration
Instrumentation File (src/telemetry/setup.ts)
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, andnetinstrumentations 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-jsskill —references/declarative-setup.mdfor 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.