Sizing and TCO

Appendix F · The observability model, in full

Metrics, logs and traces, for an estate stated in ch02’s terms. Vendor neutral: nothing on this page names a product, and the structure is what transfers.

This is the book’s second model, and it shows what Appendix E does not. Three multiplicative chains hang off the same few roots, so they move together whether or not anybody says so. Label cardinality is a product of uncertain counts and therefore dominates everything downstream of it. The control knobs are sampling and retention rather than money. Three tiers, ingest, store and query, carry four ceilings between them, and no single number summarises them.

And it has a hole in it, deliberately left open.

What is not yet measured

Not measured yet

observability declares 2 constant(s) that nobody has measured:

  • collector throughput per core, measured — needs bench/results/collector-throughput-per-core.json

  • spans per request — needs bench/results/traces-spans-per-request.json

8 node(s) downstream of those cannot be computed and are shown as — rather than filled in. Nothing is estimated in their place: this book publishes measurements or it publishes nothing.

Source — observability-reference · every input on a slider · 2 constant(s) not yet measured

Spans per request is a property of somebody’s instrumented application at a particular version. It is not a property of any corpus, so this repository cannot derive it, and it is not a property of any machine, so the reference rig cannot measure it either. It belongs to the estate target: an observation somebody takes of a system they run.

Nobody has taken it. So the whole traces chain has no value, and the figures below show that rather than filling it in. This is not a gap waiting to be tidied up before publication. It is what an honest sizing model looks like before the work is done, and putting it on a published page is the clearest statement this book can make about the difference between a missing number and an invented one.

The same is true of collector throughput per core. The model declares it twice on purpose: once as the vendor’s quoted figure, which lets a ceiling be computed, and once as a measurement nobody has taken, which leaves a second ceiling with nothing in it but the words not yet measured. Reading those two rows next to each other is most of ch03 · Where the numbers come from.

The graph

Try it

The same model, as an interactive page. Turn the four knobs and watch which ceilings move, and which one does not. The knobs-turned-down scenario is the same model with all four already moved.

An observability platform — metrics, logs and traces — dependency graphinputderivedmeasuredceilingyou decideactive seriesannual growthmetric names per hostcollector corescollector throughput percore, measurednot measuredcollector throughputquotedgrowth factorhorizonhorizon periodshostsinstalled usableingest, metrics and logsonlyretention cost, metricsand logs onlystored, metrics and logsonlylabel cardinalityaccidental label valueslabel values endpointlabel values statuslines per requestfraction of log lines keptlog line ratelogs ingestbytes per log line,compressedlogs retentionlogs storedmeasured pipeline capacitymetrics ingestmetrics retentionbytes per sample,compressedmetrics storedone sample per seriesone yearlimit on ingestutilisation, measuredqueries per secondquery capacityquery nodesquery scan rate quotedquery series ratelimit on query pathutilisationquoted pipeline capacitylimit on ingestutilisation, quotedrequest ratesample ratescrape intervalseries per queryseries per query, beforelabelsspan ratespans per requestnot measuredstorage pricelimit on retention storefillstore nodestotal ingesttrace sampling ratetraces ingesttraces retentionbytes per span, compressedtraces storedusable tb per node

The unmeasured constants are drawn hollow, and everything downstream of them is dimmed. The dimmed nodes are the answer to “what would measuring this one thing unlock”, and nobody had to write that answer down.

Every formula

The graph above, as text: every derived quantity and every ceiling, with the formula the file gives it. The table is rendered from the file, so it cannot disagree with it.

QuantityFormulaUnit
horizon periods (horizon_periods)horizon / one_year
growth factor (growth_factor)annual_growth ** horizon_periods
label cardinality (label_cardinality)label_values_endpoint * label_values_status * label_values_accidental
active series (active_series)hosts * base_metrics_per_host * label_cardinality * growth_factorseries
sample rate (sample_rate)one_sample_per_series / scrape_intervalsample/series/second
metrics ingest (metrics_ingest)active_series * sample_rate * metrics_sample_bytesMB/s
metrics stored (metrics_stored)metrics_ingest * metrics_retentionTB
log line rate (log_line_rate)request_rate * lines_per_request * log_keep_rate * growth_factorline/second
logs ingest (logs_ingest)log_line_rate * logs_line_bytesMB/s
logs stored (logs_stored)logs_ingest * logs_retentionTB
span rate (span_rate)request_rate * spans_per_request * trace_keep_rate * growth_factorspan/second
traces ingest (traces_ingest)span_rate * traces_span_bytesMB/s
traces stored (traces_stored)traces_ingest * traces_retentionTB
ingest, metrics and logs only (known_ingest)metrics_ingest + logs_ingestMB/s
total ingest (total_ingest)metrics_ingest + logs_ingest + traces_ingestMB/s
quoted pipeline capacity (quoted_pipeline_capacity)collector_cores * collector_throughput_quotedMB/s
measured pipeline capacity (measured_pipeline_capacity)collector_cores * collector_throughput_measuredMB/s
ingest utilisation, quoted (quoted_pipeline_utilisation)known_ingest / quoted_pipeline_capacity against a limit of 1, keeping 0.4 below it
ingest utilisation, measured (pipeline_utilisation)total_ingest / measured_pipeline_capacity against a limit of 1, keeping 0.4 below it
installed usable (installed_usable)store_nodes * usable_tb_per_nodeTB
stored, metrics and logs only (known_stored)metrics_stored + logs_storedTB
retention store fill (store_fill)known_stored / installed_usable against a limit of 1, keeping 0.3 below it
series per query (series_per_query)series_per_query_base * label_cardinalityseries/query
query series rate (query_series_rate)queries_per_second * series_per_queryseries/second
query capacity (query_capacity)query_nodes * query_scan_rate_quotedseries/second
query path utilisation (query_utilisation)query_series_rate / query_capacity against a limit of 1, keeping 0.4 below it
retention cost, metrics and logs only (known_storage_cost)known_stored * storage_priceUSD/month

