Backend activity (server-side)¶
Your backend is just another OTLP source. Point an OpenTelemetry SDK at nilalytics and every request/operation becomes a span — successful or not — and, if you propagate identity, it's tied to the same user (and even the same client action) as your web/mobile events.
flowchart LR
C["Web / Mobile<br/>(Faro / OTel)"] -->|"API call<br/>traceparent + baggage (user.id)"| B["Backend service<br/>(OTel SDK)"]
C -->|OTLP| N[("nilalytics<br/>otlp_traces / otlp_logs / otlp_metrics")]
B -->|OTLP| N
Nothing new to ingest — backend telemetry lands in the same tables and is queried the same way.
1. Instrument the backend¶
Most OTel SDKs auto-instrument the web framework, so every request becomes a span with method, route, status and duration. Example (FastAPI):
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(
endpoint="http://nilalytics-host:4318/v1/traces",
headers={"Authorization": "Bearer <ingest-token>"},
)))
trace.set_tracer_provider(provider)
FastAPIInstrumentor().instrument_app(app) # each request → a span
The same pattern applies to Node (Express/Fastify), Go, Java, etc. — pick the matching OpenTelemetry instrumentation.
2. Success or failure¶
Captured automatically, and easy to enrich:
- Automatic: a
4xx/5xxresponse or an unhandled exception sets the span status = ERROR and records the exception. - Explicit business outcome:
span = trace.get_current_span()
try:
do_checkout()
span.set_attribute("outcome", "success")
except PaymentError as exc:
span.set_attribute("outcome", "failed")
span.record_exception(exc)
span.set_status(Status(StatusCode.ERROR))
raise
3. Tie it to the user (client ↔ backend)¶
Propagate W3C trace context + baggage so backend spans share the client's
trace_id and carry the same identity attributes.
// browser (Faro): propagate trace headers to your API
new TracingInstrumentation({
instrumentationOptions: { propagateTraceHeaderCorsUrls: [/api\.example\.com/] },
});
# backend: read the propagated identity onto the span
from opentelemetry import baggage
span.set_attribute("user.id", baggage.get_baggage("user.id"))
Now a backend request links to the same person (user.id) and the same
client action (trace_id). See Identity & cross-device.
4. Auth for backends¶
Backends are trusted servers, so they skip the browser flow:
- Send OTLP directly to the ingest server (
:4318) on your private network with the ingest token (NILA_OTLP_TOKEN) — no gateway, no CORS. - Or route through the gateway with a minted token if the backend is outside the trusted network.
5. Query it¶
Backend spans land in otlp_traces (status_code: 1 = ok, 2 = error;
duration_time_unix_nano; service_name; span_attributes; trace_id). Query
over Quack (see Querying):
-- success / failure + latency per backend route
FROM remote.query('
SELECT json_extract_string(span_attributes, ''$."http.route"'') AS route,
count(*) AS calls,
count(*) FILTER (WHERE status_code = 2) AS errors,
round(quantile_cont(duration_time_unix_nano, 0.95) / 1e6) AS p95_ms
FROM lake.main.otlp_traces
GROUP BY 1 ORDER BY calls DESC
');
-- one user's full journey: client events + backend spans, by shared trace_id
FROM remote.query('
SELECT time_unix_nano, service_name, name, status_code
FROM lake.main.otlp_traces
WHERE trace_id = ''<trace-id>''
ORDER BY time_unix_nano
');
This gives you end-to-end visibility: what the user did in the app and whether the backend calls behind it succeeded — in one lake.