# Instrument Express with OpenTelemetry

> Send traces, logs and metrics from an Express app to Telemetry Machine with OpenTelemetry's zero-code Node.js instrumentation. No code changes.

OpenTelemetry's zero-code Node.js instrumentation traces every request, sends your Pino, Winston or Bunyan logs as logs, and records HTTP and runtime metrics. Your code doesn't change.

## Create the datasets

Each signal goes to its own dataset. Create three, and an ingest key that can write to all of them, with [tm-cli](https://telemetrymachine.com/docs/quick-start#tm-cli) or the [MCP server](https://telemetrymachine.com/docs/quick-start#option-1-set-up-with-a-coding-agent):

```console tab="tm-cli"
$ tm-cli datasets create --name shop-prod-traces --type traces
$ tm-cli datasets create --name shop-prod-logs --type logs
$ tm-cli datasets create --name shop-prod-metrics --type metrics
$ tm-cli api-keys create --name shop-api --dataset shop-prod-traces --dataset shop-prod-logs --dataset shop-prod-metrics
```

```json tab="MCP"
create_dataset { "name": "shop-prod-traces", "signal_type": "traces" }
create_dataset { "name": "shop-prod-logs", "signal_type": "logs" }
create_dataset { "name": "shop-prod-metrics", "signal_type": "metrics" }
create_ingest_key {
  "name": "shop-api",
  "datasets": ["shop-prod-traces", "shop-prod-logs", "shop-prod-metrics"]
}
```

tm-cli shows the key once, in `api_key.api_key`. Over MCP, your agent gets it once and saves it in your app's configuration. In the app, [create each dataset](https://telemetrymachine.com/datasets/new), then a key for all three under **API keys** in your organization's settings.

## Install

```console
$ npm install @opentelemetry/api @opentelemetry/auto-instrumentations-node
```

## Configure

```bash
export TM_INGEST_KEY='<ingest-key>'
export OTEL_SERVICE_NAME=shop-api
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.telemetrymachine.com
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="X-API-Key=$TM_INGEST_KEY,X-Dataset-Name=shop-prod-traces"
export OTEL_EXPORTER_OTLP_LOGS_HEADERS="X-API-Key=$TM_INGEST_KEY,X-Dataset-Name=shop-prod-logs"
export OTEL_EXPORTER_OTLP_METRICS_HEADERS="X-API-Key=$TM_INGEST_KEY,X-Dataset-Name=shop-prod-metrics"
```

## Run

Load the instrumentation before your app:

```console
$ node --require @opentelemetry/auto-instrumentations-node/register app.js
```

If something else starts Node, such as `npm start`, set it in `NODE_OPTIONS` instead:

```bash
export NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register"
```

## Check it arrived

Send your app a few requests, then open each explorer and choose the dataset:

- **Traces**, `shop-prod-traces`: one server span per request, named after its route, such as `GET /items/:id`, with a span for each middleware.
- **Logs**, `shop-prod-logs`: your logger's records, each with its request's `trace.trace_id`. `console.log` isn't sent.
- **Metrics**, `shop-prod-metrics`: `http.server.request.duration` and the `nodejs.*` and `v8js.*` runtime metrics, sent once a minute.

Or count spans by name with tm-cli or the MCP server:

```console tab="tm-cli"
$ tm-cli query shop-prod-traces --since 15m --select "name, count(*)" --group-by name
```

```json tab="MCP"
run_query {
  "dataset": "shop-prod-traces",
  "description": "Spans by name",
  "since": "15m",
  "select": "name, count(*)",
  "group_by": "name"
}
```

## Troubleshooting

- **Server spans are named just `GET`, with no `http.route`:** your app is an ES module (`"type": "module"`, or `.mjs`). Start it with the loader hook as well: `node --experimental-loader=@opentelemetry/instrumentation/hook.mjs --import @opentelemetry/auto-instrumentations-node/register app.js`.
- **The app no longer stops on `SIGTERM`:** the instrumentation listens for `SIGTERM` to send what it holds, so Node doesn't exit by itself. Close your server on it: `process.on('SIGTERM', () => server.close())`.
- **Winston logs don't arrive:** also install `@opentelemetry/winston-transport`. Pino and Bunyan need nothing more.
- **`MetadataLookupWarning` at startup:** the Google Cloud resource detector found no metadata server. Outside Google Cloud, skip it with `export OTEL_NODE_RESOURCE_DETECTORS=env,host,os,process`.

---

All pages: https://telemetrymachine.com/docs/llms.txt
