diff --git a/lib/prometheus/client/data_stores/README.md b/lib/prometheus/client/data_stores/README.md index 3e396a77..32225aca 100644 --- a/lib/prometheus/client/data_stores/README.md +++ b/lib/prometheus/client/data_stores/README.md @@ -102,11 +102,11 @@ module Prometheus # Store a value for this metric and a set of labels # Internally, may add extra "labels" to disambiguate values between, # for example, different processes - def set(labels:, val:) + def set(labels:, val:, exemplar_labels: {}) raise NotImplementedError end - def increment(labels:, by: 1) + def increment(labels:, by: 1, exemplar_labels: {}) raise NotImplementedError end diff --git a/lib/prometheus/client/data_stores/direct_file_store.rb b/lib/prometheus/client/data_stores/direct_file_store.rb index 1c09dc4d..e23fc718 100644 --- a/lib/prometheus/client/data_stores/direct_file_store.rb +++ b/lib/prometheus/client/data_stores/direct_file_store.rb @@ -99,13 +99,13 @@ def synchronize end end - def set(labels:, val:) + def set(labels:, val:, exemplar_labels: {}) # no exemplar support, this change to make tests pass in_process_sync do internal_store.write_value(store_key(labels), val.to_f) end end - def increment(labels:, by: 1) + def increment(labels:, by: 1, exemplar_labels: {}) # no exemplar support, this change to make tests pass if @values_aggregation_mode == DirectFileStore::MOST_RECENT raise InvalidStoreSettingsError, "The :most_recent aggregation does not support the use of increment"\ @@ -263,6 +263,19 @@ def all_values end end + # I completely punted on this. Here is a sketch of a solution with no promises that this is a good idea + # + # Instead of storing a single value, store the following four values for each labelset on the metric: + # - value: 8 byte float + # - exemplar label: 140 bytes (fixed width) + # 128 bytes of label data + some amount of buffer for control characters generated in store_key (& = etc) + # - exemplar value: 8 byte float + # - exemplar timestamp: 8 byte float (need to double check the spec) + # + # - update @positions and @used for this wider value in init_value ~164 bytes + # - add read_value_with_exemplar and write_value_with_exemplar + # - keep read_value and write_value exactly the same. The only difference when they run is + # the return value of @positions[key] will have a bigger offset than before. def read_value(key) if !@positions.has_key?(key) init_value(key) diff --git a/lib/prometheus/client/data_stores/single_threaded.rb b/lib/prometheus/client/data_stores/single_threaded.rb index f05cf813..7e65e18c 100644 --- a/lib/prometheus/client/data_stores/single_threaded.rb +++ b/lib/prometheus/client/data_stores/single_threaded.rb @@ -32,11 +32,11 @@ def synchronize yield end - def set(labels:, val:) + def set(labels:, val:, exemplar_labels: {}) # no exemplar support for now, this change to make tests pass @internal_store[labels] = val.to_f end - def increment(labels:, by: 1) + def increment(labels:, by: 1, exemplar_labels: {}) # no exemplar support for now, this change to make tests pass @internal_store[labels] += by end diff --git a/lib/prometheus/client/data_stores/synchronized.rb b/lib/prometheus/client/data_stores/synchronized.rb index d0a74608..71fb7c1e 100644 --- a/lib/prometheus/client/data_stores/synchronized.rb +++ b/lib/prometheus/client/data_stores/synchronized.rb @@ -1,3 +1,5 @@ +require 'prometheus/client/value_with_exemplar' + module Prometheus module Client module DataStores @@ -24,7 +26,7 @@ def validate_metric_settings(metric_settings:) class MetricStore def initialize - @internal_store = Hash.new { |hash, key| hash[key] = 0.0 } + @internal_store = Hash.new { |hash, key| hash[key] = ValueWithExemplar.new } @lock = Monitor.new end @@ -32,25 +34,54 @@ def synchronize @lock.synchronize { yield } end - def set(labels:, val:) + def set(labels:, val:, exemplar_labels: {}) synchronize do - @internal_store[labels] = val.to_f + vwe = @internal_store[labels] + vwe.value = val + vwe.setup_exemplar(exemplar_labels) + + vwe.value end end - def increment(labels:, by: 1) + def increment(labels:, by: 1, exemplar_labels: {}) synchronize do - @internal_store[labels] += by + vwe = @internal_store[labels] + vwe.value = vwe.value + by + vwe.setup_exemplar(exemplar_labels) + + vwe.value end end + # get and get_with_exemplars probably combined into a single method eventually def get(labels:) synchronize do - @internal_store[labels] + @internal_store[labels].value end end + def get_with_exemplars(labels:) + synchronize do + @internal_store[labels] + end + end + + # all_values and all_values_with_exemplars probably combined into a single method + # eventually def all_values + synchronize do + # this code is bad and I feel bad about it + new_store = Hash.new { |hash, key| hash[key] = 0.0 } + @internal_store.keys.each do |k| + new_store[k] = @internal_store[k].value + end + + new_store + end + end + + def all_values_with_exemplars synchronize { @internal_store.dup } end end diff --git a/lib/prometheus/client/formats/open_metrics.rb b/lib/prometheus/client/formats/open_metrics.rb new file mode 100644 index 00000000..d0df3e9c --- /dev/null +++ b/lib/prometheus/client/formats/open_metrics.rb @@ -0,0 +1,21 @@ +# encoding: UTF-8 + +module Prometheus + module Client + module Formats + module OpenMetrics + # used by the middleware to determine if this format works for the request + MEDIA_TYPE = 'application/openmetrics-text'.freeze + VERSION = '1.0.0'.freeze + CONTENT_TYPE = "#{MEDIA_TYPE}; version=#{VERSION}; charset=utf-8".freeze + DELIMITER = "\n".freeze + EOF = "# EOF\n".freeze + + # public interface to generate out the /metrics payload + def self.marshal(registry) + "hello world" + end + end + end + end +end diff --git a/lib/prometheus/client/gauge.rb b/lib/prometheus/client/gauge.rb index fbcfdd4a..7e8e7c10 100644 --- a/lib/prometheus/client/gauge.rb +++ b/lib/prometheus/client/gauge.rb @@ -12,12 +12,12 @@ def type end # Sets the value for the given label set - def set(value, labels: {}) + def set(value, labels: {}, exemplar_labels: {}) unless value.is_a?(Numeric) raise ArgumentError, 'value must be a number' end - @store.set(labels: label_set_for(labels), val: value) + @store.set(labels: label_set_for(labels), val: value, exemplar_labels: exemplar_labels) end def set_to_current_time(labels: {}) @@ -26,16 +26,16 @@ def set_to_current_time(labels: {}) # Increments Gauge value by 1 or adds the given value to the Gauge. # (The value can be negative, resulting in a decrease of the Gauge.) - def increment(by: 1, labels: {}) + def increment(by: 1, labels: {}, exemplar_labels: {}) label_set = label_set_for(labels) - @store.increment(labels: label_set, by: by) + @store.increment(labels: label_set, by: by, exemplar_labels: exemplar_labels) end # Decrements Gauge value by 1 or subtracts the given value from the Gauge. # (The value can be negative, resulting in a increase of the Gauge.) - def decrement(by: 1, labels: {}) + def decrement(by: 1, labels: {}, exemplar_labels: {}) label_set = label_set_for(labels) - @store.increment(labels: label_set, by: -by) + @store.increment(labels: label_set, by: -by, exemplar_labels: exemplar_labels) end end end diff --git a/lib/prometheus/client/value_with_exemplar.rb b/lib/prometheus/client/value_with_exemplar.rb new file mode 100644 index 00000000..00c52e35 --- /dev/null +++ b/lib/prometheus/client/value_with_exemplar.rb @@ -0,0 +1,52 @@ +# encoding: UTF-8 + +module Prometheus + module Client + class ValueWithExemplar + attr_accessor :value, :exemplar_labels, :exemplar_value, :exemplar_timestamp + + def self.from_json(json) + attributes = JSON.parse(json) + new(attributes) # this needs some more initialize args to actually work + end + + def initialize(val: 0) + self.value = val + end + + def value=(new_val) + @value = new_val.to_f + end + + # to be cleaned up + def setup_exemplar(data) + if !data.empty? # don't store an exemplar on an empty values hash (might need to be converted to a nil check) + now = Time.now + + if !exemplar_timestamp # no previous exemplar set + exemplar_labels = data + exemplar_value = value + exemplar_timestamp = now + elsif now > exemplar_timestamp # previous exemplar in the past + exemplar_labels = data + exemplar_value = value + exemplar_timestamp = now + end + end + end + + def to_hash + { + value: value, + exemplar_labels: exemplar_labels, + exemplar_value: exemplar_value, + exemplar_timestamp: exemplar_timestamp + } + end + + def to_json + to_hash.to_json + end + end + end +end diff --git a/lib/prometheus/middleware/exporter.rb b/lib/prometheus/middleware/exporter.rb index a377525c..dc4b7b44 100644 --- a/lib/prometheus/middleware/exporter.rb +++ b/lib/prometheus/middleware/exporter.rb @@ -2,6 +2,7 @@ require 'prometheus/client' require 'prometheus/client/formats/text' +require 'prometheus/client/formats/open_metrics' module Prometheus module Middleware @@ -14,7 +15,7 @@ module Middleware class Exporter attr_reader :app, :registry, :path - FORMATS = [Client::Formats::Text].freeze + FORMATS = [Client::Formats::Text, Client::Formats::OpenMetrics].freeze # I think this is all that is needed but I might be missing something FALLBACK = Client::Formats::Text def initialize(app, options = {}) diff --git a/spec/prometheus/middleware/exporter_spec.rb b/spec/prometheus/middleware/exporter_spec.rb index e8232fc5..376841ee 100644 --- a/spec/prometheus/middleware/exporter_spec.rb +++ b/spec/prometheus/middleware/exporter_spec.rb @@ -42,7 +42,7 @@ shared_examples 'not acceptable' do |headers| it 'responds with 406 Not Acceptable' do - message = 'Supported media types: text/plain' + message = 'Supported media types: text/plain, application/openmetrics-text' get '/metrics', nil, headers