Spans
Spans are the underlying primitive TracePath 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 |
|---|---|
span_id | 8-byte OTel span id, stored as the low 8 bytes of a UUID |
trace_id | The id of the owning entity (endpoint, task, or ai_trace); see "Owning entity" below |
parent_span_id | The span's direct parent within the trace, if any |
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 |
Owning entity, not OTel trace id
Each row in the spans table carries a trace_id column, but in TracePath it points to the owning entity (an endpoint, task, or ai_trace id), not the raw OTel trace id. This is what makes the waterfall view cheap:
- "Show me the children of this endpoint" is a single indexed read on
spans.trace_id = <endpoint.id>. - The full cross-process trace is still reachable via the entity's
distributed_trace_id, which equals the OTel trace id.
Re-rooting happens at ingestion. When a non-root entity (e.g. a CONSUMER task) is promoted, its descendant spans get the task's id as their trace_id. Sibling spans under the originating endpoint keep the endpoint's id. So a single OTel trace that produced an endpoint + a task ends up with two clean waterfalls (one rooted at the endpoint, one at the task), joinable through distributed_trace_id.
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 TracePath'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
Any standard OpenTelemetry SDK creates spans manually — TracePath imposes no span model of its own, so whatever your language's tracer produces is what you get here. See the per-framework client docs for the exact API; the underlying shape is always the OTel span model described above.