Skip to content

Commit 469a92d

Browse files
committed
wip: metric specs refactor
Signed-off-by: Chris Sinjakli <chris@sinjakli.co.uk>
1 parent 8a34593 commit 469a92d

6 files changed

Lines changed: 249 additions & 392 deletions

File tree

spec/examples/metric_example.rb

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
describe '.new' do
3333
it 'returns a new metric' do
34-
expect(subject).to be
34+
expect(subject).to be_a(Prometheus::Client::Metric)
3535
end
3636

3737
it 'raises an exception if a reserved base label is used' do
@@ -76,18 +76,4 @@
7676
expect(subject.type).to be_a(Symbol)
7777
end
7878
end
79-
80-
describe '#get' do
81-
it 'returns the current metric value' do
82-
expect(subject.get).to be_a(type)
83-
end
84-
85-
context "with a subject that expects labels" do
86-
subject { described_class.new(:foo, docstring: 'Labels', labels: [:test]) }
87-
88-
it 'returns the current metric value for a given label set' do
89-
expect(subject.get(labels: { test: 'label' })).to be_a(type)
90-
end
91-
end
92-
end
9379
end

spec/prometheus/client/counter_spec.rb

Lines changed: 1 addition & 197 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@
1919
labels: expected_labels)
2020
end
2121

22-
it_behaves_like Prometheus::Client::Metric do
23-
let(:type) { Float }
24-
end
22+
it_behaves_like Prometheus::Client::Metric
2523

2624
describe '#increment' do
2725
it 'increments the counter' do
@@ -30,12 +28,6 @@
3028
end.to change { counter.get }.by(1.0)
3129
end
3230

33-
it 'raises an InvalidLabelSetError if sending unexpected labels' do
34-
expect do
35-
counter.increment(labels: { test: 'label' })
36-
end.to raise_error Prometheus::Client::LabelSetValidator::InvalidLabelSetError
37-
end
38-
3931
context "with a an expected label set" do
4032
let(:expected_labels) { [:test] }
4133