Source — models/observability/model.yaml

The three chains

OutputPoint estimate90% intervalUnit
metrics ingest4.690.709 to 29.1MB/s
logs ingest44.79.66 to 164MB/s
traces ingestnot yet measurednot yet measuredMB/s
traces storednot yet measurednot yet measuredTB
ingest, metrics and logs only49.412.5 to 182MB/s
stored, metrics and logs only27874.6 to 1,301TB
active series15,019,9172,269,389 to 93,180,409series
label cardinality76.317.9 to 300
retention cost, metrics and logs only$4,085$957.47 to $21,788USD/month
retention store fill0.5790.155 to 2.71
ingest utilisation, quoted0.5140.130 to 1.90
query path utilisation0.5690.0591 to 3.37
ingest utilisation, measurednot yet measurednot yet measured

Source — observability-reference · every input on a slider · 2 constant(s) not yet measured

Metrics are cheap in bytes and expensive in series. Logs are the reverse. Traces would be somewhere in between, and the model declines to guess.

Look hardest at the active series count. A cardinality node drives it, and that node is the product of three uncertain counts, and a product of uncertain things is far more uncertain than any of them:

Distribution of label cardinalitylabel cardinality — 100,000 samples90% interval 17.9 to 300 · median 75.2p5pointp951.972795560.9% of samples run on to 2,088

The four ceilings, one of which cannot be computed

CeilingAt the planHeadroomAllowedLimitVerdictOver allowedOver limit
ingest utilisation, measurednot yet measured
query path utilisation0.5740%0.601.00ok43%28%
ingest utilisation, quoted0.5140%0.601.00ok44%21%
retention store fill0.5830%0.701.00ok45%30%

Source — observability-reference · every input on a slider · 2 constant(s) not yet measured

Three tiers, three different mechanisms, three different margins: that is the argument for declaring headroom per ceiling rather than globally. The ingest and query margins are about queueing, where response time climbs long before anything is busy (ch06). The store margin is about rebuild, where losing a node costs capacity you were using (ch11). They only look alike because they are both percentages.

Two of the three that can be computed rest on an incomplete total. The because on each says so, and ch20 is about what it costs to forget.

What moves the answer

Tornado for active seriesactive series (series) — point estimate 15,019,917and 8 that do not move it at allaccidental label values30,659,278annual growth18,638,856label values endpoint17,624,612label values status9,611,786
InputKindactive series at its p10at its p90Swing
accidental label valuesinput6,131,85636,791,13330,659,278
annual growthinput8,356,81726,995,67318,638,856
label values endpointinput8,016,21425,640,82617,624,612
label values statusinput11,136,22020,748,0069,611,786
lines per requestinput15,019,91715,019,9170
bytes per log line, compressedmeasured15,019,91715,019,9170
bytes per sample, compressedmeasured15,019,91715,019,9170
queries per secondinput15,019,91715,019,9170

Source — observability-reference · every input on a slider · 2 constant(s) not yet measured

The accidental label is at the top of the tornado: the one nobody planned, added during an incident and never removed. ch08 is the chapter about that, and here it is demonstrated rather than warned about.

Turning the knobs

OutputReference scenarioThe four knobs, turned down
metrics ingest4.69
0.709 to 29.1
2.35
0.354 to 14.6
logs ingest44.7
9.66 to 164
4.47
0.966 to 16.4
traces ingestnot yet measurednot yet measured
traces storednot yet measurednot yet measured
ingest, metrics and logs only49.4
12.5 to 182
6.81
1.90 to 27.5
stored, metrics and logs only278
74.6 to 1,301
29.8
7.90 to 142
active series15,019,917
2,269,389 to 93,180,409
15,019,917
2,269,389 to 93,180,409
label cardinality76.3
17.9 to 300
76.3
17.9 to 300
retention cost, metrics and logs only$4,085
$957.47 to $21,788
$438.25
$101.75 to $2,374
retention store fill0.579
0.155 to 2.71
0.0621
0.0165 to 0.296
ingest utilisation, quoted0.514
0.130 to 1.90
0.0710
0.0198 to 0.287
query path utilisation0.569
0.0591 to 3.37
0.569
0.0591 to 3.37
ingest utilisation, measurednot yet measurednot yet measured
query path utilisation — over its limit28%28%
ingest utilisation, quoted — over its limit21%0%
retention store fill — over its limit30%0%

