prometheus-client-mmap 0.7.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,84 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'prometheus/client/metric'
4
+
5
+ module Prometheus
6
+ module Client
7
+ # A histogram samples observations (usually things like request durations
8
+ # or response sizes) and counts them in configurable buckets. It also
9
+ # provides a sum of all observed values.
10
+ class Histogram < Metric
11
+ # Value represents the state of a Histogram at a given point.
12
+ class Value < Hash
13
+ attr_accessor :sum, :total
14
+
15
+ def initialize(type, name, labels, buckets)
16
+ @sum = ValueClass.new(type, name, name.to_s + '_sum', labels)
17
+ # TODO: get rid of total and use +Inf bucket instead.
18
+ @total = ValueClass.new(type, name, name.to_s + '_count', labels)
19
+
20
+ buckets.each do |bucket|
21
+ # TODO: check that there are no user-defined "le" labels.
22
+ self[bucket] = ValueClass.new(type, name, name.to_s + '_bucket', labels.merge({'le' => bucket.to_s}))
23
+ end
24
+ end
25
+
26
+ def observe(value)
27
+ @sum.increment(value)
28
+ @total.increment()
29
+
30
+ each_key do |bucket|
31
+ self[bucket].increment() if value <= bucket
32
+ end
33
+ end
34
+
35
+ def get()
36
+ hash = {}
37
+ each_key do |bucket|
38
+ hash[bucket] = self[bucket].get()
39
+ end
40
+ hash
41
+ end
42
+ end
43
+
44
+ # DEFAULT_BUCKETS are the default Histogram buckets. The default buckets
45
+ # are tailored to broadly measure the response time (in seconds) of a
46
+ # network service. (From DefBuckets client_golang)
47
+ DEFAULT_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1,
48
+ 2.5, 5, 10].freeze
49
+
50
+ # Offer a way to manually specify buckets
51
+ def initialize(name, docstring, base_labels = {},
52
+ buckets = DEFAULT_BUCKETS)
53
+ raise ArgumentError, 'Unsorted buckets, typo?' unless sorted? buckets
54
+
55
+ @buckets = buckets
56
+ super(name, docstring, base_labels)
57
+ end
58
+
59
+ def type
60
+ :histogram
61
+ end
62
+
63
+ def observe(labels, value)
64
+ if labels[:le]
65
+ raise ArgumentError, 'Label with name "le" is not permitted'
66
+ end
67
+
68
+ label_set = label_set_for(labels)
69
+ synchronize { @values[label_set].observe(value) }
70
+ end
71
+
72
+ private
73
+
74
+ def default(labels)
75
+ # TODO: default function needs to know key of hash info (label names and values)
76
+ Value.new(type, @name, labels, @buckets)
77
+ end
78
+
79
+ def sorted?(bucket)
80
+ bucket.each_cons(2).all? { |i, j| i <= j }
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,69 @@
1
+ # encoding: UTF-8
2
+
3
+ module Prometheus
4
+ module Client
5
+ # LabelSetValidator ensures that all used label sets comply with the
6
+ # Prometheus specification.
7
+ class LabelSetValidator
8
+ # TODO: we might allow setting :instance in the future
9
+ RESERVED_LABELS = [:job, :instance].freeze
10
+
11
+ class LabelSetError < StandardError; end
12
+ class InvalidLabelSetError < LabelSetError; end
13
+ class InvalidLabelError < LabelSetError; end
14
+ class ReservedLabelError < LabelSetError; end
15
+
16
+ def initialize
17
+ @validated = {}
18
+ end
19
+
20
+ def valid?(labels)
21
+ unless labels.respond_to?(:all?)
22
+ raise InvalidLabelSetError, "#{labels} is not a valid label set"
23
+ end
24
+
25
+ labels.all? do |key, _|
26
+ validate_symbol(key)
27
+ validate_name(key)
28
+ validate_reserved_key(key)
29
+ end
30
+ end
31
+
32
+ def validate(labels)
33
+ return labels if @validated.key?(labels.hash)
34
+
35
+ valid?(labels)
36
+
37
+ unless @validated.empty? || match?(labels, @validated.first.last)
38
+ raise InvalidLabelSetError, 'labels must have the same signature'
39
+ end
40
+
41
+ @validated[labels.hash] = labels
42
+ end
43
+
44
+ private
45
+
46
+ def match?(a, b)
47
+ a.keys.sort == b.keys.sort
48
+ end
49
+
50
+ def validate_symbol(key)
51
+ return true if key.is_a?(Symbol)
52
+
53
+ raise InvalidLabelError, "label #{key} is not a symbol"
54
+ end
55
+
56
+ def validate_name(key)
57
+ return true unless key.to_s.start_with?('__')
58
+
59
+ raise ReservedLabelError, "label #{key} must not start with __"
60
+ end
61
+
62
+ def validate_reserved_key(key)
63
+ return true unless RESERVED_LABELS.include?(key)
64
+
65
+ raise ReservedLabelError, "#{key} is reserved"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,70 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'thread'
4
+ require 'prometheus/client/label_set_validator'
5
+ require 'prometheus/client/valuetype'
6
+
7
+ module Prometheus
8
+ module Client
9
+ # Metric
10
+ class Metric
11
+ attr_reader :name, :docstring, :base_labels
12
+
13
+ def initialize(name, docstring, base_labels = {})
14
+ @mutex = Mutex.new
15
+ @validator = LabelSetValidator.new
16
+ @values = Hash.new { |hash, key| hash[key] = default(key) }
17
+
18
+ validate_name(name)
19
+ validate_docstring(docstring)
20
+ @validator.valid?(base_labels)
21
+
22
+ @name = name
23
+ @docstring = docstring
24
+ @base_labels = base_labels
25
+ end
26
+
27
+ # Returns the value for the given label set
28
+ def get(labels = {})
29
+ @validator.valid?(labels)
30
+
31
+ @values[labels].get
32
+ end
33
+
34
+ # Returns all label sets with their values
35
+ def values
36
+ synchronize do
37
+ @values.each_with_object({}) do |(labels, value), memo|
38
+ memo[labels] = value
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def default(labels)
46
+ ValueClass.new(type, @name, @name, labels)
47
+ end
48
+
49
+ def validate_name(name)
50
+ return true if name.is_a?(Symbol)
51
+
52
+ raise ArgumentError, 'given name must be a symbol'
53
+ end
54
+
55
+ def validate_docstring(docstring)
56
+ return true if docstring.respond_to?(:empty?) && !docstring.empty?
57
+
58
+ raise ArgumentError, 'docstring must be given'
59
+ end
60
+
61
+ def label_set_for(labels)
62
+ @validator.validate(labels)
63
+ end
64
+
65
+ def synchronize(&block)
66
+ @mutex.synchronize(&block)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,72 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+
6
+ require 'prometheus/client'
7
+ require 'prometheus/client/formats/text'
8
+
9
+ module Prometheus
10
+ # Client is a ruby implementation for a Prometheus compatible client.
11
+ module Client
12
+ # Push implements a simple way to transmit a given registry to a given
13
+ # Pushgateway.
14
+ class Push
15
+ DEFAULT_GATEWAY = 'http://localhost:9091'.freeze
16
+ PATH = '/metrics/jobs/%s'.freeze
17
+ INSTANCE_PATH = '/metrics/jobs/%s/instances/%s'.freeze
18
+ HEADER = { 'Content-Type' => Formats::Text::CONTENT_TYPE }.freeze
19
+
20
+ attr_reader :job, :instance, :gateway, :path
21
+
22
+ def initialize(job, instance = nil, gateway = nil)
23
+ @job = job
24
+ @instance = instance
25
+ @gateway = gateway || DEFAULT_GATEWAY
26
+ @uri = parse(@gateway)
27
+ @path = build_path(job, instance)
28
+ @http = Net::HTTP.new(@uri.host, @uri.port)
29
+ end
30
+
31
+ def add(registry)
32
+ request('POST', registry)
33
+ end
34
+
35
+ def replace(registry)
36
+ request('PUT', registry)
37
+ end
38
+
39
+ def delete
40
+ @http.send_request('DELETE', path)
41
+ end
42
+
43
+ private
44
+
45
+ def parse(url)
46
+ uri = URI.parse(url)
47
+
48
+ if uri.scheme == 'http'
49
+ uri
50
+ else
51
+ raise ArgumentError, 'only HTTP gateway URLs are supported currently.'
52
+ end
53
+ rescue URI::InvalidURIError => e
54
+ raise ArgumentError, "#{url} is not a valid URL: #{e}"
55
+ end
56
+
57
+ def build_path(job, instance)
58
+ if instance
59
+ format(INSTANCE_PATH, URI.escape(job), URI.escape(instance))
60
+ else
61
+ format(PATH, URI.escape(job))
62
+ end
63
+ end
64
+
65
+ def request(method, registry)
66
+ data = Formats::Text.marshal(registry)
67
+
68
+ @http.send_request(method, path, data, HEADER)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,82 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'prometheus/client'
4
+
5
+ module Prometheus
6
+ module Client
7
+ module Rack
8
+ # Collector is a Rack middleware that provides a sample implementation of
9
+ # a HTTP tracer. The default label builder can be modified to export a
10
+ # different set of labels per recorded metric.
11
+ class Collector
12
+ attr_reader :app, :registry
13
+
14
+ def initialize(app, options = {}, &label_builder)
15
+ @app = app
16
+ @registry = options[:registry] || Client.registry
17
+ @label_builder = label_builder || DEFAULT_LABEL_BUILDER
18
+
19
+ init_request_metrics
20
+ init_exception_metrics
21
+ end
22
+
23
+ def call(env) # :nodoc:
24
+ trace(env) { @app.call(env) }
25
+ end
26
+
27
+ protected
28
+
29
+ DEFAULT_LABEL_BUILDER = proc do |env|
30
+ {
31
+ method: env['REQUEST_METHOD'].downcase,
32
+ host: env['HTTP_HOST'].to_s,
33
+ path: env['PATH_INFO'].to_s,
34
+ }
35
+ end
36
+
37
+ def init_request_metrics
38
+ @requests = @registry.counter(
39
+ :http_requests_total,
40
+ 'A counter of the total number of HTTP requests made.',
41
+ )
42
+ @durations = @registry.summary(
43
+ :http_request_duration_seconds,
44
+ 'A histogram of the response latency.',
45
+ )
46
+ end
47
+
48
+ def init_exception_metrics
49
+ @exceptions = @registry.counter(
50
+ :http_exceptions_total,
51
+ 'A counter of the total number of exceptions raised.',
52
+ )
53
+ end
54
+
55
+ def trace(env)
56
+ start = Time.now
57
+ yield.tap do |response|
58
+ duration = (Time.now - start).to_f
59
+ record(labels(env, response), duration)
60
+ end
61
+ rescue => exception
62
+ @exceptions.increment(exception: exception.class.name)
63
+ raise
64
+ end
65
+
66
+ def labels(env, response)
67
+ @label_builder.call(env).tap do |labels|
68
+ labels[:code] = response.first.to_s
69
+ end
70
+ end
71
+
72
+ def record(labels, duration)
73
+ @requests.increment(labels)
74
+ @durations.observe(labels, duration)
75
+ rescue
76
+ # TODO: log unexpected exception during request recording
77
+ nil
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,91 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'prometheus/client'
4
+ require 'prometheus/client/formats/text'
5
+
6
+ module Prometheus
7
+ module Client
8
+ module Rack
9
+ # Exporter is a Rack middleware that provides a sample implementation of
10
+ # a Prometheus HTTP client API.
11
+ class Exporter
12
+ attr_reader :app, :registry, :path
13
+
14
+ FORMATS = [Formats::Text].freeze
15
+ FALLBACK = Formats::Text
16
+
17
+ def initialize(app, options = {})
18
+ @app = app
19
+ @registry = options[:registry] || Client.registry
20
+ @path = options[:path] || '/metrics'
21
+ @acceptable = build_dictionary(FORMATS, FALLBACK)
22
+ end
23
+
24
+ def call(env)
25
+ if env['PATH_INFO'] == @path
26
+ format = negotiate(env['HTTP_ACCEPT'], @acceptable)
27
+ format ? respond_with(format) : not_acceptable(FORMATS)
28
+ else
29
+ @app.call(env)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def negotiate(accept, formats)
36
+ accept = '*/*' if accept.to_s.empty?
37
+
38
+ parse(accept).each do |content_type, _|
39
+ return formats[content_type] if formats.key?(content_type)
40
+ end
41
+
42
+ nil
43
+ end
44
+
45
+ def parse(header)
46
+ header.to_s.split(/\s*,\s*/).map do |type|
47
+ attributes = type.split(/\s*;\s*/)
48
+ quality = extract_quality(attributes)
49
+
50
+ [attributes.join('; '), quality]
51
+ end.sort_by(&:last).reverse
52
+ end
53
+
54
+ def extract_quality(attributes, default = 1.0)
55
+ quality = default
56
+
57
+ attributes.delete_if do |attr|
58
+ quality = attr.split('q=').last.to_f if attr.start_with?('q=')
59
+ end
60
+
61
+ quality
62
+ end
63
+
64
+ def respond_with(format)
65
+ [
66
+ 200,
67
+ { 'Content-Type' => format::CONTENT_TYPE },
68
+ [format.marshal(@registry)],
69
+ ]
70
+ end
71
+
72
+ def not_acceptable(formats)
73
+ types = formats.map { |format| format::MEDIA_TYPE }
74
+
75
+ [
76
+ 406,
77
+ { 'Content-Type' => 'text/plain' },
78
+ ["Supported media types: #{types.join(', ')}"],
79
+ ]
80
+ end
81
+
82
+ def build_dictionary(formats, fallback)
83
+ formats.each_with_object('*/*' => fallback) do |format, memo|
84
+ memo[format::CONTENT_TYPE] = format
85
+ memo[format::MEDIA_TYPE] = format
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end