Your backend
There is no ouo11y backend SDK, and there will not be one. Instrument your services with the OpenTelemetry SDK for their language and export OTLP to the same collector as the browser. Three things join the backend to the user’s session:
- W3C Trace Context. The browser’s fetch span sends
traceparentto the URLs you allow (tracing.propagateToin our SDK,propagateTraceHeaderCorsUrlsin plain OpenTelemetry). Your server span becomes its child, so one trace runs from the click to the database. - Baggage. The browser sends
session.id,user.idandouo.platformin thebaggageheader. A span processor copies them onto your spans, so you can search backend traces by session. - CORS, for an API on another origin: the preflight must allow
traceparentandbaggage, or the browser drops them.
- 1 GET /api/products · traceparent + baggage
- 2 extract context · TraceContext + Baggage propagators
- 3 server span · parent = browser fetch span; session.id from baggage
- 4 spans · OTLP + project credential
- 5 one trace · searchable by span.session.id
What arrives at your server looks like this:
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01baggage: session.id=a4d47dfa-…,user.id=3c63803d-…,ouo.platform=webThe credential
Section titled “The credential”Backend exporters authenticate like the browser, with the project credential as HTTP Basic. Build the header value once:
printf '%s' 'demo:pt_dev_demo_0000000000000000' | base64That prints ZGVtbzpwdF9kZXZfZGVtb18wMDAwMDAwMDAwMDAwMDAw for the local stack’s demo project. Without the header the collector answers 401 and your exporter logs an export error. Server-side requests carry no Origin, so CORS does not apply to them; the project’s rate and size limits do.
From Quickstart 2, OpenTelemetry JS 3.0 with Express. Install:
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/sdk-trace @opentelemetry/resources @opentelemetry/semantic-conventions @opentelemetry/exporter-trace-otlp-http @opentelemetry/instrumentation-http @opentelemetry/instrumentation-express @opentelemetry/baggage-span-processorThen this file, loaded before your app so the instrumentations can patch http and express first:
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);node --require ./instrumentation.js server.jsThe terminal prints [otel] tracing to http://localhost:4318/v1/traces. BaggageSpanProcessor(ALLOW_ALL_BAGGAGE_KEYS) copies every baggage member onto every span; pass a predicate such as (key) => ["session.id", "user.id", "ouo.platform"].includes(key) to copy only ours.
With the OpenTelemetry Go SDK and otelhttp. Install:
go get go.opentelemetry.io/otel go.opentelemetry.io/otel/sdk go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp go.opentelemetry.io/contrib/instrumentation/net/http/otelhttpThe exporter reads its endpoint and headers from the environment:
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318export OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic%20ZGVtbzpwdF9kZXZfZGVtb18wMDAwMDAwMDAwMDAwMDAwexport OTEL_SERVICE_NAME=orders-apiThe OTLP spec says header values are URL-encoded, hence %20 for the space.
The span processor that copies baggage onto spans is short enough to own. (The contrib module go.opentelemetry.io/contrib/processors/baggagecopy does the same, if you prefer a dependency.)
package telemetry
import ( "context"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/baggage" sdktrace "go.opentelemetry.io/otel/sdk/trace")
// BaggageKeys are the members the ouo11y browser and mobile SDKs propagate.var BaggageKeys = []string{"session.id", "user.id", "ouo.platform"}
// BaggageSpanProcessor copies those baggage members onto every span as attributes.type BaggageSpanProcessor struct{}
var _ sdktrace.SpanProcessor = BaggageSpanProcessor{}
func (BaggageSpanProcessor) OnStart(ctx context.Context, span sdktrace.ReadWriteSpan) { bag := baggage.FromContext(ctx) for _, k := range BaggageKeys { if v := bag.Member(k).Value(); v != "" { span.SetAttributes(attribute.String(k, v)) } }}
func (BaggageSpanProcessor) OnEnd(sdktrace.ReadOnlySpan) {}func (BaggageSpanProcessor) Shutdown(context.Context) error { return nil }func (BaggageSpanProcessor) ForceFlush(context.Context) error { return nil }And the setup, called first in main:
package telemetry
import ( "context"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace")
func Setup(ctx context.Context) (shutdown func(context.Context) error, err error) { // TraceContext joins the browser's trace; Baggage carries session.id. // Set it before building the handler: otelhttp reads the global propagator when it is created. otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}, ))
exp, err := otlptracehttp.New(ctx) // OTEL_EXPORTER_OTLP_ENDPOINT and _HEADERS if err != nil { return nil, err } res, _ := resource.New(ctx, resource.WithFromEnv()) // OTEL_SERVICE_NAME, OTEL_RESOURCE_ATTRIBUTES tp := sdktrace.NewTracerProvider( sdktrace.WithResource(res), sdktrace.WithSpanProcessor(BaggageSpanProcessor{}), sdktrace.WithBatcher(exp), ) otel.SetTracerProvider(tp) return tp.Shutdown, nil}Wrap your handler, with CORS outside so the preflight is answered before a span opens:
handler := CORS(otelhttp.NewHandler(mux, "orders-api"))Call shutdown with a timeout on exit, or the spans of the last requests are lost. Do not call otel.SetTextMapPropagator again elsewhere with only TraceContext{}: that drops baggage, and your spans lose session.id.
Any other language
Section titled “Any other language”Every OpenTelemetry SDK reads the same environment variables:
| Variable | Value |
|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
the collector’s OTLP/HTTP base, http://localhost:4318 locally |
OTEL_EXPORTER_OTLP_HEADERS |
Authorization=Basic%20<base64 of "<project id>:<project token>"> |
OTEL_EXPORTER_OTLP_PROTOCOL |
http/protobuf |
OTEL_SERVICE_NAME |
your service name, for example orders-api |
OTEL_RESOURCE_ATTRIBUTES |
deployment.environment.name=production,service.version=1.4.2 |
OTEL_PROPAGATORS |
tracecontext,baggage (the default in most SDKs) |
Then add a baggage span processor. Java, .NET and Python have one in their contrib packages; in any other language it is the short OnStart hook shown for Go: read the baggage from the span’s parent context, set session.id, user.id and ouo.platform as attributes.
Use span attributes, not resource attributes: a resource describes the process, and one process serves many sessions.
Browsers strip any non-safelisted request header the preflight did not allow. If your Access-Control-Allow-Headers lists only content-type, the traceparent and baggage headers never reach you and the traces are not joined, while the request itself still succeeds. Allow them:
Access-Control-Allow-Headers: content-type, traceparent, tracestate, baggage- Express:
app.use(cors())from thecorspackage answers every preflight with the headers the browser asked for, which includes these. With an explicit list, add them:cors({ origin: "https://shop.example.com", allowedHeaders: ["content-type", "traceparent", "tracestate", "baggage"] }). - Go: in your CORS middleware,
w.Header().Set("Access-Control-Allow-Headers", "content-type, traceparent, tracestate, baggage"), and answerOPTIONSwith204before the request reachesotelhttp. - Anything else: the same header list, in your framework’s CORS settings or your gateway’s.
Same-origin APIs need nothing: no preflight happens.
Check it
Section titled “Check it”- Click through your app so it calls the API.
- In Grafana, Explore, pick Tempo and run
{ resource.service.name = "orders-api" }(your service name). The root of each trace is the browser’s span; your server span is its child. - Run
{ span.session.id = "<a session id>" }with an id from the ouo11y app: the browser’s spans and yours come back together.
If the server span is a root of its own, traceparent did not arrive: check propagateTo and CORS. If it is a child but has no session.id, baggage did not arrive or no baggage processor is registered.