Skip to content

Instantly share code, notes, and snippets.

@RobinDaugherty
Last active July 12, 2022 15:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RobinDaugherty/72f2bce7a438dd022776f2cee08e2f0d to your computer and use it in GitHub Desktop.
Save RobinDaugherty/72f2bce7a438dd022776f2cee08e2f0d to your computer and use it in GitHub Desktop.
Instrumented module that uses Appsignal to migrate from Instrumental
# 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
# 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