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.
@@ -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