prometheus-client 0.2.0 → 0.3.0.pre.rc.1
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 +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
|