Skip to content
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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
## Unreleased

### Features

- Support for tracing `Sequel` queries ([#2814](https://github.com/getsentry/sentry-ruby/pull/2814))

```ruby
require "sentry"
require "sentry/sequel"

Sentry.init do |config|
config.enabled_patches << :sequel
end

DB = Sequel.sqlite
DB.extension(:sentry)
```

### Bug Fixes

- Handle empty frames case gracefully with local vars ([#2807](https://github.com/getsentry/sentry-ruby/pull/2807))
Expand Down
2 changes: 2 additions & 0 deletions sentry-rails/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,5 @@ gem "benchmark-ips"
gem "benchmark_driver"
gem "benchmark-ipsa"
gem "benchmark-memory"

gem "sequel"
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

require "sequel"
require "sentry/sequel"

class SequelUsersController < ActionController::Base
def index
users = SEQUEL_DB[:users].all
render json: users
end

def create
id = SEQUEL_DB[:users].insert(name: params[:name], email: params[:email])
render json: { id: id, name: params[:name], email: params[:email] }, status: :created
end

def show
user = SEQUEL_DB[:users].where(id: params[:id]).first
if user
render json: user
else
render json: { error: "Not found" }, status: :not_found
end
end

def update
SEQUEL_DB[:users].where(id: params[:id]).update(name: params[:name])
user = SEQUEL_DB[:users].where(id: params[:id]).first
render json: user
end

def destroy
SEQUEL_DB[:users].where(id: params[:id]).delete
head :no_content
end

def exception
SEQUEL_DB[:users].all
raise "Something went wrong!"
end
end
10 changes: 10 additions & 0 deletions sentry-rails/spec/dummy/test_rails_app/config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ def configure
end
end

# Sequel-specific routes for testing Sequel tracing
scope "/sequel" do
get "/users", to: "sequel_users#index"
post "/users", to: "sequel_users#create"
get "/users/:id", to: "sequel_users#show"
put "/users/:id", to: "sequel_users#update"
delete "/users/:id", to: "sequel_users#destroy"
get "/exception", to: "sequel_users#exception"
end

get "500", to: "hello#reporting"

root to: "hello#world"
Expand Down
200 changes: 200 additions & 0 deletions sentry-rails/spec/isolated/sequel_tracing_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# frozen_string_literal: true

begin
require "simplecov"
SimpleCov.command_name "SequelTracing"
rescue LoadError
end

require "sequel"
require "sentry/sequel"

require_relative "../dummy/test_rails_app/app/controllers/sequel_users_controller"

RSpec.describe "Sequel Tracing with Rails", type: :request do
before(:all) do
if RUBY_ENGINE == "jruby"
SEQUEL_DB = Sequel.connect("jdbc:sqlite::memory:")
else
SEQUEL_DB = Sequel.sqlite
end

SEQUEL_DB.create_table :users do
primary_key :id
String :name
String :email
end

SEQUEL_DB[:users].count
SEQUEL_DB.extension(:sentry)
end

after(:all) do
SEQUEL_DB.drop_table?(:users)
Object.send(:remove_const, :SEQUEL_DB)
end

before do
make_basic_app do |config, app|
config.traces_sample_rate = 1.0
config.enabled_patches << :sequel
end
end

let(:transport) { Sentry.get_current_client.transport }

describe "SELECT queries" do
it "creates a transaction with Sequel span for index action" do
get "/sequel/users"

expect(response).to have_http_status(:ok)
expect(transport.events.count).to eq(1)

transaction = transport.events.last.to_h

expect(transaction[:type]).to eq("transaction")
expect(transaction.dig(:contexts, :trace, :op)).to eq("http.server")

sequel_span = transaction[:spans].find { |span| span[:op] == "db.sql.sequel" }

expect(sequel_span).not_to be_nil
expect(sequel_span[:description]).to include("SELECT")
expect(sequel_span[:description]).to include("users")
expect(sequel_span[:origin]).to eq("auto.db.sequel")
expect(sequel_span[:data]["db.system"]).to eq("sqlite")
end
end

describe "INSERT queries" do
it "creates a transaction with Sequel span for create action" do
post "/sequel/users", params: { name: "John Doe", email: "[email protected]" }

expect(response).to have_http_status(:created)

transaction = transport.events.last.to_h
expect(transaction[:type]).to eq("transaction")

insert_span = transaction[:spans].find do |span|
span[:op] == "db.sql.sequel" && span[:description]&.include?("INSERT")
end

expect(insert_span).not_to be_nil
expect(insert_span[:description]).to include("INSERT")
expect(insert_span[:description]).to include("users")
expect(insert_span[:origin]).to eq("auto.db.sequel")
end
end

describe "UPDATE queries" do
it "creates a transaction with Sequel span for update action" do
SEQUEL_DB[:users].insert(name: "Jane Doe", email: "[email protected]")

put "/sequel/users/1", params: { name: "Jane Smith" }

expect(response).to have_http_status(:ok)

transaction = transport.events.last.to_h

update_span = transaction[:spans].find do |span|
span[:op] == "db.sql.sequel" && span[:description]&.include?("UPDATE")
end

expect(update_span).not_to be_nil
expect(update_span[:description]).to include("UPDATE")
expect(update_span[:description]).to include("users")
end
end

describe "DELETE queries" do
it "creates a transaction with Sequel span for delete action" do
SEQUEL_DB[:users].insert(name: "Delete Me", email: "[email protected]")

delete "/sequel/users/1"

expect(response).to have_http_status(:no_content)

transaction = transport.events.last.to_h

delete_span = transaction[:spans].find do |span|
span[:op] == "db.sql.sequel" && span[:description]&.include?("DELETE")
end

expect(delete_span).not_to be_nil
expect(delete_span[:description]).to include("DELETE")
expect(delete_span[:description]).to include("users")
end
end

describe "exception handling" do
it "creates both error event and transaction with Sequel span" do
get "/sequel/exception"

expect(response).to have_http_status(:internal_server_error)

expect(transport.events.count).to eq(2)

error_event = transport.events.first.to_h
transaction = transport.events.last.to_h

expect(error_event[:exception][:values].first[:type]).to eq("RuntimeError")
expect(error_event[:exception][:values].first[:value]).to include("Something went wrong!")

sequel_span = transaction[:spans].find { |span| span[:op] == "db.sql.sequel" }
expect(sequel_span).not_to be_nil
expect(sequel_span[:description]).to include("SELECT")

expect(error_event.dig(:contexts, :trace, :trace_id)).to eq(
transaction.dig(:contexts, :trace, :trace_id)
)
end
end

describe "span timing" do
it "records proper start and end timestamps" do
get "/sequel/users"

transaction = transport.events.last.to_h
sequel_span = transaction[:spans].find { |span| span[:op] == "db.sql.sequel" }

expect(sequel_span[:start_timestamp]).not_to be_nil
expect(sequel_span[:timestamp]).not_to be_nil
expect(sequel_span[:start_timestamp]).to be < sequel_span[:timestamp]
end
end

describe "Sequel and ActiveRecord coexistence" do
it "records spans for both database systems in the same application" do
Post.create!(title: "Test Post")

SEQUEL_DB[:users].insert(name: "Sequel User", email: "[email protected]")

transport.events.clear

get "/sequel/users"

expect(response).to have_http_status(:ok)

transaction = transport.events.last.to_h
sequel_spans = transaction[:spans].select { |span| span[:op] == "db.sql.sequel" }

expect(sequel_spans.length).to be >= 1
expect(sequel_spans.first[:data]["db.system"]).to eq("sqlite")
end

it "records ActiveRecord spans separately from Sequel spans" do
transport.events.clear

get "/posts"

expect(response).to have_http_status(:internal_server_error) # raises "foo" in PostsController#index

transaction = transport.events.last.to_h

ar_spans = transaction[:spans].select { |span| span[:op] == "db.sql.active_record" }
sequel_spans = transaction[:spans].select { |span| span[:op] == "db.sql.sequel" }

expect(ar_spans.length).to be >= 1
expect(sequel_spans.length).to eq(0)
end
end
end
24 changes: 24 additions & 0 deletions sentry-ruby/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ eval_gemfile "../Gemfile.dev"

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

ruby_version = Gem::Version.new(RUBY_VERSION)

rack_version = ENV["RACK_VERSION"]
rack_version = "3.0.0" if rack_version.nil?

gem "rack", "~> #{Gem::Version.new(rack_version)}" unless rack_version == "0"

redis_rb_version = ENV.fetch("REDIS_RB_VERSION", "5.0")
Expand All @@ -32,3 +35,24 @@ gem "webrick"
gem "faraday"
gem "excon"
gem "webmock"

group :sequel do
gem "sequel"

sqlite_version = if ruby_version >= Gem::Version.new("3.2")
"2.1.0"
elsif ruby_version >= Gem::Version.new("3.0")
"1.4.0"
else
"1.3.0"
end

platform :ruby do
gem "sqlite3", "~> #{sqlite_version}"
end

platform :jruby do
gem "activerecord-jdbcmysql-adapter"
gem "jdbc-sqlite3"
end
end
35 changes: 35 additions & 0 deletions sentry-ruby/lib/sentry/sequel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

module Sentry
module Sequel
OP_NAME = "db.sql.sequel"
SPAN_ORIGIN = "auto.db.sequel"

# Sequel Database extension module that instruments queries
module DatabaseExtension
def log_connection_yield(sql, conn, args = nil)
return super unless Sentry.initialized?

Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) do |span|
result = super

if span
span.set_description(sql)
span.set_data(Span::DataConventions::DB_SYSTEM, database_type.to_s)
span.set_data(Span::DataConventions::DB_NAME, opts[:database]) if opts[:database]
span.set_data(Span::DataConventions::SERVER_ADDRESS, opts[:host]) if opts[:host]
span.set_data(Span::DataConventions::SERVER_PORT, opts[:port]) if opts[:port]
end

result
end
end
end
end

::Sequel::Database.register_extension(:sentry, Sentry::Sequel::DatabaseExtension)
end

Sentry.register_patch(:sequel) do
::Sequel::Database.extension(:sentry) if defined?(::Sequel::Database)
end
Loading
Loading