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 or the MCP server:
$ 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-metricscreate_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, then a key for all three under API keys in your organization's settings.
Install
$ npm install @opentelemetry/api @opentelemetry/auto-instrumentations-nodeConfigure
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:
$ node --require @opentelemetry/auto-instrumentations-node/register app.jsIf something else starts Node, such as npm start, set it in NODE_OPTIONS instead:
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 asGET /items/:id, with a span for each middleware. - Logs,
shop-prod-logs: your logger's records, each with its request'strace.trace_id.console.logisn't sent. - Metrics,
shop-prod-metrics:http.server.request.durationand thenodejs.*andv8js.*runtime metrics, sent once a minute.
Or count spans by name with tm-cli or the MCP server:
$ tm-cli query shop-prod-traces --since 15m --select "name, count(*)" --group-by namerun_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 nohttp.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 forSIGTERMto 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. MetadataLookupWarningat startup: the Google Cloud resource detector found no metadata server. Outside Google Cloud, skip it withexport OTEL_NODE_RESOURCE_DETECTORS=env,host,os,process.