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

ЁЯФн рдзрдбрд╛ 06 тАФ OpenTelemetry: рд╕рдЧрд│реЗ рдЧреЛрд│рд╛ рдХрд░рдгреНрдпрд╛рдЪрд╛ рдПрдХрдЪ рдорд╛рд░реНрдЧ

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


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

рдзрдбреЗ 01тАУ05, рдЖрдгрд┐ рддрд┐рдиреНрд╣реА signals рддрдпрд╛рд░ рдХрд░рдгреНрдпрд╛рдЪреА рдЖрдгрд┐ рдкреБрдвреЗ рдиреЗрдгреНрдпрд╛рдЪреА standard рдкрджреНрдзрдд: OpenTelemetry (OTel). рддреБрдореНрд╣реА API рдЖрдгрд┐ SDK, OTLP protocol, Collector (receivers тЖТ processors тЖТ exporters), head рд╡рд┐рд░реБрджреНрдз tail sampling, рдЖрдгрд┐ semantic conventions рд╢рд┐рдХрддрд╛. obs/signals.py рдордзреАрд▓ Collector class tail sampling рдЪреЗ model рдЖрд╣реЗ; obs/demo.py рдордзреАрд▓ otel() 1,000 traces рддреНрдпрд╛рдордзреВрди рдЪрд╛рд▓рд╡рддреЗ.

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

рдкреВрд░реНрд╡реА рд╢рд╛рд│реЗрддрд▓реА рдкреНрд░рддреНрдпреЗрдХ рдЦреЛрд▓реА рдЖрдкрд╛рдкрд▓реНрдпрд╛ рдкрджреНрдзрддреАрдиреЗ рдиреЛрдВрджреА рдареЗрд╡рд╛рдпрдЪреА. Office рдкреЗрдиреНрд╕рд┐рд▓рдиреЗ рд▓рд┐рд╣рд╛рдпрдЪреЗ, рдЧреНрд░рдВрдерд╛рд▓рдп cards рд╡рд╛рдкрд░рд╛рдпрдЪреЗ, рд╢рд┐рдХреНрд╖рдХ рдХрдХреНрд╖ whiteboard рд╡рд╛рдкрд░рд╛рдпрдЪрд╛. рдХрддрд░рд┐рдирд╛рд▓рд╛ рддреНрдпрд╛рдВрдЪреА рдмреЗрд░реАрдЬ рдХрд░рддрд╛ рдпреЗрдд рдирд╡реНрд╣рддреА.

