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.
@@ -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
- ### Library
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(:http_requests, 'A counter of HTTP requests made', http_requests)
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
- A Counter is a metric that exposes merely a sum or tally of things.
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
- A Gauge is a metric that exposes merely an instantaneous value or some
81
- snapshot thereof.
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
- The Summary is an accumulator for samples. It captures Numeric data and provides
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
- rspec
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
@@ -1,2 +1,5 @@
1
+ # encoding: UTF-8
2
+
3
+ # Prometheus is a generic time-series collection and computation server.
1
4
  module Prometheus
2
5
  end
@@ -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
- @@registry ||= Registry.new
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
- private
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
@@ -1,7 +1,11 @@
1
+ # encoding: UTF-8
2
+
1
3
  require 'prometheus/client/metric'
2
4
 
3
5
  module Prometheus
4
6
  module Client
7
+ # A Gauge is a metric that exposes merely an instantaneous value or some
8
+ # snapshot thereof.
5
9
  class Gauge < Metric
6
10
  def type
7
11
  :gauge
@@ -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/label_set'
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
- def initialize
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
- raise NotImplementedError
27
+ fail NotImplementedError
16
28
  end
17
29
 
18
30
  # Returns the value for the given label set
19
31
  def get(labels = {})
20
- @values[label_set_for(labels)]
32
+ @validator.valid?(labels)
33
+
34
+ @values[labels]
21
35
  end
22
36
 
23
- # Generates JSON representation
24
- def to_json(*json)
25
- {
26
- 'type' => type,
27
- 'value' => value
28
- }.to_json(*json)
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
- private
46
+ private
32
47
 
33
48
  def default
34
49
  nil
35
50
  end
36
51
 
37
- def value
38
- @values.map do |labels, value|
39
- { :labels => labels, :value => get(labels) }
40
- end
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
- LabelSet.new(labels)
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
- init_metrics
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
- protected
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 init_metrics
23
- @requests = @registry.counter(:http_requests_total, 'A counter of the total number of HTTP requests made')
24
- @requests_duration = @registry.counter(:http_request_durations_total_microseconds, 'The total amount of time Rack has spent answering HTTP requests (microseconds).')
25
- @durations = @registry.summary(:http_request_durations_microseconds, 'A histogram of the response latency for requests made (microseconds).')
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, &block)
52
+ def trace(env)
29
53
  start = Time.now
30
- response = yield
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 record(duration, env, response, exception)
39
- labels = {
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 => error
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
- API_VERSION = '0.0.2'
10
- CONTENT_TYPE = 'application/json; schema="prometheus/telemetry"; version=' + API_VERSION
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
- metrics_response
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
- protected
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
- def metrics_response
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
- require 'json'
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
- @containers = {}
15
+ def initialize
16
+ @metrics = {}
19
17
  @mutex = Mutex.new
20
18
  end
21
19
 
22
- def register(name, docstring, metric, base_labels = {})
23
- container = Container.new(name, docstring, metric, base_labels)
20
+ def register(metric)
21
+ name = metric.name
24
22
 
25
23
  @mutex.synchronize do
26
- if exist?(name)
27
- raise AlreadyRegisteredError, "#{name} has already been registered"
24
+ if exist?(name.to_sym)
25
+ fail AlreadyRegisteredError, "#{name} has already been registered"
28
26
  else
29
- @containers[name] = container
27
+ @metrics[name.to_sym] = metric
30
28
  end
31
29
  end
32
30
 
33
- container
31
+ metric
34
32
  end
35
33
 
36
34
  def counter(name, docstring, base_labels = {})
37
- register(name, docstring, Counter.new, base_labels).metric
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, Summary.new, base_labels).metric
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, Gauge.new, base_labels).metric
43
+ register(Gauge.new(name, docstring, base_labels))
46
44
  end
47
45
 
48
46
  def exist?(name)
49
- @containers.has_key?(name)
47
+ @metrics.key?(name)
50
48
  end
51
49
 
52
50
  def get(name)
53
- @containers[name]
51
+ @metrics[name.to_sym]
54
52
  end
55
53
 
56
- def to_json(*json)
57
- @containers.values.to_json(*json)
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
- :histogram
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
- estimator = @values[label_set_for(labels)]
21
- estimator.invariants.inject({}) do |memo, invariant|
22
- memo[invariant.quantile] = estimator.query(invariant.quantile)
23
- memo
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
- private
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
@@ -1,5 +1,7 @@
1
+ # encoding: UTF-8
2
+
1
3
  module Prometheus
2
4
  module Client
3
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0-rc.1'
4
6
  end
5
7
  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.2.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: 2013-11-13 00:00:00.000000000 Z
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: &19137520 !ruby/object:Gem::Requirement
17
- none: false
15
+ requirement: !ruby/object:Gem::Requirement
18
16
  requirements:
19
17
  - - ~>
20
18
  - !ruby/object:Gem::Version
21
- version: 0.1.0
19
+ version: 0.2.0
22
20
  type: :runtime
23
21
  prerelease: false
24
- version_requirements: *19137520
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/client/counter.rb
34
- - lib/prometheus/client/gauge.rb
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/version.rb
41
- - lib/prometheus/client/label_set.rb
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: '0'
66
+ version: 1.3.1
64
67
  requirements: []
65
68
  rubyforge_project:
66
- rubygems_version: 1.8.5
69
+ rubygems_version: 2.1.11
67
70
  signing_key:
68
- specification_version: 3
69
- summary: A suite of instrumentation metric primitives for Ruby that can be exposed
70
- through a JSON web services interface.
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