Tutorial: trace your first app
In this tutorial you build a small support chat in Python and watch Spanlight record it. You start in an empty folder and end in the dashboard, reading one conversation as a trace tree, as a session and as a cost.
The app answers two questions in a row. It calls Claude through the Spanlight wrapper, looks up an order in a function that stands in for a tool, and tags every run with a user and a session. That is enough to see most of what Spanlight shows for an app. It takes about twenty minutes. The Quickstart is the short version of the same path.
Before you start
- A running Spanlight instance. With Docker Compose it is at http://localhost:8080; if you run the API directly for development it is at http://localhost:8000. The Quickstart starts one
- A project API key that starts with spl_live_, with the default ingest:write scope. The setup wizard creates one; you can also use Settings, Project, API keys, then Create key
- An Anthropic API key (it starts with sk-ant-)
- Python 3.10 or newer, and a terminal with bash or zsh
-
Make a folder and install the packages.
Terminal window mkdir spanlight-tutorialcd spanlight-tutorialpython3 -m venv .venvsource .venv/bin/activatepip install "spanlight[anthropic]==0.1.0" "anthropic==1.9.0"The
anthropicextra makes sure the Anthropic package is installed beside the SDK. The second package pins the exact version of the Anthropic client this tutorial was tested with. -
Tell the app where to send things. Use your own values for the keys.
SPANLIGHT_HOSTpicks the instance. With Docker Compose use thewebservice,http://localhost:8080. If you run the API directly for development (uvicorn), usehttp://localhost:8000.Terminal window export SPANLIGHT_HOST=http://localhost:8080export SPANLIGHT_API_KEY=spl_live_...export ANTHROPIC_API_KEY=sk-ant-...The SDK reads the first two and the Anthropic client reads the third, so none of them appear in the code. Set them again in every new terminal. If you leave
SPANLIGHT_HOSTunset, the SDK useshttp://localhost:8000. If no trace arrives later, a wrong host is the first thing to check; see the SDK points at the wrong host. -
Trace one model call. Save this as
app.py:import spanlightfrom anthropic import Anthropicspanlight.init(environment="development")client = spanlight.wrap_anthropic(Anthropic())reply = client.messages.create(model="claude-haiku-4-5",max_tokens=200,messages=[{"role": "user", "content": "In one sentence, what is an LLM trace?"}],)print(reply.content[0].text)spanlight.flush()spanlight.initstarts the SDK.environment="development"labels every trace, so you can later tell development from production.wrap_anthropicchanges the one client you pass it, and the rest of the code is plain Anthropic code.spanlight.flush()sends what is still queued before the script ends. The SDK sends spans from a background thread, and a short script would otherwise exit first.Run it:
Terminal window python3 app.pyIt prints Claude’s answer. Open Traces in the dashboard sidebar. The newest row is named
chat claude-haiku-4-5, and its Models column showsclaude-haiku-4-5. The list shows the last 24 hours by default; if you do not see the row, wait a few seconds and look again.This single call became one trace holding one span. A trace is one run of your code, and a span is one step inside it.
-
Add a tool step, a user, a session and a second turn. Replace the contents of
app.pywith this:import uuidimport spanlightfrom anthropic import Anthropicspanlight.init(environment="development")client = spanlight.wrap_anthropic(Anthropic())ORDERS = {"1042": "Order 1042 shipped on 2 October and arrives in 3 days.",}@spanlight.observe(kind="tool")def lookup_order(order_id: str) -> str:return ORDERS.get(order_id, f"No order {order_id} found.")@spanlight.observe(name="support-chat", capture_input=False)def chat_turn(history: list[dict[str, str]], question: str, user_id: str, session_id: str) -> str:spanlight.update_trace(user_id=user_id, session_id=session_id)span = spanlight.get_current_span()if span is not None:span.set_input(question)order = lookup_order("1042")history.append({"role": "user", "content": question})reply = client.messages.create(model="claude-haiku-4-5",max_tokens=200,system=f"You are a support agent. Order data: {order}",messages=history,)answer = reply.content[0].texthistory.append({"role": "assistant", "content": answer})return answersession_id = f"tutorial-{uuid.uuid4().hex[:8]}"print("session:", session_id)history: list[dict[str, str]] = []for question in ["Where is my order 1042?", "Can I change the delivery address?"]:print(chat_turn(history, question, "user-42", session_id))spanlight.flush()Four things are new:
@spanlight.observe(kind="tool")turnslookup_orderinto a span of kindtool. A real app would call a database or an API here.@spanlight.observe(name="support-chat")turns each call ofchat_turninto a trace, with the function as its root span. Spans created inside it, the tool step and the model call, become its children.capture_input=Falseandspan.set_input(question)change what thesupport-chatspan records as its input. By default@observerecords every argument, and here that would include the wholehistorylist on every turn. With these two lines the span records only the question. The model call inside it still records the full conversation it was sent.spanlight.update_trace(user_id=..., session_id=...)attaches a user and a session to the current trace. A session groups the traces of one conversation. Each call ofchat_turnis one turn, so two turns make two traces with the same session ID.- The
historylist carries the first question and answer into the second call, so the second model call sees the whole conversation.
The session ID is new on every run, so each run makes a separate session. Run it:
Terminal window python3 app.pyIt prints the session ID and then the two answers:
session: tutorial-1a2b3c4dNote the ID. Your value differs.
-
Read the trace tree. Open Traces again. The two newest rows are named
support-chat. The User / session column showsuser-42and the session ID. On a narrow window that column is hidden, so widen the window or open a trace to see them. Click the newer row, which is the second question.- The header shows the trace name
support-chat, an OK badge and the trace ID. The summary strip shows Started, Duration, Spans (3), Tokens (in → out), Cost and Environment. Below it, the chips show the Useruser-42and the Session. - The Spans card shows the tree.
support-chatis the root, andlookup_orderandchat claude-haiku-4-5sit one level below it. Each row has a bar on a shared time axis, coloured by kind: Chain, Tool and LLM. - Click
chat claude-haiku-4-5. The Usage card shows its Model, Provider, Input tokens, Output tokens, Cached tokens, Cache writes (0 here, since nothing asked for prompt caching), Cost and Pricing version. Time to first token shows — with a tooltip, because this call was not streamed. - On the Input / Output tab you see the conversation sent to the model. For this trace it holds three messages: the first question, its answer, and the second question. The answer from the model is on the output side.
- Click
support-chat. Its Input / Output tab shows only the question of this turn,Can I change the delivery address?, not the history. - Click
lookup_order. The Usage card now says “This span made no model call. Select an LLM span to see its tokens and cost.” Its Input / Output tab shows the order ID that went in and the text that came out.
Traces and sessions describes every control on this page.
- The header shows the trace name
-
Open the session. Click the Session chip in the trace header, or choose Sessions in the sidebar and open the tile with your session ID.
- In the list, the tile shows the session ID, the time it started and how long it lasted, Traces (2), Cost and No errors.
- The session page has the title Session, your session ID with a copy button, and a one-line summary: the number of turns, the total cost, “No errors” and the time window.
- The Conversation card shows Turn 1 and Turn 2, each with the user message on the right and the assistant reply on the left, then the model, the latency, the cost and Open trace. The text comes from the model call of each trace.
- The Summary card shows the Total cost with a line such as “About … per turn”, the counts Turns, Errors, Tokens and Time span, the Models used, and a Timeline.
-
Look at the cost. Spanlight works out the cost of each model call from its token counts and its price table. The Usage card shows the cost of one call, with the Pricing version that was used. The trace’s Cost adds up the spans of the trace, and the session’s Cost and Total cost add up its traces. This app makes one model call per trace, so each trace costs what its call costs.
claude-haiku-4-5is in the price table, so it has a cost. For a model that is not in the table, the dashboard shows — and a tooltip says why; it never shows 0. How cost is calculated explains the formula and how to add your own prices. -
Open the Doctor. Choose Doctor in the sidebar. This app is not expected to trigger any detector, so the Doctor has nothing to report for it. On a project that has never had a finding, the Open tab reads “No findings yet — the Doctor checks every 15 minutes.” If every earlier finding is resolved, the tab reads “Nothing open.” instead. A project with open findings from other traffic still lists them on the Open tab.
Each detector needs a pattern this app does not produce: enough calls of one kind, or a specific failure sequence. The detector catalogue gives the exact conditions of every detector, and the Doctor explains how findings are opened.
Check it worked
Section titled “Check it worked”You have finished when all of these are true:
- Traces lists two traces named
support-chatforuser-42, each with 3 spans: the rootsupport-chat, atoolspanlookup_orderand anllmspanchat claude-haiku-4-5. - The LLM span’s Usage card shows the provider
anthropic, the modelclaude-haiku-4-5, token counts and a cost. - Sessions lists the session ID that your script printed, with 2 traces and No errors, and its page shows Turn 1 and Turn 2.
- Doctor has no finding caused by this app. None is expected from it. Findings from other traffic in the project may still be listed.
Troubleshooting
Section titled “Troubleshooting”- Nothing appears in Traces. Work through No trace arrives. The common causes are a wrong
SPANLIGHT_HOST(the SDK points at the wrong host), a missingSPANLIGHT_API_KEY(the SDK has no API key), and a script that ends before the spans are sent (a short script exits before the spans are sent). Also check that the dashboard shows the project that owns your key (you are looking at another project). - The script stops with
anthropic.AuthenticationError. Anthropic refused the key inANTHROPIC_API_KEY. The wrapper passes the exception on unchanged, so your code sees what it would see without Spanlight. The failed run still appears in Traces: the trace shows an error and thechat claude-haiku-4-5span has the statuserror. - The script stops with a
TypeErrorthat says “Could not resolve authentication method”.ANTHROPIC_API_KEYis not set in this terminal. Run theexportline from step 2 again. - Traces appear, but Sessions is empty or a trace has no Session chip. The session comes from
spanlight.update_trace(session_id=...), which must run inside the traced function. Check that yourapp.pymatches step 4. Sessions lists only sessions with a trace in the selected time range; see Follow a conversation in Sessions. - The cost shows —. The model has no price in the table, and you have set no override. See When a cost is unknown.
- A trace has no prompt or answer. The project has prompt and completion capture turned off; see Privacy.