vinted-prometheus-client-mmap 1.5.0-x86_64-linux
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/README.md +5 -0
- data/ext/fast_mmaped_file_rs/Cargo.toml +40 -0
- data/ext/fast_mmaped_file_rs/README.md +52 -0
- data/ext/fast_mmaped_file_rs/build.rs +7 -0
- data/ext/fast_mmaped_file_rs/extconf.rb +28 -0
- data/ext/fast_mmaped_file_rs/src/error.rs +174 -0
- data/ext/fast_mmaped_file_rs/src/exemplars.rs +25 -0
- data/ext/fast_mmaped_file_rs/src/file_entry.rs +1252 -0
- data/ext/fast_mmaped_file_rs/src/file_info.rs +240 -0
- data/ext/fast_mmaped_file_rs/src/lib.rs +89 -0
- data/ext/fast_mmaped_file_rs/src/macros.rs +14 -0
- data/ext/fast_mmaped_file_rs/src/map.rs +519 -0
- data/ext/fast_mmaped_file_rs/src/metrics.proto +153 -0
- data/ext/fast_mmaped_file_rs/src/mmap/inner.rs +775 -0
- data/ext/fast_mmaped_file_rs/src/mmap.rs +977 -0
- data/ext/fast_mmaped_file_rs/src/raw_entry.rs +547 -0
- data/ext/fast_mmaped_file_rs/src/testhelper.rs +222 -0
- data/ext/fast_mmaped_file_rs/src/util.rs +140 -0
- data/lib/.DS_Store +0 -0
- data/lib/2.7/fast_mmaped_file_rs.so +0 -0
- data/lib/3.0/fast_mmaped_file_rs.so +0 -0
- data/lib/3.1/fast_mmaped_file_rs.so +0 -0
- data/lib/3.2/fast_mmaped_file_rs.so +0 -0
- data/lib/3.3/fast_mmaped_file_rs.so +0 -0
- data/lib/prometheus/.DS_Store +0 -0
- data/lib/prometheus/client/configuration.rb +24 -0
- data/lib/prometheus/client/counter.rb +27 -0
- data/lib/prometheus/client/formats/protobuf.rb +93 -0
- data/lib/prometheus/client/formats/text.rb +85 -0
- data/lib/prometheus/client/gauge.rb +40 -0
- data/lib/prometheus/client/helper/entry_parser.rb +132 -0
- data/lib/prometheus/client/helper/file_locker.rb +50 -0
- data/lib/prometheus/client/helper/json_parser.rb +23 -0
- data/lib/prometheus/client/helper/metrics_processing.rb +45 -0
- data/lib/prometheus/client/helper/metrics_representation.rb +51 -0
- data/lib/prometheus/client/helper/mmaped_file.rb +64 -0
- data/lib/prometheus/client/helper/plain_file.rb +29 -0
- data/lib/prometheus/client/histogram.rb +80 -0
- data/lib/prometheus/client/label_set_validator.rb +85 -0
- data/lib/prometheus/client/metric.rb +80 -0
- data/lib/prometheus/client/mmaped_dict.rb +83 -0
- data/lib/prometheus/client/mmaped_value.rb +164 -0
- data/lib/prometheus/client/page_size.rb +17 -0
- data/lib/prometheus/client/push.rb +203 -0
- data/lib/prometheus/client/rack/collector.rb +88 -0
- data/lib/prometheus/client/rack/exporter.rb +102 -0
- data/lib/prometheus/client/registry.rb +65 -0
- data/lib/prometheus/client/simple_value.rb +31 -0
- data/lib/prometheus/client/summary.rb +69 -0
- data/lib/prometheus/client/support/puma.rb +44 -0
- data/lib/prometheus/client/support/unicorn.rb +35 -0
- data/lib/prometheus/client/uses_value_type.rb +20 -0
- data/lib/prometheus/client/version.rb +5 -0
- data/lib/prometheus/client.rb +58 -0
- data/lib/prometheus.rb +3 -0
- metadata +210 -0
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'prometheus/client/helper/json_parser'
|
2
|
+
|
3
|
+
module Prometheus
|
4
|
+
module Client
|
5
|
+
module Helper
|
6
|
+
module EntryParser
|
7
|
+
class ParsingError < RuntimeError;
|
8
|
+
end
|
9
|
+
|
10
|
+
MINIMUM_SIZE = 8
|
11
|
+
START_POSITION = 8
|
12
|
+
VALUE_BYTES = 8
|
13
|
+
ENCODED_LENGTH_BYTES = 4
|
14
|
+
|
15
|
+
def used
|
16
|
+
slice(0..3).unpack('l')[0]
|
17
|
+
end
|
18
|
+
|
19
|
+
def parts
|
20
|
+
@parts ||= File.basename(filepath, '.db')
|
21
|
+
.split('_')
|
22
|
+
.map { |e| e.gsub(/-\d+$/, '') } # remove trailing -number
|
23
|
+
end
|
24
|
+
|
25
|
+
def type
|
26
|
+
parts[0].to_sym
|
27
|
+
end
|
28
|
+
|
29
|
+
def pid
|
30
|
+
(parts[2..-1] || []).join('_')
|
31
|
+
end
|
32
|
+
|
33
|
+
def multiprocess_mode
|
34
|
+
parts[1]
|
35
|
+
end
|
36
|
+
|
37
|
+
def empty?
|
38
|
+
size < MINIMUM_SIZE || used.zero?
|
39
|
+
end
|
40
|
+
|
41
|
+
def entries(ignore_errors = false)
|
42
|
+
return Enumerator.new {} if empty?
|
43
|
+
|
44
|
+
Enumerator.new do |yielder|
|
45
|
+
used_ = used # cache used to avoid unnecessary unpack operations
|
46
|
+
|
47
|
+
pos = START_POSITION # used + padding offset
|
48
|
+
while pos < used_ && pos < size && pos > 0
|
49
|
+
data = slice(pos..-1)
|
50
|
+
unless data
|
51
|
+
raise ParsingError, "data slice is nil at pos #{pos}" unless ignore_errors
|
52
|
+
pos += 8
|
53
|
+
next
|
54
|
+
end
|
55
|
+
|
56
|
+
encoded_len, first_encoded_bytes = data.unpack('LL')
|
57
|
+
if encoded_len.nil? || encoded_len.zero? || first_encoded_bytes.nil? || first_encoded_bytes.zero?
|
58
|
+
# do not parse empty data
|
59
|
+
pos += 8
|
60
|
+
next
|
61
|
+
end
|
62
|
+
|
63
|
+
entry_len = ENCODED_LENGTH_BYTES + encoded_len
|
64
|
+
padding_len = 8 - entry_len % 8
|
65
|
+
|
66
|
+
value_offset = entry_len + padding_len # align to 8 bytes
|
67
|
+
pos += value_offset
|
68
|
+
|
69
|
+
if value_offset > 0 && (pos + VALUE_BYTES) <= size # if positions are safe
|
70
|
+
yielder.yield data, encoded_len, value_offset, pos
|
71
|
+
else
|
72
|
+
raise ParsingError, "data slice is nil at pos #{pos}" unless ignore_errors
|
73
|
+
end
|
74
|
+
pos += VALUE_BYTES
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def parsed_entries(ignore_errors = false)
|
80
|
+
result = entries(ignore_errors).map do |data, encoded_len, value_offset, _|
|
81
|
+
begin
|
82
|
+
encoded, value = data.unpack(format('@4A%d@%dd', encoded_len, value_offset))
|
83
|
+
[encoded, value]
|
84
|
+
rescue ArgumentError => e
|
85
|
+
Prometheus::Client.logger.debug("Error processing data: #{bin_to_hex(data[0, 7])} len: #{encoded_len} value_offset: #{value_offset}")
|
86
|
+
raise ParsingError, e unless ignore_errors
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
result.reject!(&:nil?) if ignore_errors
|
91
|
+
result
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_metrics(metrics = {}, ignore_errors = false)
|
95
|
+
parsed_entries(ignore_errors).each do |key, value|
|
96
|
+
begin
|
97
|
+
metric_name, name, labelnames, labelvalues = JsonParser.load(key)
|
98
|
+
labelnames ||= []
|
99
|
+
labelvalues ||= []
|
100
|
+
|
101
|
+
metric = metrics.fetch(metric_name,
|
102
|
+
metric_name: metric_name,
|
103
|
+
help: 'Multiprocess metric',
|
104
|
+
type: type,
|
105
|
+
samples: [])
|
106
|
+
if type == :gauge
|
107
|
+
metric[:multiprocess_mode] = multiprocess_mode
|
108
|
+
metric[:samples] += [[name, labelnames.zip(labelvalues) + [['pid', pid]], value]]
|
109
|
+
else
|
110
|
+
# The duplicates and labels are fixed in the next for.
|
111
|
+
metric[:samples] += [[name, labelnames.zip(labelvalues), value]]
|
112
|
+
end
|
113
|
+
metrics[metric_name] = metric
|
114
|
+
|
115
|
+
rescue JSON::ParserError => e
|
116
|
+
raise ParsingError(e) unless ignore_errors
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
metrics.reject! { |e| e.nil? } if ignore_errors
|
121
|
+
metrics
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def bin_to_hex(s)
|
127
|
+
s.each_byte.map { |b| b.to_s(16) }.join
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Prometheus
|
2
|
+
module Client
|
3
|
+
module Helper
|
4
|
+
class FileLocker
|
5
|
+
class << self
|
6
|
+
LOCK_FILE_MUTEX = Mutex.new
|
7
|
+
|
8
|
+
def lock_to_process(filepath)
|
9
|
+
LOCK_FILE_MUTEX.synchronize do
|
10
|
+
@file_locks ||= {}
|
11
|
+
return false if @file_locks[filepath]
|
12
|
+
|
13
|
+
file = File.open(filepath, 'ab')
|
14
|
+
if file.flock(File::LOCK_NB | File::LOCK_EX)
|
15
|
+
@file_locks[filepath] = file
|
16
|
+
return true
|
17
|
+
else
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def unlock(filepath)
|
24
|
+
LOCK_FILE_MUTEX.synchronize do
|
25
|
+
@file_locks ||= {}
|
26
|
+
return false unless @file_locks[filepath]
|
27
|
+
|
28
|
+
file = @file_locks[filepath]
|
29
|
+
file.flock(File::LOCK_UN)
|
30
|
+
file.close
|
31
|
+
@file_locks.delete(filepath)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def unlock_all
|
36
|
+
LOCK_FILE_MUTEX.synchronize do
|
37
|
+
@file_locks ||= {}
|
38
|
+
@file_locks.values.each do |file|
|
39
|
+
file.flock(File::LOCK_UN)
|
40
|
+
file.close
|
41
|
+
end
|
42
|
+
|
43
|
+
@file_locks = {}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Prometheus
|
4
|
+
module Client
|
5
|
+
module Helper
|
6
|
+
module JsonParser
|
7
|
+
class << self
|
8
|
+
if defined?(Oj)
|
9
|
+
def load(s)
|
10
|
+
Oj.load(s)
|
11
|
+
rescue Oj::ParseError, EncodingError => e
|
12
|
+
raise JSON::ParserError.new(e.message)
|
13
|
+
end
|
14
|
+
else
|
15
|
+
def load(s)
|
16
|
+
JSON.parse(s)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Prometheus
|
2
|
+
module Client
|
3
|
+
module Helper
|
4
|
+
module MetricsProcessing
|
5
|
+
def self.merge_metrics(metrics)
|
6
|
+
metrics.each_value do |metric|
|
7
|
+
metric[:samples] = merge_samples(metric[:samples], metric[:type], metric[:multiprocess_mode]).map do |(name, labels), value|
|
8
|
+
[name, labels.to_h, value]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.merge_samples(raw_samples, metric_type, multiprocess_mode)
|
14
|
+
samples = {}
|
15
|
+
raw_samples.each do |name, labels, value|
|
16
|
+
without_pid = labels.reject { |l| l[0] == 'pid' }
|
17
|
+
|
18
|
+
case metric_type
|
19
|
+
when :gauge
|
20
|
+
case multiprocess_mode
|
21
|
+
when 'min'
|
22
|
+
s = samples.fetch([name, without_pid], value)
|
23
|
+
samples[[name, without_pid]] = [s, value].min
|
24
|
+
when 'max'
|
25
|
+
s = samples.fetch([name, without_pid], value)
|
26
|
+
samples[[name, without_pid]] = [s, value].max
|
27
|
+
when 'livesum'
|
28
|
+
s = samples.fetch([name, without_pid], 0.0)
|
29
|
+
samples[[name, without_pid]] = s + value
|
30
|
+
else # all/liveall
|
31
|
+
samples[[name, labels]] = value
|
32
|
+
end
|
33
|
+
else
|
34
|
+
# Counter, Histogram and Summary.
|
35
|
+
s = samples.fetch([name, without_pid], 0.0)
|
36
|
+
samples[[name, without_pid]] = s + value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
samples
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Prometheus
|
2
|
+
module Client
|
3
|
+
module Helper
|
4
|
+
module MetricsRepresentation
|
5
|
+
METRIC_LINE = '%s%s %s'.freeze
|
6
|
+
TYPE_LINE = '# TYPE %s %s'.freeze
|
7
|
+
HELP_LINE = '# HELP %s %s'.freeze
|
8
|
+
|
9
|
+
LABEL = '%s="%s"'.freeze
|
10
|
+
SEPARATOR = ','.freeze
|
11
|
+
DELIMITER = "\n".freeze
|
12
|
+
|
13
|
+
REGEX = { doc: /[\n\\]/, label: /[\n\\"]/ }.freeze
|
14
|
+
REPLACE = { "\n" => '\n', '\\' => '\\\\', '"' => '\"' }.freeze
|
15
|
+
|
16
|
+
def self.to_text(metrics)
|
17
|
+
lines = []
|
18
|
+
|
19
|
+
metrics.each do |name, metric|
|
20
|
+
lines << format(HELP_LINE, name, escape(metric[:help]))
|
21
|
+
lines << format(TYPE_LINE, name, metric[:type])
|
22
|
+
metric[:samples].each do |metric_name, labels, value|
|
23
|
+
lines << metric(metric_name, format_labels(labels), value)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# there must be a trailing delimiter
|
28
|
+
(lines << nil).join(DELIMITER)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.metric(name, labels, value)
|
32
|
+
format(METRIC_LINE, name, labels, value)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.format_labels(set)
|
36
|
+
return if set.empty?
|
37
|
+
|
38
|
+
strings = set.each_with_object([]) do |(key, value), memo|
|
39
|
+
memo << format(LABEL, key, escape(value, :label))
|
40
|
+
end
|
41
|
+
|
42
|
+
"{#{strings.join(SEPARATOR)}}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.escape(string, format = :doc)
|
46
|
+
string.to_s.gsub(REGEX[format], REPLACE)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'prometheus/client/helper/entry_parser'
|
2
|
+
require 'prometheus/client/helper/file_locker'
|
3
|
+
|
4
|
+
# load precompiled extension if available
|
5
|
+
begin
|
6
|
+
ruby_version = /(\d+\.\d+)/.match(RUBY_VERSION)
|
7
|
+
require_relative "../../../#{ruby_version}/fast_mmaped_file_rs"
|
8
|
+
rescue LoadError
|
9
|
+
require 'fast_mmaped_file_rs'
|
10
|
+
end
|
11
|
+
|
12
|
+
module Prometheus
|
13
|
+
module Client
|
14
|
+
module Helper
|
15
|
+
class MmapedFile < FastMmapedFileRs
|
16
|
+
include EntryParser
|
17
|
+
|
18
|
+
attr_reader :filepath, :size
|
19
|
+
|
20
|
+
def initialize(filepath)
|
21
|
+
@filepath = filepath
|
22
|
+
|
23
|
+
File.open(filepath, 'a+b') do |file|
|
24
|
+
file.truncate(initial_mmap_file_size) if file.size < MINIMUM_SIZE
|
25
|
+
@size = file.size
|
26
|
+
end
|
27
|
+
|
28
|
+
super(filepath)
|
29
|
+
end
|
30
|
+
|
31
|
+
def close
|
32
|
+
munmap
|
33
|
+
FileLocker.unlock(filepath)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def initial_mmap_file_size
|
39
|
+
Prometheus::Client.configuration.initial_mmap_file_size
|
40
|
+
end
|
41
|
+
|
42
|
+
public
|
43
|
+
|
44
|
+
class << self
|
45
|
+
def open(filepath)
|
46
|
+
MmapedFile.new(filepath)
|
47
|
+
end
|
48
|
+
|
49
|
+
def ensure_exclusive_file(file_prefix = 'mmaped_file')
|
50
|
+
(0..Float::INFINITY).lazy
|
51
|
+
.map { |f_num| "#{file_prefix}_#{Prometheus::Client.pid}-#{f_num}.db" }
|
52
|
+
.map { |filename| File.join(Prometheus::Client.configuration.multiprocess_files_dir, filename) }
|
53
|
+
.find { |path| Helper::FileLocker.lock_to_process(path) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def open_exclusive_file(file_prefix = 'mmaped_file')
|
57
|
+
filename = Helper::MmapedFile.ensure_exclusive_file(file_prefix)
|
58
|
+
open(filename)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'prometheus/client/helper/entry_parser'
|
2
|
+
|
3
|
+
module Prometheus
|
4
|
+
module Client
|
5
|
+
module Helper
|
6
|
+
# Parses DB files without using mmap
|
7
|
+
class PlainFile
|
8
|
+
include EntryParser
|
9
|
+
attr_reader :filepath
|
10
|
+
|
11
|
+
def source
|
12
|
+
@data ||= File.read(filepath, mode: 'rb')
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(filepath)
|
16
|
+
@filepath = filepath
|
17
|
+
end
|
18
|
+
|
19
|
+
def slice(*args)
|
20
|
+
source.slice(*args)
|
21
|
+
end
|
22
|
+
|
23
|
+
def size
|
24
|
+
source.length
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'prometheus/client/metric'
|
2
|
+
require 'prometheus/client/uses_value_type'
|
3
|
+
|
4
|
+
module Prometheus
|
5
|
+
module Client
|
6
|
+
# A histogram samples observations (usually things like request durations
|
7
|
+
# or response sizes) and counts them in configurable buckets. It also
|
8
|
+
# provides a sum of all observed values.
|
9
|
+
class Histogram < Metric
|
10
|
+
# Value represents the state of a Histogram at a given point.
|
11
|
+
class Value < Hash
|
12
|
+
include UsesValueType
|
13
|
+
attr_accessor :sum, :total, :total_inf
|
14
|
+
|
15
|
+
def initialize(type, name, labels, buckets)
|
16
|
+
@sum = value_object(type, name, "#{name}_sum", labels)
|
17
|
+
@total = value_object(type, name, "#{name}_count", labels)
|
18
|
+
@total_inf = value_object(type, name, "#{name}_bucket", labels.merge(le: "+Inf"))
|
19
|
+
|
20
|
+
buckets.each do |bucket|
|
21
|
+
self[bucket] = value_object(type, name, "#{name}_bucket", labels.merge(le: bucket.to_s))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def observe(value, exemplar_name = '', exemplar_value = '')
|
26
|
+
@sum.increment(value, exemplar_name, exemplar_value)
|
27
|
+
@total.increment(1, exemplar_name, exemplar_value)
|
28
|
+
@total_inf.increment(1, exemplar_name, exemplar_value)
|
29
|
+
|
30
|
+
each_key do |bucket|
|
31
|
+
self[bucket].increment(1, exemplar_name, exemplar_value) if value <= bucket
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def get()
|
36
|
+
hash = {}
|
37
|
+
each_key do |bucket|
|
38
|
+
hash[bucket] = self[bucket].get()
|
39
|
+
end
|
40
|
+
hash
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# DEFAULT_BUCKETS are the default Histogram buckets. The default buckets
|
45
|
+
# are tailored to broadly measure the response time (in seconds) of a
|
46
|
+
# network service. (From DefBuckets client_golang)
|
47
|
+
DEFAULT_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1,
|
48
|
+
2.5, 5, 10].freeze
|
49
|
+
|
50
|
+
# Offer a way to manually specify buckets
|
51
|
+
def initialize(name, docstring, base_labels = {},
|
52
|
+
buckets = DEFAULT_BUCKETS)
|
53
|
+
raise ArgumentError, 'Unsorted buckets, typo?' unless sorted? buckets
|
54
|
+
|
55
|
+
@buckets = buckets
|
56
|
+
super(name, docstring, base_labels)
|
57
|
+
end
|
58
|
+
|
59
|
+
def type
|
60
|
+
:histogram
|
61
|
+
end
|
62
|
+
|
63
|
+
def observe(labels, value, exemplar_name = '', exemplar_value = '')
|
64
|
+
label_set = label_set_for(labels)
|
65
|
+
synchronize { @values[label_set].observe(value, exemplar_name, exemplar_value) }
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def default(labels)
|
71
|
+
# TODO: default function needs to know key of hash info (label names and values)
|
72
|
+
Value.new(type, @name, labels, @buckets)
|
73
|
+
end
|
74
|
+
|
75
|
+
def sorted?(bucket)
|
76
|
+
bucket.each_cons(2).all? { |i, j| i <= j }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,85 @@
|
|
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
|
+
RESERVED_LABELS = [].freeze
|
9
|
+
|
10
|
+
class LabelSetError < StandardError; end
|
11
|
+
class InvalidLabelSetError < LabelSetError; end
|
12
|
+
class InvalidLabelError < LabelSetError; end
|
13
|
+
class ReservedLabelError < LabelSetError; end
|
14
|
+
|
15
|
+
def initialize(reserved_labels = [])
|
16
|
+
@reserved_labels = (reserved_labels + RESERVED_LABELS).freeze
|
17
|
+
@validated = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid?(labels)
|
21
|
+
unless labels.is_a?(Hash)
|
22
|
+
raise InvalidLabelSetError, "#{labels} is not a valid label set"
|
23
|
+
end
|
24
|
+
|
25
|
+
labels.all? do |key, value|
|
26
|
+
validate_symbol(key)
|
27
|
+
validate_name(key)
|
28
|
+
validate_reserved_key(key)
|
29
|
+
validate_value(key, value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate(labels)
|
34
|
+
return labels if @validated.key?(labels.hash)
|
35
|
+
|
36
|
+
valid?(labels)
|
37
|
+
|
38
|
+
unless @validated.empty? || match?(labels, @validated.first.last)
|
39
|
+
raise InvalidLabelSetError, "labels must have the same signature: (#{label_diff(labels, @validated.first.last)})"
|
40
|
+
end
|
41
|
+
|
42
|
+
@validated[labels.hash] = labels
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def label_diff(a, b)
|
48
|
+
"expected keys: #{b.keys.sort}, got: #{a.keys.sort}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def match?(a, b)
|
52
|
+
a.keys.sort == b.keys.sort
|
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
|
+
return true unless key.to_s.start_with?('__')
|
63
|
+
|
64
|
+
raise ReservedLabelError, "label #{key} must not start with __"
|
65
|
+
end
|
66
|
+
|
67
|
+
def validate_reserved_key(key)
|
68
|
+
return true unless @reserved_labels.include?(key)
|
69
|
+
|
70
|
+
raise ReservedLabelError, "#{key} is reserved"
|
71
|
+
end
|
72
|
+
|
73
|
+
def validate_value(key, value)
|
74
|
+
return true if value.is_a?(String) ||
|
75
|
+
value.is_a?(Numeric) ||
|
76
|
+
value.is_a?(Symbol) ||
|
77
|
+
value.is_a?(FalseClass) ||
|
78
|
+
value.is_a?(TrueClass) ||
|
79
|
+
value.nil?
|
80
|
+
|
81
|
+
raise InvalidLabelError, "#{key} does not contain a valid value (type #{value.class})"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'prometheus/client/label_set_validator'
|
3
|
+
require 'prometheus/client/uses_value_type'
|
4
|
+
|
5
|
+
module Prometheus
|
6
|
+
module Client
|
7
|
+
class Metric
|
8
|
+
include UsesValueType
|
9
|
+
attr_reader :name, :docstring, :base_labels
|
10
|
+
|
11
|
+
def initialize(name, docstring, base_labels = {})
|
12
|
+
@mutex = Mutex.new
|
13
|
+
@validator = case type
|
14
|
+
when :summary
|
15
|
+
LabelSetValidator.new(['quantile'])
|
16
|
+
when :histogram
|
17
|
+
LabelSetValidator.new(['le'])
|
18
|
+
else
|
19
|
+
LabelSetValidator.new
|
20
|
+
end
|
21
|
+
@values = Hash.new { |hash, key| hash[key] = default(key) }
|
22
|
+
|
23
|
+
validate_name(name)
|
24
|
+
validate_docstring(docstring)
|
25
|
+
@validator.valid?(base_labels)
|
26
|
+
|
27
|
+
@name = name
|
28
|
+
@docstring = docstring
|
29
|
+
@base_labels = base_labels
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the value for the given label set
|
33
|
+
def get(labels = {})
|
34
|
+
label_set = label_set_for(labels)
|
35
|
+
@validator.valid?(label_set)
|
36
|
+
|
37
|
+
@values[label_set].get
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns all label sets with their values
|
41
|
+
def values
|
42
|
+
synchronize do
|
43
|
+
@values.each_with_object({}) do |(labels, value), memo|
|
44
|
+
memo[labels] = value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def touch_default_value
|
52
|
+
@values[label_set_for({})]
|
53
|
+
end
|
54
|
+
|
55
|
+
def default(labels)
|
56
|
+
value_object(type, @name, @name, labels)
|
57
|
+
end
|
58
|
+
|
59
|
+
def validate_name(name)
|
60
|
+
return true if name.is_a?(Symbol)
|
61
|
+
|
62
|
+
raise ArgumentError, 'given name must be a symbol'
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate_docstring(docstring)
|
66
|
+
return true if docstring.respond_to?(:empty?) && !docstring.empty?
|
67
|
+
|
68
|
+
raise ArgumentError, 'docstring must be given'
|
69
|
+
end
|
70
|
+
|
71
|
+
def label_set_for(labels)
|
72
|
+
@validator.validate(@base_labels.merge(labels))
|
73
|
+
end
|
74
|
+
|
75
|
+
def synchronize(&block)
|
76
|
+
@mutex.synchronize(&block)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|