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.
@@ -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