Skip to content

Commit cad74d8

Browse files
committed
feat(sequel): add Sequel database extension for query instrumentation
1 parent 7d10c02 commit cad74d8

File tree

3 files changed

+225
-0
lines changed

3 files changed

+225
-0
lines changed

sentry-ruby/Gemfile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ eval_gemfile "../Gemfile.dev"
77

88
gem "sentry-ruby", path: "./"
99

10+
ruby_version = Gem::Version.new(RUBY_VERSION)
11+
1012
rack_version = ENV["RACK_VERSION"]
1113
rack_version = "3.0.0" if rack_version.nil?
14+
1215
gem "rack", "~> #{Gem::Version.new(rack_version)}" unless rack_version == "0"
1316

1417
redis_rb_version = ENV.fetch("REDIS_RB_VERSION", "5.0")
@@ -32,3 +35,24 @@ gem "webrick"
3235
gem "faraday"
3336
gem "excon"
3437
gem "webmock"
38+
39+
group :sequel do
40+
gem "sequel"
41+
42+
sqlite_version = if ruby_version >= Gem::Version.new("3.2")
43+
"2.1.0"
44+
elsif ruby_version >= Gem::Version.new("3.0")
45+
"1.4.0"
46+
else
47+
"1.3.0"
48+
end
49+
50+
platform :ruby do
51+
gem "sqlite3", "~> #{sqlite_version}"
52+
end
53+
54+
platform :jruby do
55+
gem "activerecord-jdbcmysql-adapter"
56+
gem "jdbc-sqlite3"
57+
end
58+
end

