Plot traces¶
Timeline.spans draws the spans of traces as a waterfall: a
lane per span, under the span that called it, its duration beside it, and the critical
path (what the end of the request waited for) as a dark line through the bars. Lanes fold,
so many traces fit: here eight requests, folded to one lane each, with the events in their
spans (exceptions, retries, cache misses).
from anytimeline import Timeline
from anytimeline.synthetic import traces
tr = traces(8, seed=3) # .spans and .events: dicts of columns
plot = Timeline(title="Eight requests", height=380)
plot.spans(tr.spans, color="service", tooltip=["service", "status"], expand=1)
plot.events(tr.events, time="time", lane="span_id", symbol="name", color="#333333",
tooltip=["attributes"], name="span events")
print(plot._embed_html())
Fold, unfold, zoom¶
- Click the name of a span with a triangle before it to show the spans under it; click it again to fold them. Alt + click does every level under it at once.
- Double-click a name to zoom to that span and everything under it.
- ] unfolds every lane, [ folds them all to the roots.
expand=sets what shows at first:1the roots only,2their children too,Noneeverything. The default,"auto", opens as many levels as keep about 200 lanes.
A folded span says how many spans it hides (+9). Events on hidden spans are hidden with
them.
One trace¶
With one trace, everything is open. The request below failed once: the query timed out after 250 ms (the ● is its exception) and was retried (■). The critical path runs through the timeout and the retry; the parallel stock checks are not on it except the one that ended last.
bad = tr.spans["trace_id"][list(tr.spans["status"]).index("error")]
spans = {k: v[tr.spans["trace_id"] == bad] for k, v in tr.spans.items()}
events = {k: v[tr.events["trace_id"] == bad] for k, v in tr.events.items()}
plot = Timeline(title="GET /orders, with a retry")
plot.spans(spans, color="service", tooltip=["service", "status"])
plot.events(events, time="time", lane="span_id", symbol="name", color="#333333",
tooltip=["attributes"], name="span events")
print(plot._embed_html())
Switch to Order in the toolbar: each start and end gets the same width, so the order of the calls shows however long each took.
Traces from OpenTelemetry¶
read_otlp reads OTLP/JSON: what the OpenTelemetry
Collector's file exporter writes (one export request per line), and what Jaeger and Tempo
export.
from anytimeline import Timeline
from anytimeline.otel import read_otlp
tr = read_otlp("traces.jsonl")
plot = Timeline().spans(tr.spans, color="service")
plot.events(tr.events, time="time", lane="span_id", symbol="name")
plot
The spans have the columns trace_id, span_id, parent_id, name, service, kind,
status, status_message, start, end and attributes; any of them can go into
color= or tooltip=. Times are UTC: give the plot Timeline(timezone="Europe/Amsterdam")
for local times.
Your own table¶
Any table with a row per span works. By default spans() reads the columns span_id,
parent_id, name, start and end; name yours with id=, parent=, label=, start=
and end= (or duration=). Span ids must be unique in the table; a span whose parent is not
in it (a trace cut short) is shown at the top.
Other trees¶
The tree is not only for traces: intervals(..., id=, parent=) draws any bars with parents
(jobs with steps, tasks with sub-tasks), and gantt(..., parent=)
gives a Gantt chart with sub-tasks. Set critical_path=True or durations=True there as
you like.