Skip to content

Commit

Permalink
Merge pull request #234 from prometheus/sinjo-push-client-improvements
Browse files Browse the repository at this point in the history
Finish improvements to push client
  • Loading branch information
Sinjo authored Jan 9, 2022
2 parents 853a839 + 2105319 commit 309de65
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 28 deletions.
2 changes: 1 addition & 1 deletion lib/prometheus/client/metric.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module Prometheus
module Client
# Metric
class Metric
attr_reader :name, :docstring, :preset_labels
attr_reader :name, :docstring, :labels, :preset_labels

def initialize(name,
docstring:,
Expand Down
90 changes: 79 additions & 11 deletions lib/prometheus/client/push.rb
Original file line number Diff line number Diff line change
@@ -1,35 +1,44 @@
# encoding: UTF-8

require 'base64'
require 'thread'
require 'net/http'
require 'uri'
require 'erb'
require 'set'

require 'prometheus/client'
require 'prometheus/client/formats/text'
require 'prometheus/client/label_set_validator'

module Prometheus
# Client is a ruby implementation for a Prometheus compatible client.
module Client
# Push implements a simple way to transmit a given registry to a given
# Pushgateway.
class Push
class HttpError < StandardError; end
class HttpRedirectError < HttpError; end
class HttpClientError < HttpError; end
class HttpServerError < HttpError; end

DEFAULT_GATEWAY = 'http://localhost:9091'.freeze
PATH = '/metrics/job/%s'.freeze
INSTANCE_PATH = '/metrics/job/%s/instance/%s'.freeze
SUPPORTED_SCHEMES = %w(http https).freeze

attr_reader :job, :instance, :gateway, :path
attr_reader :job, :gateway, :path

def initialize(job:, instance: nil, gateway: DEFAULT_GATEWAY, **kwargs)
def initialize(job:, gateway: DEFAULT_GATEWAY, grouping_key: {}, **kwargs)
raise ArgumentError, "job cannot be nil" if job.nil?
raise ArgumentError, "job cannot be empty" if job.empty?
@validator = LabelSetValidator.new(expected_labels: grouping_key.keys)
@validator.validate_symbols!(grouping_key)

@mutex = Mutex.new
@job = job
@instance = instance
@gateway = gateway || DEFAULT_GATEWAY
@path = build_path(job, instance)
@grouping_key = grouping_key
@path = build_path(job, grouping_key)
@uri = parse("#{@gateway}#{@path}")

@http = Net::HTTP.new(@uri.host, @uri.port)
Expand Down Expand Up @@ -70,26 +79,85 @@ def parse(url)
raise ArgumentError, "#{url} is not a valid URL: #{e}"
end

def build_path(job, instance)
if instance && !instance.empty?
format(INSTANCE_PATH, ERB::Util::url_encode(job), ERB::Util::url_encode(instance))
else
format(PATH, ERB::Util::url_encode(job))
def build_path(job, grouping_key)
path = format(PATH, ERB::Util::url_encode(job))

grouping_key.each do |label, value|
if value.include?('/')
encoded_value = Base64.urlsafe_encode64(value)
path += "/#{label}@base64/#{encoded_value}"
# While it's valid for the urlsafe_encode64 function to return an
# empty string when the input string is empty, it doesn't work for
# our specific use case as we're putting the result into a URL path
# segment. A double slash (`//`) can be normalised away by HTTP
# libraries, proxies, and web servers.
#
# For empty strings, we use a single padding character (`=`) as the
# value.
#
# See the pushgateway docs for more details:
#
# https://github.com/prometheus/pushgateway/blob/6393a901f56d4dda62cd0f6ab1f1f07c495b6354/README.md#url
elsif value.empty?
path += "/#{label}@base64/="
else
path += "/#{label}/#{ERB::Util::url_encode(value)}"
end
end

path
end

def request(req_class, registry = nil)
validate_no_label_clashes!(registry) if registry

req = req_class.new(@uri)
req.content_type = Formats::Text::CONTENT_TYPE
req.basic_auth(@uri.user, @uri.password) if @uri.user
req.body = Formats::Text.marshal(registry) if registry

@http.request(req)
response = @http.request(req)
validate_response!(response)

response
end

def synchronize
@mutex.synchronize { yield }
end

def validate_no_label_clashes!(registry)
# There's nothing to check if we don't have a grouping key
return if @grouping_key.empty?

# We could be doing a lot of comparisons, so let's do them against a
# set rather than an array
grouping_key_labels = @grouping_key.keys.to_set

registry.metrics.each do |metric|
metric.labels.each do |label|
if grouping_key_labels.include?(label)
raise LabelSetValidator::InvalidLabelSetError,
"label :#{label} from grouping key collides with label of the " \
"same name from metric :#{metric.name} and would overwrite it"
end
end
end
end

def validate_response!(response)
status = Integer(response.code)
if status >= 300
message = "status: #{response.code}, message: #{response.message}, body: #{response.body}"
if status <= 399
raise HttpRedirectError, message
elsif status <= 499
raise HttpClientError, message
else
raise HttpServerError, message
end
end
end
end
end
end
Loading

0 comments on commit 309de65

Please sign in to comment.