рдореНрд╣рдгреВрди рджреАрдкрд┐рдХрд╛ рдкреНрд░рддреНрдпреЗрдХ рдЦреЛрд▓реАрд▓рд╛ рдПрдХрдЪ form ЁЯУЛ рджреЗрддреЗ, рддреНрдпрд╛рдЪ рдЪреМрдХрдЯреА рдЖрдгрд┐ рддреЗрдЪ рд╢рдмреНрдж рдЕрд╕рд▓реЗрд▓рд╛: "рд╡рд░реНрдЧ", "рдЖрдд рдпреЗрдгреНрдпрд╛рдЪреА рд╡реЗрд│", "рдХрд╛рдп рдШрдбрд▓реЗ". рдкреНрд░рддреНрдпреЗрдХ рдЦреЛрд▓реА рдЖрдкрд▓реЗ forms рд╡реНрд╣рд░рд╛рдВрдбреНрдпрд╛рддрд▓реНрдпрд╛ рдПрдХрд╛рдЪ рдкрддреНрд░рдкреЗрдЯреАрдд ЁЯУо рдЯрд╛рдХрддреЗ. рдПрдХ рдорджрддрдиреАрд╕ рдкрддреНрд░рдкреЗрдЯреА рд░рд┐рдХрд╛рдореА рдХрд░рддреЗ: рддреА forms рдЪреЗ рдЧрдареНрдареЗ рдмрд╛рдВрдзрддреЗ, рдмрд╣реБрддреЗрдХ рдХрдВрдЯрд╛рд│рд╡рд╛рдгреЗ forms рдЯрд╛рдХреВрди рджреЗрддреЗ ("рд╡рд┐рджреНрдпрд╛рд░реНрдереА рдареАрдХ, 2 рдорд┐рдирд┐рдЯреЗ"), рдЖрдЬрд╛рд░реА рдЕрд╕рд▓реЗрд▓реНрдпрд╛ рдХрд┐рдВрд╡рд╛ рдЦреВрдк рд╡реЗрд│ рд╡рд╛рдЯ рдкрд╛рд╣рд┐рд▓реЗрд▓реНрдпрд╛ рд╡рд┐рджреНрдпрд╛рд░реНрдереНрдпрд╛рдмрджреНрджрд▓рдЪрд╛ рдкреНрд░рддреНрдпреЗрдХ form рдареЗрд╡рддреЗ, рдЖрдгрд┐ рдЧрдареНрдард╛ рдЬрд┐рдереЗ рдЧрд░рдЬ рдЖрд╣реЗ рддрд┐рдереЗ рдкрд╛рдард╡рддреЗ тАФ рдЖрд░реЛрдЧреНрдп рдХрдХреНрд╖, рдореБрдЦреНрдпрд╛рдзреНрдпрд╛рдкрд┐рдХреЗрдЪреЗ office, рдХрд┐рдВрд╡рд╛ рджреЛрдиреНрд╣реАрдХрдбреЗ.

рддреЛрдЪ рдПрдХ form рдореНрд╣рдгрдЬреЗ semantic conventions. рдкрддреНрд░рдкреЗрдЯреА рдЖрдгрд┐ рдорджрддрдиреАрд╕ рдореНрд╣рдгрдЬреЗ Collector. Forms рд╡рд╛рдЪрд▓реНрдпрд╛рдирдВрддрд░ рд░реЛрдЪрдХ forms рдареЗрд╡рдгреЗ рдореНрд╣рдгрдЬреЗ tail sampling.

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

flowchart LR
    subgraph app["ЁЯН│ results-api"]
      api["OTel API<br/>(your code calls this)"] --> sdk["OTel SDK<br/>resource: service.name<br/>batch + export"]
    end
    sdk -->|"OTLP<br/>gRPC 4317 ┬╖ HTTP 4318"| rx
    subgraph col["ЁЯУо OpenTelemetry Collector"]
      rx["receivers<br/>otlp, prometheus"] --> pr["processors<br/>memory_limiter тЖТ tail_sampling тЖТ batch"] --> exp["exporters<br/>otlp, prometheusremotewrite"]
    end
    exp --> t["ЁЯЧ║я╕П Tempo / Jaeger / X-Ray"]
    exp --> m["ЁЯУК Prometheus / Mimir"]
    exp --> dd["ЁЯР╢ Datadog"]
    s["1,000 traces тЖТ keep 113<br/>all 4 errors ┬╖ all 10 slow ┬╖ 1 in 10 of the rest"]

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг standard рдирд╕рддрд╛рдирд╛, рдкреНрд░рддреНрдпреЗрдХ vendor рдЪрд╛ рд╕реНрд╡рддрдГрдЪрд╛ agent рдЖрдгрд┐ рд╕реНрд╡рддрдГрдЪреА library рд╣реЛрддреА. Backend рдмрджрд▓рдгреЗ рдореНрд╣рдгрдЬреЗ рдкреНрд░рддреНрдпреЗрдХ service рдкреБрдиреНрд╣рд╛ instrument рдХрд░рдгреЗ. OTel рд╕рд╣, app Collector рд╢реА OTLP рдордзреНрдпреЗ рдмреЛрд▓рддреЗ, рдЖрдгрд┐ Collector рдордзреНрдпреЗрдЪ рддреБрдореНрд╣реА backends рдирд┐рд╡рдбрддрд╛, рдЧреБрдкрд┐рддреЗ рдХрд╛рдвреВрди рдЯрд╛рдХрддрд╛, рдЖрдгрд┐ рдЦрд░реНрдЪ рдирд┐рдпрдВрддреНрд░рд┐рдд рдХрд░рддрд╛ тАФ рдореБрдЦреНрдпрддрдГ sampling рдиреЗ. рд╡реНрдпрд╕реНрдд рдорд╛рд░реНрдЧрд╛рд╡рд░рдЪреНрдпрд╛ рдкреНрд░рддреНрдпреЗрдХ request рдЪрд╛ trace рдШреЗрдгреЗ рдорд╣рд╛рдЧ рдЕрд╕рддреЗ; рдпрд╛рджреГрдЪреНрдЫрд┐рдХ 10% рдЪрд╛ trace рдШреЗрддрд▓рд╛ рддрд░ рддреБрдореНрд╣рд╛рд▓рд╛ рд╣рд╡рд╛ рдЕрд╕рд▓реЗрд▓рд╛ рдПрдХрдореЗрд╡ error trace рдЧрд│реВ рд╢рдХрддреЛ. Tail sampling рд░реЛрдЪрдХ traces рдареЗрд╡рддреЗ.

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

