prometheus-client-mmap 0.7.0.beta1
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 +183 -0
- data/lib/prometheus.rb +5 -0
- data/lib/prometheus/client.rb +13 -0
- data/lib/prometheus/client/counter.rb +28 -0
- data/lib/prometheus/client/formats/text.rb +200 -0
- data/lib/prometheus/client/gauge.rb +32 -0
- data/lib/prometheus/client/histogram.rb +84 -0
- data/lib/prometheus/client/label_set_validator.rb +69 -0
- data/lib/prometheus/client/metric.rb +70 -0
- data/lib/prometheus/client/push.rb +72 -0
- data/lib/prometheus/client/rack/collector.rb +82 -0
- data/lib/prometheus/client/rack/exporter.rb +91 -0
- data/lib/prometheus/client/registry.rb +65 -0
- data/lib/prometheus/client/summary.rb +71 -0
- data/lib/prometheus/client/valuetype.rb +204 -0
- data/lib/prometheus/client/version.rb +7 -0
- metadata +75 -0
@@ -0,0 +1,84 @@
|
|
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 sum of all observed values.
|
10
|
+
class Histogram < Metric
|
11
|
+
# Value represents the state of a Histogram at a given point.
|
12
|
+
class Value < Hash
|
13
|
+
attr_accessor :sum, :total
|
14
|
+
|
15
|
+
def initialize(type, name, labels, buckets)
|
16
|
+
@sum = ValueClass.new(type, name, name.to_s + '_sum', labels)
|
17
|
+
# TODO: get rid of total and use +Inf bucket instead.
|
18
|
+
@total = ValueClass.new(type, name, name.to_s + '_count', labels)
|
19
|
+
|
20
|
+
buckets.each do |bucket|
|
21
|
+
# TODO: check that there are no user-defined "le" labels.
|
22
|
+
self[bucket] = ValueClass.new(type, name, name.to_s + '_bucket', labels.merge({'le' => bucket.to_s}))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def observe(value)
|
27
|
+
@sum.increment(value)
|
28
|
+
@total.increment()
|
29
|
+
|
30
|
+
each_key do |bucket|
|
31
|
+
self[bucket].increment() 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)
|
64
|
+
if labels[:le]
|
65
|
+
raise ArgumentError, 'Label with name "le" is not permitted'
|
66
|
+
end
|
67
|
+
|
68
|
+
label_set = label_set_for(labels)
|
69
|
+
synchronize { @values[label_set].observe(value) }
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def default(labels)
|
75
|
+
# TODO: default function needs to know key of hash info (label names and values)
|
76
|
+
Value.new(type, @name, labels, @buckets)
|
77
|
+
end
|
78
|
+
|
79
|
+
def sorted?(bucket)
|
80
|
+
bucket.each_cons(2).all? { |i, j| i <= j }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,69 @@
|
|
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
|
+
# TODO: we might allow setting :instance in the future
|
9
|
+
RESERVED_LABELS = [:job, :instance].freeze
|
10
|
+
|
11
|
+
class LabelSetError < StandardError; end
|
12
|
+
class InvalidLabelSetError < LabelSetError; end
|
13
|
+
class InvalidLabelError < LabelSetError; end
|
14
|
+
class ReservedLabelError < LabelSetError; end
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@validated = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid?(labels)
|
21
|
+
unless labels.respond_to?(:all?)
|
22
|
+
raise InvalidLabelSetError, "#{labels} is not a valid label set"
|
23
|
+
end
|
24
|
+
|
25
|
+
labels.all? do |key, _|
|
26
|
+
validate_symbol(key)
|
27
|
+
validate_name(key)
|
28
|
+
validate_reserved_key(key)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def validate(labels)
|
33
|
+
return labels if @validated.key?(labels.hash)
|
34
|
+
|
35
|
+
valid?(labels)
|
36
|
+
|
37
|
+
unless @validated.empty? || match?(labels, @validated.first.last)
|
38
|
+
raise InvalidLabelSetError, 'labels must have the same signature'
|
39
|
+
end
|
40
|
+
|
41
|
+
@validated[labels.hash] = labels
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def match?(a, b)
|
47
|
+
a.keys.sort == b.keys.sort
|
48
|
+
end
|
49
|
+
|
50
|
+
def validate_symbol(key)
|
51
|
+
return true if key.is_a?(Symbol)
|
52
|
+
|
53
|
+
raise InvalidLabelError, "label #{key} is not a symbol"
|
54
|
+
end
|
55
|
+
|
56
|
+
def validate_name(key)
|
57
|
+
return true unless key.to_s.start_with?('__')
|
58
|
+
|
59
|
+
raise ReservedLabelError, "label #{key} must not start with __"
|
60
|
+
end
|
61
|
+
|
62
|
+
def validate_reserved_key(key)
|
63
|
+
return true unless RESERVED_LABELS.include?(key)
|
64
|
+
|
65
|
+
raise ReservedLabelError, "#{key} is reserved"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
require 'prometheus/client/label_set_validator'
|
5
|
+
require 'prometheus/client/valuetype'
|
6
|
+
|
7
|
+
module Prometheus
|
8
|
+
module Client
|
9
|
+
# Metric
|
10
|
+
class Metric
|
11
|
+
attr_reader :name, :docstring, :base_labels
|
12
|
+
|
13
|
+
def initialize(name, docstring, base_labels = {})
|
14
|
+
@mutex = Mutex.new
|
15
|
+
@validator = LabelSetValidator.new
|
16
|
+
@values = Hash.new { |hash, key| hash[key] = default(key) }
|
17
|
+
|
18
|
+
validate_name(name)
|
19
|
+
validate_docstring(docstring)
|
20
|
+
@validator.valid?(base_labels)
|
21
|
+
|
22
|
+
@name = name
|
23
|
+
@docstring = docstring
|
24
|
+
@base_labels = base_labels
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the value for the given label set
|
28
|
+
def get(labels = {})
|
29
|
+
@validator.valid?(labels)
|
30
|
+
|
31
|
+
@values[labels].get
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns all label sets with their values
|
35
|
+
def values
|
36
|
+
synchronize do
|
37
|
+
@values.each_with_object({}) do |(labels, value), memo|
|
38
|
+
memo[labels] = value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def default(labels)
|
46
|
+
ValueClass.new(type, @name, @name, labels)
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate_name(name)
|
50
|
+
return true if name.is_a?(Symbol)
|
51
|
+
|
52
|
+
raise ArgumentError, 'given name must be a symbol'
|
53
|
+
end
|
54
|
+
|
55
|
+
def validate_docstring(docstring)
|
56
|
+
return true if docstring.respond_to?(:empty?) && !docstring.empty?
|
57
|
+
|
58
|
+
raise ArgumentError, 'docstring must be given'
|
59
|
+
end
|
60
|
+
|
61
|
+
def label_set_for(labels)
|
62
|
+
@validator.validate(labels)
|
63
|
+
end
|
64
|
+
|
65
|
+
def synchronize(&block)
|
66
|
+
@mutex.synchronize(&block)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
require 'prometheus/client'
|
7
|
+
require 'prometheus/client/formats/text'
|
8
|
+
|
9
|
+
module Prometheus
|
10
|
+
# Client is a ruby implementation for a Prometheus compatible client.
|
11
|
+
module Client
|
12
|
+
# Push implements a simple way to transmit a given registry to a given
|
13
|
+
# Pushgateway.
|
14
|
+
class Push
|
15
|
+
DEFAULT_GATEWAY = 'http://localhost:9091'.freeze
|
16
|
+
PATH = '/metrics/jobs/%s'.freeze
|
17
|
+
INSTANCE_PATH = '/metrics/jobs/%s/instances/%s'.freeze
|
18
|
+
HEADER = { 'Content-Type' => Formats::Text::CONTENT_TYPE }.freeze
|
19
|
+
|
20
|
+
attr_reader :job, :instance, :gateway, :path
|
21
|
+
|
22
|
+
def initialize(job, instance = nil, gateway = nil)
|
23
|
+
@job = job
|
24
|
+
@instance = instance
|
25
|
+
@gateway = gateway || DEFAULT_GATEWAY
|
26
|
+
@uri = parse(@gateway)
|
27
|
+
@path = build_path(job, instance)
|
28
|
+
@http = Net::HTTP.new(@uri.host, @uri.port)
|
29
|
+
end
|
30
|
+
|
31
|
+
def add(registry)
|
32
|
+
request('POST', registry)
|
33
|
+
end
|
34
|
+
|
35
|
+
def replace(registry)
|
36
|
+
request('PUT', registry)
|
37
|
+
end
|
38
|
+
|
39
|
+
def delete
|
40
|
+
@http.send_request('DELETE', path)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def parse(url)
|
46
|
+
uri = URI.parse(url)
|
47
|
+
|
48
|
+
if uri.scheme == 'http'
|
49
|
+
uri
|
50
|
+
else
|
51
|
+
raise ArgumentError, 'only HTTP gateway URLs are supported currently.'
|
52
|
+
end
|
53
|
+
rescue URI::InvalidURIError => e
|
54
|
+
raise ArgumentError, "#{url} is not a valid URL: #{e}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def build_path(job, instance)
|
58
|
+
if instance
|
59
|
+
format(INSTANCE_PATH, URI.escape(job), URI.escape(instance))
|
60
|
+
else
|
61
|
+
format(PATH, URI.escape(job))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def request(method, registry)
|
66
|
+
data = Formats::Text.marshal(registry)
|
67
|
+
|
68
|
+
@http.send_request(method, path, data, HEADER)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,82 @@
|
|
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 histogram of the response latency.',
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def init_exception_metrics
|
49
|
+
@exceptions = @registry.counter(
|
50
|
+
:http_exceptions_total,
|
51
|
+
'A counter of the total number of exceptions raised.',
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
def trace(env)
|
56
|
+
start = Time.now
|
57
|
+
yield.tap do |response|
|
58
|
+
duration = (Time.now - start).to_f
|
59
|
+
record(labels(env, response), duration)
|
60
|
+
end
|
61
|
+
rescue => exception
|
62
|
+
@exceptions.increment(exception: exception.class.name)
|
63
|
+
raise
|
64
|
+
end
|
65
|
+
|
66
|
+
def labels(env, response)
|
67
|
+
@label_builder.call(env).tap do |labels|
|
68
|
+
labels[:code] = response.first.to_s
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def record(labels, duration)
|
73
|
+
@requests.increment(labels)
|
74
|
+
@durations.observe(labels, duration)
|
75
|
+
rescue
|
76
|
+
# TODO: log unexpected exception during request recording
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'prometheus/client'
|
4
|
+
require 'prometheus/client/formats/text'
|
5
|
+
|
6
|
+
module Prometheus
|
7
|
+
module Client
|
8
|
+
module Rack
|
9
|
+
# Exporter is a Rack middleware that provides a sample implementation of
|
10
|
+
# a Prometheus HTTP client API.
|
11
|
+
class Exporter
|
12
|
+
attr_reader :app, :registry, :path
|
13
|
+
|
14
|
+
FORMATS = [Formats::Text].freeze
|
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
|
+
@acceptable = build_dictionary(FORMATS, FALLBACK)
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(env)
|
25
|
+
if env['PATH_INFO'] == @path
|
26
|
+
format = negotiate(env['HTTP_ACCEPT'], @acceptable)
|
27
|
+
format ? respond_with(format) : not_acceptable(FORMATS)
|
28
|
+
else
|
29
|
+
@app.call(env)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def negotiate(accept, formats)
|
36
|
+
accept = '*/*' if accept.to_s.empty?
|
37
|
+
|
38
|
+
parse(accept).each do |content_type, _|
|
39
|
+
return formats[content_type] if formats.key?(content_type)
|
40
|
+
end
|
41
|
+
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse(header)
|
46
|
+
header.to_s.split(/\s*,\s*/).map do |type|
|
47
|
+
attributes = type.split(/\s*;\s*/)
|
48
|
+
quality = extract_quality(attributes)
|
49
|
+
|
50
|
+
[attributes.join('; '), quality]
|
51
|
+
end.sort_by(&:last).reverse
|
52
|
+
end
|
53
|
+
|
54
|
+
def extract_quality(attributes, default = 1.0)
|
55
|
+
quality = default
|
56
|
+
|
57
|
+
attributes.delete_if do |attr|
|
58
|
+
quality = attr.split('q=').last.to_f if attr.start_with?('q=')
|
59
|
+
end
|
60
|
+
|
61
|
+
quality
|
62
|
+
end
|
63
|
+
|
64
|
+
def respond_with(format)
|
65
|
+
[
|
66
|
+
200,
|
67
|
+
{ 'Content-Type' => format::CONTENT_TYPE },
|
68
|
+
[format.marshal(@registry)],
|
69
|
+
]
|
70
|
+
end
|
71
|
+
|
72
|
+
def not_acceptable(formats)
|
73
|
+
types = formats.map { |format| format::MEDIA_TYPE }
|
74
|
+
|
75
|
+
[
|
76
|
+
406,
|
77
|
+
{ 'Content-Type' => 'text/plain' },
|
78
|
+
["Supported media types: #{types.join(', ')}"],
|
79
|
+
]
|
80
|
+
end
|
81
|
+
|
82
|
+
def build_dictionary(formats, fallback)
|
83
|
+
formats.each_with_object('*/*' => fallback) do |format, memo|
|
84
|
+
memo[format::CONTENT_TYPE] = format
|
85
|
+
memo[format::MEDIA_TYPE] = format
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|