<bmx-trace-viewer>
A distributed-trace viewer in the page, for observability portals, support tools and incident write-ups: give it a trace exported from OpenTelemetry, Zipkin or Jaeger and it shows where the time went:
12 properties · 3 events · 15 methods · 6 parts
Example
Three checkout requests at a fictional shop, as an OpenTelemetry export, and a normal run as the baseline. The slow one waited on a retried card payment: turn on Critical path, or open Compare.
Show markup
<bmx-trace-viewer id="ex-tv" style="--bmx-trace-viewer-height: 26rem"></bmx-trace-viewer>
<p class="note" id="ex-tv-out" role="status" style="margin: 12px 0 0">Three checkout requests at a fictional shop, as an OpenTelemetry export, and a normal run as the baseline. The slow one waited on a retried card payment: turn on Critical path, or open Compare.</p>
<script type="module">
await customElements.whenDefined('bmx-trace-viewer');
const tv = document.getElementById('ex-tv');
const out = document.getElementById('ex-tv-out');
// Checkout traces for Harbour & Pine, a fictional shop, made here with a seeded random generator and
// written as OTLP JSON - what the OpenTelemetry collector's file exporter writes.
let seed = 11;
const rnd = () => (seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296;
const hex = n => Array.from({ length: n }, () => Math.floor(rnd() * 16).toString(16)).join('');
const ms = 1000; // microseconds
function checkout(startUs, { slow = false, fail = false } = {}) {
const traceId = hex(32);
const spans = [];
const add = (parent, service, name, kind, start, dur, attrs = {}, extra = {}) => {
const s = { traceId, spanId: hex(16), parentSpanId: parent?.spanId, service, name, kind, start, end: start + dur, attrs, ...extra };
spans.push(s);
return s;
};
const t0 = startUs;
const total = slow ? 1640 : 420;
const root = add(null, 'storefront', 'POST /checkout', 2, t0, total * ms, { 'http.method': 'POST', 'http.route': '/checkout', 'http.status_code': fail ? '502' : '200' });
const gw = add(root, 'api-gateway', 'POST /api/orders', 2, t0 + 4 * ms, (total - 10) * ms, { 'http.route': '/api/orders', 'net.peer.name': 'storefront' });
add(gw, 'api-gateway', 'authenticate', 1, t0 + 5 * ms, 9 * ms, { 'enduser.id': 'customer-1042' });
const co = add(gw, 'checkout', 'PlaceOrder', 2, t0 + 16 * ms, (total - 26) * ms, { 'rpc.system': 'grpc', 'order.items': '3' });
// Cart, prices and stock are looked up side by side.
const cart = add(co, 'cart', 'GetCart', 2, t0 + 18 * ms, 34 * ms, { 'cart.items': '3' });
add(cart, 'cart', 'redis HGETALL', 3, t0 + 20 * ms, 6 * ms, { 'db.system': 'redis', 'db.statement': 'HGETALL cart:1042' });
const price = add(co, 'pricing', 'PriceItems', 2, t0 + 19 * ms, 48 * ms, { 'pricing.currency': 'GBP' });
add(price, 'pricing', 'SELECT price_rules', 3, t0 + 23 * ms, 22 * ms, { 'db.system': 'postgresql', 'db.statement': 'SELECT * FROM price_rules WHERE sku = ANY($1)' });
const stock = add(co, 'inventory', 'ReserveStock', 2, t0 + 19 * ms, 61 * ms, { 'inventory.warehouse': 'Leeds' });
add(stock, 'inventory', 'UPDATE stock', 3, t0 + 28 * ms, 38 * ms, { 'db.system': 'postgresql', 'db.statement': 'UPDATE stock SET reserved = reserved + $1 WHERE sku = $2' });
// Then the payment.
let at = t0 + 84 * ms;
const pay = add(co, 'payments', 'Charge', 2, at, (slow ? 1360 : 170) * ms, { 'payment.method': 'card', 'payment.amount': '84.50' });
add(pay, 'fraud-check', 'Score', 2, at + 3 * ms, 41 * ms, { 'fraud.score': '0.07' });
at += 48 * ms;
if (slow) {
// The bank timed out once; the retry went through.
add(pay, 'bank-adapter', 'POST /v2/authorisations', 3, at, 1000 * ms, { 'http.status_code': '504', 'retry.attempt': '1' }, { error: 'upstream timed out after 1000 ms', events: [{ at: at + 1000 * ms, name: 'exception', attrs: { 'exception.type': 'TimeoutError', 'exception.message': 'upstream timed out after 1000 ms' } }] });
at += 1004 * ms;
add(pay, 'bank-adapter', 'POST /v2/authorisations', 3, at, 112 * ms, { 'http.status_code': fail ? '502' : '201', 'retry.attempt': '2' }, fail ? { error: 'card issuer unavailable' } : {});
at += 116 * ms;
} else {
add(pay, 'bank-adapter', 'POST /v2/authorisations', 3, at, 104 * ms, { 'http.status_code': '201', 'retry.attempt': '1' });
at += 108 * ms;
}
at = Math.max(at, pay.end) + 2 * ms;
const order = add(co, 'orders', 'CreateOrder', 2, at, 54 * ms, { 'order.id': `HP-${700000 + Math.floor(rnd() * 99999)}` });
add(order, 'orders', 'INSERT orders', 3, at + 4 * ms, 18 * ms, { 'db.system': 'postgresql', 'db.statement': 'INSERT INTO orders (...) VALUES (...)' });
add(order, 'orders', 'INSERT order_lines', 3, at + 24 * ms, 21 * ms, { 'db.system': 'postgresql', 'db.statement': 'INSERT INTO order_lines (...) VALUES (...)' });
const pub = add(order, 'orders', 'publish order.created', 4, at + 47 * ms, 4 * ms, { 'messaging.system': 'kafka', 'messaging.destination': 'order.created' });
// Sent on after the response: e-mail and shipping pick the message up.
const mail = add(pub, 'notifications', 'consume order.created', 5, at + 60 * ms, 85 * ms, { 'messaging.system': 'kafka' });
add(mail, 'notifications', 'send confirmation e-mail', 3, at + 70 * ms, 70 * ms, { 'email.template': 'order-confirmation' });
add(pub, 'shipping', 'consume order.created', 5, at + 58 * ms, 40 * ms, { 'messaging.system': 'kafka', 'shipping.carrier': 'Northline' });
if (fail) root.error = 'payment failed';
// Grouped by service, as OTLP has it.
const byService = new Map();
for (const s of spans) (byService.get(s.service) ?? byService.set(s.service, []).get(s.service)).push(s);
const nanos = us => `${Math.round(us)}000`;
const attrs = o => Object.entries(o).map(([key, v]) => ({ key, value: { stringValue: String(v) } }));
return [...byService.entries()].map(([service, list]) => ({
resource: { attributes: attrs({ 'service.name': service, 'service.version': '4.12.0', 'deployment.environment': 'production', 'host.name': `${service}-7d9f` }) },
scopeSpans: [
{
scope: { name: 'opentelemetry-instrumentation' },
spans: list.map(s => ({
traceId: s.traceId,
spanId: s.spanId,
...(s.parentSpanId ? { parentSpanId: s.parentSpanId } : {}),
name: s.name,
kind: s.kind,
startTimeUnixNano: nanos(s.start),
endTimeUnixNano: nanos(s.end),
attributes: attrs(s.attrs),
events: (s.events ?? []).map(e => ({ timeUnixNano: nanos(e.at), name: e.name, attributes: attrs(e.attrs) })),
status: s.error ? { code: 2, message: s.error } : { code: 0 },
})),
},
],
}));
}
const now = Date.UTC(2026, 9, 14, 9, 30) * 1000;
tv.baseline = { resourceSpans: checkout(now - 3_600_000_000) };
tv.data = {
resourceSpans: [
...checkout(now, { slow: true }),
...checkout(now - 60_000_000),
...checkout(now - 120_000_000, { slow: true, fail: true }),
],
};
tv.addEventListener('bmxSelect', e => {
const s = e.detail.span;
out.textContent = `bmxSelect: ${s.service}: ${s.name}${s.status === 'error' ? ' (error)' : ''}.`;
});
tv.addEventListener('bmxTraceChange', e => {
out.textContent = `bmxTraceChange: trace ${e.detail.traceId.slice(0, 8)}…`;
});
</script>
- the timeline (waterfall): every span as a bar on a shared time axis,
in its place in the call tree, folding, with an overview to drag across
and zoom into a stretch of time;
- the critical path - the work that decided how long the request took - marked on the bars, with each span's self time and time on the path;
- a flame graph of the same trace;
- figures by service and by operation, and the service map;
- comparing two traces of the same request: what got slower, faster, appeared or went away;
- search across names, services and attributes, errors only;
- exports: spans as CSV, the trace as OTLP JSON, the timeline and the service map as SVG pictures.
<bmx-trace-viewer id="tv"></bmx-trace-viewer>
<script>
tv.data = await (await fetch('/api/traces/4bf92f3577b34da6')).json(); // OTLP, Zipkin or Jaeger
</script>
The trace never leaves the page. The same engine is in
dist/node/bmx-trace-viewer.mjs for a Worker or a server.
The timeline is an ARIA tree (only the rows in view are drawn, so a trace of tens of thousands of spans scrolls smoothly): arrow keys move through the spans, Right and Left open and fold, and each span says its service, operation, duration, start, and whether it failed or is on the critical path.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
allowOpen |
allow-open |
boolean |
true |
Offer the "Open a trace" button. |
baseline |
property only | unknown |
'' |
A trace (or traces) to compare against - a known-good run of the same request - in any of the same formats. |
compareWith |
compare-with |
string |
'' |
The trace the Compare view compares with (its id; default the baseline, or the next trace). Mutable. |
criticalPath |
critical-path |
boolean |
true |
Mark the critical path. Mutable. |
data |
property only | unknown |
'' |
The traces: OTLP JSON, Zipkin v2 JSON or Jaeger JSON - text or parsed - or JSON lines. Mutable (opening a file sets it). |
errorsOnly |
errors-only |
boolean |
false |
Show only spans that failed (with what called them). Mutable. |
fileName |
file-name |
string |
'trace' |
The name downloads are given (without the extension). |
label |
label |
string |
— | The element's accessible name. Default: "Trace viewer". |
selected |
selected |
string |
'' |
The chosen span's id. Mutable. |
strings |
strings |
Partial<BmxTraceViewerStrings> | string |
— | Words to show instead of the English ones, as an object or JSON. |
traceId |
trace-id |
string |
'' |
The trace shown (its id; default the first). Mutable. |
view |
view |
BmxTraceView |
'timeline' |
The view shown. Mutable. |
Events
| Event | Detail | Description |
|---|---|---|
bmxLoad |
BmxTraceLoadDetail |
Fired when traces have been read. |
bmxSelect |
BmxTraceSelectDetail |
Fired when a span is chosen. |
bmxTraceChange |
BmxTraceChangeDetail |
Fired when another trace is shown. |
Methods
| Method | Signature | Description |
|---|---|---|
collapseAll |
collapseAll() => Promise<void> |
Folds every span that has children (the roots stay open). |
compare |
compare(otherTraceId?: string) => Promise<ReturnType<typeof compareTraces> | undefined> |
The trace shown compared with another (default the Compare view's choice). |
download |
download(format?: BmxTraceExportFormat, fileName?: string) => Promise<void> |
Saves an export as a file. |
expandAll |
expandAll() => Promise<void> |
Unfolds every span. |
exportCsv |
exportCsv() => Promise<string> |
The trace shown's spans as CSV. |
exportOtlp |
exportOtlp(all?: boolean) => Promise<string> |
The trace shown as OTLP JSON (all traces read, with all). |
exportSvg |
exportSvg(kind?: "waterfall" | "map") => Promise<string> |
The timeline or the service map as a stand-alone SVG picture. |
find |
find(query: string) => Promise<string[]> |
Spans matching the text (name, service, id, attributes, events), in tree order. |
getCriticalPath |
getCriticalPath() => Promise<BmxCriticalSegment[]> |
The critical path of the trace shown, in time order. |
getTrace |
getTrace(traceId?: string) => Promise<BmxTrace | undefined> |
A trace (default the one shown), with its tree, self times and critical path. |
getTraces |
getTraces() => Promise<BmxTraceSummary[]> |
The traces read, newest first (with the baseline's last). |
resetZoom |
resetZoom() => Promise<void> |
Shows the whole trace again. |
selectSpan |
selectSpan(spanId: string) => Promise<void> |
Chooses a span (unfolding the way to it) and scrolls it into view. |
setTrace |
setTrace(traceId: string) => Promise<void> |
Shows a trace. |
zoomTo |
zoomTo(from: number, to: number) => Promise<void> |
Zooms the timeline to a stretch of the trace (microseconds from its start). |
CSS shadow parts
| Part | Description |
|---|---|
bar |
A span's bar. |
details |
The span details panel. |
header |
The trace's figures across the top. |
row |
A span's row in the timeline; also row-selected. |
table |
A table (services, operations, comparison). |
tabs |
The row of views. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-trace-viewer-critical |
The critical-path mark. |
--bmx-trace-viewer-details-width |
The details panel's width. |
--bmx-trace-viewer-height |
The height of the timeline's list and the flame graph. |
--bmx-trace-viewer-names |
The width of the names column. |
--bmx-trace-viewer-radius |
Corner radius of the element. |
--bmx-trace-viewer-service-1 |
Service colour 1 (and 2 to 10). |