obs/signals.py рдордзреАрд▓ Collector(slow_ms=500, keep_1_in=10) рд╣рд╛ рдПрдХ tail sampler рдЖрд╣реЗ: export(traces) рдкреНрд░рддреНрдпреЗрдХ рдкреВрд░реНрдг рдЭрд╛рд▓реЗрд▓рд╛ trace рдкрд╛рд╣рддреЛ рдЖрдгрд┐ рддреНрдпрд╛рдд error рдЕрд╕реЗрд▓, рдХрд┐рдВрд╡рд╛ рддреЛ slow_ms рдкреЗрдХреНрд╖рд╛ рд╣рд│реВ рдЕрд╕реЗрд▓, рдХрд┐рдВрд╡рд╛ рддреЛ рджрд░ keep_1_in-рд╡рд╛ trace рдЕрд╕реЗрд▓, рддрд░ рддреЛ рдареЗрд╡рддреЛ; рдмрд╛рдХреАрдЪреЗ рдЯрд╛рдХрд▓реЗ рдЬрд╛рддрд╛рдд. рддреЛ (kept, dropped) рдкрд░рдд рдХрд░рддреЛ. otel() 1,000 traces рдмрдирд╡рддреЗ: рдмрд╣реБрддреЗрдХрд╛рдВрдирд╛ 90тАУ389 ms рд▓рд╛рдЧрддрд╛рдд, рджрд░ 97 рд╡рд╛ рд╣рд│реВ рдЕрд╕рддреЛ (+900 ms) рдЖрдгрд┐ рджрд░ 250 рд╡реНрдпрд╛рдд error рдЕрд╕рддреЗ.

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

Tail-sampling рдЪрд╛ рдирд┐рдпрдо рдмрджрд▓рд╛, рдордЧ head sampling рд╢реА рддреБрд▓рдирд╛ рдХрд░рд╛ (рд╕реБрд░реБрд╡рд╛рддреАрд▓рд╛рдЪ, рдХрд╛рд╣реАрд╣реА рди рдХрд│рддрд╛ 10 рдкреИрдХреА 1 рдард░рд╡рдгреЗ):

python3 obs/demo.py otel
python3 - <<'EOF'
import sys; sys.path.insert(0, "obs"); from signals import Collector
traces = [dict(id=i, ms=90 + (i * 53) % 300 + (900 if i % 97 == 0 else 0), error=(i % 250 == 0)) for i in range(1, 1001)]
for slow_ms, k in ((500, 10), (500, 100), (300, 10), (1000, 1000)):
    kept, dropped = Collector(slow_ms=slow_ms, keep_1_in=k).export(traces)
    print(f"keep errors + traces > {slow_ms:>4} ms + 1 in {k:<4} тЖТ kept {kept:>3}, dropped {dropped}")
