Skip to main content
Dashboards and alerts only cover the questions someone thought to ask in advance. An agent can ask new ones, as long as it can read the logs. In this guide, your services write their logs as plain JSON files straight to an Archil disk, and an agent gets read-only access to them. You’ll build two things on top:
  • A triage job that runs every few minutes. Deterministic SQL finds anomalies, a decision model that costs a fraction of a cent per run picks the ones that matter, and a frontier model investigates only those. Quiet runs don’t call a model at all.
  • Dashboards on request. Ask a question in plain English and get back a link to a page of charts that answers it, plus a script that rebuilds the page without the model.
There’s no log pipeline to run: no collector, no ingest service, no index. Apart from Archil and the model, the tools are open source: This works well on Archil for a few reasons:
  • Logs are ordinary files, so the agent uses grep and SQL, which it already knows well, instead of a vendor query language.
  • Queries run next to the data. The agent’s commands run in serverless microVMs that mount the disk, so logs are never downloaded to where the agent runs. In our runs, a DuckDB query across every log file returned in 237 ms.
  • The agent can’t change the logs. It mounts the log disk read-only and keeps its notes, reports, and dashboards on a second disk.
  • Every instance writes to the same disk. In conditional mode, any number of hosts can write to one disk at the same time, and each line is readable by other clients as soon as it’s written.

Prerequisites

Export your Archil API key, a region that offers sandboxes (aws-us-east-1, aws-us-west-2, or aws-eu-west-1), and an AI Gateway key for the model. Then install the dependencies:
Save each file in this guide in your project directory, and run scripts with npx tsx <name>.ts.

Write logs to a disk

You’ll run a small demo shop in a persistent sandbox that mounts the log disk and writes to it directly. In production, you mount the disk wherever your services run; see Run it in production.

The log handler

archil_logging.py is a standard Python logging handler. It writes each record as one JSON line to <service>/<YYYY-MM-DD>/<HH>/<host>.ndjson, with hours in UTC:
  • One directory per hour, so a query for a time window only reads the hours it needs.
  • One file per host. When many hosts write to one disk in conditional mode and two of them write the same part of a file, the last write wins. Giving every writer its own file avoids that. The host name comes from the INSTANCE environment variable, or the machine’s hostname.
  • Every line has ts, level, service, host, and msg, plus whatever fields you pass, so the agent can rely on them.
  • Writes happen on a background thread. archil_logger puts records on a queue with Python’s QueueHandler, and a QueueListener thread writes them out. In our runs, an append took microseconds, but closing the previous hour’s file waits for its writes to reach the disk in conditional mode. The queue keeps waits like that off your request path.
archil_logging.py

The demo services

shop.py stands in for three services: api, checkout, and worker. Every api request has a request_id that also appears in the checkout service’s log line, so the agent can follow a request across services. The worker fails about 8% of send_receipt jobs with an SMTP error. This is the kind of steady background noise that a useful triage job should not page you for. Two flag files let you cause trouble when you’re ready: /tmp/break-checkout makes checkout’s payment calls time out, and /tmp/heic-uploads makes the worker fail on HEIC images, a new error that doesn’t affect anyone’s order.
shop.py

Start everything

The setup script creates two disks: app-logs for the logs and obs-agent for the agent’s tools and output. It then starts the sandbox and runs the shop as a supervised service. The sandbox mounts app-logs in conditional mode, the mode you’d use for a fleet of hosts. idleTtlSeconds: 0 keeps it running when no one is connected. Finally, the script installs the agent’s tools on obs-agent: the DuckDB command-line binary and Vega. With them, this small script renders a Vega-Lite chart to SVG:
vl2svg.mjs
setup.ts
The shop starts logging immediately. After a minute, check that the lines are arriving. This command runs DuckDB in a serverless microVM with both disks mounted read-only:
query.ts

Give the agent the logs

createDiskTools turns a workspace of Archil disks into AI SDK tools: read_file, write_file, delete_file, list_files, glob, grep, and run_bash. This workspace mounts app-logs read-only at /logs and obs-agent at /agent. run_bash runs each command in a fresh serverless microVM with both disks mounted, and grep searches files in parallel next to the data with disk.grep. The instructions describe the log layout and how to query it. Both agents in this guide share this module:
agent.ts

Triage errors on a schedule

