prometheus-client 0.2.0 → 0.3.0.pre.rc.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/README.md +44 -15
- data/lib/prometheus.rb +3 -0
- data/lib/prometheus/client.rb +4 -3
- data/lib/prometheus/client/counter.rb +4 -2
- data/lib/prometheus/client/formats/json.rb +34 -0
- data/lib/prometheus/client/formats/text.rb +76 -0
- data/lib/prometheus/client/gauge.rb +4 -0
- data/lib/prometheus/client/label_set_validator.rb +69 -0
- data/lib/prometheus/client/metric.rb +37 -16
- data/lib/prometheus/client/push.rb +59 -0
- data/lib/prometheus/client/rack/collector.rb +43 -23
- data/lib/prometheus/client/rack/exporter.rb +56 -7
- data/lib/prometheus/client/registry.rb +17 -19
- data/lib/prometheus/client/summary.rb +32 -7
- data/lib/prometheus/client/version.rb +3 -1
- metadata +26 -23
- data/lib/prometheus/client/container.rb +0 -26
- data/lib/prometheus/client/label_set.rb +0 -44
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MjNjNjA5ODM2MDg5NzA0Mjk5ZmFhY2Q3MzgzYzU5NmQ0MmEwNjIzYw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MWM4NWNhOWY5ZjA0YmUyNjY2YzJiZTA3Zjk0OTRkNjAyZTFhZDAzZg==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
M2JlODIxYWUwZGE0ZDQxODJkODdlNzNmYmYzZjU5NDhiYmYzOTQxYTVlNDg0
|
10
|
+
ZGQ2NzE3MmEzZDk1ZTUzZmExMDc5ZGE0NDEyMmU5ZjQyZWUyODdmOWE5YWQ0
|
11
|
+
NThkZDRkNmYzM2Q4YTI0YmM1ZjliOWJmY2Q4OGJhZGZhMDg1NjY=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZWZjODRiYzFiNjMxNzliYWM3M2E4M2ZhYTM2Y2Q0MjYyNmRhOWMzZDdiMzU3
|
14
|
+
ZTBmNDkzYjk1Y2IwMGY2ZTliYmNhYTk1NGEzYTU4YzAyNDc2YzNkZWRlOGY2
|
15
|
+
NjllNTA2OTFlMGY4ZjQ4NmZjNWFiNDMzODE3YWMyNzljZWYyNzE=
|
data/README.md
CHANGED
@@ -4,9 +4,15 @@ A suite of instrumentation metric primitives for Ruby that can be exposed
|
|
4
4
|
through a JSON web services interface. Intended to be used together with a
|
5
5
|
[Prometheus server][1].
|
6
6
|
|
7
|
+
[![Gem Version][4]](http://badge.fury.io/rb/prometheus-client)
|
8
|
+
[![Build Status][3]](http://travis-ci.org/prometheus/client_ruby)
|
9
|
+
[![Dependency Status][5]](https://gemnasium.com/prometheus/client_ruby)
|
10
|
+
[![Code Climate][6]](https://codeclimate.com/github/prometheus/client_ruby)
|
11
|
+
[![Coverage Status][7]](https://coveralls.io/r/prometheus/client_ruby)
|
12
|
+
|
7
13
|
## Usage
|
8
14
|
|
9
|
-
###
|
15
|
+
### Overview
|
10
16
|
|
11
17
|
```ruby
|
12
18
|
require 'prometheus/client'
|
@@ -15,9 +21,9 @@ require 'prometheus/client'
|
|
15
21
|
prometheus = Prometheus::Client.registry
|
16
22
|
|
17
23
|
# create a new counter metric
|
18
|
-
http_requests = Prometheus::Client::Counter.new
|
24
|
+
http_requests = Prometheus::Client::Counter.new(:http_requests, 'A counter of HTTP requests made')
|
19
25
|
# register the metric
|
20
|
-
prometheus.register(
|
26
|
+
prometheus.register(http_requests)
|
21
27
|
|
22
28
|
# equivalent helper function
|
23
29
|
http_requests = prometheus.counter(:http_requests, 'A counter of HTTP requests made')
|
@@ -50,16 +56,38 @@ Start the server and have a look at the metrics endpoint:
|
|
50
56
|
For further instructions and other scripts to get started, have a look at the
|
51
57
|
integrated [example application](examples/rack/README.md).
|
52
58
|
|
59
|
+
### Pushgateway
|
60
|
+
|
61
|
+
The Ruby client can also be used to push its collected metrics to a
|
62
|
+
[Pushgateway][8]. This comes in handy with batch jobs or in other scenarios
|
63
|
+
where it's not possible or feasible to let a Prometheus server scrape a Ruby
|
64
|
+
process.
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
require 'prometheus/client'
|
68
|
+
require 'prometheus/client/push'
|
69
|
+
|
70
|
+
prometheus = Prometheus::Client.registry
|
71
|
+
# ... register some metrics, set/add/increment/etc. their values
|
72
|
+
|
73
|
+
# push the registry state to the default gateway
|
74
|
+
Prometheus::Client::Push.new('my-batch-job').push(prometheus)
|
75
|
+
|
76
|
+
# optional: specify the instance name (instead of IP) and gateway
|
77
|
+
Prometheus::Client::Push.new(
|
78
|
+
'my-job', 'instance-name', 'http://example.domain:1234').push(prometheus)
|
79
|
+
```
|
80
|
+
|
53
81
|
## Metrics
|
54
82
|
|
55
83
|
The following metric types are currently supported.
|
56
84
|
|
57
85
|
### Counter
|
58
86
|
|
59
|
-
|
87
|
+
Counter is a metric that exposes merely a sum or tally of things.
|
60
88
|
|
61
89
|
```ruby
|
62
|
-
counter = Prometheus::Client::Counter.new
|
90
|
+
counter = Prometheus::Client::Counter.new(:foo, '...')
|
63
91
|
|
64
92
|
# increment the counter for a given label set
|
65
93
|
counter.increment(service: 'foo')
|
@@ -77,11 +105,11 @@ counter.get(service: 'bar')
|
|
77
105
|
|
78
106
|
### Gauge
|
79
107
|
|
80
|
-
|
81
|
-
|
108
|
+
Gauge is a metric that exposes merely an instantaneous value or some snapshot
|
109
|
+
thereof.
|
82
110
|
|
83
111
|
```ruby
|
84
|
-
gauge = Prometheus::Client::Gauge.new
|
112
|
+
gauge = Prometheus::Client::Gauge.new(:bar, '...')
|
85
113
|
|
86
114
|
# set a value
|
87
115
|
gauge.set({ role: 'base' }, 'up')
|
@@ -93,11 +121,11 @@ gauge.get({ role: 'problematic' })
|
|
93
121
|
|
94
122
|
### Summary
|
95
123
|
|
96
|
-
|
124
|
+
Summary is an accumulator for samples. It captures Numeric data and provides
|
97
125
|
an efficient percentile calculation mechanism.
|
98
126
|
|
99
127
|
```ruby
|
100
|
-
summary = Prometheus::Client::Summary.new
|
128
|
+
summary = Prometheus::Client::Summary.new(:baz, '...')
|
101
129
|
|
102
130
|
# record a value
|
103
131
|
summary.add({ service: 'slow' }, Benchmark.realtime { service.call(arg) })
|
@@ -109,21 +137,22 @@ summary.get({ service: 'database' })
|
|
109
137
|
|
110
138
|
## Todo
|
111
139
|
|
112
|
-
* add push support to a vanilla prometheus exporter
|
113
|
-
* use a more performant JSON library
|
114
140
|
* add protobuf support
|
115
141
|
|
116
142
|
## Tests
|
117
143
|
|
118
|
-
[![Build Status][3]](http://travis-ci.org/prometheus/client_ruby)
|
119
|
-
|
120
144
|
Install necessary development gems with `bundle install` and run tests with
|
121
145
|
rspec:
|
122
146
|
|
123
147
|
```bash
|
124
|
-
|
148
|
+
rake
|
125
149
|
```
|
126
150
|
|
127
151
|
[1]: https://github.com/prometheus/prometheus
|
128
152
|
[2]: http://rack.github.io/
|
129
153
|
[3]: https://secure.travis-ci.org/prometheus/client_ruby.png?branch=master
|
154
|
+
[4]: https://badge.fury.io/rb/prometheus-client.svg
|
155
|
+
[5]: https://gemnasium.com/prometheus/client_ruby.svg
|
156
|
+
[6]: https://codeclimate.com/github/prometheus/client_ruby.png
|
157
|
+
[7]: https://coveralls.io/repos/prometheus/client_ruby/badge.png?branch=master
|
158
|
+
[8]: https://github.com/prometheus/pushgateway
|
data/lib/prometheus.rb
CHANGED
data/lib/prometheus/client.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
1
3
|
require 'prometheus/client/registry'
|
2
4
|
|
3
5
|
module Prometheus
|
6
|
+
# Client is a ruby implementation for a Prometheus compatible client.
|
4
7
|
module Client
|
5
|
-
|
6
8
|
# Returns a default registry object
|
7
9
|
def self.registry
|
8
|
-
|
10
|
+
@registry ||= Registry.new
|
9
11
|
end
|
10
|
-
|
11
12
|
end
|
12
13
|
end
|
@@ -1,7 +1,10 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
1
3
|
require 'prometheus/client/metric'
|
2
4
|
|
3
5
|
module Prometheus
|
4
6
|
module Client
|
7
|
+
# Counter is a metric that exposes merely a sum or tally of things.
|
5
8
|
class Counter < Metric
|
6
9
|
def type
|
7
10
|
:counter
|
@@ -16,12 +19,11 @@ module Prometheus
|
|
16
19
|
increment(labels, -by)
|
17
20
|
end
|
18
21
|
|
19
|
-
|
22
|
+
private
|
20
23
|
|
21
24
|
def default
|
22
25
|
0
|
23
26
|
end
|
24
|
-
|
25
27
|
end
|
26
28
|
end
|
27
29
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Prometheus
|
6
|
+
module Client
|
7
|
+
module Formats
|
8
|
+
# JSON format is a deprecated, human-readable format to expose the state
|
9
|
+
# of a given registry.
|
10
|
+
module JSON
|
11
|
+
MEDIA_TYPE = 'application/json'
|
12
|
+
SCHEMA = 'prometheus/telemetry'
|
13
|
+
VERSION = '0.0.2'
|
14
|
+
CONTENT_TYPE = \
|
15
|
+
%Q(#{MEDIA_TYPE}; schema="#{SCHEMA}"; version=#{VERSION})
|
16
|
+
|
17
|
+
MAPPING = { summary: :histogram }
|
18
|
+
|
19
|
+
def self.marshal(registry)
|
20
|
+
registry.metrics.map do |metric|
|
21
|
+
{
|
22
|
+
baseLabels: metric.base_labels.merge(__name__: metric.name),
|
23
|
+
docstring: metric.docstring,
|
24
|
+
metric: {
|
25
|
+
type: MAPPING[metric.type] || metric.type,
|
26
|
+
value: metric.values.map { |l, v| { labels: l, value: v } },
|
27
|
+
},
|
28
|
+
}
|
29
|
+
end.to_json
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,76 @@
|
|
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'
|
9
|
+
VERSION = '0.0.4'
|
10
|
+
CONTENT_TYPE = %Q(#{MEDIA_TYPE}; version=#{VERSION})
|
11
|
+
|
12
|
+
METRIC_LINE = '%s%s %s'
|
13
|
+
TYPE_LINE = '# TYPE %s %s'
|
14
|
+
HELP_LINE = '# HELP %s %s'
|
15
|
+
|
16
|
+
LABEL = '%s="%s"'
|
17
|
+
SEPARATOR = ','
|
18
|
+
DELIMITER = "\n"
|
19
|
+
|
20
|
+
REGEX = { doc: /[\n\\]/, label: /[\n\\"]/ }
|
21
|
+
REPLACE = { "\n" => '\n', '\\' => '\\\\', '"' => '\"' }
|
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
|
+
set = metric.base_labels.merge(label_set)
|
32
|
+
representation(metric, set, value) { |l| lines << l }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# there must be a trailing delimiter
|
37
|
+
(lines << nil).join(DELIMITER)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def self.representation(metric, set, value)
|
43
|
+
if metric.type == :summary
|
44
|
+
value.each do |q, v|
|
45
|
+
yield metric(metric.name, labels(set.merge(quantile: q)), v)
|
46
|
+
end
|
47
|
+
|
48
|
+
l = labels(set)
|
49
|
+
yield metric("#{metric.name}_sum", l, value.sum)
|
50
|
+
yield metric("#{metric.name}_total", l, value.total)
|
51
|
+
else
|
52
|
+
yield metric(metric.name, labels(set), value)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.metric(name, labels, value)
|
57
|
+
format(METRIC_LINE, name, labels, value)
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.labels(set)
|
61
|
+
return if set.empty?
|
62
|
+
|
63
|
+
strings = set.each_with_object([]) do |(key, value), memo|
|
64
|
+
memo << format(LABEL, key, escape(value, :label))
|
65
|
+
end
|
66
|
+
|
67
|
+
"{#{strings.join(SEPARATOR)}}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.escape(string, format = :doc)
|
71
|
+
string.to_s.gsub(REGEX[format], REPLACE)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
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]
|
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
|
+
fail 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
|
+
fail 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
|
+
fail 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
|
+
fail 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
|
+
fail ReservedLabelError, "#{key} is reserved"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -1,47 +1,68 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
1
3
|
require 'thread'
|
2
|
-
require 'prometheus/client/
|
4
|
+
require 'prometheus/client/label_set_validator'
|
3
5
|
|
4
6
|
module Prometheus
|
5
7
|
module Client
|
6
8
|
# Metric
|
7
9
|
class Metric
|
8
|
-
|
10
|
+
attr_reader :name, :docstring, :base_labels
|
11
|
+
|
12
|
+
def initialize(name, docstring, base_labels = {})
|
9
13
|
@mutex = Mutex.new
|
14
|
+
@validator = LabelSetValidator.new
|
10
15
|
@values = Hash.new { |hash, key| hash[key] = default }
|
16
|
+
|
17
|
+
validate_name(name)
|
18
|
+
validate_docstring(docstring)
|
19
|
+
@validator.valid?(base_labels)
|
20
|
+
|
21
|
+
@name, @docstring = name, docstring
|
22
|
+
@base_labels = base_labels
|
11
23
|
end
|
12
24
|
|
13
25
|
# Returns the metric type
|
14
26
|
def type
|
15
|
-
|
27
|
+
fail NotImplementedError
|
16
28
|
end
|
17
29
|
|
18
30
|
# Returns the value for the given label set
|
19
31
|
def get(labels = {})
|
20
|
-
@
|
32
|
+
@validator.valid?(labels)
|
33
|
+
|
34
|
+
@values[labels]
|
21
35
|
end
|
22
36
|
|
23
|
-
#
|
24
|
-
def
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
37
|
+
# Returns all label sets with their values
|
38
|
+
def values
|
39
|
+
synchronize do
|
40
|
+
@values.each_with_object({}) do |(labels, value), memo|
|
41
|
+
memo[labels] = value
|
42
|
+
end
|
43
|
+
end
|
29
44
|
end
|
30
45
|
|
31
|
-
|
46
|
+
private
|
32
47
|
|
33
48
|
def default
|
34
49
|
nil
|
35
50
|
end
|
36
51
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
52
|
+
def validate_name(name)
|
53
|
+
return true if name.is_a?(Symbol)
|
54
|
+
|
55
|
+
fail ArgumentError, 'given name must be a symbol'
|
56
|
+
end
|
57
|
+
|
58
|
+
def validate_docstring(docstring)
|
59
|
+
return true if docstring.respond_to?(:empty?) && !docstring.empty?
|
60
|
+
|
61
|
+
fail ArgumentError, 'docstring must be given'
|
41
62
|
end
|
42
63
|
|
43
64
|
def label_set_for(labels)
|
44
|
-
|
65
|
+
@validator.validate(labels)
|
45
66
|
end
|
46
67
|
|
47
68
|
def synchronize(&block)
|
@@ -0,0 +1,59 @@
|
|
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'
|
16
|
+
PATH = '/metrics/jobs/%s'
|
17
|
+
INSTANCE_PATH = '/metrics/jobs/%s/instances/%s'
|
18
|
+
HEADER = { 'Content-Type' => Formats::Text::CONTENT_TYPE }
|
19
|
+
|
20
|
+
attr_reader :job, :instance, :gateway, :path
|
21
|
+
|
22
|
+
def initialize(job, instance = nil, gateway = nil)
|
23
|
+
@job, @instance, @gateway = job, instance, gateway || DEFAULT_GATEWAY
|
24
|
+
|
25
|
+
@uri = parse(@gateway)
|
26
|
+
@path = build_path(job, instance)
|
27
|
+
end
|
28
|
+
|
29
|
+
def push(registry)
|
30
|
+
data = Formats::Text.marshal(registry)
|
31
|
+
http = Net::HTTP.new(@uri.host, @uri.port)
|
32
|
+
|
33
|
+
http.send_request('PUT', path, data, HEADER)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def parse(url)
|
39
|
+
uri = URI.parse(url)
|
40
|
+
|
41
|
+
if uri.scheme == 'http'
|
42
|
+
uri
|
43
|
+
else
|
44
|
+
fail ArgumentError, 'only HTTP gateway URLs are supported currently.'
|
45
|
+
end
|
46
|
+
rescue URI::InvalidURIError => e
|
47
|
+
raise ArgumentError, "#{url} is not a valid URL: #{e}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def build_path(job, instance)
|
51
|
+
if instance
|
52
|
+
format(INSTANCE_PATH, URI.escape(job), URI.escape(instance))
|
53
|
+
else
|
54
|
+
format(PATH, URI.escape(job))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -1,59 +1,79 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
1
3
|
require 'prometheus/client'
|
2
4
|
|
3
5
|
module Prometheus
|
4
6
|
module Client
|
5
7
|
module Rack
|
8
|
+
# Collector is a Rack middleware that provides a sample implementation of
|
9
|
+
# a Prometheus HTTP client API.
|
6
10
|
class Collector
|
7
11
|
attr_reader :app, :registry
|
8
12
|
|
9
|
-
def initialize(app, options = {})
|
13
|
+
def initialize(app, options = {}, &label_builder)
|
10
14
|
@app = app
|
11
15
|
@registry = options[:registry] || Client.registry
|
16
|
+
@label_builder = label_builder || proc do |env|
|
17
|
+
{
|
18
|
+
method: env['REQUEST_METHOD'].downcase,
|
19
|
+
path: env['PATH_INFO'].to_s,
|
20
|
+
}
|
21
|
+
end
|
12
22
|
|
13
|
-
|
23
|
+
init_request_metrics
|
24
|
+
init_exception_metrics
|
14
25
|
end
|
15
26
|
|
16
27
|
def call(env) # :nodoc:
|
17
28
|
trace(env) { @app.call(env) }
|
18
29
|
end
|
19
30
|
|
20
|
-
|
31
|
+
protected
|
32
|
+
|
33
|
+
def init_request_metrics
|
34
|
+
@requests = @registry.counter(
|
35
|
+
:http_requests_total,
|
36
|
+
'A counter of the total number of HTTP requests made.',)
|
37
|
+
@requests_duration = @registry.counter(
|
38
|
+
:http_request_durations_total_microseconds,
|
39
|
+
'The total amount of time spent answering HTTP requests ' \
|
40
|
+
'(microseconds).',)
|
41
|
+
@durations = @registry.summary(
|
42
|
+
:http_request_durations_microseconds,
|
43
|
+
'A histogram of the response latency (microseconds).',)
|
44
|
+
end
|
21
45
|
|
22
|
-
def
|
23
|
-
@
|
24
|
-
|
25
|
-
|
46
|
+
def init_exception_metrics
|
47
|
+
@exceptions = @registry.counter(
|
48
|
+
:http_exceptions_total,
|
49
|
+
'A counter of the total number of exceptions raised.',)
|
26
50
|
end
|
27
51
|
|
28
|
-
def trace(env
|
52
|
+
def trace(env)
|
29
53
|
start = Time.now
|
30
|
-
|
54
|
+
yield.tap do |response|
|
55
|
+
duration = ((Time.now - start) * 1_000_000).to_i
|
56
|
+
record(labels(env, response), duration)
|
57
|
+
end
|
31
58
|
rescue => exception
|
59
|
+
@exceptions.increment(exception: exception.class.name)
|
32
60
|
raise
|
33
|
-
ensure
|
34
|
-
duration = ((Time.now - start) * 1_000_000).to_i
|
35
|
-
record(duration, env, response, exception)
|
36
61
|
end
|
37
62
|
|
38
|
-
def
|
39
|
-
|
40
|
-
:method => env['REQUEST_METHOD'].downcase,
|
41
|
-
:path => env['PATH_INFO'].to_s,
|
42
|
-
}
|
43
|
-
|
44
|
-
if response
|
63
|
+
def labels(env, response)
|
64
|
+
@label_builder.call(env).tap do |labels|
|
45
65
|
labels[:code] = response.first.to_s
|
46
|
-
else
|
47
|
-
labels[:exception] = exception.class.name
|
48
66
|
end
|
67
|
+
end
|
49
68
|
|
69
|
+
def record(labels, duration)
|
50
70
|
@requests.increment(labels)
|
51
71
|
@requests_duration.increment(labels, duration)
|
52
72
|
@durations.add(labels, duration)
|
53
|
-
rescue
|
73
|
+
rescue
|
54
74
|
# TODO: log unexpected exception during request recording
|
75
|
+
nil
|
55
76
|
end
|
56
|
-
|
57
77
|
end
|
58
78
|
end
|
59
79
|
end
|
@@ -1,35 +1,84 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
1
3
|
require 'prometheus/client'
|
4
|
+
require 'prometheus/client/formats/json'
|
5
|
+
require 'prometheus/client/formats/text'
|
2
6
|
|
3
7
|
module Prometheus
|
4
8
|
module Client
|
5
9
|
module Rack
|
10
|
+
# Exporter is a Rack middleware that provides a sample implementation of
|
11
|
+
# a HTTP tracer. The default label builder can be modified to export a
|
12
|
+
# differet set of labels per recorded metric.
|
6
13
|
class Exporter
|
7
14
|
attr_reader :app, :registry, :path
|
8
15
|
|
9
|
-
|
10
|
-
|
11
|
-
HEADERS = { 'Content-Type' => CONTENT_TYPE }
|
16
|
+
FORMATS = [Formats::Text, Formats::JSON]
|
17
|
+
FALLBACK = Formats::JSON
|
12
18
|
|
13
19
|
def initialize(app, options = {})
|
14
20
|
@app = app
|
15
21
|
@registry = options[:registry] || Client.registry
|
16
22
|
@path = options[:path] || '/metrics'
|
23
|
+
@accpetable = build_dictionary(FORMATS)
|
17
24
|
end
|
18
25
|
|
19
26
|
def call(env)
|
20
27
|
if env['PATH_INFO'] == @path
|
21
|
-
|
28
|
+
format = negotiate(env['HTTP_ACCEPT'], @accpetable, FALLBACK)
|
29
|
+
format ? respond_with(format) : not_acceptable(FORMATS)
|
22
30
|
else
|
23
31
|
@app.call(env)
|
24
32
|
end
|
25
33
|
end
|
26
34
|
|
27
|
-
|
35
|
+
private
|
36
|
+
|
37
|
+
def negotiate(accept, formats, fallback)
|
38
|
+
return fallback if accept.to_s.empty?
|
39
|
+
|
40
|
+
parse(accept).each do |content_type, _|
|
41
|
+
return formats[content_type] if formats.key?(content_type)
|
42
|
+
end
|
28
43
|
|
29
|
-
|
30
|
-
[200, HEADERS, [@registry.to_json]]
|
44
|
+
nil
|
31
45
|
end
|
32
46
|
|
47
|
+
def parse(header)
|
48
|
+
header.to_s.split(/\s*,\s*/).map do |type|
|
49
|
+
attributes = type.split(/\s*;\s*/)
|
50
|
+
quality = 1.0
|
51
|
+
attributes.delete_if do |attr|
|
52
|
+
quality = attr.split('q=').last.to_f if attr.start_with?('q=')
|
53
|
+
end
|
54
|
+
[attributes.join('; '), quality]
|
55
|
+
end.sort_by(&:last).reverse
|
56
|
+
end
|
57
|
+
|
58
|
+
def respond_with(format)
|
59
|
+
[
|
60
|
+
200,
|
61
|
+
{ 'Content-Type' => format::CONTENT_TYPE },
|
62
|
+
[format.marshal(@registry)],
|
63
|
+
]
|
64
|
+
end
|
65
|
+
|
66
|
+
def not_acceptable(formats)
|
67
|
+
types = formats.map { |format| format::MEDIA_TYPE }
|
68
|
+
|
69
|
+
[
|
70
|
+
406,
|
71
|
+
{ 'Content-Type' => 'text/plain' },
|
72
|
+
["Supported media types: #{types.join(', ')}"],
|
73
|
+
]
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_dictionary(formats)
|
77
|
+
formats.each_with_object({}) do |format, memo|
|
78
|
+
memo[format::CONTENT_TYPE] = format
|
79
|
+
memo[format::MEDIA_TYPE] = format
|
80
|
+
end
|
81
|
+
end
|
33
82
|
end
|
34
83
|
end
|
35
84
|
end
|
@@ -1,7 +1,7 @@
|
|
1
|
-
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
2
3
|
require 'thread'
|
3
4
|
|
4
|
-
require 'prometheus/client/container'
|
5
5
|
require 'prometheus/client/counter'
|
6
6
|
require 'prometheus/client/summary'
|
7
7
|
require 'prometheus/client/gauge'
|
@@ -9,52 +9,50 @@ require 'prometheus/client/gauge'
|
|
9
9
|
module Prometheus
|
10
10
|
module Client
|
11
11
|
# Registry
|
12
|
-
#
|
13
|
-
#
|
14
12
|
class Registry
|
15
13
|
class AlreadyRegisteredError < StandardError; end
|
16
14
|
|
17
|
-
def initialize
|
18
|
-
@
|
15
|
+
def initialize
|
16
|
+
@metrics = {}
|
19
17
|
@mutex = Mutex.new
|
20
18
|
end
|
21
19
|
|
22
|
-
def register(
|
23
|
-
|
20
|
+
def register(metric)
|
21
|
+
name = metric.name
|
24
22
|
|
25
23
|
@mutex.synchronize do
|
26
|
-
if exist?(name)
|
27
|
-
|
24
|
+
if exist?(name.to_sym)
|
25
|
+
fail AlreadyRegisteredError, "#{name} has already been registered"
|
28
26
|
else
|
29
|
-
@
|
27
|
+
@metrics[name.to_sym] = metric
|
30
28
|
end
|
31
29
|
end
|
32
30
|
|
33
|
-
|
31
|
+
metric
|
34
32
|
end
|
35
33
|
|
36
34
|
def counter(name, docstring, base_labels = {})
|
37
|
-
register(name, docstring,
|
35
|
+
register(Counter.new(name, docstring, base_labels))
|
38
36
|
end
|
39
37
|
|
40
38
|
def summary(name, docstring, base_labels = {})
|
41
|
-
register(name, docstring,
|
39
|
+
register(Summary.new(name, docstring, base_labels))
|
42
40
|
end
|
43
41
|
|
44
42
|
def gauge(name, docstring, base_labels = {})
|
45
|
-
register(name, docstring,
|
43
|
+
register(Gauge.new(name, docstring, base_labels))
|
46
44
|
end
|
47
45
|
|
48
46
|
def exist?(name)
|
49
|
-
@
|
47
|
+
@metrics.key?(name)
|
50
48
|
end
|
51
49
|
|
52
50
|
def get(name)
|
53
|
-
@
|
51
|
+
@metrics[name.to_sym]
|
54
52
|
end
|
55
53
|
|
56
|
-
def
|
57
|
-
@
|
54
|
+
def metrics
|
55
|
+
@metrics.values
|
58
56
|
end
|
59
57
|
end
|
60
58
|
end
|
@@ -1,11 +1,30 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
1
3
|
require 'quantile'
|
2
4
|
require 'prometheus/client/metric'
|
3
5
|
|
4
6
|
module Prometheus
|
5
7
|
module Client
|
8
|
+
# Summary is an accumulator for samples. It captures Numeric data and
|
9
|
+
# provides an efficient quantile calculation mechanism.
|
6
10
|
class Summary < Metric
|
11
|
+
# Value represents the state of a Summary at a given point.
|
12
|
+
class Value < Hash
|
13
|
+
attr_accessor :sum, :total
|
14
|
+
|
15
|
+
def initialize(estimator)
|
16
|
+
@sum, @total = estimator.sum, estimator.observations
|
17
|
+
|
18
|
+
values = estimator.invariants.each_with_object({}) do |i, memo|
|
19
|
+
memo[i.quantile] = estimator.query(i.quantile)
|
20
|
+
end
|
21
|
+
|
22
|
+
replace(values)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
7
26
|
def type
|
8
|
-
:
|
27
|
+
:summary
|
9
28
|
end
|
10
29
|
|
11
30
|
# Records a given value.
|
@@ -16,21 +35,27 @@ module Prometheus
|
|
16
35
|
|
17
36
|
# Returns the value for the given label set
|
18
37
|
def get(labels = {})
|
38
|
+
@validator.valid?(labels)
|
39
|
+
|
19
40
|
synchronize do
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
41
|
+
Value.new(@values[labels])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns all label sets with their values
|
46
|
+
def values
|
47
|
+
synchronize do
|
48
|
+
@values.each_with_object({}) do |(labels, value), memo|
|
49
|
+
memo[labels] = Value.new(value)
|
24
50
|
end
|
25
51
|
end
|
26
52
|
end
|
27
53
|
|
28
|
-
|
54
|
+
private
|
29
55
|
|
30
56
|
def default
|
31
57
|
Quantile::Estimator.new
|
32
58
|
end
|
33
|
-
|
34
59
|
end
|
35
60
|
end
|
36
61
|
end
|
metadata
CHANGED
@@ -1,27 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: prometheus-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.3.0.pre.rc.1
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Tobias Schmidt
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2014-05-22 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: quantile
|
16
|
-
requirement:
|
17
|
-
none: false
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
18
16
|
requirements:
|
19
17
|
- - ~>
|
20
18
|
- !ruby/object:Gem::Version
|
21
|
-
version: 0.
|
19
|
+
version: 0.2.0
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
|
-
version_requirements:
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.2.0
|
25
27
|
description:
|
26
28
|
email:
|
27
29
|
- ts@soundcloud.com
|
@@ -30,42 +32,43 @@ extensions: []
|
|
30
32
|
extra_rdoc_files: []
|
31
33
|
files:
|
32
34
|
- README.md
|
33
|
-
- lib/prometheus
|
34
|
-
- lib/prometheus/client
|
35
|
-
- lib/prometheus/client/metric.rb
|
36
|
-
- lib/prometheus/client/container.rb
|
35
|
+
- lib/prometheus.rb
|
36
|
+
- lib/prometheus/client.rb
|
37
37
|
- lib/prometheus/client/registry.rb
|
38
38
|
- lib/prometheus/client/rack/collector.rb
|
39
39
|
- lib/prometheus/client/rack/exporter.rb
|
40
|
-
- lib/prometheus/client/
|
41
|
-
- lib/prometheus/client/
|
40
|
+
- lib/prometheus/client/gauge.rb
|
41
|
+
- lib/prometheus/client/label_set_validator.rb
|
42
|
+
- lib/prometheus/client/formats/json.rb
|
43
|
+
- lib/prometheus/client/formats/text.rb
|
44
|
+
- lib/prometheus/client/push.rb
|
45
|
+
- lib/prometheus/client/metric.rb
|
46
|
+
- lib/prometheus/client/counter.rb
|
42
47
|
- lib/prometheus/client/summary.rb
|
43
|
-
- lib/prometheus/client.rb
|
44
|
-
- lib/prometheus.rb
|
48
|
+
- lib/prometheus/client/version.rb
|
45
49
|
homepage: https://github.com/prometheus/client_ruby
|
46
50
|
licenses:
|
47
51
|
- Apache 2.0
|
52
|
+
metadata: {}
|
48
53
|
post_install_message:
|
49
54
|
rdoc_options: []
|
50
55
|
require_paths:
|
51
56
|
- lib
|
52
57
|
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
-
none: false
|
54
58
|
requirements:
|
55
59
|
- - ! '>='
|
56
60
|
- !ruby/object:Gem::Version
|
57
61
|
version: '0'
|
58
62
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
-
none: false
|
60
63
|
requirements:
|
61
|
-
- - ! '
|
64
|
+
- - ! '>'
|
62
65
|
- !ruby/object:Gem::Version
|
63
|
-
version:
|
66
|
+
version: 1.3.1
|
64
67
|
requirements: []
|
65
68
|
rubyforge_project:
|
66
|
-
rubygems_version: 1.
|
69
|
+
rubygems_version: 2.1.11
|
67
70
|
signing_key:
|
68
|
-
specification_version:
|
69
|
-
summary: A suite of instrumentation metric
|
70
|
-
|
71
|
+
specification_version: 4
|
72
|
+
summary: A suite of instrumentation metric primitivesthat can be exposed through a
|
73
|
+
web services interface.
|
71
74
|
test_files: []
|
@@ -1,26 +0,0 @@
|
|
1
|
-
require 'prometheus/client/label_set'
|
2
|
-
|
3
|
-
module Prometheus
|
4
|
-
module Client
|
5
|
-
# Metric Container
|
6
|
-
class Container
|
7
|
-
attr_reader :name, :docstring, :metric, :base_labels
|
8
|
-
|
9
|
-
def initialize(name, docstring, metric, base_labels)
|
10
|
-
@name = name
|
11
|
-
@docstring = docstring
|
12
|
-
@metric = metric
|
13
|
-
@base_labels = LabelSet.new(base_labels)
|
14
|
-
end
|
15
|
-
|
16
|
-
def to_json(*json)
|
17
|
-
{
|
18
|
-
'baseLabels' => base_labels.merge(:name => name),
|
19
|
-
'docstring' => docstring,
|
20
|
-
'metric' => metric
|
21
|
-
}.to_json(*json)
|
22
|
-
end
|
23
|
-
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
@@ -1,44 +0,0 @@
|
|
1
|
-
module Prometheus
|
2
|
-
module Client
|
3
|
-
# LabelSet is a pseudo class used to ensure given labels are semantically
|
4
|
-
# correct.
|
5
|
-
class LabelSet
|
6
|
-
# TODO: we might allow setting :instance in the future
|
7
|
-
RESERVED_LABELS = [:name, :job, :instance]
|
8
|
-
|
9
|
-
class LabelSetError < StandardError; end
|
10
|
-
class InvalidLabelSetError < LabelSetError; end
|
11
|
-
class InvalidLabelError < LabelSetError; end
|
12
|
-
class ReservedLabelError < LabelSetError; end
|
13
|
-
|
14
|
-
# A list of validated label sets
|
15
|
-
@@validated = {}
|
16
|
-
|
17
|
-
def self.new(labels)
|
18
|
-
validate(labels)
|
19
|
-
labels
|
20
|
-
end
|
21
|
-
|
22
|
-
protected
|
23
|
-
|
24
|
-
def self.validate(labels)
|
25
|
-
@@validated[labels.hash] ||= begin
|
26
|
-
labels.keys.each do |key|
|
27
|
-
unless Symbol === key
|
28
|
-
raise InvalidLabelError, "label name #{key} is not a symbol"
|
29
|
-
end
|
30
|
-
|
31
|
-
if RESERVED_LABELS.include?(key)
|
32
|
-
raise ReservedLabelError, "labels may not contain reserved #{key} label"
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
true
|
37
|
-
end
|
38
|
-
rescue NoMethodError
|
39
|
-
raise InvalidLabelSetError, "#{labels} is not a valid label set"
|
40
|
-
end
|
41
|
-
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|