head = [t for t in traces if t["id"] % 10 == 3]
print(f"head sampling 1 in 10 (decided at the start) тЖТ kept {len(head)} ┬╖ errors kept {sum(t['error'] for t in head)} of 4 ┬╖ slow kept {sum(t['ms'] > 500 for t in head)} of 10")
EOF
python3 obs/test_obs.py

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

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

тФАтФА OpenTelemetry: one SDK in the app тЖТ the Collector тЖТ any backend (Tempo, Jaeger, X-Ray, DatadogтАж)
   app (SDK: traces, metrics, logs) тЖТ OTLP тЖТ collector (receive тЖТ batch тЖТ sample тЖТ export) тЖТ backends
тФАтФА 1,000 traces, tail sampling (keep every error and every trace > 500 ms, 1 in 10 of the rest) тЖТ kept 113, dropped 887
   all 4 error traces and all 10 slow traces survive тАФ the boring ones are sampled
   semantic conventions: http.request.method, http.response.status_code, service.name тАФ the same names everywhere

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

keep errors + traces >  500 ms + 1 in 10   тЖТ kept 113, dropped 887
keep errors + traces >  500 ms + 1 in 100  тЖТ kept  24, dropped 976
keep errors + traces >  300 ms + 1 in 10   тЖТ kept 377, dropped 623
keep errors + traces > 1000 ms + 1 in 1000 тЖТ kept  15, dropped 985
head sampling 1 in 10 (decided at the start) тЖТ kept 100 ┬╖ errors kept 0 of 4 ┬╖ slow kept 1 of 10

Tests рдордзреНрдпреЗ тЬЕ L06 tail sampling keeps every error and every slow trace рдЕрд╕рддреЗ.

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

100 рдкреИрдХреА 1 рд╡рд░рдЪреНрдпрд╛ tail sampling рдиреЗ 1,000 рдРрд╡рдЬреА 24 traces рд╕рд╛рдард╡рд▓реЗ тАФ рдЖрдгрд┐ рддрд░реАрд╣реА рдкреНрд░рддреНрдпреЗрдХ error рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рд╣рд│реВ trace рдареЗрд╡рд▓рд╛. "рд╣рд│реВ" рдЪреА рд░реЗрд╖рд╛ 300 ms рдкрд░реНрдпрдВрдд рдЦрд╛рд▓реА рдЖрдгрд▓реНрдпрд╛рдиреЗ storage рддрд┐рдкреНрдкрдЯ рдЭрд╛рд▓реЗ (377), рдХрд╛рд░рдг рдЕрдиреЗрдХ рд╕рд╛рдорд╛рдиреНрдп traces 300 ms рдУрд▓рд╛рдВрдбрддрд╛рдд: рдорд░реНрдпрд╛рджрд╛ рд╣рд╛ рдЦрд░реНрдЪрд╛рдЪрд╛ рдирд┐рд░реНрдгрдп рдЖрд╣реЗ. Head sampling рдиреЗ рдкрд╣рд┐рд▓реНрдпрд╛ рдирд┐рдпрдорд╛рдЗрддрдХреАрдЪ рд╕рдВрдЦреНрдпрд╛ рдареЗрд╡рд▓реА (100 рд╡рд┐рд░реБрджреНрдз 113) рдкрдг 4 рдкреИрдХреА рдПрдХрд╣реА error рдирд╛рд╣реА рдЖрдгрд┐ 10 рдкреИрдХреА рдлрдХреНрдд 1 рд╣рд│реВ trace рдареЗрд╡рд▓рд╛ тАФ рддреНрдпрд╛рдиреЗ рдХрд│рдгреНрдпрд╛рдЖрдзреАрдЪ рдирд┐рд╡рдб рдХреЗрд▓реА.

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

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

On a real account тАФ Collector рдХрдбреЗ OTLP exporter рд╕рд╣ Python SDK (packages opentelemetry-sdk рдЖрдгрд┐ opentelemetry-exporter-otlp):

