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.
@@ -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
- attr_accessor :selects, :rows, :elapsed_time
5
- attr_reader :flagged_queries, :non_flagged_queries
6
- attr_writer :caching
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
- @selects = 0
10
- @rows = 0
26
+ @queries = []
11
27
  @elapsed_time = 0.0
12
- @flagged_queries = []
13
- @non_flagged_queries = []
14
- @caching = false
28
+ @rows = 0
29
+ @result_size = 0
15
30
  end
16
-
17
- # Analyze all queries within a block.
18
- def self.analyze
19
- Thread.current[:sql_safety_net] = new
20
- begin
21
- yield
22
- return current
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
- # Get the current analysis object.
29
- def self.current
30
- Thread.current[:sql_safety_net]
39
+
40
+ def total_queries
41
+ queries.size
31
42
  end
32
-
33
- # Clear all query information from the current analysis.
34
- def self.clear
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 caching?
39
- @caching
48
+ def alerts?
49
+ queries.any?{|query| query.alerts?}
40
50
  end
41
51
 
42
- def flagged?
43
- too_many_selects? || too_many_rows? || flagged_queries?
52
+ def too_many_rows?
53
+ rows > SqlSafetyNet.config.returned_rows_limit
44
54
  end
45
55
 
46
- def too_many_selects?
47
- selects > SqlSafetyNet.config.query_limit
56
+ def too_many_queries?
57
+ total_queries > SqlSafetyNet.config.query_limit
48
58
  end
49
-
50
- def too_many_rows?
51
- rows > SqlSafetyNet.config.return_rows_limit
59
+
60
+ def results_too_big?
61
+ result_size > SqlSafetyNet.config.result_size_limit
52
62
  end
53
-
54
- def flagged_queries?
55
- !flagged_queries.empty?
63
+
64
+ def too_much_time?
65
+ elapsed_time > SqlSafetyNet.config.elapsed_time_limit
56
66
  end
57
67
 
58
- def add_query(sql, name, rows, elapsed_time, flagged)
59
- info = {:sql => sql, :name => name, :rows => rows, :elapsed_time => elapsed_time, :cached => caching?}
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
@@ -1,27 +1,78 @@
1
- require 'active_record'
2
- require 'action_controller'
3
-
4
- # Collects and displays debugging information about SQL statements.
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('../sql_safety_net/cache_store', __FILE__)
7
- autoload :Configuration, File.expand_path('../sql_safety_net/configuration', __FILE__)
8
- autoload :ConnectionAdapter, File.expand_path('../sql_safety_net/connection_adapter', __FILE__)
9
- autoload :QueryAnalysis, File.expand_path('../sql_safety_net/query_analysis', __FILE__)
10
- autoload :RackHandler, File.expand_path('../sql_safety_net/rack_handler', __FILE__)
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
- # Get the configuration for the safety net.
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 ||= SqlSafetyNet::Configuration.new
61
+ @config ||= Configuration.new
16
62
  end
17
63
 
18
- def init_rails
19
- SqlSafetyNet.config.enable_on(ActiveRecord::Base.connection.class)
20
- if Rails.env == "development"
21
- SqlSafetyNet.config.debug = true
22
- ActiveSupport::Cache::Store.send(:include, CacheStore)
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
@@ -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
- it "should set the caching flag on the current query analysis when fetching with a block" do
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 fetch properly even when there is not current query analysis" do
18
- SqlSafetyNet::QueryAnalysis.current.should == nil
19
- val = cache.fetch("key") do
20
- SqlSafetyNet::QueryAnalysis.current.should == nil
21
- "woot"
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 == "woot"
13
+ val.should == "bar"
14
+ SqlSafetyNet::CacheStore.in_fetch_block?.should == false
24
15
  end
25
16
 
26
17
  end
@@ -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
- it "should be able to set debug" do
10
- @config.debug?.should == false
11
- @config.debug = true
12
- @config.debug?.should == true
13
- @config.debug = false
14
- @config.debug?.should == false
15
- end
16
-
17
- it "should be able to set header" do
18
- @config.header?.should == false
19
- @config.header = true
20
- @config.header?.should == true
21
- @config.header = false
22
- @config.header?.should == false
23
- end
24
-
25
- it "should be able to set table_scan_limit" do
26
- @config.table_scan_limit = 500
27
- @config.table_scan_limit.should == 500
28
- end
29
-
30
- it "should be able to set temporary_table_limit" do
31
- @config.temporary_table_limit = 50
32
- @config.temporary_table_limit.should == 50
33
- end
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
- it "should be able to set time_limit" do
56
- @config.time_limit = 200
57
- @config.time_limit.should == 200
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
- it "should be able to set always_show" do
61
- @config.always_show = true
62
- @config.always_show?.should == true
63
- @config.always_show = nil
64
- @config.always_show?.should == false
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