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 sentry-delayed_job integration #1273

Merged
merged 7 commits into from
Feb 9, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add Sentry::DelayedJob::Plugin
  • Loading branch information
st0012 committed Feb 9, 2021
commit e334de7a373a7e5640ccce6bb32a15d4293118b2
4 changes: 4 additions & 0 deletions sentry-delayed_job/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ gem "rspec", "~> 3.0"
gem "codecov", "0.2.12"

gem "delayed_job"
gem "delayed_job_active_record"
gem "rails"
gem "activerecord-jdbcmysql-adapter", platform: :jruby
gem "jdbc-sqlite3", platform: :jruby
gem "sqlite3", platform: :ruby

gem "sentry-ruby", path: "../sentry-ruby"
gem "sentry-rails", path: "../sentry-rails"
Expand Down
9 changes: 8 additions & 1 deletion sentry-delayed_job/example/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
require "active_record"
require "delayed_job"
require "delayed_job_active_record"
require "logger"
require "sentry-delayed_job"
# require "logger"

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
Expand All @@ -24,6 +25,12 @@
end
end

Sentry.init do |config|
config.breadcrumbs_logger = [:sentry_logger]
# replace it with your sentry dsn
config.dsn = 'https://[email protected]/5434472'
end

class MyJob < ActiveJob::Base
self.queue_adapter = :delayed_job

Expand Down
1 change: 1 addition & 0 deletions sentry-delayed_job/lib/sentry-delayed_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require "sentry-ruby"
require "sentry/integrable"
require "sentry/delayed_job/version"
require "sentry/delayed_job/plugin"

module Sentry
module DelayedJob
Expand Down
42 changes: 42 additions & 0 deletions sentry-delayed_job/lib/sentry/delayed_job/plugin.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true
require "delayed_job"

module Sentry
module DelayedJob
class Plugin < ::Delayed::Plugin
callbacks do |lifecycle|
lifecycle.around(:invoke_job) do |job, *args, &block|
Sentry.with_scope do |scope|
scope.set_extras(**generate_extra(job))
scope.set_tags("delayed_job.queue" => job.queue, "delayed_job.id" => job.id)
st0012 marked this conversation as resolved.
Show resolved Hide resolved

begin
block.call(job, *args)
rescue Exception => e
Sentry::DelayedJob.capture_exception(e, hint: { background: false })

raise
end
end
end
end

def self.generate_extra(job)
{
"delayed_job.id": job.id,
"delayed_job.priority": job.priority,
"delayed_job.attempts": job.attempts,
"delayed_job.run_at": job.run_at,
"delayed_job.locked_at": job.locked_at,
"delayed_job.locked_by": job.locked_by,
"delayed_job.queue": job.queue,
"delayed_job.created_at": job.created_at,
"delayed_job.last_error": job.last_error&.byteslice(0..1000),
"delayed_job.handler": job.handler&.byteslice(0..1000)
}
end
end
end
end

Delayed::Worker.plugins << Sentry::DelayedJob::Plugin
110 changes: 110 additions & 0 deletions sentry-delayed_job/spec/sentry/delayed_job_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,115 @@
let(:transport) do
Sentry.get_current_client.transport
end

class Post
def raise_error
1 / 0
end

def tagged_error(number: 1)
Sentry.set_tags(number: number)
raise
end

def tagged_report(number: 1)
Sentry.set_tags(number: number)
Sentry.capture_message("tagged report")
end

def report
Sentry.capture_message("report")
end
end

it "sets correct extra/tags context for each job" do
Post.new.delay.report
enqueued_job = Delayed::Backend::ActiveRecord::Job.last
enqueued_job.invoke_job

expect(transport.events.count).to eq(1)
event = transport.events.last.to_hash
expect(event[:message]).to eq("report")
expect(event[:extra][:"delayed_job.id"]).to eq(enqueued_job.id)
expect(event[:tags]).to eq({ "delayed_job.id" => enqueued_job.id, "delayed_job.queue" => nil })
end

it "doesn't leak scope data outside of the job" do
Post.new.delay.report
enqueued_job = Delayed::Backend::ActiveRecord::Job.last
enqueued_job.invoke_job

