Render summaries in the text format from their value objects
What does this MR do and why?
Prometheus::Client::Push renders the registry with Formats::Text.marshal, which read a summary's quantiles, sum and count as value.get.map, value.get.sum and value.get.total. Summary#values hands the format value.sum, the value object of the sum alone, and its get is a Float, so a registry holding a summary with any observation raised NoMethodError: undefined method 'map' for an instance of Float and could not be pushed. The count lives in Summary::Value#total, which Summary#values never hands out, so the format had no way to reach it either. This is the summary half that !299 (merged) left out because it needs a change to Summary itself.
This MR has four commits:
- Describe what Summary#get and Summary#values return. The
#getand#valuesexamples insummary_spec.rbwere skipped withxdescribeand expected quantiles, whichSummarydoes not compute. They now describe what both methods return today, onSimpleValue:#getis the sum of a label set's observations (0.0without any), and each value#valueshands out answersgetwith that sum. They pass on master. The spec also requiresprometheus/clientnow, as the counter, gauge and histogram specs do: run on its own on master, 3 of its examples fail withNoMethodError: undefined method 'logger' for module Prometheus::Client, because nothing has loadedprometheus/client, which definesPrometheus::Client.configurationand.logger. - Hand out each label set's Summary::Value from Summary#values.
Summarydrops its#valuesoverride and inheritsMetric#values, so it hands out each label set'sSummary::Value, asHistogramdoes withHistogram::Value.Summary::Valuegains#get, returning the sum, sosummary.values[labels].getkeeps returning the number it returned before (the first commit's example still passes), whilesumandtotalreach both value objects. A new example reads both; it fails on the first commit withundefined method 'sum' for an instance of Prometheus::Client::SimpleValue. - Render summaries in the text format from their value objects.
Formats::Text.summaryreads the sum fromvalue.sum.getand the count fromvalue.total.get, and writes noquantilesamples, sinceSummary::Valuekeeps none. That is also whatmarshal_multiprocesswrites for a summary. The format spec renders a realSummaryin place of the double (aHashof quantiles stubbed to answersumandtotal, a shapeSummarynever produces), and a new Push spec adds a registry holding one. Both fail on the second commit with theNoMethodErrorabove. - Document that a Summary keeps a sum and a count, not quantiles. The README described "an efficient percentile calculation mechanism" and showed
Summary#getreturning aHashof quantiles, and the class comment said the same. Both now say what a summary keeps, and the example shows#getreturning the sum.
Each commit passes the whole suite on its own, on the four Ruby versions the ruby CI job runs (details below). With this MR the issue's example renders as:
# HELP request_seconds Request time
# TYPE request_seconds summary
request_seconds_sum{route="x"} 0.75
request_seconds_count{route="x"} 2.0Closes #82 (closed)
What changes for callers
Summary#values is public, so this is the part to look at. In this gem its only reader is Formats::Text, and Push reads only its keys. A caller elsewhere now gets a Summary::Value for each label set instead of the sum's value object. get on it returns the same number as before, which the first commit pins and the second keeps passing; a caller that called increment or set on the old object would have to go through .sum. Summary#get and Formats::Text.marshal_multiprocess are unchanged.
The text format no longer tries to write quantile samples. On master it wrote none either: it raised before writing a summary at all.
I did not add a CHANGELOG.md entry, since the entries are written at release time, but I can add one under Unreleased if you prefer.
The new examples on the previous commit and on this branch
Second commit's summary_spec.rb with the first commit's summary.rb, Ruby 3.4.11:
$ bundle exec rspec spec/prometheus/client/summary_spec.rb
1) Prometheus::Client::Summary#values returns each label set with its sum and its count
[...]
NoMethodError:
undefined method 'sum' for an instance of Prometheus::Client::SimpleValue
# ./spec/prometheus/client/summary_spec.rb:72:in 'block (4 levels) in <top (required)>'
[...]
14 examples, 1 failureThird commit's format and Push specs with the second commit's formats/text.rb:
$ bundle exec rspec spec/prometheus/client/formats/text_spec.rb spec/prometheus/client/push_spec.rb
1) Prometheus::Client::Formats::Text single process metrics .marshal returns a Text format version 0.0.4 compatible representation
[...]
NoMethodError:
undefined method 'map' for an instance of Float
# ./lib/prometheus/client/formats/text.rb:61:in 'Prometheus::Client::Formats::Text.summary'
[...]
2) Prometheus::Client::Push#add with a summary in the registry sends the summary in the text format
[...]
NoMethodError:
undefined method 'map' for an instance of Float
# ./lib/prometheus/client/formats/text.rb:61:in 'Prometheus::Client::Formats::Text.summary'
[...]
30 examples, 2 failures, 1 pendingThe same three spec files from this branch with summary.rb and formats/text.rb from master 6be5328 give 44 examples, 3 failures, 1 pending, the three failures above. On each commit its own spec files pass, and Ruby 3.2.11, 3.3.12 and 4.0.7 give the same counts as 3.4.11.
The whole suite and RuboCop, per commit
bundle exec rake spec with prometheus_multiproc_dir=tmp/ and SKIP_C_EXTENSION_TESTING=1, as the ruby job runs it, after bundle exec rake compile with Rust 1.89.0, on Ruby 3.2.11, 3.3.12, 3.4.11 and 4.0.7 (the same counts on all four):
| Tree | Result |
|---|---|
| master | 183 examples, 0 failures, 19 pending |
| first commit | 182 examples, 0 failures, 15 pending |
| second commit | 183 examples, 0 failures, 15 pending |
| third commit | 184 examples, 0 failures, 15 pending |
| fourth commit | 184 examples, 0 failures, 15 pending |
The first commit replaces the four skipped examples with three that run.
bundle exec rubocop with the project's .rubocop.yml stops with exit 2 on master and on every commit before inspecting anything, because RuboCop 1.75.6 (the version Gemfile.lock pins) refuses its AlignHash and Style/TrailingCommaInLiteral keys as obsolete. With a copy of the config that renames them as the error asks, over lib, spec, examples, Rakefile and the gemspec:
- With
TargetRubyVersion: 2.2as configured, master and the first two commits have 270 offenses, and the third and fourth 273. The 3 new ones areLint/Syntaxin the Push spec: its new example uses a squiggly heredoc, like the histogram example above it, and the Ruby 2.2 parser cannot read one, which is also why master already reports 14Lint/Syntaxoffenses in that file and no other cop runs on it. - With
TargetRubyVersion: 3.2, the gemspec'srequired_ruby_version, master and the first commit have 373 offenses and the other three 372, and none falls on a line this MR adds.
The issue's example against a Pushgateway
The issue's code with the gem's default MmapedValue, pushed to prom/pushgateway 1.11.4 in Docker, Ruby 3.4.11.
On master both calls raise, so nothing reaches the Pushgateway:
--- Formats::Text.marshal(registry)
NoMethodError: undefined method 'map' for an instance of Float
/src/lib/prometheus/client/formats/text.rb:61:in 'Prometheus::Client::Formats::Text.summary'
--- Push#add
NoMethodError: undefined method 'map' for an instance of Float
/src/lib/prometheus/client/formats/text.rb:61:in 'Prometheus::Client::Formats::Text.summary'
--- what the Pushgateway serves for the jobOn this branch:
--- Formats::Text.marshal(registry)
# HELP request_seconds Request time
# TYPE request_seconds summary
request_seconds_sum{route="x"} 0.75
request_seconds_count{route="x"} 2.0
--- Push#add
HTTP 200
--- what the Pushgateway serves for the job
# HELP request_seconds Request time
# TYPE request_seconds summary
request_seconds_sum{instance="",job="example",route="x"} 0.75
request_seconds_count{instance="",job="example",route="x"} 2Formats::Text.marshal_multiprocess gives request_seconds_count{route="x"} 2 and request_seconds_sum{route="x"} 0.75 on both.