from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.trace.sampling import ParentBased, ALWAYS_ON
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

provider = TracerProvider(
    resource=Resource.create({"service.name": "results-api", "service.version": "v41",
                              "deployment.environment": "production"}),
    sampler=ParentBased(ALWAYS_ON),        # send everything; the Collector tail-samples
)
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317", insecure=True)))
trace.set_tracer_provider(provider)

рдХрд┐рдВрд╡рд╛ code рди рдмрджрд▓рддрд╛ (auto-instrumentation):

pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
OTEL_SERVICE_NAME=results-api OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 \
    opentelemetry-instrument python app.py

Tail sampling рдЕрд╕рд▓реЗрд▓реА Collector pipeline тАФ errors рдареЗрд╡рд╛, рд╣рд│реВ traces рдареЗрд╡рд╛, рдмрд╛рдХреАрдЪреЗ 10% рдареЗрд╡рд╛ (tail_sampling Collector рдЪреНрдпрд╛ contrib distribution рдордзреНрдпреЗ рдЖрд╣реЗ):

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 20
  tail_sampling:
    decision_wait: 10s              # wait for late spans before deciding
    policies:
      - name: errors
        type: status_code
        status_code: {status_codes: [ERROR]}
      - name: slow
        type: latency
        latency: {threshold_ms: 500}
      - name: the-rest
        type: probabilistic
        probabilistic: {sampling_percentage: 10}
  batch: {}

exporters:
  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true
  prometheusremotewrite:
    endpoint: http://prometheus:9090/api/v1/write

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, tail_sampling, batch]
      exporters: [otlp/tempo]
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [prometheusremotewrite]

AWS рд╡рд░, ADOT Collector (AWS Distro for OpenTelemetry) X-Ray рдЖрдгрд┐ CloudWatch рд╕рд╛рдареА exporters рдЬреЛрдбрддреЛ; Datadog Agent рдЖрдгрд┐ Datadog рдЪрд╛ Collector exporter рд╕реБрджреНрдзрд╛ OTLP рд╕реНрд╡реАрдХрд╛рд░рддрд╛рдд.

ЁЯПн Production рдордзреНрдпреЗ рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдкрд╣рд┐рд▓реНрдпрд╛ рджрд┐рд╡рд╕рд╛рдкрд╛рд╕реВрдирдЪ рддреБрдордЪреЗ apps рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ backend рдпрд╛рдВрдЪреНрдпрд╛рдордзреНрдпреЗ рдПрдХ Collector рдареЗрд╡рд╛. рддрд┐рдереЗрдЪ рддреБрдореНрд╣реА рдЧреБрдкрд┐рддреЗ рдХрд╛рдврддрд╛, sample рдХрд░рддрд╛, рдЖрдгрд┐ vendors рдмрджрд▓рддрд╛ тАФ рдПрдХрд╛рд╣реА service рд▓рд╛ рд╣рд╛рдд рди рд▓рд╛рд╡рддрд╛.

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

Signals рд╡рд╛рд╣реВ рд▓рд╛рдЧрд▓реЗ рдЖрд╣реЗрдд. рдЖрддрд╛ metrics рд╕рд╛рдард╡рд╛, рддреНрдпрд╛рдВрдирд╛ рдкреНрд░рд╢реНрди рд╡рд┐рдЪрд╛рд░рд╛ рдЖрдгрд┐ рддреНрдпрд╛рдВрдЪреЗ рдЪрд┐рддреНрд░ рдХрд╛рдврд╛: Prometheus рдЖрдгрд┐ Grafana.

git checkout lesson-07-prometheus-grafana

ЁЯФн Lesson 06 тАФ OpenTelemetry: one way to collect everything

ЁЯУН You are here: Lesson 06 of 12 ┬╖ Previous: lesson-05-tracing ┬╖ Next: lesson-07-prometheus-grafana


ЁЯУж What's in this branch

