Track A · Task A4
Agent Trace Explorer
AI agents emit JSON that changes shape every release. Store it in VARIANT and query it like real columns.
- 60–120 min
- Intermediate
- Any language · CLI, notebook or web
- AI tools welcome
Stuck for more than 10 minutes? Come to the Doris table and ask Mingyu, or post in #dev on the Apache Doris Slack.
Get set up
What you are building, why Doris makes it simple, and a running cluster.
What you'll build
Your agent's JSON is a mess. Query it anyway.
LLM calls, tool calls, errors, final answers — each with its own fields, and new fields every release. Doris VARIANT turns that JSON into typed columns automatically, so you can filter, search and aggregate it with plain SQL. No schema migrations.
A trace explorer for 1,000 AI-agent sessions (about 20,000 events):
- a sessions list with event counts, errors, tokens and latency,
- a session timeline: click a session to replay what the agent did, step by step,
- tool and model stats: p95 latency, error rate, token usage,
- search inside the JSON: find every tool error that mentions "timeout".
Then answer: did the agent's v2 release (deployed at 13:00) make things better or worse?
The sketch sits at the top of the code panel
Why it's interesting on Doris
Schema-on-write, without the schema. VARIANT infers a type for every JSON path and stores frequent paths as real columnar subcolumns, so
payload['latency_ms']reads one column instead of parsing every document.New fields just appear. When v2 events add
cost_usd, the new path shows up as a subcolumn on the next load, with noALTER TABLE.Search inside JSON. An inverted index on the VARIANT column makes every text path searchable with
MATCH_*.
Panel: the index lines of the table definition
Before you start
docker pull apache/doris:all-in-one-4.1.3docker run -d --name doris -p 9030:9030 -p 8030:8030 -p 8040:8040 apache/doris:all-in-one-4.1.3docker exec -it doris mysql -uroot -h127.0.0.1 -P9030curl -LO https://github.com/morningman/demo-env/releases/download/for-hackathon/doris-hackathon-glasgow-2026.zip && unzip doris-hackathon-glasgow-2026.zip && cd doris-hackathon-glasgow-2026
- Host
- 127.0.0.1
- Port
- 9030
- User
- rootno password
- Database
- hackathon
The data
hackathon.agent_events — one row per agent event, the event body in a VARIANT column:
CREATE TABLE agent_events ( session_id VARCHAR(40) NOT NULL, ts DATETIME(3) NOT NULL, event_id BIGINT NOT NULL, event_type VARCHAR(32) NOT NULL, -- user_message / llm_call / tool_call / error / final_answer payload VARIANT NOT NULL, INDEX idx_payload (payload) USING INVERTED PROPERTIES ("parser" = "english"))DUPLICATE KEY(session_id, ts)DISTRIBUTED BY HASH(session_id) BUCKETS 1PROPERTIES ("replication_num" = "1", "storage_format" = "V3");Example payloads — note how the shapes differ:
{"text": "Why did checkout fail at 11:40?", "lang": "en", "agent_version": "v1"}{"model": "model-large", "usage": {"input_tokens": 812, "output_tokens": 96}, "latency_ms": 1430, "agent_version": "v1"}{"tool": "sql_query", "args": {"sql": "SELECT ..."}, "latency_ms": 37, "status": "ok", "agent_version": "v1"}{"tool": "http_get", "args": {"url": "..."}, "latency_ms": 3012, "status": "error", "agent_version": "v1", "error": {"type": "Timeout", "message": "read timeout after 3000 ms calling https://api.internal/orders"}}Sessions that start at 13:00 or later run agent v2: their payloads say "agent_version": "v2", and v2 LLM calls add a cache_hit flag. Milestone 5 then loads events with fields the table has never seen.
Panel → Table: the full table definition
Build with AI
Pair with your AI coding agent — that's encouraged. Doris 4.x search and AI features are newer than what most models were trained on, so give your agent the right context before it writes SQL:
Paste the context prompt below into your agent.
Point it at the reference pages listed on this page and at
AGENTS.mdin the starter kit. Many coding agents read that file on their own.Make it run every SQL statement against your live cluster before wiring it into code.
Better still: connect the Doris MCP Server (see Task A2) so your agent can inspect the schema and test queries itself.
I'm building an agent trace explorer on Apache Doris 4.1 for a hackathon.Connection: MySQL protocol, 127.0.0.1:9030, user root, empty password, database `hackathon`.Use a plain MySQL driver and raw SQL.Table `agent_events`: session_id, ts DATETIME(3), event_id, event_type(user_message / llm_call / tool_call / error / final_answer), payload VARIANT(inverted index, english parser). Payload shapes differ by event_type. Every payload hasagent_version: "v1" before 13:00, "v2" from 13:00; v2 llm_calls add cache_hit.Doris SQL rules:1. Read JSON paths as payload['a']['b']. CAST to a concrete type before comparing, sorting or aggregating: CAST(payload['latency_ms'] AS DOUBLE).2. VARIANT cannot be a key, a sort key or a join key.3. Text search inside JSON: payload['error']['message'] MATCH_ANY 'timeout'. Never LIKE.4. Percentiles: percentile_approx(expr, 0.95).5. `SET describe_extend_variant_column = true; DESC agent_events;` lists inferred subcolumns.6. To insert JSON yourself, CAST the text: CAST('{"a": 1}' AS JSON).Build: sessions list, session timeline, tool/model stats, and JSON search.Run every SQL statement against the live cluster first.Doris traps your agent will probably fall into
| Trap | What to do instead |
|---|---|
| Comparing VARIANT paths without a cast | CAST(payload['latency_ms'] AS INT); VARIANT can't be a key or join column |
Expecting DESC to list the JSON paths | Run SET describe_extend_variant_column = true; first; then DESC shows every inferred subcolumn and its type |
Using LIKE '%term%' for text search | col MATCH_ANY 'a b' (OR), MATCH_ALL (AND), MATCH_PHRASE 'a b' — they use the inverted index |
| Using an ORM | Prefer a plain MySQL driver and raw SQL; ORMs may emit statements Doris doesn't support |
Build it
6 milestones, M0 → M5. The panel follows the step you are reading.
Load and inspect
From inside the starter-kit folder:
M0 · Loadbashdocker exec -i doris mysql -uroot -h127.0.0.1 -P9030 < seed/a4_agent_events.sqlThen, in a SQL shell:
M0 · InspectsqlUSE hackathon;SET describe_extend_variant_column = true;DESC agent_events; -- see the subcolumns Doris inferred: payload.tool, payload.usage.input_tokens, ...CheckpointThe load reports 1,000 sessions and 20,401 events;
DESClists about 20payload.*paths, each with an inferred type.Query JSON paths
M1 · Query JSON pathssqlSELECT CAST(payload['tool'] AS STRING) AS tool, COUNT(*) AS callsFROM agent_eventsWHERE event_type = 'tool_call'GROUP BY toolORDER BY calls DESC;CheckpointFive tools;
sql_queryis called most (3,506 times).Aggregate inferred subcolumns
M2 · Aggregate inferred subcolumnssqlSELECT CAST(payload['tool'] AS STRING) AS tool, COUNT(*) AS calls, percentile_approx(CAST(payload['latency_ms'] AS DOUBLE), 0.95) AS p95_ms, SUM(CASE WHEN CAST(payload['status'] AS STRING) = 'error' THEN 1 ELSE 0 END) AS errorsFROM agent_eventsWHERE event_type = 'tool_call'GROUP BY toolORDER BY p95_ms DESC;Checkpointpython_exechas the worst p95 (about 2.1 s);http_getfails most often (425 errors).Search inside the JSON
M3 · Search inside the JSONsqlSELECT session_id, ts, CAST(payload['error']['message'] AS STRING) AS error_messageFROM agent_eventsWHERE payload['error']['message'] MATCH_ANY 'timeout'ORDER BY tsLIMIT 20;Checkpoint398 events mention a timeout in total. Count them per hour: that is your first clue about v2.
Session timeline
M4 · Session timelinesqlSELECT ts, event_type, CAST(payload['tool'] AS STRING) AS tool, CAST(payload['status'] AS STRING) AS status, CAST(payload['latency_ms'] AS INT) AS latency_ms, CAST(payload['text'] AS STRING) AS textFROM agent_eventsWHERE session_id = 's-0042'ORDER BY ts;Build the explorer UI around these queries: sessions list → timeline → stats.
CheckpointSession
s-0042has 25 events: a question, LLM and tool calls (one failedsql_query, retried), and a final answer.Watch the schema evolve
M5 · Load the drift batchbashdocker exec -i doris mysql -uroot -h127.0.0.1 -P9030 < a4/drift_batch.sql # events with new fieldsM5 · Query the new fieldssqlSELECT CAST(payload['agent_version'] AS STRING) AS version, COUNT(*) AS llm_calls, ROUND(SUM(CAST(payload['cost_usd'] AS DOUBLE)), 2) AS cost_usdFROM agent_eventsWHERE event_type = 'llm_call'GROUP BY versionORDER BY version;Run
DESC agent_eventsagain: the new paths are there. NoALTER TABLEhappened.CheckpointA third version appears,
v2.1: only its LLM calls carrycost_usd, andDESCnow listspayload.cost_usdandpayload.retries.
Ship it
Check it off, show it, collect your badge.
Definition of Done
Your checklistSubmit & get your badge
Take part → submit → get your badge.
Put your work in a folder named after your GitHub ID: the code, plus a README with what it does, how to run it, one screenshot or GIF, and the Doris features you used.
Open a pull request that adds it to morningman/demo-env as
doris-hackathon-glasgow-2026/<your-github-id>/. Fork and push, or use GitHub's Add file → Upload files. Nothing else to fill in.Show it at the Doris table (a 2-minute demo is plenty), or at the show-and-tell around 14:30.
Join the Apache Doris Slack and say hi in
#dev: questions, submissions and badges are all handled there.
Everyone who takes part in a task on site gets the Apache Doris Contributor badge: no merged pull request to Doris needed. Didn't finish by 15:00? Keep going — submissions are open until 31 October. Teams are fine, but not needed: list every member's GitHub ID in the README.
Stretch goals
Bring your own traces. Export your own AI coding agent's local session logs (JSONL) and load them, one
CAST('…' AS JSON)per line. It all stays on your laptop.Schema Template. Pin hot paths with
VARIANT<'latency_ms': INT, 'tool': STRING>in a copy of the table and compareDESCoutput and query behaviour. Fill it withINSERT … SELECT …, CAST(payload AS JSON): a VARIANT does not cast straight into a templated one.Let an assistant explore it. Connect the Doris MCP Server (Task A2) and ask your assistant to find the worst session.
Classify errors with SQL (bring your own API key):
AI_CLASSIFY(CAST(payload['error']['message'] AS STRING), ['timeout', 'auth', 'bad input', 'other']). See LLM SQL Functions.
Troubleshooting & references
| Symptom | Fix |
|---|---|
docker: command not found on macOS | Start Docker Desktop. If it is running, link the CLI: sudo ln -s /Applications/Docker.app/Contents/Resources/bin/docker /usr/local/bin/docker |
error getting credentials when pulling images | Remove the credsStore field from ~/.docker/config.json (local dev only) |
port is already allocated | Something else uses 9030, 8030 or 8040, often another Doris. Stop it (docker ps), or publish other host ports, e.g. -p 19030:9030, and connect to 19030 |
The container name "/doris" is already in use | You started it before: docker start doris. To start over, docker rm -f doris (this deletes its data) |
The container exits, or never turns (healthy) | Read docker logs doris. Usually it is memory: give Docker Desktop 6 GB or more (Settings → Resources). On Apple Silicon, don't add --platform linux/amd64 |
permission denied: ./doris.sh | Your unzip tool dropped the executable bit: chmod +x doris.sh, or run bash doris.sh start |
Current database is not set | Run USE hackathon; first, or connect with -Dhackathon |
score() function requires WHERE clause with MATCH function, ORDER BY and LIMIT | Add a MATCH_* predicate, ORDER BY score() DESC and a LIMIT, and keep score() unwrapped (see the traps above) |
| Stuck for more than 10 minutes | Come to the Doris table and ask Mingyu, or post in #dev on the Apache Doris Slack |
References
VARIANT Data TypeVARIANT SQL referenceFull-text SearchLLM SQL Functions
Apache Doris Hackathon
