fast-prometheus 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS.md +8 -1
- data/CHANGELOG.md +108 -0
- data/README.md +15 -8
- data/lib/fast/prometheus/counter.rb +1 -1
- data/lib/fast/prometheus/formats/metrics_proto.rb +95 -0
- data/lib/fast/prometheus/formats/protobuf.rb +209 -89
- data/lib/fast/prometheus/formats/text.rb +48 -38
- data/lib/fast/prometheus/gauge.rb +7 -3
- data/lib/fast/prometheus/histogram.rb +53 -17
- data/lib/fast/prometheus/metric.rb +76 -16
- data/lib/fast/prometheus/native_histogram.rb +22 -8
- data/lib/fast/prometheus/otlp/mapper.rb +59 -61
- data/lib/fast/prometheus/otlp/proto.rb +169 -0
- data/lib/fast/prometheus/otlp/service_interface.rb +3 -3
- data/lib/fast/prometheus/registry.rb +21 -9
- data/lib/fast/prometheus/snapshot.rb +29 -55
- data/lib/fast/prometheus/store.rb +4 -2
- data/lib/fast/prometheus/summary.rb +22 -3
- data/lib/fast/prometheus/version.rb +1 -1
- metadata +7 -10
- data/lib/fast/prometheus/formats/metrics_pb.rb +0 -33
- data/lib/fast/prometheus/otlp/pb/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.rb +0 -27
- data/lib/fast/prometheus/otlp/pb/opentelemetry/proto/common/v1/common_pb.rb +0 -26
- data/lib/fast/prometheus/otlp/pb/opentelemetry/proto/metrics/v1/metrics_pb.rb +0 -41
- data/lib/fast/prometheus/otlp/pb/opentelemetry/proto/resource/v1/resource_pb.rb +0 -23
|
@@ -7,6 +7,10 @@ module Fast
|
|
|
7
7
|
module Formats
|
|
8
8
|
# Prometheus text exposition format 0.0.4.
|
|
9
9
|
# Consumes Snapshot value objects; never reads live metrics.
|
|
10
|
+
#
|
|
11
|
+
# Every piece is appended straight to the output buffer: no line, label
|
|
12
|
+
# or name is built as its own String first, so a render allocates one
|
|
13
|
+
# String per sample (the value's to_s) and nothing per label.
|
|
10
14
|
module Text
|
|
11
15
|
CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8"
|
|
12
16
|
|
|
@@ -20,69 +24,75 @@ module Fast
|
|
|
20
24
|
snapshot.metrics.each_with_object(String.new) do |metric, output|
|
|
21
25
|
next if metric.type == :native_histogram
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
output << "#
|
|
27
|
+
name = metric.name.name
|
|
28
|
+
output << "# HELP " << name << " " << escape_doc(metric.docstring) << "\n"
|
|
29
|
+
output << "# TYPE " << name << " " << metric.type.name << "\n"
|
|
25
30
|
|
|
26
|
-
metric.
|
|
27
|
-
|
|
31
|
+
names = metric.label_names
|
|
32
|
+
metric.series.each_pair do |values, value|
|
|
33
|
+
render_series(output, metric.type, name, names, values, value)
|
|
28
34
|
end
|
|
29
35
|
end
|
|
30
36
|
end
|
|
31
37
|
|
|
32
|
-
private_class_method def self.render_series(output, type, name,
|
|
38
|
+
private_class_method def self.render_series(output, type, name, names, values, value)
|
|
33
39
|
case type
|
|
34
40
|
when :counter, :gauge
|
|
35
|
-
|
|
41
|
+
sample(output, name, nil, names, values, value)
|
|
36
42
|
when :histogram
|
|
37
|
-
histogram_lines(output, name,
|
|
43
|
+
histogram_lines(output, name, names, values, value)
|
|
38
44
|
when :summary
|
|
39
|
-
|
|
45
|
+
sample(output, name, "_sum", names, values, value.sum)
|
|
46
|
+
sample(output, name, "_count", names, values, value.count)
|
|
40
47
|
end
|
|
41
48
|
end
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
# One sample line: name, optional suffix, labels (names and values
|
|
51
|
+
# side by side, plus an le label carrying +upper_bound+ for histogram
|
|
52
|
+
# buckets), value.
|
|
53
|
+
private_class_method def self.sample(output, name, suffix, names, values, value, upper_bound = nil)
|
|
54
|
+
output << name
|
|
55
|
+
output << suffix if suffix
|
|
56
|
+
append_labels(output, names, values, upper_bound)
|
|
57
|
+
output << " " << value.to_s << "\n"
|
|
47
58
|
end
|
|
48
59
|
|
|
49
|
-
private_class_method def self.
|
|
50
|
-
|
|
51
|
-
return string unless LABEL_ESCAPE.match?(string)
|
|
60
|
+
private_class_method def self.append_labels(output, names, values, upper_bound)
|
|
61
|
+
return if names.empty? && upper_bound.nil?
|
|
52
62
|
|
|
53
|
-
|
|
63
|
+
output << "{"
|
|
64
|
+
names.each_index { |i| output << label_name(names[i]) << "=\"" << escape_label(values[i]) << "\"," }
|
|
65
|
+
output << "le=\"" << upper_bound << "\"," if upper_bound
|
|
66
|
+
output.chop!
|
|
67
|
+
output << "}"
|
|
54
68
|
end
|
|
55
69
|
|
|
56
|
-
private_class_method def self.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
output = String.new("{")
|
|
60
|
-
labels.each do |key, value|
|
|
61
|
-
output << "#{key}=\"#{escape_label(value)}\""
|
|
62
|
-
output << ","
|
|
70
|
+
private_class_method def self.histogram_lines(output, name, names, values, value)
|
|
71
|
+
value.cumulative_buckets.each do |boundary, count|
|
|
72
|
+
sample(output, name, "_bucket", names, values, count, boundary.infinite? ? "+Inf" : boundary.to_s)
|
|
63
73
|
end
|
|
64
|
-
|
|
65
|
-
output
|
|
66
|
-
output
|
|
74
|
+
|
|
75
|
+
sample(output, name, "_sum", names, values, value.sum)
|
|
76
|
+
sample(output, name, "_count", names, values, value.count)
|
|
67
77
|
end
|
|
68
78
|
|
|
69
|
-
|
|
70
|
-
|
|
79
|
+
# Label names are Symbols (Symbol#name is frozen and shared); a
|
|
80
|
+
# String name is appended as is.
|
|
81
|
+
private_class_method def self.label_name(key)
|
|
82
|
+
key.is_a?(Symbol) ? key.name : key
|
|
71
83
|
end
|
|
72
84
|
|
|
73
|
-
private_class_method def self.
|
|
74
|
-
|
|
75
|
-
le = boundary.infinite? ? "+Inf" : boundary.to_s
|
|
76
|
-
metric_line(output, "#{name}_bucket", labels.merge("le" => le), count)
|
|
77
|
-
end
|
|
85
|
+
private_class_method def self.escape_doc(string)
|
|
86
|
+
return string unless DOC_ESCAPE.match?(string)
|
|
78
87
|
|
|
79
|
-
|
|
80
|
-
metric_line(output, "#{name}_count", labels, value.count)
|
|
88
|
+
string.gsub(DOC_ESCAPE, DOC_REPLACE)
|
|
81
89
|
end
|
|
82
90
|
|
|
83
|
-
private_class_method def self.
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
private_class_method def self.escape_label(string)
|
|
92
|
+
string = string.to_s
|
|
93
|
+
return string unless LABEL_ESCAPE.match?(string)
|
|
94
|
+
|
|
95
|
+
string.gsub(LABEL_ESCAPE, LABEL_REPLACE)
|
|
86
96
|
end
|
|
87
97
|
end
|
|
88
98
|
end
|
|
@@ -10,14 +10,18 @@ module Fast
|
|
|
10
10
|
:gauge
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def set(value, labels:
|
|
13
|
+
def set(value, labels: NO_LABELS)
|
|
14
14
|
raise ArgumentError, "value must be a numeric" unless value.is_a?(Numeric)
|
|
15
15
|
|
|
16
16
|
key = resolve(labels)
|
|
17
17
|
store.synchronize { store[key] = value.to_f }
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def
|
|
20
|
+
def set_to_current_time(labels: NO_LABELS)
|
|
21
|
+
set(Time.now.to_f, labels: labels)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def increment(by: 1, labels: NO_LABELS)
|
|
21
25
|
raise ArgumentError, "by must be a numeric" unless by.is_a?(Numeric)
|
|
22
26
|
return if by.zero?
|
|
23
27
|
|
|
@@ -25,7 +29,7 @@ module Fast
|
|
|
25
29
|
store.synchronize { store[key] = (store[key] || 0.0) + by.to_f }
|
|
26
30
|
end
|
|
27
31
|
|
|
28
|
-
def decrement(by: 1, labels:
|
|
32
|
+
def decrement(by: 1, labels: NO_LABELS)
|
|
29
33
|
raise ArgumentError, "by must be a numeric" unless by.is_a?(Numeric)
|
|
30
34
|
return if by.zero?
|
|
31
35
|
|
|
@@ -25,15 +25,19 @@ module Fast
|
|
|
25
25
|
@cells = [0] * (buckets.size + 1)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# Cumulative bucket counts
|
|
28
|
+
# Cumulative bucket counts as frozen [boundary, count] pairs, ending
|
|
29
|
+
# with [Float::INFINITY, count]. Frozen on the way out so a snapshot
|
|
30
|
+
# can hold it as is.
|
|
29
31
|
def cumulative_buckets
|
|
30
32
|
cumulative = 0
|
|
31
|
-
result = @buckets.
|
|
33
|
+
result = Array.new(@buckets.size + 1)
|
|
34
|
+
@buckets.each_index do |i|
|
|
32
35
|
cumulative += @cells[i]
|
|
33
|
-
[
|
|
36
|
+
result[i] = [@buckets[i], cumulative].freeze
|
|
34
37
|
end
|
|
35
38
|
cumulative += @cells.last # overflow cell
|
|
36
|
-
result
|
|
39
|
+
result[@buckets.size] = [Float::INFINITY, cumulative].freeze
|
|
40
|
+
result.freeze
|
|
37
41
|
end
|
|
38
42
|
end
|
|
39
43
|
|
|
@@ -56,7 +60,7 @@ module Fast
|
|
|
56
60
|
|
|
57
61
|
# Record an observation. Finds the first boundary >= value (inclusive le)
|
|
58
62
|
# using bsearch_index. Values above the last boundary go to the overflow cell.
|
|
59
|
-
def observe(value, labels:
|
|
63
|
+
def observe(value, labels: NO_LABELS)
|
|
60
64
|
key = resolve(labels)
|
|
61
65
|
index = @buckets.bsearch_index { |boundary| boundary >= value } || @buckets.size
|
|
62
66
|
|
|
@@ -69,25 +73,35 @@ module Fast
|
|
|
69
73
|
end
|
|
70
74
|
end
|
|
71
75
|
|
|
72
|
-
# Cumulative bucket counts for a specific series
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
# Cumulative bucket counts for a specific series, ending with
|
|
77
|
+
# [Float::INFINITY, count]. Zero-valued pairs for an unobserved series.
|
|
78
|
+
def cumulative_buckets(labels: NO_LABELS)
|
|
79
|
+
key = resolve(labels)
|
|
80
|
+
store.synchronize { slot_or_zero(key).cumulative_buckets }
|
|
75
81
|
end
|
|
76
82
|
|
|
77
|
-
#
|
|
78
|
-
|
|
83
|
+
# Prometheus-client-shaped Hash: each bucket boundary's to_s => cumulative
|
|
84
|
+
# count, then "+Inf" => cumulative count, then "sum" => Float sum. A
|
|
85
|
+
# fresh Hash per call; zero-valued for an unobserved series.
|
|
86
|
+
def get(labels: NO_LABELS)
|
|
79
87
|
key = resolve(labels)
|
|
80
|
-
store.synchronize {
|
|
88
|
+
store.synchronize { hash_shape(slot_or_zero(key)) }
|
|
81
89
|
end
|
|
82
90
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
store.synchronize { get(labels: labels)&.sum }
|
|
91
|
+
def values
|
|
92
|
+
series_map { |slot| hash_shape(slot) }
|
|
86
93
|
end
|
|
87
94
|
|
|
88
|
-
# Reader for
|
|
89
|
-
def
|
|
90
|
-
|
|
95
|
+
# Reader for sum of a specific series; 0.0 when unobserved.
|
|
96
|
+
def sum(labels: NO_LABELS)
|
|
97
|
+
key = resolve(labels)
|
|
98
|
+
store.synchronize { slot_or_zero(key).sum }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Reader for count of a specific series; 0 when unobserved.
|
|
102
|
+
def count(labels: NO_LABELS)
|
|
103
|
+
key = resolve(labels)
|
|
104
|
+
store.synchronize { slot_or_zero(key).count }
|
|
91
105
|
end
|
|
92
106
|
|
|
93
107
|
def self.linear_buckets(start:, width:, count:)
|
|
@@ -107,6 +121,28 @@ module Fast
|
|
|
107
121
|
def construction_options
|
|
108
122
|
{ buckets: @buckets }
|
|
109
123
|
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def zero_value
|
|
128
|
+
HistogramSlot.new(buckets: @buckets)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def slot_or_zero(key)
|
|
132
|
+
store[key] || zero_value
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def snapshot_value(slot)
|
|
136
|
+
HistogramValue.new(slot.sum, slot.count, slot.cumulative_buckets).freeze
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def hash_shape(slot)
|
|
140
|
+
pairs = slot.cumulative_buckets
|
|
141
|
+
hash = pairs[0..-2].to_h { |boundary, count| [boundary.to_s, count] }
|
|
142
|
+
hash["+Inf"] = pairs.last.last
|
|
143
|
+
hash["sum"] = slot.sum
|
|
144
|
+
hash
|
|
145
|
+
end
|
|
110
146
|
end
|
|
111
147
|
end
|
|
112
148
|
end
|
|
@@ -15,33 +15,44 @@ module Fast
|
|
|
15
15
|
METRIC_NAME = /\A[a-zA-Z_:][a-zA-Z0-9_:]*\z/
|
|
16
16
|
LABEL_NAME = /\A[a-zA-Z_][a-zA-Z0-9_]*\z/
|
|
17
17
|
|
|
18
|
+
# The default label set, shared so an unlabeled call allocates nothing.
|
|
19
|
+
NO_LABELS = {}.freeze
|
|
20
|
+
|
|
18
21
|
def initialize(name, docstring:, labels: [], preset_labels: {}, store: nil)
|
|
19
22
|
validate_metric_name(name)
|
|
20
23
|
validate_docstring(docstring)
|
|
21
24
|
validate_label_names(labels)
|
|
22
25
|
validate_preset_labels(labels, preset_labels)
|
|
23
26
|
|
|
24
|
-
@name = name
|
|
27
|
+
@name = name.to_s.to_sym
|
|
25
28
|
@docstring = docstring
|
|
26
|
-
@
|
|
29
|
+
@labels = labels
|
|
27
30
|
@preset_labels = preset_labels.transform_values { |v| v.to_s.freeze }
|
|
28
31
|
@store = store || Store.new
|
|
29
32
|
|
|
30
33
|
return unless fully_bound?
|
|
31
34
|
|
|
32
35
|
@resolved_key = resolve_internal(@preset_labels)
|
|
36
|
+
seed(@resolved_key)
|
|
33
37
|
end
|
|
34
38
|
|
|
35
|
-
attr_reader :name, :docstring, :
|
|
39
|
+
attr_reader :name, :docstring, :labels, :preset_labels
|
|
36
40
|
|
|
37
|
-
# Returns the current value for the given label set.
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
def get(labels:
|
|
41
|
+
# Returns the current value for the given label set. Scalar types
|
|
42
|
+
# (Counter, Gauge) return 0.0 for unobserved series. Histogram, Summary,
|
|
43
|
+
# and NativeHistogram override this with their own zero-valued shapes.
|
|
44
|
+
def get(labels: NO_LABELS)
|
|
41
45
|
key = resolve(labels)
|
|
42
46
|
store.synchronize { store[key] || 0.0 }
|
|
43
47
|
end
|
|
44
48
|
|
|
49
|
+
# Creates the series for +labels+ at its zero value if absent. Never
|
|
50
|
+
# resets a series that already exists. Raises InvalidLabelSet on an
|
|
51
|
+
# unknown or incomplete label set, same as any mutator.
|
|
52
|
+
def init_label_set(labels = NO_LABELS)
|
|
53
|
+
seed(resolve(labels))
|
|
54
|
+
end
|
|
55
|
+
|
|
45
56
|
def type
|
|
46
57
|
raise NotImplementedError, "subclasses must implement #type"
|
|
47
58
|
end
|
|
@@ -52,7 +63,7 @@ module Fast
|
|
|
52
63
|
self.class.new(
|
|
53
64
|
@name,
|
|
54
65
|
docstring: @docstring,
|
|
55
|
-
labels: @
|
|
66
|
+
labels: @labels,
|
|
56
67
|
preset_labels: merged,
|
|
57
68
|
store: @store,
|
|
58
69
|
**construction_options
|
|
@@ -60,15 +71,25 @@ module Fast
|
|
|
60
71
|
end
|
|
61
72
|
|
|
62
73
|
def values
|
|
63
|
-
|
|
74
|
+
series_map { |slot| snapshot_value(slot) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Every series' value in its frozen snapshot shape (see HistogramValue,
|
|
78
|
+
# SummaryValue, NativeHistogramValue; Counter/Gauge use the already-
|
|
79
|
+
# frozen Float), keyed by the series' label values in #labels order,
|
|
80
|
+
# the store's own key. The store is copied and its slots frozen under
|
|
81
|
+
# the lock, so this is a consistent view of every series at one
|
|
82
|
+
# instant that costs one Hash. The seam MetricSnapshot uses.
|
|
83
|
+
def snapshot_values
|
|
84
|
+
store.synchronize { store.to_h.transform_values! { |slot| snapshot_value(slot) } }
|
|
64
85
|
end
|
|
65
86
|
|
|
66
|
-
# Runs
|
|
87
|
+
# Runs the block exclusively with respect to every other mutation or read
|
|
67
88
|
# of this metric's store (including on metrics sharing it via
|
|
68
89
|
# #with_labels). Reentrant on the same thread. This is the seam
|
|
69
90
|
# MetricSnapshot uses to build a consistent snapshot of every series.
|
|
70
|
-
def synchronize
|
|
71
|
-
store.synchronize
|
|
91
|
+
def synchronize
|
|
92
|
+
store.synchronize { yield } # rubocop:disable Style/ExplicitBlockArgument
|
|
72
93
|
end
|
|
73
94
|
|
|
74
95
|
protected
|
|
@@ -86,7 +107,7 @@ module Fast
|
|
|
86
107
|
|
|
87
108
|
validate_label_keys(labels)
|
|
88
109
|
|
|
89
|
-
@
|
|
110
|
+
@labels.map do |n|
|
|
90
111
|
if labels.key?(n)
|
|
91
112
|
labels[n].to_s
|
|
92
113
|
else
|
|
@@ -100,8 +121,47 @@ module Fast
|
|
|
100
121
|
|
|
101
122
|
private
|
|
102
123
|
|
|
124
|
+
# Zero value for a fresh series. Counter/Gauge share this Float
|
|
125
|
+
# default; Histogram, Summary, and NativeHistogram override it with
|
|
126
|
+
# their own empty slot.
|
|
127
|
+
def zero_value
|
|
128
|
+
0.0
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Turns one slot into its frozen snapshot value. Counter/Gauge slots
|
|
132
|
+
# are already-frozen Floats; Histogram, Summary, and NativeHistogram
|
|
133
|
+
# override this to build their own snapshot value type.
|
|
134
|
+
def snapshot_value(slot)
|
|
135
|
+
slot
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Shared shape behind #values and #snapshot_values: every series'
|
|
139
|
+
# storage transformed by the block, keyed by a fresh frozen label hash,
|
|
140
|
+
# under the lock. yield, not &block: a captured block is a Proc
|
|
141
|
+
# allocation on every call.
|
|
142
|
+
def series_map
|
|
143
|
+
store.synchronize do
|
|
144
|
+
store.to_h.transform_keys { |key| label_hash(key) }.transform_values { |slot| yield slot } # rubocop:disable Style/ExplicitBlockArgument
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# {name => value} for a resolved key, built without the per-pair Arrays
|
|
149
|
+
# Array#zip would make (or the iterator object each_with_index would).
|
|
150
|
+
def label_hash(key)
|
|
151
|
+
hash = {}
|
|
152
|
+
@labels.each_index { |i| hash[@labels[i]] = key[i] }
|
|
153
|
+
hash.freeze
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Creates the series at +key+ at its zero value if absent. Never resets
|
|
157
|
+
# a series that already exists. Shared by #initialize (auto-seeding a
|
|
158
|
+
# fully-bound metric) and #init_label_set.
|
|
159
|
+
def seed(key)
|
|
160
|
+
store.synchronize { store[key] ||= zero_value }
|
|
161
|
+
end
|
|
162
|
+
|
|
103
163
|
def fully_bound?
|
|
104
|
-
@preset_labels.size == @
|
|
164
|
+
@preset_labels.size == @labels.size
|
|
105
165
|
end
|
|
106
166
|
|
|
107
167
|
def validate_metric_name(name)
|
|
@@ -132,12 +192,12 @@ module Fast
|
|
|
132
192
|
|
|
133
193
|
def validate_label_keys(labels)
|
|
134
194
|
labels.each_key do |key|
|
|
135
|
-
raise InvalidLabelSet, "unknown label name: #{key.inspect}" unless @
|
|
195
|
+
raise InvalidLabelSet, "unknown label name: #{key.inspect}" unless @labels.include?(key)
|
|
136
196
|
end
|
|
137
197
|
end
|
|
138
198
|
|
|
139
199
|
def resolve_internal(merged)
|
|
140
|
-
@
|
|
200
|
+
@labels.map { |n| merged.fetch(n) }.freeze
|
|
141
201
|
end
|
|
142
202
|
end
|
|
143
203
|
end
|
|
@@ -25,14 +25,14 @@ module Fast
|
|
|
25
25
|
@negative = {}
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# Sorted
|
|
28
|
+
# Sorted frozen [index, count] pairs for the positive side.
|
|
29
29
|
def positive_buckets
|
|
30
|
-
@positive.sort
|
|
30
|
+
@positive.sort.each(&:freeze).freeze
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
# Sorted
|
|
33
|
+
# Sorted frozen [index, count] pairs for the negative side.
|
|
34
34
|
def negative_buckets
|
|
35
|
-
@negative.sort
|
|
35
|
+
@negative.sort.each(&:freeze).freeze
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -64,7 +64,7 @@ module Fast
|
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
# Record an observation. Never raises — NaN/±Inf are handled gracefully.
|
|
67
|
-
def observe(value, labels:
|
|
67
|
+
def observe(value, labels: NO_LABELS)
|
|
68
68
|
v = value.to_f
|
|
69
69
|
key = resolve(labels)
|
|
70
70
|
|
|
@@ -107,10 +107,15 @@ module Fast
|
|
|
107
107
|
end
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
#
|
|
111
|
-
|
|
110
|
+
# A frozen NativeHistogramValue for a label set; zero-valued at this
|
|
111
|
+
# metric's schema/zero_threshold for an unobserved series.
|
|
112
|
+
def get(labels: NO_LABELS)
|
|
112
113
|
key = resolve(labels)
|
|
113
|
-
store.synchronize { store[key] }
|
|
114
|
+
store.synchronize { snapshot_value(store[key] || zero_value) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def values
|
|
118
|
+
series_map { |slot| snapshot_value(slot) }
|
|
114
119
|
end
|
|
115
120
|
|
|
116
121
|
protected
|
|
@@ -121,6 +126,15 @@ module Fast
|
|
|
121
126
|
|
|
122
127
|
private
|
|
123
128
|
|
|
129
|
+
def zero_value
|
|
130
|
+
Slot.new(schema: @schema, zero_threshold: @zero_threshold)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def snapshot_value(slot)
|
|
134
|
+
NativeHistogramValue.new(slot.schema, slot.zero_threshold, slot.zero_count, slot.sum, slot.count,
|
|
135
|
+
slot.positive_buckets, slot.negative_buckets).freeze
|
|
136
|
+
end
|
|
137
|
+
|
|
124
138
|
def index_for(value, schema)
|
|
125
139
|
factor = 2.0**schema
|
|
126
140
|
idx = (Math.log2(value) * factor).ceil
|