ЁЯПл The SchoolтА║ЁЯй║ ObservabilityтА║ЁЯз╡ рдзрдбрд╛ 05 тАФ Distributed tracing: рдПрдХрд╛ рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреАрдЪреЗ рдорд╛рд░реНрдЧ рдХрд╛рд░реНрдб
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯз╡ рдзрдбрд╛ 05 тАФ Distributed tracing: рдПрдХрд╛ рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреАрдЪреЗ рдорд╛рд░реНрдЧ рдХрд╛рд░реНрдб

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 05 ┬╖ рдорд╛рдЧреЗ: lesson-04-percentiles ┬╖ рдкреБрдвреЗ: lesson-06-opentelemetry


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ04, рдЖрдгрд┐ рддрд┐рд╕рд░рд╛ signal рд╡ рднрд╛рдЧ 2 рдЪреА рд╕реБрд░реБрд╡рд╛рдд: traces. рддреБрдореНрд╣реА spans, parent рдЖрдгрд┐ child, critical path, рдЖрдгрд┐ trace рдПрдХрд╛ service рдХрдбреВрди рдкреБрдврдЪреНрдпрд╛ service рдХрдбреЗ рдиреЗрдгрд╛рд░рд╛ W3C traceparent header рд╢рд┐рдХрддрд╛. Span, trace_tree(), critical_path() рдЖрдгрд┐ traceparent() obs/signals.py рдордзреНрдпреЗ рдЖрд╣реЗрдд; obs/demo.py рдордзреАрд▓ tracing() рдПрдХрд╛ рдкрд╛рд▓рдХрд╛рдЪреА рдЕрдпрд╢рд╕реНрд╡реА request рдХрд╛рдвреВрди рджрд╛рдЦрд╡рддреЗ.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдПрдХрд╛ рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреАрд▓рд╛ рдмрд░реЗ рд╡рд╛рдЯрдд рдирд╛рд╣реА рдЖрдгрд┐ рддрд┐рд▓рд╛ рдЖрд░реЛрдЧреНрдп рдХрдХреНрд╖рд╛рдд рдкрд╛рдард╡рд▓реЗ рдЬрд╛рддреЗ. рд╡рд╛рдЯреЗрдд рддреА office, рд╢рд┐рдХреНрд╖рдХ рдХрдХреНрд╖ рдЖрдгрд┐ рдЧреНрд░рдВрдерд╛рд▓рдпрд╛рдд рдЬрд╛рддреЗ. рдХреБрдареЗрддрд░реА рддрд┐рдЪреА 15 рдорд┐рдирд┐рдЯреЗ рдЧреЗрд▓реА. рдХреБрдареЗ?

рдореНрд╣рдгреВрди рдХрддрд░рд┐рдирд╛ рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рджреНрдпрд╛рд░реНрдереНрдпрд╛рд▓рд╛ рдПрдХ рдорд╛рд░реНрдЧ рдХрд╛рд░реНрдб ЁЯЧ║я╕П рджреЗрддреЗ, рдЬреНрдпрд╛рд╡рд░ рд╡рд░ рдПрдХ рдХреНрд░рдорд╛рдВрдХ рдЕрд╕рддреЛ. рддреА рдЬрд┐рдереЗ рдЬрд╛рддреЗ рддреА рдкреНрд░рддреНрдпреЗрдХ рдЦреЛрд▓реА рдХрд╛рд░реНрдбрд╛рд╡рд░ рд╢рд┐рдХреНрдХрд╛ рдорд╛рд░рддреЗ: рдЦреЛрд▓реАрдЪреЗ рдирд╛рд╡, рдЖрдд рдпреЗрдгреНрдпрд╛рдЪреА рд╡реЗрд│, рдмрд╛рд╣реЗрд░ рдЬрд╛рдгреНрдпрд╛рдЪреА рд╡реЗрд│. Office рдЬреЗрд╡реНрд╣рд╛ рддрд┐рд▓рд╛ рд╢рд┐рдХреНрд╖рдХ рдХрдХреНрд╖рд╛рдд рдкрд╛рдард╡рддреЗ, рддреЗрд╡реНрд╣рд╛ office рдХрд╛рд░реНрдбрд╛рд╡рд░ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ рд╢рд┐рдХреНрдХреНрдпрд╛рдЪрд╛ рдХреНрд░рдорд╛рдВрдХ рд▓рд┐рд╣рд┐рддреЗ, рдореНрд╣рдгрдЬреЗ рд╢рд┐рдХреНрд╖рдХ рдХрдХреНрд╖рд╛рд▓рд╛ рдХрд│рддреЗ рдХреА рддрд┐рд▓рд╛ рдХреЛрдгреА рдкрд╛рдард╡рд▓реЗ.

