Extension for Ruby Prometheus Exporter. Adds DSL for building your custom Prometheus instrumentations and collectors. Allow to remove/zero expired gauge metrics in a collector.
Install the gem and add to the application's Gemfile by executing:
$ bundle add prometheus_exporter-ext
If bundler is not being used to manage dependencies, install the gem by executing:
$ gem install prometheus_exporter-ext
create instrumentation
# lib/prometheus/my_instrumentation.rb
require 'prometheus_exporter/ext'
require 'prometheus_exporter/ext/instrumentation/base_stats'
module Prometheus
class MyInstrumentation < ::PrometheusExporter::Ext::Instrumentation::BaseStats
self.type = 'my'
def collect(duration, operation)
collect_data(
labels: { operation_name: operation },
last_duration_seconds: duration,
duration_seconds_sum: duration,
duration_seconds_count: 1
)
rescue StandardError => e
Rails.logger.error("Failed to send metrics Prometheus #{self.class.name} #{e}")
Rails.error.report(e, handled: true, severity: :error, context: { prometheus: self.class.name })
end
end
end
then send metrics from your code
time_start = Time.current.to_i
begin
MyOperation.run
ensure
duration = Time.current.to_i - time_start
Prometheus::MyInstrumentation.new.collect(duration, 'my_operation')
## you can add additional labels or override client
Prometheus::MyInstrumentation.new(
client: PrometheusExporter::Client.new(...),
metric_labels: { foo: 'bar' }
).collect(duration)
end
so metrics will be collected by
require 'prometheus_exporter/ext'
require 'prometheus_exporter/ext/server/stats_collector'
module Prometheus
class MyCollector < ::PrometheusExporter::Server::TypeCollector
include ::PrometheusExporter::Ext::Server::StatsCollector
self.type = 'my'
# The `register_gauge_with_expire` will remove or zero expired metric.
# when no :strategy option passed, default is `:removing`, available options are `:removing, :zeroing`.
# when no :ttl option passed, default is 60, any numeric greater than 0 can be used.
register_gauge_with_expire :last_duration_seconds, 'duration of last operation execution', ttl: 300
register_counter :task_duration_seconds_sum, 'sum of operation execution durations'
register_counter :task_duration_seconds_count, 'sum of operation execution runs'
end
end
as alternative you can use ExpiredStatsCollector
if you want all metric data to be removed after expiration
require 'prometheus_exporter/ext'
require 'prometheus_exporter/ext/server/stats_collector'
module Prometheus
class MyCollector < ::PrometheusExporter::Server::TypeCollector
include ::PrometheusExporter::Ext::Server::ExpiredStatsCollector
self.type = 'my'
self.ttl = 300 # default 60
# Optionally you can expire old_metric when specific new metric is collected.
# If this block returns true then old_metric will be removed.
unique_metric_by do |new_metric, old_metric|
new_metric['labels'] == old_metric['labels']
end
register_gauge :last_duration_seconds, 'duration of last operation execution'
register_counter :task_duration_seconds_sum, 'sum of operation execution durations'
register_counter :task_duration_seconds_count, 'sum of operation execution runs'
end
end
create instrumentation
# lib/prometheus/my_instrumentation.rb
require 'prometheus_exporter/ext'
require 'prometheus_exporter/ext/instrumentation/periodic_stats'
module Prometheus
class MyPeriodicInstrumentation < ::PrometheusExporter::Ext::Instrumentation::PeriodicStats
self.type = 'my'
def collect
count = MyItem.processed.count
last_duration = MyItem.processed.last&.duration
collect_data(
labels: { some_label: 'some_value' },
last_processed_duration: last_duration || 0,
processed_count: count
)
rescue StandardError => e
Rails.logger.error("Failed to send metrics Prometheus #{self.class.name} #{e}")
Rails.error.report(e, handled: true, severity: :error, context: { prometheus: self.class.name })
end
end
end
then send metrics from your code
Prometheus::MyInstrumentation.start
## you can override frequency in seconds
Prometheus::MyInstrumentation.start(frequency: 60)
## also you can add additional labels or override client
Prometheus::MyInstrumentation.start(
client: PrometheusExporter::Client.new(...),
metric_labels: { foo: 'bar' }
)
# to stop instrumentation call `Prometheus::MyInstrumentation.stop`
so metrics will be collected by
require 'prometheus_exporter/ext'
require 'prometheus_exporter/ext/server/stats_collector'
module Prometheus
class MyCollector < ::PrometheusExporter::Server::TypeCollector
include ::PrometheusExporter::Ext::Server::StatsCollector
self.type = 'my'
# Default ttl 60, default strategy `:removing`.
register_gauge_with_expire :last_processed_duration, 'duration of last processed record'
register_metric :processed_count, :gauge_with_time, 'count of processed records'
end
end
as alternative you can use ExpiredStatsCollector
if you want all metric data to be removed after expiration
require 'prometheus_exporter/ext'
require 'prometheus_exporter/ext/server/stats_collector'
module Prometheus
class MyCollector < ::PrometheusExporter::Server::TypeCollector
include ::PrometheusExporter::Ext::Server::ExpiredStatsCollector
self.type = 'my'
# By default ttl is 60
# By default deletes old metrics only when it's expired
register_gauge :last_processed_duration, 'duration of last processed record'
register_metric :processed_count, :gauge_with_time, 'count of processed records'
end
end
instrumentation test
require 'prometheus_exporter/ext/rspec'
RSpec.describe Prometheus::MyInstrumentation do
describe '#collect' do
subject { described_class.new.collect(duration, operation) }
let(:duration) { 1.23 }
let(:operation) { 'test' }
it 'sends prometheus metrics' do
expect { subject }.to send_metrics(
[
type: 'my',
metric_labels: {},
labels: { operation_name: operation },
last_duration_seconds: duration,
duration_seconds_sum: duration,
duration_seconds_count: 1
]
)
end
end
end
collector test
RSpec.describe Prometheus::MyCollector do
describe '#collect' do
subject do
collector.metrics
end
let(:collector) { described_class.new }
let(:metric) do
{
type: 'my',
metric_labels: {},
labels: { operation_name: 'test' },
last_duration_seconds: 1.2,
duration_seconds_sum: 3.4,
duration_seconds_count: 1
}
end
let(:collect_data) do
collector.collect(metric.deep_stringify_keys)
end
it 'observes prometheus metrics' do
subject
expect(collector.metrics).to contain_exactly(
a_gauge_with_expire_metric('my_last_duration_seconds').with(1.2, metric[:labels]),
a_counter_metric('my_duration_seconds_sum').with(3.4, metric[:labels]),
a_counter_metric('my_duration_seconds_count').with(1, metric[:labels])
)
end
context 'when collected data is expired' do
let(:collect_data) do
super()
sleep 60.1 # when gauge_with_expire ttl is 60
end
it 'observes empty prometheus metrics' do
subject
expect(collector.metrics).to contain_exactly(
a_gauge_with_expire_metric('my_last_duration_seconds').empty,
a_counter_metric('my_duration_seconds_sum').with(3.4, metric[:labels]),
a_counter_metric('my_duration_seconds_count').with(1, metric[:labels])
)
end
end
end
end
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and the created tag, and push the .gem
file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/didww/prometheus_exporter-ext.
The gem is available as open source under the terms of the MIT License.