Spans
Spans are the underlying primitive Traceway stores. Everything else (Endpoints, Tasks, AI Traces, Issues) is a materialized view derived from the spans you ingest. The shape is OpenTelemetry-native, so your existing OTel instrumentation works without translation.
What a span captures
| Field | Description |
|---|---|
trace_id | The OTel trace id the span arrived with, 32 hex characters |
span_id | 16 hex characters for OTLP input; native and migrated UUID IDs can be 32 in the Traceway API |
parent_span_id | The span's direct parent within the trace, empty for a root span |
name | Descriptive label (db.users.find, POST https://api.stripe.com/v1/charges, etc.) |
kind | One of OTel's SERVER, CLIENT, INTERNAL, PRODUCER, CONSUMER |
start_time / duration | When it started and how long it ran |
attributes | Arbitrary key-value metadata (HTTP, DB, AI, custom) |
events | Timestamped events on the span; "exception" events become Issues |
A trace is a trace, a span is a span
Traceway stores accepted spans after sampling, permission and healthcheck filtering, with the trace id, span id and parent span id it arrived with. Nothing is renamed or re-rooted at ingestion.
An endpoint, a task and an AI trace are each a copy of one span, the one that started the request, the job or the LLM call. The copy carries the same three ids as its span. That is all the pages need:
- The waterfall of an endpoint is the spans of its trace that sit below its span.
- Everything connected to it is whatever else shares its trace id, in any project you can open.
- An exception records the trace and the span it happened on. The endpoint or task it belongs to is the nearest one above that span.
Canonical span storage preserves IDs across export batches. Entity classification can depend on visible ancestry: a nested HTTP server span is suppressed only when a local HTTP server ancestor is present in its resource block. Missing parents are treated as unknown, so separately exported nested server spans can produce extra endpoint projections. A child can join its parent once both are stored and included in the query window and graph limits.
Span reads look 24 hours either side of the endpoint, task or AI trace you opened. A span recorded further away than that is not shown on its page.
What spans are good for
Spans tell you where the time went. If an endpoint takes 500ms and 450ms of that is in a db.users.find span, you know what to optimize. The dashboard's drill-down (Endpoints / Tasks / AI Traces → instance → waterfall) bottoms out in spans.
Common things to wrap in a span:
- Database queries (auto-instrumented in most stacks)
- External HTTP calls (
http.clientauto-instrumentation) - Cache operations
- File I/O
- Business-logic steps you want to time
Naming
Hierarchical names group well in the dashboard's filters:
db.users.find
db.orders.insert
api.stripe.charge
api.sendgrid.send
cache.redis.get
logic.pricing.calculatecategory.resource.action is a useful default. Pick something and stay consistent.
Nesting
In Traceway's storage, span nesting is reconstructed from parent_span_id. The waterfall view renders children indented under their parent in chronological order, exactly the way the OTel spec defines a trace tree.
endpoint POST /api/checkout: 200ms
├── validate.cart 20ms
├── pricing.calculate 80ms
│ └── db.products.batch 40ms
├── stripe.charge 60ms
└── db.orders.insert 30msManual spans
The Traceway SDKs and any standard OTel SDK can create spans manually. See the per-framework client docs for the exact API; the underlying shape is always the same OTel span model described above.