Most triage runs find nothing, and a frontier model is an expensive way to find nothing. So triage runs in three steps, and each step runs only if the one before it found something:
  1. Detect with deterministic SQL. A DuckDB query compares the last 15 minutes with the hour before. It flags error types that are new or at least twice as common as before, and paths whose p95 latency at least doubled. It runs in one serverless call, next to the data, with no model.
  2. Classify with a decision model. experimental_decide asks a small decision model whether each anomaly needs an investigation, and gets back a typed answer with probabilities instead of prose. This guide uses typesafe-ai/jev through AI Gateway. Any decision model in the gateway works.
  3. Investigate with a frontier model, only for what the classifier flagged. The agent from the previous section confirms the problem, measures its impact, traces requests across services, writes a report, and records the issue in /agent/triage/known-issues.md. The classifier reads those notes on later runs, so an issue that’s already been reported isn’t investigated again unless it gets worse.
The detection query groups errors by fingerprint: the message and error text with numbers masked, so smtp 550 and smtp 551 count as one error type:
detect.sql
triage.ts runs the three steps and prints the cost of each model call, which AI Gateway reports with every response. The investigation returns structured output, validated against a Zod schema, so you can route issues by severity or open tickets from them without parsing prose. If SLACK_WEBHOOK_URL is set, the script also posts each issue to that incoming webhook.
triage.ts
Let the shop run for an hour so the detector has a baseline, then run triage:
The worker’s SMTP failures are steady, so the detector doesn’t flag them, and no model is called. Now cause two problems at once, a real incident and a harmless new error:
break.ts
Run triage.ts again a couple of minutes later:
The detector flagged five anomalies in about half a second. The classifier sent the four related to checkout to investigation and dropped the HEIC errors, for less than a hundredth of a cent. The investigation found that the four share one cause, found the first failing request to the millisecond, and matched every api error to its checkout error by request_id. It took 9 steps and $0.47, with most of its input served from the gateway’s cache. The model writes its own summary, so your wording and numbers will differ. Run triage again while checkout is still broken. The classifier finds the issue in the notes and stops there:
To run triage continuously, schedule triage.ts every few minutes with any scheduler, such as cron or a scheduled GitHub Actions workflow. Quiet runs cost one serverless call:

Build dashboards on request

The dashboard agent explores the logs, and then writes build.sh, a script that queries the logs with DuckDB, renders each chart to SVG, and writes a static index.html. Then it runs the script and checks the page. The script uses time windows relative to when it runs, so the page can be rebuilt later without the model. The script shares the page with a share URL. Anyone with the link can open it, with no Archil account, until it expires (24 hours by default, up to 7 days). The link serves the page with scripts disabled, so the charts are rendered to SVG ahead of time rather than in the browser.
dashboard.ts
Checkout health dashboard. The share of failed checkouts per minute jumps from 0 to about 35% at 16:41 UTC and drops back to 0 after 16:54. Payment authorization p95 jumps from about 250 ms to 5 seconds over the same minutes while p50 stays near 150 ms. A third chart shows 30 to 70 users a minute hitting a failed checkout during the incident.

The top of the dashboard the agent built, rebuilt without the model after checkout recovered.

The agent decided what to chart on its own. The run took about two minutes. A dashboard is a question a person asked, so it goes straight to the frontier model, unlike triage.

Rebuild a dashboard without the model

build.sh is an ordinary script on the disk, so rebuilding a dashboard is one serverless call, with no model:
refresh.ts
The script recomputes the takeaways as well as the charts. After checkout recovered, the rebuilt page’s first takeaway read “the last 5 minutes are at 1.6%, so it has mostly recovered.” Rebuild a dashboard on a schedule to keep it current, or ask the agent for a new one when the question changes.

Run it in production

Mount the app-logs disk in conditional mode wherever your services run, and log through archil_logger. The agent scripts stay the same.
Mount the disk with the CSI driver as a ReadWriteMany volume, which mounts it in conditional mode. Create the PersistentVolume and a PersistentVolumeClaim named app-logs as shown in Static provisioning, then mount the claim into each service’s pods:
A pod’s hostname is its name, so each replica writes its own files without setting INSTANCE.
A few things to plan for:
  • Several processes per host. If a host runs several processes that log, such as Gunicorn workers, give each one its own file: pass host=f"{socket.gethostname()}-{os.getpid()}" to archil_logger.
  • Other languages. Any logger can write the same layout: one JSON object per line with ts, level, service, host, and msg, appended to <service>/<YYYY-MM-DD>/<HH>/<host>.ndjson, one file per process, written off the request path.
  • Retention. Each day is its own directory, so retention is a daily delete. This serverless command removes the logs from 30 days ago. It mounts the disk in conditional mode, so it can delete while your services keep writing:
  • Big log volumes. Tell the agent to glob only the hours it needs (the instructions in agent.ts already do) so its queries read only those files. For long-range questions, have a scheduled job compact older hours into Parquet with DuckDB’s COPY ... TO.
  • Sensitive data. The agent and anyone with a share link can see what’s in the logs. Remove secrets and personal data before you log them.

Clean up

Delete the sandbox and both disks:
cleanup.ts

Next steps