Skip to content

Backend traces

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:

Terminal window
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-conventions

Create backend/instrumentation.js:

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:14318
const 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 demo project credential, Basic base64("demo:pt_dev_demo_0000000000000000").
  • The resource names the service, tiny-shop-otel-backend, its version and its environment.
  • HttpInstrumentation and ExpressInstrumentation make a server span for every request, and a span for each Express middleware and handler.
  • BaggageSpanProcessor is the line that matters for this quickstart. In step 6a the browser sends baggage: 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
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:

Terminal window
npm pkg set scripts.start="node --require ./instrumentation.js server.js"
Terminal window
npm start

Collector on 14318 (step 4c)? Stop it and start it with OUO_OTLP_ENDPOINT=http://localhost:14318 npm start instead.

The API prints:

[otel] tracing to http://localhost:4318/v1/traces

The 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.

  1. tiny-shop-otel-backendPOST /api/checkout
    158 ms
  2. middleware - corsMiddleware
    1 ms
  3. middleware - jsonParser
    2 ms
  4. request handler - /api/checkout
    1 ms
  5. checkoutapp.cart.items
    151 ms
  6. validate cart
    30 ms
  7. charge cardevent: payment accepted
    121 ms
POST /api/checkout as the API traces it. The manual spans are the last three rows.

Next: the browser, by hand.