# Instrument: Node & OpenTelemetry

## One call, any Node process

```bash
npm i @getloopops/otel
```

```js
import { initLoopOps } from "@getloopops/otel";
initLoopOps({ serviceName: "my-service" }); // reads LOOPOPS_URL + LOOPOPS_INGEST_KEY
```

Every OTel span your process emits — your own tracers, plus any
instrumentations — flows to LoopOps as ordinary OTLP. Install
`@opentelemetry/auto-instrumentations-node` alongside and
`@getloopops/otel` ≥0.4 picks it up automatically: its outbound client
spans (db, http, queues) are what the system map turns into relationships
with latency, and `serviceVersion` anchors deploy-level verify. Without
them the map grades `wiring.relationships: false` and stays one box.
(Opt out with `autoInstrument: false`; pass your own list via
`instrumentations`.)

## Already running OpenTelemetry?

Point your existing exporter — no LoopOps package needed:

```js
new OTLPTraceExporter({
  url: "https://loopops.dev/v1/traces",
  headers: { authorization: `Bearer ${process.env.LOOPOPS_INGEST_KEY}` }
})
```

Keep these resource attributes — they power the loop's correlation and
regression detection:

- `service.name` — the workload identity in the console
- `service.version` — rides on every event; verify and regression detection anchor to it
- `deployment.environment.name` — environment scoping

## Exceptions

Use OTel's canonical path — `span.recordException(err)` — and LoopOps extracts
the type, message, and stacktrace from the exception event; errored spans
cluster on the real failure, not the span name.

## Collector gateway

Running an OpenTelemetry Collector? Add an `otlphttp` exporter:

```yaml
exporters:
  otlphttp/loopops:
    endpoint: https://loopops.dev
    headers:
      authorization: Bearer ${env:LOOPOPS_INGEST_KEY}
```

The collector's batching, retry, and queueing all apply; LoopOps ingest caps
are in [Limits & health](/docs/limits-and-health).

**Next:** [Any language over plain OTLP →](/docs/instrument-any-otlp)