@@ -73,193 +65,5 @@
7365
end.each(&:join)
7466
end.to change { counter.get }.by(100.0)
7567
end
76-
77-
context "with non-string label values" do
78-
subject { described_class.new(:foo, docstring: 'Labels', labels: [:foo]) }
79-
80-
it "converts labels to strings for consistent storage" do
81-
subject.increment(labels: { foo: :label })
82-
expect(subject.get(labels: { foo: 'label' })).to eq(1.0)
83-
end
84-
85-
context "and some labels preset" do
86-
subject do
87-
described_class.new(:foo,
88-
docstring: 'Labels',
89-
labels: [:foo, :bar],
90-
preset_labels: { foo: :label })
91-
end
92-
93-
it "converts labels to strings for consistent storage" do
94-
subject.increment(labels: { bar: :label })
95-
expect(subject.get(labels: { foo: 'label', bar: 'label' })).to eq(1.0)
96-
end
97-
end
98-
end
99-
end
100-
101-
describe '#init_label_set' do
102-
context "with labels" do
103-
let(:expected_labels) { [:test] }
104-
105-
it 'initializes the metric for a given label set' do
106-
expect(counter.values).to eql({})
107-
108-
counter.init_label_set(test: 'value')
109-
110-
expect(counter.values).to eql({test: 'value'} => 0.0)
111-
end
112-
end
113-
114-
context "without labels" do
115-
it 'automatically initializes the metric' do
116-
expect(counter.values).to eql({} => 0.0)
117-
end
118-
end
119-
end
120-
121-
describe '#with_labels' do
122-
let(:expected_labels) { [:foo] }
123-
124-
it 'pre-sets labels for observations' do
125-
expect { counter.increment }
126-
.to raise_error(Prometheus::Client::LabelSetValidator::InvalidLabelSetError)
127-
expect { counter.with_labels(foo: 'label').increment }.not_to raise_error
128-
end
129-
130-
it 'registers `with_labels` observations in the original metric store' do
131-
counter.increment(labels: { foo: 'value1'})
132-
counter_with_labels = counter.with_labels({ foo: 'value2'})
133-
counter_with_labels.increment(by: 2)
134-
135-
expect(counter_with_labels.values).to eql({foo: 'value1'} => 1.0, {foo: 'value2'} => 2.0)
136-
expect(counter.values).to eql({foo: 'value1'} => 1.0, {foo: 'value2'} => 2.0)
137-
end
138-
139-
context 'when using DirectFileStore' do
140-
before do
141-
Dir.glob('/tmp/prometheus_test/*').each { |file| File.delete(file) }
142-
Prometheus::Client.config.data_store = Prometheus::Client::DataStores::DirectFileStore.new(dir: '/tmp/prometheus_test')
143-
end
144-
145-
let(:expected_labels) { [:foo, :bar] }
146-
147-
# Testing for file corruption: this is weird and complicated, so it needs explaining
148-
#
149-
# Files get corrupted when we have two different instances of `FileMappedDict`
150-
# reading and writing the same file. This corruption is expected; we should never have
151-
# two instances of `FileMappedDict` for the same file. If we do, it's a bug in our client.
152-
#
153-
# To clarify, the bug is that *we ended up with two instances for the same file*, not
154-
# that the instances are now corrupting the file.
155-
#
156-
# This is why we're testing this in `with_labels`. It's the only use case we've found
157-
# were we ended up with two instances (before we fixed that bug). `with_labels` is
158-
# incidental, if we find another way to get "duplicate" instances, we should add this
159-
# same exact test, except for the first line, where we need to instead reproduce
160-
# whatever bug gets us that second instance.
161-
#
162-
# The first thing we need to understand is why having two instances of `FileMappedDict`
163-
# corrupts the files:
164-
#
165-
# `FileMappedDict` keeps track, in an internal variable, of how many bytes in the file
166-
# have been used. When adding a new "entry" (observing a new labelset), it serializes
167-
# it and adds it at "the end" (according to its internal byte counter), and it also updates
168-
# the counter at the beginning of the file. However, it never re-reads that counter
169-
# from the file, because there shouldn't be any reason for it to have changed.
170-
#
171-
# If there are two instances pointing to the same file, initially they will both
172-
# share that internal counter, as they do the first read of the file, but if then
173-
# each of them adds an entry, their internal "length" counters will disagree, and
174-
# they'll start overwriting each other's entries.
175-
#
176-
# Importantly, if all of the entries happen to have the same length, it will be "fine".
177-
# Some of the labelsets will effectively disappear, but there will be no corruption,
178-
# because all the important things will fall in the right offsets by pure chance. This
179-
# would be very rare in production, but in a test, it's what normally happens because
180-
# we set all labels to "foo", "bar", etc. This is the reason for "longervalue" below,
181-
# we need to have different labelset lenghts to reproduce the corruption.
182-
#
183-
# With this background about the internals, we can now get to why the specific sequence of
184-
# steps below ends up in corrupted files.
185-
#
186-
# For this to make sense, i'll need to describe the contents of the file at each step.
187-
# I'll represent it like this: `27|labelset1,value1|labelset2,value2|labelset3,value3|`
188-
#
189-
# These are not the bytes we store in the file, but conceptually it's equivalent,
190-
# with two caveats:
191-
# - The counter at the beginning (27 == 3 * 9) here shows the combined length of labelsets.
192-
# It'd normally also include the length of values, but doing that makes this explanation
193-
# much harder to follow.
194-
# - Each entry also starts with a 4-byte int specifying the length of its labelset, so
195-
# we know how much to read. Again, I'm omitting that for readability.
196-
#
197-
#
198-
# Steps to reproduce:
199-
# - We declare `counter` and `counter_with_labels` as a clone. Neither has read the file.
200-
# - We increment `counter`, which creates the file and adds the entry ("labelset1")
201-
# - File: `9|labelset1,value1|`
202-
# - We increment `counter_with_labels`, which reads the file, and adds the new entry
203-
# to it ("muchlongerlabelset2").
204-
# - File: `28|labelset1,value1|muchlongerlabelset2, value2|`
205-
# - `counter` and `counter_with_labels` now disagree about the length of this file
206-
# (`counter` doesn't know the file has grown).
207-
# - We now add a new entry to `counter` ("labelset3"), which thinks the file is shorter
208-
# than it actually is.
209-
# - File: `18|labelset1,value1|labelset3,value3|et2, value2|`
210-
# - The initial counter reflects both labelsets for `counter`; then we have those
211-
# labelsetsp; and finally some "garbage" after the "end" (the garbage is the
212-
# last few bytes of the much longer entry added before by `counter_with_labels`)
213-
# - so far, though, we're still good. If you read the file, all entries are "fine",
214-
# because you're only reading up to the "18" length specified at the beginning.
215-
# - for the problem to manifest itself, we need to increment that counter at the
216-
# beginning, so we'll read the garbage. **BUT**, if we add a new labelset to
217-
# `counter`, it'll overwrite the "garbage" with good data, and the file will
218-
# continue to be fine.
219-
# - We add a new entry to `counter_with_labels`. This updates the length counter at
220-
# the beginning of the file.
221-
# - File: `47|labelset1,value1|labelset3,value3|et2, value2|muchlongerlabelset4, value4|`
222-
#
223-
# - Now the file is properly corrupted. When reading it, `FileMappedDict` sees:
224-
# - labelset1,value1 (cool)
225-
# - labelset3,value3 (cool)
226-
# - et2, value2 (boom)
227-
# |-> the beginning of this entry is garbage because we're actually at the middle
228-
# of an entry, not a beginning.
229-
#
230-
# What actually breaks is that each of these entries is expected to have, at their
231-
# beginning, the length in bytes of its labelset, so we know how much to read.
232-
# Now we have garbage in that position, and `FileMappedDict` will either:
233-
# - Try to interpret those four bytes as a long, get an invalid result.
234-
# - Try to read an invalid amount of data (maybe a negative amount).
235-
# - After reading the labelset, try to read the float and go past the end of the file
236-
# - Actually read what it thinks is a float, try to `unpack` it, and fail because
237-
# it's actually garbage.
238-
# - I'm sure there are other fun ways for it to fail.
239-
it "doesn't corrupt the data files" do
240-
counter_with_labels = counter.with_labels({ foo: 'longervalue'})
241-
242-
# Initialize / read the files for both views of the metric
243-
counter.increment(labels: { foo: 'value1', bar: 'zzz'})
244-
counter_with_labels.increment(by: 2, labels: {bar: 'zzz'})
245-
246-
# After both MetricStores have their files, add a new entry to both
247-
counter.increment(labels: { foo: 'value1', bar: 'aaa'}) # If there's a bug, we partially overwrite { foo: 'longervalue', bar: 'zzz'}
248-
counter_with_labels.increment(by: 2, labels: {bar: 'aaa'}) # Extend the file so we read past that overwrite
249-
250-
expect { counter.values }.not_to raise_error # Check it hasn't corrupted our files
251-
expect { counter_with_labels.values }.not_to raise_error # Check it hasn't corrupted our files
252-
253-
expected_values = {
254-
{foo: 'value1', bar: 'zzz'} => 1.0,
255-
{foo: 'value1', bar: 'aaa'} => 1.0,
256-
{foo: 'longervalue', bar: 'zzz'} => 2.0,
257-
{foo: 'longervalue', bar: 'aaa'} => 2.0,
258-
}
259-
260-
expect(counter.values).to eql(expected_values)
261-
expect(counter_with_labels.values).to eql(expected_values)
262-
end
263-
end
26468
end
26569
end

