prometheus-client-mmap 0.7.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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