loading
Generated 2023-10-01T01:13:49+00:00

All Files ( 91.74% covered at 1424.23 hits/line )

46 files in total.
1936 relevant lines, 1776 lines covered and 160 lines missed. ( 91.74% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/prometheus/client.rb 96.15 % 58 26 25 1 1572.12
lib/prometheus/client/configuration.rb 94.12 % 24 17 16 1 0.94
lib/prometheus/client/counter.rb 100.00 % 27 13 13 0 21581.08
lib/prometheus/client/formats/text.rb 89.29 % 96 56 50 6 2.73
lib/prometheus/client/gauge.rb 94.74 % 40 19 18 1 14.74
lib/prometheus/client/helper/entry_parser.rb 61.97 % 132 71 44 27 71.46
lib/prometheus/client/helper/file_locker.rb 76.67 % 50 30 23 7 37.63
lib/prometheus/client/helper/json_parser.rb 75.00 % 23 12 9 3 0.83
lib/prometheus/client/helper/loader.rb 89.47 % 44 19 17 2 1.00
lib/prometheus/client/helper/metrics_processing.rb 25.00 % 45 24 6 18 0.25
lib/prometheus/client/helper/metrics_representation.rb 100.00 % 51 29 29 0 7.45
lib/prometheus/client/helper/mmaped_file.rb 97.44 % 75 39 38 1 37.33
lib/prometheus/client/helper/plain_file.rb 100.00 % 29 15 15 0 56.67
lib/prometheus/client/histogram.rb 100.00 % 80 40 40 0 16.80
lib/prometheus/client/label_set_validator.rb 100.00 % 85 42 42 0 9599.10
lib/prometheus/client/metric.rb 95.45 % 80 44 42 2 13676.27
lib/prometheus/client/mmaped_dict.rb 92.11 % 79 38 35 3 2200.34
lib/prometheus/client/mmaped_value.rb 95.40 % 154 87 83 4 13470.08
lib/prometheus/client/page_size.rb 100.00 % 17 10 10 0 3.00
lib/prometheus/client/push.rb 100.00 % 203 95 95 0 6.64
lib/prometheus/client/rack/collector.rb 97.50 % 88 40 39 1 3.18
lib/prometheus/client/rack/exporter.rb 38.30 % 98 47 18 29 0.38
lib/prometheus/client/registry.rb 100.00 % 65 33 33 0 18.21
lib/prometheus/client/simple_value.rb 100.00 % 31 16 16 0 14.44
lib/prometheus/client/summary.rb 91.43 % 69 35 32 3 4.51
lib/prometheus/client/support/puma.rb 96.15 % 44 26 25 1 2.31
lib/prometheus/client/support/unicorn.rb 100.00 % 35 20 20 0 1.40
lib/prometheus/client/uses_value_type.rb 72.73 % 20 11 8 3 7318.36
spec/examples/metric_example.rb 100.00 % 47 23 23 0 4.57
spec/prometheus/client/counter_spec.rb 100.00 % 67 40 40 0 2004.55
spec/prometheus/client/formats/text_spec.rb 92.31 % 251 52 48 4 2.29
spec/prometheus/client/gauge_spec.rb 100.00 % 70 46 46 0 1.63
spec/prometheus/client/helpers/json_parser_spec.rb 100.00 % 32 18 18 0 1.44
spec/prometheus/client/helpers/mmaped_file_spec.rb 100.00 % 113 56 56 0 1.96
spec/prometheus/client/histogram_spec.rb 66.67 % 83 45 30 15 0.93
spec/prometheus/client/label_set_validator_spec.rb 100.00 % 79 40 40 0 1.53
spec/prometheus/client/mmaped_dict_spec.rb 100.00 % 185 91 91 0 17.09
spec/prometheus/client/mmaped_value_spec.rb 100.00 % 148 84 84 0 2.85
spec/prometheus/client/push_spec.rb 100.00 % 326 175 175 0 1.41
spec/prometheus/client/rack/collector_spec.rb 100.00 % 90 44 44 0 1.48
spec/prometheus/client/rack/exporter_spec.rb 69.81 % 100 53 37 16 0.83
spec/prometheus/client/registry_spec.rb 100.00 % 110 55 55 0 1.75
spec/prometheus/client/summary_spec.rb 60.00 % 56 30 18 12 0.73
spec/prometheus/client/support/puma_spec.rb 100.00 % 60 31 31 0 1.55
spec/prometheus/client/support/unicorn_spec.rb 100.00 % 108 58 58 0 1.26
spec/prometheus/client_spec.rb 100.00 % 73 41 41 0 1.90

lib/prometheus/client.rb

96.15% lines covered

26 relevant lines. 25 lines covered and 1 lines missed.
    
  1. 1 require 'prometheus/client/registry'
  2. 1 require 'prometheus/client/configuration'
  3. 1 require 'prometheus/client/mmaped_value'
  4. 1 module Prometheus
  5. # Client is a ruby implementation for a Prometheus compatible client.
  6. 1 module Client
  7. 1 class << self
  8. 1 attr_writer :configuration
  9. 1 def configuration
  10. 40695 @configuration ||= Configuration.new
  11. end
  12. 1 def configure
  13. yield(configuration)
  14. end
  15. # Returns a default registry object
  16. 1 def registry
  17. 8 @registry ||= Registry.new
  18. end
  19. 1 def logger
  20. 3 configuration.logger
  21. end
  22. 1 def pid
  23. 111 configuration.pid_provider.call
  24. end
  25. # Resets the registry and reinitializes all metrics files.
  26. # Use case: clean up everything in specs `before` block,
  27. # to prevent leaking the state between specs which are updating metrics.
  28. 1 def reset!
  29. 5 @registry = nil
  30. 5 ::Prometheus::Client::MmapedValue.reset_and_reinitialize
  31. end
  32. 1 def cleanup!
  33. 27 Dir.glob("#{configuration.multiprocess_files_dir}/*.db").each { |f| File.unlink(f) if File.exist?(f) }
  34. end
  35. # With `force: false`: reinitializes metric files only for processes with the changed PID.
  36. # With `force: true`: reinitializes all metrics files.
  37. # Always keeps the registry.
  38. # Use case (`force: false`): pick up new metric files on each worker start,
  39. # without resetting already registered files for the master or previously initialized workers.
  40. 1 def reinitialize_on_pid_change(force: false)
  41. 3 if force
  42. 1 ::Prometheus::Client::MmapedValue.reset_and_reinitialize
  43. else
  44. 2 ::Prometheus::Client::MmapedValue.reinitialize_on_pid_change
  45. end
  46. end
  47. end
  48. end
  49. end

lib/prometheus/client/configuration.rb

94.12% lines covered

17 relevant lines. 16 lines covered and 1 lines missed.
    
  1. 1 require 'prometheus/client/registry'
  2. 1 require 'prometheus/client/mmaped_value'
  3. 1 require 'prometheus/client/page_size'
  4. 1 require 'logger'
  5. 1 require 'tmpdir'
  6. 1 module Prometheus
  7. 1 module Client
  8. 1 class Configuration
  9. 1 attr_accessor :value_class, :multiprocess_files_dir, :initial_mmap_file_size, :logger, :pid_provider, :rust_multiprocess_metrics
  10. 1 def initialize
  11. 1 @value_class = ::Prometheus::Client::MmapedValue
  12. 1 @initial_mmap_file_size = ::Prometheus::Client::PageSize.page_size(fallback_page_size: 4096)
  13. 1 @logger = Logger.new($stdout)
  14. 1 @pid_provider = Process.method(:pid)
  15. 1 @rust_multiprocess_metrics = ENV.fetch('prometheus_rust_multiprocess_metrics', 'true') == 'true'
  16. 1 @multiprocess_files_dir = ENV.fetch('prometheus_multiproc_dir') do
  17. Dir.mktmpdir("prometheus-mmap")
  18. end
  19. end
  20. end
  21. end
  22. end

lib/prometheus/client/counter.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. # encoding: UTF-8
  2. 1 require 'prometheus/client/metric'
  3. 1 module Prometheus
  4. 1 module Client
  5. # Counter is a metric that exposes merely a sum or tally of things.
  6. 1 class Counter < Metric
  7. 1 def type
  8. 80047 :counter
  9. end
  10. 1 def increment(labels = {}, by = 1)
  11. 40120 raise ArgumentError, 'increment must be a non-negative number' if by < 0
  12. 40119 label_set = label_set_for(labels)
  13. 80238 synchronize { @values[label_set].increment(by) }
  14. end
  15. 1 private
  16. 1 def default(labels)
  17. 40022 value_object(type, @name, @name, labels)
  18. end
  19. end
  20. end
  21. end

lib/prometheus/client/formats/text.rb

89.29% lines covered

56 relevant lines. 50 lines covered and 6 lines missed.
    
  1. 1 require 'prometheus/client/uses_value_type'
  2. 1 require 'prometheus/client/helper/json_parser'
  3. 1 require 'prometheus/client/helper/loader'
  4. 1 require 'prometheus/client/helper/plain_file'
  5. 1 require 'prometheus/client/helper/metrics_processing'
  6. 1 require 'prometheus/client/helper/metrics_representation'
  7. 1 module Prometheus
  8. 1 module Client
  9. 1 module Formats
  10. # Text format is human readable mainly used for manual inspection.
  11. 1 module Text
  12. 1 MEDIA_TYPE = 'text/plain'.freeze
  13. 1 VERSION = '0.0.4'.freeze
  14. 1 CONTENT_TYPE = "#{MEDIA_TYPE}; version=#{VERSION}".freeze
  15. 1 class << self
  16. 1 def marshal(registry)
  17. 10 metrics = registry.metrics.map do |metric|
  18. 5 samples = metric.values.flat_map do |label_set, value|
  19. 7 representation(metric, label_set, value)
  20. end
  21. 5 [metric.name, { type: metric.type, help: metric.docstring, samples: samples }]
  22. end
  23. 10 Helper::MetricsRepresentation.to_text(metrics)
  24. end
  25. 1 def marshal_multiprocess(path = Prometheus::Client.configuration.multiprocess_files_dir, use_rust: true)
  26. 4 file_list = Dir.glob(File.join(path, '*.db')).sort
  27. 20 .map {|f| Helper::PlainFile.new(f) }
  28. 20 .map {|f| [f.filepath, f.multiprocess_mode.to_sym, f.type.to_sym, f.pid] }
  29. 4 if use_rust && Prometheus::Client::Helper::Loader.rust_impl_available?
  30. 3 FastMmapedFileRs.to_metrics(file_list.to_a)
  31. else
  32. 1 FastMmapedFile.to_metrics(file_list.to_a)
  33. end
  34. end
  35. 1 def rust_impl_available?
  36. return @rust_available unless @rust_available.nil?
  37. check_for_rust
  38. end
  39. 1 private
  40. 1 def load_metrics(path)
  41. metrics = {}
  42. Dir.glob(File.join(path, '*.db')).sort.each do |f|
  43. Helper::PlainFile.new(f).to_metrics(metrics)
  44. end
  45. metrics
  46. end
  47. 1 def representation(metric, label_set, value)
  48. 7 labels = metric.base_labels.merge(label_set)
  49. 7 if metric.type == :summary
  50. 1 summary(metric.name, labels, value)
  51. 6 elsif metric.type == :histogram
  52. 1 histogram(metric.name, labels, value)
  53. else
  54. 5 [[metric.name, labels, value.get]]
  55. end
  56. end
  57. 1 def summary(name, set, value)
  58. 1 rv = value.get.map do |q, v|
  59. 3 [name, set.merge(quantile: q), v]
  60. end
  61. 1 rv << ["#{name}_sum", set, value.get.sum]
  62. 1 rv << ["#{name}_count", set, value.get.total]
  63. 1 rv
  64. end
  65. 1 def histogram(name, set, value)
  66. # |metric_name, labels, value|
  67. 1 rv = value.get.map do |q, v|
  68. 3 [name, set.merge(le: q), v]
  69. end
  70. 1 rv << [name, set.merge(le: '+Inf'), value.get.total]
  71. 1 rv << ["#{name}_sum", set, value.get.sum]
  72. 1 rv << ["#{name}_count", set, value.get.total]
  73. 1 rv
  74. end
  75. end
  76. end
  77. end
  78. end
  79. end

lib/prometheus/client/gauge.rb

94.74% lines covered

19 relevant lines. 18 lines covered and 1 lines missed.
    
  1. # encoding: UTF-8
  2. 1 require 'prometheus/client/metric'
  3. 1 module Prometheus
  4. 1 module Client
  5. # A Gauge is a metric that exposes merely an instantaneous value or some
  6. # snapshot thereof.
  7. 1 class Gauge < Metric
  8. 1 def initialize(name, docstring, base_labels = {}, multiprocess_mode=:all)
  9. 39 super(name, docstring, base_labels)
  10. 36 if value_class.multiprocess and ![:min, :max, :livesum, :liveall, :all].include?(multiprocess_mode)
  11. raise ArgumentError, 'Invalid multiprocess mode: ' + multiprocess_mode
  12. end
  13. 36 @multiprocess_mode = multiprocess_mode
  14. end
  15. 1 def type
  16. 80 :gauge
  17. end
  18. 1 def default(labels)
  19. 40 value_object(type, @name, @name, labels, @multiprocess_mode)
  20. end
  21. # Sets the value for the given label set
  22. 1 def set(labels, value)
  23. 34 @values[label_set_for(labels)].set(value)
  24. end
  25. 1 def increment(labels, value)
  26. 3 @values[label_set_for(labels)].increment(value)
  27. end
  28. 1 def decrement(labels, value)
  29. 2 @values[label_set_for(labels)].decrement(value)
  30. end
  31. end
  32. end
  33. end

lib/prometheus/client/helper/entry_parser.rb

61.97% lines covered

71 relevant lines. 44 lines covered and 27 lines missed.
    
  1. 1 require 'prometheus/client/helper/json_parser'
  2. 1 module Prometheus
  3. 1 module Client
  4. 1 module Helper
  5. 1 module EntryParser
  6. 1 class ParsingError < RuntimeError;
  7. end
  8. 1 MINIMUM_SIZE = 8
  9. 1 START_POSITION = 8
  10. 1 VALUE_BYTES = 8
  11. 1 ENCODED_LENGTH_BYTES = 4
  12. 1 def used
  13. 107 slice(0..3).unpack('l')[0]
  14. end
  15. 1 def parts
  16. 60 @parts ||= File.basename(filepath, '.db')
  17. .split('_')
  18. 78 .map { |e| e.gsub(/-\d+$/, '') } # remove trailing -number
  19. end
  20. 1 def type
  21. 20 parts[0].to_sym
  22. end
  23. 1 def pid
  24. 20 (parts[2..-1] || []).join('_')
  25. end
  26. 1 def multiprocess_mode
  27. 20 parts[1]
  28. end
  29. 1 def empty?
  30. 95 size < MINIMUM_SIZE || used.zero?
  31. end
  32. 1 def entries(ignore_errors = false)
  33. 95 return Enumerator.new {} if empty?
  34. 12 Enumerator.new do |yielder|
  35. 12 used_ = used # cache used to avoid unnecessary unpack operations
  36. 12 pos = START_POSITION # used + padding offset
  37. 12 while pos < used_ && pos < size && pos > 0
  38. 410 data = slice(pos..-1)
  39. 410 unless data
  40. raise ParsingError, "data slice is nil at pos #{pos}" unless ignore_errors
  41. pos += 8
  42. next
  43. end
  44. 410 encoded_len, first_encoded_bytes = data.unpack('LL')
  45. 410 if encoded_len.nil? || encoded_len.zero? || first_encoded_bytes.nil? || first_encoded_bytes.zero?
  46. # do not parse empty data
  47. pos += 8
  48. next
  49. end
  50. 410 entry_len = ENCODED_LENGTH_BYTES + encoded_len
  51. 410 padding_len = 8 - entry_len % 8
  52. 410 value_offset = entry_len + padding_len # align to 8 bytes
  53. 410 pos += value_offset
  54. 410 if value_offset > 0 && (pos + VALUE_BYTES) <= size # if positions are safe
  55. 410 yielder.yield data, encoded_len, value_offset, pos
  56. else
  57. raise ParsingError, "data slice is nil at pos #{pos}" unless ignore_errors
  58. end
  59. 410 pos += VALUE_BYTES
  60. end
  61. end
  62. end
  63. 1 def parsed_entries(ignore_errors = false)
  64. result = entries(ignore_errors).map do |data, encoded_len, value_offset, _|
  65. begin
  66. encoded, value = data.unpack(format('@4A%d@%dd', encoded_len, value_offset))
  67. [encoded, value]
  68. rescue ArgumentError => e
  69. Prometheus::Client.logger.debug("Error processing data: #{bin_to_hex(data[0, 7])} len: #{encoded_len} value_offset: #{value_offset}")
  70. raise ParsingError, e unless ignore_errors
  71. end
  72. end
  73. result.reject!(&:nil?) if ignore_errors
  74. result
  75. end
  76. 1 def to_metrics(metrics = {}, ignore_errors = false)
  77. parsed_entries(ignore_errors).each do |key, value|
  78. begin
  79. metric_name, name, labelnames, labelvalues = JsonParser.load(key)
  80. labelnames ||= []
  81. labelvalues ||= []
  82. metric = metrics.fetch(metric_name,
  83. metric_name: metric_name,
  84. help: 'Multiprocess metric',
  85. type: type,
  86. samples: [])
  87. if type == :gauge
  88. metric[:multiprocess_mode] = multiprocess_mode
  89. metric[:samples] += [[name, labelnames.zip(labelvalues) + [['pid', pid]], value]]
  90. else
  91. # The duplicates and labels are fixed in the next for.
  92. metric[:samples] += [[name, labelnames.zip(labelvalues), value]]
  93. end
  94. metrics[metric_name] = metric
  95. rescue JSON::ParserError => e
  96. raise ParsingError(e) unless ignore_errors
  97. end
  98. end
  99. metrics.reject! { |e| e.nil? } if ignore_errors
  100. metrics
  101. end
  102. 1 private
  103. 1 def bin_to_hex(s)
  104. s.each_byte.map { |b| b.to_s(16) }.join
  105. end
  106. end
  107. end
  108. end
  109. end

lib/prometheus/client/helper/file_locker.rb

76.67% lines covered

30 relevant lines. 23 lines covered and 7 lines missed.
    
  1. 1 module Prometheus
  2. 1 module Client
  3. 1 module Helper
  4. 1 class FileLocker
  5. 1 class << self
  6. 1 LOCK_FILE_MUTEX = Mutex.new
  7. 1 def lock_to_process(filepath)
  8. 111 LOCK_FILE_MUTEX.synchronize do
  9. 111 @file_locks ||= {}
  10. 111 return false if @file_locks[filepath]
  11. 99 file = File.open(filepath, 'ab')
  12. 99 if file.flock(File::LOCK_NB | File::LOCK_EX)
  13. 99 @file_locks[filepath] = file
  14. 99 return true
  15. else
  16. return false
  17. end
  18. end
  19. end
  20. 1 def unlock(filepath)
  21. 65 LOCK_FILE_MUTEX.synchronize do
  22. 65 @file_locks ||= {}
  23. 65 return false unless @file_locks[filepath]
  24. 49 file = @file_locks[filepath]
  25. 49 file.flock(File::LOCK_UN)
  26. 49 file.close
  27. 49 @file_locks.delete(filepath)
  28. end
  29. end
  30. 1 def unlock_all
  31. LOCK_FILE_MUTEX.synchronize do
  32. @file_locks ||= {}
  33. @file_locks.values.each do |file|
  34. file.flock(File::LOCK_UN)
  35. file.close
  36. end
  37. @file_locks = {}
  38. end
  39. end
  40. end
  41. end
  42. end
  43. end
  44. end

lib/prometheus/client/helper/json_parser.rb

75.0% lines covered

12 relevant lines. 9 lines covered and 3 lines missed.
    
  1. 1 require 'json'
  2. 1 module Prometheus
  3. 1 module Client
  4. 1 module Helper
  5. 1 module JsonParser
  6. 1 class << self
  7. 1 if defined?(Oj)
  8. def load(s)
  9. Oj.load(s)
  10. rescue Oj::ParseError, EncodingError => e
  11. raise JSON::ParserError.new(e.message)
  12. end
  13. else
  14. 1 def load(s)
  15. 2 JSON.parse(s)
  16. end
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

lib/prometheus/client/helper/loader.rb

89.47% lines covered

19 relevant lines. 17 lines covered and 2 lines missed.
    
  1. 1 module Prometheus
  2. 1 module Client
  3. 1 module Helper
  4. 1 module Loader
  5. 1 class << self
  6. 1 def rust_impl_available?
  7. 3 return @rust_available unless @rust_available.nil?
  8. 1 check_for_rust
  9. end
  10. 1 private
  11. 1 def load_rust_extension
  12. begin
  13. 1 ruby_version = /(\d+\.\d+)/.match(RUBY_VERSION)
  14. 1 require_relative "../../../#{ruby_version}/fast_mmaped_file_rs"
  15. 1 rescue LoadError
  16. 1 require 'fast_mmaped_file_rs'
  17. end
  18. end
  19. 1 def check_for_rust
  20. # This will be evaluated on each invocation even with `||=` if
  21. # `@rust_available` if false. Running a `require` statement is slow,
  22. # so the `rust_impl_available?` method memoizes the result, external
  23. # callers can only trigger this method a single time.
  24. @rust_available = begin
  25. 1 load_rust_extension
  26. 1 true
  27. rescue LoadError
  28. warn <<~WARN
  29. WARNING: The Rust extension for prometheus-client-mmap is unavailable, falling back to the legacy C extension.
  30. The Rust extension will be required in the next version. If you are compiling this gem from source,
  31. ensure your build system has a Rust compiler and clang: https://gitlab.com/gitlab-org/ruby/gems/prometheus-client-mmap
  32. WARN
  33. false
  34. end
  35. end
  36. end
  37. end
  38. end
  39. end
  40. end

lib/prometheus/client/helper/metrics_processing.rb

25.0% lines covered

24 relevant lines. 6 lines covered and 18 lines missed.
    
  1. 1 module Prometheus
  2. 1 module Client
  3. 1 module Helper
  4. 1 module MetricsProcessing
  5. 1 def self.merge_metrics(metrics)
  6. metrics.each_value do |metric|
  7. metric[:samples] = merge_samples(metric[:samples], metric[:type], metric[:multiprocess_mode]).map do |(name, labels), value|
  8. [name, labels.to_h, value]
  9. end
  10. end
  11. end
  12. 1 def self.merge_samples(raw_samples, metric_type, multiprocess_mode)
  13. samples = {}
  14. raw_samples.each do |name, labels, value|
  15. without_pid = labels.reject { |l| l[0] == 'pid' }
  16. case metric_type
  17. when :gauge
  18. case multiprocess_mode
  19. when 'min'
  20. s = samples.fetch([name, without_pid], value)
  21. samples[[name, without_pid]] = [s, value].min
  22. when 'max'
  23. s = samples.fetch([name, without_pid], value)
  24. samples[[name, without_pid]] = [s, value].max
  25. when 'livesum'
  26. s = samples.fetch([name, without_pid], 0.0)
  27. samples[[name, without_pid]] = s + value
  28. else # all/liveall
  29. samples[[name, labels]] = value
  30. end
  31. else
  32. # Counter, Histogram and Summary.
  33. s = samples.fetch([name, without_pid], 0.0)
  34. samples[[name, without_pid]] = s + value
  35. end
  36. end
  37. samples
  38. end
  39. end
  40. end
  41. end
  42. end

lib/prometheus/client/helper/metrics_representation.rb

100.0% lines covered

29 relevant lines. 29 lines covered and 0 lines missed.
    
  1. 1 module Prometheus
  2. 1 module Client
  3. 1 module Helper
  4. 1 module MetricsRepresentation
  5. 1 METRIC_LINE = '%s%s %s'.freeze
  6. 1 TYPE_LINE = '# TYPE %s %s'.freeze
  7. 1 HELP_LINE = '# HELP %s %s'.freeze
  8. 1 LABEL = '%s="%s"'.freeze
  9. 1 SEPARATOR = ','.freeze
  10. 1 DELIMITER = "\n".freeze
  11. 1 REGEX = { doc: /[\n\\]/, label: /[\n\\"]/ }.freeze
  12. 1 REPLACE = { "\n" => '\n', '\\' => '\\\\', '"' => '\"' }.freeze
  13. 1 def self.to_text(metrics)
  14. 10 lines = []
  15. 10 metrics.each do |name, metric|
  16. 5 lines << format(HELP_LINE, name, escape(metric[:help]))
  17. 5 lines << format(TYPE_LINE, name, metric[:type])
  18. 5 metric[:samples].each do |metric_name, labels, value|
  19. 16 lines << metric(metric_name, format_labels(labels), value)
  20. end
  21. end
  22. # there must be a trailing delimiter
  23. 10 (lines << nil).join(DELIMITER)
  24. end
  25. 1 def self.metric(name, labels, value)
  26. 16 format(METRIC_LINE, name, labels, value)
  27. end
  28. 1 def self.format_labels(set)
  29. 16 return if set.empty?
  30. 16 strings = set.each_with_object([]) do |(key, value), memo|
  31. 35 memo << format(LABEL, key, escape(value, :label))
  32. end
  33. 16 "{#{strings.join(SEPARATOR)}}"
  34. end
  35. 1 def self.escape(string, format = :doc)
  36. 40 string.to_s.gsub(REGEX[format], REPLACE)
  37. end
  38. end
  39. end
  40. end
  41. end

lib/prometheus/client/helper/mmaped_file.rb

97.44% lines covered

39 relevant lines. 38 lines covered and 1 lines missed.
    
  1. 1 require 'prometheus/client/helper/entry_parser'
  2. 1 require 'prometheus/client/helper/file_locker'
  3. 1 require 'prometheus/client/helper/loader'
  4. # load precompiled extension if available
  5. begin
  6. 1 ruby_version = /(\d+\.\d+)/.match(RUBY_VERSION)
  7. 1 require_relative "../../../#{ruby_version}/fast_mmaped_file"
  8. rescue LoadError
  9. 1 require 'fast_mmaped_file'
  10. end
  11. 1 module Prometheus
  12. 1 module Client
  13. 1 module Helper
  14. # We can't check `Prometheus::Client.configuration` as this creates a circular dependency
  15. 1 if (ENV.fetch('prometheus_rust_mmaped_file', 'true') == "true" &&
  16. Prometheus::Client::Helper::Loader.rust_impl_available?)
  17. class MmapedFile < FastMmapedFileRs
  18. end
  19. else
  20. 1 class MmapedFile < FastMmapedFile
  21. end
  22. end
  23. 1 class MmapedFile
  24. 1 include EntryParser
  25. 1 attr_reader :filepath, :size
  26. 1 def initialize(filepath)
  27. 105 @filepath = filepath
  28. 105 File.open(filepath, 'a+b') do |file|
  29. 105 file.truncate(initial_mmap_file_size) if file.size < MINIMUM_SIZE
  30. 105 @size = file.size
  31. end
  32. 105 super(filepath)
  33. end
  34. 1 def close
  35. 59 munmap
  36. 59 FileLocker.unlock(filepath)
  37. end
  38. 1 private
  39. 1 def initial_mmap_file_size
  40. 93 Prometheus::Client.configuration.initial_mmap_file_size
  41. end
  42. 1 public
  43. 1 class << self
  44. 1 def open(filepath)
  45. 105 MmapedFile.new(filepath)
  46. end
  47. 1 def ensure_exclusive_file(file_prefix = 'mmaped_file')
  48. 99 (0..Float::INFINITY).lazy
  49. 111 .map { |f_num| "#{file_prefix}_#{Prometheus::Client.pid}-#{f_num}.db" }
  50. 111 .map { |filename| File.join(Prometheus::Client.configuration.multiprocess_files_dir, filename) }
  51. 111 .find { |path| Helper::FileLocker.lock_to_process(path) }
  52. end
  53. 1 def open_exclusive_file(file_prefix = 'mmaped_file')
  54. 80 filename = Helper::MmapedFile.ensure_exclusive_file(file_prefix)
  55. 80 open(filename)
  56. end
  57. end
  58. end
  59. end
  60. end
  61. end

lib/prometheus/client/helper/plain_file.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. 1 require 'prometheus/client/helper/entry_parser'
  2. 1 module Prometheus
  3. 1 module Client
  4. 1 module Helper
  5. # Parses DB files without using mmap
  6. 1 class PlainFile
  7. 1 include EntryParser
  8. 1 attr_reader :filepath
  9. 1 def source
  10. 408 @data ||= File.read(filepath, mode: 'rb')
  11. end
  12. 1 def initialize(filepath)
  13. 23 @filepath = filepath
  14. end
  15. 1 def slice(*args)
  16. 139 source.slice(*args)
  17. end
  18. 1 def size
  19. 269 source.length
  20. end
  21. end
  22. end
  23. end
  24. end

lib/prometheus/client/histogram.rb

100.0% lines covered

40 relevant lines. 40 lines covered and 0 lines missed.
    
  1. 1 require 'prometheus/client/metric'
  2. 1 require 'prometheus/client/uses_value_type'
  3. 1 module Prometheus
  4. 1 module Client
  5. # A histogram samples observations (usually things like request durations
  6. # or response sizes) and counts them in configurable buckets. It also
  7. # provides a sum of all observed values.
  8. 1 class Histogram < Metric
  9. # Value represents the state of a Histogram at a given point.
  10. 1 class Value < Hash
  11. 1 include UsesValueType
  12. 1 attr_accessor :sum, :total, :total_inf
  13. 1 def initialize(type, name, labels, buckets)
  14. 11 @sum = value_object(type, name, "#{name}_sum", labels)
  15. 11 @total = value_object(type, name, "#{name}_count", labels)
  16. 11 @total_inf = value_object(type, name, "#{name}_bucket", labels.merge(le: "+Inf"))
  17. 11 buckets.each do |bucket|
  18. 113 self[bucket] = value_object(type, name, "#{name}_bucket", labels.merge(le: bucket.to_s))
  19. end
  20. end
  21. 1 def observe(value)
  22. 9 @sum.increment(value)
  23. 9 @total.increment()
  24. 9 @total_inf.increment()
  25. 9 each_key do |bucket|
  26. 91 self[bucket].increment() if value <= bucket
  27. end
  28. end
  29. 1 def get()
  30. 4 hash = {}
  31. 4 each_key do |bucket|
  32. 28 hash[bucket] = self[bucket].get()
  33. end
  34. 4 hash
  35. end
  36. end
  37. # DEFAULT_BUCKETS are the default Histogram buckets. The default buckets
  38. # are tailored to broadly measure the response time (in seconds) of a
  39. # network service. (From DefBuckets client_golang)
  40. 1 DEFAULT_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1,
  41. 2.5, 5, 10].freeze
  42. # Offer a way to manually specify buckets
  43. 1 def initialize(name, docstring, base_labels = {},
  44. buckets = DEFAULT_BUCKETS)
  45. 20 raise ArgumentError, 'Unsorted buckets, typo?' unless sorted? buckets
  46. 19 @buckets = buckets
  47. 19 super(name, docstring, base_labels)
  48. end
  49. 1 def type
  50. 31 :histogram
  51. end
  52. 1 def observe(labels, value)
  53. 9 label_set = label_set_for(labels)
  54. 18 synchronize { @values[label_set].observe(value) }
  55. end
  56. 1 private
  57. 1 def default(labels)
  58. # TODO: default function needs to know key of hash info (label names and values)
  59. 11 Value.new(type, @name, labels, @buckets)
  60. end
  61. 1 def sorted?(bucket)
  62. 203 bucket.each_cons(2).all? { |i, j| i <= j }
  63. end
  64. end
  65. end
  66. end

lib/prometheus/client/label_set_validator.rb

100.0% lines covered

42 relevant lines. 42 lines covered and 0 lines missed.
    
  1. # encoding: UTF-8
  2. 1 module Prometheus
  3. 1 module Client
  4. # LabelSetValidator ensures that all used label sets comply with the
  5. # Prometheus specification.
  6. 1 class LabelSetValidator
  7. 1 RESERVED_LABELS = [].freeze
  8. 1 class LabelSetError < StandardError; end
  9. 1 class InvalidLabelSetError < LabelSetError; end
  10. 1 class InvalidLabelError < LabelSetError; end
  11. 1 class ReservedLabelError < LabelSetError; end
  12. 1 def initialize(reserved_labels = [])
  13. 40133 @reserved_labels = (reserved_labels + RESERVED_LABELS).freeze
  14. 40133 @validated = {}
  15. end
  16. 1 def valid?(labels)
  17. 80255 unless labels.is_a?(Hash)
  18. 2 raise InvalidLabelSetError, "#{labels} is not a valid label set"
  19. end
  20. 80253 labels.all? do |key, value|
  21. 225 validate_symbol(key)
  22. 223 validate_name(key)
  23. 218 validate_reserved_key(key)
  24. 217 validate_value(key, value)
  25. end
  26. end
  27. 1 def validate(labels)
  28. 40248 return labels if @validated.key?(labels.hash)
  29. 40109 valid?(labels)
  30. 40107 unless @validated.empty? || match?(labels, @validated.first.last)
  31. 1 raise InvalidLabelSetError, "labels must have the same signature: (#{label_diff(labels, @validated.first.last)})"
  32. end
  33. 40106 @validated[labels.hash] = labels
  34. end
  35. 1 private
  36. 1 def label_diff(a, b)
  37. 1 "expected keys: #{b.keys.sort}, got: #{a.keys.sort}"
  38. end
  39. 1 def match?(a, b)
  40. 21 a.keys.sort == b.keys.sort
  41. end
  42. 1 def validate_symbol(key)
  43. 225 return true if key.is_a?(Symbol)
  44. 2 raise InvalidLabelError, "label #{key} is not a symbol"
  45. end
  46. 1 def validate_name(key)
  47. 223 return true unless key.to_s.start_with?('__')
  48. 5 raise ReservedLabelError, "label #{key} must not start with __"
  49. end
  50. 1 def validate_reserved_key(key)
  51. 218 return true unless @reserved_labels.include?(key)
  52. 1 raise ReservedLabelError, "#{key} is reserved"
  53. end
  54. 1 def validate_value(key, value)
  55. 217 return true if value.is_a?(String) ||
  56. value.is_a?(Numeric) ||
  57. value.is_a?(Symbol) ||
  58. value.is_a?(FalseClass) ||
  59. value.is_a?(TrueClass) ||
  60. value.nil?
  61. 1 raise InvalidLabelError, "#{key} does not contain a valid value (type #{value.class})"
  62. end
  63. end
  64. end
  65. end

lib/prometheus/client/metric.rb

95.45% lines covered

44 relevant lines. 42 lines covered and 2 lines missed.
    
  1. 1 require 'thread'
  2. 1 require 'prometheus/client/label_set_validator'
  3. 1 require 'prometheus/client/uses_value_type'
  4. 1 module Prometheus
  5. 1 module Client
  6. 1 class Metric
  7. 1 include UsesValueType
  8. 1 attr_reader :name, :docstring, :base_labels
  9. 1 def initialize(name, docstring, base_labels = {})
  10. 40100 @mutex = Mutex.new
  11. 40100 @validator = case type
  12. when :summary
  13. 18 LabelSetValidator.new(['quantile'])
  14. when :histogram
  15. 19 LabelSetValidator.new(['le'])
  16. else
  17. 40063 LabelSetValidator.new
  18. end
  19. 80184 @values = Hash.new { |hash, key| hash[key] = default(key) }
  20. 40100 validate_name(name)
  21. 40096 validate_docstring(docstring)
  22. 40092 @validator.valid?(base_labels)
  23. 40087 @name = name
  24. 40087 @docstring = docstring
  25. 40087 @base_labels = base_labels
  26. end
  27. # Returns the value for the given label set
  28. 1 def get(labels = {})
  29. 45 label_set = label_set_for(labels)
  30. 45 @validator.valid?(label_set)
  31. 45 @values[label_set].get
  32. end
  33. # Returns all label sets with their values
  34. 1 def values
  35. 1 synchronize do
  36. 1 @values.each_with_object({}) do |(labels, value), memo|
  37. 1 memo[labels] = value
  38. end
  39. end
  40. end
  41. 1 private
  42. 1 def touch_default_value
  43. @values[label_set_for({})]
  44. end
  45. 1 def default(labels)
  46. value_object(type, @name, @name, labels)
  47. end
  48. 1 def validate_name(name)
  49. 40100 return true if name.is_a?(Symbol)
  50. 4 raise ArgumentError, 'given name must be a symbol'
  51. end
  52. 1 def validate_docstring(docstring)
  53. 40096 return true if docstring.respond_to?(:empty?) && !docstring.empty?
  54. 4 raise ArgumentError, 'docstring must be given'
  55. end
  56. 1 def label_set_for(labels)
  57. 40221 @validator.validate(@base_labels.merge(labels))
  58. end
  59. 1 def synchronize(&block)
  60. 40142 @mutex.synchronize(&block)
  61. end
  62. end
  63. end
  64. end

lib/prometheus/client/mmaped_dict.rb

92.11% lines covered

38 relevant lines. 35 lines covered and 3 lines missed.
    
  1. 1 require 'prometheus/client/helper/mmaped_file'
  2. 1 require 'prometheus/client/helper/plain_file'
  3. 1 require 'prometheus/client'
  4. 1 module Prometheus
  5. 1 module Client
  6. 1 class ParsingError < StandardError
  7. end
  8. # A dict of doubles, backed by an mmapped file.
  9. #
  10. # The file starts with a 4 byte int, indicating how much of it is used.
  11. # Then 4 bytes of padding.
  12. # There's then a number of entries, consisting of a 4 byte int which is the
  13. # size of the next field, a utf-8 encoded string key, padding to an 8 byte
  14. # alignment, and then a 8 byte float which is the value.
  15. 1 class MmapedDict
  16. 1 attr_reader :m, :used, :positions
  17. 1 def self.read_all_values(f)
  18. 3 Helper::PlainFile.new(f).entries.map do |data, encoded_len, value_offset, _|
  19. 133 encoded, value = data.unpack(format('@4A%d@%dd', encoded_len, value_offset))
  20. 133 [encoded, value]
  21. end
  22. end
  23. 1 def initialize(m)
  24. 92 @mutex = Mutex.new
  25. 92 @m = m
  26. # @m.mlock # TODO: Ensure memory is locked to RAM
  27. 92 @positions = {}
  28. 92 read_all_positions.each do |key, pos|
  29. 277 @positions[key] = pos
  30. end
  31. rescue StandardError => e
  32. raise ParsingError, "exception #{e} while processing metrics file #{path}"
  33. end
  34. 1 def read_value(key)
  35. 40595 @m.fetch_entry(@positions, key, 0.0)
  36. end
  37. 1 def write_value(key, value)
  38. 41343 @m.upsert_entry(@positions, key, value)
  39. end
  40. 1 def path
  41. 1 @m.filepath if @m
  42. end
  43. 1 def close
  44. 47 @m.sync
  45. 47 @m.close
  46. rescue TypeError => e
  47. Prometheus::Client.logger.warn("munmap raised error #{e}")
  48. end
  49. 1 def inspect
  50. 2 "#<#{self.class}:0x#{(object_id << 1).to_s(16)}>"
  51. end
  52. 1 private
  53. 1 def init_value(key)
  54. @m.add_entry(@positions, key, 0.0)
  55. end
  56. # Yield (key, pos). No locking is performed.
  57. 1 def read_all_positions
  58. 92 @m.entries.map do |data, encoded_len, _, absolute_pos|
  59. 277 encoded, = data.unpack(format('@4A%d', encoded_len))
  60. 277 [encoded, absolute_pos]
  61. end
  62. end
  63. end
  64. end
  65. end

lib/prometheus/client/mmaped_value.rb

95.4% lines covered

87 relevant lines. 83 lines covered and 4 lines missed.
    
  1. 1 require 'prometheus/client'
  2. 1 require 'prometheus/client/mmaped_dict'
  3. 1 require 'json'
  4. 1 module Prometheus
  5. 1 module Client
  6. # A float protected by a mutex backed by a per-process mmaped file.
  7. 1 class MmapedValue
  8. 1 VALUE_LOCK = Mutex.new
  9. 1 @@files = {}
  10. 1 @@pid = -1
  11. 1 def initialize(type, metric_name, name, labels, multiprocess_mode = '')
  12. 40170 @file_prefix = type.to_s
  13. 40170 @metric_name = metric_name
  14. 40170 @name = name
  15. 40170 @labels = labels
  16. 40170 if type == :gauge
  17. 34 @file_prefix += '_' + multiprocess_mode.to_s
  18. end
  19. 40170 @pid = -1
  20. 40170 @mutex = Mutex.new
  21. 40170 initialize_file
  22. end
  23. 1 def increment(amount = 1)
  24. 40170 @mutex.synchronize do
  25. 40170 initialize_file if pid_changed?
  26. 40170 @value += amount
  27. 40170 write_value(@key, @value)
  28. 40170 @value
  29. end
  30. end
  31. 1 def decrement(amount = 1)
  32. increment(-amount)
  33. end
  34. 1 def set(value)
  35. 32 @mutex.synchronize do
  36. 32 initialize_file if pid_changed?
  37. 32 @value = value
  38. 32 write_value(@key, @value)
  39. 32 @value
  40. end
  41. end
  42. 1 def get
  43. 64 @mutex.synchronize do
  44. 64 initialize_file if pid_changed?
  45. 64 return @value
  46. end
  47. end
  48. 1 def pid_changed?
  49. 40333 @pid != Process.pid
  50. end
  51. # method needs to be run in VALUE_LOCK mutex
  52. 1 def unsafe_reinitialize_file(check_pid = true)
  53. 469 unsafe_initialize_file if !check_pid || pid_changed?
  54. end
  55. 1 def self.reset_and_reinitialize
  56. 8 VALUE_LOCK.synchronize do
  57. 8 @@pid = Process.pid
  58. 8 @@files = {}
  59. 8 ObjectSpace.each_object(MmapedValue).each do |v|
  60. 402 v.unsafe_reinitialize_file(false)
  61. end
  62. end
  63. end
  64. 1 def self.reset_on_pid_change
  65. 40598 if pid_changed?
  66. 11 @@pid = Process.pid
  67. 11 @@files = {}
  68. end
  69. end
  70. 1 def self.reinitialize_on_pid_change
  71. 3 VALUE_LOCK.synchronize do
  72. 3 reset_on_pid_change
  73. 3 ObjectSpace.each_object(MmapedValue, &:unsafe_reinitialize_file)
  74. end
  75. end
  76. 1 def self.pid_changed?
  77. 40598 @@pid != Process.pid
  78. end
  79. 1 def self.multiprocess
  80. 26 true
  81. end
  82. 1 private
  83. 1 def initialize_file
  84. 40175 VALUE_LOCK.synchronize do
  85. 40175 unsafe_initialize_file
  86. end
  87. end
  88. 1 def unsafe_initialize_file
  89. 40595 self.class.reset_on_pid_change
  90. 40595 @pid = Process.pid
  91. 40595 unless @@files.has_key?(@file_prefix)
  92. 80 unless @file.nil?
  93. 41 @file.close
  94. end
  95. 80 mmaped_file = Helper::MmapedFile.open_exclusive_file(@file_prefix)
  96. 80 @@files[@file_prefix] = MmapedDict.new(mmaped_file)
  97. end
  98. 40595 @file = @@files[@file_prefix]
  99. 40595 @key = rebuild_key
  100. 40595 @value = read_value(@key)
  101. end
  102. 1 def rebuild_key
  103. 40595 keys = @labels.keys.sort
  104. 40595 values = @labels.values_at(*keys)
  105. 40595 [@metric_name, @name, keys, values].to_json
  106. end
  107. 1 def write_value(key, val)
  108. 40202 @file.write_value(key, val)
  109. rescue StandardError => e
  110. 1 Prometheus::Client.logger.warn("writing value to #{@file.path} failed with #{e}")
  111. 1 Prometheus::Client.logger.debug(e.backtrace.join("\n"))
  112. end
  113. 1 def read_value(key)
  114. 40595 @file.read_value(key)
  115. rescue StandardError => e
  116. Prometheus::Client.logger.warn("reading value from #{@file.path} failed with #{e}")
  117. Prometheus::Client.logger.debug(e.backtrace.join("\n"))
  118. 0
  119. end
  120. end
  121. end
  122. end

lib/prometheus/client/page_size.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 require 'open3'
  2. 1 module Prometheus
  3. 1 module Client
  4. 1 module PageSize
  5. 1 def self.page_size(fallback_page_size: 4096)
  6. 5 stdout, status = Open3.capture2('getconf PAGESIZE')
  7. 5 return fallback_page_size if status.nil? || !status.success?
  8. 5 page_size = stdout.chomp.to_i
  9. 5 return fallback_page_size if page_size <= 0
  10. 5 page_size
  11. end
  12. end
  13. end
  14. end

lib/prometheus/client/push.rb

100.0% lines covered

95 relevant lines. 95 lines covered and 0 lines missed.
    
  1. # encoding: UTF-8
  2. 1 require 'base64'
  3. 1 require 'thread'
  4. 1 require 'net/http'
  5. 1 require 'uri'
  6. 1 require 'erb'
  7. 1 require 'set'
  8. 1 require 'prometheus/client'
  9. 1 require 'prometheus/client/formats/text'
  10. 1 require 'prometheus/client/label_set_validator'
  11. 1 module Prometheus
  12. # Client is a ruby implementation for a Prometheus compatible client.
  13. 1 module Client
  14. # Push implements a simple way to transmit a given registry to a given
  15. # Pushgateway.
  16. 1 class Push
  17. 1 class HttpError < StandardError; end
  18. 1 class HttpRedirectError < HttpError; end
  19. 1 class HttpClientError < HttpError; end
  20. 1 class HttpServerError < HttpError; end
  21. 1 DEFAULT_GATEWAY = 'http://localhost:9091'.freeze
  22. 1 PATH = '/metrics/job/%s'.freeze
  23. 1 SUPPORTED_SCHEMES = %w(http https).freeze
  24. 1 attr_reader :job, :gateway, :path
  25. 1 def initialize(job:, gateway: DEFAULT_GATEWAY, grouping_key: {}, **kwargs)
  26. 25 raise ArgumentError, "job cannot be nil" if job.nil?
  27. 24 raise ArgumentError, "job cannot be empty" if job.empty?
  28. 23 @validator = LabelSetValidator.new()
  29. 23 @validator.validate(grouping_key)
  30. 22 @mutex = Mutex.new
  31. 22 @job = job
  32. 22 @gateway = gateway || DEFAULT_GATEWAY
  33. 22 @grouping_key = grouping_key
  34. 22 @path = build_path(job, grouping_key)
  35. 22 @uri = parse("#{@gateway}#{@path}")
  36. 20 validate_no_basic_auth!(@uri)
  37. 19 @http = Net::HTTP.new(@uri.host, @uri.port)
  38. 19 @http.use_ssl = (@uri.scheme == 'https')
  39. 19 @http.open_timeout = kwargs[:open_timeout] if kwargs[:open_timeout]
  40. 19 @http.read_timeout = kwargs[:read_timeout] if kwargs[:read_timeout]
  41. end
  42. 1 def basic_auth(user, password)
  43. 1 @user = user
  44. 1 @password = password
  45. end
  46. 1 def add(registry)
  47. 1 synchronize do
  48. 1 request(Net::HTTP::Post, registry)
  49. end
  50. end
  51. 1 def replace(registry)
  52. 1 synchronize do
  53. 1 request(Net::HTTP::Put, registry)
  54. end
  55. end
  56. 1 def delete
  57. 1 synchronize do
  58. 1 request(Net::HTTP::Delete)
  59. end
  60. end
  61. 1 private
  62. 1 def parse(url)
  63. 22 uri = URI.parse(url)
  64. 21 unless SUPPORTED_SCHEMES.include?(uri.scheme)
  65. 1 raise ArgumentError, 'only HTTP gateway URLs are supported currently.'
  66. end
  67. 20 uri
  68. rescue URI::InvalidURIError => e
  69. 1 raise ArgumentError, "#{url} is not a valid URL: #{e}"
  70. end
  71. 1 def build_path(job, grouping_key)
  72. 22 path = format(PATH, ERB::Util::url_encode(job))
  73. 22 grouping_key.each do |label, value|
  74. 6 if value.include?('/')
  75. 1 encoded_value = Base64.urlsafe_encode64(value)
  76. 1 path += "/#{label}@base64/#{encoded_value}"
  77. # While it's valid for the urlsafe_encode64 function to return an
  78. # empty string when the input string is empty, it doesn't work for
  79. # our specific use case as we're putting the result into a URL path
  80. # segment. A double slash (`//`) can be normalised away by HTTP
  81. # libraries, proxies, and web servers.
  82. #
  83. # For empty strings, we use a single padding character (`=`) as the
  84. # value.
  85. #
  86. # See the pushgateway docs for more details:
  87. #
  88. # https://github.com/prometheus/pushgateway/blob/6393a901f56d4dda62cd0f6ab1f1f07c495b6354/README.md#url
  89. 5 elsif value.empty?
  90. 1 path += "/#{label}@base64/="
  91. else
  92. 4 path += "/#{label}/#{ERB::Util::url_encode(value)}"
  93. end
  94. end
  95. 22 path
  96. end
  97. 1 def request(req_class, registry = nil)
  98. 8 validate_no_label_clashes!(registry) if registry
  99. 7 req = req_class.new(@uri)
  100. 7 req.content_type = Formats::Text::CONTENT_TYPE
  101. 7 req.basic_auth(@user, @password) if @user
  102. 7 req.body = Formats::Text.marshal(registry) if registry
  103. 7 response = @http.request(req)
  104. 7 validate_response!(response)
  105. 4 response
  106. end
  107. 1 def synchronize
  108. 6 @mutex.synchronize { yield }
  109. end
  110. 1 def validate_no_basic_auth!(uri)
  111. 20 if uri.user || uri.password
  112. 1 raise ArgumentError, <<~EOF
  113. Setting Basic Auth credentials in the gateway URL is not supported, please call the `basic_auth` method.
  114. Received username `#{uri.user}` in gateway URL. Instead of passing
  115. Basic Auth credentials like this:
  116. ```
  117. push = Prometheus::Client::Push.new(job: "my-job", gateway: "http://user:password@localhost:9091")
  118. ```
  119. please pass them like this:
  120. ```
  121. push = Prometheus::Client::Push.new(job: "my-job", gateway: "http://localhost:9091")
  122. push.basic_auth("user", "password")
  123. ```
  124. While URLs do support passing Basic Auth credentials using the
  125. `http://user:password@example.com/` syntax, the username and
  126. password in that syntax have to follow the usual rules for URL
  127. encoding of characters per RFC 3986
  128. (https://datatracker.ietf.org/doc/html/rfc3986#section-2.1).
  129. Rather than place the burden of correctly performing that encoding
  130. on users of this gem, we decided to have a separate method for
  131. supplying Basic Auth credentials, with no requirement to URL encode
  132. the characters in them.
  133. EOF
  134. end
  135. end
  136. 1 def validate_no_label_clashes!(registry)
  137. # There's nothing to check if we don't have a grouping key
  138. 7 return if @grouping_key.empty?
  139. # We could be doing a lot of comparisons, so let's do them against a
  140. # set rather than an array
  141. 1 grouping_key_labels = @grouping_key.keys.to_set
  142. 1 registry.metrics.each do |metric|
  143. 1 metric.values.keys.first.keys.each do |label|
  144. 1 if grouping_key_labels.include?(label)
  145. 1 raise LabelSetValidator::InvalidLabelSetError,
  146. "label :#{label} from grouping key collides with label of the " \
  147. "same name from metric :#{metric.name} and would overwrite it"
  148. end
  149. end
  150. end
  151. end
  152. 1 def validate_response!(response)
  153. 7 status = Integer(response.code)
  154. 7 if status >= 300
  155. 3 message = "status: #{response.code}, message: #{response.message}, body: #{response.body}"
  156. 3 if status <= 399
  157. 1 raise HttpRedirectError, message
  158. 2 elsif status <= 499
  159. 1 raise HttpClientError, message
  160. else
  161. 1 raise HttpServerError, message
  162. end
  163. end
  164. end
  165. end
  166. end
  167. end

lib/prometheus/client/rack/collector.rb

97.5% lines covered

40 relevant lines. 39 lines covered and 1 lines missed.
    
  1. # encoding: UTF-8
  2. 1 require 'prometheus/client'
  3. 1 module Prometheus
  4. 1 module Client
  5. 1 module Rack
  6. # Collector is a Rack middleware that provides a sample implementation of
  7. # a HTTP tracer. The default label builder can be modified to export a
  8. # different set of labels per recorded metric.
  9. 1 class Collector
  10. 1 attr_reader :app, :registry
  11. 1 def initialize(app, options = {}, &label_builder)
  12. 5 @app = app
  13. 5 @registry = options[:registry] || Client.registry
  14. 5 @label_builder = label_builder || DEFAULT_LABEL_BUILDER
  15. 5 init_request_metrics
  16. 5 init_exception_metrics
  17. end
  18. 1 def call(env) # :nodoc:
  19. 12 trace(env) { @app.call(env) }
  20. end
  21. 1 protected
  22. 1 DEFAULT_LABEL_BUILDER = proc do |env|
  23. {
  24. 4 method: env['REQUEST_METHOD'].downcase,
  25. host: env['HTTP_HOST'].to_s,
  26. path: env['PATH_INFO'].to_s,
  27. }
  28. end
  29. 1 def init_request_metrics
  30. 5 @requests = @registry.counter(
  31. :http_requests_total,
  32. 'A counter of the total number of HTTP requests made.',
  33. )
  34. 5 @durations = @registry.summary(
  35. :http_request_duration_seconds,
  36. 'A summary of the response latency.',
  37. )
  38. 5 @durations_hist = @registry.histogram(
  39. :http_req_duration_seconds,
  40. 'A histogram of the response latency.',
  41. )
  42. end
  43. 1 def init_exception_metrics
  44. 5 @exceptions = @registry.counter(
  45. :http_exceptions_total,
  46. 'A counter of the total number of exceptions raised.',
  47. )
  48. end
  49. 1 def trace(env)
  50. 6 start = Time.now
  51. 6 yield.tap do |response|
  52. 5 duration = (Time.now - start).to_f
  53. 5 record(labels(env, response), duration)
  54. end
  55. rescue => exception
  56. 2 @exceptions.increment(exception: exception.class.name)
  57. 2 raise
  58. end
  59. 1 def labels(env, response)
  60. 5 @label_builder.call(env).tap do |labels|
  61. 5 labels[:code] = response.first.to_s
  62. end
  63. end
  64. 1 def record(labels, duration)
  65. 5 @requests.increment(labels)
  66. 4 @durations.observe(labels, duration)
  67. 4 @durations_hist.observe(labels, duration)
  68. rescue => exception
  69. 1 @exceptions.increment(exception: exception.class.name)
  70. 1 raise
  71. nil
  72. end
  73. end
  74. end
  75. end
  76. end

lib/prometheus/client/rack/exporter.rb

38.3% lines covered

47 relevant lines. 18 lines covered and 29 lines missed.
    
  1. # encoding: UTF-8
  2. 1 require 'prometheus/client'
  3. 1 require 'prometheus/client/formats/text'
  4. 1 module Prometheus
  5. 1 module Client
  6. 1 module Rack
  7. # Exporter is a Rack middleware that provides a sample implementation of
  8. # a Prometheus HTTP client API.
  9. 1 class Exporter
  10. 1 attr_reader :app, :registry, :path
  11. 1 FORMATS = [Formats::Text].freeze
  12. 1 FALLBACK = Formats::Text
  13. 1 def initialize(app, options = {})
  14. @app = app
  15. @registry = options[:registry] || Client.registry
  16. @path = options[:path] || '/metrics'
  17. @acceptable = build_dictionary(FORMATS, FALLBACK)
  18. end
  19. 1 def call(env)
  20. if env['PATH_INFO'] == @path
  21. format = negotiate(env['HTTP_ACCEPT'], @acceptable)
  22. format ? respond_with(format) : not_acceptable(FORMATS)
  23. else
  24. @app.call(env)
  25. end
  26. end
  27. 1 private
  28. 1 def negotiate(accept, formats)
  29. accept = '*/*' if accept.to_s.empty?
  30. parse(accept).each do |content_type, _|
  31. return formats[content_type] if formats.key?(content_type)
  32. end
  33. nil
  34. end
  35. 1 def parse(header)
  36. header.to_s.split(/\s*,\s*/).map do |type|
  37. attributes = type.split(/\s*;\s*/)
  38. quality = extract_quality(attributes)
  39. [attributes.join('; '), quality]
  40. end.sort_by(&:last).reverse
  41. end
  42. 1 def extract_quality(attributes, default = 1.0)
  43. quality = default
  44. attributes.delete_if do |attr|
  45. quality = attr.split('q=').last.to_f if attr.start_with?('q=')
  46. end
  47. quality
  48. end
  49. 1 def respond_with(format)
  50. rust_enabled = Prometheus::Client.configuration.rust_multiprocess_metrics
  51. response = if Prometheus::Client.configuration.value_class.multiprocess
  52. format.marshal_multiprocess(use_rust: rust_enabled)
  53. else
  54. format.marshal
  55. end
  56. [
  57. 200,
  58. { 'Content-Type' => format::CONTENT_TYPE },
  59. [response],
  60. ]
  61. end
  62. 1 def not_acceptable(formats)
  63. types = formats.map { |format| format::MEDIA_TYPE }
  64. [
  65. 406,
  66. { 'Content-Type' => 'text/plain' },
  67. ["Supported media types: #{types.join(', ')}"],
  68. ]
  69. end
  70. 1 def build_dictionary(formats, fallback)
  71. formats.each_with_object('*/*' => fallback) do |format, memo|
  72. memo[format::CONTENT_TYPE] = format
  73. memo[format::MEDIA_TYPE] = format
  74. end
  75. end
  76. end
  77. end
  78. end
  79. end

lib/prometheus/client/registry.rb

100.0% lines covered

33 relevant lines. 33 lines covered and 0 lines missed.
    
  1. # encoding: UTF-8
  2. 1 require 'thread'
  3. 1 require 'prometheus/client/counter'
  4. 1 require 'prometheus/client/summary'
  5. 1 require 'prometheus/client/gauge'
  6. 1 require 'prometheus/client/histogram'
  7. 1 module Prometheus
  8. 1 module Client
  9. # Registry
  10. 1 class Registry
  11. 1 class AlreadyRegisteredError < StandardError; end
  12. 1 def initialize
  13. 36 @metrics = {}
  14. 36 @mutex = Mutex.new
  15. end
  16. 1 def register(metric)
  17. 72 name = metric.name
  18. 72 @mutex.synchronize do
  19. 72 if exist?(name.to_sym)
  20. 5 raise AlreadyRegisteredError, "#{name} has already been registered"
  21. else
  22. 67 @metrics[name.to_sym] = metric
  23. end
  24. end
  25. 67 metric
  26. end
  27. 1 def counter(name, docstring, base_labels = {})
  28. 15 register(Counter.new(name, docstring, base_labels))
  29. end
  30. 1 def summary(name, docstring, base_labels = {})
  31. 10 register(Summary.new(name, docstring, base_labels))
  32. end
  33. 1 def gauge(name, docstring, base_labels = {}, multiprocess_mode = :all)
  34. 21 register(Gauge.new(name, docstring, base_labels, multiprocess_mode))
  35. end
  36. 1 def histogram(name, docstring, base_labels = {},
  37. buckets = Histogram::DEFAULT_BUCKETS)
  38. 10 register(Histogram.new(name, docstring, base_labels, buckets))
  39. end
  40. 1 def exist?(name)
  41. 74 @metrics.key?(name)
  42. end
  43. 1 def get(name)
  44. 11 @metrics[name.to_sym]
  45. end
  46. 1 def metrics
  47. 15 @metrics.values
  48. end
  49. end
  50. end
  51. end

lib/prometheus/client/simple_value.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. 1 require 'json'
  2. 1 module Prometheus
  3. 1 module Client
  4. 1 class SimpleValue
  5. 1 def initialize(_type, _metric_name, _name, _labels, *_args)
  6. 88 @value = 0.0
  7. end
  8. 1 def set(value)
  9. 12 @value = value
  10. end
  11. 1 def increment(by = 1)
  12. 74 @value += by
  13. end
  14. 1 def decrement(by = 1)
  15. 2 @value -= by
  16. end
  17. 1 def get
  18. 35 @value
  19. end
  20. 1 def self.multiprocess
  21. 10 false
  22. end
  23. end
  24. end
  25. end

lib/prometheus/client/summary.rb

91.43% lines covered

35 relevant lines. 32 lines covered and 3 lines missed.
    
  1. 1 require 'prometheus/client/metric'
  2. 1 require 'prometheus/client/uses_value_type'
  3. 1 module Prometheus
  4. 1 module Client
  5. # Summary is an accumulator for samples. It captures Numeric data and
  6. # provides an efficient quantile calculation mechanism.
  7. 1 class Summary < Metric
  8. 1 extend Gem::Deprecate
  9. # Value represents the state of a Summary at a given point.
  10. 1 class Value < Hash
  11. 1 include UsesValueType
  12. 1 attr_accessor :sum, :total
  13. 1 def initialize(type, name, labels)
  14. 11 @sum = value_object(type, name, "#{name}_sum", labels)
  15. 11 @total = value_object(type, name, "#{name}_count", labels)
  16. end
  17. 1 def observe(value)
  18. 9 @sum.increment(value)
  19. 9 @total.increment
  20. end
  21. end
  22. 1 def initialize(name, docstring, base_labels = {})
  23. 18 super(name, docstring, base_labels)
  24. end
  25. 1 def type
  26. 30 :summary
  27. end
  28. # Records a given value.
  29. 1 def observe(labels, value)
  30. 9 label_set = label_set_for(labels)
  31. 18 synchronize { @values[label_set].observe(value) }
  32. end
  33. 1 alias add observe
  34. 1 deprecate :add, :observe, 2016, 10
  35. # Returns the value for the given label set
  36. 1 def get(labels = {})
  37. 4 @validator.valid?(labels)
  38. 4 synchronize do
  39. 4 @values[labels].sum.get
  40. end
  41. end
  42. # Returns all label sets with their values
  43. 1 def values
  44. synchronize do
  45. @values.each_with_object({}) do |(labels, value), memo|
  46. memo[labels] = value.sum
  47. end
  48. end
  49. end
  50. 1 private
  51. 1 def default(labels)
  52. 11 Value.new(type, @name, labels)
  53. end
  54. end
  55. end
  56. end

lib/prometheus/client/support/puma.rb

96.15% lines covered

26 relevant lines. 25 lines covered and 1 lines missed.
    
  1. 1 module Prometheus
  2. 1 module Client
  3. 1 module Support
  4. 1 module Puma
  5. 1 extend self
  6. 1 def worker_pid_provider
  7. 4 wid = worker_id
  8. 4 if wid = worker_id
  9. 3 wid
  10. else
  11. 1 "process_id_#{Process.pid}"
  12. end
  13. end
  14. 1 private
  15. 1 def object_based_worker_id
  16. 6 return unless defined?(::Puma::Cluster::Worker)
  17. 2 workers = ObjectSpace.each_object(::Puma::Cluster::Worker)
  18. 2 return if workers.nil?
  19. 2 workers_first = workers.first
  20. 2 workers_first.index unless workers_first.nil?
  21. end
  22. 1 def program_name
  23. $PROGRAM_NAME
  24. end
  25. 1 def worker_id
  26. 8 if matchdata = program_name.match(/puma.*cluster worker ([0-9]+):/)
  27. 2 "puma_#{matchdata[1]}"
  28. 6 elsif object_worker_id = object_based_worker_id
  29. 2 "puma_#{object_worker_id}"
  30. 4 elsif program_name.include?('puma')
  31. 2 'puma_master'
  32. end
  33. end
  34. end
  35. end
  36. end
  37. end

lib/prometheus/client/support/unicorn.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. 1 module Prometheus
  2. 1 module Client
  3. 1 module Support
  4. 1 module Unicorn
  5. 1 def self.worker_pid_provider
  6. 2 wid = worker_id
  7. 2 if wid.nil?
  8. 1 "process_id_#{Process.pid}"
  9. else
  10. 1 "worker_id_#{wid}"
  11. end
  12. end
  13. 1 def self.worker_id
  14. 2 match = $0.match(/worker\[([^\]]+)\]/)
  15. 2 if match
  16. 1 match[1]
  17. else
  18. 1 object_based_worker_id
  19. end
  20. end
  21. 1 def self.object_based_worker_id
  22. 3 return unless defined?(::Unicorn::Worker)
  23. 2 workers = ObjectSpace.each_object(::Unicorn::Worker)
  24. 2 return if workers.nil?
  25. 1 workers_first = workers.first
  26. 1 workers_first.nr unless workers_first.nil?
  27. end
  28. end
  29. end
  30. end
  31. end

lib/prometheus/client/uses_value_type.rb

72.73% lines covered

11 relevant lines. 8 lines covered and 3 lines missed.
    
  1. 1 require 'prometheus/client/simple_value'
  2. 1 module Prometheus
  3. 1 module Client
  4. # Module providing convenience methods for creating value_object
  5. 1 module UsesValueType
  6. 1 def value_class
  7. 40266 Prometheus::Client.configuration.value_class
  8. end
  9. 1 def value_object(type, metric_name, name, labels, *args)
  10. 40230 value_class.new(type, metric_name, name, labels, *args)
  11. rescue StandardError => e
  12. Prometheus::Client.logger.info("error #{e} while creating instance of #{value_class} defaulting to SimpleValue")
  13. Prometheus::Client.logger.debug("error #{e} backtrace #{e.backtrace.join("\n")}")
  14. Prometheus::Client::SimpleValue.new(type, metric_name, name, labels)
  15. end
  16. end
  17. end
  18. end

spec/examples/metric_example.rb

100.0% lines covered

23 relevant lines. 23 lines covered and 0 lines missed.
    
  1. # encoding: UTF-8
  2. 1 shared_examples_for Prometheus::Client::Metric do
  3. 20 subject { described_class.new(:foo, 'foo description') }
  4. 4 describe '.new' do
  5. 4 it 'returns a new metric' do
  6. 4 expect(subject).to be
  7. end
  8. 4 it 'raises an exception if a reserved base label is used' do
  9. 4 exception = Prometheus::Client::LabelSetValidator::ReservedLabelError
  10. 4 expect do
  11. 4 described_class.new(:foo, 'foo docstring', __name__: 'reserved')
  12. end.to raise_exception exception
  13. end
  14. 4 it 'raises an exception if the given name is blank' do
  15. 4 expect do
  16. 4 described_class.new(nil, 'foo')
  17. end.to raise_exception ArgumentError
  18. end
  19. 4 it 'raises an exception if docstring is missing' do
  20. 4 expect do
  21. 4 described_class.new(:foo, '')
  22. end.to raise_exception ArgumentError
  23. end
  24. end
  25. 4 describe '#type' do
  26. 4 it 'returns the metric type as symbol' do
  27. 4 expect(subject.type).to be_a(Symbol)
  28. end
  29. end
  30. 4 describe '#get' do
  31. 4 it 'returns the current metric value' do
  32. 4 expect(subject.get).to be_a(type)
  33. end
  34. 4 it 'returns the current metric value for a given label set' do
  35. 4 expect(subject.get(test: 'label')).to be_a(type)
  36. end
  37. end
  38. end

spec/prometheus/client/counter_spec.rb

100.0% lines covered

40 relevant lines. 40 lines covered and 0 lines missed.
    
  1. 1 require 'prometheus/client/counter'
  2. 1 require 'prometheus/client'
  3. 1 require 'examples/metric_example'
  4. 1 describe Prometheus::Client::Counter do
  5. 1 before do
  6. 14 allow(Prometheus::Client.configuration).to receive(:multiprocess_files_dir).and_return('tmp/')
  7. end
  8. 7 let(:counter) { Prometheus::Client::Counter.new(:foo, 'foo description') }
  9. 1 it_behaves_like Prometheus::Client::Metric do
  10. 3 let(:type) { Float }
  11. end
  12. 1 describe 'Memory Error tests' do
  13. 1 it "creating many counters shouldn't cause a SIGBUS" do
  14. 1 4.times do |j|
  15. 4 9999.times do |i|
  16. 39996 counter = Prometheus::Client::Counter.new("foo#{j}_z#{i}".to_sym, 'some string')
  17. 39996 counter.increment
  18. end
  19. 4 GC.start
  20. end
  21. end
  22. end
  23. 1 describe '#increment' do
  24. 1 it 'increments the counter' do
  25. 4 expect { counter.increment }.to change { counter.get }.by(1)
  26. end
  27. 1 it 'increments the counter for a given label set' do
  28. 1 expect do
  29. 1 expect do
  30. 1 counter.increment(test: 'label')
  31. 2 end.to change { counter.get(test: 'label') }.by(1)
  32. 2 end.to_not change { counter.get(test: 'other_label') }
  33. end
  34. 1 it 'increments the counter by a given value' do
  35. 1 expect do
  36. 1 counter.increment({}, 5)
  37. 2 end.to change { counter.get }.by(5)
  38. end
  39. 1 it 'raises an ArgumentError on negative increments' do
  40. 1 expect do
  41. 1 counter.increment({}, -1)
  42. end.to raise_error ArgumentError
  43. end
  44. 1 it 'returns the new counter value' do
  45. 1 expect(counter.increment).to eql(counter.get)
  46. end
  47. 1 it 'is thread safe' do
  48. 1 expect do
  49. 1 Array.new(10) do
  50. 10 Thread.new do
  51. 110 10.times { counter.increment }
  52. end
  53. end.each(&:join)
  54. 2 end.to change { counter.get }.by(100)
  55. end
  56. end
  57. end

spec/prometheus/client/formats/text_spec.rb

92.31% lines covered

52 relevant lines. 48 lines covered and 4 lines missed.
    
  1. 1 require 'spec_helper'
  2. 1 require 'prometheus/client/formats/text'
  3. 1 require 'prometheus/client/mmaped_value'
  4. 1 describe Prometheus::Client::Formats::Text do
  5. 1 context 'single process metrics' do
  6. 2 let(:value_class) { Prometheus::Client::SimpleValue }
  7. 1 let(:summary_value) do
  8. 1 { 0.5 => 4.2, 0.9 => 8.32, 0.99 => 15.3 }.tap do |value|
  9. 1 allow(value).to receive_messages(sum: 1243.21, total: 93)
  10. end
  11. end
  12. 1 let(:histogram_value) do
  13. 1 { 10 => 1, 20 => 2, 30 => 2 }.tap do |value|
  14. 1 allow(value).to receive_messages(sum: 15.2, total: 2)
  15. end
  16. end
  17. 1 let(:registry) do
  18. metrics = [
  19. 1 double(
  20. name: :foo,
  21. docstring: 'foo description',
  22. base_labels: { umlauts: 'Björn', utf: '佖佥' },
  23. type: :counter,
  24. values: {
  25. { code: 'red' } => 42,
  26. { code: 'green' } => 3.14E42,
  27. { code: 'blue' } => -1.23e-45,
  28. },
  29. ),
  30. double(
  31. name: :bar,
  32. docstring: "bar description\nwith newline",
  33. base_labels: { status: 'success' },
  34. type: :gauge,
  35. values: {
  36. { code: 'pink' } => 15,
  37. },
  38. ),
  39. double(
  40. name: :baz,
  41. docstring: 'baz "description" \\escaping',
  42. base_labels: {},
  43. type: :counter,
  44. values: {
  45. { text: "with \"quotes\", \\escape \n and newline" } => 15,
  46. },
  47. ),
  48. double(
  49. name: :qux,
  50. docstring: 'qux description',
  51. base_labels: { for: 'sake' },
  52. type: :summary,
  53. values: {
  54. { code: '1' } => summary_value,
  55. },
  56. ),
  57. double(
  58. name: :xuq,
  59. docstring: 'xuq description',
  60. base_labels: {},
  61. type: :histogram,
  62. values: {
  63. { code: 'ah' } => histogram_value,
  64. },
  65. ),
  66. ]
  67. 1 metrics.each do |m|
  68. 5 m.values.each do |k, v|
  69. 7 m.values[k] = value_class.new(m.type, m.name, m.name, k)
  70. 7 m.values[k].set(v)
  71. end
  72. end
  73. 1 double(metrics: metrics)
  74. end
  75. 1 describe '.marshal' do
  76. 1 it 'returns a Text format version 0.0.4 compatible representation' do
  77. 1 expect(subject.marshal(registry)).to eql <<-'TEXT'.gsub(/^\s+/, '')
  78. # HELP foo foo description
  79. # TYPE foo counter
  80. foo{umlauts="Björn",utf="佖佥",code="red"} 42
  81. foo{umlauts="Björn",utf="佖佥",code="green"} 3.14e+42
  82. foo{umlauts="Björn",utf="佖佥",code="blue"} -1.23e-45
  83. # HELP bar bar description\nwith newline
  84. # TYPE bar gauge
  85. bar{status="success",code="pink"} 15
  86. # HELP baz baz "description" \\escaping
  87. # TYPE baz counter
  88. baz{text="with \"quotes\", \\escape \n and newline"} 15
  89. # HELP qux qux description
  90. # TYPE qux summary
  91. qux{for="sake",code="1",quantile="0.5"} 4.2
  92. qux{for="sake",code="1",quantile="0.9"} 8.32
  93. qux{for="sake",code="1",quantile="0.99"} 15.3
  94. qux_sum{for="sake",code="1"} 1243.21
  95. qux_count{for="sake",code="1"} 93
  96. # HELP xuq xuq description
  97. # TYPE xuq histogram
  98. xuq{code="ah",le="10"} 1
  99. xuq{code="ah",le="20"} 2
  100. xuq{code="ah",le="30"} 2
  101. xuq{code="ah",le="+Inf"} 2
  102. xuq_sum{code="ah"} 15.2
  103. xuq_count{code="ah"} 2
  104. TEXT
  105. end
  106. end
  107. end
  108. 1 context 'multi process metrics', :temp_metrics_dir do
  109. 1 [true, false].each do |use_rust|
  110. 2 context "when rust_multiprocess_metrics is #{use_rust}" do
  111. 6 let(:registry) { Prometheus::Client::Registry.new }
  112. 2 before do
  113. 4 allow(Prometheus::Client.configuration).to receive(:multiprocess_files_dir).and_return(temp_metrics_dir)
  114. 4 allow(Prometheus::Client.configuration).to receive(:rust_multiprocess_metrics).and_return(use_rust)
  115. # reset all current metrics
  116. 4 Prometheus::Client::MmapedValue.class_variable_set(:@@files, {})
  117. end
  118. 2 context 'pid provider returns compound ID', :temp_metrics_dir, :sample_metrics do
  119. 2 before do
  120. 12 allow(Prometheus::Client.configuration).to receive(:pid_provider).and_return(-> { 'pid_provider_id_1' })
  121. # Prometheus::Client::MmapedValue.class_variable_set(:@@files, {})
  122. 2 add_simple_metrics(registry)
  123. end
  124. 2 it '.marshal_multiprocess' do
  125. 2 expect(described_class.marshal_multiprocess(temp_metrics_dir, use_rust: true)).to eq <<-'TEXT'.gsub(/^\s+/, '')
  126. # HELP counter Multiprocess metric
  127. # TYPE counter counter
  128. counter{a="1",b="1"} 1
  129. counter{a="1",b="2"} 1
  130. counter{a="2",b="1"} 1
  131. # HELP gauge Multiprocess metric
  132. # TYPE gauge gauge
  133. gauge{b="1"} 1
  134. gauge{b="2"} 1
  135. # HELP gauge_with_big_value Multiprocess metric
  136. # TYPE gauge_with_big_value gauge
  137. gauge_with_big_value{a="0.12345678901234566"} 0.12345678901234566
  138. gauge_with_big_value{a="12345678901234567"} 12345678901234568
  139. # HELP gauge_with_null_labels Multiprocess metric
  140. # TYPE gauge_with_null_labels gauge
  141. gauge_with_null_labels{a="",b=""} 1
  142. # HELP gauge_with_pid Multiprocess metric
  143. # TYPE gauge_with_pid gauge
  144. gauge_with_pid{b="1",c="1",pid="pid_provider_id_1"} 1
  145. # HELP histogram Multiprocess metric
  146. # TYPE histogram histogram
  147. histogram_bucket{a="1",le="+Inf"} 1
  148. histogram_bucket{a="1",le="0.005"} 0
  149. histogram_bucket{a="1",le="0.01"} 0
  150. histogram_bucket{a="1",le="0.025"} 0
  151. histogram_bucket{a="1",le="0.05"} 0
  152. histogram_bucket{a="1",le="0.1"} 0
  153. histogram_bucket{a="1",le="0.25"} 0
  154. histogram_bucket{a="1",le="0.5"} 0
  155. histogram_bucket{a="1",le="1"} 1
  156. histogram_bucket{a="1",le="10"} 1
  157. histogram_bucket{a="1",le="2.5"} 1
  158. histogram_bucket{a="1",le="5"} 1
  159. histogram_count{a="1"} 1
  160. histogram_sum{a="1"} 1
  161. # HELP summary Multiprocess metric
  162. # TYPE summary summary
  163. summary_count{a="1",b="1"} 1
  164. summary_sum{a="1",b="1"} 1
  165. TEXT
  166. end
  167. end
  168. 2 context 'pid provider returns numerical value', :temp_metrics_dir, :sample_metrics do
  169. 2 before do
  170. 12 allow(Prometheus::Client.configuration).to receive(:pid_provider).and_return(-> { -1 })
  171. 2 add_simple_metrics(registry)
  172. end
  173. 2 it '.marshal_multiprocess' do
  174. 2 expect(described_class.marshal_multiprocess(temp_metrics_dir, use_rust: use_rust)).to eq <<-'TEXT'.gsub(/^\s+/, '')
  175. # HELP counter Multiprocess metric
  176. # TYPE counter counter
  177. counter{a="1",b="1"} 1
  178. counter{a="1",b="2"} 1
  179. counter{a="2",b="1"} 1
  180. # HELP gauge Multiprocess metric
  181. # TYPE gauge gauge
  182. gauge{b="1"} 1
  183. gauge{b="2"} 1
  184. # HELP gauge_with_big_value Multiprocess metric
  185. # TYPE gauge_with_big_value gauge
  186. gauge_with_big_value{a="0.12345678901234566"} 0.12345678901234566
  187. gauge_with_big_value{a="12345678901234567"} 12345678901234568
  188. # HELP gauge_with_null_labels Multiprocess metric
  189. # TYPE gauge_with_null_labels gauge
  190. gauge_with_null_labels{a="",b=""} 1
  191. # HELP gauge_with_pid Multiprocess metric
  192. # TYPE gauge_with_pid gauge
  193. gauge_with_pid{b="1",c="1",pid="-1"} 1
  194. # HELP histogram Multiprocess metric
  195. # TYPE histogram histogram
  196. histogram_bucket{a="1",le="+Inf"} 1
  197. histogram_bucket{a="1",le="0.005"} 0
  198. histogram_bucket{a="1",le="0.01"} 0
  199. histogram_bucket{a="1",le="0.025"} 0
  200. histogram_bucket{a="1",le="0.05"} 0
  201. histogram_bucket{a="1",le="0.1"} 0
  202. histogram_bucket{a="1",le="0.25"} 0
  203. histogram_bucket{a="1",le="0.5"} 0
  204. histogram_bucket{a="1",le="1"} 1
  205. histogram_bucket{a="1",le="10"} 1
  206. histogram_bucket{a="1",le="2.5"} 1
  207. histogram_bucket{a="1",le="5"} 1
  208. histogram_count{a="1"} 1
  209. histogram_sum{a="1"} 1
  210. # HELP summary Multiprocess metric
  211. # TYPE summary summary
  212. summary_count{a="1",b="1"} 1
  213. summary_sum{a="1",b="1"} 1
  214. TEXT
  215. end
  216. end
  217. 2 context 'when OJ is available uses OJ to parse keys' do
  218. 2 let(:oj) { double(oj) }
  219. 2 before do
  220. stub_const 'Oj', oj
  221. allow(oj).to receive(:load)
  222. end
  223. end
  224. 2 context 'with metric having whitespace and UTF chars', :temp_metrics_dir do
  225. 2 before do
  226. registry.gauge(:gauge, "bar description\nwith newline", { umlauts: 'Björn', utf: '佖佥' }, :all).set({ umlauts: 'Björn', utf: '佖佥' }, 1)
  227. end
  228. 2 xit '.marshall_multiprocess' do
  229. expect(described_class.marshal_multiprocess(temp_metrics_dir, use_rust: true)).to eq <<-'TEXT'.gsub(/^\s+/, '')
  230. TODO...
  231. TEXT
  232. end
  233. end
  234. end
  235. end
  236. end
  237. end

spec/prometheus/client/gauge_spec.rb

100.0% lines covered

46 relevant lines. 46 lines covered and 0 lines missed.
    
  1. 1 require 'prometheus/client'
  2. 1 require 'prometheus/client/gauge'
  3. 1 require 'examples/metric_example'
  4. 1 describe Prometheus::Client::Gauge do
  5. 7 let(:gauge) { Prometheus::Client::Gauge.new(:foo, 'foo description', test: nil) }
  6. 1 before do
  7. 13 allow(Prometheus::Client.configuration).to receive(:value_class).and_return(Prometheus::Client::SimpleValue)
  8. end
  9. 1 it_behaves_like Prometheus::Client::Metric do
  10. 3 let(:type) { Float }
  11. end
  12. 1 describe '#set' do
  13. 1 it 'sets a metric value' do
  14. 1 expect do
  15. 1 gauge.set({}, 42)
  16. 2 end.to change { gauge.get }.from(0).to(42)
  17. end
  18. 1 it 'sets a metric value for a given label set' do
  19. 1 expect do
  20. 1 expect do
  21. 1 gauge.set({ test: 'value' }, 42)
  22. 2 end.to(change { gauge.get(test: 'value') }.from(0).to(42))
  23. 2 end.to_not(change { gauge.get })
  24. end
  25. end
  26. 1 describe '#increment' do
  27. 1 it 'increments a metric value' do
  28. 1 gauge.set({}, 1)
  29. 1 expect do
  30. 1 gauge.increment({}, 42)
  31. 2 end.to change { gauge.get }.from(1).to(43)
  32. end
  33. 1 it 'sets a metric value for a given label set' do
  34. 1 gauge.increment({ test: 'value' }, 1)
  35. 1 expect do
  36. 1 expect do
  37. 1 gauge.increment({ test: 'value' }, 42)
  38. 2 end.to(change { gauge.get(test: 'value') }.from(1).to(43))
  39. 2 end.to_not(change { gauge.get })
  40. end
  41. end
  42. 1 describe '#decrement' do
  43. 1 it 'decrements a metric value' do
  44. 1 gauge.set({}, 10)
  45. 1 expect do
  46. 1 gauge.decrement({}, 1)
  47. 2 end.to change { gauge.get }.from(10).to(9)
  48. end
  49. 1 it 'sets a metric value for a given label set' do
  50. 1 gauge.set({ test: 'value' }, 10)
  51. 1 expect do
  52. 1 expect do
  53. 1 gauge.decrement({ test: 'value' }, 5)
  54. 2 end.to(change { gauge.get(test: 'value') }.from(10).to(5))
  55. 2 end.to_not(change { gauge.get })
  56. end
  57. end
  58. end

spec/prometheus/client/helpers/json_parser_spec.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. 1 require 'spec_helper'
  2. 1 require 'oj'
  3. 1 require 'prometheus/client/helper/json_parser'
  4. 1 describe Prometheus::Client::Helper::JsonParser do
  5. 1 describe '.load' do
  6. 3 let(:input) { %({ "a": 1 }) }
  7. 1 shared_examples 'JSON parser' do
  8. 2 it 'parses JSON' do
  9. 2 expect(described_class.load(input)).to eq({ 'a' => 1 })
  10. end
  11. 2 it 'raises JSON::ParserError' do
  12. 4 expect { described_class.load("{false}") }.to raise_error(JSON::ParserError)
  13. end
  14. end
  15. 1 context 'with Oj' do
  16. 1 it_behaves_like 'JSON parser'
  17. end
  18. 1 context 'without Oj' do
  19. 1 before(:all) do
  20. 1 Object.send(:remove_const, 'Oj')
  21. 1 load File.join(__dir__, "../../../../lib/prometheus/client/helper/json_parser.rb")
  22. end
  23. 1 it_behaves_like 'JSON parser'
  24. end
  25. end
  26. end

spec/prometheus/client/helpers/mmaped_file_spec.rb

100.0% lines covered

56 relevant lines. 56 lines covered and 0 lines missed.
    
  1. 1 require 'spec_helper'
  2. 1 require 'prometheus/client/helper/mmaped_file'
  3. 1 require 'prometheus/client/page_size'
  4. 1 describe Prometheus::Client::Helper::MmapedFile do
  5. 11 let(:filename) { Dir::Tmpname.create('mmaped_file_') {} }
  6. 1 after do
  7. 10 File.delete(filename) if File.exist?(filename)
  8. end
  9. 1 describe '.open' do
  10. 1 it 'initialize PRIVATE mmaped file read only' do
  11. 1 expect(described_class).to receive(:new).with(filename).and_call_original
  12. 1 expect(described_class.open(filename)).to be_instance_of(described_class)
  13. end
  14. end
  15. 1 context 'file does not exist' do
  16. 4 let (:subject) { described_class.open(filename) }
  17. 1 it 'creates and initializes file correctly' do
  18. 1 expect(File.exist?(filename)).to be_falsey
  19. 1 subject
  20. 1 expect(File.exist?(filename)).to be_truthy
  21. end
  22. 1 it 'creates a file with minimum initial size' do
  23. 1 expect(File.size(subject.filepath)).to eq(subject.send(:initial_mmap_file_size))
  24. end
  25. 1 context 'when initial mmap size is larger' do
  26. 2 let(:page_size) { Prometheus::Client::PageSize.page_size }
  27. 2 let (:initial_mmap_file_size) { page_size + 1024 }
  28. 1 before do
  29. 1 allow_any_instance_of(described_class).to receive(:initial_mmap_file_size).and_return(initial_mmap_file_size)
  30. end
  31. 1 it 'creates a file with increased minimum initial size' do
  32. 1 expect(File.size(subject.filepath)).to eq(page_size * 2);
  33. end
  34. end
  35. end
  36. 1 describe '.ensure_exclusive_file' do
  37. 7 let(:tmpdir) { Dir.mktmpdir('mmaped_file') }
  38. 7 let(:pid) { 'pid' }
  39. 1 before do
  40. 6 allow(Prometheus::Client.configuration).to receive(:multiprocess_files_dir).and_return(tmpdir)
  41. 6 allow(Prometheus::Client.configuration).to receive(:pid_provider).and_return(pid.method(:to_s))
  42. end
  43. 1 context 'when no files are already locked' do
  44. 1 it 'provides first possible filename' do
  45. 1 expect(described_class.ensure_exclusive_file('mmaped_file'))
  46. .to match(/.*mmaped_file_pid-0\.db/)
  47. end
  48. 1 it 'provides first and second possible filenames for two invocations' do
  49. 1 expect(described_class.ensure_exclusive_file('mmaped_file'))
  50. .to match(/.*mmaped_file_pid-0\.db/)
  51. 1 expect(described_class.ensure_exclusive_file('mmaped_file'))
  52. .to match(/.*mmaped_file_pid-1\.db/)
  53. end
  54. end
  55. 1 context 'when first possible file exists for current file ID' do
  56. 5 let(:first_mmaped_file) { described_class.ensure_exclusive_file('mmaped_file') }
  57. 1 before do
  58. 4 first_mmaped_file
  59. end
  60. 1 context 'first file is unlocked' do
  61. 1 before do
  62. 2 Prometheus::Client::Helper::FileLocker.unlock(first_mmaped_file)
  63. end
  64. 1 it 'provides first possible filename discarding the lock' do
  65. 1 expect(described_class.ensure_exclusive_file('mmaped_file'))
  66. .to match(/.*mmaped_file_pid-0\.db/)
  67. end
  68. 1 it 'provides second possible filename for second invocation' do
  69. 1 expect(described_class.ensure_exclusive_file('mmaped_file'))
  70. .to match(/.*mmaped_file_pid-0\.db/)
  71. 1 expect(described_class.ensure_exclusive_file('mmaped_file'))
  72. .to match(/.*mmaped_file_pid-1\.db/)
  73. end
  74. end
  75. 1 context 'first file is not unlocked' do
  76. 1 it 'provides second possible filename' do
  77. 1 expect(described_class.ensure_exclusive_file('mmaped_file'))
  78. .to match(/.*mmaped_file_pid-1\.db/)
  79. end
  80. 1 it 'provides second and third possible filename for two invocations' do
  81. 1 expect(described_class.ensure_exclusive_file('mmaped_file'))
  82. .to match(/.*mmaped_file_pid-1\.db/)
  83. 1 expect(described_class.ensure_exclusive_file('mmaped_file'))
  84. .to match(/.*mmaped_file_pid-2\.db/)
  85. end
  86. end
  87. end
  88. end
  89. end

spec/prometheus/client/histogram_spec.rb

66.67% lines covered

45 relevant lines. 30 lines covered and 15 lines missed.
    
  1. # encoding: UTF-8
  2. 1 require 'prometheus/client'
  3. 1 require 'prometheus/client/histogram'
  4. 1 require 'examples/metric_example'
  5. 1 describe Prometheus::Client::Histogram do
  6. 1 before do
  7. 10 allow(Prometheus::Client.configuration).to receive(:multiprocess_files_dir).and_return('tmp/')
  8. end
  9. 1 let(:histogram) do
  10. 1 described_class.new(:bar, 'bar description', {}, [2.5, 5, 10])
  11. end
  12. 1 it_behaves_like Prometheus::Client::Metric do
  13. 3 let(:type) { Hash }
  14. end
  15. 1 describe '#initialization' do
  16. 1 it 'raise error for unsorted buckets' do
  17. 1 expect do
  18. 1 described_class.new(:bar, 'bar description', {}, [5, 2.5, 10])
  19. end.to raise_error ArgumentError
  20. end
  21. 1 it 'raise error for accidentally missing out an argument' do
  22. 1 expect do
  23. 1 described_class.new(:bar, 'bar description', [5, 2.5, 10])
  24. end.to raise_error Prometheus::Client::LabelSetValidator::InvalidLabelSetError
  25. end
  26. end
  27. 1 describe '#observe' do
  28. 1 it 'records the given value' do
  29. 1 expect do
  30. 1 histogram.observe({}, 5)
  31. 2 end.to change { histogram.get }
  32. end
  33. 1 xit 'raise error for le labels' do
  34. expect do
  35. histogram.observe({ le: 1 }, 5)
  36. end.to raise_error ArgumentError
  37. end
  38. end
  39. 1 describe '#get' do
  40. 1 before do
  41. histogram.observe({ foo: 'bar' }, 3)
  42. histogram.observe({ foo: 'bar' }, 5.2)
  43. histogram.observe({ foo: 'bar' }, 13)
  44. histogram.observe({ foo: 'bar' }, 4)
  45. end
  46. 1 xit 'returns a set of buckets values' do
  47. expect(histogram.get(foo: 'bar')).to eql(2.5 => 0, 5 => 2, 10 => 3)
  48. end
  49. 1 xit 'returns a value which responds to #sum and #total' do
  50. value = histogram.get(foo: 'bar')
  51. expect(value.sum).to eql(25.2)
  52. expect(value.total).to eql(4)
  53. expect(value.total_inf).to eql(4)
  54. end
  55. 1 xit 'uses zero as default value' do
  56. expect(histogram.get({})).to eql(2.5 => 0, 5 => 0, 10 => 0)
  57. end
  58. end
  59. 1 xdescribe '#values' do
  60. 1 it 'returns a hash of all recorded summaries' do
  61. histogram.observe({ status: 'bar' }, 3)
  62. histogram.observe({ status: 'foo' }, 6)
  63. expect(histogram.values).to eql(
  64. { status: 'bar' } => { 2.5 => 0, 5 => 1, 10 => 1 },
  65. { status: 'foo' } => { 2.5 => 0, 5 => 0, 10 => 1 },
  66. )
  67. end
  68. end
  69. end

spec/prometheus/client/label_set_validator_spec.rb

100.0% lines covered

40 relevant lines. 40 lines covered and 0 lines missed.
    
  1. # encoding: UTF-8
  2. 1 require 'prometheus/client/label_set_validator'
  3. 1 describe Prometheus::Client::LabelSetValidator do
  4. 11 let(:validator) { Prometheus::Client::LabelSetValidator.new reserved_labels }
  5. 10 let(:reserved_labels) { [] }
  6. 1 describe '.new' do
  7. 1 it 'returns an instance of a LabelSetValidator' do
  8. 1 expect(validator).to be_a(Prometheus::Client::LabelSetValidator)
  9. end
  10. end
  11. 1 describe '#valid?' do
  12. 1 it 'returns true for a valid label check' do
  13. 1 expect(validator.valid?(version: 'alpha')).to eql(true)
  14. end
  15. 1 it 'raises InvalidLabelError if a label value is an array' do
  16. 1 expect do
  17. 1 validator.valid?(version: [1, 2, 3])
  18. end.to raise_exception(described_class::InvalidLabelError)
  19. end
  20. 1 it 'raises Invaliddescribed_classError if a label set is not a hash' do
  21. 1 expect do
  22. 1 validator.valid?('invalid')
  23. end.to raise_exception(described_class::InvalidLabelSetError)
  24. end
  25. 1 it 'raises InvalidLabelError if a label key is not a symbol' do
  26. 1 expect do
  27. 1 validator.valid?('key' => 'value')
  28. end.to raise_exception(described_class::InvalidLabelError)
  29. end
  30. 1 it 'raises InvalidLabelError if a label key starts with __' do
  31. 1 expect do
  32. 1 validator.valid?(__reserved__: 'key')
  33. end.to raise_exception(described_class::ReservedLabelError)
  34. end
  35. 1 context "when reserved labels were set" do
  36. 2 let(:reserved_labels) { [:reserved] }
  37. 1 it 'raises ReservedLabelError if a label key is reserved' do
  38. 1 reserved_labels.each do |label|
  39. 1 expect do
  40. 1 validator.valid?(label => 'value')
  41. end.to raise_exception(described_class::ReservedLabelError)
  42. end
  43. end
  44. end
  45. end
  46. 1 describe '#validate' do
  47. 1 it 'returns a given valid label set' do
  48. 1 hash = { version: 'alpha' }
  49. 1 expect(validator.validate(hash)).to eql(hash)
  50. end
  51. 1 it 'raises an exception if a given label set is not valid' do
  52. 1 input = 'broken'
  53. 1 expect(validator).to receive(:valid?).with(input).and_raise(described_class::InvalidLabelSetError)
  54. 2 expect { validator.validate(input) }.to raise_exception(described_class::InvalidLabelSetError)
  55. end
  56. 1 it 'raises InvalidLabelSetError for varying label sets' do
  57. 1 validator.validate(method: 'get', code: '200')
  58. 1 expect do
  59. 1 validator.validate(method: 'get', exception: 'NoMethodError')
  60. end.to raise_exception(described_class::InvalidLabelSetError, "labels must have the same signature: (expected keys: [:code, :method], got: [:exception, :method])")
  61. end
  62. end
  63. end

spec/prometheus/client/mmaped_dict_spec.rb

100.0% lines covered

91 relevant lines. 91 lines covered and 0 lines missed.
    
  1. 1 require 'prometheus/client/mmaped_dict'
  2. 1 require 'prometheus/client/page_size'
  3. 1 require 'tempfile'
  4. 1 describe Prometheus::Client::MmapedDict do
  5. 9 let(:tmp_file) { Tempfile.new('mmaped_dict') }
  6. 9 let(:tmp_mmaped_file) { Prometheus::Client::Helper::MmapedFile.open(tmp_file.path) }
  7. 1 after do
  8. 8 tmp_mmaped_file.close
  9. 8 tmp_file.close
  10. 8 tmp_file.unlink
  11. end
  12. 1 describe '#initialize' do
  13. 1 describe "empty mmap'ed file" do
  14. 1 it 'is initialized with correct size' do
  15. 1 described_class.new(tmp_mmaped_file)
  16. 1 expect(File.size(tmp_file.path)).to eq(tmp_mmaped_file.send(:initial_mmap_file_size))
  17. end
  18. end
  19. 1 describe "mmap'ed file that is above minimum size" do
  20. 2 let(:above_minimum_size) { Prometheus::Client::Helper::EntryParser::MINIMUM_SIZE + 1 }
  21. 2 let(:page_size) { Prometheus::Client::PageSize.page_size }
  22. 1 before do
  23. 1 tmp_file.truncate(above_minimum_size)
  24. end
  25. 1 it 'is initialized with the a page size' do
  26. 1 described_class.new(tmp_mmaped_file)
  27. 1 tmp_file.open
  28. 1 expect(tmp_file.size).to eq(page_size);
  29. end
  30. end
  31. end
  32. 1 describe 'read on boundary conditions' do
  33. 2 let(:locked_file) { Prometheus::Client::Helper::MmapedFile.ensure_exclusive_file }
  34. 1 let(:mmaped_file) { Prometheus::Client::Helper::MmapedFile.open(locked_file) }
  35. 2 let(:page_size) { Prometheus::Client::PageSize.page_size }
  36. 1 let(:target_size) { page_size }
  37. 2 let(:iterations) { page_size / 32 }
  38. 2 let(:dummy_key) { '1234' }
  39. 2 let(:dummy_value) { 1.0 }
  40. 2 let(:expected) { { dummy_key => dummy_value } }
  41. 1 before do
  42. 1 Prometheus::Client.configuration.multiprocess_files_dir = Dir.tmpdir
  43. 1 data = described_class.new(Prometheus::Client::Helper::MmapedFile.open(locked_file))
  44. # This test exercises the case when the value ends on the last byte.
  45. # To generate a file like this, we create entries that require 32 bytes
  46. # total to store with 7 bytes of padding at the end.
  47. #
  48. # To make things align evenly against the system page size, add a dummy
  49. # entry that will occupy the next 3 bytes to start on a 32-byte boundary.
  50. # The filestructure looks like:
  51. #
  52. # Bytes 0-3 : Total used size of file
  53. # Bytes 4-7 : Padding
  54. # Bytes 8-11 : Length of '1234' (4)
  55. # Bytes 12-15: '1234'
  56. # Bytes 24-31: 1.0
  57. # Bytes 32-35: Length of '1000000000000' (13)
  58. # Bytes 36-48: '1000000000000'
  59. # Bytes 49-55: Padding
  60. # Bytes 56-63: 0.0
  61. # Bytes 64-67: Length of '1000000000001' (13)
  62. # Bytes 68-80: '1000000000001'
  63. # Bytes 81-87: Padding
  64. # Bytes 88-95: 1.0
  65. # ...
  66. 1 data.write_value(dummy_key, dummy_value)
  67. 1 (1..iterations - 1).each do |i|
  68. # Using a 13-byte string
  69. 127 text = (1000000000000 + i).to_s
  70. 127 expected[text] = i.to_f
  71. 127 data.write_value(text, i)
  72. end
  73. 1 data.close
  74. end
  75. 1 it '#read_all_values' do
  76. 1 values = described_class.read_all_values(locked_file)
  77. 1 expect(values.count).to eq(iterations)
  78. 1 expect(values).to match_array(expected.to_a)
  79. end
  80. end
  81. 1 describe 'read and write values' do
  82. 6 let(:locked_file) { Prometheus::Client::Helper::MmapedFile.ensure_exclusive_file }
  83. 6 let(:mmaped_file) { Prometheus::Client::Helper::MmapedFile.open(locked_file) }
  84. 1 before do
  85. 5 Prometheus::Client.configuration.multiprocess_files_dir = Dir.tmpdir
  86. 5 data = described_class.new(Prometheus::Client::Helper::MmapedFile.open(locked_file))
  87. 5 data.write_value('foo', 100)
  88. 5 data.write_value('bar', 500)
  89. 5 data.close
  90. end
  91. 1 after do
  92. 5 mmaped_file.close if File.exist?(mmaped_file.filepath)
  93. 5 Prometheus::Client::Helper::FileLocker.unlock(locked_file) if File.exist?(mmaped_file.filepath)
  94. 5 File.unlink(locked_file) if File.exist?(mmaped_file.filepath)
  95. end
  96. 1 it '#inspect' do
  97. 1 data = described_class.new(Prometheus::Client::Helper::MmapedFile.open(locked_file))
  98. 1 expect(data.inspect).to match(/#{described_class}:0x/)
  99. 1 expect(data.inspect).not_to match(/@position/)
  100. end
  101. 1 it '#read_all_values' do
  102. 1 values = described_class.read_all_values(locked_file)
  103. 1 expect(values.count).to eq(2)
  104. 1 expect(values[0]).to eq(['foo', 100])
  105. 1 expect(values[1]).to eq(['bar', 500])
  106. end
  107. 1 it '#read_all_positions' do
  108. 1 data = described_class.new(Prometheus::Client::Helper::MmapedFile.open(locked_file))
  109. 1 positions = data.positions
  110. # Generated via https://github.com/luismartingarcia/protocol:
  111. # protocol "Used:4,Pad:4,K1 Size:4,K1 Name:4,K1 Value:8,K2 Size:4,K2 Name:4,K2 Value:8"
  112. #
  113. # 0 1 2 3
  114. # 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
  115. # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  116. # | Used | Pad |K1 Size|K1 Name| K1 Value |K2 Size|K2 Name|
  117. # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  118. # | K2 Value |
  119. # +-+-+-+-+-+-+-+
  120. 1 expect(positions).to eq({ 'foo' => 16, 'bar' => 32 })
  121. end
  122. 1 describe '#write_value' do
  123. 1 it 'writes values' do
  124. # Reload dictionary
  125. #
  126. 1 data = described_class.new(mmaped_file)
  127. 1 data.write_value('new value', 500)
  128. # Overwrite existing values
  129. 1 data.write_value('foo', 200)
  130. 1 data.write_value('bar', 300)
  131. 1 values = described_class.read_all_values(locked_file)
  132. 1 expect(values.count).to eq(3)
  133. 1 expect(values[0]).to eq(['foo', 200])
  134. 1 expect(values[1]).to eq(['bar', 300])
  135. 1 expect(values[2]).to eq(['new value', 500])
  136. end
  137. 1 context 'when mmaped_file got deleted' do
  138. 1 it 'is able to write to and expand metrics file' do
  139. 1 data = described_class.new(mmaped_file)
  140. 1 data.write_value('new value', 500)
  141. 1 FileUtils.rm(mmaped_file.filepath)
  142. 1 1000.times do |i|
  143. 1000 data.write_value("new new value #{i}", 567)
  144. end
  145. 1 expect(File.exist?(locked_file)).not_to be_truthy
  146. end
  147. end
  148. end
  149. end
  150. end

spec/prometheus/client/mmaped_value_spec.rb

100.0% lines covered

84 relevant lines. 84 lines covered and 0 lines missed.
    
  1. 1 require 'prometheus/client/mmaped_dict'
  2. 1 require 'prometheus/client/page_size'
  3. 1 require 'tempfile'
  4. 1 describe Prometheus::Client::MmapedValue, :temp_metrics_dir do
  5. 1 before do
  6. 13 allow(Prometheus::Client.configuration).to receive(:multiprocess_files_dir).and_return(temp_metrics_dir)
  7. end
  8. 1 describe '.reset_and_reinitialize' do
  9. 3 let(:counter) { described_class.new(:counter, :counter, 'counter', {}) }
  10. 1 before do
  11. 2 counter.increment(1)
  12. end
  13. 1 it 'calls reinitialize on the counter' do
  14. 1 expect(counter).to receive(:unsafe_reinitialize_file).with(false).and_call_original
  15. 1 described_class.reset_and_reinitialize
  16. end
  17. 1 context 'when metrics folder changes' do
  18. 1 around do |example|
  19. 1 Dir.mktmpdir('temp_metrics_dir') do |path|
  20. 1 @tmp_path = path
  21. 1 example.run
  22. end
  23. end
  24. 1 before do
  25. 1 allow(Prometheus::Client.configuration).to receive(:multiprocess_files_dir).and_return(@tmp_path)
  26. end
  27. 1 it 'resets the counter to zero' do
  28. 1 expect(counter).to receive(:unsafe_reinitialize_file).with(false).and_call_original
  29. 4 expect { described_class.reset_and_reinitialize }.to(change { counter.get }.from(1).to(0))
  30. end
  31. end
  32. end
  33. 1 describe '#initialize' do
  34. 12 let(:pid) { 1234 }
  35. 1 before do
  36. 11 described_class.class_variable_set(:@@files, {})
  37. 11 described_class.class_variable_set(:@@pid, pid)
  38. 23 allow(Prometheus::Client.configuration).to receive(:pid_provider).and_return(-> { pid })
  39. 11 allow(Process).to receive(:pid).and_return(pid)
  40. end
  41. 1 describe 'counter type object initialized' do
  42. 12 let!(:counter) { described_class.new(:counter, :counter, 'counter', {}) }
  43. 1 describe 'PID unchanged' do
  44. 1 it 'initializing gauge MmapValue object type keeps old file data' do
  45. 1 described_class.new(:gauge, :gauge, 'gauge', {}, :all)
  46. 1 expect(described_class.class_variable_get(:@@files)).to have_key('counter')
  47. 1 expect(described_class.class_variable_get(:@@files)).to have_key('gauge_all')
  48. end
  49. end
  50. 1 describe 'PID changed' do
  51. 10 let(:new_pid) { pid - 1 }
  52. 2 let(:page_size) { Prometheus::Client::PageSize.page_size }
  53. 1 before do
  54. 9 counter.increment
  55. 9 @old_value = counter.get
  56. 20 allow(Prometheus::Client.configuration).to receive(:pid_provider).and_return(-> { new_pid })
  57. 9 allow(Process).to receive(:pid).and_return(new_pid)
  58. end
  59. 1 it 'initializing gauge MmapValue object type keeps old file data' do
  60. 1 described_class.new(:gauge, :gauge, 'gauge', {}, :all)
  61. 1 expect(described_class.class_variable_get(:@@files)).not_to have_key('counter')
  62. 1 expect(described_class.class_variable_get(:@@files)).to have_key('gauge_all')
  63. end
  64. 1 it 'updates pid' do
  65. 2 expect { described_class.new(:gauge, :gauge, 'gauge', {}, :all) }
  66. 2 .to change { described_class.class_variable_get(:@@pid) }.from(pid).to(new_pid)
  67. end
  68. 1 it '#increment updates pid' do
  69. 2 expect { counter.increment }
  70. 2 .to change { described_class.class_variable_get(:@@pid) }.from(pid).to(new_pid)
  71. end
  72. 1 it '#increment updates pid' do
  73. 2 expect { counter.increment }
  74. 2 .to change { described_class.class_variable_get(:@@pid) }.from(pid).to(new_pid)
  75. end
  76. 1 it '#get updates pid' do
  77. 2 expect { counter.get }
  78. 2 .to change { described_class.class_variable_get(:@@pid) }.from(pid).to(new_pid)
  79. end
  80. 1 it '#set updates pid' do
  81. 2 expect { counter.set(1) }
  82. 2 .to change { described_class.class_variable_get(:@@pid) }.from(pid).to(new_pid)
  83. end
  84. 1 it '#set logs an error' do
  85. 1 counter.set(1)
  86. 1 allow(counter.instance_variable_get(:@file))
  87. .to receive(:write_value)
  88. .and_raise('error writing value')
  89. 1 expect(Prometheus::Client.logger).to receive(:warn).and_call_original
  90. 1 counter.set(1)
  91. end
  92. 1 it 'reinitialize restores all used file references and resets data' do
  93. 1 described_class.new(:gauge, :gauge, 'gauge', {}, :all)
  94. 1 described_class.reinitialize_on_pid_change
  95. 1 expect(described_class.class_variable_get(:@@files)).to have_key('counter')
  96. 1 expect(described_class.class_variable_get(:@@files)).to have_key('gauge_all')
  97. 1 expect(counter.get).not_to eq(@old_value)
  98. end
  99. 1 it 'updates strings properly upon memory expansion', :page_size do
  100. 1 described_class.new(:gauge, :gauge, 'gauge2', { label_1: 'x' * page_size * 2 }, :all)
  101. # This previously failed on Linux but not on macOS since mmap() may re-allocate the same region.
  102. 1 ObjectSpace.each_object(String, &:valid_encoding?)
  103. end
  104. end
  105. 1 context 'different label ordering' do
  106. 1 it 'does not care about label ordering' do
  107. 1 counter1 = described_class.new(:counter, :counter, 'ordered_counter', { label_1: 'hello', label_2: 'world', label_3: 'baz' }).increment
  108. 1 counter2 = described_class.new(:counter, :counter, 'ordered_counter', { label_2: 'world', label_3: 'baz', label_1: 'hello' }).increment
  109. 1 reading_counter = described_class.new(:counter, :counter, 'ordered_counter', { label_3: 'baz', label_1: 'hello', label_2: 'world' })
  110. 1 expect(reading_counter.get).to eq(2)
  111. end
  112. end
  113. end
  114. end
  115. end

spec/prometheus/client/push_spec.rb

100.0% lines covered

175 relevant lines. 175 lines covered and 0 lines missed.
    
  1. # encoding: UTF-8
  2. 1 require 'prometheus/client/gauge'
  3. 1 require 'prometheus/client/push'
  4. 1 describe Prometheus::Client::Push do
  5. 11 let(:gateway) { 'http://localhost:9091' }
  6. 10 let(:registry) { Prometheus::Client::Registry.new }
  7. 13 let(:grouping_key) { {} }
  8. 14 let(:push) { Prometheus::Client::Push.new(job: 'test-job', gateway: gateway, grouping_key: grouping_key, open_timeout: 5, read_timeout: 30) }
  9. 1 describe '.new' do
  10. 1 it 'returns a new push instance' do
  11. 1 expect(push).to be_a(Prometheus::Client::Push)
  12. end
  13. 1 it 'uses localhost as default Pushgateway' do
  14. 1 push = Prometheus::Client::Push.new(job: 'test-job')
  15. 1 expect(push.gateway).to eql('http://localhost:9091')
  16. end
  17. 1 it 'allows to specify a custom Pushgateway' do
  18. 1 push = Prometheus::Client::Push.new(job: 'test-job', gateway: 'http://pu.sh:1234')
  19. 1 expect(push.gateway).to eql('http://pu.sh:1234')
  20. end
  21. 1 it 'raises an ArgumentError if the job is nil' do
  22. 1 expect do
  23. 1 Prometheus::Client::Push.new(job: nil)
  24. end.to raise_error ArgumentError
  25. end
  26. 1 it 'raises an ArgumentError if the job is empty' do
  27. 1 expect do
  28. 1 Prometheus::Client::Push.new(job: "")
  29. end.to raise_error ArgumentError
  30. end
  31. 1 it 'raises an ArgumentError if the given gateway URL is invalid' do
  32. 1 ['inva.lid:1233', 'http://[invalid]'].each do |url|
  33. 2 expect do
  34. 2 Prometheus::Client::Push.new(job: 'test-job', gateway: url)
  35. end.to raise_error ArgumentError
  36. end
  37. end
  38. 1 it 'raises InvalidLabelError if a grouping key label has an invalid name' do
  39. 1 expect do
  40. 1 Prometheus::Client::Push.new(job: "test-job", grouping_key: { "not_a_symbol" => "foo" })
  41. end.to raise_error Prometheus::Client::LabelSetValidator::InvalidLabelError
  42. end
  43. end
  44. 1 describe '#add' do
  45. 1 it 'sends a given registry to via HTTP POST' do
  46. 1 expect(push).to receive(:request).with(Net::HTTP::Post, registry)
  47. 1 push.add(registry)
  48. end
  49. end
  50. 1 describe '#replace' do
  51. 1 it 'sends a given registry to via HTTP PUT' do
  52. 1 expect(push).to receive(:request).with(Net::HTTP::Put, registry)
  53. 1 push.replace(registry)
  54. end
  55. end
  56. 1 describe '#delete' do
  57. 1 it 'deletes existing metrics with HTTP DELETE' do
  58. 1 expect(push).to receive(:request).with(Net::HTTP::Delete)
  59. 1 push.delete
  60. end
  61. end
  62. 1 describe '#path' do
  63. 1 it 'uses the default metrics path if no grouping key given' do
  64. 1 push = Prometheus::Client::Push.new(job: 'test-job')
  65. 1 expect(push.path).to eql('/metrics/job/test-job')
  66. end
  67. 1 it 'appends additional grouping labels to the path if specified' do
  68. 1 push = Prometheus::Client::Push.new(
  69. job: 'test-job',
  70. grouping_key: { foo: "bar", baz: "qux"},
  71. )
  72. 1 expect(push.path).to eql('/metrics/job/test-job/foo/bar/baz/qux')
  73. end
  74. 1 it 'encodes grouping key label values containing `/` in url-safe base64' do
  75. 1 push = Prometheus::Client::Push.new(
  76. job: 'test-job',
  77. grouping_key: { foo: "bar/baz"},
  78. )
  79. 1 expect(push.path).to eql('/metrics/job/test-job/foo@base64/YmFyL2Jheg==')
  80. end
  81. 1 it 'encodes empty grouping key label values as a single base64 padding character' do
  82. 1 push = Prometheus::Client::Push.new(
  83. job: 'test-job',
  84. grouping_key: { foo: ""},
  85. )
  86. 1 expect(push.path).to eql('/metrics/job/test-job/foo@base64/=')
  87. end
  88. 1 it 'URL-encodes all other non-URL-safe characters' do
  89. 1 push = Prometheus::Client::Push.new(job: '<bar job>', grouping_key: { foo_label: '<bar value>' })
  90. 1 expected = '/metrics/job/%3Cbar%20job%3E/foo_label/%3Cbar%20value%3E'
  91. 1 expect(push.path).to eql(expected)
  92. end
  93. end
  94. 1 describe '#request' do
  95. 5 let(:content_type) { Prometheus::Client::Formats::Text::CONTENT_TYPE }
  96. 4 let(:data) { Prometheus::Client::Formats::Text.marshal(registry) }
  97. 8 let(:uri) { URI.parse("#{gateway}/metrics/job/test-job") }
  98. 1 let(:response) do
  99. 4 double(
  100. :response,
  101. code: '200',
  102. message: 'OK',
  103. body: 'Everything worked'
  104. )
  105. end
  106. 1 it 'sends marshalled registry to the specified gateway' do
  107. 1 request = double(:request)
  108. 1 expect(request).to receive(:content_type=).with(content_type)
  109. 1 expect(request).to receive(:body=).with(data)
  110. 1 expect(Net::HTTP::Post).to receive(:new).with(uri).and_return(request)
  111. 1 http = double(:http)
  112. 1 expect(http).to receive(:use_ssl=).with(false)
  113. 1 expect(http).to receive(:open_timeout=).with(5)
  114. 1 expect(http).to receive(:read_timeout=).with(30)
  115. 1 expect(http).to receive(:request).with(request).and_return(response)
  116. 1 expect(Net::HTTP).to receive(:new).with('localhost', 9091).and_return(http)
  117. 1 push.send(:request, Net::HTTP::Post, registry)
  118. end
  119. 1 context 'for a 3xx response' do
  120. 1 let(:response) do
  121. 1 double(
  122. :response,
  123. code: '301',
  124. message: 'Moved Permanently',
  125. body: 'Probably no body, but technically you can return one'
  126. )
  127. end
  128. 1 it 'raises a redirect error' do
  129. 1 request = double(:request)
  130. 1 allow(request).to receive(:content_type=)
  131. 1 allow(request).to receive(:body=)
  132. 1 allow(Net::HTTP::Post).to receive(:new).with(uri).and_return(request)
  133. 1 http = double(:http)
  134. 1 allow(http).to receive(:use_ssl=)
  135. 1 allow(http).to receive(:open_timeout=)
  136. 1 allow(http).to receive(:read_timeout=)
  137. 1 allow(http).to receive(:request).with(request).and_return(response)
  138. 1 allow(Net::HTTP).to receive(:new).with('localhost', 9091).and_return(http)
  139. 2 expect { push.send(:request, Net::HTTP::Post, registry) }.to raise_error(
  140. Prometheus::Client::Push::HttpRedirectError
  141. )
  142. end
  143. end
  144. 1 context 'for a 4xx response' do
  145. 1 let(:response) do
  146. 1 double(
  147. :response,
  148. code: '400',
  149. message: 'Bad Request',
  150. body: 'Info on why the request was bad'
  151. )
  152. end
  153. 1 it 'raises a client error' do
  154. 1 request = double(:request)
  155. 1 allow(request).to receive(:content_type=)
  156. 1 allow(request).to receive(:body=)
  157. 1 allow(Net::HTTP::Post).to receive(:new).with(uri).and_return(request)
  158. 1 http = double(:http)
  159. 1 allow(http).to receive(:use_ssl=)
  160. 1 allow(http).to receive(:open_timeout=)
  161. 1 allow(http).to receive(:read_timeout=)
  162. 1 allow(http).to receive(:request).with(request).and_return(response)
  163. 1 allow(Net::HTTP).to receive(:new).with('localhost', 9091).and_return(http)
  164. 2 expect { push.send(:request, Net::HTTP::Post, registry) }.to raise_error(
  165. Prometheus::Client::Push::HttpClientError
  166. )
  167. end
  168. end
  169. 1 context 'for a 5xx response' do
  170. 1 let(:response) do
  171. 1 double(
  172. :response,
  173. code: '500',
  174. message: 'Internal Server Error',
  175. body: 'Apology for the server code being broken'
  176. )
  177. end
  178. 1 it 'raises a server error' do
  179. 1 request = double(:request)
  180. 1 allow(request).to receive(:content_type=)
  181. 1 allow(request).to receive(:body=)
  182. 1 allow(Net::HTTP::Post).to receive(:new).with(uri).and_return(request)
  183. 1 http = double(:http)
  184. 1 allow(http).to receive(:use_ssl=)
  185. 1 allow(http).to receive(:open_timeout=)
  186. 1 allow(http).to receive(:read_timeout=)
  187. 1 allow(http).to receive(:request).with(request).and_return(response)
  188. 1 allow(Net::HTTP).to receive(:new).with('localhost', 9091).and_return(http)
  189. 2 expect { push.send(:request, Net::HTTP::Post, registry) }.to raise_error(
  190. Prometheus::Client::Push::HttpServerError
  191. )
  192. end
  193. end
  194. 1 it 'deletes data from the registry' do
  195. 1 request = double(:request)
  196. 1 expect(request).to receive(:content_type=).with(content_type)
  197. 1 expect(Net::HTTP::Delete).to receive(:new).with(uri).and_return(request)
  198. 1 http = double(:http)
  199. 1 expect(http).to receive(:use_ssl=).with(false)
  200. 1 expect(http).to receive(:open_timeout=).with(5)
  201. 1 expect(http).to receive(:read_timeout=).with(30)
  202. 1 expect(http).to receive(:request).with(request).and_return(response)
  203. 1 expect(Net::HTTP).to receive(:new).with('localhost', 9091).and_return(http)
  204. 1 push.send(:request, Net::HTTP::Delete)
  205. end
  206. 1 context 'HTTPS support' do
  207. 2 let(:gateway) { 'https://localhost:9091' }
  208. 1 it 'uses HTTPS when requested' do
  209. 1 request = double(:request)
  210. 1 expect(request).to receive(:content_type=).with(content_type)
  211. 1 expect(request).to receive(:body=).with(data)
  212. 1 expect(Net::HTTP::Post).to receive(:new).with(uri).and_return(request)
  213. 1 http = double(:http)
  214. 1 expect(http).to receive(:use_ssl=).with(true)
  215. 1 expect(http).to receive(:open_timeout=).with(5)
  216. 1 expect(http).to receive(:read_timeout=).with(30)
  217. 1 expect(http).to receive(:request).with(request).and_return(response)
  218. 1 expect(Net::HTTP).to receive(:new).with('localhost', 9091).and_return(http)
  219. 1 push.send(:request, Net::HTTP::Post, registry)
  220. end
  221. end
  222. 1 context 'Basic Auth support' do
  223. 1 context 'when credentials are passed in the gateway URL' do
  224. 2 let(:gateway) { 'https://super:secret@localhost:9091' }
  225. 1 it "raises an ArgumentError explaining why we don't support that mechanism" do
  226. 2 expect { push }.to raise_error ArgumentError, /in the gateway URL.*username `super`/m
  227. end
  228. end
  229. 1 context 'when credentials are passed to the separate `basic_auth` method' do
  230. 2 let(:gateway) { 'https://localhost:9091' }
  231. 1 it 'passes the credentials on to the HTTP client' do
  232. 1 request = double(:request)
  233. 1 expect(request).to receive(:content_type=).with(content_type)
  234. 1 expect(request).to receive(:basic_auth).with('super', 'secret')
  235. 1 expect(request).to receive(:body=).with(data)
  236. 1 expect(Net::HTTP::Put).to receive(:new).with(uri).and_return(request)
  237. 1 http = double(:http)
  238. 1 expect(http).to receive(:use_ssl=).with(true)
  239. 1 expect(http).to receive(:open_timeout=).with(5)
  240. 1 expect(http).to receive(:read_timeout=).with(30)
  241. 1 expect(http).to receive(:request).with(request).and_return(response)
  242. 1 expect(Net::HTTP).to receive(:new).with('localhost', 9091).and_return(http)
  243. 1 push.basic_auth("super", "secret")
  244. 1 push.send(:request, Net::HTTP::Put, registry)
  245. end
  246. end
  247. end
  248. 1 context 'with a grouping key that clashes with a metric label' do
  249. 2 let(:grouping_key) { { foo: "bar"} }
  250. 1 before do
  251. 1 gauge = Prometheus::Client::Gauge.new(
  252. :test_gauge,
  253. 'test docstring',
  254. foo: nil
  255. )
  256. 1 registry.register(gauge)
  257. 1 gauge.set({ foo: "bar"}, 42)
  258. end
  259. 1 it 'raises an error when grouping key labels conflict with metric labels' do
  260. 2 expect { push.send(:request, Net::HTTP::Post, registry) }.to raise_error(
  261. Prometheus::Client::LabelSetValidator::InvalidLabelSetError
  262. )
  263. end
  264. end
  265. end
  266. end

spec/prometheus/client/rack/collector_spec.rb

100.0% lines covered

44 relevant lines. 44 lines covered and 0 lines missed.
    
  1. # encoding: UTF-8
  2. 1 require 'rack/test'
  3. 1 require 'prometheus/client/rack/collector'
  4. 1 describe Prometheus::Client::Rack::Collector do
  5. 1 include Rack::Test::Methods
  6. 1 before do
  7. 5 allow(Prometheus::Client.configuration).to receive(:value_class).and_return(Prometheus::Client::SimpleValue)
  8. end
  9. 1 let(:registry) do
  10. 5 Prometheus::Client::Registry.new
  11. end
  12. 1 let(:original_app) do
  13. 8 ->(_) { [200, { 'Content-Type' => 'text/html' }, ['OK']] }
  14. end
  15. 1 let!(:app) do
  16. 4 described_class.new(original_app, registry: registry)
  17. end
  18. 1 it 'returns the app response' do
  19. 1 get '/foo'
  20. 1 expect(last_response).to be_ok
  21. 1 expect(last_response.body).to eql('OK')
  22. end
  23. 1 it 'propagates errors in the registry' do
  24. 1 counter = registry.get(:http_requests_total)
  25. 1 expect(counter).to receive(:increment).and_raise(NoMethodError)
  26. 2 expect { get '/foo' }.to raise_error(NoMethodError)
  27. end
  28. 1 it 'traces request information' do
  29. # expect(Time).to receive(:now).and_return(Time.at(0.0), Time.at(0.2))
  30. 1 labels = { method: 'get', host: 'example.org', path: '/foo', code: '200' }
  31. 1 get '/foo'
  32. 1 {
  33. http_requests_total: 1.0,
  34. # http_request_duration_seconds: { 0.5 => 0.2, 0.9 => 0.2, 0.99 => 0.2 }, # TODO: Fix summaries
  35. }.each do |metric, result|
  36. 1 expect(registry.get(metric).get(labels)).to eql(result)
  37. end
  38. end
  39. 1 context 'when the app raises an exception' do
  40. 1 let(:original_app) do
  41. 1 lambda do |env|
  42. 2 raise NoMethodError if env['PATH_INFO'] == '/broken'
  43. 1 [200, { 'Content-Type' => 'text/html' }, ['OK']]
  44. end
  45. end
  46. 1 before do
  47. 1 get '/foo'
  48. end
  49. 1 it 'traces exceptions' do
  50. 1 labels = { exception: 'NoMethodError' }
  51. 2 expect { get '/broken' }.to raise_error NoMethodError
  52. 1 expect(registry.get(:http_exceptions_total).get(labels)).to eql(1.0)
  53. end
  54. end
  55. 1 context 'setting up with a block' do
  56. 1 let(:app) do
  57. 1 described_class.new(original_app, registry: registry) do |env|
  58. 1 { method: env['REQUEST_METHOD'].downcase } # and ignore the path
  59. end
  60. end
  61. 1 it 'allows labels configuration' do
  62. 1 get '/foo/bar'
  63. 1 labels = { method: 'get', code: '200' }
  64. 1 expect(registry.get(:http_requests_total).get(labels)).to eql(1.0)
  65. end
  66. end
  67. end

spec/prometheus/client/rack/exporter_spec.rb

69.81% lines covered

53 relevant lines. 37 lines covered and 16 lines missed.
    
  1. # encoding: UTF-8
  2. 1 require 'rack/test'
  3. 1 require 'prometheus/client/rack/exporter'
  4. 1 xdescribe Prometheus::Client::Rack::Exporter do
  5. 1 include Rack::Test::Methods
  6. 1 let(:registry) do
  7. Prometheus::Client::Registry.new
  8. end
  9. 1 let(:app) do
  10. app = ->(_) { [200, { 'Content-Type' => 'text/html' }, ['OK']] }
  11. Prometheus::Client::Rack::Exporter.new(app, registry: registry)
  12. end
  13. 1 context 'when requesting app endpoints' do
  14. 1 it 'returns the app response' do
  15. get '/foo'
  16. expect(last_response).to be_ok
  17. expect(last_response.body).to eql('OK')
  18. end
  19. end
  20. 1 context 'when requesting /metrics' do
  21. 1 text = Prometheus::Client::Formats::Text
  22. 1 shared_examples 'ok' do |headers, fmt|
  23. 7 it "responds with 200 OK and Content-Type #{fmt::CONTENT_TYPE}" do
  24. registry.counter(:foo, 'foo counter').increment({}, 9)
  25. get '/metrics', nil, headers
  26. expect(last_response.status).to eql(200)
  27. expect(last_response.header['Content-Type']).to eql(fmt::CONTENT_TYPE)
  28. expect(last_response.body).to eql(fmt.marshal(registry))
  29. end
  30. end
  31. 1 shared_examples 'not acceptable' do |headers|
  32. 2 it 'responds with 406 Not Acceptable' do
  33. message = 'Supported media types: text/plain'
  34. get '/metrics', nil, headers
  35. expect(last_response.status).to eql(406)
  36. expect(last_response.header['Content-Type']).to eql('text/plain')
  37. expect(last_response.body).to eql(message)
  38. end
  39. end
  40. 1 context 'when client does not send a Accept header' do
  41. 1 include_examples 'ok', {}, text
  42. end
  43. 1 context 'when client accpets any media type' do
  44. 1 include_examples 'ok', { 'HTTP_ACCEPT' => '*/*' }, text
  45. end
  46. 1 context 'when client requests application/json' do
  47. 1 include_examples 'not acceptable', 'HTTP_ACCEPT' => 'application/json'
  48. end
  49. 1 context 'when client requests text/plain' do
  50. 1 include_examples 'ok', { 'HTTP_ACCEPT' => 'text/plain' }, text
  51. end
  52. 1 context 'when client uses different white spaces in Accept header' do
  53. 1 accept = 'text/plain;q=1.0 ; version=0.0.4'
  54. 1 include_examples 'ok', { 'HTTP_ACCEPT' => accept }, text
  55. end
  56. 1 context 'when client does not include quality attribute' do
  57. 1 accept = 'application/json;q=0.5, text/plain'
  58. 1 include_examples 'ok', { 'HTTP_ACCEPT' => accept }, text
  59. end
  60. 1 context 'when client accepts some unknown formats' do
  61. 1 accept = 'text/plain;q=0.3, proto/buf;q=0.7'
  62. 1 include_examples 'ok', { 'HTTP_ACCEPT' => accept }, text
  63. end
  64. 1 context 'when client accepts only unknown formats' do
  65. 1 accept = 'fancy/woo;q=0.3, proto/buf;q=0.7'
  66. 1 include_examples 'not acceptable', 'HTTP_ACCEPT' => accept
  67. end
  68. 1 context 'when client accepts unknown formats and wildcard' do
  69. 1 accept = 'fancy/woo;q=0.3, proto/buf;q=0.7, */*;q=0.1'
  70. 1 include_examples 'ok', { 'HTTP_ACCEPT' => accept }, text
  71. end
  72. end
  73. end

spec/prometheus/client/registry_spec.rb

100.0% lines covered

55 relevant lines. 55 lines covered and 0 lines missed.
    
  1. # encoding: UTF-8
  2. 1 require 'thread'
  3. 1 require 'prometheus/client/registry'
  4. 1 describe Prometheus::Client::Registry do
  5. 13 let(:registry) { Prometheus::Client::Registry.new }
  6. 1 describe '.new' do
  7. 1 it 'returns a new registry instance' do
  8. 1 expect(registry).to be_a(Prometheus::Client::Registry)
  9. end
  10. end
  11. 1 describe '#register' do
  12. 1 it 'registers a new metric container and returns it' do
  13. 1 metric = double(name: :test)
  14. 1 expect(registry.register(metric)).to eql(metric)
  15. end
  16. 1 it 'raises an exception if a metric name gets registered twice' do
  17. 1 metric = double(name: :test)
  18. 1 registry.register(metric)
  19. 1 expect do
  20. 1 registry.register(metric)
  21. end.to raise_exception described_class::AlreadyRegisteredError
  22. end
  23. 1 it 'is thread safe' do
  24. 1 mutex = Mutex.new
  25. 1 containers = []
  26. 1 def registry.exist?(*args)
  27. 10 super.tap { sleep(0.01) }
  28. end
  29. 1 Array.new(5) do
  30. 5 Thread.new do
  31. result = begin
  32. 5 registry.register(double(name: :test))
  33. rescue Prometheus::Client::Registry::AlreadyRegisteredError
  34. 4 nil
  35. end
  36. 10 mutex.synchronize { containers << result }
  37. end
  38. end.each(&:join)
  39. 1 expect(containers.compact.size).to eql(1)
  40. end
  41. end
  42. 1 describe '#counter' do
  43. 1 it 'registers a new counter metric container and returns the counter' do
  44. 1 metric = registry.counter(:test, 'test docstring')
  45. 1 expect(metric).to be_a(Prometheus::Client::Counter)
  46. end
  47. end
  48. 1 describe '#gauge' do
  49. 1 it 'registers a new gauge metric container and returns the gauge' do
  50. 1 metric = registry.gauge(:test, 'test docstring')
  51. 1 expect(metric).to be_a(Prometheus::Client::Gauge)
  52. end
  53. end
  54. 1 describe '#summary' do
  55. 1 it 'registers a new summary metric container and returns the summary' do
  56. 1 metric = registry.summary(:test, 'test docstring')
  57. 1 expect(metric).to be_a(Prometheus::Client::Summary)
  58. end
  59. end
  60. 1 describe '#histogram' do
  61. 1 it 'registers a new histogram metric container and returns the histogram' do
  62. 1 metric = registry.histogram(:test, 'test docstring')
  63. 1 expect(metric).to be_a(Prometheus::Client::Histogram)
  64. end
  65. end
  66. 1 describe '#exist?' do
  67. 1 it 'returns true if a metric name has been registered' do
  68. 1 registry.register(double(name: :test))
  69. 1 expect(registry.exist?(:test)).to eql(true)
  70. end
  71. 1 it 'returns false if a metric name has not been registered yet' do
  72. 1 expect(registry.exist?(:test)).to eql(false)
  73. end
  74. end
  75. 1 describe '#get' do
  76. 1 it 'returns a previously registered metric container' do
  77. 1 registry.register(double(name: :test))
  78. 1 expect(registry.get(:test)).to be
  79. end
  80. 1 it 'returns nil if the metric has not been registered yet' do
  81. 1 expect(registry.get(:test)).to eql(nil)
  82. end
  83. end
  84. end

spec/prometheus/client/summary_spec.rb

60.0% lines covered

30 relevant lines. 18 lines covered and 12 lines missed.
    
  1. # encoding: UTF-8
  2. 1 require 'prometheus/client/summary'
  3. 1 require 'examples/metric_example'
  4. 1 describe Prometheus::Client::Summary do
  5. 2 let(:summary) { Prometheus::Client::Summary.new(:bar, 'bar description') }
  6. 1 it_behaves_like Prometheus::Client::Metric do
  7. 3 let(:type) { Float }
  8. end
  9. 1 describe '#observe' do
  10. 1 it 'records the given value' do
  11. 1 expect do
  12. 1 summary.observe({}, 5)
  13. 2 end.to change { summary.get }
  14. end
  15. end
  16. 1 xdescribe '#get' do
  17. 1 before do
  18. summary.observe({ foo: 'bar' }, 3)
  19. summary.observe({ foo: 'bar' }, 5.2)
  20. summary.observe({ foo: 'bar' }, 13)
  21. summary.observe({ foo: 'bar' }, 4)
  22. end
  23. 1 it 'returns a set of quantile values' do
  24. expect(summary.get(foo: 'bar')).to eql(0.5 => 4, 0.9 => 5.2, 0.99 => 5.2)
  25. end
  26. 1 it 'returns a value which responds to #sum and #total' do
  27. value = summary.get(foo: 'bar')
  28. expect(value.sum).to eql(25.2)
  29. expect(value.total).to eql(4)
  30. end
  31. 1 it 'uses nil as default value' do
  32. expect(summary.get({})).to eql(0.5 => nil, 0.9 => nil, 0.99 => nil)
  33. end
  34. end
  35. 1 xdescribe '#values' do
  36. 1 it 'returns a hash of all recorded summaries' do
  37. summary.observe({ status: 'bar' }, 3)
  38. summary.observe({ status: 'foo' }, 5)
  39. expect(summary.values).to eql(
  40. { status: 'bar' } => { 0.5 => 3, 0.9 => 3, 0.99 => 3 },
  41. { status: 'foo' } => { 0.5 => 5, 0.9 => 5, 0.99 => 5 },
  42. )
  43. end
  44. end
  45. end

spec/prometheus/client/support/puma_spec.rb

100.0% lines covered

31 relevant lines. 31 lines covered and 0 lines missed.
    
  1. 1 require 'spec_helper'
  2. 1 require 'prometheus/client/support/puma'
  3. 1 class FakePumaWorker
  4. 1 attr_reader :index
  5. 1 def initialize(index)
  6. 1 @index = index
  7. end
  8. end
  9. 1 describe Prometheus::Client::Support::Puma do
  10. 1 describe '.worker_pid_provider' do
  11. 1 let(:worker_id) { '2' }
  12. 3 let(:program_name) { $PROGRAM_NAME }
  13. 5 subject(:worker_pid_provider) { described_class.worker_pid_provider }
  14. 1 before do
  15. 4 expect(described_class).to receive(:program_name)
  16. .at_least(:once)
  17. .and_return(program_name)
  18. end
  19. 1 context 'when the current process is a Puma cluster worker' do
  20. 1 context 'when the process name contains a worker id' do
  21. 2 let(:program_name) { 'puma: cluster worker 2: 34740 [my-app]' }
  22. 2 it { is_expected.to eq('puma_2') }
  23. end
  24. 1 context 'when the process name does not include a worker id' do
  25. 2 let(:worker_number) { 10 }
  26. 1 before do
  27. 1 stub_const('Puma::Cluster::Worker', FakePumaWorker)
  28. 1 FakePumaWorker.new(worker_number)
  29. end
  30. 2 it { is_expected.to eq("puma_#{worker_number}") }
  31. end
  32. end
  33. 1 context 'when the current process is the Puma master' do
  34. 2 let(:program_name) { 'bin/puma' }
  35. 2 it { is_expected.to eq('puma_master') }
  36. end
  37. 1 context 'when it cannot be determined that Puma is running' do
  38. 2 let(:process_id) { 10 }
  39. 1 before do
  40. 1 allow(Process).to receive(:pid).and_return(process_id)
  41. end
  42. 2 it { is_expected.to eq("process_id_#{process_id}") }
  43. end
  44. end
  45. end

spec/prometheus/client/support/unicorn_spec.rb

100.0% lines covered

58 relevant lines. 58 lines covered and 0 lines missed.
    
  1. 1 require 'spec_helper'
  2. 1 require 'prometheus/client/support/unicorn'
  3. 1 class FakeUnicornWorker
  4. 1 attr_reader :nr
  5. 1 def initialize(nr)
  6. 2 @nr = nr
  7. end
  8. end
  9. 1 describe Prometheus::Client::Support::Unicorn do
  10. 1 describe '.worker_id' do
  11. 3 let(:worker_id) { '09' }
  12. 1 around do |example|
  13. 2 old_name = $0
  14. 2 example.run
  15. 2 $0 = old_name
  16. end
  17. 1 context 'process name contains worker id' do
  18. 1 before do
  19. 1 $0 = "program worker[#{worker_id}] arguments"
  20. end
  21. 1 it 'returns worker_id' do
  22. 1 expect(subject.worker_id).to eq(worker_id)
  23. end
  24. end
  25. 1 context 'process name is without worker id' do
  26. 1 it 'calls .object_based_worker_id id provider' do
  27. 1 expect(subject).to receive(:object_based_worker_id).and_return(worker_id)
  28. 1 expect(subject.worker_id).to eq(worker_id)
  29. end
  30. end
  31. end
  32. 1 describe '.object_based_worker_id' do
  33. 1 context 'when Unicorn is defined' do
  34. 1 before do
  35. 4 stub_const('Unicorn::Worker', FakeUnicornWorker)
  36. end
  37. 1 context 'Worker instance is present in ObjectSpace' do
  38. 3 let(:worker_number) { 10 }
  39. 3 let!(:unicorn_worker) { FakeUnicornWorker.new(worker_number) }
  40. 1 it 'Unicorn::Worker to be defined' do
  41. 1 expect(defined?(Unicorn::Worker)).to be_truthy
  42. end
  43. 1 it 'returns worker id' do
  44. 1 expect(described_class.object_based_worker_id).to eq(worker_number)
  45. end
  46. end
  47. 1 context 'Worker instance is not present in ObjectSpace' do
  48. 1 it 'Unicorn::Worker id defined' do
  49. 1 expect(defined?(Unicorn::Worker)).to be_truthy
  50. end
  51. 1 it 'returns no worker id' do
  52. 1 expect(ObjectSpace).to receive(:each_object).with(::Unicorn::Worker).and_return(nil)
  53. 1 expect(described_class.object_based_worker_id).to eq(nil)
  54. end
  55. end
  56. end
  57. 1 context 'Unicorn::Worker is not defined' do
  58. 1 it 'Unicorn::Worker not defined' do
  59. 1 expect(defined?(Unicorn::Worker)).to be_falsey
  60. end
  61. 1 it 'returns no worker_id' do
  62. 1 expect(described_class.object_based_worker_id).to eq(nil)
  63. end
  64. end
  65. end
  66. 1 describe '.worker_pid_provider' do
  67. 1 context 'worker_id is provided' do
  68. 2 let(:worker_id) { 2 }
  69. 1 before do
  70. 1 allow(described_class).to receive(:worker_id).and_return(worker_id)
  71. end
  72. 1 it 'returns worker pid created from worker id' do
  73. 1 expect(described_class.worker_pid_provider).to eq("worker_id_#{worker_id}")
  74. end
  75. end
  76. 1 context 'worker_id is not provided' do
  77. 2 let(:process_id) { 10 }
  78. 1 before do
  79. 1 allow(described_class).to receive(:worker_id).and_return(nil)
  80. 1 allow(Process).to receive(:pid).and_return(process_id)
  81. end
  82. 1 it 'returns worker pid created from Process ID' do
  83. 1 expect(described_class.worker_pid_provider).to eq("process_id_#{process_id}")
  84. end
  85. end
  86. end
  87. end

spec/prometheus/client_spec.rb

100.0% lines covered

41 relevant lines. 41 lines covered and 0 lines missed.
    
  1. # encoding: UTF-8
  2. 1 require 'prometheus/client'
  3. 1 describe Prometheus::Client do
  4. 1 describe '.registry' do
  5. 1 it 'returns a registry object' do
  6. 1 expect(described_class.registry).to be_a(described_class::Registry)
  7. end
  8. 1 it 'memorizes the returned object' do
  9. 1 expect(described_class.registry).to eql(described_class.registry)
  10. end
  11. end
  12. 1 context '.reset! and .reinitialize_on_pid_change' do
  13. 5 let(:metric_name) { :room_temperature_celsius }
  14. 5 let(:label) { { room: 'kitchen' } }
  15. 5 let(:value) { 21 }
  16. 5 let(:gauge) { Prometheus::Client::Gauge.new(metric_name, 'test') }
  17. 1 before do
  18. 4 described_class.cleanup!
  19. 4 described_class.reset! # registering metrics will leak into other specs
  20. 4 registry = described_class.registry
  21. 4 gauge.set(label, value)
  22. 4 registry.register(gauge)
  23. 4 expect(registry.metrics.count).to eq(1)
  24. 4 expect(registry.get(metric_name).get(label)).to eq(value)
  25. end
  26. 1 describe '.reset!' do
  27. 1 it 'resets registry and clears existing metrics' do
  28. 1 described_class.cleanup!
  29. 1 described_class.reset!
  30. 1 registry = described_class.registry
  31. 1 expect(registry.metrics.count).to eq(0)
  32. 1 registry.register(gauge)
  33. 1 expect(registry.get(metric_name).get(label)).not_to eq(value)
  34. end
  35. end
  36. 1 describe '.reinitialize_on_pid_change' do
  37. 1 context 'with force: false' do
  38. 1 it 'calls `MmapedValue.reinitialize_on_pid_change`' do
  39. 1 expect(Prometheus::Client::MmapedValue).to receive(:reinitialize_on_pid_change).and_call_original
  40. 1 described_class.reinitialize_on_pid_change(force: false)
  41. end
  42. end
  43. 1 context 'without explicit :force param' do
  44. 1 it 'defaults to `false` and calls `MmapedValue.reinitialize_on_pid_change`' do
  45. 1 expect(Prometheus::Client::MmapedValue).to receive(:reinitialize_on_pid_change).and_call_original
  46. 1 described_class.reinitialize_on_pid_change
  47. end
  48. end
  49. 1 context 'with force: true' do
  50. 1 it 'calls `MmapedValue.reset_and_reinitialize`' do
  51. 1 expect(Prometheus::Client::MmapedValue).to receive(:reset_and_reinitialize).and_call_original
  52. 1 described_class.reinitialize_on_pid_change(force: true)
  53. end
  54. end
  55. end
  56. end
  57. end