Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finish improvements to push client #234

Merged
merged 7 commits into from
Jan 9, 2022
Merged
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
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