Backend traces
Step 5. Instrument the backend
Section titled “Step 5. Instrument the backend”The backend is the same as in quickstart 1: the OpenTelemetry Node SDK has never needed the ouo11y SDK. Stop the API with Ctrl+C in its terminal tab (still in backend/), then:
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/sdk-trace @opentelemetry/exporter-trace-otlp-http @opentelemetry/instrumentation-http @opentelemetry/instrumentation-express @opentelemetry/baggage-span-processor @opentelemetry/resources @opentelemetry/semantic-conventionsCreate backend/instrumentation.js:
backend/instrumentation.js
// OpenTelemetry SDK init. Loaded before the app with `node --require ./instrumentation.js`// so the http and express instrumentations can patch those modules before server.js loads them.const { NodeSDK } = require("@opentelemetry/sdk-node");const { BatchSpanProcessor } = require("@opentelemetry/sdk-trace");const { OTLPTraceExporter } = require("@opentelemetry/exporter-trace-otlp-http");const { HttpInstrumentation } = require("@opentelemetry/instrumentation-http");const { ExpressInstrumentation } = require("@opentelemetry/instrumentation-express");const { BaggageSpanProcessor, ALLOW_ALL_BAGGAGE_KEYS } = require("@opentelemetry/baggage-span-processor");const { resourceFromAttributes } = require("@opentelemetry/resources");const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } = require("@opentelemetry/semantic-conventions");
// The ouo collector, the only ingest. Moved ports? OUO_OTLP_ENDPOINT=http://localhost:14318const endpoint = process.env.OUO_OTLP_ENDPOINT || "http://localhost:4318";
// Every request needs the project credential: Basic base64("<project id>:<project token>").// This is the quickstart's `demo` project: base64("demo:pt_dev_demo_0000000000000000").const exporter = new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers: { Authorization: "Basic ZGVtbzpwdF9kZXZfZGVtb18wMDAwMDAwMDAwMDAwMDAw" },});
const sdk = new NodeSDK({ resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "tiny-shop-otel-backend", [ATTR_SERVICE_VERSION]: "1.0.0", "deployment.environment.name": "local", }), spanProcessors: [ // The browser sends `baggage: session.id=...,user.id=...`; copy it onto every span so // backend spans can be searched by the RUM session. new BaggageSpanProcessor(ALLOW_ALL_BAGGAGE_KEYS), // OTel JS 3.0: options object, `{ exporter }` (2.x took the exporter itself). new BatchSpanProcessor({ exporter }), ], instrumentations: [new HttpInstrumentation(), new ExpressInstrumentation()],});
sdk.start();console.log(`[otel] tracing to ${endpoint}/v1/traces`);
const shutdown = () => sdk.shutdown().finally(() => process.exit(0));process.on("SIGINT", shutdown);process.on("SIGTERM", shutdown);- The exporter sends spans to the collector over OTLP/HTTP with the
demoproject credential,Basic base64("demo:pt_dev_demo_0000000000000000"). - The resource names the service,
tiny-shop-otel-backend, its version and its environment. HttpInstrumentationandExpressInstrumentationmake a server span for every request, and a span for each Express middleware and handler.BaggageSpanProcessoris the line that matters for this quickstart. In step 6a the browser sendsbaggage: session.id=...,user.id=...with every API call, and this processor copies both onto every backend span. That is how a backend span is found by its RUM session.
Replace backend/server.js with:
backend/server.js
// The API. HTTP server spans come from the auto-instrumentation; the spans below are manual.const express = require("express");const cors = require("cors");const { trace, SpanStatusCode } = require("@opentelemetry/api");
const tracer = trace.getTracer("tiny-shop-otel-backend");const app = express();// The browser calls this API from another origin and adds `traceparent` and `baggage`// headers; cors() answers the preflight for them.app.use(cors());app.use(express.json());
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));const products = [ { id: 1, name: "Coffee", price: 4 }, { id: 2, name: "Tea", price: 3 }, { id: 3, name: "Cake", price: 6 },];
app.get("/api/products", async (req, res) => { // A manual span around a "database" call. const result = await tracer.startActiveSpan("db.select products", async (span) => { span.setAttribute("db.system.name", "fake-db"); await sleep(50 + Math.random() * 100); span.setAttribute("app.products.count", products.length); span.end(); return products; }); res.json(result);});
app.post("/api/checkout", async (req, res) => { const items = req.body.items || []; // A parent span with two nested children. await tracer.startActiveSpan("checkout", async (span) => { span.setAttribute("app.cart.items", items.length); await tracer.startActiveSpan("validate cart", async (child) => { await sleep(30); child.end(); }); await tracer.startActiveSpan("charge card", async (child) => { await sleep(120); child.addEvent("payment accepted"); child.end(); }); span.end(); }); res.json({ ok: true, orderId: Math.floor(Math.random() * 100000) });});
app.get("/api/fail", (req, res) => { // Enrich the auto-created HTTP span with the error. const span = trace.getActiveSpan(); const err = new Error("inventory service unavailable"); span?.recordException(err); span?.setStatus({ code: SpanStatusCode.ERROR, message: err.message }); res.status(500).json({ error: err.message });});
const port = Number(process.env.PORT || 5001);app.listen(port, () => console.log(`tiny-shop API on http://localhost:${port}`));The routes are the same as in step 2. The new parts are spans you write by hand: db.select products around a pretend database call, checkout with two children, validate cart and charge card, and on /api/fail an exception and an error status on the HTTP span the instrumentation made.
The SDK has to load before Express so the instrumentation can patch it. Save that as the start script, then start the API:
npm pkg set scripts.start="node --require ./instrumentation.js server.js"npm startCollector on 14318 (step 4c)? Stop it and start it with OUO_OTLP_ENDPOINT=http://localhost:14318 npm start instead.
What you should see
Section titled “What you should see”The API prints:
[otel] tracing to http://localhost:4318/v1/tracesThe shop page still works as before. When you click Checkout, the API makes this trace. On its own it starts at the API: the browser span above it comes in the next step.
- tiny-shop-otel-backend
POST /api/checkout158 ms middleware - corsMiddleware1 msmiddleware - jsonParser2 msrequest handler - /api/checkout1 mscheckoutapp.cart.items151 msvalidate cart30 mscharge cardevent: payment accepted121 ms
Next: the browser, by hand.