Pick a depth. Each prompt opens in your AI pre-loaded with the lesson. Click a row to preview the prompt.
You can use OpenTelemetry for a year without knowing its data model, and you will pay for it every time a query does not work the way you expect. There are only five objects worth internalising. A Resource describes the thing emitting (a service instance) and is attached once, not per-span. An Instrumentation Scope names the library that produced the telemetry, which is how you tell your spans from your framework's. A Span is a timed operation with attributes, events, links and a status. A Metric point is a value plus an attribute set plus a time window. A Log record is a timestamped body with attributes and, crucially, a trace context. Almost every 'why can't I group by that' question resolves to putting an attribute on the wrong one of these five.
This is what actually crosses the wire, flattened for readability. Notice the nesting: attributes live at three different levels, and which level you choose decides whether you can group by it cheaply, at all, or only per-request.
# What one OTLP trace export actually contains, flattened.
Resource # once per process, not per span
service.name = "checkout" # REQUIRED. Everything groups by this.
service.version = "2024.11.3"
service.instance.id = "checkout-7d9f-abc"
deployment.environment.name = "production"
host.name / k8s.pod.name / cloud.region ...
ScopeSpans
InstrumentationScope
name = "checkout.handlers" # YOUR code
version = "1.0.0"
Span
trace_id = 4bf92f3577b34da6a3ce929d0e0e4736 # 16 bytes
span_id = 00f067aa0ba902b7 # 8 bytes
parent_span_id = (empty -> this is the root)
name = "POST /checkout" # LOW cardinality. Not the URL.
kind = SERVER
start / end = unix nanos
status = { code: ERROR, message: "upstream timeout" }
attributes = { http.request.method: "POST",
http.route: "/checkout",
tenant.id: "acme" }
events = [ { name: "exception", time: ...,
attributes: { exception.type: "TimeoutError" } } ]
links = [ ] # other traces this one relates to
# Where an attribute lives decides what you can do with it:
# Resource -> free to group by, same for every span in the process
# Scope -> tells you which library emitted it
# Span -> per-request, costs bytes, groupable in a trace store
# Metric pt -> per-request-class, costs SERIES (see module 6)python3 main.py