Learn
Spans

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

FieldDescription
span_id8-byte OTel span id, stored as the low 8 bytes of a UUID
trace_idThe id of the owning entity (endpoint, task, or ai_trace); see "Owning entity" below
parent_span_idThe span's direct parent within the trace, if any
nameDescriptive label (db.users.find, POST https://api.stripe.com/v1/charges, etc.)
kindOne of OTel's SERVER, CLIENT, INTERNAL, PRODUCER, CONSUMER
start_time / durationWhen it started and how long it ran
attributesArbitrary key-value metadata (HTTP, DB, AI, custom)
eventsTimestamped 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.client auto-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.calculate

category.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        30ms

Manual 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.