Skip to content

Commit adf63e8

Browse files
committed
Use the original metric's store in with_labels clone
When calling `with_labels` on a metric object (let's call it "the original"), we instantiate a new metric (the "clone") that is identical except that it has some more pre-set labels, that allow the caller to observe it without having to specify the labels every time. "currying", if you will. The problem with the existing code (as exemplified by issue #225, and by the tests introduced in the previous commit), is that as part of making this new metric, we end up instantiating a new store for this metric. With in-memory stores, the new one will be empty. With file stores, it'll bring over the data from the original metric until the point the clone gets observed once, at which point they fork, while pointing at the same file and keeping separate internal state. An almost sure recipe for file corruption. And when exporting, only the data in the "original" metric's store will be exported, the clone's will be ignored, assuming files didn't get corrupted. The fix is not particularly elegant, but I don't see any way around it: we replace the internal store of the "clone" metric with the one from the "original", through the use of a protected method. The only real alternative is getting rid of `with_labels`, which is a nice-to-have for convenience and performance, but not a necessity. Signed-off-by: Daniel Magliola <dmagliola@crystalgears.com>
1 parent c8ad029 commit adf63e8

2 files changed

Lines changed: 30 additions & 11 deletions

File tree

lib/prometheus/client/histogram.rb

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,18 @@ def self.exponential_buckets(start:, factor: 2, count:)
4242
end
4343

4444
def with_labels(labels)
45-
self.class.new(name,
46-
docstring: docstring,
47-
labels: @labels,
48-
preset_labels: preset_labels.merge(labels),
49-
buckets: @buckets,
50-
store_settings: @store_settings)
45+
new_metric = self.class.new(name,
46+
docstring: docstring,
47+
labels: @labels,
48+
preset_labels: preset_labels.merge(labels),
49+
buckets: @buckets,
50+
store_settings: @store_settings)
51+
52+
# The new metric needs to use the same store as the "main" declared one, otherwise
53+
# any observations on that copy with the pre-set labels won't actually be exported.
54+
new_metric.replace_internal_store(@store)
55+
56+
new_metric
5157
end
5258

5359
def type

lib/prometheus/client/metric.rb

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,21 +41,34 @@ def initialize(name,
4141
metric_settings: store_settings
4242
)
4343

44+
# WARNING: Our internal store can be replaced later by `with_labels`
45+
# Everything we do after this point needs to still work if @store gets replaced
4446
init_label_set({}) if labels.empty?
4547
end
4648

49+
protected def replace_internal_store(new_store)
50+
@store = new_store
51+
end
52+
53+
4754
# Returns the value for the given label set
4855
def get(labels: {})
4956
label_set = label_set_for(labels)
5057
@store.get(labels: label_set)
5158
end
5259

5360
def with_labels(labels)
54-
self.class.new(name,
55-
docstring: docstring,
56-
labels: @labels,
57-
preset_labels: preset_labels.merge(labels),
58-
store_settings: @store_settings)
61+
new_metric = self.class.new(name,
62+
docstring: docstring,
63+
labels: @labels,
64+
preset_labels: preset_labels.merge(labels),
65+
store_settings: @store_settings)
66+
67+
# The new metric needs to use the same store as the "main" declared one, otherwise
68+
# any observations on that copy with the pre-set labels won't actually be exported.
69+
new_metric.replace_internal_store(@store)
70+
71+
new_metric
5972
end
6073

6174
def init_label_set(labels)

0 commit comments

Comments
 (0)