All Files ( 91.74% covered at 1424.22 hits/line )
46 files in total.
1936 relevant lines,
1776 lines covered and
160 lines missed.
(
91.74%
)
- 1
require 'prometheus/client/registry'
- 1
require 'prometheus/client/configuration'
- 1
require 'prometheus/client/mmaped_value'
- 1
module Prometheus
# Client is a ruby implementation for a Prometheus compatible client.
- 1
module Client
- 1
class << self
- 1
attr_writer :configuration
- 1
def configuration
- 40695
@configuration ||= Configuration.new
end
- 1
def configure
yield(configuration)
end
# Returns a default registry object
- 1
def registry
- 8
@registry ||= Registry.new
end
- 1
def logger
- 3
configuration.logger
end
- 1
def pid
- 111
configuration.pid_provider.call
end
# Resets the registry and reinitializes all metrics files.
# Use case: clean up everything in specs `before` block,
# to prevent leaking the state between specs which are updating metrics.
- 1
def reset!
- 5
@registry = nil
- 5
::Prometheus::Client::MmapedValue.reset_and_reinitialize
end
- 1
def cleanup!
- 27
Dir.glob("#{configuration.multiprocess_files_dir}/*.db").each { |f| File.unlink(f) if File.exist?(f) }
end
# With `force: false`: reinitializes metric files only for processes with the changed PID.
# With `force: true`: reinitializes all metrics files.
# Always keeps the registry.
# Use case (`force: false`): pick up new metric files on each worker start,
# without resetting already registered files for the master or previously initialized workers.
- 1
def reinitialize_on_pid_change(force: false)
- 3
if force
- 1
::Prometheus::Client::MmapedValue.reset_and_reinitialize
else
- 2
::Prometheus::Client::MmapedValue.reinitialize_on_pid_change
end
end
end
end
end
- 1
require 'prometheus/client/registry'
- 1
require 'prometheus/client/mmaped_value'
- 1
require 'prometheus/client/page_size'
- 1
require 'logger'
- 1
require 'tmpdir'
- 1
module Prometheus
- 1
module Client
- 1
class Configuration
- 1
attr_accessor :value_class, :multiprocess_files_dir, :initial_mmap_file_size, :logger, :pid_provider, :rust_multiprocess_metrics
- 1
def initialize
- 1
@value_class = ::Prometheus::Client::MmapedValue
- 1
@initial_mmap_file_size = ::Prometheus::Client::PageSize.page_size(fallback_page_size: 4096)
- 1
@logger = Logger.new($stdout)
- 1
@pid_provider = Process.method(:pid)
- 1
@rust_multiprocess_metrics = ENV.fetch('prometheus_rust_multiprocess_metrics', 'true') == 'true'
- 1
@multiprocess_files_dir = ENV.fetch('prometheus_multiproc_dir') do
Dir.mktmpdir("prometheus-mmap")
end
end
end
end
end
# encoding: UTF-8
- 1
require 'prometheus/client/metric'
- 1
module Prometheus
- 1
module Client
# Counter is a metric that exposes merely a sum or tally of things.
- 1
class Counter < Metric
- 1
def type
- 80047
:counter
end
- 1
def increment(labels = {}, by = 1)
- 40120
raise ArgumentError, 'increment must be a non-negative number' if by < 0
- 40119
label_set = label_set_for(labels)
- 80238
synchronize { @values[label_set].increment(by) }
end
- 1
private
- 1
def default(labels)
- 40022
value_object(type, @name, @name, labels)
end
end
end
end
- 1
require 'prometheus/client/uses_value_type'
- 1
require 'prometheus/client/helper/json_parser'
- 1
require 'prometheus/client/helper/loader'
- 1
require 'prometheus/client/helper/plain_file'
- 1
require 'prometheus/client/helper/metrics_processing'
- 1
require 'prometheus/client/helper/metrics_representation'
- 1
module Prometheus
- 1
module Client
- 1
module Formats
# Text format is human readable mainly used for manual inspection.
- 1
module Text
- 1
MEDIA_TYPE = 'text/plain'.freeze
- 1
VERSION = '0.0.4'.freeze
- 1
CONTENT_TYPE = "#{MEDIA_TYPE}; version=#{VERSION}".freeze
- 1
class << self
- 1
def marshal(registry)
- 10
metrics = registry.metrics.map do |metric|
- 5
samples = metric.values.flat_map do |label_set, value|
- 7
representation(metric, label_set, value)
end
- 5
[metric.name, { type: metric.type, help: metric.docstring, samples: samples }]
end
- 10
Helper::MetricsRepresentation.to_text(metrics)
end
- 1
def marshal_multiprocess(path = Prometheus::Client.configuration.multiprocess_files_dir, use_rust: true)
- 4
file_list = Dir.glob(File.join(path, '*.db')).sort
- 20
.map {|f| Helper::PlainFile.new(f) }
- 20
.map {|f| [f.filepath, f.multiprocess_mode.to_sym, f.type.to_sym, f.pid] }
- 4
if use_rust && Prometheus::Client::Helper::Loader.rust_impl_available?
- 3
FastMmapedFileRs.to_metrics(file_list.to_a)
else
- 1
FastMmapedFile.to_metrics(file_list.to_a)
end
end
- 1
def rust_impl_available?
return @rust_available unless @rust_available.nil?
check_for_rust
end
- 1
private
- 1
def load_metrics(path)
metrics = {}
Dir.glob(File.join(path, '*.db')).sort.each do |f|
Helper::PlainFile.new(f).to_metrics(metrics)
end
metrics
end
- 1
def representation(metric, label_set, value)
- 7
labels = metric.base_labels.merge(label_set)
- 7
if metric.type == :summary
- 1
summary(metric.name, labels, value)
- 6
elsif metric.type == :histogram
- 1
histogram(metric.name, labels, value)
else
- 5
[[metric.name, labels, value.get]]
end
end
- 1
def summary(name, set, value)
- 1
rv = value.get.map do |q, v|
- 3
[name, set.merge(quantile: q), v]
end
- 1
rv << ["#{name}_sum", set, value.get.sum]
- 1
rv << ["#{name}_count", set, value.get.total]
- 1
rv
end
- 1
def histogram(name, set, value)
# |metric_name, labels, value|
- 1
rv = value.get.map do |q, v|
- 3
[name, set.merge(le: q), v]
end
- 1
rv << [name, set.merge(le: '+Inf'), value.get.total]
- 1
rv << ["#{name}_sum", set, value.get.sum]
- 1
rv << ["#{name}_count", set, value.get.total]
- 1
rv
end
end
end
end
end
end
# encoding: UTF-8
- 1
require 'prometheus/client/metric'
- 1
module Prometheus
- 1
module Client
# A Gauge is a metric that exposes merely an instantaneous value or some
# snapshot thereof.
- 1
class Gauge < Metric
- 1
def initialize(name, docstring, base_labels = {}, multiprocess_mode=:all)
- 39
super(name, docstring, base_labels)
- 36
if value_class.multiprocess and ![:min, :max, :livesum, :liveall, :all].include?(multiprocess_mode)
raise ArgumentError, 'Invalid multiprocess mode: ' + multiprocess_mode
end
- 36
@multiprocess_mode = multiprocess_mode
end
- 1
def type
- 80
:gauge
end
- 1
def default(labels)
- 40
value_object(type, @name, @name, labels, @multiprocess_mode)
end
# Sets the value for the given label set
- 1
def set(labels, value)
- 34
@values[label_set_for(labels)].set(value)
end
- 1
def increment(labels, value)
- 3
@values[label_set_for(labels)].increment(value)
end
- 1
def decrement(labels, value)
- 2
@values[label_set_for(labels)].decrement(value)
end
end
end
end
- 1
require 'prometheus/client/helper/json_parser'
- 1
module Prometheus
- 1
module Client
- 1
module Helper
- 1
module EntryParser
- 1
class ParsingError < RuntimeError;
end
- 1
MINIMUM_SIZE = 8
- 1
START_POSITION = 8
- 1
VALUE_BYTES = 8
- 1
ENCODED_LENGTH_BYTES = 4
- 1
def used
- 107
slice(0..3).unpack('l')[0]
end
- 1
def parts
- 60
@parts ||= File.basename(filepath, '.db')
.split('_')
- 78
.map { |e| e.gsub(/-\d+$/, '') } # remove trailing -number
end
- 1
def type
- 20
parts[0].to_sym
end
- 1
def pid
- 20
(parts[2..-1] || []).join('_')
end
- 1
def multiprocess_mode
- 20
parts[1]
end
- 1
def empty?
- 95
size < MINIMUM_SIZE || used.zero?
end
- 1
def entries(ignore_errors = false)
- 95
return Enumerator.new {} if empty?
- 12
Enumerator.new do |yielder|
- 12
used_ = used # cache used to avoid unnecessary unpack operations
- 12
pos = START_POSITION # used + padding offset
- 12
while pos < used_ && pos < size && pos > 0
- 410
data = slice(pos..-1)
- 410
unless data
raise ParsingError, "data slice is nil at pos #{pos}" unless ignore_errors
pos += 8
next
end
- 410
encoded_len, first_encoded_bytes = data.unpack('LL')
- 410
if encoded_len.nil? || encoded_len.zero? || first_encoded_bytes.nil? || first_encoded_bytes.zero?
# do not parse empty data
pos += 8
next
end
- 410
entry_len = ENCODED_LENGTH_BYTES + encoded_len
- 410
padding_len = 8 - entry_len % 8
- 410
value_offset = entry_len + padding_len # align to 8 bytes
- 410
pos += value_offset
- 410
if value_offset > 0 && (pos + VALUE_BYTES) <= size # if positions are safe
- 410
yielder.yield data, encoded_len, value_offset, pos
else
raise ParsingError, "data slice is nil at pos #{pos}" unless ignore_errors
end
- 410
pos += VALUE_BYTES
end
end
end
- 1
def parsed_entries(ignore_errors = false)
result = entries(ignore_errors).map do |data, encoded_len, value_offset, _|
begin
encoded, value = data.unpack(format('@4A%d@%dd', encoded_len, value_offset))
[encoded, value]
rescue ArgumentError => e
Prometheus::Client.logger.debug("Error processing data: #{bin_to_hex(data[0, 7])} len: #{encoded_len} value_offset: #{value_offset}")
raise ParsingError, e unless ignore_errors
end
end
result.reject!(&:nil?) if ignore_errors
result
end
- 1
def to_metrics(metrics = {}, ignore_errors = false)
parsed_entries(ignore_errors).each do |key, value|
begin
metric_name, name, labelnames, labelvalues = JsonParser.load(key)
labelnames ||= []
labelvalues ||= []
metric = metrics.fetch(metric_name,
metric_name: metric_name,
help: 'Multiprocess metric',
type: type,
samples: [])
if type == :gauge
metric[:multiprocess_mode] = multiprocess_mode
metric[:samples] += [[name, labelnames.zip(labelvalues) + [['pid', pid]], value]]
else
# The duplicates and labels are fixed in the next for.
metric[:samples] += [[name, labelnames.zip(labelvalues), value]]
end
metrics[metric_name] = metric
rescue JSON::ParserError => e
raise ParsingError(e) unless ignore_errors
end
end
metrics.reject! { |e| e.nil? } if ignore_errors
metrics
end
- 1
private
- 1
def bin_to_hex(s)
s.each_byte.map { |b| b.to_s(16) }.join
end
end
end
end
end
- 1
module Prometheus
- 1
module Client
- 1
module Helper
- 1
class FileLocker
- 1
class << self
- 1
LOCK_FILE_MUTEX = Mutex.new
- 1
def lock_to_process(filepath)
- 111
LOCK_FILE_MUTEX.synchronize do
- 111
@file_locks ||= {}
- 111
return false if @file_locks[filepath]
- 99
file = File.open(filepath, 'ab')
- 99
if file.flock(File::LOCK_NB | File::LOCK_EX)
- 99
@file_locks[filepath] = file
- 99
return true
else
return false
end
end
end
- 1
def unlock(filepath)
- 65
LOCK_FILE_MUTEX.synchronize do
- 65
@file_locks ||= {}
- 65
return false unless @file_locks[filepath]
- 49
file = @file_locks[filepath]
- 49
file.flock(File::LOCK_UN)
- 49
file.close
- 49
@file_locks.delete(filepath)
end
end
- 1
def unlock_all
LOCK_FILE_MUTEX.synchronize do
@file_locks ||= {}
@file_locks.values.each do |file|
file.flock(File::LOCK_UN)
file.close
end
@file_locks = {}
end
end
end
end
end
end
end
- 1
require 'json'
- 1
module Prometheus
- 1
module Client
- 1
module Helper
- 1
module JsonParser
- 1
class << self
- 1
if defined?(Oj)
def load(s)
Oj.load(s)
rescue Oj::ParseError, EncodingError => e
raise JSON::ParserError.new(e.message)
end
else
- 1
def load(s)
- 2
JSON.parse(s)
end
end
end
end
end
end
end
- 1
module Prometheus
- 1
module Client
- 1
module Helper
- 1
module Loader
- 1
class << self
- 1
def rust_impl_available?
- 3
return @rust_available unless @rust_available.nil?
- 1
check_for_rust
end
- 1
private
- 1
def load_rust_extension
begin
- 1
ruby_version = /(\d+\.\d+)/.match(RUBY_VERSION)
- 1
require_relative "../../../#{ruby_version}/fast_mmaped_file_rs"
- 1
rescue LoadError
- 1
require 'fast_mmaped_file_rs'
end
end
- 1
def check_for_rust
# This will be evaluated on each invocation even with `||=` if
# `@rust_available` if false. Running a `require` statement is slow,
# so the `rust_impl_available?` method memoizes the result, external
# callers can only trigger this method a single time.
@rust_available = begin
- 1
load_rust_extension
- 1
true
rescue LoadError
warn <<~WARN
WARNING: The Rust extension for prometheus-client-mmap is unavailable, falling back to the legacy C extension.
The Rust extension will be required in the next version. If you are compiling this gem from source,
ensure your build system has a Rust compiler and clang: https://gitlab.com/gitlab-org/ruby/gems/prometheus-client-mmap
WARN
false
end
end
end
end
end
end
end
- 1
module Prometheus
- 1
module Client
- 1
module Helper
- 1
module MetricsProcessing
- 1
def self.merge_metrics(metrics)
metrics.each_value do |metric|
metric[:samples] = merge_samples(metric[:samples], metric[:type], metric[:multiprocess_mode]).map do |(name, labels), value|
[name, labels.to_h, value]
end
end
end
- 1
def self.merge_samples(raw_samples, metric_type, multiprocess_mode)
samples = {}
raw_samples.each do |name, labels, value|
without_pid = labels.reject { |l| l[0] == 'pid' }
case metric_type
when :gauge
case multiprocess_mode
when 'min'
s = samples.fetch([name, without_pid], value)
samples[[name, without_pid]] = [s, value].min
when 'max'
s = samples.fetch([name, without_pid], value)
samples[[name, without_pid]] = [s, value].max
when 'livesum'
s = samples.fetch([name, without_pid], 0.0)
samples[[name, without_pid]] = s + value
else # all/liveall
samples[[name, labels]] = value
end
else
# Counter, Histogram and Summary.
s = samples.fetch([name, without_pid], 0.0)
samples[[name, without_pid]] = s + value
end
end
samples
end
end
end
end
end
- 1
module Prometheus
- 1
module Client
- 1
module Helper
- 1
module MetricsRepresentation
- 1
METRIC_LINE = '%s%s %s'.freeze
- 1
TYPE_LINE = '# TYPE %s %s'.freeze
- 1
HELP_LINE = '# HELP %s %s'.freeze
- 1
LABEL = '%s="%s"'.freeze
- 1
SEPARATOR = ','.freeze
- 1
DELIMITER = "\n".freeze
- 1
REGEX = { doc: /[\n\\]/, label: /[\n\\"]/ }.freeze
- 1
REPLACE = { "\n" => '\n', '\\' => '\\\\', '"' => '\"' }.freeze
- 1
def self.to_text(metrics)
- 10
lines = []
- 10
metrics.each do |name, metric|
- 5
lines << format(HELP_LINE, name, escape(metric[:help]))
- 5
lines << format(TYPE_LINE, name, metric[:type])
- 5
metric[:samples].each do |metric_name, labels, value|
- 16
lines << metric(metric_name, format_labels(labels), value)
end
end
# there must be a trailing delimiter
- 10
(lines << nil).join(DELIMITER)
end
- 1
def self.metric(name, labels, value)
- 16
format(METRIC_LINE, name, labels, value)
end
- 1
def self.format_labels(set)
- 16
return if set.empty?
- 16
strings = set.each_with_object([]) do |(key, value), memo|
- 35
memo << format(LABEL, key, escape(value, :label))
end
- 16
"{#{strings.join(SEPARATOR)}}"
end
- 1
def self.escape(string, format = :doc)
- 40
string.to_s.gsub(REGEX[format], REPLACE)
end
end
end
end
end
- 1
require 'prometheus/client/helper/entry_parser'
- 1
require 'prometheus/client/helper/file_locker'
- 1
require 'prometheus/client/helper/loader'
# load precompiled extension if available
begin
- 1
ruby_version = /(\d+\.\d+)/.match(RUBY_VERSION)
- 1
require_relative "../../../#{ruby_version}/fast_mmaped_file"
rescue LoadError
- 1
require 'fast_mmaped_file'
end
- 1
module Prometheus
- 1
module Client
- 1
module Helper
# We can't check `Prometheus::Client.configuration` as this creates a circular dependency
- 1
if (ENV.fetch('prometheus_rust_mmaped_file', 'true') == "true" &&
Prometheus::Client::Helper::Loader.rust_impl_available?)
class MmapedFile < FastMmapedFileRs
end
else
- 1
class MmapedFile < FastMmapedFile
end
end
- 1
class MmapedFile
- 1
include EntryParser
- 1
attr_reader :filepath, :size
- 1
def initialize(filepath)
- 105
@filepath = filepath
- 105
File.open(filepath, 'a+b') do |file|
- 105
file.truncate(initial_mmap_file_size) if file.size < MINIMUM_SIZE
- 105
@size = file.size
end
- 105
super(filepath)
end
- 1
def close
- 59
munmap
- 59
FileLocker.unlock(filepath)
end
- 1
private
- 1
def initial_mmap_file_size
- 93
Prometheus::Client.configuration.initial_mmap_file_size
end
- 1
public
- 1
class << self
- 1
def open(filepath)
- 105
MmapedFile.new(filepath)
end
- 1
def ensure_exclusive_file(file_prefix = 'mmaped_file')
- 99
(0..Float::INFINITY).lazy
- 111
.map { |f_num| "#{file_prefix}_#{Prometheus::Client.pid}-#{f_num}.db" }
- 111
.map { |filename| File.join(Prometheus::Client.configuration.multiprocess_files_dir, filename) }
- 111
.find { |path| Helper::FileLocker.lock_to_process(path) }
end
- 1
def open_exclusive_file(file_prefix = 'mmaped_file')
- 80
filename = Helper::MmapedFile.ensure_exclusive_file(file_prefix)
- 80
open(filename)
end
end
end
end
end
end
- 1
require 'prometheus/client/helper/entry_parser'
- 1
module Prometheus
- 1
module Client
- 1
module Helper
# Parses DB files without using mmap
- 1
class PlainFile
- 1
include EntryParser
- 1
attr_reader :filepath
- 1
def source
- 408
@data ||= File.read(filepath, mode: 'rb')
end
- 1
def initialize(filepath)
- 23
@filepath = filepath
end
- 1
def slice(*args)
- 139
source.slice(*args)
end
- 1
def size
- 269
source.length
end
end
end
end
end
- 1
require 'prometheus/client/metric'
- 1
require 'prometheus/client/uses_value_type'
- 1
module Prometheus
- 1
module Client
# A histogram samples observations (usually things like request durations
# or response sizes) and counts them in configurable buckets. It also
# provides a sum of all observed values.
- 1
class Histogram < Metric
# Value represents the state of a Histogram at a given point.
- 1
class Value < Hash
- 1
include UsesValueType
- 1
attr_accessor :sum, :total, :total_inf
- 1
def initialize(type, name, labels, buckets)
- 11
@sum = value_object(type, name, "#{name}_sum", labels)
- 11
@total = value_object(type, name, "#{name}_count", labels)
- 11
@total_inf = value_object(type, name, "#{name}_bucket", labels.merge(le: "+Inf"))
- 11
buckets.each do |bucket|
- 113
self[bucket] = value_object(type, name, "#{name}_bucket", labels.merge(le: bucket.to_s))
end
end
- 1
def observe(value)
- 9
@sum.increment(value)
- 9
@total.increment()
- 9
@total_inf.increment()
- 9
each_key do |bucket|
- 91
self[bucket].increment() if value <= bucket
end
end
- 1
def get()
- 4
hash = {}
- 4
each_key do |bucket|
- 28
hash[bucket] = self[bucket].get()
end
- 4
hash
end
end
# DEFAULT_BUCKETS are the default Histogram buckets. The default buckets
# are tailored to broadly measure the response time (in seconds) of a
# network service. (From DefBuckets client_golang)
- 1
DEFAULT_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1,
2.5, 5, 10].freeze
# Offer a way to manually specify buckets
- 1
def initialize(name, docstring, base_labels = {},
buckets = DEFAULT_BUCKETS)
- 20
raise ArgumentError, 'Unsorted buckets, typo?' unless sorted? buckets
- 19
@buckets = buckets
- 19
super(name, docstring, base_labels)
end
- 1
def type
- 31
:histogram
end
- 1
def observe(labels, value)
- 9
label_set = label_set_for(labels)
- 18
synchronize { @values[label_set].observe(value) }
end
- 1
private
- 1
def default(labels)
# TODO: default function needs to know key of hash info (label names and values)
- 11
Value.new(type, @name, labels, @buckets)
end
- 1
def sorted?(bucket)
- 203
bucket.each_cons(2).all? { |i, j| i <= j }
end
end
end
end
# encoding: UTF-8
- 1
module Prometheus
- 1
module Client
# LabelSetValidator ensures that all used label sets comply with the
# Prometheus specification.
- 1
class LabelSetValidator
- 1
RESERVED_LABELS = [].freeze
- 1
class LabelSetError < StandardError; end
- 1
class InvalidLabelSetError < LabelSetError; end
- 1
class InvalidLabelError < LabelSetError; end
- 1
class ReservedLabelError < LabelSetError; end
- 1
def initialize(reserved_labels = [])
- 40133
@reserved_labels = (reserved_labels + RESERVED_LABELS).freeze
- 40133
@validated = {}
end
- 1
def valid?(labels)
- 80255
unless labels.is_a?(Hash)
- 2
raise InvalidLabelSetError, "#{labels} is not a valid label set"
end
- 80253
labels.all? do |key, value|
- 225
validate_symbol(key)
- 223
validate_name(key)
- 218
validate_reserved_key(key)
- 217
validate_value(key, value)
end
end
- 1
def validate(labels)
- 40248
return labels if @validated.key?(labels.hash)
- 40109
valid?(labels)
- 40107
unless @validated.empty? || match?(labels, @validated.first.last)
- 1
raise InvalidLabelSetError, "labels must have the same signature: (#{label_diff(labels, @validated.first.last)})"
end
- 40106
@validated[labels.hash] = labels
end
- 1
private
- 1
def label_diff(a, b)
- 1
"expected keys: #{b.keys.sort}, got: #{a.keys.sort}"
end
- 1
def match?(a, b)
- 21
a.keys.sort == b.keys.sort
end
- 1
def validate_symbol(key)
- 225
return true if key.is_a?(Symbol)
- 2
raise InvalidLabelError, "label #{key} is not a symbol"
end
- 1
def validate_name(key)
- 223
return true unless key.to_s.start_with?('__')
- 5
raise ReservedLabelError, "label #{key} must not start with __"
end
- 1
def validate_reserved_key(key)
- 218
return true unless @reserved_labels.include?(key)
- 1
raise ReservedLabelError, "#{key} is reserved"
end
- 1
def validate_value(key, value)
- 217
return true if value.is_a?(String) ||
value.is_a?(Numeric) ||
value.is_a?(Symbol) ||
value.is_a?(FalseClass) ||
value.is_a?(TrueClass) ||
value.nil?
- 1
raise InvalidLabelError, "#{key} does not contain a valid value (type #{value.class})"
end
end
end
end
- 1
require 'thread'
- 1
require 'prometheus/client/label_set_validator'
- 1
require 'prometheus/client/uses_value_type'
- 1
module Prometheus
- 1
module Client
- 1
class Metric
- 1
include UsesValueType
- 1
attr_reader :name, :docstring, :base_labels
- 1
def initialize(name, docstring, base_labels = {})
- 40100
@mutex = Mutex.new
- 40100
@validator = case type
when :summary
- 18
LabelSetValidator.new(['quantile'])
when :histogram
- 19
LabelSetValidator.new(['le'])
else
- 40063
LabelSetValidator.new
end
- 80184
@values = Hash.new { |hash, key| hash[key] = default(key) }
- 40100
validate_name(name)
- 40096
validate_docstring(docstring)
- 40092
@validator.valid?(base_labels)
- 40087
@name = name
- 40087
@docstring = docstring
- 40087
@base_labels = base_labels
end
# Returns the value for the given label set
- 1
def get(labels = {})
- 45
label_set = label_set_for(labels)
- 45
@validator.valid?(label_set)
- 45
@values[label_set].get
end
# Returns all label sets with their values
- 1
def values
- 1
synchronize do
- 1
@values.each_with_object({}) do |(labels, value), memo|
- 1
memo[labels] = value
end
end
end
- 1
private
- 1
def touch_default_value
@values[label_set_for({})]
end
- 1
def default(labels)
value_object(type, @name, @name, labels)
end
- 1
def validate_name(name)
- 40100
return true if name.is_a?(Symbol)
- 4
raise ArgumentError, 'given name must be a symbol'
end
- 1
def validate_docstring(docstring)
- 40096
return true if docstring.respond_to?(:empty?) && !docstring.empty?
- 4
raise ArgumentError, 'docstring must be given'
end
- 1
def label_set_for(labels)
- 40221
@validator.validate(@base_labels.merge(labels))
end
- 1
def synchronize(&block)
- 40142
@mutex.synchronize(&block)
end
end
end
end
- 1
require 'prometheus/client/helper/mmaped_file'
- 1
require 'prometheus/client/helper/plain_file'
- 1
require 'prometheus/client'
- 1
module Prometheus
- 1
module Client
- 1
class ParsingError < StandardError
end
# A dict of doubles, backed by an mmapped file.
#
# The file starts with a 4 byte int, indicating how much of it is used.
# Then 4 bytes of padding.
# There's then a number of entries, consisting of a 4 byte int which is the
# size of the next field, a utf-8 encoded string key, padding to an 8 byte
# alignment, and then a 8 byte float which is the value.
- 1
class MmapedDict
- 1
attr_reader :m, :used, :positions
- 1
def self.read_all_values(f)
- 3
Helper::PlainFile.new(f).entries.map do |data, encoded_len, value_offset, _|
- 133
encoded, value = data.unpack(format('@4A%d@%dd', encoded_len, value_offset))
- 133
[encoded, value]
end
end
- 1
def initialize(m)
- 92
@mutex = Mutex.new
- 92
@m = m
# @m.mlock # TODO: Ensure memory is locked to RAM
- 92
@positions = {}
- 92
read_all_positions.each do |key, pos|
- 277
@positions[key] = pos
end
rescue StandardError => e
raise ParsingError, "exception #{e} while processing metrics file #{path}"
end
- 1
def read_value(key)
- 40594
@m.fetch_entry(@positions, key, 0.0)
end
- 1
def write_value(key, value)
- 41343
@m.upsert_entry(@positions, key, value)
end
- 1
def path
- 1
@m.filepath if @m
end
- 1
def close
- 47
@m.sync
- 47
@m.close
rescue TypeError => e
Prometheus::Client.logger.warn("munmap raised error #{e}")
end
- 1
def inspect
- 2
"#<#{self.class}:0x#{(object_id << 1).to_s(16)}>"
end
- 1
private
- 1
def init_value(key)
@m.add_entry(@positions, key, 0.0)
end
# Yield (key, pos). No locking is performed.
- 1
def read_all_positions
- 92
@m.entries.map do |data, encoded_len, _, absolute_pos|
- 277
encoded, = data.unpack(format('@4A%d', encoded_len))
- 277
[encoded, absolute_pos]
end
end
end
end
end
- 1
require 'prometheus/client'
- 1
require 'prometheus/client/mmaped_dict'
- 1
require 'json'
- 1
module Prometheus
- 1
module Client
# A float protected by a mutex backed by a per-process mmaped file.
- 1
class MmapedValue
- 1
VALUE_LOCK = Mutex.new
- 1
@@files = {}
- 1
@@pid = -1
- 1
def initialize(type, metric_name, name, labels, multiprocess_mode = '')
- 40170
@file_prefix = type.to_s
- 40170
@metric_name = metric_name
- 40170
@name = name
- 40170
@labels = labels
- 40170
if type == :gauge
- 34
@file_prefix += '_' + multiprocess_mode.to_s
end
- 40170
@pid = -1
- 40170
@mutex = Mutex.new
- 40170
initialize_file
end
- 1
def increment(amount = 1)
- 40170
@mutex.synchronize do
- 40170
initialize_file if pid_changed?
- 40170
@value += amount
- 40170
write_value(@key, @value)
- 40170
@value
end
end
- 1
def decrement(amount = 1)
increment(-amount)
end
- 1
def set(value)
- 32
@mutex.synchronize do
- 32
initialize_file if pid_changed?
- 32
@value = value
- 32
write_value(@key, @value)
- 32
@value
end
end
- 1
def get
- 64
@mutex.synchronize do
- 64
initialize_file if pid_changed?
- 64
return @value
end
end
- 1
def pid_changed?
- 40332
@pid != Process.pid
end
# method needs to be run in VALUE_LOCK mutex
- 1
def unsafe_reinitialize_file(check_pid = true)
- 468
unsafe_initialize_file if !check_pid || pid_changed?
end
- 1
def self.reset_and_reinitialize
- 8
VALUE_LOCK.synchronize do
- 8
@@pid = Process.pid
- 8
@@files = {}
- 8
ObjectSpace.each_object(MmapedValue).each do |v|
- 402
v.unsafe_reinitialize_file(false)
end
end
end
- 1
def self.reset_on_pid_change
- 40597
if pid_changed?
- 11
@@pid = Process.pid
- 11
@@files = {}
end
end
- 1
def self.reinitialize_on_pid_change
- 3
VALUE_LOCK.synchronize do
- 3
reset_on_pid_change
- 3
ObjectSpace.each_object(MmapedValue, &:unsafe_reinitialize_file)
end
end
- 1
def self.pid_changed?
- 40597
@@pid != Process.pid
end
- 1
def self.multiprocess
- 26
true
end
- 1
private
- 1
def initialize_file
- 40175
VALUE_LOCK.synchronize do
- 40175
unsafe_initialize_file
end
end
- 1
def unsafe_initialize_file
- 40594
self.class.reset_on_pid_change
- 40594
@pid = Process.pid
- 40594
unless @@files.has_key?(@file_prefix)
- 80
unless @file.nil?
- 41
@file.close
end
- 80
mmaped_file = Helper::MmapedFile.open_exclusive_file(@file_prefix)
- 80
@@files[@file_prefix] = MmapedDict.new(mmaped_file)
end
- 40594
@file = @@files[@file_prefix]
- 40594
@key = rebuild_key
- 40594
@value = read_value(@key)
end
- 1
def rebuild_key
- 40594
keys = @labels.keys.sort
- 40594
values = @labels.values_at(*keys)
- 40594
[@metric_name, @name, keys, values].to_json
end
- 1
def write_value(key, val)
- 40202
@file.write_value(key, val)
rescue StandardError => e
- 1
Prometheus::Client.logger.warn("writing value to #{@file.path} failed with #{e}")
- 1
Prometheus::Client.logger.debug(e.backtrace.join("\n"))
end
- 1
def read_value(key)
- 40594
@file.read_value(key)
rescue StandardError => e
Prometheus::Client.logger.warn("reading value from #{@file.path} failed with #{e}")
Prometheus::Client.logger.debug(e.backtrace.join("\n"))
0
end
end
end
end
- 1
require 'open3'
- 1
module Prometheus
- 1
module Client
- 1
module PageSize
- 1
def self.page_size(fallback_page_size: 4096)
- 5
stdout, status = Open3.capture2('getconf PAGESIZE')
- 5
return fallback_page_size if status.nil? || !status.success?
- 5
page_size = stdout.chomp.to_i
- 5
return fallback_page_size if page_size <= 0
- 5
page_size
end
end
end
end
# encoding: UTF-8
- 1
require 'base64'
- 1
require 'thread'
- 1
require 'net/http'
- 1
require 'uri'
- 1
require 'erb'
- 1
require 'set'
- 1
require 'prometheus/client'
- 1
require 'prometheus/client/formats/text'
- 1
require 'prometheus/client/label_set_validator'
- 1
module Prometheus
# Client is a ruby implementation for a Prometheus compatible client.
- 1
module Client
# Push implements a simple way to transmit a given registry to a given
# Pushgateway.
- 1
class Push
- 1
class HttpError < StandardError; end
- 1
class HttpRedirectError < HttpError; end
- 1
class HttpClientError < HttpError; end
- 1
class HttpServerError < HttpError; end
- 1
DEFAULT_GATEWAY = 'http://localhost:9091'.freeze
- 1
PATH = '/metrics/job/%s'.freeze
- 1
SUPPORTED_SCHEMES = %w(http https).freeze
- 1
attr_reader :job, :gateway, :path
- 1
def initialize(job:, gateway: DEFAULT_GATEWAY, grouping_key: {}, **kwargs)
- 25
raise ArgumentError, "job cannot be nil" if job.nil?
- 24
raise ArgumentError, "job cannot be empty" if job.empty?
- 23
@validator = LabelSetValidator.new()
- 23
@validator.validate(grouping_key)
- 22
@mutex = Mutex.new
- 22
@job = job
- 22
@gateway = gateway || DEFAULT_GATEWAY
- 22
@grouping_key = grouping_key
- 22
@path = build_path(job, grouping_key)
- 22
@uri = parse("#{@gateway}#{@path}")
- 20
validate_no_basic_auth!(@uri)
- 19
@http = Net::HTTP.new(@uri.host, @uri.port)
- 19
@http.use_ssl = (@uri.scheme == 'https')
- 19
@http.open_timeout = kwargs[:open_timeout] if kwargs[:open_timeout]
- 19
@http.read_timeout = kwargs[:read_timeout] if kwargs[:read_timeout]
end
- 1
def basic_auth(user, password)
- 1
@user = user
- 1
@password = password
end
- 1
def add(registry)
- 1
synchronize do
- 1
request(Net::HTTP::Post, registry)
end
end
- 1
def replace(registry)
- 1
synchronize do
- 1
request(Net::HTTP::Put, registry)
end
end
- 1
def delete
- 1
synchronize do
- 1
request(Net::HTTP::Delete)
end
end
- 1
private
- 1
def parse(url)
- 22
uri = URI.parse(url)
- 21
unless SUPPORTED_SCHEMES.include?(uri.scheme)
- 1
raise ArgumentError, 'only HTTP gateway URLs are supported currently.'
end
- 20
uri
rescue URI::InvalidURIError => e
- 1
raise ArgumentError, "#{url} is not a valid URL: #{e}"
end
- 1
def build_path(job, grouping_key)
- 22
path = format(PATH, ERB::Util::url_encode(job))
- 22
grouping_key.each do |label, value|
- 6
if value.include?('/')
- 1
encoded_value = Base64.urlsafe_encode64(value)
- 1
path += "/#{label}@base64/#{encoded_value}"
# While it's valid for the urlsafe_encode64 function to return an
# empty string when the input string is empty, it doesn't work for
# our specific use case as we're putting the result into a URL path
# segment. A double slash (`//`) can be normalised away by HTTP
# libraries, proxies, and web servers.
#
# For empty strings, we use a single padding character (`=`) as the
# value.
#
# See the pushgateway docs for more details:
#
# https://github.com/prometheus/pushgateway/blob/6393a901f56d4dda62cd0f6ab1f1f07c495b6354/README.md#url
- 5
elsif value.empty?
- 1
path += "/#{label}@base64/="
else
- 4
path += "/#{label}/#{ERB::Util::url_encode(value)}"
end
end
- 22
path
end
- 1
def request(req_class, registry = nil)
- 8
validate_no_label_clashes!(registry) if registry
- 7
req = req_class.new(@uri)
- 7
req.content_type = Formats::Text::CONTENT_TYPE
- 7
req.basic_auth(@user, @password) if @user
- 7
req.body = Formats::Text.marshal(registry) if registry
- 7
response = @http.request(req)
- 7
validate_response!(response)
- 4
response
end
- 1
def synchronize
- 6
@mutex.synchronize { yield }
end
- 1
def validate_no_basic_auth!(uri)
- 20
if uri.user || uri.password
- 1
raise ArgumentError, <<~EOF
Setting Basic Auth credentials in the gateway URL is not supported, please call the `basic_auth` method.
Received username `#{uri.user}` in gateway URL. Instead of passing
Basic Auth credentials like this:
```
push = Prometheus::Client::Push.new(job: "my-job", gateway: "http://user:password@localhost:9091")
```
please pass them like this:
```
push = Prometheus::Client::Push.new(job: "my-job", gateway: "http://localhost:9091")
push.basic_auth("user", "password")
```
While URLs do support passing Basic Auth credentials using the
`http://user:password@example.com/` syntax, the username and
password in that syntax have to follow the usual rules for URL
encoding of characters per RFC 3986
(https://datatracker.ietf.org/doc/html/rfc3986#section-2.1).
Rather than place the burden of correctly performing that encoding
on users of this gem, we decided to have a separate method for
supplying Basic Auth credentials, with no requirement to URL encode
the characters in them.
EOF
end
end
- 1
def validate_no_label_clashes!(registry)
# There's nothing to check if we don't have a grouping key
- 7
return if @grouping_key.empty?
# We could be doing a lot of comparisons, so let's do them against a
# set rather than an array
- 1
grouping_key_labels = @grouping_key.keys.to_set
- 1
registry.metrics.each do |metric|
- 1
metric.values.keys.first.keys.each do |label|
- 1
if grouping_key_labels.include?(label)
- 1
raise LabelSetValidator::InvalidLabelSetError,
"label :#{label} from grouping key collides with label of the " \
"same name from metric :#{metric.name} and would overwrite it"
end
end
end
end
- 1
def validate_response!(response)
- 7
status = Integer(response.code)
- 7
if status >= 300
- 3
message = "status: #{response.code}, message: #{response.message}, body: #{response.body}"
- 3
if status <= 399
- 1
raise HttpRedirectError, message
- 2
elsif status <= 499
- 1
raise HttpClientError, message
else
- 1
raise HttpServerError, message
end
end
end
end
end
end
# encoding: UTF-8
- 1
require 'prometheus/client'
- 1
module Prometheus
- 1
module Client
- 1
module Rack
# Collector is a Rack middleware that provides a sample implementation of
# a HTTP tracer. The default label builder can be modified to export a
# different set of labels per recorded metric.
- 1
class Collector
- 1
attr_reader :app, :registry
- 1
def initialize(app, options = {}, &label_builder)
- 5
@app = app
- 5
@registry = options[:registry] || Client.registry
- 5
@label_builder = label_builder || DEFAULT_LABEL_BUILDER
- 5
init_request_metrics
- 5
init_exception_metrics
end
- 1
def call(env) # :nodoc:
- 12
trace(env) { @app.call(env) }
end
- 1
protected
- 1
DEFAULT_LABEL_BUILDER = proc do |env|
{
- 4
method: env['REQUEST_METHOD'].downcase,
host: env['HTTP_HOST'].to_s,
path: env['PATH_INFO'].to_s,
}
end
- 1
def init_request_metrics
- 5
@requests = @registry.counter(
:http_requests_total,
'A counter of the total number of HTTP requests made.',
)
- 5
@durations = @registry.summary(
:http_request_duration_seconds,
'A summary of the response latency.',
)
- 5
@durations_hist = @registry.histogram(
:http_req_duration_seconds,
'A histogram of the response latency.',
)
end
- 1
def init_exception_metrics
- 5
@exceptions = @registry.counter(
:http_exceptions_total,
'A counter of the total number of exceptions raised.',
)
end
- 1
def trace(env)
- 6
start = Time.now
- 6
yield.tap do |response|
- 5
duration = (Time.now - start).to_f
- 5
record(labels(env, response), duration)
end
rescue => exception
- 2
@exceptions.increment(exception: exception.class.name)
- 2
raise
end
- 1
def labels(env, response)
- 5
@label_builder.call(env).tap do |labels|
- 5
labels[:code] = response.first.to_s
end
end
- 1
def record(labels, duration)
- 5
@requests.increment(labels)
- 4
@durations.observe(labels, duration)
- 4
@durations_hist.observe(labels, duration)
rescue => exception
- 1
@exceptions.increment(exception: exception.class.name)
- 1
raise
nil
end
end
end
end
end
# encoding: UTF-8
- 1
require 'prometheus/client'
- 1
require 'prometheus/client/formats/text'
- 1
module Prometheus
- 1
module Client
- 1
module Rack
# Exporter is a Rack middleware that provides a sample implementation of
# a Prometheus HTTP client API.
- 1
class Exporter
- 1
attr_reader :app, :registry, :path
- 1
FORMATS = [Formats::Text].freeze
- 1
FALLBACK = Formats::Text
- 1
def initialize(app, options = {})
@app = app
@registry = options[:registry] || Client.registry
@path = options[:path] || '/metrics'
@acceptable = build_dictionary(FORMATS, FALLBACK)
end
- 1
def call(env)
if env['PATH_INFO'] == @path
format = negotiate(env['HTTP_ACCEPT'], @acceptable)
format ? respond_with(format) : not_acceptable(FORMATS)
else
@app.call(env)
end
end
- 1
private
- 1
def negotiate(accept, formats)
accept = '*/*' if accept.to_s.empty?
parse(accept).each do |content_type, _|
return formats[content_type] if formats.key?(content_type)
end
nil
end
- 1
def parse(header)
header.to_s.split(/\s*,\s*/).map do |type|
attributes = type.split(/\s*;\s*/)
quality = extract_quality(attributes)
[attributes.join('; '), quality]
end.sort_by(&:last).reverse
end
- 1
def extract_quality(attributes, default = 1.0)
quality = default
attributes.delete_if do |attr|
quality = attr.split('q=').last.to_f if attr.start_with?('q=')
end
quality
end
- 1
def respond_with(format)
rust_enabled = Prometheus::Client.configuration.rust_multiprocess_metrics
response = if Prometheus::Client.configuration.value_class.multiprocess
format.marshal_multiprocess(use_rust: rust_enabled)
else
format.marshal
end
[
200,
{ 'Content-Type' => format::CONTENT_TYPE },
[response],
]
end
- 1
def not_acceptable(formats)
types = formats.map { |format| format::MEDIA_TYPE }
[
406,
{ 'Content-Type' => 'text/plain' },
["Supported media types: #{types.join(', ')}"],
]
end
- 1
def build_dictionary(formats, fallback)
formats.each_with_object('*/*' => fallback) do |format, memo|
memo[format::CONTENT_TYPE] = format
memo[format::MEDIA_TYPE] = format
end
end
end
end
end
end
# encoding: UTF-8
- 1
require 'thread'
- 1
require 'prometheus/client/counter'
- 1
require 'prometheus/client/summary'
- 1
require 'prometheus/client/gauge'
- 1
require 'prometheus/client/histogram'
- 1
module Prometheus
- 1
module Client
# Registry
- 1
class Registry
- 1
class AlreadyRegisteredError < StandardError; end
- 1
def initialize
- 36
@metrics = {}
- 36
@mutex = Mutex.new
end
- 1
def register(metric)
- 72
name = metric.name
- 72
@mutex.synchronize do
- 72
if exist?(name.to_sym)
- 5
raise AlreadyRegisteredError, "#{name} has already been registered"
else
- 67
@metrics[name.to_sym] = metric
end
end
- 67
metric
end
- 1
def counter(name, docstring, base_labels = {})
- 15
register(Counter.new(name, docstring, base_labels))
end
- 1
def summary(name, docstring, base_labels = {})
- 10
register(Summary.new(name, docstring, base_labels))
end
- 1
def gauge(name, docstring, base_labels = {}, multiprocess_mode = :all)
- 21
register(Gauge.new(name, docstring, base_labels, multiprocess_mode))
end
- 1
def histogram(name, docstring, base_labels = {},
buckets = Histogram::DEFAULT_BUCKETS)
- 10
register(Histogram.new(name, docstring, base_labels, buckets))
end
- 1
def exist?(name)
- 74
@metrics.key?(name)
end
- 1
def get(name)
- 11
@metrics[name.to_sym]
end
- 1
def metrics
- 15
@metrics.values
end
end
end
end
- 1
require 'json'
- 1
module Prometheus
- 1
module Client
- 1
class SimpleValue
- 1
def initialize(_type, _metric_name, _name, _labels, *_args)
- 88
@value = 0.0
end
- 1
def set(value)
- 12
@value = value
end
- 1
def increment(by = 1)
- 74
@value += by
end
- 1
def decrement(by = 1)
- 2
@value -= by
end
- 1
def get
- 35
@value
end
- 1
def self.multiprocess
- 10
false
end
end
end
end
- 1
require 'prometheus/client/metric'
- 1
require 'prometheus/client/uses_value_type'
- 1
module Prometheus
- 1
module Client
# Summary is an accumulator for samples. It captures Numeric data and
# provides an efficient quantile calculation mechanism.
- 1
class Summary < Metric
- 1
extend Gem::Deprecate
# Value represents the state of a Summary at a given point.
- 1
class Value < Hash
- 1
include UsesValueType
- 1
attr_accessor :sum, :total
- 1
def initialize(type, name, labels)
- 11
@sum = value_object(type, name, "#{name}_sum", labels)
- 11
@total = value_object(type, name, "#{name}_count", labels)
end
- 1
def observe(value)
- 9
@sum.increment(value)
- 9
@total.increment
end
end
- 1
def initialize(name, docstring, base_labels = {})
- 18
super(name, docstring, base_labels)
end
- 1
def type
- 30
:summary
end
# Records a given value.
- 1
def observe(labels, value)
- 9
label_set = label_set_for(labels)
- 18
synchronize { @values[label_set].observe(value) }
end
- 1
alias add observe
- 1
deprecate :add, :observe, 2016, 10
# Returns the value for the given label set
- 1
def get(labels = {})
- 4
@validator.valid?(labels)
- 4
synchronize do
- 4
@values[labels].sum.get
end
end
# Returns all label sets with their values
- 1
def values
synchronize do
@values.each_with_object({}) do |(labels, value), memo|
memo[labels] = value.sum
end
end
end
- 1
private
- 1
def default(labels)
- 11
Value.new(type, @name, labels)
end
end
end
end
- 1
module Prometheus
- 1
module Client
- 1
module Support
- 1
module Puma
- 1
extend self
- 1
def worker_pid_provider
- 4
wid = worker_id
- 4
if wid = worker_id
- 3
wid
else
- 1
"process_id_#{Process.pid}"
end
end
- 1
private
- 1
def object_based_worker_id
- 6
return unless defined?(::Puma::Cluster::Worker)
- 2
workers = ObjectSpace.each_object(::Puma::Cluster::Worker)
- 2
return if workers.nil?
- 2
workers_first = workers.first
- 2
workers_first.index unless workers_first.nil?
end
- 1
def program_name
$PROGRAM_NAME
end
- 1
def worker_id
- 8
if matchdata = program_name.match(/puma.*cluster worker ([0-9]+):/)
- 2
"puma_#{matchdata[1]}"
- 6
elsif object_worker_id = object_based_worker_id
- 2
"puma_#{object_worker_id}"
- 4
elsif program_name.include?('puma')
- 2
'puma_master'
end
end
end
end
end
end
- 1
module Prometheus
- 1
module Client
- 1
module Support
- 1
module Unicorn
- 1
def self.worker_pid_provider
- 2
wid = worker_id
- 2
if wid.nil?
- 1
"process_id_#{Process.pid}"
else
- 1
"worker_id_#{wid}"
end
end
- 1
def self.worker_id
- 2
match = $0.match(/worker\[([^\]]+)\]/)
- 2
if match
- 1
match[1]
else
- 1
object_based_worker_id
end
end
- 1
def self.object_based_worker_id
- 3
return unless defined?(::Unicorn::Worker)
- 2
workers = ObjectSpace.each_object(::Unicorn::Worker)
- 2
return if workers.nil?
- 1
workers_first = workers.first
- 1
workers_first.nr unless workers_first.nil?
end
end
end
end
end
- 1
require 'prometheus/client/simple_value'
- 1
module Prometheus
- 1
module Client
# Module providing convenience methods for creating value_object
- 1
module UsesValueType
- 1
def value_class
- 40266
Prometheus::Client.configuration.value_class
end
- 1
def value_object(type, metric_name, name, labels, *args)
- 40230
value_class.new(type, metric_name, name, labels, *args)
rescue StandardError => e
Prometheus::Client.logger.info("error #{e} while creating instance of #{value_class} defaulting to SimpleValue")
Prometheus::Client.logger.debug("error #{e} backtrace #{e.backtrace.join("\n")}")
Prometheus::Client::SimpleValue.new(type, metric_name, name, labels)
end
end
end
end
# encoding: UTF-8
- 1
shared_examples_for Prometheus::Client::Metric do
- 20
subject { described_class.new(:foo, 'foo description') }
- 4
describe '.new' do
- 4
it 'returns a new metric' do
- 4
expect(subject).to be
end
- 4
it 'raises an exception if a reserved base label is used' do
- 4
exception = Prometheus::Client::LabelSetValidator::ReservedLabelError
- 4
expect do
- 4
described_class.new(:foo, 'foo docstring', __name__: 'reserved')
end.to raise_exception exception
end
- 4
it 'raises an exception if the given name is blank' do
- 4
expect do
- 4
described_class.new(nil, 'foo')
end.to raise_exception ArgumentError
end
- 4
it 'raises an exception if docstring is missing' do
- 4
expect do
- 4
described_class.new(:foo, '')
end.to raise_exception ArgumentError
end
end
- 4
describe '#type' do
- 4
it 'returns the metric type as symbol' do
- 4
expect(subject.type).to be_a(Symbol)
end
end
- 4
describe '#get' do
- 4
it 'returns the current metric value' do
- 4
expect(subject.get).to be_a(type)
end
- 4
it 'returns the current metric value for a given label set' do
- 4
expect(subject.get(test: 'label')).to be_a(type)
end
end
end
- 1
require 'prometheus/client/counter'
- 1
require 'prometheus/client'
- 1
require 'examples/metric_example'
- 1
describe Prometheus::Client::Counter do
- 1
before do
- 14
allow(Prometheus::Client.configuration).to receive(:multiprocess_files_dir).and_return('tmp/')
end
- 7
let(:counter) { Prometheus::Client::Counter.new(:foo, 'foo description') }
- 1
it_behaves_like Prometheus::Client::Metric do
- 3
let(:type) { Float }
end
- 1
describe 'Memory Error tests' do
- 1
it "creating many counters shouldn't cause a SIGBUS" do
- 1
4.times do |j|
- 4
9999.times do |i|
- 39996
counter = Prometheus::Client::Counter.new("foo#{j}_z#{i}".to_sym, 'some string')
- 39996
counter.increment
end
- 4
GC.start
end
end
end
- 1
describe '#increment' do
- 1
it 'increments the counter' do
- 4
expect { counter.increment }.to change { counter.get }.by(1)
end
- 1
it 'increments the counter for a given label set' do
- 1
expect do
- 1
expect do
- 1
counter.increment(test: 'label')
- 2
end.to change { counter.get(test: 'label') }.by(1)
- 2
end.to_not change { counter.get(test: 'other_label') }
end
- 1
it 'increments the counter by a given value' do
- 1
expect do
- 1
counter.increment({}, 5)
- 2
end.to change { counter.get }.by(5)
end
- 1
it 'raises an ArgumentError on negative increments' do
- 1
expect do
- 1
counter.increment({}, -1)
end.to raise_error ArgumentError
end
- 1
it 'returns the new counter value' do
- 1
expect(counter.increment).to eql(counter.get)
end
- 1
it 'is thread safe' do
- 1
expect do
- 1
Array.new(10) do
- 10
Thread.new do
- 110
10.times { counter.increment }
end
end.each(&:join)
- 2
end.to change { counter.get }.by(100)
end
end
end
- 1
require 'spec_helper'
- 1
require 'prometheus/client/formats/text'
- 1
require 'prometheus/client/mmaped_value'
- 1
describe Prometheus::Client::Formats::Text do
- 1
context 'single process metrics' do
- 2
let(:value_class) { Prometheus::Client::SimpleValue }
- 1
let(:summary_value) do
- 1
{ 0.5 => 4.2, 0.9 => 8.32, 0.99 => 15.3 }.tap do |value|
- 1
allow(value).to receive_messages(sum: 1243.21, total: 93)
end
end
- 1
let(:histogram_value) do
- 1
{ 10 => 1, 20 => 2, 30 => 2 }.tap do |value|
- 1
allow(value).to receive_messages(sum: 15.2, total: 2)
end
end
- 1
let(:registry) do
metrics = [
- 1
double(
name: :foo,
docstring: 'foo description',
base_labels: { umlauts: 'Björn', utf: '佖佥' },
type: :counter,
values: {
{ code: 'red' } => 42,
{ code: 'green' } => 3.14E42,
{ code: 'blue' } => -1.23e-45,
},
),
double(
name: :bar,
docstring: "bar description\nwith newline",
base_labels: { status: 'success' },
type: :gauge,
values: {
{ code: 'pink' } => 15,
},
),
double(
name: :baz,
docstring: 'baz "description" \\escaping',
base_labels: {},
type: :counter,
values: {
{ text: "with \"quotes\", \\escape \n and newline" } => 15,
},
),
double(
name: :qux,
docstring: 'qux description',
base_labels: { for: 'sake' },
type: :summary,
values: {
{ code: '1' } => summary_value,
},
),
double(
name: :xuq,
docstring: 'xuq description',
base_labels: {},
type: :histogram,
values: {
{ code: 'ah' } => histogram_value,
},
),
]
- 1
metrics.each do |m|
- 5
m.values.each do |k, v|
- 7
m.values[k] = value_class.new(m.type, m.name, m.name, k)
- 7
m.values[k].set(v)
end
end
- 1
double(metrics: metrics)
end
- 1
describe '.marshal' do
- 1
it 'returns a Text format version 0.0.4 compatible representation' do
- 1
expect(subject.marshal(registry)).to eql <<-'TEXT'.gsub(/^\s+/, '')
# HELP foo foo description
# TYPE foo counter
foo{umlauts="Björn",utf="佖佥",code="red"} 42
foo{umlauts="Björn",utf="佖佥",code="green"} 3.14e+42
foo{umlauts="Björn",utf="佖佥",code="blue"} -1.23e-45
# HELP bar bar description\nwith newline
# TYPE bar gauge
bar{status="success",code="pink"} 15
# HELP baz baz "description" \\escaping
# TYPE baz counter
baz{text="with \"quotes\", \\escape \n and newline"} 15
# HELP qux qux description
# TYPE qux summary
qux{for="sake",code="1",quantile="0.5"} 4.2
qux{for="sake",code="1",quantile="0.9"} 8.32
qux{for="sake",code="1",quantile="0.99"} 15.3
qux_sum{for="sake",code="1"} 1243.21
qux_count{for="sake",code="1"} 93
# HELP xuq xuq description
# TYPE xuq histogram
xuq{code="ah",le="10"} 1
xuq{code="ah",le="20"} 2
xuq{code="ah",le="30"} 2
xuq{code="ah",le="+Inf"} 2
xuq_sum{code="ah"} 15.2
xuq_count{code="ah"} 2
TEXT
end
end
end
- 1
context 'multi process metrics', :temp_metrics_dir do
- 1
[true, false].each do |use_rust|
- 2
context "when rust_multiprocess_metrics is #{use_rust}" do
- 6
let(:registry) { Prometheus::Client::Registry.new }
- 2
before do
- 4
allow(Prometheus::Client.configuration).to receive(:multiprocess_files_dir).and_return(temp_metrics_dir)
- 4
allow(Prometheus::Client.configuration).to receive(:rust_multiprocess_metrics).and_return(use_rust)
# reset all current metrics
- 4
Prometheus::Client::MmapedValue.class_variable_set(:@@files, {})
end
- 2
context 'pid provider returns compound ID', :temp_metrics_dir, :sample_metrics do
- 2
before do
- 12
allow(Prometheus::Client.configuration).to receive(:pid_provider).and_return(-> { 'pid_provider_id_1' })
# Prometheus::Client::MmapedValue.class_variable_set(:@@files, {})
- 2
add_simple_metrics(registry)
end
- 2
it '.marshal_multiprocess' do
- 2
expect(described_class.marshal_multiprocess(temp_metrics_dir, use_rust: true)).to eq <<-'TEXT'.gsub(/^\s+/, '')
# HELP counter Multiprocess metric
# TYPE counter counter
counter{a="1",b="1"} 1
counter{a="1",b="2"} 1
counter{a="2",b="1"} 1
# HELP gauge Multiprocess metric
# TYPE gauge gauge
gauge{b="1"} 1
gauge{b="2"} 1
# HELP gauge_with_big_value Multiprocess metric
# TYPE gauge_with_big_value gauge
gauge_with_big_value{a="0.12345678901234566"} 0.12345678901234566
gauge_with_big_value{a="12345678901234567"} 12345678901234568
# HELP gauge_with_null_labels Multiprocess metric
# TYPE gauge_with_null_labels gauge
gauge_with_null_labels{a="",b=""} 1
# HELP gauge_with_pid Multiprocess metric
# TYPE gauge_with_pid gauge
gauge_with_pid{b="1",c="1",pid="pid_provider_id_1"} 1
# HELP histogram Multiprocess metric
# TYPE histogram histogram
histogram_bucket{a="1",le="+Inf"} 1
histogram_bucket{a="1",le="0.005"} 0
histogram_bucket{a="1",le="0.01"} 0
histogram_bucket{a="1",le="0.025"} 0
histogram_bucket{a="1",le="0.05"} 0
histogram_bucket{a="1",le="0.1"} 0
histogram_bucket{a="1",le="0.25"} 0
histogram_bucket{a="1",le="0.5"} 0
histogram_bucket{a="1",le="1"} 1
histogram_bucket{a="1",le="10"} 1
histogram_bucket{a="1",le="2.5"} 1
histogram_bucket{a="1",le="5"} 1
histogram_count{a="1"} 1
histogram_sum{a="1"} 1
# HELP summary Multiprocess metric
# TYPE summary summary
summary_count{a="1",b="1"} 1
summary_sum{a="1",b="1"} 1
TEXT
end
end
- 2
context 'pid provider returns numerical value', :temp_metrics_dir, :sample_metrics do
- 2
before do
- 12
allow(Prometheus::Client.configuration).to receive(:pid_provider).and_return(-> { -1 })
- 2
add_simple_metrics(registry)
end
- 2
it '.marshal_multiprocess' do
- 2
expect(described_class.marshal_multiprocess(temp_metrics_dir, use_rust: use_rust)).to eq <<-'TEXT'.gsub(/^\s+/, '')
# HELP counter Multiprocess metric
# TYPE counter counter
counter{a="1",b="1"} 1
counter{a="1",b="2"} 1
counter{a="2",b="1"} 1
# HELP gauge Multiprocess metric
# TYPE gauge gauge
gauge{b="1"} 1
gauge{b="2"} 1
# HELP gauge_with_big_value Multiprocess metric
# TYPE gauge_with_big_value gauge
gauge_with_big_value{a="0.12345678901234566"} 0.12345678901234566
gauge_with_big_value{a="12345678901234567"} 12345678901234568
# HELP gauge_with_null_labels Multiprocess metric
# TYPE gauge_with_null_labels gauge
gauge_with_null_labels{a="",b=""} 1
# HELP gauge_with_pid Multiprocess metric
# TYPE gauge_with_pid gauge
gauge_with_pid{b="1",c="1",pid="-1"} 1
# HELP histogram Multiprocess metric
# TYPE histogram histogram
histogram_bucket{a="1",le="+Inf"} 1
histogram_bucket{a="1",le="0.005"} 0
histogram_bucket{a="1",le="0.01"} 0
histogram_bucket{a="1",le="0.025"} 0
histogram_bucket{a="1",le="0.05"} 0
histogram_bucket{a="1",le="0.1"} 0
histogram_bucket{a="1",le="0.25"} 0
histogram_bucket{a="1",le="0.5"} 0
histogram_bucket{a="1",le="1"} 1
histogram_bucket{a="1",le="10"} 1
histogram_bucket{a="1",le="2.5"} 1
histogram_bucket{a="1",le="5"} 1
histogram_count{a="1"} 1
histogram_sum{a="1"} 1
# HELP summary Multiprocess metric
# TYPE summary summary
summary_count{a="1",b="1"} 1
summary_sum{a="1",b="1"} 1
TEXT
end
end
- 2
context 'when OJ is available uses OJ to parse keys' do
- 2
let(:oj) { double(oj) }
- 2
before do
stub_const 'Oj', oj
allow(oj).to receive(:load)
end
end
- 2
context 'with metric having whitespace and UTF chars', :temp_metrics_dir do
- 2
before do
registry.gauge(:gauge, "bar description\nwith newline", { umlauts: 'Björn', utf: '佖佥' }, :all).set({ umlauts: 'Björn', utf: '佖佥' }, 1)
end
- 2
xit '.marshall_multiprocess' do
expect(described_class.marshal_multiprocess(temp_metrics_dir, use_rust: true)).to eq <<-'TEXT'.gsub(/^\s+/, '')
TODO...
TEXT
end
end
end
end
end
end
- 1
require 'prometheus/client'
- 1
require 'prometheus/client/gauge'
- 1
require 'examples/metric_example'
- 1
describe Prometheus::Client::Gauge do
- 7
let(:gauge) { Prometheus::Client::Gauge.new(:foo, 'foo description', test: nil) }
- 1
before do
- 13
allow(Prometheus::Client.configuration).to receive(:value_class).and_return(Prometheus::Client::SimpleValue)
end
- 1
it_behaves_like Prometheus::Client::Metric do
- 3
let(:type) { Float }
end
- 1
describe '#set' do
- 1
it 'sets a metric value' do
- 1
expect do
- 1
gauge.set({}, 42)
- 2
end.to change { gauge.get }.from(0).to(42)
end
- 1
it 'sets a metric value for a given label set' do
- 1
expect do
- 1
expect do
- 1
gauge.set({ test: 'value' }, 42)
- 2
end.to(change { gauge.get(test: 'value') }.from(0).to(42))
- 2
end.to_not(change { gauge.get })
end
end
- 1
describe '#increment' do
- 1
it 'increments a metric value' do
- 1
gauge.set({}, 1)
- 1
expect do
- 1
gauge.increment({}, 42)
- 2
end.to change { gauge.get }.from(1).to(43)
end
- 1
it 'sets a metric value for a given label set' do
- 1
gauge.increment({ test: 'value' }, 1)
- 1
expect do
- 1
expect do
- 1
gauge.increment({ test: 'value' }, 42)
- 2
end.to(change { gauge.get(test: 'value') }.from(1).to(43))
- 2
end.to_not(change { gauge.get })
end
end
- 1
describe '#decrement' do
- 1
it 'decrements a metric value' do
- 1
gauge.set({}, 10)
- 1
expect do
- 1
gauge.decrement({}, 1)
- 2
end.to change { gauge.get }.from(10).to(9)
end
- 1
it 'sets a metric value for a given label set' do
- 1
gauge.set({ test: 'value' }, 10)
- 1
expect do
- 1
expect do
- 1
gauge.decrement({ test: 'value' }, 5)
- 2
end.to(change { gauge.get(test: 'value') }.from(10).to(5))
- 2
end.to_not(change { gauge.get })
end
end
end
- 1
require 'spec_helper'
- 1
require 'oj'
- 1
require 'prometheus/client/helper/json_parser'
- 1
describe Prometheus::Client::Helper::JsonParser do
- 1
describe '.load' do
- 3
let(:input) { %({ "a": 1 }) }
- 1
shared_examples 'JSON parser' do
- 2
it 'parses JSON' do
- 2
expect(described_class.load(input)).to eq({ 'a' => 1 })
end
- 2
it 'raises JSON::ParserError' do
- 4
expect { described_class.load("{false}") }.to raise_error(JSON::ParserError)
end
end
- 1
context 'with Oj' do
- 1
it_behaves_like 'JSON parser'
end
- 1
context 'without Oj' do
- 1
before(:all) do
- 1
Object.send(:remove_const, 'Oj')
- 1
load File.join(__dir__, "../../../../lib/prometheus/client/helper/json_parser.rb")
end
- 1
it_behaves_like 'JSON parser'
end
end
end
- 1
require 'spec_helper'
- 1
require 'prometheus/client/helper/mmaped_file'
- 1
require 'prometheus/client/page_size'
- 1
describe Prometheus::Client::Helper::MmapedFile do
- 11
let(:filename) { Dir::Tmpname.create('mmaped_file_') {} }
- 1
after do
- 10
File.delete(filename) if File.exist?(filename)
end
- 1
describe '.open' do
- 1
it 'initialize PRIVATE mmaped file read only' do
- 1
expect(described_class).to receive(:new).with(filename).and_call_original
- 1
expect(described_class.open(filename)).to be_instance_of(described_class)
end
end
- 1
context 'file does not exist' do
- 4
let (:subject) { described_class.open(filename) }
- 1
it 'creates and initializes file correctly' do
- 1
expect(File.exist?(filename)).to be_falsey
- 1
subject
- 1
expect(File.exist?(filename)).to be_truthy
end
- 1
it 'creates a file with minimum initial size' do
- 1
expect(File.size(subject.filepath)).to eq(subject.send(:initial_mmap_file_size))
end
- 1
context 'when initial mmap size is larger' do
- 2
let(:page_size) { Prometheus::Client::PageSize.page_size }
- 2
let (:initial_mmap_file_size) { page_size + 1024 }
- 1
before do
- 1
allow_any_instance_of(described_class).to receive(:initial_mmap_file_size).and_return(initial_mmap_file_size)
end
- 1
it 'creates a file with increased minimum initial size' do
- 1
expect(File.size(subject.filepath)).to eq(page_size * 2);
end
end
end
- 1
describe '.ensure_exclusive_file' do
- 7
let(:tmpdir) { Dir.mktmpdir('mmaped_file') }
- 7
let(:pid) { 'pid' }
- 1
before do
- 6
allow(Prometheus::Client.configuration).to receive(:multiprocess_files_dir).and_return(tmpdir)
- 6
allow(Prometheus::Client.configuration).to receive(:pid_provider).and_return(pid.method(:to_s))
end
- 1
context 'when no files are already locked' do
- 1
it 'provides first possible filename' do
- 1
expect(described_class.ensure_exclusive_file('mmaped_file'))
.to match(/.*mmaped_file_pid-0\.db/)
end
- 1
it 'provides first and second possible filenames for two invocations' do
- 1
expect(described_class.ensure_exclusive_file('mmaped_file'))
.to match(/.*mmaped_file_pid-0\.db/)
- 1
expect(described_class.ensure_exclusive_file('mmaped_file'))
.to match(/.*mmaped_file_pid-1\.db/)
end
end
- 1
context 'when first possible file exists for current file ID' do
- 5
let(:first_mmaped_file) { described_class.ensure_exclusive_file('mmaped_file') }
- 1
before do
- 4
first_mmaped_file
end
- 1
context 'first file is unlocked' do
- 1
before do
- 2
Prometheus::Client::Helper::FileLocker.unlock(first_mmaped_file)
end
- 1
it 'provides first possible filename discarding the lock' do
- 1
expect(described_class.ensure_exclusive_file('mmaped_file'))
.to match(/.*mmaped_file_pid-0\.db/)
end
- 1
it 'provides second possible filename for second invocation' do
- 1
expect(described_class.ensure_exclusive_file('mmaped_file'))
.to match(/.*mmaped_file_pid-0\.db/)
- 1
expect(described_class.ensure_exclusive_file('mmaped_file'))
.to match(/.*mmaped_file_pid-1\.db/)
end
end
- 1
context 'first file is not unlocked' do
- 1
it 'provides second possible filename' do
- 1
expect(described_class.ensure_exclusive_file('mmaped_file'))
.to match(/.*mmaped_file_pid-1\.db/)
end
- 1
it 'provides second and third possible filename for two invocations' do
- 1
expect(described_class.ensure_exclusive_file('mmaped_file'))
.to match(/.*mmaped_file_pid-1\.db/)
- 1
expect(described_class.ensure_exclusive_file('mmaped_file'))
.to match(/.*mmaped_file_pid-2\.db/)
end
end
end
end
end
# encoding: UTF-8
- 1
require 'prometheus/client'
- 1
require 'prometheus/client/histogram'
- 1
require 'examples/metric_example'
- 1
describe Prometheus::Client::Histogram do
- 1
before do
- 10
allow(Prometheus::Client.configuration).to receive(:multiprocess_files_dir).and_return('tmp/')
end
- 1
let(:histogram) do
- 1
described_class.new(:bar, 'bar description', {}, [2.5, 5, 10])
end
- 1
it_behaves_like Prometheus::Client::Metric do
- 3
let(:type) { Hash }
end
- 1
describe '#initialization' do
- 1
it 'raise error for unsorted buckets' do
- 1
expect do
- 1
described_class.new(:bar, 'bar description', {}, [5, 2.5, 10])
end.to raise_error ArgumentError
end
- 1
it 'raise error for accidentally missing out an argument' do
- 1
expect do
- 1
described_class.new(:bar, 'bar description', [5, 2.5, 10])
end.to raise_error Prometheus::Client::LabelSetValidator::InvalidLabelSetError
end
end
- 1
describe '#observe' do
- 1
it 'records the given value' do
- 1
expect do
- 1
histogram.observe({}, 5)
- 2
end.to change { histogram.get }
end
- 1
xit 'raise error for le labels' do
expect do
histogram.observe({ le: 1 }, 5)
end.to raise_error ArgumentError
end
end
- 1
describe '#get' do
- 1
before do
histogram.observe({ foo: 'bar' }, 3)
histogram.observe({ foo: 'bar' }, 5.2)
histogram.observe({ foo: 'bar' }, 13)
histogram.observe({ foo: 'bar' }, 4)
end
- 1
xit 'returns a set of buckets values' do
expect(histogram.get(foo: 'bar')).to eql(2.5 => 0, 5 => 2, 10 => 3)
end
- 1
xit 'returns a value which responds to #sum and #total' do
value = histogram.get(foo: 'bar')
expect(value.sum).to eql(25.2)
expect(value.total).to eql(4)
expect(value.total_inf).to eql(4)
end
- 1
xit 'uses zero as default value' do
expect(histogram.get({})).to eql(2.5 => 0, 5 => 0, 10 => 0)
end
end
- 1
xdescribe '#values' do
- 1
it 'returns a hash of all recorded summaries' do
histogram.observe({ status: 'bar' }, 3)
histogram.observe({ status: 'foo' }, 6)
expect(histogram.values).to eql(
{ status: 'bar' } => { 2.5 => 0, 5 => 1, 10 => 1 },
{ status: 'foo' } => { 2.5 => 0, 5 => 0, 10 => 1 },
)
end
end
end
# encoding: UTF-8
- 1
require 'prometheus/client/label_set_validator'
- 1
describe Prometheus::Client::LabelSetValidator do
- 11
let(:validator) { Prometheus::Client::LabelSetValidator.new reserved_labels }
- 10
let(:reserved_labels) { [] }
- 1
describe '.new' do
- 1
it 'returns an instance of a LabelSetValidator' do
- 1
expect(validator).to be_a(Prometheus::Client::LabelSetValidator)
end
end
- 1
describe '#valid?' do
- 1
it 'returns true for a valid label check' do
- 1
expect(validator.valid?(version: 'alpha')).to eql(true)
end
- 1
it 'raises InvalidLabelError if a label value is an array' do
- 1
expect do
- 1
validator.valid?(version: [1, 2, 3])
end.to raise_exception(described_class::InvalidLabelError)
end
- 1
it 'raises Invaliddescribed_classError if a label set is not a hash' do
- 1
expect do
- 1
validator.valid?('invalid')
end.to raise_exception(described_class::InvalidLabelSetError)
end
- 1
it 'raises InvalidLabelError if a label key is not a symbol' do
- 1
expect do
- 1
validator.valid?('key' => 'value')
end.to raise_exception(described_class::InvalidLabelError)
end
- 1
it 'raises InvalidLabelError if a label key starts with __' do
- 1
expect do
- 1
validator.valid?(__reserved__: 'key')
end.to raise_exception(described_class::ReservedLabelError)
end
- 1
context "when reserved labels were set" do
- 2
let(:reserved_labels) { [:reserved] }
- 1
it 'raises ReservedLabelError if a label key is reserved' do
- 1
reserved_labels.each do |label|
- 1
expect do
- 1
validator.valid?(label => 'value')
end.to raise_exception(described_class::ReservedLabelError)
end
end
end
end
- 1
describe '#validate' do
- 1
it 'returns a given valid label set' do
- 1
hash = { version: 'alpha' }
- 1
expect(validator.validate(hash)).to eql(hash)
end
- 1
it 'raises an exception if a given label set is not valid' do
- 1
input = 'broken'
- 1
expect(validator).to receive(:valid?).with(input).and_raise(described_class::InvalidLabelSetError)
- 2
expect { validator.validate(input) }.to raise_exception(described_class::InvalidLabelSetError)
end
- 1
it 'raises InvalidLabelSetError for varying label sets' do
- 1
validator.validate(method: 'get', code: '200')
- 1
expect do
- 1
validator.validate(method: 'get', exception: 'NoMethodError')
end.to raise_exception(described_class::InvalidLabelSetError, "labels must have the same signature: (expected keys: [:code, :method], got: [:exception, :method])")
end
end
end
- 1
require 'prometheus/client/mmaped_dict'
- 1
require 'prometheus/client/page_size'
- 1
require 'tempfile'
- 1
describe Prometheus::Client::MmapedDict do
- 9
let(:tmp_file) { Tempfile.new('mmaped_dict') }
- 9
let(:tmp_mmaped_file) { Prometheus::Client::Helper::MmapedFile.open(tmp_file.path) }
- 1
after do
- 8
tmp_mmaped_file.close
- 8
tmp_file.close
- 8
tmp_file.unlink
end
- 1
describe '#initialize' do
- 1
describe "empty mmap'ed file" do
- 1
it 'is initialized with correct size' do
- 1
described_class.new(tmp_mmaped_file)
- 1
expect(File.size(tmp_file.path)).to eq(tmp_mmaped_file.send(:initial_mmap_file_size))
end
end
- 1
describe "mmap'ed file that is above minimum size" do
- 2
let(:above_minimum_size) { Prometheus::Client::Helper::EntryParser::MINIMUM_SIZE + 1 }
- 2
let(:page_size) { Prometheus::Client::PageSize.page_size }
- 1
before do
- 1
tmp_file.truncate(above_minimum_size)
end
- 1
it 'is initialized with the a page size' do
- 1
described_class.new(tmp_mmaped_file)
- 1
tmp_file.open
- 1
expect(tmp_file.size).to eq(page_size);
end
end
end
- 1
describe 'read on boundary conditions' do
- 2
let(:locked_file) { Prometheus::Client::Helper::MmapedFile.ensure_exclusive_file }
- 1
let(:mmaped_file) { Prometheus::Client::Helper::MmapedFile.open(locked_file) }
- 2
let(:page_size) { Prometheus::Client::PageSize.page_size }
- 1
let(:target_size) { page_size }
- 2
let(:iterations) { page_size / 32 }
- 2
let(:dummy_key) { '1234' }
- 2
let(:dummy_value) { 1.0 }
- 2
let(:expected) { { dummy_key => dummy_value } }
- 1
before do
- 1
Prometheus::Client.configuration.multiprocess_files_dir = Dir.tmpdir
- 1
data = described_class.new(Prometheus::Client::Helper::MmapedFile.open(locked_file))
# This test exercises the case when the value ends on the last byte.
# To generate a file like this, we create entries that require 32 bytes
# total to store with 7 bytes of padding at the end.
#
# To make things align evenly against the system page size, add a dummy
# entry that will occupy the next 3 bytes to start on a 32-byte boundary.
# The filestructure looks like:
#
# Bytes 0-3 : Total used size of file
# Bytes 4-7 : Padding
# Bytes 8-11 : Length of '1234' (4)
# Bytes 12-15: '1234'
# Bytes 24-31: 1.0
# Bytes 32-35: Length of '1000000000000' (13)
# Bytes 36-48: '1000000000000'
# Bytes 49-55: Padding
# Bytes 56-63: 0.0
# Bytes 64-67: Length of '1000000000001' (13)
# Bytes 68-80: '1000000000001'
# Bytes 81-87: Padding
# Bytes 88-95: 1.0
# ...
- 1
data.write_value(dummy_key, dummy_value)
- 1
(1..iterations - 1).each do |i|
# Using a 13-byte string
- 127
text = (1000000000000 + i).to_s
- 127
expected[text] = i.to_f
- 127
data.write_value(text, i)
end
- 1
data.close
end
- 1
it '#read_all_values' do
- 1
values = described_class.read_all_values(locked_file)
- 1
expect(values.count).to eq(iterations)
- 1
expect(values).to match_array(expected.to_a)
end
end
- 1
describe 'read and write values' do
- 6
let(:locked_file) { Prometheus::Client::Helper::MmapedFile.ensure_exclusive_file }
- 6
let(:mmaped_file) { Prometheus::Client::Helper::MmapedFile.open(locked_file) }
- 1
before do
- 5
Prometheus::Client.configuration.multiprocess_files_dir = Dir.tmpdir
- 5
data = described_class.new(Prometheus::Client::Helper::MmapedFile.open(locked_file))
- 5
data.write_value('foo', 100)
- 5
data.write_value('bar', 500)
- 5
data.close
end
- 1
after do
- 5
mmaped_file.close if File.exist?(mmaped_file.filepath)
- 5
Prometheus::Client::Helper::FileLocker.unlock(locked_file) if File.exist?(mmaped_file.filepath)
- 5
File.unlink(locked_file) if File.exist?(mmaped_file.filepath)
end
- 1
it '#inspect' do
- 1
data = described_class.new(Prometheus::Client::Helper::MmapedFile.open(locked_file))
- 1
expect(data.inspect).to match(/#{described_class}:0x/)
- 1
expect(data.inspect).not_to match(/@position/)
end
- 1
it '#read_all_values' do
- 1
values = described_class.read_all_values(locked_file)
- 1
expect(values.count).to eq(2)
- 1
expect(values[0]).to eq(['foo', 100])
- 1
expect(values[1]).to eq(['bar', 500])
end
- 1
it '#read_all_positions' do
- 1
data = described_class.new(Prometheus::Client::Helper::MmapedFile.open(locked_file))
- 1
positions = data.positions
# Generated via https://github.com/luismartingarcia/protocol:
# protocol "Used:4,Pad:4,K1 Size:4,K1 Name:4,K1 Value:8,K2 Size:4,K2 Name:4,K2 Value:8"
#
# 0 1 2 3
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | Used | Pad |K1 Size|K1 Name| K1 Value |K2 Size|K2 Name|
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | K2 Value |
# +-+-+-+-+-+-+-+
- 1
expect(positions).to eq({ 'foo' => 16, 'bar' => 32 })
end
- 1
describe '#write_value' do
- 1
it 'writes values' do
# Reload dictionary
#
- 1
data = described_class.new(mmaped_file)
- 1
data.write_value('new value', 500)
# Overwrite existing values
- 1
data.write_value('foo', 200)
- 1
data.write_value('bar', 300)
- 1
values = described_class.read_all_values(locked_file)
- 1
expect(values.count).to eq(3)
- 1
expect(values[0]).to eq(['foo', 200])
- 1
expect(values[1]).to eq(['bar', 300])
- 1
expect(values[2]).to eq(['new value', 500])
end
- 1
context 'when mmaped_file got deleted' do
- 1
it 'is able to write to and expand metrics file' do
- 1
data = described_class.new(mmaped_file)
- 1
data.write_value('new value', 500)
- 1
FileUtils.rm(mmaped_file.filepath)
- 1
1000.times do |i|
- 1000
data.write_value("new new value #{i}", 567)
end
- 1
expect(File.exist?(locked_file)).not_to be_truthy
end
end
end
end
end
- 1
require 'prometheus/client/mmaped_dict'
- 1
require 'prometheus/client/page_size'
- 1
require 'tempfile'
- 1
describe Prometheus::Client::MmapedValue, :temp_metrics_dir do
- 1
before do
- 13
allow(Prometheus::Client.configuration).to receive(:multiprocess_files_dir).and_return(temp_metrics_dir)
end
- 1
describe '.reset_and_reinitialize' do
- 3
let(:counter) { described_class.new(:counter, :counter, 'counter', {}) }
- 1
before do
- 2
counter.increment(1)
end
- 1
it 'calls reinitialize on the counter' do
- 1
expect(counter).to receive(:unsafe_reinitialize_file).with(false).and_call_original
- 1
described_class.reset_and_reinitialize
end
- 1
context 'when metrics folder changes' do
- 1
around do |example|
- 1
Dir.mktmpdir('temp_metrics_dir') do |path|
- 1
@tmp_path = path
- 1
example.run
end
end
- 1
before do
- 1
allow(Prometheus::Client.configuration).to receive(:multiprocess_files_dir).and_return(@tmp_path)
end
- 1
it 'resets the counter to zero' do
- 1
expect(counter).to receive(:unsafe_reinitialize_file).with(false).and_call_original
- 4
expect { described_class.reset_and_reinitialize }.to(change { counter.get }.from(1).to(0))
end
end
end
- 1
describe '#initialize' do
- 12
let(:pid) { 1234 }
- 1
before do
- 11
described_class.class_variable_set(:@@files, {})
- 11
described_class.class_variable_set(:@@pid, pid)
- 23
allow(Prometheus::Client.configuration).to receive(:pid_provider).and_return(-> { pid })
- 11
allow(Process).to receive(:pid).and_return(pid)
end
- 1
describe 'counter type object initialized' do
- 12
let!(:counter) { described_class.new(:counter, :counter, 'counter', {}) }
- 1
describe 'PID unchanged' do
- 1
it 'initializing gauge MmapValue object type keeps old file data' do
- 1
described_class.new(:gauge, :gauge, 'gauge', {}, :all)
- 1
expect(described_class.class_variable_get(:@@files)).to have_key('counter')
- 1
expect(described_class.class_variable_get(:@@files)).to have_key('gauge_all')
end
end
- 1
describe 'PID changed' do
- 10
let(:new_pid) { pid - 1 }
- 2
let(:page_size) { Prometheus::Client::PageSize.page_size }
- 1
before do
- 9
counter.increment
- 9
@old_value = counter.get
- 20
allow(Prometheus::Client.configuration).to receive(:pid_provider).and_return(-> { new_pid })
- 9
allow(Process).to receive(:pid).and_return(new_pid)
end
- 1
it 'initializing gauge MmapValue object type keeps old file data' do
- 1
described_class.new(:gauge, :gauge, 'gauge', {}, :all)
- 1
expect(described_class.class_variable_get(:@@files)).not_to have_key('counter')
- 1
expect(described_class.class_variable_get(:@@files)).to have_key('gauge_all')
end
- 1
it 'updates pid' do
- 2
expect { described_class.new(:gauge, :gauge, 'gauge', {}, :all) }
- 2
.to change { described_class.class_variable_get(:@@pid) }.from(pid).to(new_pid)
end
- 1
it '#increment updates pid' do
- 2
expect { counter.increment }
- 2
.to change { described_class.class_variable_get(:@@pid) }.from(pid).to(new_pid)
end
- 1
it '#increment updates pid' do
- 2
expect { counter.increment }
- 2
.to change { described_class.class_variable_get(:@@pid) }.from(pid).to(new_pid)
end
- 1
it '#get updates pid' do
- 2
expect { counter.get }
- 2
.to change { described_class.class_variable_get(:@@pid) }.from(pid).to(new_pid)
end
- 1
it '#set updates pid' do
- 2
expect { counter.set(1) }
- 2
.to change { described_class.class_variable_get(:@@pid) }.from(pid).to(new_pid)
end
- 1
it '#set logs an error' do
- 1
counter.set(1)
- 1
allow(counter.instance_variable_get(:@file))
.to receive(:write_value)
.and_raise('error writing value')
- 1
expect(Prometheus::Client.logger).to receive(:warn).and_call_original
- 1
counter.set(1)
end
- 1
it 'reinitialize restores all used file references and resets data' do
- 1
described_class.new(:gauge, :gauge, 'gauge', {}, :all)
- 1
described_class.reinitialize_on_pid_change
- 1
expect(described_class.class_variable_get(:@@files)).to have_key('counter')
- 1
expect(described_class.class_variable_get(:@@files)).to have_key('gauge_all')
- 1
expect(counter.get).not_to eq(@old_value)
end
- 1
it 'updates strings properly upon memory expansion', :page_size do
- 1
described_class.new(:gauge, :gauge, 'gauge2', { label_1: 'x' * page_size * 2 }, :all)
# This previously failed on Linux but not on macOS since mmap() may re-allocate the same region.
- 1
ObjectSpace.each_object(String, &:valid_encoding?)
end
end
- 1
context 'different label ordering' do
- 1
it 'does not care about label ordering' do
- 1
counter1 = described_class.new(:counter, :counter, 'ordered_counter', { label_1: 'hello', label_2: 'world', label_3: 'baz' }).increment
- 1
counter2 = described_class.new(:counter, :counter, 'ordered_counter', { label_2: 'world', label_3: 'baz', label_1: 'hello' }).increment
- 1
reading_counter = described_class.new(:counter, :counter, 'ordered_counter', { label_3: 'baz', label_1: 'hello', label_2: 'world' })
- 1
expect(reading_counter.get).to eq(2)
end
end
end
end
end
# encoding: UTF-8
- 1
require 'prometheus/client/gauge'
- 1
require 'prometheus/client/push'
- 1
describe Prometheus::Client::Push do
- 11
let(:gateway) { 'http://localhost:9091' }
- 10
let(:registry) { Prometheus::Client::Registry.new }
- 13
let(:grouping_key) { {} }
- 14
let(:push) { Prometheus::Client::Push.new(job: 'test-job', gateway: gateway, grouping_key: grouping_key, open_timeout: 5, read_timeout: 30) }
- 1
describe '.new' do
- 1
it 'returns a new push instance' do
- 1
expect(push).to be_a(Prometheus::Client::Push)
end
- 1
it 'uses localhost as default Pushgateway' do
- 1
push = Prometheus::Client::Push.new(job: 'test-job')
- 1
expect(push.gateway).to eql('http://localhost:9091')
end
- 1
it 'allows to specify a custom Pushgateway' do
- 1
push = Prometheus::Client::Push.new(job: 'test-job', gateway: 'http://pu.sh:1234')
- 1
expect(push.gateway).to eql('http://pu.sh:1234')
end
- 1
it 'raises an ArgumentError if the job is nil' do
- 1
expect do
- 1
Prometheus::Client::Push.new(job: nil)
end.to raise_error ArgumentError
end
- 1
it 'raises an ArgumentError if the job is empty' do
- 1
expect do
- 1
Prometheus::Client::Push.new(job: "")
end.to raise_error ArgumentError
end
- 1
it 'raises an ArgumentError if the given gateway URL is invalid' do
- 1
['inva.lid:1233', 'http://[invalid]'].each do |url|
- 2
expect do
- 2
Prometheus::Client::Push.new(job: 'test-job', gateway: url)
end.to raise_error ArgumentError
end
end
- 1
it 'raises InvalidLabelError if a grouping key label has an invalid name' do
- 1
expect do
- 1
Prometheus::Client::Push.new(job: "test-job", grouping_key: { "not_a_symbol" => "foo" })
end.to raise_error Prometheus::Client::LabelSetValidator::InvalidLabelError
end
end
- 1
describe '#add' do
- 1
it 'sends a given registry to via HTTP POST' do
- 1
expect(push).to receive(:request).with(Net::HTTP::Post, registry)
- 1
push.add(registry)
end
end
- 1
describe '#replace' do
- 1
it 'sends a given registry to via HTTP PUT' do
- 1
expect(push).to receive(:request).with(Net::HTTP::Put, registry)
- 1
push.replace(registry)
end
end
- 1
describe '#delete' do
- 1
it 'deletes existing metrics with HTTP DELETE' do
- 1
expect(push).to receive(:request).with(Net::HTTP::Delete)
- 1
push.delete
end
end
- 1
describe '#path' do
- 1
it 'uses the default metrics path if no grouping key given' do
- 1
push = Prometheus::Client::Push.new(job: 'test-job')
- 1
expect(push.path).to eql('/metrics/job/test-job')
end
- 1
it 'appends additional grouping labels to the path if specified' do
- 1
push = Prometheus::Client::Push.new(
job: 'test-job',
grouping_key: { foo: "bar", baz: "qux"},
)
- 1
expect(push.path).to eql('/metrics/job/test-job/foo/bar/baz/qux')
end
- 1
it 'encodes grouping key label values containing `/` in url-safe base64' do
- 1
push = Prometheus::Client::Push.new(
job: 'test-job',
grouping_key: { foo: "bar/baz"},
)
- 1
expect(push.path).to eql('/metrics/job/test-job/foo@base64/YmFyL2Jheg==')
end
- 1
it 'encodes empty grouping key label values as a single base64 padding character' do
- 1
push = Prometheus::Client::Push.new(
job: 'test-job',
grouping_key: { foo: ""},
)
- 1
expect(push.path).to eql('/metrics/job/test-job/foo@base64/=')
end
- 1
it 'URL-encodes all other non-URL-safe characters' do
- 1
push = Prometheus::Client::Push.new(job: '<bar job>', grouping_key: { foo_label: '<bar value>' })
- 1
expected = '/metrics/job/%3Cbar%20job%3E/foo_label/%3Cbar%20value%3E'
- 1
expect(push.path).to eql(expected)
end
end
- 1
describe '#request' do
- 5
let(:content_type) { Prometheus::Client::Formats::Text::CONTENT_TYPE }
- 4
let(:data) { Prometheus::Client::Formats::Text.marshal(registry) }
- 8
let(:uri) { URI.parse("#{gateway}/metrics/job/test-job") }
- 1
let(:response) do
- 4
double(
:response,
code: '200',
message: 'OK',
body: 'Everything worked'
)
end
- 1
it 'sends marshalled registry to the specified gateway' do
- 1
request = double(:request)
- 1
expect(request).to receive(:content_type=).with(content_type)
- 1
expect(request).to receive(:body=).with(data)
- 1
expect(Net::HTTP::Post).to receive(:new).with(uri).and_return(request)
- 1
http = double(:http)
- 1
expect(http).to receive(:use_ssl=).with(false)
- 1
expect(http).to receive(:open_timeout=).with(5)
- 1
expect(http).to receive(:read_timeout=).with(30)
- 1
expect(http).to receive(:request).with(request).and_return(response)
- 1
expect(Net::HTTP).to receive(:new).with('localhost', 9091).and_return(http)
- 1
push.send(:request, Net::HTTP::Post, registry)
end
- 1
context 'for a 3xx response' do
- 1
let(:response) do
- 1
double(
:response,
code: '301',
message: 'Moved Permanently',
body: 'Probably no body, but technically you can return one'
)
end
- 1
it 'raises a redirect error' do
- 1
request = double(:request)
- 1
allow(request).to receive(:content_type=)
- 1
allow(request).to receive(:body=)
- 1
allow(Net::HTTP::Post).to receive(:new).with(uri).and_return(request)
- 1
http = double(:http)
- 1
allow(http).to receive(:use_ssl=)
- 1
allow(http).to receive(:open_timeout=)
- 1
allow(http).to receive(:read_timeout=)
- 1
allow(http).to receive(:request).with(request).and_return(response)
- 1
allow(Net::HTTP).to receive(:new).with('localhost', 9091).and_return(http)
- 2
expect { push.send(:request, Net::HTTP::Post, registry) }.to raise_error(
Prometheus::Client::Push::HttpRedirectError
)
end
end
- 1
context 'for a 4xx response' do
- 1
let(:response) do
- 1
double(
:response,
code: '400',
message: 'Bad Request',
body: 'Info on why the request was bad'
)
end
- 1
it 'raises a client error' do
- 1
request = double(:request)
- 1
allow(request).to receive(:content_type=)
- 1
allow(request).to receive(:body=)
- 1
allow(Net::HTTP::Post).to receive(:new).with(uri).and_return(request)
- 1
http = double(:http)
- 1
allow(http).to receive(:use_ssl=)
- 1
allow(http).to receive(:open_timeout=)
- 1
allow(http).to receive(:read_timeout=)
- 1
allow(http).to receive(:request).with(request).and_return(response)
- 1
allow(Net::HTTP).to receive(:new).with('localhost', 9091).and_return(http)
- 2
expect { push.send(:request, Net::HTTP::Post, registry) }.to raise_error(
Prometheus::Client::Push::HttpClientError
)
end
end
- 1
context 'for a 5xx response' do
- 1
let(:response) do
- 1
double(
:response,
code: '500',
message: 'Internal Server Error',
body: 'Apology for the server code being broken'
)
end
- 1
it 'raises a server error' do
- 1
request = double(:request)
- 1
allow(request).to receive(:content_type=)
- 1
allow(request).to receive(:body=)
- 1
allow(Net::HTTP::Post).to receive(:new).with(uri).and_return(request)
- 1
http = double(:http)
- 1
allow(http).to receive(:use_ssl=)
- 1
allow(http).to receive(:open_timeout=)
- 1
allow(http).to receive(:read_timeout=)
- 1
allow(http).to receive(:request).with(request).and_return(response)
- 1
allow(Net::HTTP).to receive(:new).with('localhost', 9091).and_return(http)
- 2
expect { push.send(:request, Net::HTTP::Post, registry) }.to raise_error(
Prometheus::Client::Push::HttpServerError
)
end
end
- 1
it 'deletes data from the registry' do
- 1
request = double(:request)
- 1
expect(request).to receive(:content_type=).with(content_type)
- 1
expect(Net::HTTP::Delete).to receive(:new).with(uri).and_return(request)
- 1
http = double(:http)
- 1
expect(http).to receive(:use_ssl=).with(false)
- 1
expect(http).to receive(:open_timeout=).with(5)
- 1
expect(http).to receive(:read_timeout=).with(30)
- 1
expect(http).to receive(:request).with(request).and_return(response)
- 1
expect(Net::HTTP).to receive(:new).with('localhost', 9091).and_return(http)
- 1
push.send(:request, Net::HTTP::Delete)
end
- 1
context 'HTTPS support' do
- 2
let(:gateway) { 'https://localhost:9091' }
- 1
it 'uses HTTPS when requested' do
- 1
request = double(:request)
- 1
expect(request).to receive(:content_type=).with(content_type)
- 1
expect(request).to receive(:body=).with(data)
- 1
expect(Net::HTTP::Post).to receive(:new).with(uri).and_return(request)
- 1
http = double(:http)
- 1
expect(http).to receive(:use_ssl=).with(true)
- 1
expect(http).to receive(:open_timeout=).with(5)
- 1
expect(http).to receive(:read_timeout=).with(30)
- 1
expect(http).to receive(:request).with(request).and_return(response)
- 1
expect(Net::HTTP).to receive(:new).with('localhost', 9091).and_return(http)
- 1
push.send(:request, Net::HTTP::Post, registry)
end
end
- 1
context 'Basic Auth support' do
- 1
context 'when credentials are passed in the gateway URL' do
- 2
let(:gateway) { 'https://super:secret@localhost:9091' }
- 1
it "raises an ArgumentError explaining why we don't support that mechanism" do
- 2
expect { push }.to raise_error ArgumentError, /in the gateway URL.*username `super`/m
end
end
- 1
context 'when credentials are passed to the separate `basic_auth` method' do
- 2
let(:gateway) { 'https://localhost:9091' }
- 1
it 'passes the credentials on to the HTTP client' do
- 1
request = double(:request)
- 1
expect(request).to receive(:content_type=).with(content_type)
- 1
expect(request).to receive(:basic_auth).with('super', 'secret')
- 1
expect(request).to receive(:body=).with(data)
- 1
expect(Net::HTTP::Put).to receive(:new).with(uri).and_return(request)
- 1
http = double(:http)
- 1
expect(http).to receive(:use_ssl=).with(true)
- 1
expect(http).to receive(:open_timeout=).with(5)
- 1
expect(http).to receive(:read_timeout=).with(30)
- 1
expect(http).to receive(:request).with(request).and_return(response)
- 1
expect(Net::HTTP).to receive(:new).with('localhost', 9091).and_return(http)
- 1
push.basic_auth("super", "secret")
- 1
push.send(:request, Net::HTTP::Put, registry)
end
end
end
- 1
context 'with a grouping key that clashes with a metric label' do
- 2
let(:grouping_key) { { foo: "bar"} }
- 1
before do
- 1
gauge = Prometheus::Client::Gauge.new(
:test_gauge,
'test docstring',
foo: nil
)
- 1
registry.register(gauge)
- 1
gauge.set({ foo: "bar"}, 42)
end
- 1
it 'raises an error when grouping key labels conflict with metric labels' do
- 2
expect { push.send(:request, Net::HTTP::Post, registry) }.to raise_error(
Prometheus::Client::LabelSetValidator::InvalidLabelSetError
)
end
end
end
end
# encoding: UTF-8
- 1
require 'rack/test'
- 1
require 'prometheus/client/rack/collector'
- 1
describe Prometheus::Client::Rack::Collector do
- 1
include Rack::Test::Methods
- 1
before do
- 5
allow(Prometheus::Client.configuration).to receive(:value_class).and_return(Prometheus::Client::SimpleValue)
end
- 1
let(:registry) do
- 5
Prometheus::Client::Registry.new
end
- 1
let(:original_app) do
- 8
->(_) { [200, { 'Content-Type' => 'text/html' }, ['OK']] }
end
- 1
let!(:app) do
- 4
described_class.new(original_app, registry: registry)
end
- 1
it 'returns the app response' do
- 1
get '/foo'
- 1
expect(last_response).to be_ok
- 1
expect(last_response.body).to eql('OK')
end
- 1
it 'propagates errors in the registry' do
- 1
counter = registry.get(:http_requests_total)
- 1
expect(counter).to receive(:increment).and_raise(NoMethodError)
- 2
expect { get '/foo' }.to raise_error(NoMethodError)
end
- 1
it 'traces request information' do
# expect(Time).to receive(:now).and_return(Time.at(0.0), Time.at(0.2))
- 1
labels = { method: 'get', host: 'example.org', path: '/foo', code: '200' }
- 1
get '/foo'
- 1
{
http_requests_total: 1.0,
# http_request_duration_seconds: { 0.5 => 0.2, 0.9 => 0.2, 0.99 => 0.2 }, # TODO: Fix summaries
}.each do |metric, result|
- 1
expect(registry.get(metric).get(labels)).to eql(result)
end
end
- 1
context 'when the app raises an exception' do
- 1
let(:original_app) do
- 1
lambda do |env|
- 2
raise NoMethodError if env['PATH_INFO'] == '/broken'
- 1
[200, { 'Content-Type' => 'text/html' }, ['OK']]
end
end
- 1
before do
- 1
get '/foo'
end
- 1
it 'traces exceptions' do
- 1
labels = { exception: 'NoMethodError' }
- 2
expect { get '/broken' }.to raise_error NoMethodError
- 1
expect(registry.get(:http_exceptions_total).get(labels)).to eql(1.0)
end
end
- 1
context 'setting up with a block' do
- 1
let(:app) do
- 1
described_class.new(original_app, registry: registry) do |env|
- 1
{ method: env['REQUEST_METHOD'].downcase } # and ignore the path
end
end
- 1
it 'allows labels configuration' do
- 1
get '/foo/bar'
- 1
labels = { method: 'get', code: '200' }
- 1
expect(registry.get(:http_requests_total).get(labels)).to eql(1.0)
end
end
end
# encoding: UTF-8
- 1
require 'rack/test'
- 1
require 'prometheus/client/rack/exporter'
- 1
xdescribe Prometheus::Client::Rack::Exporter do
- 1
include Rack::Test::Methods
- 1
let(:registry) do
Prometheus::Client::Registry.new
end
- 1
let(:app) do
app = ->(_) { [200, { 'Content-Type' => 'text/html' }, ['OK']] }
Prometheus::Client::Rack::Exporter.new(app, registry: registry)
end
- 1
context 'when requesting app endpoints' do
- 1
it 'returns the app response' do
get '/foo'
expect(last_response).to be_ok
expect(last_response.body).to eql('OK')
end
end
- 1
context 'when requesting /metrics' do
- 1
text = Prometheus::Client::Formats::Text
- 1
shared_examples 'ok' do |headers, fmt|
- 7
it "responds with 200 OK and Content-Type #{fmt::CONTENT_TYPE}" do
registry.counter(:foo, 'foo counter').increment({}, 9)
get '/metrics', nil, headers
expect(last_response.status).to eql(200)
expect(last_response.header['Content-Type']).to eql(fmt::CONTENT_TYPE)
expect(last_response.body).to eql(fmt.marshal(registry))
end
end
- 1
shared_examples 'not acceptable' do |headers|
- 2
it 'responds with 406 Not Acceptable' do
message = 'Supported media types: text/plain'
get '/metrics', nil, headers
expect(last_response.status).to eql(406)
expect(last_response.header['Content-Type']).to eql('text/plain')
expect(last_response.body).to eql(message)
end
end
- 1
context 'when client does not send a Accept header' do
- 1
include_examples 'ok', {}, text
end
- 1
context 'when client accpets any media type' do
- 1
include_examples 'ok', { 'HTTP_ACCEPT' => '*/*' }, text
end
- 1
context 'when client requests application/json' do
- 1
include_examples 'not acceptable', 'HTTP_ACCEPT' => 'application/json'
end
- 1
context 'when client requests text/plain' do
- 1
include_examples 'ok', { 'HTTP_ACCEPT' => 'text/plain' }, text
end
- 1
context 'when client uses different white spaces in Accept header' do
- 1
accept = 'text/plain;q=1.0 ; version=0.0.4'
- 1
include_examples 'ok', { 'HTTP_ACCEPT' => accept }, text
end
- 1
context 'when client does not include quality attribute' do
- 1
accept = 'application/json;q=0.5, text/plain'
- 1
include_examples 'ok', { 'HTTP_ACCEPT' => accept }, text
end
- 1
context 'when client accepts some unknown formats' do
- 1
accept = 'text/plain;q=0.3, proto/buf;q=0.7'
- 1
include_examples 'ok', { 'HTTP_ACCEPT' => accept }, text
end
- 1
context 'when client accepts only unknown formats' do
- 1
accept = 'fancy/woo;q=0.3, proto/buf;q=0.7'
- 1
include_examples 'not acceptable', 'HTTP_ACCEPT' => accept
end
- 1
context 'when client accepts unknown formats and wildcard' do
- 1
accept = 'fancy/woo;q=0.3, proto/buf;q=0.7, */*;q=0.1'
- 1
include_examples 'ok', { 'HTTP_ACCEPT' => accept }, text
end
end
end
# encoding: UTF-8
- 1
require 'thread'
- 1
require 'prometheus/client/registry'
- 1
describe Prometheus::Client::Registry do
- 13
let(:registry) { Prometheus::Client::Registry.new }
- 1
describe '.new' do
- 1
it 'returns a new registry instance' do
- 1
expect(registry).to be_a(Prometheus::Client::Registry)
end
end
- 1
describe '#register' do
- 1
it 'registers a new metric container and returns it' do
- 1
metric = double(name: :test)
- 1
expect(registry.register(metric)).to eql(metric)
end
- 1
it 'raises an exception if a metric name gets registered twice' do
- 1
metric = double(name: :test)
- 1
registry.register(metric)
- 1
expect do
- 1
registry.register(metric)
end.to raise_exception described_class::AlreadyRegisteredError
end
- 1
it 'is thread safe' do
- 1
mutex = Mutex.new
- 1
containers = []
- 1
def registry.exist?(*args)
- 10
super.tap { sleep(0.01) }
end
- 1
Array.new(5) do
- 5
Thread.new do
result = begin
- 5
registry.register(double(name: :test))
rescue Prometheus::Client::Registry::AlreadyRegisteredError
- 4
nil
end
- 10
mutex.synchronize { containers << result }
end
end.each(&:join)
- 1
expect(containers.compact.size).to eql(1)
end
end
- 1
describe '#counter' do
- 1
it 'registers a new counter metric container and returns the counter' do
- 1
metric = registry.counter(:test, 'test docstring')
- 1
expect(metric).to be_a(Prometheus::Client::Counter)
end
end
- 1
describe '#gauge' do
- 1
it 'registers a new gauge metric container and returns the gauge' do
- 1
metric = registry.gauge(:test, 'test docstring')
- 1
expect(metric).to be_a(Prometheus::Client::Gauge)
end
end
- 1
describe '#summary' do
- 1
it 'registers a new summary metric container and returns the summary' do
- 1
metric = registry.summary(:test, 'test docstring')
- 1
expect(metric).to be_a(Prometheus::Client::Summary)
end
end
- 1
describe '#histogram' do
- 1
it 'registers a new histogram metric container and returns the histogram' do
- 1
metric = registry.histogram(:test, 'test docstring')
- 1
expect(metric).to be_a(Prometheus::Client::Histogram)
end
end
- 1
describe '#exist?' do
- 1
it 'returns true if a metric name has been registered' do
- 1
registry.register(double(name: :test))
- 1
expect(registry.exist?(:test)).to eql(true)
end
- 1
it 'returns false if a metric name has not been registered yet' do
- 1
expect(registry.exist?(:test)).to eql(false)
end
end
- 1
describe '#get' do
- 1
it 'returns a previously registered metric container' do
- 1
registry.register(double(name: :test))
- 1
expect(registry.get(:test)).to be
end
- 1
it 'returns nil if the metric has not been registered yet' do
- 1
expect(registry.get(:test)).to eql(nil)
end
end
end
# encoding: UTF-8
- 1
require 'prometheus/client/summary'
- 1
require 'examples/metric_example'
- 1
describe Prometheus::Client::Summary do
- 2
let(:summary) { Prometheus::Client::Summary.new(:bar, 'bar description') }
- 1
it_behaves_like Prometheus::Client::Metric do
- 3
let(:type) { Float }
end
- 1
describe '#observe' do
- 1
it 'records the given value' do
- 1
expect do
- 1
summary.observe({}, 5)
- 2
end.to change { summary.get }
end
end
- 1
xdescribe '#get' do
- 1
before do
summary.observe({ foo: 'bar' }, 3)
summary.observe({ foo: 'bar' }, 5.2)
summary.observe({ foo: 'bar' }, 13)
summary.observe({ foo: 'bar' }, 4)
end
- 1
it 'returns a set of quantile values' do
expect(summary.get(foo: 'bar')).to eql(0.5 => 4, 0.9 => 5.2, 0.99 => 5.2)
end
- 1
it 'returns a value which responds to #sum and #total' do
value = summary.get(foo: 'bar')
expect(value.sum).to eql(25.2)
expect(value.total).to eql(4)
end
- 1
it 'uses nil as default value' do
expect(summary.get({})).to eql(0.5 => nil, 0.9 => nil, 0.99 => nil)
end
end
- 1
xdescribe '#values' do
- 1
it 'returns a hash of all recorded summaries' do
summary.observe({ status: 'bar' }, 3)
summary.observe({ status: 'foo' }, 5)
expect(summary.values).to eql(
{ status: 'bar' } => { 0.5 => 3, 0.9 => 3, 0.99 => 3 },
{ status: 'foo' } => { 0.5 => 5, 0.9 => 5, 0.99 => 5 },
)
end
end
end
- 1
require 'spec_helper'
- 1
require 'prometheus/client/support/puma'
- 1
class FakePumaWorker
- 1
attr_reader :index
- 1
def initialize(index)
- 1
@index = index
end
end
- 1
describe Prometheus::Client::Support::Puma do
- 1
describe '.worker_pid_provider' do
- 1
let(:worker_id) { '2' }
- 3
let(:program_name) { $PROGRAM_NAME }
- 5
subject(:worker_pid_provider) { described_class.worker_pid_provider }
- 1
before do
- 4
expect(described_class).to receive(:program_name)
.at_least(:once)
.and_return(program_name)
end
- 1
context 'when the current process is a Puma cluster worker' do
- 1
context 'when the process name contains a worker id' do
- 2
let(:program_name) { 'puma: cluster worker 2: 34740 [my-app]' }
- 2
it { is_expected.to eq('puma_2') }
end
- 1
context 'when the process name does not include a worker id' do
- 2
let(:worker_number) { 10 }
- 1
before do
- 1
stub_const('Puma::Cluster::Worker', FakePumaWorker)
- 1
FakePumaWorker.new(worker_number)
end
- 2
it { is_expected.to eq("puma_#{worker_number}") }
end
end
- 1
context 'when the current process is the Puma master' do
- 2
let(:program_name) { 'bin/puma' }
- 2
it { is_expected.to eq('puma_master') }
end
- 1
context 'when it cannot be determined that Puma is running' do
- 2
let(:process_id) { 10 }
- 1
before do
- 1
allow(Process).to receive(:pid).and_return(process_id)
end
- 2
it { is_expected.to eq("process_id_#{process_id}") }
end
end
end
- 1
require 'spec_helper'
- 1
require 'prometheus/client/support/unicorn'
- 1
class FakeUnicornWorker
- 1
attr_reader :nr
- 1
def initialize(nr)
- 2
@nr = nr
end
end
- 1
describe Prometheus::Client::Support::Unicorn do
- 1
describe '.worker_id' do
- 3
let(:worker_id) { '09' }
- 1
around do |example|
- 2
old_name = $0
- 2
example.run
- 2
$0 = old_name
end
- 1
context 'process name contains worker id' do
- 1
before do
- 1
$0 = "program worker[#{worker_id}] arguments"
end
- 1
it 'returns worker_id' do
- 1
expect(subject.worker_id).to eq(worker_id)
end
end
- 1
context 'process name is without worker id' do
- 1
it 'calls .object_based_worker_id id provider' do
- 1
expect(subject).to receive(:object_based_worker_id).and_return(worker_id)
- 1
expect(subject.worker_id).to eq(worker_id)
end
end
end
- 1
describe '.object_based_worker_id' do
- 1
context 'when Unicorn is defined' do
- 1
before do
- 4
stub_const('Unicorn::Worker', FakeUnicornWorker)
end
- 1
context 'Worker instance is present in ObjectSpace' do
- 3
let(:worker_number) { 10 }
- 3
let!(:unicorn_worker) { FakeUnicornWorker.new(worker_number) }
- 1
it 'Unicorn::Worker to be defined' do
- 1
expect(defined?(Unicorn::Worker)).to be_truthy
end
- 1
it 'returns worker id' do
- 1
expect(described_class.object_based_worker_id).to eq(worker_number)
end
end
- 1
context 'Worker instance is not present in ObjectSpace' do
- 1
it 'Unicorn::Worker id defined' do
- 1
expect(defined?(Unicorn::Worker)).to be_truthy
end
- 1
it 'returns no worker id' do
- 1
expect(ObjectSpace).to receive(:each_object).with(::Unicorn::Worker).and_return(nil)
- 1
expect(described_class.object_based_worker_id).to eq(nil)
end
end
end
- 1
context 'Unicorn::Worker is not defined' do
- 1
it 'Unicorn::Worker not defined' do
- 1
expect(defined?(Unicorn::Worker)).to be_falsey
end
- 1
it 'returns no worker_id' do
- 1
expect(described_class.object_based_worker_id).to eq(nil)
end
end
end
- 1
describe '.worker_pid_provider' do
- 1
context 'worker_id is provided' do
- 2
let(:worker_id) { 2 }
- 1
before do
- 1
allow(described_class).to receive(:worker_id).and_return(worker_id)
end
- 1
it 'returns worker pid created from worker id' do
- 1
expect(described_class.worker_pid_provider).to eq("worker_id_#{worker_id}")
end
end
- 1
context 'worker_id is not provided' do
- 2
let(:process_id) { 10 }
- 1
before do
- 1
allow(described_class).to receive(:worker_id).and_return(nil)
- 1
allow(Process).to receive(:pid).and_return(process_id)
end
- 1
it 'returns worker pid created from Process ID' do
- 1
expect(described_class.worker_pid_provider).to eq("process_id_#{process_id}")
end
end
end
end
# encoding: UTF-8
- 1
require 'prometheus/client'
- 1
describe Prometheus::Client do
- 1
describe '.registry' do
- 1
it 'returns a registry object' do
- 1
expect(described_class.registry).to be_a(described_class::Registry)
end
- 1
it 'memorizes the returned object' do
- 1
expect(described_class.registry).to eql(described_class.registry)
end
end
- 1
context '.reset! and .reinitialize_on_pid_change' do
- 5
let(:metric_name) { :room_temperature_celsius }
- 5
let(:label) { { room: 'kitchen' } }
- 5
let(:value) { 21 }
- 5
let(:gauge) { Prometheus::Client::Gauge.new(metric_name, 'test') }
- 1
before do
- 4
described_class.cleanup!
- 4
described_class.reset! # registering metrics will leak into other specs
- 4
registry = described_class.registry
- 4
gauge.set(label, value)
- 4
registry.register(gauge)
- 4
expect(registry.metrics.count).to eq(1)
- 4
expect(registry.get(metric_name).get(label)).to eq(value)
end
- 1
describe '.reset!' do
- 1
it 'resets registry and clears existing metrics' do
- 1
described_class.cleanup!
- 1
described_class.reset!
- 1
registry = described_class.registry
- 1
expect(registry.metrics.count).to eq(0)
- 1
registry.register(gauge)
- 1
expect(registry.get(metric_name).get(label)).not_to eq(value)
end
end
- 1
describe '.reinitialize_on_pid_change' do
- 1
context 'with force: false' do
- 1
it 'calls `MmapedValue.reinitialize_on_pid_change`' do
- 1
expect(Prometheus::Client::MmapedValue).to receive(:reinitialize_on_pid_change).and_call_original
- 1
described_class.reinitialize_on_pid_change(force: false)
end
end
- 1
context 'without explicit :force param' do
- 1
it 'defaults to `false` and calls `MmapedValue.reinitialize_on_pid_change`' do
- 1
expect(Prometheus::Client::MmapedValue).to receive(:reinitialize_on_pid_change).and_call_original
- 1
described_class.reinitialize_on_pid_change
end
end
- 1
context 'with force: true' do
- 1
it 'calls `MmapedValue.reset_and_reinitialize`' do
- 1
expect(Prometheus::Client::MmapedValue).to receive(:reset_and_reinitialize).and_call_original
- 1
described_class.reinitialize_on_pid_change(force: true)
end
end
end
end
end