From cf8f7ae0b083ee517b0a6be8d4c2d9e99c468bb8 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Tue, 12 Mar 2024 14:12:41 +0100 Subject: [PATCH] Add Sentry::Metrics.timing API to measure blocks (#2254) --- CHANGELOG.md | 6 +++ sentry-ruby/lib/sentry/metrics.rb | 17 ++++++ sentry-ruby/lib/sentry/metrics/timing.rb | 43 +++++++++++++++ .../spec/sentry/metrics/timing_spec.rb | 54 +++++++++++++++++++ sentry-ruby/spec/sentry/metrics_spec.rb | 25 +++++++++ 5 files changed, 145 insertions(+) create mode 100644 sentry-ruby/lib/sentry/metrics/timing.rb create mode 100644 sentry-ruby/spec/sentry/metrics/timing_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index ee66bffa6..480ab8720 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Add `hint:` support to `Sentry::Rails::ErrorSubscriber` [#2235](https://github.com/getsentry/sentry-ruby/pull/2235) - Add [Metrics](https://docs.sentry.io/product/metrics/) support - Add main APIs and `Aggregator` thread [#2247](https://github.com/getsentry/sentry-ruby/pull/2247) + - Add `Sentry::Metrics.timing` API for measuring block duration [#2254](https://github.com/getsentry/sentry-ruby/pull/2254) The SDK now supports recording and aggregating metrics. A new thread will be started for aggregation and will flush the pending data to Sentry every 5 seconds. @@ -36,6 +37,11 @@ # set - get unique counts of elements Sentry::Metrics.set('user_view', 'jane') + + # timing - measure duration of code block, defaults to seconds + Sentry::Metrics.timing('how_long') { sleep(1) } + # timing - measure duration of code block in other duraton units + Sentry::Metrics.timing('how_long_ms', unit: 'millisecond') { sleep(0.5) } ``` ### Bug Fixes diff --git a/sentry-ruby/lib/sentry/metrics.rb b/sentry-ruby/lib/sentry/metrics.rb index 190c026aa..c8d043c51 100644 --- a/sentry-ruby/lib/sentry/metrics.rb +++ b/sentry-ruby/lib/sentry/metrics.rb @@ -5,10 +5,15 @@ require 'sentry/metrics/distribution_metric' require 'sentry/metrics/gauge_metric' require 'sentry/metrics/set_metric' +require 'sentry/metrics/timing' require 'sentry/metrics/aggregator' module Sentry module Metrics + DURATION_UNITS = %w[nanosecond microsecond millisecond second minute hour day week] + INFORMATION_UNITS = %w[bit byte kilobyte kibibyte megabyte mebibyte gigabyte gibibyte terabyte tebibyte petabyte pebibyte exabyte exbibyte] + FRACTIONAL_UNITS = %w[ratio percent] + class << self def increment(key, value = 1.0, unit: 'none', tags: {}, timestamp: nil) Sentry.metrics_aggregator&.add(:c, key, value, unit: unit, tags: tags, timestamp: timestamp) @@ -25,6 +30,18 @@ def set(key, value, unit: 'none', tags: {}, timestamp: nil) def gauge(key, value, unit: 'none', tags: {}, timestamp: nil) Sentry.metrics_aggregator&.add(:g, key, value, unit: unit, tags: tags, timestamp: timestamp) end + + def timing(key, unit: 'second', tags: {}, timestamp: nil, &block) + return unless Sentry.metrics_aggregator + return unless block_given? + return unless DURATION_UNITS.include?(unit) + + start = Timing.send(unit.to_sym) + yield + value = Timing.send(unit.to_sym) - start + + Sentry.metrics_aggregator.add(:d, key, value, unit: unit, tags: tags, timestamp: timestamp) + end end end end diff --git a/sentry-ruby/lib/sentry/metrics/timing.rb b/sentry-ruby/lib/sentry/metrics/timing.rb new file mode 100644 index 000000000..510434583 --- /dev/null +++ b/sentry-ruby/lib/sentry/metrics/timing.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Sentry + module Metrics + module Timing + class << self + def nanosecond + time = Sentry.utc_now + time.to_i * (10 ** 9) + time.nsec + end + + def microsecond + time = Sentry.utc_now + time.to_i * (10 ** 6) + time.usec + end + + def millisecond + Sentry.utc_now.to_i * (10 ** 3) + end + + def second + Sentry.utc_now.to_i + end + + def minute + Sentry.utc_now.to_i / 60.0 + end + + def hour + Sentry.utc_now.to_i / 3600.0 + end + + def day + Sentry.utc_now.to_i / (3600.0 * 24.0) + end + + def week + Sentry.utc_now.to_i / (3600.0 * 24.0 * 7.0) + end + end + end + end +end diff --git a/sentry-ruby/spec/sentry/metrics/timing_spec.rb b/sentry-ruby/spec/sentry/metrics/timing_spec.rb new file mode 100644 index 000000000..8142fbb4d --- /dev/null +++ b/sentry-ruby/spec/sentry/metrics/timing_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +RSpec.describe Sentry::Metrics::Timing do + let(:fake_time) { Time.new(2024, 1, 2, 3, 4, 5) } + before { allow(Time).to receive(:now).and_return(fake_time) } + + describe '.nanosecond' do + it 'returns nanoseconds' do + expect(described_class.nanosecond).to eq(fake_time.to_i * 10 ** 9) + end + end + + describe '.microsecond' do + it 'returns microseconds' do + expect(described_class.microsecond).to eq(fake_time.to_i * 10 ** 6) + end + end + + describe '.millisecond' do + it 'returns milliseconds' do + expect(described_class.millisecond).to eq(fake_time.to_i * 10 ** 3) + end + end + + describe '.second' do + it 'returns seconds' do + expect(described_class.second).to eq(fake_time.to_i) + end + end + + describe '.minute' do + it 'returns minutes' do + expect(described_class.minute).to eq(fake_time.to_i / 60.0) + end + end + + describe '.hour' do + it 'returns hours' do + expect(described_class.hour).to eq(fake_time.to_i / 3600.0) + end + end + + describe '.day' do + it 'returns days' do + expect(described_class.day).to eq(fake_time.to_i / (3600.0 * 24.0)) + end + end + + describe '.week' do + it 'returns weeks' do + expect(described_class.week).to eq(fake_time.to_i / (3600.0 * 24.0 * 7.0)) + end + end +end diff --git a/sentry-ruby/spec/sentry/metrics_spec.rb b/sentry-ruby/spec/sentry/metrics_spec.rb index 04acc66b3..9989876a4 100644 --- a/sentry-ruby/spec/sentry/metrics_spec.rb +++ b/sentry-ruby/spec/sentry/metrics_spec.rb @@ -82,4 +82,29 @@ described_class.gauge('foo', 5.0, unit: 'second', tags: { fortytwo: 42 }, timestamp: fake_time) end end + + describe '.timing' do + it 'does nothing without a block' do + expect(aggregator).not_to receive(:add) + described_class.timing('foo') + end + + it 'does nothing with a non-duration unit' do + expect(aggregator).not_to receive(:add) + described_class.timing('foo', unit: 'ratio') { } + end + + it 'measures time taken as distribution and passes through args to aggregator' do + expect(aggregator).to receive(:add).with( + :d, + 'foo', + an_instance_of(Integer), + unit: 'millisecond', + tags: { fortytwo: 42 }, + timestamp: fake_time + ) + + described_class.timing('foo', unit: 'millisecond', tags: { fortytwo: 42 }, timestamp: fake_time) { sleep(0.1) } + end + end end