sql_safety_net 1.1.11 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +32 -11
- data/Rakefile +3 -5
- data/lib/sql_safety_net/cache_store.rb +17 -7
- data/lib/sql_safety_net/configuration.rb +35 -46
- data/lib/sql_safety_net/connection_adapter.rb +49 -80
- data/lib/sql_safety_net/explain_plan/mysql.rb +33 -0
- data/lib/sql_safety_net/explain_plan/postgresql.rb +30 -0
- data/lib/sql_safety_net/explain_plan.rb +24 -0
- data/lib/sql_safety_net/formatter.rb +178 -0
- data/lib/sql_safety_net/middleware.rb +41 -0
- data/lib/sql_safety_net/query_analysis.rb +52 -49
- data/lib/sql_safety_net/query_info.rb +33 -0
- data/lib/sql_safety_net.rb +68 -17
- data/spec/cache_store_spec.rb +8 -17
- data/spec/configuration_spec.rb +63 -58
- data/spec/connection_adapter_spec.rb +109 -114
- data/spec/explain_plan/mysql_spec.rb +113 -0
- data/spec/explain_plan/postgresql_spec.rb +44 -0
- data/spec/formatter_spec.rb +46 -0
- data/spec/middleware_spec.rb +90 -0
- data/spec/query_analysis_spec.rb +92 -50
- data/spec/query_info_spec.rb +46 -0
- data/spec/spec_helper.rb +7 -26
- data/spec/sql_safety_net_spec.rb +21 -0
- metadata +112 -89
- data/lib/sql_safety_net/connection_adapter/mysql_adapter.rb +0 -43
- data/lib/sql_safety_net/connection_adapter/postgresql_adapter.rb +0 -48
- data/lib/sql_safety_net/rack_handler.rb +0 -196
- data/spec/example_database.yml +0 -14
- data/spec/mysql_connection_adapter_spec.rb +0 -32
- data/spec/postgres_connection_adapter_spec.rb +0 -43
- data/spec/rack_handler_spec.rb +0 -193
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
module SqlSafetyNet
|
4
|
+
# Rack middleware for analyzing queries on a request.
|
5
|
+
#
|
6
|
+
# The X-SqlSafetyNet header will be set with summary info about the queries.
|
7
|
+
#
|
8
|
+
# If the request responds with HTML and the request queries are flagged or if the +always_show+
|
9
|
+
# option is set, debugging info will be injected into the page.
|
10
|
+
class Middleware
|
11
|
+
HTML_CONTENT_TYPE = /text\/(x?)html/i.freeze
|
12
|
+
|
13
|
+
def initialize(app)
|
14
|
+
@app = app
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
QueryAnalysis.capture do |analysis|
|
19
|
+
response = @app.call(env)
|
20
|
+
unless analysis.queries.empty?
|
21
|
+
formatter = Formatter.new(analysis)
|
22
|
+
Rails.logger.debug(formatter.to_s) if ActiveRecord::Base.logger
|
23
|
+
request = Rack::Request.new(env)
|
24
|
+
wrapped_response = Rack::Response.new(response[2], response[0], response[1])
|
25
|
+
wrapped_response["X-SqlSafetyNet"] = formatter.summary
|
26
|
+
|
27
|
+
if SqlSafetyNet.config.always_show || analysis.flagged?
|
28
|
+
unless request.xhr? || analysis.queries.empty?
|
29
|
+
content_type = wrapped_response.content_type
|
30
|
+
if content_type && content_type.match(HTML_CONTENT_TYPE) && !wrapped_response.redirection?
|
31
|
+
wrapped_response.write(formatter.to_html)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
response = wrapped_response.finish
|
36
|
+
end
|
37
|
+
response
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,69 +1,72 @@
|
|
1
1
|
module SqlSafetyNet
|
2
|
-
# Analysis container for the sql queries in a context.
|
3
2
|
class QueryAnalysis
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
attr_reader :elapsed_time, :rows, :result_size, :queries
|
4
|
+
|
5
|
+
class << self
|
6
|
+
# Get the current analysis object in scope.
|
7
|
+
def current
|
8
|
+
Thread.current[:sql_safety_net_request_queries]
|
9
|
+
end
|
10
|
+
|
11
|
+
# Capture queries in a block for analysis. Within the block the +current+ method
|
12
|
+
# can be called to the the current analysis object.
|
13
|
+
def capture
|
14
|
+
save_val = Thread.current[:sql_safety_net_request_queries]
|
15
|
+
begin
|
16
|
+
queries = new
|
17
|
+
Thread.current[:sql_safety_net_request_queries] = queries
|
18
|
+
yield queries
|
19
|
+
ensure
|
20
|
+
Thread.current[:sql_safety_net_request_queries] = save_val
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
8
25
|
def initialize
|
9
|
-
@
|
10
|
-
@rows = 0
|
26
|
+
@queries = []
|
11
27
|
@elapsed_time = 0.0
|
12
|
-
@
|
13
|
-
@
|
14
|
-
@caching = false
|
28
|
+
@rows = 0
|
29
|
+
@result_size = 0
|
15
30
|
end
|
16
|
-
|
17
|
-
#
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
ensure
|
24
|
-
clear
|
25
|
-
end
|
31
|
+
|
32
|
+
# Add a QueryInfo object to the analysis.
|
33
|
+
def <<(query_info)
|
34
|
+
@queries << query_info
|
35
|
+
@elapsed_time += query_info.elapsed_time
|
36
|
+
@rows += query_info.rows
|
37
|
+
@result_size += query_info.result_size
|
26
38
|
end
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
Thread.current[:sql_safety_net]
|
39
|
+
|
40
|
+
def total_queries
|
41
|
+
queries.size
|
31
42
|
end
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
Thread.current[:sql_safety_net] = nil
|
43
|
+
|
44
|
+
def alerted_queries
|
45
|
+
queries.select{|query| query.alerts?}.size
|
36
46
|
end
|
37
47
|
|
38
|
-
def
|
39
|
-
|
48
|
+
def alerts?
|
49
|
+
queries.any?{|query| query.alerts?}
|
40
50
|
end
|
41
51
|
|
42
|
-
def
|
43
|
-
|
52
|
+
def too_many_rows?
|
53
|
+
rows > SqlSafetyNet.config.returned_rows_limit
|
44
54
|
end
|
45
55
|
|
46
|
-
def
|
47
|
-
|
56
|
+
def too_many_queries?
|
57
|
+
total_queries > SqlSafetyNet.config.query_limit
|
48
58
|
end
|
49
|
-
|
50
|
-
def
|
51
|
-
|
59
|
+
|
60
|
+
def results_too_big?
|
61
|
+
result_size > SqlSafetyNet.config.result_size_limit
|
52
62
|
end
|
53
|
-
|
54
|
-
def
|
55
|
-
|
63
|
+
|
64
|
+
def too_much_time?
|
65
|
+
elapsed_time > SqlSafetyNet.config.elapsed_time_limit
|
56
66
|
end
|
57
67
|
|
58
|
-
def
|
59
|
-
|
60
|
-
if flagged.blank?
|
61
|
-
non_flagged_queries << info
|
62
|
-
else
|
63
|
-
info[:query_plan] = flagged[:query_plan]
|
64
|
-
info[:flags] = flagged[:flags]
|
65
|
-
flagged_queries << info
|
66
|
-
end
|
68
|
+
def flagged?
|
69
|
+
alerts? || too_many_rows? || too_many_queries? || results_too_big? || too_much_time?
|
67
70
|
end
|
68
71
|
end
|
69
72
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module SqlSafetyNet
|
2
|
+
# Class to store information about queries.
|
3
|
+
class QueryInfo
|
4
|
+
attr_reader :sql, :elapsed_time, :rows, :result_size, :alerts
|
5
|
+
|
6
|
+
def initialize(sql, options = {})
|
7
|
+
@sql = sql
|
8
|
+
@elapsed_time = options[:elapsed_time] || 0.0
|
9
|
+
@rows = options[:rows] || 0
|
10
|
+
@result_size = options[:result_size] || 0
|
11
|
+
@alerts = options[:alerts] || []
|
12
|
+
@cached = !!options[:cached]
|
13
|
+
analyze!
|
14
|
+
end
|
15
|
+
|
16
|
+
def cached?
|
17
|
+
@cached
|
18
|
+
end
|
19
|
+
|
20
|
+
def alerts?
|
21
|
+
!alerts.empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def analyze!
|
27
|
+
config = SqlSafetyNet.config
|
28
|
+
alerts << "query took #{elapsed_time * 1000} ms" if elapsed_time > config.elapsed_time_limit
|
29
|
+
alerts << "query returned #{rows}" if rows > config.returned_rows_limit
|
30
|
+
alerts << "query returned ~#{result_size} bytes" if result_size > config.result_size_limit
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/sql_safety_net.rb
CHANGED
@@ -1,27 +1,78 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
#
|
1
|
+
# Root module for the gem.
|
2
|
+
#
|
3
|
+
# This module provide access to the singleton configuration object as well as hooks
|
4
|
+
# for enabling features.
|
5
|
+
#
|
6
|
+
# Since the analysis code is intended only for development mode
|
7
|
+
# it is not enabled by default. You can enable it by calling the enable methods
|
8
|
+
# in an config/initializers file:
|
9
|
+
#
|
10
|
+
# if Rails.env.development?
|
11
|
+
# SqlSafetyNet.enable_on_cache_store!(ActiveSupport::Cache::Store)
|
12
|
+
# SqlSafetyNet.enable_on_connection_adapter!(ActiveRecord::Base.connection.class)
|
13
|
+
# SqlSafetyNet::ExplainPlan.enable_on_connection_adapter!(ActiveRecord::Base.connection.class, :mysql) # assuming MySQL adapter
|
14
|
+
# Rails.configuration.middleware.use(SqlSafetyNet::Middleware)
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# Or you can simply enable the default configuration by calling:
|
18
|
+
#
|
19
|
+
# SqlSafetyNet.enable! if Rails.env.development?
|
5
20
|
module SqlSafetyNet
|
6
|
-
autoload :CacheStore, File.expand_path(
|
7
|
-
autoload :Configuration, File.expand_path(
|
8
|
-
autoload :ConnectionAdapter, File.expand_path(
|
9
|
-
autoload :
|
10
|
-
autoload :
|
21
|
+
autoload :CacheStore, File.expand_path("../sql_safety_net/cache_store.rb", __FILE__)
|
22
|
+
autoload :Configuration, File.expand_path("../sql_safety_net/configuration.rb", __FILE__)
|
23
|
+
autoload :ConnectionAdapter, File.expand_path("../sql_safety_net/connection_adapter.rb", __FILE__)
|
24
|
+
autoload :ExplainPlan, File.expand_path("../sql_safety_net/explain_plan.rb", __FILE__)
|
25
|
+
autoload :Formatter, File.expand_path("../sql_safety_net/formatter.rb", __FILE__)
|
26
|
+
autoload :Middleware, File.expand_path("../sql_safety_net/middleware.rb", __FILE__)
|
27
|
+
autoload :QueryAnalysis, File.expand_path("../sql_safety_net/query_analysis.rb", __FILE__)
|
28
|
+
autoload :QueryInfo, File.expand_path("../sql_safety_net/query_info.rb", __FILE__)
|
11
29
|
|
12
30
|
class << self
|
13
|
-
#
|
31
|
+
# Enable SQL analysis on your Rails app. This method can be called from your development.rb
|
32
|
+
# file. It will enable analysis on the default database connection class and insert
|
33
|
+
# middleware into the Rack stack that will add debugging information to responses.
|
34
|
+
def enable!
|
35
|
+
enable_on_cache_store!(ActiveSupport::Cache::Store)
|
36
|
+
connection_class = ActiveRecord::Base.connection.class
|
37
|
+
enable_on_connection_adapter!(connection_class)
|
38
|
+
if connection_class.name.match(/mysql/i)
|
39
|
+
ExplainPlan.enable_on_connection_adapter!(connection_class, :mysql)
|
40
|
+
elsif connection_class.name.match(/postgres/i)
|
41
|
+
ExplainPlan.enable_on_connection_adapter!(connection_class, :postgresql)
|
42
|
+
end
|
43
|
+
Rails.configuration.middleware.use(Middleware)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Enable SQL analysis on a connection adapter.
|
47
|
+
def enable_on_connection_adapter!(connection_adapter_class)
|
48
|
+
connection_adapter_class.send(:include, ConnectionAdapter) unless connection_adapter_class.include?(ConnectionAdapter)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Enable monitoring on fetches from an ActiveSupport::Cache::Store class. This
|
52
|
+
# will allow reporting which queries are in cache blocks in the analysis.
|
53
|
+
def enable_on_cache_store!(cache_store_class)
|
54
|
+
cache_store_class.send(:include, CacheStore) unless cache_store_class.include?(CacheStore)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Get the configuration. There is only ever one configuration. The values in the
|
58
|
+
# configuration can be changed. If you only need to change them temporarily, see
|
59
|
+
# +override_config+.
|
14
60
|
def config
|
15
|
-
@config ||=
|
61
|
+
@config ||= Configuration.new
|
16
62
|
end
|
17
63
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
64
|
+
# Set configuration values within a block. The block given to this method will
|
65
|
+
# be yielded to with a clone of the configuration. Any changes to the configuration
|
66
|
+
# will only persist within the block.
|
67
|
+
def override_config
|
68
|
+
save_val = config
|
69
|
+
begin
|
70
|
+
@config = save_val.dup
|
71
|
+
@config.style = @config.style.dup
|
72
|
+
yield(@config)
|
73
|
+
ensure
|
74
|
+
@config = save_val
|
23
75
|
end
|
24
|
-
Rails.configuration.middleware.use(SqlSafetyNet::RackHandler)
|
25
76
|
end
|
26
77
|
end
|
27
78
|
end
|
data/spec/cache_store_spec.rb
CHANGED
@@ -1,26 +1,17 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe SqlSafetyNet::CacheStore do
|
4
|
-
let(:cache){ ActiveSupport::Cache::MemoryStore.new }
|
5
4
|
|
6
|
-
|
7
|
-
SqlSafetyNet::QueryAnalysis.analyze do
|
8
|
-
SqlSafetyNet::QueryAnalysis.current.caching?.should == false
|
9
|
-
val = cache.fetch("key") do
|
10
|
-
SqlSafetyNet::QueryAnalysis.current.caching?.should == true
|
11
|
-
"woot"
|
12
|
-
end
|
13
|
-
val.should == "woot"
|
14
|
-
end
|
15
|
-
end
|
5
|
+
let(:cache){ ActiveSupport::Cache::MemoryStore.new }
|
16
6
|
|
17
|
-
it "should
|
18
|
-
SqlSafetyNet::
|
19
|
-
val = cache.fetch("
|
20
|
-
SqlSafetyNet::
|
21
|
-
"
|
7
|
+
it "should determine if code is inside a cache fetch block" do
|
8
|
+
SqlSafetyNet::CacheStore.in_fetch_block?.should == false
|
9
|
+
val = cache.fetch("foo") do
|
10
|
+
SqlSafetyNet::CacheStore.in_fetch_block?.should == true
|
11
|
+
"bar"
|
22
12
|
end
|
23
|
-
val.should == "
|
13
|
+
val.should == "bar"
|
14
|
+
SqlSafetyNet::CacheStore.in_fetch_block?.should == false
|
24
15
|
end
|
25
16
|
|
26
17
|
end
|
data/spec/configuration_spec.rb
CHANGED
@@ -1,67 +1,72 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe SqlSafetyNet::Configuration do
|
4
|
-
|
5
|
-
before :each do
|
6
|
-
@config = SqlSafetyNet::Configuration.new
|
7
|
-
end
|
8
4
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
it "should be able to set filesort_limit" do
|
36
|
-
@config.filesort_limit = 600
|
37
|
-
@config.filesort_limit.should == 600
|
38
|
-
end
|
39
|
-
|
40
|
-
it "should be able to set examine_rows_limit" do
|
41
|
-
@config.examine_rows_limit = 1000
|
42
|
-
@config.examine_rows_limit.should == 1000
|
43
|
-
end
|
44
|
-
|
45
|
-
it "should be able to set return_rows_limit" do
|
46
|
-
@config.return_rows_limit = 1000
|
47
|
-
@config.return_rows_limit.should == 1000
|
48
|
-
end
|
49
|
-
|
50
|
-
it "should be able to set query_limit" do
|
51
|
-
@config.query_limit = 20
|
52
|
-
@config.query_limit.should == 20
|
5
|
+
let(:config){ SqlSafetyNet::Configuration.new }
|
6
|
+
|
7
|
+
describe "standard settings" do
|
8
|
+
it "should have a query_limit with a default of 10" do
|
9
|
+
config.query_limit.should == 10
|
10
|
+
config.query_limit = 100
|
11
|
+
config.query_limit.should == 100
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should have a returned_rows_limit with a default of 100" do
|
15
|
+
config.returned_rows_limit.should == 100
|
16
|
+
config.returned_rows_limit = 200
|
17
|
+
config.returned_rows_limit.should == 200
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should have a result_size_limit with a default of 16K" do
|
21
|
+
config.result_size_limit.should == 16 * 1024
|
22
|
+
config.result_size_limit = 10000
|
23
|
+
config.result_size_limit.should == 10000
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should have a elapsed_time_limit with a default of 300ms" do
|
27
|
+
config.elapsed_time_limit.should == 0.3
|
28
|
+
config.elapsed_time_limit = 1
|
29
|
+
config.elapsed_time_limit.should == 1
|
30
|
+
end
|
53
31
|
end
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
32
|
+
|
33
|
+
describe "query plan limits" do
|
34
|
+
it "should have a table_scan_limit with a default of 100 rows" do
|
35
|
+
config.table_scan_limit.should == 100
|
36
|
+
config.table_scan_limit = 200
|
37
|
+
config.table_scan_limit.should == 200
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should have a temporary_table_limit with a default of 100 rows" do
|
41
|
+
config.temporary_table_limit.should == 100
|
42
|
+
config.temporary_table_limit = 200
|
43
|
+
config.temporary_table_limit.should == 200
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should have a filesort_limit with a default of 100 rows" do
|
47
|
+
config.filesort_limit.should == 100
|
48
|
+
config.filesort_limit = 200
|
49
|
+
config.filesort_limit.should == 200
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should have an examined_rows_limit with a default of 5000 rows" do
|
53
|
+
config.examined_rows_limit.should == 5000
|
54
|
+
config.examined_rows_limit = 10000
|
55
|
+
config.examined_rows_limit.should == 10000
|
56
|
+
end
|
58
57
|
end
|
59
58
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
59
|
+
describe "debugging information" do
|
60
|
+
it "should have a flag to always_show debugging info" do
|
61
|
+
config.always_show.should == false
|
62
|
+
config.always_show = true
|
63
|
+
config.always_show.should == true
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should have css style" do
|
67
|
+
config.style.should == {}
|
68
|
+
config.style = {"top" => "5px", "right" => "5px"}
|
69
|
+
config.style.should == {"top" => "5px", "right" => "5px"}
|
70
|
+
end
|
65
71
|
end
|
66
|
-
|
67
72
|
end
|