213 lines
6.5 KiB
Ruby
213 lines
6.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class MetricsRegistry
|
|
HTTP_LABELS = %i[method controller action status].freeze
|
|
REQUEST_DURATION_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10].freeze
|
|
STARTED_AT = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
|
|
class << self
|
|
def record_http_request(method:, controller:, action:, status:, duration:)
|
|
labels = http_labels(method:, controller:, action:, status:)
|
|
|
|
synchronize do
|
|
@http_requests[labels] += 1
|
|
histogram = @http_request_duration[labels]
|
|
histogram[:count] += 1
|
|
histogram[:sum] += duration.to_f
|
|
REQUEST_DURATION_BUCKETS.each do |bucket|
|
|
histogram[:buckets][bucket] += 1 if duration.to_f <= bucket
|
|
end
|
|
end
|
|
end
|
|
|
|
def action_cable_connected
|
|
synchronize do
|
|
@action_cable_connections += 1
|
|
@action_cable_connections_total += 1
|
|
end
|
|
end
|
|
|
|
def action_cable_disconnected
|
|
synchronize do
|
|
@action_cable_connections = [@action_cable_connections - 1, 0].max
|
|
end
|
|
end
|
|
|
|
def render_prometheus
|
|
snapshot = snapshot_metrics
|
|
|
|
PrometheusRenderer.new(snapshot).render
|
|
end
|
|
|
|
def reset!
|
|
@mutex = Mutex.new
|
|
@http_requests = Hash.new(0)
|
|
@http_request_duration = Hash.new do |hash, labels|
|
|
hash[labels] = {
|
|
count: 0,
|
|
sum: 0.0,
|
|
buckets: REQUEST_DURATION_BUCKETS.index_with(0)
|
|
}
|
|
end
|
|
@action_cable_connections = 0
|
|
@action_cable_connections_total = 0
|
|
end
|
|
|
|
private
|
|
|
|
def synchronize(&block)
|
|
reset! if @mutex.nil?
|
|
|
|
@mutex.synchronize(&block)
|
|
end
|
|
|
|
def snapshot_metrics
|
|
synchronize do
|
|
{
|
|
http_requests: @http_requests.dup,
|
|
http_request_duration: @http_request_duration.transform_values do |histogram|
|
|
{
|
|
count: histogram.fetch(:count),
|
|
sum: histogram.fetch(:sum),
|
|
buckets: histogram.fetch(:buckets).dup
|
|
}
|
|
end,
|
|
action_cable_connections: @action_cable_connections,
|
|
action_cable_connections_total: @action_cable_connections_total,
|
|
uptime_seconds: Process.clock_gettime(Process::CLOCK_MONOTONIC) - STARTED_AT,
|
|
active_record_pool: active_record_pool_stats,
|
|
git_commit_sha: ENV.fetch('GIT_COMMIT_SHA', 'unknown')
|
|
}
|
|
end
|
|
end
|
|
|
|
def http_labels(method:, controller:, action:, status:)
|
|
{
|
|
method: method.to_s.upcase,
|
|
controller: controller.to_s,
|
|
action: action.to_s,
|
|
status: status.to_i.to_s
|
|
}.freeze
|
|
end
|
|
|
|
def active_record_pool_stats
|
|
return {} unless defined?(ActiveRecord::Base)
|
|
|
|
ActiveRecord::Base.connection_pool.stat
|
|
rescue StandardError
|
|
{}
|
|
end
|
|
end
|
|
|
|
class PrometheusRenderer
|
|
def initialize(snapshot)
|
|
@snapshot = snapshot
|
|
@lines = []
|
|
end
|
|
|
|
def render
|
|
render_build_info
|
|
render_http_requests
|
|
render_http_request_duration
|
|
render_action_cable_connections
|
|
render_process_metrics
|
|
render_active_record_pool
|
|
|
|
"#{lines.join("\n")}\n"
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :snapshot, :lines
|
|
|
|
def render_build_info
|
|
help('turniere_build_info', 'Build metadata for this process.')
|
|
type('turniere_build_info', 'gauge')
|
|
sample('turniere_build_info', 1, git_commit_sha: snapshot.fetch(:git_commit_sha))
|
|
end
|
|
|
|
def render_http_requests
|
|
help('turniere_http_requests_total', 'Total HTTP requests handled by Rails controllers.')
|
|
type('turniere_http_requests_total', 'counter')
|
|
snapshot.fetch(:http_requests).each do |labels, count|
|
|
sample('turniere_http_requests_total', count, labels)
|
|
end
|
|
end
|
|
|
|
def render_http_request_duration
|
|
help('turniere_http_request_duration_seconds', 'HTTP request duration in seconds.')
|
|
type('turniere_http_request_duration_seconds', 'histogram')
|
|
snapshot.fetch(:http_request_duration).each do |labels, histogram|
|
|
histogram.fetch(:buckets).each do |bucket, count|
|
|
sample('turniere_http_request_duration_seconds_bucket', count, labels.merge(le: format_number(bucket)))
|
|
end
|
|
sample('turniere_http_request_duration_seconds_bucket', histogram.fetch(:count), labels.merge(le: '+Inf'))
|
|
sample('turniere_http_request_duration_seconds_sum', histogram.fetch(:sum), labels)
|
|
sample('turniere_http_request_duration_seconds_count', histogram.fetch(:count), labels)
|
|
end
|
|
end
|
|
|
|
def render_action_cable_connections
|
|
help('turniere_action_cable_connections', 'Currently open ActionCable websocket connections.')
|
|
type('turniere_action_cable_connections', 'gauge')
|
|
sample('turniere_action_cable_connections', snapshot.fetch(:action_cable_connections))
|
|
|
|
help('turniere_action_cable_connections_total', 'Total ActionCable websocket connections opened.')
|
|
type('turniere_action_cable_connections_total', 'counter')
|
|
sample('turniere_action_cable_connections_total', snapshot.fetch(:action_cable_connections_total))
|
|
end
|
|
|
|
def render_process_metrics
|
|
help('turniere_process_uptime_seconds', 'Process uptime in seconds.')
|
|
type('turniere_process_uptime_seconds', 'gauge')
|
|
sample('turniere_process_uptime_seconds', snapshot.fetch(:uptime_seconds))
|
|
|
|
help('turniere_ruby_threads', 'Ruby threads visible in this process.')
|
|
type('turniere_ruby_threads', 'gauge')
|
|
sample('turniere_ruby_threads', Thread.list.count)
|
|
end
|
|
|
|
def render_active_record_pool
|
|
help('turniere_active_record_connection_pool', 'ActiveRecord connection pool state.')
|
|
type('turniere_active_record_connection_pool', 'gauge')
|
|
snapshot.fetch(:active_record_pool).each do |state, value|
|
|
sample('turniere_active_record_connection_pool', value, state:)
|
|
end
|
|
end
|
|
|
|
def help(name, text)
|
|
lines << "# HELP #{name} #{text}"
|
|
end
|
|
|
|
def type(name, type)
|
|
lines << "# TYPE #{name} #{type}"
|
|
end
|
|
|
|
def sample(name, value, labels = {})
|
|
lines << "#{name}#{labels_string(labels)} #{format_number(value)}"
|
|
end
|
|
|
|
def labels_string(labels)
|
|
return '' if labels.empty?
|
|
|
|
formatted = labels.map do |key, value|
|
|
%(#{key}="#{escape_label_value(value)}")
|
|
end
|
|
|
|
"{#{formatted.join(',')}}"
|
|
end
|
|
|
|
def escape_label_value(value)
|
|
value.to_s.gsub('\\', '\\\\\\').gsub("\n", '\\n').gsub('"', '\"')
|
|
end
|
|
|
|
def format_number(value)
|
|
return value.to_s if value.is_a?(Integer)
|
|
|
|
format('%.12g', value)
|
|
end
|
|
end
|
|
end
|
|
|
|
MetricsRegistry.reset!
|