Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new Sentry.capture_check_in API for Cron monitoring #2117

Merged
merged 1 commit into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@

- Record client reports for profiles [#2107](https://github.com/getsentry/sentry-ruby/pull/2107)
- Adopt Rails 7.1's new BroadcastLogger [#2120](https://github.com/getsentry/sentry-ruby/pull/2120)
- Add `Sentry.capture_check_in` API for Cron Monitoring [#2117](https://github.com/getsentry/sentry-ruby/pull/2117)

You can now track progress of long running scheduled jobs.

```rb
check_in_id = Sentry.capture_check_in('job_name', :in_progress)
# do job stuff
Sentry.capture_check_in('job_name', :ok, check_in_id: check_in_id)
```

### Bug Fixes

Expand Down
19 changes: 19 additions & 0 deletions sentry-ruby/lib/sentry-ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
require "sentry/event"
require "sentry/error_event"
require "sentry/transaction_event"
require "sentry/check_in_event"
require "sentry/span"
require "sentry/transaction"
require "sentry/hub"
Expand Down Expand Up @@ -430,6 +431,24 @@
get_current_hub.capture_event(event)
end

# Captures a check-in and sends it to Sentry via the currently active hub.
#
# @param slug [String] identifier of this monitor
# @param status [Symbol] status of this check-in, one of {CheckInEvent::VALID_STATUSES}
#
# @param [Hash] options extra check-in options
# @option options [String] check_in_id for updating the status of an existing monitor
# @option options [Integer] duration seconds elapsed since this monitor started
# @option options [Cron::MonitorConfig] monitor_config configuration for this monitor
#
# @yieldparam scope [Scope]
#
# @return [String, nil] The {CheckInEvent#check_in_id} to use for later updates on the same slug
def capture_check_in(slug, status, **options, &block)
return unless initialized?
get_current_hub.capture_check_in(slug, status, **options, &block)

Check warning on line 449 in sentry-ruby/lib/sentry-ruby.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry-ruby.rb#L448-L449

Added lines #L448 - L449 were not covered by tests
end

# Takes or initializes a new Sentry::Transaction and makes a sampling decision for it.
#
# @return [Transaction, nil]
Expand Down
60 changes: 60 additions & 0 deletions sentry-ruby/lib/sentry/check_in_event.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal

require 'securerandom'
require 'sentry/cron/monitor_config'

module Sentry
class CheckInEvent < Event
TYPE = 'check_in'

# uuid to identify this check-in.
# @return [String]
attr_accessor :check_in_id

# Identifier of the monitor for this check-in.
# @return [String]
attr_accessor :monitor_slug

# Duration of this check since it has started in seconds.
# @return [Integer, nil]
attr_accessor :duration

# Monitor configuration to support upserts.
# @return [Cron::MonitorConfig, nil]
attr_accessor :monitor_config

# Status of this check-in.
# @return [Symbol]
attr_accessor :status

VALID_STATUSES = %i(ok in_progress error)

def initialize(
slug:,
status:,
duration: nil,
monitor_config: nil,
check_in_id: nil,
**options
)
super(**options)

Check warning on line 40 in sentry-ruby/lib/sentry/check_in_event.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/check_in_event.rb#L40

Added line #L40 was not covered by tests

self.monitor_slug = slug
self.status = status
self.duration = duration
self.monitor_config = monitor_config
self.check_in_id = check_in_id || SecureRandom.uuid.delete('-')

Check warning on line 46 in sentry-ruby/lib/sentry/check_in_event.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/check_in_event.rb#L42-L46

Added lines #L42 - L46 were not covered by tests
end

# @return [Hash]
def to_hash
data = super
data[:check_in_id] = check_in_id
data[:monitor_slug] = monitor_slug
data[:status] = status
data[:duration] = duration if duration
data[:monitor_config] = monitor_config.to_hash if monitor_config
data

Check warning on line 57 in sentry-ruby/lib/sentry/check_in_event.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/check_in_event.rb#L51-L57

Added lines #L51 - L57 were not covered by tests
end
end
end
31 changes: 31 additions & 0 deletions sentry-ruby/lib/sentry/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,37 @@
event
end

# Initializes a CheckInEvent object with the given options.
#
# @param slug [String] identifier of this monitor
# @param status [Symbol] status of this check-in, one of {CheckInEvent::VALID_STATUSES}
# @param hint [Hash] the hint data that'll be passed to `before_send` callback and the scope's event processors.
# @param duration [Integer, nil] seconds elapsed since this monitor started
# @param monitor_config [Cron::MonitorConfig, nil] configuration for this monitor
# @param check_in_id [String, nil] for updating the status of an existing monitor
#
# @return [Event]
def event_from_check_in(
slug,
status,
hint = {},
duration: nil,
monitor_config: nil,
check_in_id: nil
)
return unless configuration.sending_allowed?

Check warning on line 125 in sentry-ruby/lib/sentry/client.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/client.rb#L125

Added line #L125 was not covered by tests

CheckInEvent.new(

Check warning on line 127 in sentry-ruby/lib/sentry/client.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/client.rb#L127

Added line #L127 was not covered by tests
configuration: configuration,
integration_meta: Sentry.integrations[hint[:integration]],
slug: slug,
status: status,
duration: duration,
monitor_config: monitor_config,
check_in_id: check_in_id
)
end

# Initializes an Event object with the given Transaction object.
# @param transaction [Transaction] the transaction to be recorded.
# @return [TransactionEvent]
Expand Down
53 changes: 53 additions & 0 deletions sentry-ruby/lib/sentry/cron/monitor_config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal

require 'sentry/cron/monitor_schedule'

module Sentry
module Cron
class MonitorConfig
# The monitor schedule configuration
# @return [MonitorSchedule::Crontab, MonitorSchedule::Interval]
attr_accessor :schedule

# How long (in minutes) after the expected checkin time will we wait
# until we consider the checkin to have been missed.
# @return [Integer, nil]
attr_accessor :checkin_margin

# How long (in minutes) is the checkin allowed to run for in in_progress
# before it is considered failed.
# @return [Integer, nil]
attr_accessor :max_runtime

# tz database style timezone string
# @return [String, nil]
attr_accessor :timezone

def initialize(schedule, checkin_margin: nil, max_runtime: nil, timezone: nil)
@schedule = schedule
@checkin_margin = checkin_margin
@max_runtime = max_runtime
@timezone = timezone

Check warning on line 30 in sentry-ruby/lib/sentry/cron/monitor_config.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/cron/monitor_config.rb#L27-L30

Added lines #L27 - L30 were not covered by tests
end

def self.from_crontab(crontab, **options)
new(MonitorSchedule::Crontab.new(crontab), **options)

Check warning on line 34 in sentry-ruby/lib/sentry/cron/monitor_config.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/cron/monitor_config.rb#L34

Added line #L34 was not covered by tests
end

def self.from_interval(num, unit, **options)
return nil unless MonitorSchedule::Interval::VALID_UNITS.include?(unit)

Check warning on line 38 in sentry-ruby/lib/sentry/cron/monitor_config.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/cron/monitor_config.rb#L38

Added line #L38 was not covered by tests

new(MonitorSchedule::Interval.new(num, unit), **options)

Check warning on line 40 in sentry-ruby/lib/sentry/cron/monitor_config.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/cron/monitor_config.rb#L40

Added line #L40 was not covered by tests
end

def to_hash
{
schedule: schedule.to_hash,

Check warning on line 45 in sentry-ruby/lib/sentry/cron/monitor_config.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/cron/monitor_config.rb#L45

Added line #L45 was not covered by tests
checkin_margin: checkin_margin,
max_runtime: max_runtime,
timezone: timezone
}.compact
end
end
end
end
42 changes: 42 additions & 0 deletions sentry-ruby/lib/sentry/cron/monitor_schedule.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal

module Sentry
module Cron
module MonitorSchedule
class Crontab
# A crontab formatted string such as "0 * * * *".
# @return [String]
attr_accessor :value

def initialize(value)
@value = value

Check warning on line 12 in sentry-ruby/lib/sentry/cron/monitor_schedule.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/cron/monitor_schedule.rb#L12

Added line #L12 was not covered by tests
end

def to_hash
{ type: :crontab, value: value }

Check warning on line 16 in sentry-ruby/lib/sentry/cron/monitor_schedule.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/cron/monitor_schedule.rb#L16

Added line #L16 was not covered by tests
end
end

class Interval
# The number representing duration of the interval.
# @return [Integer]
attr_accessor :value

# The unit representing duration of the interval.
# @return [Symbol]
attr_accessor :unit

VALID_UNITS = %i(year month week day hour minute)

def initialize(value, unit)
@value = value
@unit = unit

Check warning on line 33 in sentry-ruby/lib/sentry/cron/monitor_schedule.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/cron/monitor_schedule.rb#L32-L33

Added lines #L32 - L33 were not covered by tests
end

def to_hash
{ type: :interval, value: value, unit: unit }

Check warning on line 37 in sentry-ruby/lib/sentry/cron/monitor_schedule.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/cron/monitor_schedule.rb#L37

Added line #L37 was not covered by tests
end
end
end
end
end
26 changes: 25 additions & 1 deletion sentry-ruby/lib/sentry/hub.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,30 @@
capture_event(event, **options, &block)
end

def capture_check_in(slug, status, **options, &block)
check_argument_type!(slug, ::String)
check_argument_includes!(status, Sentry::CheckInEvent::VALID_STATUSES)

Check warning on line 161 in sentry-ruby/lib/sentry/hub.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/hub.rb#L160-L161

Added lines #L160 - L161 were not covered by tests

return unless current_client

Check warning on line 163 in sentry-ruby/lib/sentry/hub.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/hub.rb#L163

Added line #L163 was not covered by tests

options[:hint] ||= {}
options[:hint][:slug] = slug

Check warning on line 166 in sentry-ruby/lib/sentry/hub.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/hub.rb#L165-L166

Added lines #L165 - L166 were not covered by tests

event = current_client.event_from_check_in(

Check warning on line 168 in sentry-ruby/lib/sentry/hub.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/hub.rb#L168

Added line #L168 was not covered by tests
slug,
status,
options[:hint],
duration: options.delete(:duration),
monitor_config: options.delete(:monitor_config),
check_in_id: options.delete(:check_in_id)
)

return unless event

Check warning on line 177 in sentry-ruby/lib/sentry/hub.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/hub.rb#L177

Added line #L177 was not covered by tests

capture_event(event, **options, &block)
event.check_in_id

Check warning on line 180 in sentry-ruby/lib/sentry/hub.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/hub.rb#L179-L180

Added lines #L179 - L180 were not covered by tests
end

def capture_event(event, **options, &block)
check_argument_type!(event, Sentry::Event)

Expand All @@ -178,7 +202,7 @@
configuration.log_debug(event.to_json_compatible)
end

@last_event_id = event&.event_id unless event.is_a?(Sentry::TransactionEvent)
@last_event_id = event&.event_id if event.is_a?(Sentry::ErrorEvent)
event
end

Expand Down
6 changes: 6 additions & 0 deletions sentry-ruby/lib/sentry/integrable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,11 @@
options[:hint][:integration] = integration_name
Sentry.capture_message(message, **options, &block)
end

def capture_check_in(slug, status, **options, &block)
options[:hint] ||= {}
options[:hint][:integration] = integration_name
Sentry.capture_check_in(slug, status, **options, &block)

Check warning on line 29 in sentry-ruby/lib/sentry/integrable.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/integrable.rb#L27-L29

Added lines #L27 - L29 were not covered by tests
end
end
end
6 changes: 6 additions & 0 deletions sentry-ruby/lib/sentry/utils/argument_checking_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,11 @@
raise ArgumentError, "expect the argument to be a #{expected_types.join(' or ')}, got #{argument.class} (#{argument.inspect})"
end
end

def check_argument_includes!(argument, values)
unless values.include?(argument)
raise ArgumentError, "expect the argument to be one of #{values.map(&:inspect).join(' or ')}, got #{argument.inspect}"

Check warning on line 15 in sentry-ruby/lib/sentry/utils/argument_checking_helper.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/utils/argument_checking_helper.rb#L14-L15

Added lines #L14 - L15 were not covered by tests
end
end
end
end
53 changes: 53 additions & 0 deletions sentry-ruby/spec/sentry/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,59 @@ module ExcTag; end
end
end

describe "#event_from_check_in" do
let(:slug) { "test_slug" }
let(:status) { :ok }

it 'returns an event' do
event = subject.event_from_check_in(slug, status)
expect(event).to be_a(Sentry::CheckInEvent)

hash = event.to_hash
expect(hash[:monitor_slug]).to eq(slug)
expect(hash[:status]).to eq(status)
expect(hash[:check_in_id].length).to eq(32)
end

it 'returns an event with correct optional attributes from crontab config' do
event = subject.event_from_check_in(
slug,
status,
duration: 30,
check_in_id: "xxx-yyy",
monitor_config: Sentry::Cron::MonitorConfig.from_crontab("* * * * *")
)

expect(event).to be_a(Sentry::CheckInEvent)

hash = event.to_hash
expect(hash[:monitor_slug]).to eq(slug)
expect(hash[:status]).to eq(status)
expect(hash[:check_in_id]).to eq("xxx-yyy")
expect(hash[:duration]).to eq(30)
expect(hash[:monitor_config]).to eq({ schedule: { type: :crontab, value: "* * * * *" } })
end

it 'returns an event with correct optional attributes from interval config' do
event = subject.event_from_check_in(
slug,
status,
duration: 30,
check_in_id: "xxx-yyy",
monitor_config: Sentry::Cron::MonitorConfig.from_interval(30, :minute)
)

expect(event).to be_a(Sentry::CheckInEvent)

hash = event.to_hash
expect(hash[:monitor_slug]).to eq(slug)
expect(hash[:status]).to eq(status)
expect(hash[:check_in_id]).to eq("xxx-yyy")
expect(hash[:duration]).to eq(30)
expect(hash[:monitor_config]).to eq({ schedule: { type: :interval, value: 30, unit: :minute } })
end
end

describe "#generate_sentry_trace" do
let(:string_io) { StringIO.new }
let(:logger) do
Expand Down
Loading
Loading