рд╢реЗрд╡рдЯреА, рдХрддрд░рд┐рдирд╛ рдХрд╛рд░реНрдб рдЯреЗрдмрд▓рд╡рд░ рдареЗрд╡рддреЗ. рдкреНрд░рддреНрдпреЗрдХ рд╢рд┐рдХреНрдХрд╛ рдореНрд╣рдгрдЬреЗ рдПрдХ рдЪреМрдХрдЯ. рдЪреМрдХрдЯреА рдЗрддрд░ рдЪреМрдХрдЯреАрдВрдЪреНрдпрд╛ рдЖрдд рдЕрд╕рддрд╛рдд: рд╢рд┐рдХреНрд╖рдХ рдХрдХреНрд╖рд╛рдЪреНрдпрд╛ рднреЗрдЯреАрдд рддреНрдпрд╛рдЪреА рдЧреНрд░рдВрдерд╛рд▓рдпрд╛рдЪреА рдлреЗрд░реА рдпреЗрддреЗ. рдЪреМрдХрдЯреАрдВрдЪреА рдПрдХ рд╕рд╛рдЦрд│реА рдмрд╛рдХреА рд╕рдЧрд│реНрдпрд╛рдВрдкреЗрдХреНрд╖рд╛ рд▓рд╛рдВрдм рдЖрд╣реЗ тАФ 15 рдкреИрдХреА 12 рдорд┐рдирд┐рдЯреЗ рдЧреНрд░рдВрдерд╛рд▓рдпрд╛рдд рдЧреЗрд▓реА. рддреА рд╕рд╛рдЦрд│реА рдореНрд╣рдгрдЬреЗ critical path. рдЗрддрд░ рдХреЛрдгрддреАрд╣реА рдЦреЛрд▓реА рдЬрд▓рдж рдХреЗрд▓реА рддрд░реА рддрд┐рд▓рд╛ рдЕрдЬрд┐рдмрд╛рдд рдорджрдд рд╣реЛрдгрд╛рд░ рдирд╛рд╣реА.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart TB
    a["GET /results/3A ┬╖ gateway ┬╖ 0тАУ1040 ms тЬЧ<br/>(root span)"]
    b["check parent login ┬╖ auth ┬╖ 40 ms"]
    c["load class list ┬╖ results-api ┬╖ 40 ms"]
    d["fetch grades ┬╖ results-api ┬╖ 940 ms тЬЧ"]
    e["SELECT grades ┬╖ postgres ┬╖ 60 ms"]
    f["grade service call ┬╖ grade-service ┬╖ 865 ms тЬЧ"]
    a --> b
    a --> c
    a ==>|"critical path"| d
    d --> e
    d ==>|"critical path"| f
    h["ЁЯУи every hop carries:<br/>traceparent: 00-4bf92f35тАж4736-00f067aa0ba902b7-01"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/observability/lesson-diagrams.html#l05

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдПрдХ page load рдЕрдиреЗрдХ services рдУрд▓рд╛рдВрдбрддреЛ, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ team рд▓рд╛ рдлрдХреНрдд рддрд┐рдЪреЗ рд╕реНрд╡рддрдГрдЪреЗ рджрд┐рд╕рддреЗ. Gateway team рд▓рд╛ 1,040 ms рдЪреА request рджрд┐рд╕рддреЗ. results-api team рд▓рд╛ 940 ms рдЪрд╛ call рджрд┐рд╕рддреЛ. рдлрдХреНрдд trace рджрд╛рдЦрд╡рддреЛ рдХреА рддреНрдпрд╛рддрд▓реЗ 865 ms grade service рд▓рд╛ рдХреЗрд▓реЗрд▓реНрдпрд╛ рдПрдХрд╛ call рдордзреНрдпреЗ рдЧреЗрд▓реЗ тАФ рдЖрдгрд┐ database, auth рдЖрдгрд┐ class list рдареАрдХ рд╣реЛрддреЗ. рддреНрдпрд╛рд╢рд┐рд╡рд╛рдп, рдкрд╛рдЪ teams рдкрд╛рдЪ dashboards рдкрд╛рд╣рддрд╛рдд рдЖрдгрд┐ рд╡рд╛рдж рдШрд╛рд▓рддрд╛рдд.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

obs/signals.py рдордзреАрд▓ Span(span_id, parent, name, start, end, service, error=False) рдореНрд╣рдгрдЬреЗ рдХрд╛рд░реНрдбрд╛рд╡рд░рдЪрд╛ рдПрдХ рд╢рд┐рдХреНрдХрд╛; .ms рддреНрдпрд╛рдЪрд╛ рдХрд╛рд▓рд╛рд╡рдзреА. trace_tree(spans) spans indent рдХреЗрд▓реЗрд▓реНрдпрд╛ рдЭрд╛рдбрд╛рд╕рд╛рд░рдЦреЗ print рдХрд░рддреЗ (children рд╕реБрд░реБрд╡рд╛рддреАрдЪреНрдпрд╛ рд╡реЗрд│реЗрдиреБрд╕рд╛рд░ sort рдХреЗрд▓реЗрд▓реЗ, errors рд╕рд╛рдареА тЬЧ). critical_path(spans) root рдкрд╛рд╕реВрди рд╕реБрд░реВ рдХрд░реВрди рд╕рд░реНрд╡рд╛рдд рд╢реЗрд╡рдЯреА рд╕рдВрдкрдгрд╛рд▒реНрдпрд╛ child рдЪрд╛ рдкрд╛рдард▓рд╛рдЧ рдХрд░рдд рд░рд╛рд╣рддреЗ. traceparent(trace_id, span_id, sampled=True) W3C header рдмрдирд╡рддреЗ. tracing() 11:40 рдЪреА рдЕрдпрд╢рд╕реНрд╡реА request рдмрдирд╡рддреЗ: рдкрд╛рдЪ services рдордзрд▓реЗ рд╕рд╣рд╛ spans.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

Grade service рджреБрд░реБрд╕реНрдд рдХрд░рд╛ (865 ms рдЪреНрдпрд╛ call рдРрд╡рдЬреА 5 ms рдЪреЗ cache read), parallel рдордзреНрдпреЗ load рд╣реЛрдгрд╛рд░рд╛ рдПрдХ photo рдЬреЛрдбрд╛ тАФ рдЖрдгрд┐ critical path рд╕рд░рдХрддрд╛рдирд╛ рдкрд╛рд╣рд╛:

python3 obs/demo.py tracing
python3 - <<'EOF'
import sys; sys.path.insert(0, "obs"); from signals import Span, trace_tree, critical_path, traceparent
fixed = [Span("a", None, "GET /results/3A", 0, 240, "gateway"), Span("b", "a", "check parent login", 5, 45, "auth"),
         Span("c", "a", "load class list", 50, 90, "results-api"), Span("d", "a", "fetch grades", 95, 235, "results-api"),
         Span("e", "d", "SELECT grades", 100, 160, "postgres"), Span("f", "d", "grade cache GET", 165, 170, "redis"),
         Span("g", "a", "load photo", 50, 238, "photo-service")]
for line in trace_tree(fixed): print(line)
print("critical path:", " тЖТ ".join(critical_path(fixed)))
print(traceparent("a3ce929d0e0e47364bf92f3577b34da6", "b7ad6b7169203331"))
print(traceparent("a3ce929d0e0e47364bf92f3577b34da6", "b7ad6b7169203331", sampled=False))
EOF
python3 obs/test_obs.py

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

tracing рд╣реЗ print рдХрд░рддреЗ:

тФАтФА one parent's request, as a trace (one tree of spans):
   GET /results/3A [gateway] 0тАУ1040 ms (1040 ms) тЬЧ
     check parent login [auth] 5тАУ45 ms (40 ms)
     load class list [results-api] 50тАУ90 ms (40 ms)
     fetch grades [results-api] 95тАУ1035 ms (940 ms) тЬЧ
       SELECT grades [postgres] 100тАУ160 ms (60 ms)
       grade service call [grade-service] 165тАУ1030 ms (865 ms) тЬЧ
   critical path: GET /results/3A тЖТ fetch grades тЖТ grade service call тАФ 865 of 1040 ms is the grade service
   passed between services in a header: traceparent: 00-a3ce929d0e0e47364bf92f3577b34da6-00f067aa0ba902b7-01

рддреБрдордЪрд╛ snippet рд╣реЗ print рдХрд░рддреЛ:

GET /results/3A [gateway] 0тАУ240 ms (240 ms)
  check parent login [auth] 5тАУ45 ms (40 ms)
  load class list [results-api] 50тАУ90 ms (40 ms)
  load photo [photo-service] 50тАУ238 ms (188 ms)
  fetch grades [results-api] 95тАУ235 ms (140 ms)
    SELECT grades [postgres] 100тАУ160 ms (60 ms)
    grade cache GET [redis] 165тАУ170 ms (5 ms)
critical path: GET /results/3A тЖТ load photo
00-a3ce929d0e0e47364bf92f3577b34da6-b7ad6b7169203331-01
00-a3ce929d0e0e47364bf92f3577b34da6-b7ad6b7169203331-00

Tests рдордзреНрдпреЗ тЬЕ L05 the critical path follows the child that ends last рдЖрдгрд┐ тЬЕ L05 W3C traceparent has version-trace-span-flags рдЕрд╕рддрд╛рдд.

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рдЕрдпрд╢рд╕реНрд╡реА request рдордзреНрдпреЗ, critical path fetch grades тЖТ grade service call рдордзреВрди рдЧреЗрд▓рд╛: 1,040 рдкреИрдХреА 865 ms. рджреБрд░реБрд╕реНрддреАрдирдВрддрд░ request рд▓рд╛ 240 ms рд▓рд╛рдЧрддрд╛рдд, рдЖрдгрд┐ рдЖрддрд╛ critical path рдореНрд╣рдгрдЬреЗ photo (188 ms, 50 рддреЗ 238 ms parallel рдордзреНрдпреЗ рдЪрд╛рд▓рдгрд╛рд░рд╛). fetch grades рдЖрдгрдЦреА рдЬрд▓рдж рдХреЗрд▓реЗ рддрд░реА рдХрд╛рд╣реАрдЪ рдмрджрд▓рдгрд╛рд░ рдирд╛рд╣реА тАФ рддреЛ photo рдЪреНрдпрд╛ рдЖрдзреАрдЪ рд╕рдВрдкрддреЛ. Header рдиреЗ рддреЛрдЪ trace id рдареЗрд╡рд▓рд╛; рдлрдХреНрдд parent id (рдмреЛрд▓рд╛рд╡рдгрд╛рд▒реНрдпрд╛рдЪрд╛ span) рдЖрдгрд┐ sampled flag рдмрджрд▓рд▓реЗ.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

On a real account тАФ рдПрдХрд╛ call рднреЛрд╡рддреА span, рдЖрдгрд┐ header рд╣рд╛рддрд╛рдиреЗ рдкреБрдвреЗ рдкрд╛рдард╡рд▓реЗрд▓рд╛ (OpenTelemetry SDK рдзрдбрд╛ 06 рдордзреНрдпреЗ set up рд╣реЛрддреЛ; requests, Flask, FastAPI рдЖрдгрд┐ рдЗрддрд░рд╛рдВрд╕рд╛рдареАрдЪреЗ auto-instrumentation рд╣реЗ рджреЛрдиреНрд╣реА рддреБрдордЪреНрдпрд╛рд╕рд╛рдареА рдХрд░рддреЗ):

import requests
from opentelemetry import trace
from opentelemetry.propagate import inject
from opentelemetry.trace import Status, StatusCode

tracer = trace.get_tracer("results-api")

def fetch_grades(cls):
    with tracer.start_as_current_span("fetch grades") as span:
        span.set_attribute("school.class", cls)
        headers = {}
        inject(headers)            # adds traceparent: 00-<trace-id>-<this span's id>-01
        try:
            return requests.get(f"http://grade-service/grades/{cls}", headers=headers, timeout=1.0).json()
        except requests.Timeout as e:
            span.record_exception(e)
            span.set_status(Status(StatusCode.ERROR, "grade service timeout"))
            raise

Wire рд╡рд░рдЪрд╛ header:

GET /grades/3A HTTP/1.1
Host: grade-service
traceparent: 00-a3ce929d0e0e47364bf92f3577b34da6-00f067aa0ba902b7-01

Trace backends: Grafana Tempo, Jaeger, AWS X-Ray (ADOT Collector рдорд╛рд░реНрдлрдд; X-Ray W3C trace ids рд╕реБрджреНрдзрд╛ рд╕реНрд╡реАрдХрд╛рд░рддреЛ), Datadog APM. Tempo рдордзреНрдпреЗ TraceQL рдиреЗ рд╣рд│реВ traces рд╢реЛрдзрд╛:

{ resource.service.name = "results-api" && duration > 500ms && status = error }

ЁЯПн Production рдордзреНрдпреЗ рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдкреНрд░рддреНрдпреЗрдХ рд╕реАрдореЗрд╡рд░ propagation рддрдкрд╛рд╕рд╛ тАФ HTTP, gRPC, queues, scheduled jobs. Header рдЯрд╛рдХреВрди рджреЗрдгрд╛рд░реА рдПрдХ service рддрд┐рдЪреНрдпрд╛ рдирдВрддрд░рдЪреЗ рд╕рдЧрд│реЗ рд▓рдкрд╡рддреЗ.

тПня╕П рдкреБрдвреЗ

рдкрд╛рдЪ services, рддреАрди signals, рдЕрдиреЗрдХ vendors. рдкреНрд░рддреНрдпреЗрдХ service рдПрдХрджрд╛рдЪ, рдПрдХрд╛ standard рдкрджреНрдзрддреАрдиреЗ instrument рдХрд░рд╛, рдЖрдгрд┐ data рдХреБрдареЗрд╣реА рдкрд╛рдард╡рд╛: OpenTelemetry.

git checkout lesson-06-opentelemetry

ЁЯз╡ Lesson 05 тАФ Distributed tracing: one pupil's route card

ЁЯУН You are here: Lesson 05 of 12 ┬╖ Previous: lesson-04-percentiles ┬╖ Next: lesson-06-opentelemetry


ЁЯУж What's in this branch

Lessons 01тАУ04, plus the third signal and the start of Part 2: traces. You learn spans, parent and child, the critical path, and the W3C traceparent header that carries the trace from one service to the next. Span, trace_tree(), critical_path() and traceparent() live in obs/signals.py; tracing() in obs/demo.py draws one parent's failed request.

ЁЯзТ Explain like I'm 5

A pupil feels ill and is sent to the health room. On the way she visits the office, the staff room and the library. Somewhere she lost 15 minutes. Where?

So Katrina gives every pupil a route card ЁЯЧ║я╕П with a number on top. Every room she visits stamps the card: the room name, the time in, the time out. When the office sends her on to the staff room, the office writes its own stamp number on the card, so the staff room knows who sent her.

At the end, Katrina lays the card on the table. Each stamp is a box. Boxes sit inside other boxes: the staff room's visit contains its trip to the library. One chain of boxes is longer than all the rest тАФ the library took 12 of the 15 minutes. That chain is the critical path. Making any other room faster would not help her at all.

ЁЯЧ║я╕П Diagram

flowchart TB
    a["GET /results/3A ┬╖ gateway ┬╖ 0тАУ1040 ms тЬЧ<br/>(root span)"]
    b["check parent login ┬╖ auth ┬╖ 40 ms"]
    c["load class list ┬╖ results-api ┬╖ 40 ms"]
    d["fetch grades ┬╖ results-api ┬╖ 940 ms тЬЧ"]
    e["SELECT grades ┬╖ postgres ┬╖ 60 ms"]
    f["grade service call ┬╖ grade-service ┬╖ 865 ms тЬЧ"]
    a --> b
    a --> c
    a ==>|"critical path"| d
    d --> e
    d ==>|"critical path"| f
    h["ЁЯУи every hop carries:<br/>traceparent: 00-4bf92f35тАж4736-00f067aa0ba902b7-01"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/observability/lesson-diagrams.html#l05

тЭУ What

ЁЯдФ Why

Because one page load crosses many services, and each team sees only its own. The gateway team sees a 1,040 ms request. The results-api team sees a 940 ms call. Only the trace shows that 865 ms of it was one call to the grade service тАФ and that the database, auth and class list were fine. Without it, five teams look at five dashboards and argue.

ЁЯФз How (in this repo)

Span(span_id, parent, name, start, end, service, error=False) in obs/signals.py is one stamp on the card; .ms is its duration. trace_tree(spans) prints the spans as an indented tree (children sorted by start time, тЬЧ for errors). critical_path(spans) starts at the root and keeps following the child that ends last. traceparent(trace_id, span_id, sampled=True) builds the W3C header. tracing() builds the failed request from 11:40: six spans across five services.

ЁЯзк Try it

Fix the grade service (a 5 ms cache read instead of an 865 ms call), add a photo that loads in parallel тАФ and watch the critical path move:

python3 obs/demo.py tracing
python3 - <<'EOF'
import sys; sys.path.insert(0, "obs"); from signals import Span, trace_tree, critical_path, traceparent
fixed = [Span("a", None, "GET /results/3A", 0, 240, "gateway"), Span("b", "a", "check parent login", 5, 45, "auth"),
         Span("c", "a", "load class list", 50, 90, "results-api"), Span("d", "a", "fetch grades", 95, 235, "results-api"),
         Span("e", "d", "SELECT grades", 100, 160, "postgres"), Span("f", "d", "grade cache GET", 165, 170, "redis"),
         Span("g", "a", "load photo", 50, 238, "photo-service")]
for line in trace_tree(fixed): print(line)
print("critical path:", " тЖТ ".join(critical_path(fixed)))
print(traceparent("a3ce929d0e0e47364bf92f3577b34da6", "b7ad6b7169203331"))
print(traceparent("a3ce929d0e0e47364bf92f3577b34da6", "b7ad6b7169203331", sampled=False))
EOF
python3 obs/test_obs.py

тЬЕ Verify тАФ what you should see

tracing prints:

тФАтФА one parent's request, as a trace (one tree of spans):
   GET /results/3A [gateway] 0тАУ1040 ms (1040 ms) тЬЧ
     check parent login [auth] 5тАУ45 ms (40 ms)
     load class list [results-api] 50тАУ90 ms (40 ms)
     fetch grades [results-api] 95тАУ1035 ms (940 ms) тЬЧ
       SELECT grades [postgres] 100тАУ160 ms (60 ms)
       grade service call [grade-service] 165тАУ1030 ms (865 ms) тЬЧ
   critical path: GET /results/3A тЖТ fetch grades тЖТ grade service call тАФ 865 of 1040 ms is the grade service
   passed between services in a header: traceparent: 00-a3ce929d0e0e47364bf92f3577b34da6-00f067aa0ba902b7-01

Your snippet prints:

GET /results/3A [gateway] 0тАУ240 ms (240 ms)
  check parent login [auth] 5тАУ45 ms (40 ms)
  load class list [results-api] 50тАУ90 ms (40 ms)
  load photo [photo-service] 50тАУ238 ms (188 ms)
  fetch grades [results-api] 95тАУ235 ms (140 ms)
    SELECT grades [postgres] 100тАУ160 ms (60 ms)
    grade cache GET [redis] 165тАУ170 ms (5 ms)
critical path: GET /results/3A тЖТ load photo
00-a3ce929d0e0e47364bf92f3577b34da6-b7ad6b7169203331-01
00-a3ce929d0e0e47364bf92f3577b34da6-b7ad6b7169203331-00

The tests include тЬЕ L05 the critical path follows the child that ends last and тЬЕ L05 W3C traceparent has version-trace-span-flags.

ЁЯПБ What you just proved

In the failed request, the critical path went through fetch grades тЖТ grade service call: 865 ms of the 1,040. After the fix the request takes 240 ms, and the critical path is now the photo (188 ms, running in parallel from 50 to 238 ms). Making fetch grades any faster would change nothing тАФ it ends before the photo does. The header kept the same trace id; only the parent id (the caller's span) and the sampled flag changed.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ a span around a call, and the header passed on by hand (the OpenTelemetry SDK is set up in lesson 06; auto-instrumentation for requests, Flask, FastAPI and others does both for you):

import requests
from opentelemetry import trace
from opentelemetry.propagate import inject
from opentelemetry.trace import Status, StatusCode

tracer = trace.get_tracer("results-api")

def fetch_grades(cls):
    with tracer.start_as_current_span("fetch grades") as span:
        span.set_attribute("school.class", cls)
        headers = {}
        inject(headers)            # adds traceparent: 00-<trace-id>-<this span's id>-01
        try:
            return requests.get(f"http://grade-service/grades/{cls}", headers=headers, timeout=1.0).json()
        except requests.Timeout as e:
            span.record_exception(e)
            span.set_status(Status(StatusCode.ERROR, "grade service timeout"))
            raise

The header on the wire:

GET /grades/3A HTTP/1.1
Host: grade-service
traceparent: 00-a3ce929d0e0e47364bf92f3577b34da6-00f067aa0ba902b7-01

Trace backends: Grafana Tempo, Jaeger, AWS X-Ray (through the ADOT Collector; X-Ray also accepts W3C trace ids), Datadog APM. Find slow traces in Tempo with TraceQL:

{ resource.service.name = "results-api" && duration > 500ms && status = error }

ЁЯПн Why this matters in production: check propagation at every boundary тАФ HTTP, gRPC, queues, scheduled jobs. One service that drops the header hides everything after it.

тПня╕П Next

Five services, three signals, many vendors. Instrument each service once, in one standard way, and send the data anywhere: OpenTelemetry.

git checkout lesson-06-opentelemetry
тЖР PreviouspercentilesNext тЖТopentelemetry

This page is the lesson's README from the lesson-05-tracing branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.