sentry-ruby/lib/sentry/sequel.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
module Sentry
4+
module Sequel
5+
OP_NAME = "db.sql.sequel"
6+
SPAN_ORIGIN = "auto.db.sequel"
7+
8+
# Sequel Database extension module that instruments queries
9+
module DatabaseExtension
10+
def log_connection_yield(sql, conn, args = nil)
11+
return super unless Sentry.initialized?
12+
13+
Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) do |span|
14+
result = super
15+
16+
if span
17+
span.set_description(sql)
18+
span.set_data(Span::DataConventions::DB_SYSTEM, database_type.to_s)
19+
span.set_data(Span::DataConventions::DB_NAME, opts[:database]) if opts[:database]
20+
span.set_data(Span::DataConventions::SERVER_ADDRESS, opts[:host]) if opts[:host]
21+
span.set_data(Span::DataConventions::SERVER_PORT, opts[:port]) if opts[:port]
22+
end
23+
24+
result
25+
end
26+
end
27+
end
28+
end
29+
30+
::Sequel::Database.register_extension(:sentry, Sentry::Sequel::DatabaseExtension)
31+
end
32+
33+
Sentry.register_patch(:sequel) do
34+
::Sequel::Database.extension(:sentry) if defined?(::Sequel::Database)
35+
end
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
require "sequel"
5+
6+
# Load the sequel patch
7+
require "sentry/sequel"
8+
9+
RSpec.describe Sentry::Sequel do
10+
let(:db) do
11+
if RUBY_ENGINE == "jruby"
12+
Sequel.connect("jdbc:sqlite::memory:")
13+
else
14+
Sequel.sqlite
15+
end
16+
end
17+
18+
before do
19+
# Create a simple test table
20+
db.create_table :posts do
21+
primary_key :id
22+
String :title
23+
end
24+
25+
# Trigger Sequel's internal initialization (e.g., SELECT sqlite_version())
26+
db[:posts].count
27+
end
28+
29+
after do
30+
db.drop_table?(:posts)
31+
end
32+
33+
context "with tracing enabled" do
34+
before do
35+
perform_basic_setup do |config|
36+
config.traces_sample_rate = 1.0
37+
config.enabled_patches << :sequel
38+
end
39+
40+
# Apply patch to this specific database instance
41+
db.extension(:sentry)
42+
end
43+
44+
it "records a span for SELECT queries" do
45+
transaction = Sentry.start_transaction
46+
Sentry.get_current_scope.set_span(transaction)
47+
48+
db[:posts].all
49+
50+
spans = transaction.span_recorder.spans
51+
db_span = spans.find { |span| span.op == "db.sql.sequel" }
52+
53+
expect(db_span).not_to be_nil
54+
expect(db_span.description).to include("SELECT")
55+
expect(db_span.description).to include("posts")
56+
expect(db_span.origin).to eq("auto.db.sequel")
57+
end
58+
59+
it "records a span for INSERT queries" do
60+
transaction = Sentry.start_transaction
61+
Sentry.get_current_scope.set_span(transaction)
62+
63+
db[:posts].insert(title: "Hello World")
64+
65+
spans = transaction.span_recorder.spans
66+
db_span = spans.find { |span| span.op == "db.sql.sequel" && span.description&.include?("INSERT") }
67+
68+
expect(db_span).not_to be_nil
69+
expect(db_span.description).to include("INSERT")
70+
expect(db_span.description).to include("posts")
71+
end
72+
73+
it "records a span for UPDATE queries" do
74+
db[:posts].insert(title: "Hello World")
75+
76+
transaction = Sentry.start_transaction
77+
Sentry.get_current_scope.set_span(transaction)
78+
79+
db[:posts].where(title: "Hello World").update(title: "Updated")
80+
81+
spans = transaction.span_recorder.spans
82+
db_span = spans.find { |span| span.op == "db.sql.sequel" && span.description&.include?("UPDATE") }
83+
84+
expect(db_span).not_to be_nil
85+
expect(db_span.description).to include("UPDATE")
86+
expect(db_span.description).to include("posts")
87+
end
88+
89+
it "records a span for DELETE queries" do
90+
db[:posts].insert(title: "Hello World")
91+
92+
transaction = Sentry.start_transaction
93+
Sentry.get_current_scope.set_span(transaction)
94+
95+
db[:posts].where(title: "Hello World").delete
96+
97+
spans = transaction.span_recorder.spans
98+
db_span = spans.find { |span| span.op == "db.sql.sequel" && span.description&.include?("DELETE") }
99+
100+
expect(db_span).not_to be_nil
101+
expect(db_span.description).to include("DELETE")
102+
expect(db_span.description).to include("posts")
103+
end
104+
105+
it "sets span data with database information" do
106+
transaction = Sentry.start_transaction
107+
Sentry.get_current_scope.set_span(transaction)
108+
109+
db[:posts].all
110+
111+
spans = transaction.span_recorder.spans
112+
db_span = spans.find { |span| span.op == "db.sql.sequel" }
113+
114+
expect(db_span.data["db.system"]).to eq("sqlite")
115+
end
116+
117+
it "sets correct timestamps on span" do
118+
transaction = Sentry.start_transaction
119+
Sentry.get_current_scope.set_span(transaction)
120+
121+
db[:posts].all
122+
123+
spans = transaction.span_recorder.spans
124+
db_span = spans.find { |span| span.op == "db.sql.sequel" }
125+
126+
expect(db_span.start_timestamp).not_to be_nil
127+
expect(db_span.timestamp).not_to be_nil
128+
expect(db_span.start_timestamp).to be < db_span.timestamp
129+
end
130+
end
131+
132+
context "without active transaction" do
133+
before do
134+
perform_basic_setup do |config|
135+
config.traces_sample_rate = 1.0
136+
config.enabled_patches << :sequel
137+
end
138+
139+
db.extension(:sentry)
140+
end
141+
142+
it "does not create spans when no transaction is active" do
143+
# No transaction started
144+
result = db[:posts].all
145+
146+
# Query should still work
147+
expect(result).to eq([])
148+
end
149+
end
150+
151+
context "when Sentry is not initialized" do
152+
before do
153+
# Don't initialize Sentry
154+
db.extension(:sentry)
155+
end
156+
157+
it "does not interfere with normal database operations" do
158+
result = db[:posts].insert(title: "Test")
159+
expect(result).to eq(1)
160+
161+
posts = db[:posts].all
162+
expect(posts.length).to eq(1)
163+
expect(posts.first[:title]).to eq("Test")
164+
end
165+
end
166+
end

0 commit comments

Comments
 (0)