Trace an async request handler with fibre_tracing
For Rust developers who already use `tracing` and want to read their spans back
You will instrument a small async request handler, collect each request as a trace with fibre_tracing and print it as a waterfall of timed spans. A failing request then shows its own trace on an error page.
Before you start
You need Rust with Cargo. fibre_tracing comes from crates.io:
fibre_tracing = "0"Set up the project#
Start a new binary crate.
Add fibre_tracing, the tracing crates it plugs into and tokio for the async runtime. Your [dependencies] section should look like this:
[]
= "0"
= "0.1"
= "0.3"
= { = "1", = ["macros", "rt", "time"] }fibre_tracing is a tracing_subscriber layer. It never installs itself as the global subscriber, so you put it on a registry yourself at the top of main.
Collect your first trace#
A trace starts at a span that carries the field fibre.root = true. Every span opened inside it joins the trace. Replace src/main.rs with this:
use SubscriberExt;
use SubscriberInitExt;
async
async
fibre_tracing::layer() returns two things: the layer you install and a Traces handle you read through. When the root span closes, the finished trace goes into a ring of recent traces (256 by default). recent(n) hands back the last n of them, newest last.
Your timings will differ from the ones shown in this tutorial. Trace ids start at 1 and count up in the order roots open.
Add child spans and print a waterfall#
A real handler does several things. This one authenticates, loads a user from a pretend database and renders a page. Each step gets its own span. The database call records how many rows it returned once it knows.
The root is now an explicit info_span! rather than an attribute, because the next section records a field on it after the handler body has finished. Replace src/main.rs with the whole program:
use Duration;
use Trace;
use sleep;
use Instrument;
use Empty;
use SubscriberExt;
use SubscriberInitExt;
async
async
async
async
async
async
async
trace.spans lists the spans in the order they opened, root first. Each Span already knows its depth, when it opened relative to the root (at) and how long it stayed open (duration), so print_waterfall is a plain loop with no tree to walk.
The db.query span sits one level under load_user because it opened inside it. The right-hand column is empty for now; it prints each span's outcome, which you add next.
Record outcomes and add an error path#
fibre_tracing reads a field called fibre.outcome as the result of a span. Span::outcome() returns it. Declare the field as Empty when the span opens and record it once you know the answer.
Change handle to hold on to the root span and record the outcome after the body returns:
async
The root is still open at that point because handle holds root. It closes when root drops at the end of the function. That close is what finishes the trace.
Give load_user its own outcome so a missing row shows up as not_found on the span that caused it:
async
Then send two requests from main, one for a user that exists and one for a user that does not:
async
The failed request has no render span because route returned early with ?. You can see where it stopped and which step reported the problem.
Read a span's fields#
Every field a span recorded is kept in span.fields, a map from field name to fibre_tracing::Value. That includes fields recorded after the span opened, such as rows. Trace::named finds every span of a given name.
Add this to the end of main:
for trace in traces.recent
The waterfalls print as before, followed by:
/users/7: rows=1 sql=select name from users where id = ?
/users/404: rows=0 sql=select name from users where id = ?
Value implements Display, so string fields print without quotes and numbers print as numbers. This is also how a test can assert on what a handler actually did: run it, take the trace and check the spans and fields it recorded.
Show the trace on an error page#
Everything so far read finished traces from the ring. Traces::current() returns the trace the current span belongs to while it is still running, with the spans that have opened so far. That is what an error page in development wants: the steps this request took before it failed.
Import Traces alongside Trace:
use ;Make handle take the Traces handle and return a response body. On failure it builds the error page while still inside the root span:
async
current() looks at the span you are in, which is why the call sits inside root.in_scope. Outside any root it returns None. A span that has not closed yet has a duration of zero. Here that is the root itself.
In main, pass the handle and print the body as a string:
for path in
The waterfalls and field lines follow as before.
Export failed traces with a listener#
A listener registered with on_finish is called with each trace as its root closes, before the trace goes into the ring. At that point the whole request is known, so you can decide what to send to an exporter based on how it went. Here the "exporter" is a println! that fires only for requests that failed or took longer than 50ms.
Register it in main right after installing the layer:
traces.on_finish
;
The [export] line prints before the error page. The listener runs on the thread that closes the root. The root closes when handle returns, before main gets to print the response. Keep listeners quick for that reason: a slow one delays every request that finishes.
If a process only exports and never reads the ring, fibre_tracing::with_ring(0) keeps no finished traces and still calls listeners.
Where to go next#
The usage guide on GitHub covers sizing the ring, composing the layer beside a log sink such as fibre_logging and running it next to tracing-opentelemetry: https://github.com/excsn/fibre/tree/main/tracing
The full API, including of_span for reading a trace from a span id, is on docs.rs: https://docs.rs/fibre_tracing
