Skip to content

Commit

Permalink
Split footprints by database name
Browse files Browse the repository at this point in the history
  • Loading branch information
Brandon Joyce committed May 3, 2016
1 parent c5e6855 commit e458030
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 27 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
/spec/reports/
/tmp/
footprint.sql
footprint.:memory:.sql
27 changes: 12 additions & 15 deletions lib/sql_footprint.rb
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
require 'sql_footprint/version'
require 'sql_footprint/sql_anonymizer'
require 'sql_footprint/sql_capturer'
require 'sql_footprint/sql_filter'
require 'sql_footprint/sql_statements'
require 'active_support/notifications'

module SqlFootprint
FILENAME = 'footprint.sql'.freeze
NEWLINE = "\n".freeze

ActiveSupport::Notifications.subscribe('sql.active_record') do |_, _, _, _, payload|
capture payload.fetch(:sql)
if @capture
adapter = ObjectSpace._id2ref(payload.fetch(:connection_id))
database_name = adapter.instance_variable_get(:@config).fetch(:database)
capturers[database_name].capture payload.fetch(:sql)
end
end

class << self
attr_reader :statements
attr_reader :capturers

def start
@anonymizer = SqlAnonymizer.new
@filter = SqlFilter.new
@capture = true
@statements = SqlStatements.new
@capture = true
@capturers = Hash.new do |hash, database_name|
hash[database_name] = SqlCapturer.new(database_name)
end
end

def stop
@capture = false
File.write FILENAME, statements.sort.join(NEWLINE) + NEWLINE
capturers.values.each(&:write)
end

def exclude
Expand All @@ -33,10 +35,5 @@ def exclude
ensure
@capture = true
end

def capture sql
return unless @capture && @filter.capture?(sql)
@statements.add @anonymizer.anonymize(sql)
end
end
end
13 changes: 13 additions & 0 deletions lib/sql_footprint/footprint_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module SqlFootprint
class FootprintSerializer
NEWLINE = "\n".freeze

def initialize statements
@statements = statements
end

def to_s
@statements.sort.join(NEWLINE) + NEWLINE
end
end
end
33 changes: 33 additions & 0 deletions lib/sql_footprint/sql_capturer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require 'sql_footprint/footprint_serializer'

module SqlFootprint
class SqlCapturer
attr_reader :statements, :database_name

def initialize database_name
@anonymizer = SqlAnonymizer.new
@filter = SqlFilter.new
@statements = SqlStatements.new
@database_name = database_name
end

def capture sql
return unless @filter.capture?(sql)
@statements.add @anonymizer.anonymize(sql)
end

def write
File.write filename, serialized_statements
end

private

def serialized_statements
SqlFootprint::FootprintSerializer.new(statements).to_s
end

def filename
"footprint.#{database_name.split('/').last}.sql"
end
end
end
20 changes: 20 additions & 0 deletions spec/sql_footprint/footprint_serializer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require 'spec_helper'

RSpec.describe SqlFootprint::FootprintSerializer do
describe '#to_s' do
let(:sql_statements) { SqlFootprint::SqlStatements.new }
let(:statements) do
Array.new(3) { SecureRandom.hex }
end

before do
statements.each do |statement|
sql_statements.add statement
end
end

it 'sorts and joins the statements into a string' do
expect(described_class.new(sql_statements).to_s).to eq(statements.sort.join("\n") + "\n")
end
end
end
86 changes: 86 additions & 0 deletions spec/sql_footprint/sql_capturer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
require 'spec_helper'

describe SqlFootprint::SqlCapturer do
let(:db_name) { SecureRandom.uuid }
let(:serialized_statements) { SecureRandom.uuid }
describe '#initialize' do
it 'requires db_name' do
expect { described_class.new }.to raise_error ArgumentError
end

it 'sets the database_name' do
described_class.new(db_name).tap do |capturer|
expect(capturer.database_name).to eq db_name
end
end
end

before do
allow(SqlFootprint::SqlFilter).to receive(:new).and_return(sql_filter_double)
allow(SqlFootprint::SqlAnonymizer).to receive(:new).and_return(sql_anonymizer_double)
allow(SqlFootprint::SqlStatements).to receive(:new).and_return(sql_statements_double)
allow(SqlFootprint::FootprintSerializer).to receive(:new).with(sql_statements_double)
.and_return(footprint_serializer_double)
end