spec/prometheus/client/gauge_spec.rb

Lines changed: 1 addition & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@
1818
labels: expected_labels)
1919
end
2020

21-
it_behaves_like Prometheus::Client::Metric do
22-
let(:type) { Float }
23-
end
21+
it_behaves_like Prometheus::Client::Metric
2422

2523
describe '#set' do
2624
it 'sets a metric value' do
@@ -29,12 +27,6 @@
2927
end.to change { gauge.get }.from(0).to(42)
3028
end
3129

32-
it 'raises an InvalidLabelSetError if sending unexpected labels' do
33-
expect do
34-
gauge.set(42, labels: { test: 'value' })
35-
end.to raise_error Prometheus::Client::LabelSetValidator::InvalidLabelSetError
36-
end
37-
3830
context "with a an expected label set" do
3931
let(:expected_labels) { [:test] }
4032

@@ -67,12 +59,6 @@
6759
end.to change { gauge.get }.by(1.0)
6860
end
6961

70-
it 'raises an InvalidLabelSetError if sending unexpected labels' do
71-
expect do
72-
gauge.increment(labels: { test: 'value' })
73-
end.to raise_error Prometheus::Client::LabelSetValidator::InvalidLabelSetError
74-
end
75-
7662
context "with a an expected label set" do
7763
let(:expected_labels) { [:test] }
7864

