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,83 @@
|
|
1
|
+
require 'prometheus/client/helper/mmaped_file'
|
2
|
+
require 'prometheus/client/helper/plain_file'
|
3
|
+
require 'prometheus/client'
|
4
|
+
|
5
|
+
module Prometheus
|
6
|
+
module Client
|
7
|
+
class ParsingError < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
# A dict of doubles, backed by an mmapped file.
|
11
|
+
#
|
12
|
+
# The file starts with a 4 byte int, indicating how much of it is used.
|
13
|
+
# Then 4 bytes of padding.
|
14
|
+
# There's then a number of entries, consisting of a 4 byte int which is the
|
15
|
+
# size of the next field, a utf-8 encoded string key, padding to an 8 byte
|
16
|
+
# alignment, and then a 8 byte float which is the value.
|
17
|
+
class MmapedDict
|
18
|
+
attr_reader :m, :used, :positions
|
19
|
+
|
20
|
+
def self.read_all_values(f)
|
21
|
+
Helper::PlainFile.new(f).entries.map do |data, encoded_len, value_offset, _|
|
22
|
+
encoded, value = data.unpack(format('@4A%d@%dd', encoded_len, value_offset))
|
23
|
+
[encoded, value]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(m)
|
28
|
+
@mutex = Mutex.new
|
29
|
+
|
30
|
+
@m = m
|
31
|
+
# @m.mlock # TODO: Ensure memory is locked to RAM
|
32
|
+
|
33
|
+
@positions = {}
|
34
|
+
read_all_positions.each do |key, pos|
|
35
|
+
@positions[key] = pos
|
36
|
+
end
|
37
|
+
rescue StandardError => e
|
38
|
+
raise ParsingError, "exception #{e} while processing metrics file #{path}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def read_value(key)
|
42
|
+
@m.fetch_entry(@positions, key, 0.0)
|
43
|
+
end
|
44
|
+
|
45
|
+
def write_value(key, value)
|
46
|
+
@m.upsert_entry(@positions, key, value)
|
47
|
+
end
|
48
|
+
|
49
|
+
def write_exemplar(key, value, exemplar_id, exemplar_val)
|
50
|
+
@m.upsert_exemplar(@positions, key, value, exemplar_id, exemplar_val)
|
51
|
+
end
|
52
|
+
|
53
|
+
def path
|
54
|
+
@m.filepath if @m
|
55
|
+
end
|
56
|
+
|
57
|
+
def close
|
58
|
+
@m.sync
|
59
|
+
@m.close
|
60
|
+
rescue TypeError => e
|
61
|
+
Prometheus::Client.logger.warn("munmap raised error #{e}")
|
62
|
+
end
|
63
|
+
|
64
|
+
def inspect
|
65
|
+
"#<#{self.class}:0x#{(object_id << 1).to_s(16)}>"
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def init_value(key)
|
71
|
+
@m.add_entry(@positions, key, 0.0)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Yield (key, pos). No locking is performed.
|
75
|
+
def read_all_positions
|
76
|
+
@m.entries.map do |data, encoded_len, _, absolute_pos|
|
77
|
+
encoded, = data.unpack(format('@4A%d', encoded_len))
|
78
|
+
[encoded, absolute_pos]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'prometheus/client'
|
2
|
+
require 'prometheus/client/mmaped_dict'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Prometheus
|
6
|
+
module Client
|
7
|
+
# A float protected by a mutex backed by a per-process mmaped file.
|
8
|
+
class MmapedValue
|
9
|
+
VALUE_LOCK = Mutex.new
|
10
|
+
|
11
|
+
@@files = {}
|
12
|
+
@@pid = -1
|
13
|
+
|
14
|
+
def initialize(type, metric_name, name, labels, multiprocess_mode = '')
|
15
|
+
@file_prefix = type.to_s
|
16
|
+
@metric_name = metric_name
|
17
|
+
@name = name
|
18
|
+
@labels = labels
|
19
|
+
if type == :gauge
|
20
|
+
@file_prefix += '_' + multiprocess_mode.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
@pid = -1
|
24
|
+
|
25
|
+
@mutex = Mutex.new
|
26
|
+
initialize_file
|
27
|
+
end
|
28
|
+
|
29
|
+
def increment(amount = 1, exemplar_name = '', exemplar_value = '')
|
30
|
+
@mutex.synchronize do
|
31
|
+
initialize_file if pid_changed?
|
32
|
+
|
33
|
+
@value += amount
|
34
|
+
write_value(@key, @value, exemplar_name, exemplar_value)
|
35
|
+
@value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def decrement(amount = 1)
|
40
|
+
increment(-amount)
|
41
|
+
end
|
42
|
+
|
43
|
+
def set(value)
|
44
|
+
@mutex.synchronize do
|
45
|
+
initialize_file if pid_changed?
|
46
|
+
|
47
|
+
@value = value
|
48
|
+
write_value(@key, @value)
|
49
|
+
@value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def get
|
54
|
+
@mutex.synchronize do
|
55
|
+
initialize_file if pid_changed?
|
56
|
+
return @value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def pid_changed?
|
61
|
+
@pid != Process.pid
|
62
|
+
end
|
63
|
+
|
64
|
+
# method needs to be run in VALUE_LOCK mutex
|
65
|
+
def unsafe_reinitialize_file(check_pid = true)
|
66
|
+
unsafe_initialize_file if !check_pid || pid_changed?
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.reset_and_reinitialize
|
70
|
+
VALUE_LOCK.synchronize do
|
71
|
+
@@pid = Process.pid
|
72
|
+
@@files = {}
|
73
|
+
|
74
|
+
ObjectSpace.each_object(MmapedValue).each do |v|
|
75
|
+
v.unsafe_reinitialize_file(false)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.reset_on_pid_change
|
81
|
+
if pid_changed?
|
82
|
+
@@pid = Process.pid
|
83
|
+
@@files = {}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.reinitialize_on_pid_change
|
88
|
+
VALUE_LOCK.synchronize do
|
89
|
+
reset_on_pid_change
|
90
|
+
|
91
|
+
ObjectSpace.each_object(MmapedValue, &:unsafe_reinitialize_file)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.pid_changed?
|
96
|
+
@@pid != Process.pid
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.multiprocess
|
100
|
+
true
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def initialize_file
|
106
|
+
VALUE_LOCK.synchronize do
|
107
|
+
unsafe_initialize_file
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def unsafe_initialize_file
|
112
|
+
self.class.reset_on_pid_change
|
113
|
+
|
114
|
+
@pid = Process.pid
|
115
|
+
unless @@files.has_key?(@file_prefix)
|
116
|
+
unless @file.nil?
|
117
|
+
@file.close
|
118
|
+
end
|
119
|
+
unless @exemplar_file.nil?
|
120
|
+
@exemplar_file.close
|
121
|
+
end
|
122
|
+
mmaped_file = Helper::MmapedFile.open_exclusive_file(@file_prefix)
|
123
|
+
exemplar_file = Helper::MmapedFile.open_exclusive_file('exemplar')
|
124
|
+
|
125
|
+
@@files[@file_prefix] = MmapedDict.new(mmaped_file)
|
126
|
+
@@files['exemplar'] = MmapedDict.new(exemplar_file)
|
127
|
+
end
|
128
|
+
|
129
|
+
@file = @@files[@file_prefix]
|
130
|
+
@exemplar_file = @@files['exemplar']
|
131
|
+
@key = rebuild_key
|
132
|
+
|
133
|
+
@value = read_value(@key)
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
def rebuild_key
|
138
|
+
keys = @labels.keys.sort
|
139
|
+
values = @labels.values_at(*keys)
|
140
|
+
|
141
|
+
[@metric_name, @name, keys, values].to_json
|
142
|
+
end
|
143
|
+
|
144
|
+
def write_value(key, val, exemplar_name = '', exemplar_value = '')
|
145
|
+
@file.write_value(key, val)
|
146
|
+
# Exemplars are only defined on counters or histograms.
|
147
|
+
if @file_prefix == 'counter' or @file_prefix == 'histogram' and exemplar_name != '' and exemplar_value != ''
|
148
|
+
@exemplar_file.write_exemplar(key, val, exemplar_name, exemplar_value)
|
149
|
+
end
|
150
|
+
rescue StandardError => e
|
151
|
+
Prometheus::Client.logger.warn("writing value to #{@file.path} failed with #{e}")
|
152
|
+
Prometheus::Client.logger.debug(e.backtrace.join("\n"))
|
153
|
+
end
|
154
|
+
|
155
|
+
def read_value(key)
|
156
|
+
@file.read_value(key)
|
157
|
+
rescue StandardError => e
|
158
|
+
Prometheus::Client.logger.warn("reading value from #{@file.path} failed with #{e}")
|
159
|
+
Prometheus::Client.logger.debug(e.backtrace.join("\n"))
|
160
|
+
0
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Prometheus
|
4
|
+
module Client
|
5
|
+
module PageSize
|
6
|
+
def self.page_size(fallback_page_size: 4096)
|
7
|
+
stdout, status = Open3.capture2('getconf PAGESIZE')
|
8
|
+
return fallback_page_size if status.nil? || !status.success?
|
9
|
+
|
10
|
+
page_size = stdout.chomp.to_i
|
11
|
+
return fallback_page_size if page_size <= 0
|
12
|
+
|
13
|
+
page_size
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'thread'
|
5
|
+
require 'net/http'
|
6
|
+
require 'uri'
|
7
|
+
require 'erb'
|
8
|
+
require 'set'
|
9
|
+
|
10
|
+
require 'prometheus/client'
|
11
|
+
require 'prometheus/client/formats/text'
|
12
|
+
require 'prometheus/client/label_set_validator'
|
13
|
+
|
14
|
+
module Prometheus
|
15
|
+
# Client is a ruby implementation for a Prometheus compatible client.
|
16
|
+
module Client
|
17
|
+
# Push implements a simple way to transmit a given registry to a given
|
18
|
+
# Pushgateway.
|
19
|
+
class Push
|
20
|
+
class HttpError < StandardError; end
|
21
|
+
class HttpRedirectError < HttpError; end
|
22
|
+
class HttpClientError < HttpError; end
|
23
|
+
class HttpServerError < HttpError; end
|
24
|
+
|
25
|
+
DEFAULT_GATEWAY = 'http://localhost:9091'.freeze
|
26
|
+
PATH = '/metrics/job/%s'.freeze
|
27
|
+
SUPPORTED_SCHEMES = %w(http https).freeze
|
28
|
+
|
29
|
+
attr_reader :job, :gateway, :path
|
30
|
+
|
31
|
+
def initialize(job:, gateway: DEFAULT_GATEWAY, grouping_key: {}, **kwargs)
|
32
|
+
raise ArgumentError, "job cannot be nil" if job.nil?
|
33
|
+
raise ArgumentError, "job cannot be empty" if job.empty?
|
34
|
+
@validator = LabelSetValidator.new()
|
35
|
+
@validator.validate(grouping_key)
|
36
|
+
|
37
|
+
@mutex = Mutex.new
|
38
|
+
@job = job
|
39
|
+
@gateway = gateway || DEFAULT_GATEWAY
|
40
|
+
@grouping_key = grouping_key
|
41
|
+
@path = build_path(job, grouping_key)
|
42
|
+
|
43
|
+
@uri = parse("#{@gateway}#{@path}")
|
44
|
+
validate_no_basic_auth!(@uri)
|
45
|
+
|
46
|
+
@http = Net::HTTP.new(@uri.host, @uri.port)
|
47
|
+
@http.use_ssl = (@uri.scheme == 'https')
|
48
|
+
@http.open_timeout = kwargs[:open_timeout] if kwargs[:open_timeout]
|
49
|
+
@http.read_timeout = kwargs[:read_timeout] if kwargs[:read_timeout]
|
50
|
+
end
|
51
|
+
|
52
|
+
def basic_auth(user, password)
|
53
|
+
@user = user
|
54
|
+
@password = password
|
55
|
+
end
|
56
|
+
|
57
|
+
def add(registry)
|
58
|
+
synchronize do
|
59
|
+
request(Net::HTTP::Post, registry)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def replace(registry)
|
64
|
+
synchronize do
|
65
|
+
request(Net::HTTP::Put, registry)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def delete
|
70
|
+
synchronize do
|
71
|
+
request(Net::HTTP::Delete)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def parse(url)
|
78
|
+
uri = URI.parse(url)
|
79
|
+
|
80
|
+
unless SUPPORTED_SCHEMES.include?(uri.scheme)
|
81
|
+
raise ArgumentError, 'only HTTP gateway URLs are supported currently.'
|
82
|
+
end
|
83
|
+
|
84
|
+
uri
|
85
|
+
rescue URI::InvalidURIError => e
|
86
|
+
raise ArgumentError, "#{url} is not a valid URL: #{e}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def build_path(job, grouping_key)
|
90
|
+
path = format(PATH, ERB::Util::url_encode(job))
|
91
|
+
|
92
|
+
grouping_key.each do |label, value|
|
93
|
+
if value.include?('/')
|
94
|
+
encoded_value = Base64.urlsafe_encode64(value)
|
95
|
+
path += "/#{label}@base64/#{encoded_value}"
|
96
|
+
# While it's valid for the urlsafe_encode64 function to return an
|
97
|
+
# empty string when the input string is empty, it doesn't work for
|
98
|
+
# our specific use case as we're putting the result into a URL path
|
99
|
+
# segment. A double slash (`//`) can be normalised away by HTTP
|
100
|
+
# libraries, proxies, and web servers.
|
101
|
+
#
|
102
|
+
# For empty strings, we use a single padding character (`=`) as the
|
103
|
+
# value.
|
104
|
+
#
|
105
|
+
# See the pushgateway docs for more details:
|
106
|
+
#
|
107
|
+
# https://github.com/prometheus/pushgateway/blob/6393a901f56d4dda62cd0f6ab1f1f07c495b6354/README.md#url
|
108
|
+
elsif value.empty?
|
109
|
+
path += "/#{label}@base64/="
|
110
|
+
else
|
111
|
+
path += "/#{label}/#{ERB::Util::url_encode(value)}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
path
|
116
|
+
end
|
117
|
+
|
118
|
+
def request(req_class, registry = nil)
|
119
|
+
validate_no_label_clashes!(registry) if registry
|
120
|
+
|
121
|
+
req = req_class.new(@uri)
|
122
|
+
req.content_type = Formats::Text::CONTENT_TYPE
|
123
|
+
req.basic_auth(@user, @password) if @user
|
124
|
+
req.body = Formats::Text.marshal(registry) if registry
|
125
|
+
|
126
|
+
response = @http.request(req)
|
127
|
+
validate_response!(response)
|
128
|
+
|
129
|
+
response
|
130
|
+
end
|
131
|
+
|
132
|
+
def synchronize
|
133
|
+
@mutex.synchronize { yield }
|
134
|
+
end
|
135
|
+
|
136
|
+
def validate_no_basic_auth!(uri)
|
137
|
+
if uri.user || uri.password
|
138
|
+
raise ArgumentError, <<~EOF
|
139
|
+
Setting Basic Auth credentials in the gateway URL is not supported, please call the `basic_auth` method.
|
140
|
+
|
141
|
+
Received username `#{uri.user}` in gateway URL. Instead of passing
|
142
|
+
Basic Auth credentials like this:
|
143
|
+
|
144
|
+
```
|
145
|
+
push = Prometheus::Client::Push.new(job: "my-job", gateway: "http://user:password@localhost:9091")
|
146
|
+
```
|
147
|
+
|
148
|
+
please pass them like this:
|
149
|
+
|
150
|
+
```
|
151
|
+
push = Prometheus::Client::Push.new(job: "my-job", gateway: "http://localhost:9091")
|
152
|
+
push.basic_auth("user", "password")
|
153
|
+
```
|
154
|
+
|
155
|
+
While URLs do support passing Basic Auth credentials using the
|
156
|
+
`http://user:password@example.com/` syntax, the username and
|
157
|
+
password in that syntax have to follow the usual rules for URL
|
158
|
+
encoding of characters per RFC 3986
|
159
|
+
(https://datatracker.ietf.org/doc/html/rfc3986#section-2.1).
|
160
|
+
|
161
|
+
Rather than place the burden of correctly performing that encoding
|
162
|
+
on users of this gem, we decided to have a separate method for
|
163
|
+
supplying Basic Auth credentials, with no requirement to URL encode
|
164
|
+
the characters in them.
|
165
|
+
EOF
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def validate_no_label_clashes!(registry)
|
170
|
+
# There's nothing to check if we don't have a grouping key
|
171
|
+
return if @grouping_key.empty?
|
172
|
+
|
173
|
+
# We could be doing a lot of comparisons, so let's do them against a
|
174
|
+
# set rather than an array
|
175
|
+
grouping_key_labels = @grouping_key.keys.to_set
|
176
|
+
|
177
|
+
registry.metrics.each do |metric|
|
178
|
+
metric.values.keys.first.keys.each do |label|
|
179
|
+
if grouping_key_labels.include?(label)
|
180
|
+
raise LabelSetValidator::InvalidLabelSetError,
|
181
|
+
"label :#{label} from grouping key collides with label of the " \
|
182
|
+
"same name from metric :#{metric.name} and would overwrite it"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def validate_response!(response)
|
189
|
+
status = Integer(response.code)
|
190
|
+
if status >= 300
|
191
|
+
message = "status: #{response.code}, message: #{response.message}, body: #{response.body}"
|
192
|
+
if status <= 399
|
193
|
+
raise HttpRedirectError, message
|
194
|
+
elsif status <= 499
|
195
|
+
raise HttpClientError, message
|
196
|
+
else
|
197
|
+
raise HttpServerError, message
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'prometheus/client'
|
4
|
+
|
5
|
+
module Prometheus
|
6
|
+
module Client
|
7
|
+
module Rack
|
8
|
+
# Collector is a Rack middleware that provides a sample implementation of
|
9
|
+
# a HTTP tracer. The default label builder can be modified to export a
|
10
|
+
# different set of labels per recorded metric.
|
11
|
+
class Collector
|
12
|
+
attr_reader :app, :registry
|
13
|
+
|
14
|
+
def initialize(app, options = {}, &label_builder)
|
15
|
+
@app = app
|
16
|
+
@registry = options[:registry] || Client.registry
|
17
|
+
@label_builder = label_builder || DEFAULT_LABEL_BUILDER
|
18
|
+
|
19
|
+
init_request_metrics
|
20
|
+
init_exception_metrics
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(env) # :nodoc:
|
24
|
+
trace(env) { @app.call(env) }
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
DEFAULT_LABEL_BUILDER = proc do |env|
|
30
|
+
{
|
31
|
+
method: env['REQUEST_METHOD'].downcase,
|
32
|
+
host: env['HTTP_HOST'].to_s,
|
33
|
+
path: env['PATH_INFO'].to_s,
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def init_request_metrics
|
38
|
+
@requests = @registry.counter(
|
39
|
+
:http_requests_total,
|
40
|
+
'A counter of the total number of HTTP requests made.',
|
41
|
+
)
|
42
|
+
@durations = @registry.summary(
|
43
|
+
:http_request_duration_seconds,
|
44
|
+
'A summary of the response latency.',
|
45
|
+
)
|
46
|
+
@durations_hist = @registry.histogram(
|
47
|
+
:http_req_duration_seconds,
|
48
|
+
'A histogram of the response latency.',
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def init_exception_metrics
|
53
|
+
@exceptions = @registry.counter(
|
54
|
+
:http_exceptions_total,
|
55
|
+
'A counter of the total number of exceptions raised.',
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def trace(env)
|
60
|
+
start = Time.now
|
61
|
+
yield.tap do |response|
|
62
|
+
duration = (Time.now - start).to_f
|
63
|
+
record(labels(env, response), duration)
|
64
|
+
end
|
65
|
+
rescue => exception
|
66
|
+
@exceptions.increment(exception: exception.class.name)
|
67
|
+
raise
|
68
|
+
end
|
69
|
+
|
70
|
+
def labels(env, response)
|
71
|
+
@label_builder.call(env).tap do |labels|
|
72
|
+
labels[:code] = response.first.to_s
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def record(labels, duration)
|
77
|
+
@requests.increment(labels)
|
78
|
+
@durations.observe(labels, duration)
|
79
|
+
@durations_hist.observe(labels, duration)
|
80
|
+
rescue => exception
|
81
|
+
@exceptions.increment(exception: exception.class.name)
|
82
|
+
raise
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'prometheus/client'
|
4
|
+
require 'prometheus/client/formats/text'
|
5
|
+
require 'prometheus/client/formats/protobuf'
|
6
|
+
|
7
|
+
module Prometheus
|
8
|
+
module Client
|
9
|
+
module Rack
|
10
|
+
# Exporter is a Rack middleware that provides a sample implementation of
|
11
|
+
# a Prometheus HTTP client API.
|
12
|
+
class Exporter
|
13
|
+
attr_reader :app, :registry, :path
|
14
|
+
|
15
|
+
FALLBACK = Formats::Text
|
16
|
+
|
17
|
+
def initialize(app, options = {})
|
18
|
+
@app = app
|
19
|
+
@registry = options[:registry] || Client.registry
|
20
|
+
@path = options[:path] || '/metrics'
|
21
|
+
|
22
|
+
if Prometheus::Client.configuration.enable_protobuf
|
23
|
+
@formats = [Formats::Text, Formats::Protobuf]
|
24
|
+
else
|
25
|
+
@formats = [Formats::Text]
|
26
|
+
end
|
27
|
+
@acceptable = build_dictionary(@formats, FALLBACK)
|
28
|
+
end
|
29
|
+
|
30
|
+
def call(env)
|
31
|
+
if env['PATH_INFO'] == @path
|
32
|
+
format = negotiate(env['HTTP_ACCEPT'], @acceptable)
|
33
|
+
format ? respond_with(format) : not_acceptable(@formats)
|
34
|
+
else
|
35
|
+
@app.call(env)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def negotiate(accept, formats)
|
42
|
+
accept = '*/*' if accept.to_s.empty?
|
43
|
+
|
44
|
+
parse(accept).each do |content_type, _|
|
45
|
+
return formats[content_type] if formats.key?(content_type)
|
46
|
+
end
|
47
|
+
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse(header)
|
52
|
+
header.to_s.split(/\s*,\s*/).map do |type|
|
53
|
+
attributes = type.split(/\s*;\s*/)
|
54
|
+
quality = extract_quality(attributes)
|
55
|
+
|
56
|
+
[attributes.join('; '), quality]
|
57
|
+
end.sort_by(&:last).reverse
|
58
|
+
end
|
59
|
+
|
60
|
+
def extract_quality(attributes, default = 1.0)
|
61
|
+
quality = default
|
62
|
+
|
63
|
+
attributes.delete_if do |attr|
|
64
|
+
quality = attr.split('q=').last.to_f if attr.start_with?('q=')
|
65
|
+
end
|
66
|
+
|
67
|
+
quality
|
68
|
+
end
|
69
|
+
|
70
|
+
def respond_with(format)
|
71
|
+
response = if Prometheus::Client.configuration.value_class.multiprocess
|
72
|
+
format.marshal_multiprocess
|
73
|
+
else
|
74
|
+
format.marshal
|
75
|
+
end
|
76
|
+
[
|
77
|
+
200,
|
78
|
+
{ 'Content-Type' => format::CONTENT_TYPE },
|
79
|
+
[response],
|
80
|
+
]
|
81
|
+
end
|
82
|
+
|
83
|
+
def not_acceptable(formats)
|
84
|
+
types = formats.map { |format| format::MEDIA_TYPE }
|
85
|
+
|
86
|
+
[
|
87
|
+
406,
|
88
|
+
{ 'Content-Type' => 'text/plain' },
|
89
|
+
["Supported media types: #{types.join(', ')}"],
|
90
|
+
]
|
91
|
+
end
|
92
|
+
|
93
|
+
def build_dictionary(formats, fallback)
|
94
|
+
formats.each_with_object('*/*' => fallback) do |format, memo|
|
95
|
+
memo[format::CONTENT_TYPE] = format
|
96
|
+
memo[format::MEDIA_TYPE] = format
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|