Skip to content

Traces, Spans, and the Service Map

A trace is the whole end-to-end journey of one request, stitched together from spans that form a parent/child tree, and Datadog builds the Service Map by reading that tree’s structure directly off live traffic — not from a diagram anyone drew by hand.

A span represents one unit of work — an HTTP request, a database query, a function call. Every span carries a handful of fields that matter:

  • name — the operation type, e.g. web.request or postgres.query.
  • resource — the more specific thing being done, e.g. GET /checkout or SELECT * FROM orders.
  • start and duration — when the span began and how long it took.
  • parent/child relationship — a parent_id pointing at the span that caused this one to happen.

A span with no parent_id is the root span — it marks the start of the trace, usually the inbound request that kicked everything off. Every other span in the trace traces its ancestry back to that root, one parent at a time, which is what turns a flat pile of spans into a tree:

{
"trace_id": "6141ee2f9a4d4b2e8f3c1a90",
"span_id": "a1b2c3d4",
"parent_id": null,
"name": "web.request",
"resource": "GET /checkout",
"service": "checkout-web",
"start": 1699999999000000000,
"duration": 45000000
}

That parent_id: null is what makes this particular span the root — every span downstream of it (the database query it triggers, the call to a payments service, and so on) carries a parent_id that eventually points back here.

Most of the spans in a trace show up without you writing any tracing code at all. A Datadog tracing library hooks into your framework, your HTTP client, and your database driver — when a request comes in, when an outbound call goes out, when a query runs — and creates a span automatically. This is auto-instrumentation, and it’s why installing a tracer against a typical web app already produces a reasonably complete trace on day one.

Auto-instrumentation stops at the boundary of your own business logic, though. If you want visibility into a specific block of code — a discount calculation, a batch job step, a cache lookup you wrote yourself — you wrap it in a manual span:

from ddtrace import tracer
def process_order(order):
# auto-instrumentation already created a span for the inbound HTTP request;
# this manual span adds visibility into a specific block of business logic
with tracer.trace("checkout.apply_discount", resource="apply_discount") as span:
span.set_tag("order.id", order.id)
discount = calculate_discount(order)
span.set_tag("discount.amount", discount)
return discount

The manual span here becomes a child of whatever span was active when process_order ran — usually the root web.request span, or another manual span nested above it. Auto-instrumentation and manual spans are not competing techniques; they combine into the same tree.

The Service Map: built from the tree, not drawn by hand

Section titled “The Service Map: built from the tree, not drawn by hand”

Every span carries a service tag — checkout-web, payments-service, inventory-service. When one span’s child has a different service tag than its parent, that’s a real, observed dependency between two services, captured at the moment the request actually crossed that boundary.

The Service Map is Datadog reading exactly that signal across every trace flowing through your system: it collapses all those parent→child, service→service edges into a single live graph. Because it’s assembled from real span relationships rather than maintained by a person, it updates itself the moment traffic patterns change — a new downstream call shows up on the map as soon as it starts happening, and a decommissioned service drops off once nothing calls it anymore. Compare that to a hand-drawn architecture diagram, which is accurate on the day someone draws it and stale a month later.

flowchart LR
  A[Root span: web.request service checkout-web] --> B[Span: db.query service checkout-web]
  A --> C[Span: http.request service payments-service]
  C --> D[Span: db.query service payments-service]
  A --> E[Span: http.request service inventory-service]
  subgraph SM[Service Map derived from the span tree]
    S1[checkout-web] --> S2[payments-service]
    S1 --> S3[inventory-service]
  end
  A -.observed service boundary crossings become.-> SM
From span tree to Service Map
What identifies a span as the root span of a trace?
What is the main difference between auto-instrumentation and a manual span?
What does the Service Map actually get built from?
Why does the Service Map stay accurate over time without anyone updating it?