@@ -104,29 +90,6 @@
10490
end.each(&:join)
10591
end.to change { gauge.get }.by(100.0)
10692
end
107-
108-
context "with non-string label values" do
109-
subject { described_class.new(:foo, docstring: 'Labels', labels: [:foo]) }
110-
111-
it "converts labels to strings for consistent storage" do
112-
subject.increment(labels: { foo: :label })
113-
expect(subject.get(labels: { foo: 'label' })).to eq(1.0)
114-
end
115-
116-
context "and some labels preset" do
117-
subject do
118-
described_class.new(:foo,
119-
docstring: 'Labels',
120-
labels: [:foo, :bar],
121-
preset_labels: { foo: :label })
122-
end
123-
124-
it "converts labels to strings for consistent storage" do
125-
subject.increment(labels: { bar: :label })
126-
expect(subject.get(labels: { foo: 'label', bar: 'label' })).to eq(1.0)
127-
end
128-
end
129-
end
13093
end
13194

13295
describe '#decrement' do
@@ -140,12 +103,6 @@
140103
end.to change { gauge.get }.by(-1.0)
141104
end
142105

143-
it 'raises an InvalidLabelSetError if sending unexpected labels' do
144-
expect do
145-
gauge.decrement(labels: { test: 'value' })
146-
end.to raise_error Prometheus::Client::LabelSetValidator::InvalidLabelSetError
147-
end
148-
149106
context "with a an expected label set" do
150107
let(:expected_labels) { [:test] }
151108

@@ -178,43 +135,4 @@
178135
end.to change { gauge.get }.by(-100.0)
179136
end
180137
end
181-
182-
describe '#init_label_set' do
183-
context "with labels" do
184-
let(:expected_labels) { [:test] }
185-
186-
it 'initializes the metric for a given label set' do
187-
expect(gauge.values).to eql({})
188-
189-
gauge.init_label_set(test: 'value')
190-
191-
expect(gauge.values).to eql({test: 'value'} => 0.0)
192-
end
193-
end
194-
195-
context "without labels" do
196-
it 'automatically initializes the metric' do
197-
expect(gauge.values).to eql({} => 0.0)
198-
end
199-
end
200-
end
201-
202-
describe '#with_labels' do
203-
let(:expected_labels) { [:foo] }
204-
205-
it 'pre-sets labels for observations' do
206-
expect { gauge.set(10) }
207-
.to raise_error(Prometheus::Client::LabelSetValidator::InvalidLabelSetError)
208-
expect { gauge.with_labels(foo: 'value').set(10) }.not_to raise_error
209-
end
210-
211-
it 'registers `with_labels` observations in the original metric store' do
212-
gauge.set(1, labels: { foo: 'value1'})
213-
gauge_with_labels = gauge.with_labels({ foo: 'value2'})
214-
gauge_with_labels.set(2)
215-
216-
expect(gauge_with_labels.values).to eql({foo: 'value1'} => 1.0, {foo: 'value2'} => 2.0)
217-
expect(gauge.values).to eql({foo: 'value1'} => 1.0, {foo: 'value2'} => 2.0)
218-
end
219-
end
220138
end

0 commit comments

Comments
 (0)