# Monitor a Node.js app with OpenTelemetry

> Install the OpenTelemetry auto-instrumentations, set the OTLP environment variables and start Node with --require. Express requests and errors reach Lampwick.

The OpenTelemetry auto-instrumentations for Node.js send requests, database queries, outgoing calls and errors to Lampwick without code changes. Logs come from pino, winston or bunyan. This page is the setup we ran against Lampwick; the general reference is [OpenTelemetry](/docs/opentelemetry/).

Tested with Node.js 26.9, `@opentelemetry/auto-instrumentations-node` 0.83.0 (`@opentelemetry/sdk-node` 0.301.1), Express 5.3, `pg` 8.23 and pino 10.4, on 10 November 2026.

## Requirements

- Node.js 22.15 or later (what the current OpenTelemetry packages require).
- The project's ingest token, from the **Connect your app** card on the project's **Settings** page. Only team owners see it.

## Let your agent do it

Paste this into your coding agent, in the app's repository:

```text
Add OpenTelemetry to this Node.js app so it reports to Lampwick: follow https://lampwick.dev/docs/node.md exactly (zero-code setup, environment variables, no code changes besides a pino logger if the app has no logger). Read the ingest token from the environment, never commit it.
```

## Install the packages

```bash
npm install @opentelemetry/api @opentelemetry/auto-instrumentations-node
```

## Set the environment variables

```bash
export OTEL_EXPORTER_OTLP_ENDPOINT=https://app.lampwick.dev
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_HEADERS=x-lampwick-token=YOUR_PROJECT_TOKEN
export OTEL_SERVICE_NAME=my-app
export OTEL_METRICS_EXPORTER=none
export OTEL_LOGS_EXPORTER=otlp
export NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register"
```

`YOUR_PROJECT_TOKEN` is the project's ingest token. `NODE_OPTIONS` loads the instrumentation before your app's own code, which it needs to patch `http`, `express` and `pg`. You can also put the `--require` in your start command instead: `node --require @opentelemetry/auto-instrumentations-node/register app.js`.

## The app we tested

Nothing in it is specific to OpenTelemetry or Lampwick:

```js
const express = require('express');
const pino = require('pino');
const { Pool } = require('pg');

const logger = pino();
const app = express();
const db = new Pool({ connectionString: process.env.DATABASE_URL });

app.get('/orders/:id', async (req, res) => {
  const { rows } = await db.query('SELECT id, total FROM orders WHERE id = $1', [req.params.id]);
  logger.info({ orderId: req.params.id }, 'order looked up');
  res.json(rows[0] ?? null);
});

app.get('/boom', () => {
  throw new Error('Checkout failed for order 42');
});

app.listen(3000, () => logger.info('listening on :3000'));
```

## Check that it works

Start the app and open `/orders/1`, `/orders/2` and `/boom`. Within about 5 seconds the project shows:

- **Requests**: `GET /orders/:id` (2) and `GET /boom` (1, status 500).
- **Queries**: `SELECT id, total FROM orders WHERE id = $1`, linked to its request.
- **Issues**: `Error: Checkout failed for order 42`, unhandled, at `app.js:16`, with the stack trace. One occurrence, although Express 5 records the error on 3 spans.
- **Logs**: `listening on :3000` and 2 × `order looked up`; the last two are linked to their request.

## What's missing

- **`console.log` isn't sent.** The instrumentations only capture pino, winston and bunyan. Use one of those for logs you want in Lampwick.
- **Errors you catch yourself** don't become issues unless you record them: `trace.getActiveSpan()?.recordException(err)` with `trace` from `@opentelemetry/api`.
- **Bundled or transpiled code**: stack traces point at the files Node runs. Source maps aren't applied.
- **Users**: set `enduser.id` on the active span (`trace.getActiveSpan()?.setAttribute('enduser.id', user.id)`) to see them under **Users**.

## Troubleshooting

| What you see | Cause and fix |
|---|---|
| Requests have no route (`GET` only) | The instrumentation loaded after `express`. Use `NODE_OPTIONS` or `--require`, not an `import` halfway through your app. |
| `OTLPExporterError: Unauthorized` | The token is wrong. Copy it again from **Settings**. |
| Metric export errors every minute | `OTEL_METRICS_EXPORTER=none` isn't set in the app's environment. |