Lessons 01тАУ05, plus the standard way to produce and move all three signals: OpenTelemetry (OTel). You learn the API and SDK, the OTLP protocol, the Collector (receivers тЖТ processors тЖТ exporters), head vs tail sampling, and semantic conventions. The Collector class in obs/signals.py models tail sampling; otel() in obs/demo.py runs 1,000 traces through it.

ЁЯзТ Explain like I'm 5

Every room in the school used to keep notes its own way. The office wrote in pencil, the library used cards, the staff room used a whiteboard. Katrina could not add them up.

So Dipika gives every room the same form ЁЯУЛ with the same boxes, in the same words: "class", "time in", "what happened". Every room drops its forms in one post box ЁЯУо in the corridor. A helper empties the post box: she bundles the forms, throws away most of the boring ones ("pupil fine, 2 minutes"), keeps every form about a pupil who was ill or waited a long time, and sends the bundle wherever it is needed тАФ the health room, the head's office, or both.

The same form is semantic conventions. The post box and helper are the Collector. Keeping the interesting forms after reading them is tail sampling.

ЁЯЧ║я╕П Diagram

flowchart LR
    subgraph app["ЁЯН│ results-api"]
      api["OTel API<br/>(your code calls this)"] --> sdk["OTel SDK<br/>resource: service.name<br/>batch + export"]
    end
    sdk -->|"OTLP<br/>gRPC 4317 ┬╖ HTTP 4318"| rx
    subgraph col["ЁЯУо OpenTelemetry Collector"]
      rx["receivers<br/>otlp, prometheus"] --> pr["processors<br/>memory_limiter тЖТ tail_sampling тЖТ batch"] --> exp["exporters<br/>otlp, prometheusremotewrite"]
    end
    exp --> t["ЁЯЧ║я╕П Tempo / Jaeger / X-Ray"]
    exp --> m["ЁЯУК Prometheus / Mimir"]
    exp --> dd["ЁЯР╢ Datadog"]
    s["1,000 traces тЖТ keep 113<br/>all 4 errors ┬╖ all 10 slow ┬╖ 1 in 10 of the rest"]

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

тЭУ What

ЁЯдФ Why

Because without a standard, each vendor had its own agent and its own library. Changing backend meant re-instrumenting every service. With OTel, the app speaks OTLP to a Collector, and the Collector is where you choose backends, drop secrets, and control cost тАФ mostly by sampling. Tracing every request on a busy path is expensive; tracing a random 10% can drop the one error trace you needed. Tail sampling keeps the interesting ones.

ЁЯФз How (in this repo)

Collector(slow_ms=500, keep_1_in=10) in obs/signals.py is a tail sampler: export(traces) looks at each finished trace and keeps it if it has an error, or is slower than slow_ms, or is every keep_1_in-th trace; the rest are dropped. It returns (kept, dropped). otel() makes 1,000 traces: most take 90тАУ389 ms, every 97th is slow (+900 ms) and every 250th has an error.

ЁЯзк Try it

Change the tail-sampling rule, then compare with head sampling (decide 1 in 10 at the start, knowing nothing):

python3 obs/demo.py otel
python3 - <<'EOF'
import sys; sys.path.insert(0, "obs"); from signals import Collector
traces = [dict(id=i, ms=90 + (i * 53) % 300 + (900 if i % 97 == 0 else 0), error=(i % 250 == 0)) for i in range(1, 1001)]
for slow_ms, k in ((500, 10), (500, 100), (300, 10), (1000, 1000)):
    kept, dropped = Collector(slow_ms=slow_ms, keep_1_in=k).export(traces)
    print(f"keep errors + traces > {slow_ms:>4} ms + 1 in {k:<4} тЖТ kept {kept:>3}, dropped {dropped}")
head = [t for t in traces if t["id"] % 10 == 3]
print(f"head sampling 1 in 10 (decided at the start) тЖТ kept {len(head)} ┬╖ errors kept {sum(t['error'] for t in head)} of 4 ┬╖ slow kept {sum(t['ms'] > 500 for t in head)} of 10")
EOF
python3 obs/test_obs.py

