Skip to content

Commit 48a83af

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

3 files changed

Lines changed: 201 additions & 0 deletions

File tree

sentry-ruby/Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,5 @@ gem "webrick"
3232
gem "faraday"
3333
gem "excon"
3434
gem "webmock"
35+
gem "sequel"
36+
gem "sqlite3"

sentry-ruby/lib/sentry/sequel.rb

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

0 commit comments

Comments
 (0)