Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/prometheus/client/data_stores/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 15 additions & 2 deletions lib/prometheus/client/data_stores/direct_file_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"\
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions lib/prometheus/client/data_stores/single_threaded.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
43 changes: 37 additions & 6 deletions lib/prometheus/client/data_stores/synchronized.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'prometheus/client/value_with_exemplar'

module Prometheus
module Client
module DataStores
Expand All @@ -24,33 +26,62 @@ 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

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
Expand Down
21 changes: 21 additions & 0 deletions lib/prometheus/client/formats/open_metrics.rb
Original file line number Diff line number Diff line change
@@ -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
12 changes: 6 additions & 6 deletions lib/prometheus/client/gauge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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: {})
Expand All @@ -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
Expand Down
52 changes: 52 additions & 0 deletions lib/prometheus/client/value_with_exemplar.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion lib/prometheus/middleware/exporter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'prometheus/client'
require 'prometheus/client/formats/text'
require 'prometheus/client/formats/open_metrics'

module Prometheus
module Middleware
Expand All @@ -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 = {})
Expand Down
2 changes: 1 addition & 1 deletion spec/prometheus/middleware/exporter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down