Source — observability-reference and observability-knobs_turned_down · every input on a slider · 4 constant(s) not yet measured

The right-hand column doubles the scrape interval, cuts metric retention by most of a year, keeps a tenth of the log lines and samples one trace in a hundred. Ingest and storage fall a long way.

The query ceiling does not move at all. Not by a little. Not at all, because none of the four knobs touches cardinality, and cardinality is what a query has to walk past. The controls a platform gives you operate on the chains that were already affordable, and the input that decides the size of the system is not one of them. That is the most useful thing on this page. Fixing cardinality means a conversation with whoever added the label.

Where the inputs came from

InputProvenanceSource
annual growthassumptionch04 — telemetry grows faster than the traffic it describes, because teams add instrumentation as well as load. Lognormal like any growth rate — it compounds and cannot go negative — and wider than the web service’s for the same reason its median is higher
metric names per hostassumptiondistinct metric names exposed per host, before labels
collector coresassumptionthe sizing decision for the ingest tier
collector throughput quotedvendor claimthroughput per core as documented by the collector. Unverified here, and marked as a claim in every figure it appears in
horizonassumptionthe planning horizon for this platform
hostsassumptionstated estate size (ch02)
accidental label valuesassumptionch08 — the cardinality explosion, as a distribution rather than as a warning. Lognormal because this one has no ceiling: a label carrying a user id multiplies the series count by a number nobody chose, and a triangular would assert a maximum that does not exist
label values endpointassumptiondistinct routes appearing as a label value. Triangular because somebody could go and count them this afternoon; the maximum is the routes that exist today, and a new service adds to it
label values statusassumptiondistinct status classes kept as a label. Triangular, and the one input in this model whose bounds are genuinely hard: a request cannot carry a status the code does not emit
lines per requestassumptionapplication log lines emitted per request served. Triangular, from reading a sample of the logs — and the maximum is the part to distrust, because a debug flag left on in production is outside it
fraction of log lines keptassumptiona control knob (ch12): drop and aggregation rules
logs retentionassumptiona control knob (ch12)
metrics retentionassumptiona control knob (ch12)
one sample per seriesfactdefinition
one yearfactdefinition
queries per secondassumptiondashboards, alert rules and people, at the busy hour. Triangular because two of those three are countable and the third is not
query nodesassumptionthe sizing decision for the query tier
query scan rate quotedvendor claimseries scanned per second per query node, quoted
request rateassumptionch03 — the busy hour across the estate, not the daily mean. Triangular: a minimum, a likely and a maximum is the whole of what a dashboard gives you, and the maximum is the busiest hour anybody has looked at rather than the busiest one there will be
scrape intervalassumptiona control knob (ch12): doubling it halves the metrics chain and loses resolution that cannot be recovered afterwards
series per query, before labelsassumptionhow many distinct metric-name-and-host combinations one dashboard panel or alert rule covers, before any label expansion. Triangular, and wide: dashboards differ from each other more than anything else in this model does
storage priceassumptionall-in cost of a usable TB-month on this tier. Lognormal because it is a price — and carried across by hand from whichever storage tier’s unit economics somebody had to hand, rather than by sampling, which is the seam ch18 is about
store nodesassumptionthe sizing decision for the retention tier
trace sampling rateassumptiona control knob (ch12), and the one with the widest range: head sampling at 1% and at 100% are two different platforms
traces retentionassumptiona control knob (ch12)
usable tb per nodeassumptionusable capacity per storage node after replication
26 inputs2 fact, 2 vendor claim, 22 assumption

Source — observability-reference · every input on a slider · 2 constant(s) not yet measured

ConstantValueStandard errorUnitMeasured against
collector throughput per core, measurednot yet measuredMB/s/corebench/results/collector-throughput-per-core.json does not exist
bytes per log line, compressed32.4± 0.0044bytes / linepython zlib (DEFLATE level 6)
bytes per sample, compressed9.37± 0.0009bytes / samplethis repository’s encoder, not any product’s
spans per requestnot yet measuredspan/requestbench/results/traces-spans-per-request.json does not exist
bytes per span, compressed73.6± 0.0173bytes / spanpython zlib (DEFLATE level 6)

Source — observability-reference · every input on a slider · 2 constant(s) not yet measured

Running it yourself

python3 -m bench.run_models --model observability
python3 scripts/verify-models.py
python3 scripts/render-figures.py
python3 -m pytest tests/test_models.py

To close the hole: measure spans per request on a system you run, stamp it as an estate observation with the system and the window disclosed, and every dimmed node on this page lights up with no other change to the model.