Skip to content

Name a route

You have one trace in the dashboard. Look at what it is called. Unless you already passed a route, it is showing up as something like Unlabeled template · 6f3a9c2d, which is not a name anybody chose. This page fixes that, and teaches the route concept by making you use it.

Budget five minutes. You need the application from send your first trace and the ability to run it again.

Every captured call carries a template_hash: a SHA-256 over the structure of your request with the variable parts normalized away, so two calls from the same prompt template hash the same even though the customer name inside them differs. It is content-free by construction.

When a row arrives with no route, the server falls back to that fingerprint and files the call under template:<hash>. The dashboard renders it as Unlabeled template plus the first eight characters. So your traffic is grouped correctly, but the group has no name, and nothing downstream can tell you what that group is for.

  1. Find the function containing the client.chat.completions.create(...) call that produced your first trace.

  2. A route is a scope. Every wrapped provider call made inside it, at any depth, is attributed to it.

    app/llm.py
    import metergraph
    def classify(ticket):
    with metergraph.route("ticket-classifier"):
    return client.chat.completions.create(
    model="gpt-5.6-luna",
    messages=[{"role": "user", "content": ticket.body}],
    )

    route() is also a decorator, which is usually tidier when the whole function belongs to one surface:

    @metergraph.route("ticket-classifier")
    def classify(ticket):
    return client.chat.completions.create(...)
  3. Make at least one call through the newly routed path, then let the batch flush or call flush().

  4. On Spend, the workload breakdown now carries ticket-classifier as a row of its own, with its own call count, error count, latency percentiles and cost. The old template: group stops growing and keeps whatever it already collected. Nothing is rewritten: rows captured before the route existed keep the identity they were captured with.

That is the whole mechanism. A route is a label you attach at the call site, the server takes it verbatim, and everything downstream groups on it.

A route is not decoration. Three things key off it.

Attribution. Spend, latency and error rate are reported per route, which is the only breakdown that survives a refactor. Function names move; a route name is something you chose.

Analysis. The engine evaluates one route at a time and calls it a workload. It picks the three eligible workloads with the most samples in the window and tests candidate models against each. A route that does not exist cannot be picked, and each workload needs a minimum number of traces before it can be analyzed at all. See run your first analysis.

Alerts. The cost-drift, failure-spike, latency-drift and retry-loop detectors compare a route against its own recent history. Splitting two unrelated surfaces into two routes is what stops one of them masking a regression in the other.

Two rules carry most of the weight.

Name the job, never how the job is currently done. ticket-classifier is a good name. gpt-5.6-luna is a bad one, because swapping the model erases the history that justified swapping the model. So are app.services.tickets.classify (that is the function, and you already have that column) and prod (that is an environment, and there is a field for it).

Keep the count in the tens. Route cardinality should track your product surfaces, not your traffic. user-8412 is a session ID wearing a route’s clothes, and it produces thousands of groups of one.

The full table of names to copy and names to avoid, with the reason for each, is on the identity model.