let(:sql_filter_double) do
double('SqlFilter').tap do |d|
allow(d).to receive(:capture?).with(sql).and_return(should_capture)
end
end
let(:sql_anonymizer_double) do
double('SqlAnonymizer').tap do |d|
allow(d).to receive(:anonymize).with(sql).and_return(anonymized_sql)
end
end
let(:sql_statements_double) do
double('SqlStatements')
end
let(:footprint_serializer_double) do
double('SqlStatementsSearializer').tap do |d|
allow(d).to receive(:to_s).and_return(serialized_statements)
end
end

let(:sql) { SecureRandom.uuid }
let(:anonymized_sql) { SecureRandom.uuid }
let(:should_capture) { true }

describe '#capture' do
subject { described_class.new(db_name) }

context 'when the SqlFilter returns false' do
let(:should_capture) { false }

it 'does not add the statement' do
expect(sql_statements_double).not_to receive(:add)
subject.capture(sql)
end
end

context 'when the SqlFilter returns true' do
let(:should_capture) { true }

it 'does add the statement' do
expect(sql_statements_double).to receive(:add).with(anonymized_sql)
subject.capture(sql)
end
end
end

describe '#write' do
subject { described_class.new(db_name) }

it 'writes the serialized statements to the contents of a file' do
expect(footprint_serializer_double).to receive(:to_s)
.and_return(serialized_statements)
expect(File).to receive(:write).with(anything, serialized_statements)
subject.write
end

it 'writes to db-specific filename' do
expect(File).to receive(:write).with("footprint.#{db_name}.sql", anything)
subject.write
end
end
end
27 changes: 15 additions & 12 deletions spec/sql_footprint_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
expect(SqlFootprint::VERSION).not_to be nil
end

let(:statements) { described_class.capturers[':memory:'].statements }
let(:footprint_file_name) { 'footprint.:memory:.sql' }

describe '.start' do
before do
described_class.start
Expand All @@ -15,19 +18,19 @@
end

it 'logs sql' do
expect { Widget.create! }.to change { described_class.statements.count }.by(+1)
expect { Widget.create! }.to change { statements.count }.by(+1)
end

it 'formats inserts' do
Widget.create!
expect(described_class.statements.to_a).to include(
expect(statements.to_a).to include(
'INSERT INTO "widgets" ("created_at", "updated_at") VALUES (?, ?)'
)
end

it 'formats selects' do
Widget.where(name: SecureRandom.uuid, quantity: 1).last
expect(described_class.statements.to_a).to include(
expect(statements.to_a).to include(
'SELECT "widgets".* FROM "widgets" ' \
'WHERE "widgets"."name" = ? AND ' \
'"widgets"."quantity" = ? ' \
Expand All @@ -39,12 +42,12 @@
expect do
Widget.create!
Widget.create!
end.to change { described_class.statements.count }.by(+1)
end.to change { statements.count }.by(+1)
end

it 'works with joins' do
Widget.joins(:cogs).where(name: SecureRandom.uuid).load
expect(described_class.statements.to_a).to include(
expect(statements.to_a).to include(
'SELECT "widgets".* FROM "widgets" ' \
'INNER JOIN "cogs" ON "cogs"."widget_id" = "widgets"."id" ' \
'WHERE "widgets"."name" = ?'
Expand All @@ -56,8 +59,8 @@
Widget.where(name: SecureRandom.hex).last
widget = described_class.exclude { Widget.create! }
expect(widget).to be_a(Widget)
end.to change { described_class.statements.count }.by(+1)
expect(described_class.statements.to_a.join).not_to include 'INSERT INTO \"widgets\"'
end.to change { statements.count }.by(+1)
expect(statements.to_a.join).not_to include 'INSERT INTO \"widgets\"'
end

it 'does not write SHOW queries' do
Expand All @@ -66,7 +69,7 @@
rescue
"We don't care about the validity of the SQL" # rubocop
end
expect(described_class.statements.to_a.join).not_to include 'SHOW'
expect(statements.to_a.join).not_to include 'SHOW'
end
end

Expand All @@ -75,7 +78,7 @@
described_class.start
Widget.create!
described_class.stop
log = File.read('footprint.sql')
log = File.read(footprint_file_name)
expect(log).to include('INSERT INTO')
end

Expand All @@ -84,8 +87,8 @@
Widget.create!
Widget.first
described_class.stop
log = File.read('footprint.sql')
expect(described_class.statements.sort).to eq(log.split("\n").sort)
log = File.read(footprint_file_name)
expect(statements.sort).to eq(log.split("\n").sort)
end

it 'removes old results' do
Expand All @@ -95,7 +98,7 @@
described_class.start
Widget.last
described_class.stop
log = File.read('footprint.sql')
log = File.read(footprint_file_name)
expect(log).not_to include('INSERT INTO')
end
end
Expand Down

0 comments on commit e458030

Please sign in to comment.