Last active
July 12, 2022 15:25
-
-
Save RobinDaugherty/72f2bce7a438dd022776f2cee08e2f0d to your computer and use it in GitHub Desktop.
Instrumented module that uses Appsignal to migrate from Instrumental
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# typed: true | |
# frozen_string_literal: true | |
## | |
# Light wrapper around AppSignal's metric methods, providing some helpers similar to Instrumental's (RIP) | |
# library. | |
# Important: Metric names should never be dynamic! | |
# AppSignal is able to take tags as part of instrumentation which can provide granularity that the metric name | |
# does not provide. | |
# But when tags are specified in an event, it's not possible to graph or evaluate the aggregate, so we must | |
# also report the untagged event. | |
# https://docs.appsignal.com/metrics/custom.html#metric-tags | |
module Instrumented | |
## | |
# A gauge is a metric value at a specific time. | |
# If you set more than one gauge with the same key, the last reported value for that moment in time is persisted. | |
# Gauges are used for things like tracking sizes of databases, disks, or other absolute values like CPU usage, | |
# several items (users, accounts, etc.). | |
def self.gauge(metric, value, tags = {}) | |
Appsignal.set_gauge(metric, value, tags) unless tags.empty? | |
Appsignal.set_gauge(metric, value) | |
end | |
## | |
# Increment a counter metric type, which stores a number value for a given time frame. | |
# These counter values are combined into a total count value for the display time frame resolution. | |
def self.increment(metric, value, tags = {}) | |
# rubocop:disable Rails/SkipsModelValidations | |
Appsignal.increment_counter(metric, value, tags) unless tags.empty? | |
Appsignal.increment_counter(metric, value) | |
# rubocop:enable Rails/SkipsModelValidations | |
end | |
## | |
# Used for things like response times and background job durations. Times should be in milliseconds. | |
def self.measurement(metric, value, tags = {}) | |
Appsignal.add_distribution_value(metric, value, tags) unless tags.empty? | |
Appsignal.add_distribution_value(metric, value) | |
end | |
## | |
# Perform the work in the passed block and set a measurement metric with the duration of block execution. | |
def self.time(metric, tags = {}, &block) | |
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) | |
retval = nil | |
begin | |
retval = yield | |
ensure | |
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) | |
measurement(metric, (end_time - start_time) * 1000, tags) | |
end | |
retval | |
end | |
## | |
# Perform the work in the passed block, and increment counters representing the number of times this work | |
# was started, and either completed or failed. | |
def self.work(key_prefix, tags = {}, &block) | |
increment("#{key_prefix}.started", 1, tags) | |
begin | |
result = time("#{key_prefix}.duration", tags, &block) | |
increment("#{key_prefix}.done", 1, tags) | |
result | |
rescue | |
increment("#{key_prefix}.fail", 1, tags) | |
raise | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# typed: false | |
# frozen_string_literal: true | |
require 'rails_helper' | |
RSpec.describe Instrumented do | |
before do | |
allow(Appsignal).to receive(:add_distribution_value) | |
allow(Appsignal).to receive(:set_gauge) | |
allow(Appsignal).to receive(:increment_counter) | |
end | |
describe '.gauge' do | |
context 'with tags' do | |
it 'calls Appsignal.set_gauge with and without the tags' do | |
described_class.gauge('metric_name', 36, tag42: 84) | |
expect(Appsignal).to have_received(:set_gauge).with('metric_name', 36, tag42: 84).once | |
expect(Appsignal).to have_received(:set_gauge).with('metric_name', 36).once | |
end | |
end | |
context 'without tags' do | |
it 'calls Appsignal.set_gauge' do | |
described_class.gauge('metric_name', 36) | |
expect(Appsignal).to have_received(:set_gauge).with('metric_name', 36).once | |
end | |
end | |
end | |
describe '.increment' do | |
context 'with tags' do | |
it 'calls Appsignal.increment_counter with and without tags' do | |
described_class.increment('metric_name', 36, tag42: 84) | |
expect(Appsignal).to have_received(:increment_counter).with('metric_name', 36, tag42: 84).once | |
expect(Appsignal).to have_received(:increment_counter).with('metric_name', 36).once | |
end | |
end | |
context 'without tags' do | |
it 'calls Appsignal.increment_counter' do | |
described_class.increment('metric_name', 36) | |
expect(Appsignal).to have_received(:increment_counter).with('metric_name', 36).once | |
end | |
end | |
end | |
describe '.measurement' do | |
context 'with tags' do | |
it 'calls Appsignal.add_distribution_value' do | |
described_class.measurement('metric_name', 36, tag42: 84) | |
expect(Appsignal).to have_received(:add_distribution_value).with('metric_name', 36, tag42: 84).once | |
expect(Appsignal).to have_received(:add_distribution_value).with('metric_name', 36).once | |
end | |
end | |
context 'without tags' do | |
it 'calls Appsignal.add_distribution_value' do | |
described_class.measurement('metric_name', 36) | |
expect(Appsignal).to have_received(:add_distribution_value).with('metric_name', 36).once | |
end | |
end | |
end | |
describe '.time' do | |
it 'yields to the block' do | |
expect { |b| described_class.time('metric_name', &b) }.to yield_control | |
end | |
it 'returns the value returned by the block' do | |
expect(described_class.time('metric_name') { 43 }).to eq(43) | |
end | |
it 'instruments the number of milliseconds that the block took to execute' do | |
described_class.time('metric_name', tag1: 42) do | |
sleep 0.2 | |
end | |
expect(Appsignal).to have_received(:add_distribution_value) | |
.with('metric_name', a_value_between(200, 500), tag1: 42) | |
end | |
context 'when the block raises an error' do | |
subject(:time) { | |
described_class.time('metric_name', tag1: 42) do | |
sleep 0.2 | |
raise 'test' | |
end | |
} | |
it 'allows the error to be raised' do | |
expect { time }.to raise_error(RuntimeError, 'test') | |
end | |
it 'instruments the number of milliseconds that the block took to execute' do | |
expect { time }.to raise_error(RuntimeError, 'test') | |
expect(Appsignal).to have_received(:add_distribution_value) | |
.with('metric_name', a_value_between(200, 500), tag1: 42) | |
end | |
end | |
end | |
describe '.work' do | |
it 'yields to the block' do | |
expect { |b| described_class.work('work_name', &b) }.to yield_control | |
end | |
it 'returns the value returned by the block' do | |
expect(described_class.work('metric_name') { 43 }).to eq(43) | |
end | |
it 'increments a counter for the start of the work' do | |
described_class.work('work_name', tag1: 42) {} | |
expect(Appsignal).to have_received(:increment_counter).with('work_name.started', 1, tag1: 42) | |
end | |
context 'when the block does not raise an error' do | |
it 'increments a counter for the completion of the work' do | |
described_class.work('work_name', tag1: 42) {} | |
expect(Appsignal).to have_received(:increment_counter).with('work_name.done', 1, tag1: 42) | |
expect(Appsignal).not_to have_received(:increment_counter).with('work_name.fail', any_args) | |
end | |
end | |
it 'instruments the number of milliseconds that the block took to execute' do | |
described_class.work('work_name', tag1: 42) do | |
sleep 0.2 | |
end | |
expect(Appsignal).to have_received(:add_distribution_value) | |
.with('work_name.duration', a_value_between(200, 500), tag1: 42) | |
end | |
context 'when the block raises an error' do | |
subject(:work) { | |
described_class.work('work_name', tag1: 42) do | |
raise 'test' | |
end | |
} | |
it 'allows the error to be raised' do | |
expect { work }.to raise_error(RuntimeError, 'test') | |
end | |
it 'instruments the failure of the work' do | |
expect { work }.to raise_error(RuntimeError, 'test') | |
expect(Appsignal).to have_received(:increment_counter).with('work_name.fail', 1, tag1: 42) | |
expect(Appsignal).not_to have_received(:increment_counter).with('work_name.done', any_args) | |
end | |
it 'instruments the number of milliseconds that the block took to execute' do | |
expect { | |
described_class.work('work_name', tag1: 42) do | |
sleep 0.2 | |
raise 'test' | |
end | |
}.to raise_error(RuntimeError, 'test') | |
expect(Appsignal).to have_received(:add_distribution_value) | |
.with('work_name.duration', a_value_between(200, 500), tag1: 42) | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment