Axis documentation
Everything you need to send your first span, understand what the dashboard shows you, and instrument a real multi-step process.
What is Axis?
Modern business processes — a checkout, an onboarding, a nightly billing run — are chains of steps spread across services you don't fully control: payment APIs, databases, schedulers, email providers, chat bots. When one link breaks, the whole process breaks, and no single tool shows you which link it was.
Axis gives you that process-level view. Your systems report each step as a span; spans that share a trace ID are stitched into one execution — one run of your process — which you can watch on a dashboard as a table, a timeline waterfall, or an interactive graph with each step colored by its outcome. Declare the expected shape of a process and Axis will even tell you when a step never ran at all.
Core concepts
Span — the atomic record of one step: which step, which execution, when it started and ended, whether it succeeded, plus any metadata you attach. One HTTP call, one DB write, one email send — each is a span.
Trace ID / execution — a correlation ID you choose (an order ID works great). Every span carrying the same trace ID belongs to the same execution of your process.
Virtual endpoint — a named collection point you create in the dashboard, usually one per third-party service ("payments", "email"). Spans reference it via endpoint_id; process definitions are built from them.
Process definition — an optional declaration of a process's expected steps (as endpoints, in order). With strict mode on, executions get a completeness verdict: missing, failed, and out-of-order steps.
Collection modes — today spans are collected passively: your systems call third parties directly and report telemetry out-of-band, so Axis is never in your critical path. An active proxy mode (route the call through Axis, zero instrumentation) is on the roadmap.
Quickstart
- Create a project. Sign up and the dashboard walks you through it — a project scopes everything: your spans, endpoints, definitions, and API keys.
- Get an API key. Ask your project admin (or, when self-hosting, run
make api-keyin the repo). Keys look liketrk_...and are shown exactly once. - Send a span.Your first span (curl)
export TRACER_API_KEY="trk_..." # see "API keys" below curl -X POST http://localhost:8001/v1/spans \ -H "Authorization: Bearer $TRACER_API_KEY" \ -H "Content-Type: application/json" \ -d '[{ "trace_id": "order-8231", "step_name": "charge_payment", "status": "success", "start_time": "2026-08-02T12:00:00Z", "end_time": "2026-08-02T12:00:00.420Z" }]' - Watch it arrive. The dashboard's Executions page polls every 10 seconds — your execution appears within moments; click it for the waterfall and graph views.
API keys
The ingestion API authenticates with project-scoped API keys. A key identifies your project — spans sent with it land in that project and nowhere else. You never send a project ID in the payload; the key is the tenant.
- Keys look like
trk_<random>and are passed as a bearer token:Authorization: Bearer trk_... - A key is shown exactly once at creation — only a hash is stored. Lose it, and you create a new one.
- Keys are machine credentials for sending telemetry only. They cannot read data or access the dashboard — that uses your normal login.
- Treat keys like passwords: keep them in environment variables or your secret manager, never in client-side code or repositories.
- Revoking a key stops its ingestion immediately; spans it already sent are unaffected.
Ingestion API reference
One endpoint: POST /v1/spans against your ingestion host (locally http://localhost:8001). The body is always a JSON array — a single span is an array of one. Up to 1,000 spans per request.
Span fields
| Field | Type | Required | Description |
|---|---|---|---|
| trace_id | string | yes | Correlation ID tying spans into one execution. Use a stable business ID (order ID, request ID). |
| step_name | string | yes | Human-readable step label, e.g. charge_payment. |
| status | enum | yes | success | failure | pending | error. failure = business outcome; error = crash/exception. |
| start_time | ISO-8601 datetime | yes | MUST include a timezone (e.g. trailing Z). Naive timestamps are rejected. |
| end_time | ISO-8601 datetime | no | Omit for spans still in flight (pending). |
| duration_ms | integer | no | Computed from start/end when omitted; send it only if you measured it yourself. |
| span_id | string (uuid) | no | Generated server-side when absent. Supply your own to reference it from parent_span_id. |
| parent_span_id | string | no | Nests this span under another — drawn as causal edges in the graph view. |
| endpoint_id | string | no | Which virtual endpoint this step maps to (from the Endpoints page). Needed for completeness. |
| process_def_id | string | no | Which process definition applies (from the Definitions page). Enables completeness verdicts. |
| attributes | object | no | Arbitrary JSON metadata — amounts, HTTP statuses, error messages. Filterable in the span explorer. |
A fuller example
[
{
"trace_id": "order-8231",
"span_id": "b2f6d9e0-52a1-4c53-9c4d-8f6f1f2a7b10",
"parent_span_id": null,
"endpoint_id": "ep_1a2b3c...",
"process_def_id": "proc_4d5e6f...",
"step_name": "charge_payment",
"status": "success",
"start_time": "2026-08-02T12:00:00Z",
"end_time": "2026-08-02T12:00:00.420Z",
"attributes": {
"http.status": 200,
"amount_cents": 4200,
"provider": "stripe"
}
},
{
"trace_id": "order-8231",
"step_name": "send_email",
"status": "failure",
"start_time": "2026-08-02T12:00:01Z",
"end_time": "2026-08-02T12:00:04.100Z",
"attributes": { "error": "SMTP relay rejected the message" }
}
]Responses
Ingestion always answers 202 Accepted for a well-formed request. Validation happens per span: bad items are rejected individually with their index and reason — the rest of the batch is unaffected.
HTTP/1.1 202 Accepted
{ "accepted": 1, "rejected": 0, "errors": [] }HTTP/1.1 202 Accepted
{
"accepted": 2,
"rejected": 1,
"errors": [
{
"index": 2,
"detail": [
{
"type": "enum",
"loc": ["status"],
"msg": "Input should be 'success', 'failure',
'pending' or 'error'"
}
]
}
]
}Retries & idempotency
Sending telemetry over a flaky network means retrying. Attach an Idempotency-Key header (any unique string per batch) and a retried request replays the original response without re-ingesting — valid for 24 hours.
curl -X POST http://localhost:8001/v1/spans \ -H "Authorization: Bearer $TRACER_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 4f7c1e9a-batch-42" \ -d '[ ... ]'
Correlation & trace IDs
Correlation is the heart of Axis: spans that share a trace_id become one execution. Two rules of thumb:
- Pick an ID your whole chain already knows. An order ID or payment intent ID means every service can tag its spans without coordination.
- Pass it along. Whatever crosses your service boundaries (message payloads, headers, job arguments) should carry the trace ID so every step tags its span with the same value.
parent_span_id is optional extra structure: set it and the graph view draws true causal edges; leave it out and spans are simply ordered by time. The Python SDK handles both automatically.
Python SDK
tracer-sdk is a zero-dependency Python client for everything above: correlation IDs, timing, statuses, nesting, batching, and retries — with one hard guarantee: telemetry failures never raise into your application. (Currently distributed with the repository under sdks/python; PyPI release planned.)
from tracer_sdk import Tracer
client = Tracer(
api_key="trk_...", # or env TRACER_API_KEY
ingest_url="http://localhost:8001", # or env TRACER_INGEST_URL
)
with client.trace("order-8231") as trace: # trace_id = your correlation ID
with trace.span("charge_payment") as span:
span.set("amount_cents", 4200) # arbitrary attributes
charge_the_card() # exception -> status="error"
with trace.span("send_email") as span:
if not deliver_email():
span.fail("SMTP relay rejected") # business failureClean block exit records success; an exception records error (with error.type / error.message attributes) and re-raises; span.fail("reason") records a business-level failure.
with client.trace("order-8231") as trace:
with trace.span("checkout"): # parent
with trace.span("charge_payment"): # child: parent_span_id set
...
with trace.span("reserve_stock"): # sibling of charge_payment
...
# Nesting is tracked per thread / asyncio task automatically —
# the dashboard's graph view draws these as solid causal edges.@trace.step("reserve_stock") # decorator form
def reserve(sku: str) -> None:
...
trace.emit("webhook_received", # point-in-time event
attributes={"source": "stripe"})
client.flush() # force-send buffered spans
client.close() # flush + stop (also runs atexit)
# Serverless / short-lived processes: disable the background thread and
# flush explicitly at the end of each invocation.
client = Tracer(api_key="trk_...", auto_flush=False)Completeness monitoring
The question most monitoring can't answer is "what never happened?" A dropped email step produces no error, no log line, no span — just silence. Completeness monitoring detects that silence.
- Create endpoints for the services in your chain (Endpoints page).
- Create a process definition listing the expected steps in order (Definitions page), and turn on strict mode.
- Tag your spans with both ids:Span opting into completeness
{ "trace_id": "order-8231", "step_name": "charge_payment", "endpoint_id": "ep_1a2b3c...", <- which declared step this span is "process_def_id": "proc_4d5e6f...", <- which process definition applies "status": "success", "start_time": "2026-08-02T12:00:00Z" }
Every execution of that process then gets a verdict on its drill-down page and graph: missing steps (declared but never ran — shown as ghost nodes), failed steps, and out-of-order steps. Strict mode is opt-in per definition: switch it off and Axis accepts any span shape as loose logging.
Errors
401— missing, invalid, or revoked API key:{"detail": "invalid or missing API key"}413— batch larger than 1,000 spans. Split it.422— the request body itself isn't a JSON array of objects. (Individual invalid spans do NOT cause 422 — they come back per-item in the 202 envelope, see above.)503— the ingestion queue is unavailable; retry with the sameIdempotency-Key.
Limits & guarantees
- Batches: up to 1,000 spans per request.
- Timestamps must be timezone-aware ISO-8601; naive datetimes are rejected per-item.
- Spans are immutable: a second span with the same
span_idis silently ignored, which makes retries safe — but it also means apendingspan is not updated by re-sending it. Prefer emitting spans when steps finish. - Idempotency-Key replay window: 24 hours.
- Ingestion is buffered and asynchronous — spans appear on the dashboard within a few seconds, and a dashboard outage never blocks your processes.