Observability · how we see inside our apps

Observability · controls and cost

What you can turn, and what it costs

Each landscape can turn each signal on or off, sample traces, cap labels and profile only some pods. The contract sets the limits; a landscape can never go past them. Costs are measured only on one machine; real-scale cost is not measured.

  • Order: env > landscape config > base config > contract default. Nothing may pass a contract ceiling. DECIDED · A193 tab 2
  • There is no supported profiling rate knob in any runtime (Go can be forced lower only through an unsupported race), so "profile a % of traffic" means "profile some pods". TESTED · Q10; Q18

Try it: what gets collected

Move the sliders. Numbers come from the design and the spikes; anything not measured says so.

One service in one landscape
Contract ceiling in the example: 0.5
Contract minimum in the example: 0.01

Sources: DECIDED · A193 tab 2 TESTED · spike 10; spike 15 OPEN · A193 pick 3 PROPOSED · jaeda: dead-link estimate

The controls

ControlContract setsLandscape configEnv override
Signal on/offwhich signals existotel.<signal>.enabledOTEL_SDK_DISABLED
Trace samplingmin_ratiootel.traces.sampler.ratioOTEL_TRACES_SAMPLER_ARG
Log level—otel.logs.min_levelDURIAN_OTEL_LOG_LEVEL
Profiled podsmax_pod_shareotel.profiles.pod_shareDURIAN_OTEL_PROFILES_ENABLED
Label budgetbudget (default 100)cardinality_limit ≤ budget × 4never raised at runtime

All rows: DECIDED · A193 tab 2. Pod share row: OPEN · A193 pick 3.

  • Example: raichu sets ratio: 0.1, profiles.enabled: true, pod_share: 0.25. Pichu keeps the base. DECIDED · A193 tab 2
  • The base config has profiling off; each landscape turns it on. PROPOSED · A193 tab 1, jaeda call 7
  • Going past a ceiling fails: raichu.yaml: otel.profiles.pod_share 0.8 > contract max_pod_share 0.5. DECIDED · A193 tab 2
  • OTEL_SDK_DISABLED=true keeps the ports, sends nothing and turns profiling off. DECIDED · A193 tab 2

The label budget

Free-form labels are allowed, with two guards: banned names, and a budget of series per metric. DECIDED · A193 tab 1 #5

One metric, e.g. checkout.charges
Example: adding payment.card_bin as a label

Sources: DECIDED · A193 tab 2; tab 3; tab 4

  • Banned names fail at once. Example: user.id on a metric → Err(invalid-input, "user.id is banned on metrics; put it on the span or log"). DECIDED · A193 tab 3
  • The budget counts label sets per metric, per landscape, across all pods. PROPOSED · A193 tab 1, jaeda call 6
  • Each process caps itself at budget × 4. Extra label sets go into one overflow series, otel.metric.overflow=true. Example: budget 30 → cap 120. DECIDED · A193 tab 4
  • obs assert fails the e2e run when a metric goes over budget. Example: checkout.charges: 212 series > budget 30 (new label: payment.card_bin). DECIDED · A193 tab 2
  • A budget over 1000 needs a written reason:. DECIDED · A193 tab 3
  • Profile labels count as series in Pyroscope too, so the same rules apply. DECIDED · A193 tab 4

Profiling: what each runtime allows

BunGo.NET
Hownode:inspector → our ~80-line pprof converterpyroscope-goPyroscope .NET profiler
TypesCPU onlyCPU, memory, goroutines, locksCPU, wall, memory, locks, exceptions
Span profilesno (link by time only)yesyes
Rate knobnone (one profile at a time)none (fixed 100 Hz)none (fixed ~9 ms)

Rows: TESTED · Q3; Q4; Q8; Q9; Q10.

  • @pyroscope/nodejs fails under Bun (ERR_DLOPEN_FAILED). Hence our own converter. TESTED · Q3
  • Flutter has no profiling. Faro sends CPU and memory measurements, not profiles. DECIDED · A193 tab 5
  • Pyroscope can also drop profiles on the server. Example: probability 0.5 keeps half of checkout's profiles. TESTED · Q18, docs and config check

Open pick 3 lives here: profile 1 in N pods, every pod, or per request. Options and trade-offs. OPEN · A193 pick 3

Measured costs

WhatNumberMeasured whereStatus
Go profiling CPUunder ~2%, but inside ±5% run noise: not a firm boundspike, one machineTESTED · Q10
.NET profiling CPUunder 1% (19.3 vs 19.3 req/s)spike, one machineTESTED · Q10
Bun profiling CPUnot measured—NOT TESTED · A193 tab 4
Profiling memoryunder 50 MB per podGrafana's published numbers, not oursNOT TESTED · Q18 cites docs
Local stack memory~630 MiB idle, under 800 MiB busy (busy = only 15 pushes)6 containers, one machineTESTED · Q15
Local stack start46 s cold, images cached (evidence: up in 18 s, ready 46 s later)one machineTESTED · Q15
Local stack CPU~2.7% of a 32-core boxone machineTESTED · Q15
Pyroscope delay1–2 min from send to queryone machineTESTED · Q15
Grafana in step 5+~270 MiBone machineTESTED · A193 pick 2

Not measured anywhere: storage and network cost at production scale, Loki/Tempo/Pyroscope in normal mode on object storage, and Bun's profiling cost. The Mimir vs VictoriaMetrics test used 10,000 series for a short time; "production scale wasn't tested". NOT TESTED · A193 tab 5

All pages