expect(transport.events.count).to eq(1)
expect(Sentry.get_current_scope.extra).to eq({})
expect(Sentry.get_current_scope.tags).to eq({})
end

it "doesn't share scope data between jobs" do
Post.new.delay.tagged_report
enqueued_job = Delayed::Backend::ActiveRecord::Job.last
enqueued_job.invoke_job

expect(transport.events.count).to eq(1)
event = transport.events.last.to_hash
expect(event[:message]).to eq("tagged report")
expect(event[:tags]).to eq({ "delayed_job.id" => enqueued_job.id, "delayed_job.queue" => nil, number: 1 })

Post.new.delay.report
enqueued_job = Delayed::Backend::ActiveRecord::Job.last
enqueued_job.invoke_job

expect(transport.events.count).to eq(2)
event = transport.events.last.to_hash
expect(event[:tags]).to eq({ "delayed_job.id" => enqueued_job.id, "delayed_job.queue" => nil })
end

context "when a job failed" do
let(:enqueued_job) do
Post.new.delay.raise_error
enqueued_job = Delayed::Backend::ActiveRecord::Job.last
end

it "reports exception" do
expect do
enqueued_job.invoke_job
end.to raise_error(ZeroDivisionError)

expect(transport.events.count).to eq(1)
event = transport.events.last.to_hash

expect(event[:sdk]).to eq({ name: "sentry.ruby.delayed_job", version: described_class::VERSION })
expect(event.dig(:exception, :values, 0, :type)).to eq("ZeroDivisionError")
expect(event[:tags]).to eq({ "delayed_job.id" => enqueued_job.id, "delayed_job.queue" => nil })
end

it "doesn't leak scope data" do
Post.new.delay.tagged_error
enqueued_job = Delayed::Backend::ActiveRecord::Job.last

expect do
enqueued_job.invoke_job
end.to raise_error(RuntimeError)

expect(transport.events.count).to eq(1)
event = transport.events.last.to_hash

expect(event[:tags]).to eq({ "delayed_job.id" => enqueued_job.id, "delayed_job.queue" => nil, number: 1 })
expect(Sentry.get_current_scope.extra).to eq({})
expect(Sentry.get_current_scope.tags).to eq({})

Post.new.delay.raise_error
enqueued_job = Delayed::Backend::ActiveRecord::Job.last

expect do
enqueued_job.invoke_job
end.to raise_error(ZeroDivisionError)

expect(transport.events.count).to eq(2)
event = transport.events.last.to_hash
expect(event[:tags]).to eq({ "delayed_job.id" => enqueued_job.id, "delayed_job.queue" => nil })
expect(Sentry.get_current_scope.extra).to eq({})
expect(Sentry.get_current_scope.tags).to eq({})
end
end
end

57 changes: 23 additions & 34 deletions sentry-delayed_job/spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
require "bundler/setup"
require "pry"

require "active_record"
require "delayed_job"
require "delayed_job_active_record"

require "sentry-ruby"

require 'simplecov'
Expand Down Expand Up @@ -41,46 +45,31 @@
end
end

def build_exception
1 / 0
rescue ZeroDivisionError => e
e
end

def build_exception_with_cause(cause = "exception a")
begin
raise cause
rescue
raise "exception b"
# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
# ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
create_table :delayed_jobs do |table|
table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue
table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually.
table.text :handler, null: false # YAML-encoded string of the object that will do work
table.text :last_error # reason for last failure (See Note below)
table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future.
table.datetime :locked_at # Set when a client is working on this object
table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead)
table.string :locked_by # Who is working on this object (if locked)
table.string :queue # The name of the queue this job is in
table.timestamps null: true
end
rescue RuntimeError => e
e
end

def build_exception_with_two_causes
begin
begin
raise "exception a"
rescue
raise "exception b"
end
rescue
raise "exception c"
end
rescue RuntimeError => e
def build_exception
1 / 0
rescue ZeroDivisionError => e
e
end

def build_exception_with_recursive_cause
backtrace = []

exception = double("Exception")
allow(exception).to receive(:cause).and_return(exception)
allow(exception).to receive(:message).and_return("example")
allow(exception).to receive(:backtrace).and_return(backtrace)
exception
end

def perform_basic_setup
Sentry.init do |config|
config.dsn = DUMMY_DSN
Expand Down