vm-client 1.0.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 +7 -0
- data/LICENSE +201 -0
- data/README.md +554 -0
- data/lib/prometheus/client/config.rb +15 -0
- data/lib/prometheus/client/counter.rb +21 -0
- data/lib/prometheus/client/data_stores/README.md +306 -0
- data/lib/prometheus/client/data_stores/direct_file_store.rb +368 -0
- data/lib/prometheus/client/data_stores/single_threaded.rb +56 -0
- data/lib/prometheus/client/data_stores/synchronized.rb +62 -0
- data/lib/prometheus/client/formats/text.rb +106 -0
- data/lib/prometheus/client/gauge.rb +42 -0
- data/lib/prometheus/client/histogram.rb +151 -0
- data/lib/prometheus/client/label_set_validator.rb +80 -0
- data/lib/prometheus/client/metric.rb +120 -0
- data/lib/prometheus/client/push.rb +226 -0
- data/lib/prometheus/client/registry.rb +100 -0
- data/lib/prometheus/client/summary.rb +69 -0
- data/lib/prometheus/client/version.rb +7 -0
- data/lib/prometheus/client/vm_histogram.rb +164 -0
- data/lib/prometheus/client.rb +18 -0
- data/lib/prometheus/middleware/collector.rb +103 -0
- data/lib/prometheus/middleware/exporter.rb +96 -0
- data/lib/prometheus.rb +5 -0
- metadata +108 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Prometheus
|
|
2
|
+
module Client
|
|
3
|
+
module DataStores
|
|
4
|
+
# Stores all the data in a simple Hash for each Metric
|
|
5
|
+
#
|
|
6
|
+
# Has *no* synchronization primitives, making it the fastest store for single-threaded
|
|
7
|
+
# scenarios, but must absolutely not be used in multi-threaded scenarios.
|
|
8
|
+
class SingleThreaded
|
|
9
|
+
class InvalidStoreSettingsError < StandardError; end
|
|
10
|
+
|
|
11
|
+
def for_metric(metric_name, metric_type:, metric_settings: {})
|
|
12
|
+
# We don't need `metric_type` or `metric_settings` for this particular store
|
|
13
|
+
validate_metric_settings(metric_settings: metric_settings)
|
|
14
|
+
MetricStore.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def validate_metric_settings(metric_settings:)
|
|
20
|
+
unless metric_settings.empty?
|
|
21
|
+
raise InvalidStoreSettingsError,
|
|
22
|
+
"SingleThreaded doesn't allow any metric_settings"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class MetricStore
|
|
27
|
+
def initialize
|
|
28
|
+
@internal_store = Hash.new { |hash, key| hash[key] = 0.0 }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def synchronize
|
|
32
|
+
yield
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def set(labels:, val:)
|
|
36
|
+
@internal_store[labels] = val.to_f
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def increment(labels:, by: 1)
|
|
40
|
+
@internal_store[labels] += by
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def get(labels:)
|
|
44
|
+
@internal_store[labels]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def all_values
|
|
48
|
+
@internal_store.dup
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private_constant :MetricStore
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Prometheus
|
|
2
|
+
module Client
|
|
3
|
+
module DataStores
|
|
4
|
+
# Stores all the data in simple hashes, one per metric. Each of these metrics
|
|
5
|
+
# synchronizes access to their hash, but multiple metrics can run observations
|
|
6
|
+
# concurrently.
|
|
7
|
+
class Synchronized
|
|
8
|
+
class InvalidStoreSettingsError < StandardError; end
|
|
9
|
+
|
|
10
|
+
def for_metric(metric_name, metric_type:, metric_settings: {})
|
|
11
|
+
# We don't need `metric_type` or `metric_settings` for this particular store
|
|
12
|
+
validate_metric_settings(metric_settings: metric_settings)
|
|
13
|
+
MetricStore.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def validate_metric_settings(metric_settings:)
|
|
19
|
+
unless metric_settings.empty?
|
|
20
|
+
raise InvalidStoreSettingsError,
|
|
21
|
+
"Synchronized doesn't allow any metric_settings"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class MetricStore
|
|
26
|
+
def initialize
|
|
27
|
+
@internal_store = Hash.new { |hash, key| hash[key] = 0.0 }
|
|
28
|
+
@lock = Monitor.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def synchronize
|
|
32
|
+
@lock.synchronize { yield }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def set(labels:, val:)
|
|
36
|
+
synchronize do
|
|
37
|
+
@internal_store[labels] = val.to_f
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def increment(labels:, by: 1)
|
|
42
|
+
synchronize do
|
|
43
|
+
@internal_store[labels] += by
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def get(labels:)
|
|
48
|
+
synchronize do
|
|
49
|
+
@internal_store[labels]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def all_values
|
|
54
|
+
synchronize { @internal_store.dup }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private_constant :MetricStore
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
|
|
3
|
+
module Prometheus
|
|
4
|
+
module Client
|
|
5
|
+
module Formats
|
|
6
|
+
# Text format is human readable mainly used for manual inspection.
|
|
7
|
+
module Text
|
|
8
|
+
MEDIA_TYPE = 'text/plain'.freeze
|
|
9
|
+
VERSION = '0.0.4'.freeze
|
|
10
|
+
CONTENT_TYPE = "#{MEDIA_TYPE}; version=#{VERSION}".freeze
|
|
11
|
+
|
|
12
|
+
METRIC_LINE = '%s%s %s'.freeze
|
|
13
|
+
TYPE_LINE = '# TYPE %s %s'.freeze
|
|
14
|
+
HELP_LINE = '# HELP %s %s'.freeze
|
|
15
|
+
|
|
16
|
+
LABEL = '%s="%s"'.freeze
|
|
17
|
+
SEPARATOR = ','.freeze
|
|
18
|
+
DELIMITER = "\n".freeze
|
|
19
|
+
|
|
20
|
+
REGEX = { doc: /[\n\\]/, label: /[\n\\"]/ }.freeze
|
|
21
|
+
REPLACE = { "\n" => '\n', '\\' => '\\\\', '"' => '\"' }.freeze
|
|
22
|
+
|
|
23
|
+
def self.marshal(registry)
|
|
24
|
+
lines = []
|
|
25
|
+
|
|
26
|
+
registry.metrics.each do |metric|
|
|
27
|
+
lines << format(TYPE_LINE, metric.name, metric.type)
|
|
28
|
+
lines << format(HELP_LINE, metric.name, escape(metric.docstring))
|
|
29
|
+
|
|
30
|
+
metric.values.each do |label_set, value|
|
|
31
|
+
representation(metric, label_set, value) { |l| lines << l }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# there must be a trailing delimiter
|
|
36
|
+
(lines << nil).join(DELIMITER)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def representation(metric, label_set, value, &block)
|
|
43
|
+
if metric.type == :summary
|
|
44
|
+
summary(metric.name, label_set, value, &block)
|
|
45
|
+
elsif metric.type == :histogram
|
|
46
|
+
histogram(metric.name, label_set, value, &block)
|
|
47
|
+
elsif metric.type == :vm_histogram
|
|
48
|
+
vm_histogram(metric.name, label_set, value, &block)
|
|
49
|
+
else
|
|
50
|
+
yield metric(metric.name, labels(label_set), value)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def summary(name, set, value)
|
|
55
|
+
l = labels(set)
|
|
56
|
+
yield metric("#{name}_sum", l, value["sum"])
|
|
57
|
+
yield metric("#{name}_count", l, value["count"])
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def histogram(name, set, value)
|
|
61
|
+
bucket = "#{name}_bucket"
|
|
62
|
+
value.each do |q, v|
|
|
63
|
+
next if q == "sum"
|
|
64
|
+
yield metric(bucket, labels(set.merge(le: q)), v)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
l = labels(set)
|
|
68
|
+
yield metric("#{name}_sum", l, value["sum"])
|
|
69
|
+
yield metric("#{name}_count", l, value["+Inf"])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def vm_histogram(name, set, value)
|
|
73
|
+
bucket = "#{name}_bucket"
|
|
74
|
+
value.each do |q, v|
|
|
75
|
+
next if ["count", "sum"].include? q
|
|
76
|
+
|
|
77
|
+
yield metric(bucket, labels(set.merge(vmrange: q)), v)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
l = labels(set)
|
|
81
|
+
yield metric("#{name}_sum", l, value["sum"])
|
|
82
|
+
yield metric("#{name}_count", l, value["count"])
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def metric(name, labels, value)
|
|
86
|
+
format(METRIC_LINE, name, labels, value)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def labels(set)
|
|
90
|
+
return if set.empty?
|
|
91
|
+
|
|
92
|
+
strings = set.each_with_object([]) do |(key, value), memo|
|
|
93
|
+
memo << format(LABEL, key, escape(value, :label))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
"{#{strings.join(SEPARATOR)}}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def escape(string, format = :doc)
|
|
100
|
+
string.to_s.gsub(REGEX[format], REPLACE)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
|
|
3
|
+
require 'prometheus/client/metric'
|
|
4
|
+
|
|
5
|
+
module Prometheus
|
|
6
|
+
module Client
|
|
7
|
+
# A Gauge is a metric that exposes merely an instantaneous value or some
|
|
8
|
+
# snapshot thereof.
|
|
9
|
+
class Gauge < Metric
|
|
10
|
+
def type
|
|
11
|
+
:gauge
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Sets the value for the given label set
|
|
15
|
+
def set(value, labels: {})
|
|
16
|
+
unless value.is_a?(Numeric)
|
|
17
|
+
raise ArgumentError, 'value must be a number'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
@store.set(labels: label_set_for(labels), val: value)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def set_to_current_time(labels: {})
|
|
24
|
+
@store.set(labels: label_set_for(labels), val: Time.now.to_f)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Increments Gauge value by 1 or adds the given value to the Gauge.
|
|
28
|
+
# (The value can be negative, resulting in a decrease of the Gauge.)
|
|
29
|
+
def increment(by: 1, labels: {})
|
|
30
|
+
label_set = label_set_for(labels)
|
|
31
|
+
@store.increment(labels: label_set, by: by)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Decrements Gauge value by 1 or subtracts the given value from the Gauge.
|
|
35
|
+
# (The value can be negative, resulting in a increase of the Gauge.)
|
|
36
|
+
def decrement(by: 1, labels: {})
|
|
37
|
+
label_set = label_set_for(labels)
|
|
38
|
+
@store.increment(labels: label_set, by: -by)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
|
|
3
|
+
require 'prometheus/client/metric'
|
|
4
|
+
|
|
5
|
+
module Prometheus
|
|
6
|
+
module Client
|
|
7
|
+
# A histogram samples observations (usually things like request durations
|
|
8
|
+
# or response sizes) and counts them in configurable buckets. It also
|
|
9
|
+
# provides a total count and sum of all observed values.
|
|
10
|
+
class Histogram < Metric
|
|
11
|
+
# DEFAULT_BUCKETS are the default Histogram buckets. The default buckets
|
|
12
|
+
# are tailored to broadly measure the response time (in seconds) of a
|
|
13
|
+
# network service. (From DefBuckets client_golang)
|
|
14
|
+
DEFAULT_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1,
|
|
15
|
+
2.5, 5, 10].freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :buckets
|
|
18
|
+
|
|
19
|
+
# Offer a way to manually specify buckets
|
|
20
|
+
def initialize(name,
|
|
21
|
+
docstring:,
|
|
22
|
+
labels: [],
|
|
23
|
+
preset_labels: {},
|
|
24
|
+
buckets: DEFAULT_BUCKETS,
|
|
25
|
+
store_settings: {})
|
|
26
|
+
raise ArgumentError, 'Unsorted buckets, typo?' unless sorted?(buckets)
|
|
27
|
+
|
|
28
|
+
@buckets = buckets
|
|
29
|
+
super(name,
|
|
30
|
+
docstring: docstring,
|
|
31
|
+
labels: labels,
|
|
32
|
+
preset_labels: preset_labels,
|
|
33
|
+
store_settings: store_settings)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.linear_buckets(start:, width:, count:)
|
|
37
|
+
count.times.map { |idx| start.to_f + idx * width }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.exponential_buckets(start:, factor: 2, count:)
|
|
41
|
+
count.times.map { |idx| start.to_f * factor ** idx }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def with_labels(labels)
|
|
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
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def type
|
|
60
|
+
:histogram
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Records a given value. The recorded value is usually positive
|
|
64
|
+
# or zero. A negative value is accepted but prevents current
|
|
65
|
+
# versions of Prometheus from properly detecting counter resets
|
|
66
|
+
# in the sum of observations. See
|
|
67
|
+
# https://prometheus.io/docs/practices/histograms/#count-and-sum-of-observations
|
|
68
|
+
# for details.
|
|
69
|
+
def observe(value, labels: {})
|
|
70
|
+
bucket = buckets.find {|upper_limit| upper_limit >= value }
|
|
71
|
+
bucket = "+Inf" if bucket.nil?
|
|
72
|
+
|
|
73
|
+
base_label_set = label_set_for(labels)
|
|
74
|
+
|
|
75
|
+
# This is basically faster than doing `.merge`
|
|
76
|
+
bucket_label_set = base_label_set.dup
|
|
77
|
+
bucket_label_set[:le] = bucket.to_s
|
|
78
|
+
sum_label_set = base_label_set.dup
|
|
79
|
+
sum_label_set[:le] = "sum"
|
|
80
|
+
|
|
81
|
+
@store.synchronize do
|
|
82
|
+
@store.increment(labels: bucket_label_set, by: 1)
|
|
83
|
+
@store.increment(labels: sum_label_set, by: value)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns a hash with all the buckets plus +Inf (count) plus Sum for the given label set
|
|
88
|
+
def get(labels: {})
|
|
89
|
+
base_label_set = label_set_for(labels)
|
|
90
|
+
|
|
91
|
+
all_buckets = buckets + ["+Inf", "sum"]
|
|
92
|
+
|
|
93
|
+
@store.synchronize do
|
|
94
|
+
all_buckets.each_with_object({}) do |upper_limit, acc|
|
|
95
|
+
acc[upper_limit.to_s] = @store.get(labels: base_label_set.merge(le: upper_limit.to_s))
|
|
96
|
+
end.tap do |acc|
|
|
97
|
+
accumulate_buckets(acc)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns all label sets with their values expressed as hashes with their buckets
|
|
103
|
+
def values
|
|
104
|
+
values = @store.all_values
|
|
105
|
+
|
|
106
|
+
result = values.each_with_object({}) do |(label_set, v), acc|
|
|
107
|
+
actual_label_set = label_set.reject{|l| l == :le }
|
|
108
|
+
acc[actual_label_set] ||= @buckets.map{|b| [b.to_s, 0.0]}.to_h
|
|
109
|
+
acc[actual_label_set][label_set[:le].to_s] = v
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
result.each do |(_label_set, v)|
|
|
113
|
+
accumulate_buckets(v)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def init_label_set(labels)
|
|
118
|
+
base_label_set = label_set_for(labels)
|
|
119
|
+
|
|
120
|
+
@store.synchronize do
|
|
121
|
+
(buckets + ["+Inf", "sum"]).each do |bucket|
|
|
122
|
+
@store.set(labels: base_label_set.merge(le: bucket.to_s), val: 0)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
# Modifies the passed in parameter
|
|
130
|
+
def accumulate_buckets(h)
|
|
131
|
+
bucket_acc = 0
|
|
132
|
+
buckets.each do |upper_limit|
|
|
133
|
+
bucket_value = h[upper_limit.to_s]
|
|
134
|
+
h[upper_limit.to_s] += bucket_acc
|
|
135
|
+
bucket_acc += bucket_value
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
inf_value = h["+Inf"] || 0.0
|
|
139
|
+
h["+Inf"] = inf_value + bucket_acc
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def reserved_labels
|
|
143
|
+
[:le]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def sorted?(bucket)
|
|
147
|
+
bucket.each_cons(2).all? { |i, j| i <= j }
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
|
|
3
|
+
module Prometheus
|
|
4
|
+
module Client
|
|
5
|
+
# LabelSetValidator ensures that all used label sets comply with the
|
|
6
|
+
# Prometheus specification.
|
|
7
|
+
class LabelSetValidator
|
|
8
|
+
BASE_RESERVED_LABELS = [:pid].freeze
|
|
9
|
+
LABEL_NAME_REGEX = /\A[a-zA-Z_][a-zA-Z0-9_]*\Z/
|
|
10
|
+
|
|
11
|
+
class LabelSetError < StandardError; end
|
|
12
|
+
class InvalidLabelSetError < LabelSetError; end
|
|
13
|
+
class InvalidLabelError < LabelSetError; end
|
|
14
|
+
class ReservedLabelError < LabelSetError; end
|
|
15
|
+
|
|
16
|
+
attr_reader :expected_labels, :reserved_labels
|
|
17
|
+
|
|
18
|
+
def initialize(expected_labels:, reserved_labels: [])
|
|
19
|
+
@expected_labels = expected_labels.sort
|
|
20
|
+
@reserved_labels = BASE_RESERVED_LABELS + reserved_labels
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate_symbols!(labels)
|
|
24
|
+
unless labels.respond_to?(:all?)
|
|
25
|
+
raise InvalidLabelSetError, "#{labels} is not a valid label set"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
labels.all? do |key, _|
|
|
29
|
+
validate_symbol(key)
|
|
30
|
+
validate_name(key)
|
|
31
|
+
validate_reserved_key(key)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def validate_labelset!(labelset)
|
|
36
|
+
begin
|
|
37
|
+
return labelset if keys_match?(labelset)
|
|
38
|
+
rescue ArgumentError
|
|
39
|
+
# If labelset contains keys that are a mixture of strings and symbols, this will
|
|
40
|
+
# raise when trying to sort them, but the error should be the same:
|
|
41
|
+
# InvalidLabelSetError
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
raise InvalidLabelSetError, "labels must have the same signature " \
|
|
45
|
+
"(keys given: #{labelset.keys} vs." \
|
|
46
|
+
" keys expected: #{expected_labels}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def keys_match?(labelset)
|
|
52
|
+
labelset.keys.sort == expected_labels
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def validate_symbol(key)
|
|
56
|
+
return true if key.is_a?(Symbol)
|
|
57
|
+
|
|
58
|
+
raise InvalidLabelError, "label #{key} is not a symbol"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def validate_name(key)
|
|
62
|
+
if key.to_s.start_with?('__')
|
|
63
|
+
raise ReservedLabelError, "label #{key} must not start with __"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
unless key.to_s =~ LABEL_NAME_REGEX
|
|
67
|
+
raise InvalidLabelError, "label name must match /#{LABEL_NAME_REGEX}/"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def validate_reserved_key(key)
|
|
74
|
+
return true unless reserved_labels.include?(key)
|
|
75
|
+
|
|
76
|
+
raise ReservedLabelError, "#{key} is reserved"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
|
|
3
|
+
require 'thread'
|
|
4
|
+
require 'prometheus/client/label_set_validator'
|
|
5
|
+
|
|
6
|
+
module Prometheus
|
|
7
|
+
module Client
|
|
8
|
+
# Metric
|
|
9
|
+
class Metric
|
|
10
|
+
attr_reader :name, :docstring, :labels, :preset_labels
|
|
11
|
+
|
|
12
|
+
def initialize(name,
|
|
13
|
+
docstring:,
|
|
14
|
+
labels: [],
|
|
15
|
+
preset_labels: {},
|
|
16
|
+
store_settings: {})
|
|
17
|
+
|
|
18
|
+
validate_name(name)
|
|
19
|
+
validate_docstring(docstring)
|
|
20
|
+
@validator = LabelSetValidator.new(expected_labels: labels,
|
|
21
|
+
reserved_labels: reserved_labels)
|
|
22
|
+
@validator.validate_symbols!(labels)
|
|
23
|
+
@validator.validate_symbols!(preset_labels)
|
|
24
|
+
|
|
25
|
+
@labels = labels
|
|
26
|
+
@store_settings = store_settings
|
|
27
|
+
|
|
28
|
+
@name = name
|
|
29
|
+
@docstring = docstring
|
|
30
|
+
@preset_labels = stringify_values(preset_labels)
|
|
31
|
+
|
|
32
|
+
@all_labels_preset = false
|
|
33
|
+
if preset_labels.keys.length == labels.length
|
|
34
|
+
@validator.validate_labelset!(preset_labels)
|
|
35
|
+
@all_labels_preset = true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@store = Prometheus::Client.config.data_store.for_metric(
|
|
39
|
+
name,
|
|
40
|
+
metric_type: type,
|
|
41
|
+
metric_settings: store_settings
|
|
42
|
+
)
|
|
43
|
+
|
|
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
|
|
46
|
+
init_label_set({}) if labels.empty?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
protected def replace_internal_store(new_store)
|
|
50
|
+
@store = new_store
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Returns the value for the given label set
|
|
55
|
+
def get(labels: {})
|
|
56
|
+
label_set = label_set_for(labels)
|
|
57
|
+
@store.get(labels: label_set)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def with_labels(labels)
|
|
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
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def init_label_set(labels)
|
|
75
|
+
@store.set(labels: label_set_for(labels), val: 0)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns all label sets with their values
|
|
79
|
+
def values
|
|
80
|
+
@store.all_values
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def reserved_labels
|
|
86
|
+
[]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def validate_name(name)
|
|
90
|
+
unless name.is_a?(Symbol)
|
|
91
|
+
raise ArgumentError, 'metric name must be a symbol'
|
|
92
|
+
end
|
|
93
|
+
unless name.to_s =~ /\A[a-zA-Z_:][a-zA-Z0-9_:]*\Z/
|
|
94
|
+
msg = 'metric name must match /[a-zA-Z_:][a-zA-Z0-9_:]*/'
|
|
95
|
+
raise ArgumentError, msg
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def validate_docstring(docstring)
|
|
100
|
+
return true if docstring.respond_to?(:empty?) && !docstring.empty?
|
|
101
|
+
|
|
102
|
+
raise ArgumentError, 'docstring must be given'
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def label_set_for(labels)
|
|
106
|
+
# We've already validated, and there's nothing to merge. Save some cycles
|
|
107
|
+
return preset_labels if @all_labels_preset && labels.empty?
|
|
108
|
+
labels = stringify_values(labels)
|
|
109
|
+
@validator.validate_labelset!(preset_labels.merge(labels))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def stringify_values(labels)
|
|
113
|
+
stringified = {}
|
|
114
|
+
labels.each { |k,v| stringified[k] = v.to_s }
|
|
115
|
+
|
|
116
|
+
stringified
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|