Skip to content

Commit 9f7603b

Browse files
author
Chris Sinjakli
committed
Convert label values to strings in base Metric class
Currently, you get an inconsistent experience when using different store implementations (e.g. `SingleThreaded` vs `DirectFileStore`). Because `DirectFileStore` has to write your metrics into a file, it has to convert label values into strings. That means that when you access them they're strings. Other stores don't do this, so you can access them as symbols if you provided them as symbols. To give users a consistent experience when swapping between stores, this commit changes the Metric base class to convert the label values to strings before they get anywhere near the underlying store. Signed-off-by: Chris Sinjakli <chris@gocardless.com>
1 parent 9cb8cd4 commit 9f7603b

6 files changed

Lines changed: 134 additions & 1 deletion

File tree

lib/prometheus/client/metric.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def initialize(name,
2727

2828
@name = name
2929
@docstring = docstring
30-
@preset_labels = preset_labels
30+
@preset_labels = stringify_values(preset_labels)
3131

3232
@store = Prometheus::Client.config.data_store.for_metric(
3333
name,
@@ -85,8 +85,13 @@ def validate_docstring(docstring)
8585
def label_set_for(labels)
8686
# We've already validated, and there's nothing to merge. Save some cycles
8787
return preset_labels if @all_labels_preset && labels.empty?
88+
labels = stringify_values(labels)
8889
@validator.validate_labelset!(preset_labels.merge(labels))
8990
end
91+
92+
def stringify_values(labels)
93+
labels.map { |k,v| [k, v.to_s] }.to_h
94+
end
9095
end
9196
end
9297
end

spec/examples/metric_example.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
# encoding: UTF-8
22

3+
# TODO: Convert these tests to use a fake metric class rather than shared examples
4+
#
5+
# Right now, we're using shared examples that we include in every metric type's tests
6+
# to validate the behaviour of the base metric class.
7+
#
8+
# This makes it difficult to test certain behaviour, as the interfaces of those metric
9+
# types differ and these tests can end up needing to know about them.
10+
#
11+
# You can see that in the tests for #get, which depend on `type` which isn't defined in
12+
# this file. The test files that include these shared examples have to do so with a block
13+
# that provides the `type` variable.
14+
#
15+
# This cropped up in a much worse way when trying to test the code that makes sure label
16+
# values are all strings. Writing a test here that gets included in all the real metric
17+
# implementations is near impossible. You need your test to call a different method to
18+
# alter a metric value (e.g. `set`, `increment` or `observe` depending on the metric type)
19+
# which means having each concrete metric type's tests passing us a lambda that we can
20+
# call agnostically of the metric type.
21+
#
22+
# The resultant code is confusing to follow, so we opted to duplicate those tests in each
23+
# metric type's test file.
24+
#
25+
# Changing this file to implement a fake metric class (e.g. `FakeTestCounter`) would let
26+
# us easily test the functionality of the base `Prometheus::Client::Metric` without
27+
# getting caught up in the specifics of the real metric types.
28+
329
shared_examples_for Prometheus::Client::Metric do
430
subject { described_class.new(:foo, docstring: 'foo description') }
531

spec/prometheus/client/counter_spec.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,28 @@
7878
end.each(&:join)
7979
end.to change { counter.get }.by(100.0)
8080
end
81+
82+
context "with non-string label values" do
83+
subject { described_class.new(:foo, docstring: 'Labels', labels: [:foo]) }
84+
85+
it "converts labels to strings for consistent storage" do
86+
subject.increment(labels: { foo: :label })
87+
expect(subject.get(labels: { foo: 'label' })).to eq(1.0)
88+
end
89+
90+
context "and some labels preset" do
91+
subject do
92+
described_class.new(:foo,
93+
docstring: 'Labels',
94+
labels: [:foo, :bar],
95+
preset_labels: { foo: :label })
96+
end
97+
98+
it "converts labels to strings for consistent storage" do
99+
subject.increment(labels: { bar: :label })
100+
expect(subject.get(labels: { foo: 'label', bar: 'label' })).to eq(1.0)
101+
end
102+
end
103+
end
81104
end
82105
end

spec/prometheus/client/gauge_spec.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,29 @@
110110
end.each(&:join)
111111
end.to change { gauge.get }.by(100.0)
112112
end
113+
114+
context "with non-string label values" do
115+
subject { described_class.new(:foo, docstring: 'Labels', labels: [:foo]) }
116+
117+
it "converts labels to strings for consistent storage" do
118+
subject.increment(labels: { foo: :label })
119+
expect(subject.get(labels: { foo: 'label' })).to eq(1.0)
120+
end
121+
122+
context "and some labels preset" do
123+
subject do
124+
described_class.new(:foo,
125+
docstring: 'Labels',
126+
labels: [:foo, :bar],
127+
preset_labels: { foo: :label })
128+
end
129+
130+
it "converts labels to strings for consistent storage" do
131+
subject.increment(labels: { bar: :label })
132+
expect(subject.get(labels: { foo: 'label', bar: 'label' })).to eq(1.0)
133+
end
134+
end
135+
end
113136
end
114137

115138
describe '#decrement' do

spec/prometheus/client/histogram_spec.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,35 @@
7373
expect { histogram.with_labels(test: 'value').observe(2) }.not_to raise_error
7474
end
7575
end
76+
77+
context "with non-string label values" do
78+
let(:histogram) do
79+
described_class.new(:foo,
80+
docstring: 'foo description',
81+
labels: [:foo],
82+
buckets: [2.5, 5, 10])
83+
end
84+
85+
it "converts labels to strings for consistent storage" do
86+
histogram.observe(5, labels: { foo: :label })
87+
expect(histogram.get(labels: { foo: 'label' })["10"]).to eq(1.0)
88+
end
89+
90+
context "and some labels preset" do
91+
let(:histogram) do
92+
described_class.new(:foo,
93+
docstring: 'foo description',
94+
labels: [:foo, :bar],
95+
preset_labels: { foo: :label },
96+
buckets: [2.5, 5, 10])
97+
end
98+
99+
it "converts labels to strings for consistent storage" do
100+
histogram.observe(5, labels: { bar: :label })
101+
expect(histogram.get(labels: { foo: 'label', bar: 'label' })["10"]).to eq(1.0)
102+
end
103+
end
104+
end
76105
end
77106

78107
describe '#get' do

spec/prometheus/client/summary_spec.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,33 @@
6868
expect { summary.with_labels(test: 'value').observe(2) }.not_to raise_error
6969
end
7070
end
71+
72+
context "with non-string label values" do
73+
let(:summary) do
74+
described_class.new(:foo,
75+
docstring: 'foo description',
76+
labels: [:foo])
77+
end
78+
79+
it "converts labels to strings for consistent storage" do
80+
summary.observe(5, labels: { foo: :label })
81+
expect(summary.get(labels: { foo: 'label' })["count"]).to eq(1.0)
82+
end
83+
84+
context "and some labels preset" do
85+
let(:summary) do
86+
described_class.new(:foo,
87+
docstring: 'foo description',
88+
labels: [:foo, :bar],
89+
preset_labels: { foo: :label })
90+
end
91+
92+
it "converts labels to strings for consistent storage" do
93+
summary.observe(5, labels: { bar: :label })
94+
expect(summary.get(labels: { foo: 'label', bar: 'label' })["count"]).to eq(1.0)
95+
end
96+
end
97+
end
7198
end
7299

73100
describe '#get' do

0 commit comments

Comments
 (0)