тЬЕ Verify тАФ what you should see

otel prints:

тФАтФА OpenTelemetry: one SDK in the app тЖТ the Collector тЖТ any backend (Tempo, Jaeger, X-Ray, DatadogтАж)
   app (SDK: traces, metrics, logs) тЖТ OTLP тЖТ collector (receive тЖТ batch тЖТ sample тЖТ export) тЖТ backends
тФАтФА 1,000 traces, tail sampling (keep every error and every trace > 500 ms, 1 in 10 of the rest) тЖТ kept 113, dropped 887
   all 4 error traces and all 10 slow traces survive тАФ the boring ones are sampled
   semantic conventions: http.request.method, http.response.status_code, service.name тАФ the same names everywhere

Your snippet prints:

keep errors + traces >  500 ms + 1 in 10   тЖТ kept 113, dropped 887
keep errors + traces >  500 ms + 1 in 100  тЖТ kept  24, dropped 976
keep errors + traces >  300 ms + 1 in 10   тЖТ kept 377, dropped 623
keep errors + traces > 1000 ms + 1 in 1000 тЖТ kept  15, dropped 985
head sampling 1 in 10 (decided at the start) тЖТ kept 100 ┬╖ errors kept 0 of 4 ┬╖ slow kept 1 of 10

The tests include тЬЕ L06 tail sampling keeps every error and every slow trace.

ЁЯПБ What you just proved

Tail sampling at 1 in 100 stored 24 traces instead of 1,000 тАФ and still kept every error and every slow one. Lowering the "slow" line to 300 ms tripled the storage (377), because many normal traces cross 300 ms: the threshold is a cost decision. Head sampling kept the same number as the first rule (100 vs 113) but none of the 4 errors and only 1 of the 10 slow traces тАФ it chose before it knew.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ the Python SDK with an OTLP exporter to a Collector (packages opentelemetry-sdk and opentelemetry-exporter-otlp):

from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.trace.sampling import ParentBased, ALWAYS_ON
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

provider = TracerProvider(
    resource=Resource.create({"service.name": "results-api", "service.version": "v41",
                              "deployment.environment": "production"}),
    sampler=ParentBased(ALWAYS_ON),        # send everything; the Collector tail-samples
)
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317", insecure=True)))
trace.set_tracer_provider(provider)

Or with no code changes (auto-instrumentation):

pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
OTEL_SERVICE_NAME=results-api OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 \
    opentelemetry-instrument python app.py

A Collector pipeline with tail sampling тАФ keep errors, keep slow traces, keep 10% of the rest (tail_sampling is in the Collector contrib distribution):

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 20
  tail_sampling:
    decision_wait: 10s              # wait for late spans before deciding
    policies:
      - name: errors
        type: status_code
        status_code: {status_codes: [ERROR]}
      - name: slow
        type: latency
        latency: {threshold_ms: 500}
      - name: the-rest
        type: probabilistic
        probabilistic: {sampling_percentage: 10}
  batch: {}

exporters:
  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true
  prometheusremotewrite:
    endpoint: http://prometheus:9090/api/v1/write

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, tail_sampling, batch]
      exporters: [otlp/tempo]
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [prometheusremotewrite]

On AWS, the ADOT Collector (AWS Distro for OpenTelemetry) adds exporters for X-Ray and CloudWatch; the Datadog Agent and Datadog's Collector exporter accept OTLP too.

ЁЯПн Why this matters in production: put a Collector between your apps and every backend from day one. It is where you drop secrets, sample, and switch vendors тАФ without touching a single service.

тПня╕П Next

The signals are flowing. Now store the metrics, ask them questions and draw them: Prometheus and Grafana.

git checkout lesson-07-prometheus-grafana
тЖР PrevioustracingNext тЖТprometheus grafana

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