sql_safety_net 1.1.11
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/License.txt +674 -0
- data/README.rdoc +19 -0
- data/Rakefile +40 -0
- data/lib/sql_safety_net/cache_store.rb +19 -0
- data/lib/sql_safety_net/configuration.rb +57 -0
- data/lib/sql_safety_net/connection_adapter/mysql_adapter.rb +43 -0
- data/lib/sql_safety_net/connection_adapter/postgresql_adapter.rb +48 -0
- data/lib/sql_safety_net/connection_adapter.rb +103 -0
- data/lib/sql_safety_net/query_analysis.rb +69 -0
- data/lib/sql_safety_net/rack_handler.rb +196 -0
- data/lib/sql_safety_net.rb +27 -0
- data/spec/cache_store_spec.rb +26 -0
- data/spec/configuration_spec.rb +67 -0
- data/spec/connection_adapter_spec.rb +143 -0
- data/spec/example_database.yml +14 -0
- data/spec/mysql_connection_adapter_spec.rb +32 -0
- data/spec/postgres_connection_adapter_spec.rb +43 -0
- data/spec/query_analysis_spec.rb +71 -0
- data/spec/rack_handler_spec.rb +193 -0
- data/spec/spec_helper.rb +56 -0
- metadata +148 -0
data/README.rdoc
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
= SQL Safety Net
|
2
|
+
|
3
|
+
This gem provides hooks into ActiveRecord to analyze SQL queries. It can be very useful in keeping an eye on tuning your code before it becomes a problem. ActiveRecord can make it very easy to send excessive load to the database.
|
4
|
+
|
5
|
+
With this gem enabled, HTML pages which have bad queries or too many queries on them will have a debug div added to the HTML with information about the queries. If you see this in development mode, you should investigate and come up with ways to reduce the load.
|
6
|
+
|
7
|
+
== Example
|
8
|
+
|
9
|
+
Add this code to an initializer:
|
10
|
+
|
11
|
+
SqlSafetyNet.init_rails
|
12
|
+
|
13
|
+
You can pass a block to this method which will yield a configuration object you can use to set threshold parameters on. See SqlSafetyNet::Configuration for all options. For example:
|
14
|
+
|
15
|
+
SqlSafetyNet.init_rails do |config|
|
16
|
+
config.query_limit = 20
|
17
|
+
end
|
18
|
+
|
19
|
+
If you need finer control over how the gem is set up, or you are using it in a non-Rails environment, you can initialize it manually. You will need to call <tt>SqlSafetyNet::Configuration.enable_on</tt> with the ActiveRecord connection object and you will need to add SqlSafetyNet::RackHandler to the Rack middleware stack.
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
desc 'Default: run unit tests'
|
5
|
+
task :default => :test
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'rspec'
|
9
|
+
require 'rspec/core/rake_task'
|
10
|
+
desc 'Run the unit tests'
|
11
|
+
RSpec::Core::RakeTask.new(:test)
|
12
|
+
rescue LoadError
|
13
|
+
task :test do
|
14
|
+
raise "You must have rspec 2.0 installed to run the tests"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
begin
|
19
|
+
require 'jeweler'
|
20
|
+
Jeweler::Tasks.new do |gem|
|
21
|
+
gem.name = "sql_safety_net"
|
22
|
+
gem.summary = %Q{Debug SQL statements in ActiveRecord}
|
23
|
+
gem.description = %Q{Debug SQL statements in ActiveRecord by displaying warnings on bad queries.}
|
24
|
+
gem.authors = ["Brian Durand"]
|
25
|
+
gem.email = ["mdobrota@tribune.com", "ddpr@tribune.com"]
|
26
|
+
gem.files = FileList["lib/**/*", "spec/**/*", "README.rdoc", "Rakefile", "License.txt"].to_a
|
27
|
+
gem.has_rdoc = true
|
28
|
+
gem.rdoc_options << '--line-numbers' << '--inline-source' << '--main' << 'README.rdoc'
|
29
|
+
gem.extra_rdoc_files = ["README.rdoc"]
|
30
|
+
gem.add_dependency('activesupport')
|
31
|
+
gem.add_dependency('activerecord', '>= 2.2.2')
|
32
|
+
gem.add_dependency('actionpack')
|
33
|
+
gem.add_development_dependency('rspec', '>= 2.0.0')
|
34
|
+
gem.add_development_dependency('mysql')
|
35
|
+
gem.add_development_dependency('pg')
|
36
|
+
gem.add_development_dependency('sqlite3-ruby')
|
37
|
+
end
|
38
|
+
Jeweler::RubygemsDotOrgTasks.new
|
39
|
+
rescue LoadError
|
40
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module SqlSafetyNet
|
2
|
+
# Hook into ActiveSupport::Cache to set a caching flag on the QueryAnalysis whenever +fetch+ is called with a block.
|
3
|
+
module CacheStore
|
4
|
+
def self.included(base)
|
5
|
+
base.alias_method_chain(:fetch, :sql_safety_net)
|
6
|
+
end
|
7
|
+
|
8
|
+
def fetch_with_sql_safety_net(*args, &block)
|
9
|
+
analysis = QueryAnalysis.current
|
10
|
+
saved_val = analysis.caching? if analysis
|
11
|
+
begin
|
12
|
+
analysis.caching = true if analysis
|
13
|
+
fetch_without_sql_safety_net(*args, &block)
|
14
|
+
ensure
|
15
|
+
analysis.caching = saved_val if analysis
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module SqlSafetyNet
|
2
|
+
# Configuration for SqlSafetyNet. The various limit attributes are used for adapters that can do query analysis. Queries
|
3
|
+
# will be flagged if their query plan exceeds a limit.
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :table_scan_limit, :temporary_table_limit, :filesort_limit, :return_rows_limit, :examine_rows_limit, :query_limit, :time_limit, :position
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@debug = false
|
9
|
+
@header = false
|
10
|
+
@table_scan_limit = 100
|
11
|
+
@temporary_table_limit = 100
|
12
|
+
@filesort_limit = 100
|
13
|
+
@return_rows_limit = 100
|
14
|
+
@examine_rows_limit = 5000
|
15
|
+
@query_limit = 10
|
16
|
+
@always_show = false
|
17
|
+
@position = "top:5px; right: 5px;"
|
18
|
+
@time_limit = 300
|
19
|
+
end
|
20
|
+
|
21
|
+
# Enable SqlSafetyNet on a connection. Unless this method is called, the code will not be mixed into the database
|
22
|
+
# adapter. This should normally be called only in the development environment.
|
23
|
+
def enable_on(connection_class)
|
24
|
+
connection_class = connection_class.constantize unless connection_class.is_a?(Class)
|
25
|
+
connection_class_name = connection_class.name.split('::').last
|
26
|
+
include_class = ConnectionAdapter.const_get(connection_class_name) if ConnectionAdapter.const_defined?(connection_class_name)
|
27
|
+
connection_class.send(:include, ConnectionAdapter) unless connection_class.include?(ConnectionAdapter)
|
28
|
+
connection_class.send(:include, include_class) if include_class && !connection_class.include?(include_class)
|
29
|
+
end
|
30
|
+
|
31
|
+
def debug=(val)
|
32
|
+
@debug = !!val
|
33
|
+
end
|
34
|
+
|
35
|
+
def debug?
|
36
|
+
@debug
|
37
|
+
end
|
38
|
+
|
39
|
+
def header=(val)
|
40
|
+
@header = !!val
|
41
|
+
end
|
42
|
+
|
43
|
+
def header?
|
44
|
+
@header
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set a flag to always show information about queries on rendered HTML pages. If this is not set to true, the debug
|
48
|
+
# information will only be shown if the queries on the page exceed one of the limits.
|
49
|
+
def always_show=(val)
|
50
|
+
@always_show = !!val
|
51
|
+
end
|
52
|
+
|
53
|
+
def always_show?
|
54
|
+
@always_show
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module SqlSafetyNet
|
2
|
+
module ConnectionAdapter
|
3
|
+
# Logic for analyzing MySQL query plans.
|
4
|
+
module MysqlAdapter
|
5
|
+
def analyze_query(sql, name, *args)
|
6
|
+
if select_statement?(sql)
|
7
|
+
query_plan = select_without_sql_safety_net("EXPLAIN #{sql}", "EXPLAIN", *args)
|
8
|
+
query_plan_flags = analyze_query_plan(query_plan)
|
9
|
+
unless query_plan_flags.empty?
|
10
|
+
@logger.debug("Flagged query plan #{name} (#{query_plan_flags.join(', ')}): #{query_plan.inspect}") if @logger
|
11
|
+
return {:query_plan => query_plan, :flags => query_plan_flags}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def analyze_query_plan(query_plan)
|
19
|
+
flagged = []
|
20
|
+
query_plan.each do |row|
|
21
|
+
select_type = (row['select_type'] || '').downcase
|
22
|
+
type = (row['type'] || '').downcase
|
23
|
+
rows = row['rows'].to_i
|
24
|
+
extra = (row['Extra'] || '').downcase
|
25
|
+
key = row['key']
|
26
|
+
possible_keys = row['possible_keys']
|
27
|
+
|
28
|
+
flagged << 'table scan' if (type.include?('all') and rows > SqlSafetyNet.config.table_scan_limit)
|
29
|
+
flagged << 'fulltext search' if type.include?('fulltext')
|
30
|
+
flagged << 'no index used' if (key.blank? and rows > SqlSafetyNet.config.table_scan_limit)
|
31
|
+
flagged << 'no indexes possible' if (possible_keys.blank? and rows > SqlSafetyNet.config.table_scan_limit)
|
32
|
+
flagged << 'dependent subquery' if select_type.include?('dependent')
|
33
|
+
flagged << 'uncacheable subquery' if select_type.include?('uncacheable')
|
34
|
+
flagged << 'full scan on null key' if extra.include?('full scan on null key')
|
35
|
+
flagged << "uses temporary table for #{rows} rows" if extra.include?('using temporary') and rows > SqlSafetyNet.config.temporary_table_limit
|
36
|
+
flagged << "uses filesort for #{rows} rows" if extra.include?('filesort') and rows > SqlSafetyNet.config.filesort_limit
|
37
|
+
flagged << "examines #{rows} rows" if rows > SqlSafetyNet.config.examine_rows_limit
|
38
|
+
end
|
39
|
+
return flagged
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module SqlSafetyNet
|
2
|
+
module ConnectionAdapter
|
3
|
+
# Logic for analyzing a query plan from PostgreSQL. These plans are not terribly useful and sometimes
|
4
|
+
# the statistics are off, so take them with a grain of salt.
|
5
|
+
module PostgreSQLAdapter
|
6
|
+
def analyze_query(sql, name, *args)
|
7
|
+
if select_statement?(sql)
|
8
|
+
query_plan = select_without_sql_safety_net("EXPLAIN #{sql}", "EXPLAIN", *args)
|
9
|
+
query_plan_flags = analyze_query_plan(query_plan)
|
10
|
+
unless query_plan_flags.empty?
|
11
|
+
@logger.debug("Flagged query plan #{name} (#{query_plan_flags.join(', ')}): #{query_plan.inspect}") if @logger
|
12
|
+
return {:query_plan => query_plan, :flags => query_plan_flags}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def analyze_query_plan(query_plan)
|
20
|
+
query_plan = query_plan.collect{|r| r.values.first}
|
21
|
+
flagged = []
|
22
|
+
limit = nil
|
23
|
+
query_plan.each do |row|
|
24
|
+
row_count = query_plan_rows_value(row)
|
25
|
+
row_count = [limit, row_count].min if limit
|
26
|
+
if row =~ /^(\s|(->))*Limit\s/
|
27
|
+
limit = row_count
|
28
|
+
elsif row =~ /^(\s|(->))*Seq Scan/
|
29
|
+
flagged << 'table scan' if row_count > SqlSafetyNet.config.table_scan_limit
|
30
|
+
elsif row_count > SqlSafetyNet.config.examine_rows_limit
|
31
|
+
flagged << "examines #{row_count} rows"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
return flagged
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def query_plan_rows_value(plan_row)
|
40
|
+
if plan_row.match(/\brows=(\d+)/)
|
41
|
+
return $~[1].to_i
|
42
|
+
else
|
43
|
+
return 0
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module SqlSafetyNet
|
2
|
+
# Logic to be mixed into connection adapters allowing them to analyze queries.
|
3
|
+
module ConnectionAdapter
|
4
|
+
autoload :MysqlAdapter, File.expand_path('../connection_adapter/mysql_adapter', __FILE__)
|
5
|
+
autoload :PostgreSQLAdapter, File.expand_path('../connection_adapter/postgresql_adapter', __FILE__)
|
6
|
+
|
7
|
+
SELECT_STATEMENT = /^\s*SELECT\b/i
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.alias_method_chain :select, :sql_safety_net
|
11
|
+
base.alias_method_chain :select_rows, :sql_safety_net
|
12
|
+
base.alias_method_chain :columns, :sql_safety_net
|
13
|
+
base.alias_method_chain :active?, :sql_safety_net
|
14
|
+
end
|
15
|
+
|
16
|
+
def active_with_sql_safety_net?
|
17
|
+
active = disable_sql_safety_net{active_without_sql_safety_net?}
|
18
|
+
if @logger && !active
|
19
|
+
@logger.warn("#{adapter_name} connection not active")
|
20
|
+
end
|
21
|
+
active
|
22
|
+
end
|
23
|
+
|
24
|
+
def columns_with_sql_safety_net(table_name, name = nil)
|
25
|
+
disable_sql_safety_net do
|
26
|
+
columns_without_sql_safety_net(table_name, name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def select_rows_with_sql_safety_net(sql, name = nil, *args)
|
31
|
+
analyze_sql_safety_net_query(sql, name, *args) do
|
32
|
+
select_rows_without_sql_safety_net(sql, name, *args)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Disable query analysis within a block.
|
37
|
+
def disable_sql_safety_net
|
38
|
+
save_disabled = Thread.current[:sql_safety_net_disable]
|
39
|
+
begin
|
40
|
+
Thread.current[:sql_safety_net_disable] = true
|
41
|
+
yield
|
42
|
+
ensure
|
43
|
+
Thread.current[:sql_safety_net_disable] = save_disabled
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def analyze_query(sql, *args)
|
48
|
+
# No op; other modules may redefine to analyze query plans
|
49
|
+
end
|
50
|
+
|
51
|
+
def select_statement?(sql)
|
52
|
+
!!sql.match(SELECT_STATEMENT)
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def select_with_sql_safety_net(sql, name = nil, *args)
|
58
|
+
analyze_sql_safety_net_query(sql, name, *args) do
|
59
|
+
select_without_sql_safety_net(sql, name, *args)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def analyze_sql_safety_net_query(sql, name, *args)
|
66
|
+
if Thread.current[:sql_safety_net_disable]
|
67
|
+
yield
|
68
|
+
else
|
69
|
+
t = Time.now.to_f
|
70
|
+
query_results = nil
|
71
|
+
disable_sql_safety_net do
|
72
|
+
query_results = yield
|
73
|
+
end
|
74
|
+
elapsed_time = Time.now.to_f - t
|
75
|
+
|
76
|
+
unless Thread.current[:sql_safety_net_disable]
|
77
|
+
query_info = Thread.current[:sql_safety_net_query_info]
|
78
|
+
if query_info
|
79
|
+
query_info.count += 1
|
80
|
+
query_info.selects += query_results.size
|
81
|
+
end
|
82
|
+
analysis = QueryAnalysis.current
|
83
|
+
if analysis
|
84
|
+
analysis.selects += 1
|
85
|
+
analysis.rows += query_results.size
|
86
|
+
analysis.elapsed_time += elapsed_time
|
87
|
+
if SqlSafetyNet.config.debug?
|
88
|
+
flagged = (@logger ? @logger.silence{analyze_query(sql, name, *args)} : analyze_query(sql, name, *args))
|
89
|
+
if elapsed_time * 1000 >= SqlSafetyNet.config.time_limit
|
90
|
+
flagged ||= {}
|
91
|
+
flagged[:flags] ||= []
|
92
|
+
flagged[:flags] << "query time exceeded #{SqlSafetyNet.config.time_limit} ms"
|
93
|
+
end
|
94
|
+
analysis.add_query(sql, name, query_results.size, elapsed_time, flagged)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
query_results
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module SqlSafetyNet
|
2
|
+
# Analysis container for the sql queries in a context.
|
3
|
+
class QueryAnalysis
|
4
|
+
attr_accessor :selects, :rows, :elapsed_time
|
5
|
+
attr_reader :flagged_queries, :non_flagged_queries
|
6
|
+
attr_writer :caching
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@selects = 0
|
10
|
+
@rows = 0
|
11
|
+
@elapsed_time = 0.0
|
12
|
+
@flagged_queries = []
|
13
|
+
@non_flagged_queries = []
|
14
|
+
@caching = false
|
15
|
+
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
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get the current analysis object.
|
29
|
+
def self.current
|
30
|
+
Thread.current[:sql_safety_net]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Clear all query information from the current analysis.
|
34
|
+
def self.clear
|
35
|
+
Thread.current[:sql_safety_net] = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def caching?
|
39
|
+
@caching
|
40
|
+
end
|
41
|
+
|
42
|
+
def flagged?
|
43
|
+
too_many_selects? || too_many_rows? || flagged_queries?
|
44
|
+
end
|
45
|
+
|
46
|
+
def too_many_selects?
|
47
|
+
selects > SqlSafetyNet.config.query_limit
|
48
|
+
end
|
49
|
+
|
50
|
+
def too_many_rows?
|
51
|
+
rows > SqlSafetyNet.config.return_rows_limit
|
52
|
+
end
|
53
|
+
|
54
|
+
def flagged_queries?
|
55
|
+
!flagged_queries.empty?
|
56
|
+
end
|
57
|
+
|
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
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
module SqlSafetyNet
|
4
|
+
# This Rack handler must be added to the middleware stack in order for query analysis to be output.
|
5
|
+
# If the configuration option for debug is set to true, it will add response headers indicating information
|
6
|
+
# about the queries executed in the course of the request.
|
7
|
+
class RackHandler
|
8
|
+
|
9
|
+
X_SQL_SAFETY_NET_HEADER = "X-SqlSafetyNet".freeze
|
10
|
+
HTML_CONTENT_TYPE_PATTERN = /text\/(x?)html/i
|
11
|
+
XML_CONTENT_TYPE_PATTERN = /application\/xml/i
|
12
|
+
|
13
|
+
def initialize(app, logger = Rails.logger)
|
14
|
+
@app = app
|
15
|
+
@logger = logger
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(env)
|
19
|
+
response = nil
|
20
|
+
analysis = QueryAnalysis.analyze do
|
21
|
+
response = @app.call(env)
|
22
|
+
end
|
23
|
+
|
24
|
+
if @logger && (analysis.too_many_selects? || analysis.too_many_rows?)
|
25
|
+
request = Rack::Request.new(env)
|
26
|
+
@logger.warn("Excess database usage: request generated #{analysis.selects} queries and returned #{analysis.rows} rows [#{request.request_method} #{request.url}]")
|
27
|
+
end
|
28
|
+
|
29
|
+
# Add a response header that contains a summary of the debug info
|
30
|
+
if SqlSafetyNet.config.header? || SqlSafetyNet.config.debug?
|
31
|
+
headers = response[1]
|
32
|
+
headers[X_SQL_SAFETY_NET_HEADER] = "selects=#{analysis.selects}; rows=#{analysis.rows}; elapsed_time=#{(analysis.elapsed_time * 1000).round}; flagged_queries=#{analysis.flagged_queries.size}" if headers
|
33
|
+
end
|
34
|
+
|
35
|
+
if SqlSafetyNet.config.debug?
|
36
|
+
wrapped_response = Rack::Response.new(response[2], response[0], response[1])
|
37
|
+
if analysis.flagged? || SqlSafetyNet.config.always_show?
|
38
|
+
request = Rack::Request.new(env)
|
39
|
+
# Ignore Ajax calls
|
40
|
+
unless request.xhr?
|
41
|
+
# Only if content type is text/html
|
42
|
+
type = wrapped_response.content_type
|
43
|
+
if type.nil? || type.to_s.match(HTML_CONTENT_TYPE_PATTERN)
|
44
|
+
wrapped_response.write(flagged_sql_html(analysis))
|
45
|
+
elsif type.to_s.match(XML_CONTENT_TYPE_PATTERN)
|
46
|
+
wrapped_response.write(xml_comment(flagged_sql_text(analysis)))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
response = wrapped_response.finish
|
51
|
+
end
|
52
|
+
|
53
|
+
response
|
54
|
+
end
|
55
|
+
|
56
|
+
def flagged_sql_html(analysis)
|
57
|
+
flagged_html = ''
|
58
|
+
cached_selects = 0
|
59
|
+
cached_rows = 0
|
60
|
+
cached_elapsed_time = 0.0
|
61
|
+
|
62
|
+
if analysis.flagged_queries?
|
63
|
+
flagged_html << '<div style="color:#C00;">'
|
64
|
+
flagged_html << "<div style=\"font-weight:bold; margin-bottom:10px;\">#{analysis.flagged_queries.size == 1 ? 'This query has' : "These #{analysis.flagged_queries.size} queries have"} flagged query plans:</div>"
|
65
|
+
analysis.flagged_queries.each do |query|
|
66
|
+
if query[:cached]
|
67
|
+
cached_selects += 1
|
68
|
+
cached_rows += query[:rows]
|
69
|
+
cached_elapsed_time += query[:elapsed_time]
|
70
|
+
end
|
71
|
+
flagged_html << '<div style="margin-bottom:10px;">'
|
72
|
+
flagged_html << "<div style=\"font-weight:bold; margin-bottom: 5px;\">#{query[:rows]} rows returned, #{(query[:elapsed_time] * 1000).round} ms#{ " <span style='color:teal;'>(CACHED)</span>" if query[:cached]}</div>"
|
73
|
+
flagged_html << "<div style=\"font-weight:bold; margin-bottom: 5px;\">#{query[:flags].join(', ')}</div>"
|
74
|
+
flagged_html << "<div style=\"margin-bottom: 5px;\">#{Rack::Utils.escape_html(query[:sql])}</div>"
|
75
|
+
flagged_html << "<div style=\"margin-bottom: 5px;\">Query Plan: #{Rack::Utils.escape_html(query[:query_plan].inspect)}</div>" if query[:query_plan]
|
76
|
+
flagged_html << '</div>'
|
77
|
+
end
|
78
|
+
flagged_html << '</div>'
|
79
|
+
end
|
80
|
+
|
81
|
+
if analysis.too_many_selects? || analysis.too_many_rows? || SqlSafetyNet.config.always_show?
|
82
|
+
flagged_html << "<div style=\"font-weight:bold; margin-bottom:10px;\">#{analysis.non_flagged_queries.size == 1 ? 'This query' : "These #{analysis.non_flagged_queries.size} queries"} did not have flagged query plans:</div>"
|
83
|
+
analysis.non_flagged_queries.each do |query|
|
84
|
+
if query[:cached]
|
85
|
+
cached_selects += 1
|
86
|
+
cached_rows += query[:rows]
|
87
|
+
cached_elapsed_time += query[:elapsed_time]
|
88
|
+
end
|
89
|
+
flagged_html << '<div style="margin-bottom:10px;">'
|
90
|
+
flagged_html << "<div style=\"font-weight:bold; margin-bottom: 5px;\">#{query[:rows]} rows returned, #{(query[:elapsed_time] * 1000).round} ms#{ " <span style='color:teal;'>(CACHED)</span>" if query[:cached]}</div>"
|
91
|
+
flagged_html << "<div style=\"margin-bottom: 5px;\">#{Rack::Utils.escape_html(query[:sql])}</div>"
|
92
|
+
flagged_html << '</div>'
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
color_scheme = '#060'
|
97
|
+
if analysis.flagged_queries?
|
98
|
+
color_scheme = '#C00'
|
99
|
+
elsif analysis.flagged?
|
100
|
+
color_scheme = '#C60'
|
101
|
+
end
|
102
|
+
label = (analysis.flagged?) ? 'SQL WARNING' : 'SQL INFO'
|
103
|
+
|
104
|
+
cache_html = nil
|
105
|
+
if cached_selects > 0
|
106
|
+
cache_html = <<-EOS
|
107
|
+
<div style="margin-bottom:10px; font-weight:bold;">
|
108
|
+
Some of the queries will be cached.
|
109
|
+
<div style="color:#C00;">
|
110
|
+
Uncached: #{analysis.selects - cached_selects} selects, #{analysis.rows - cached_rows} rows, #{((analysis.elapsed_time - cached_elapsed_time) * 1000).round} ms
|
111
|
+
</div>
|
112
|
+
<div style="color:teal;">
|
113
|
+
Cached: #{cached_selects} selects, #{cached_rows} rows, #{(cached_elapsed_time * 1000).round} ms
|
114
|
+
</div>
|
115
|
+
</div>
|
116
|
+
EOS
|
117
|
+
end
|
118
|
+
|
119
|
+
<<-EOS
|
120
|
+
<div id="sql_safety_net_warning" style="font-family:sans-serif; font-size:10px; position:fixed; z-index:999999999; text-align:left; #{SqlSafetyNet.config.position}">
|
121
|
+
<div style="background-color:#{color_scheme}; color:#FFF; padding:4px; width:160px; float:right;">
|
122
|
+
<a href="javascript:void(document.getElementById('sql_safety_net_warning').style.display = 'none')" style="text-decoration:none; float:right; display:block; font-size:9px;"><span style="color:#FFF; text-decoration:none; font-weight:bold;">×</span></a>
|
123
|
+
<a href="javascript:void(document.getElementById('sql_safety_net_flagged_queries').style.display = (document.getElementById('sql_safety_net_flagged_queries').style.display == 'block' ? 'none' : 'block'))" style="text-decoration:none;">
|
124
|
+
<span style="color:#FFF; text-decoration:none; font-weight:bold;">#{label} »</span>
|
125
|
+
</a>
|
126
|
+
<div>#{analysis.selects} selects, #{analysis.rows} rows, #{(analysis.elapsed_time * 1000).round} ms</div>
|
127
|
+
</div>
|
128
|
+
<div id="sql_safety_net_flagged_queries" style="clear:right; display:none; width:500px; padding:2px; border:1px solid #{color_scheme}; background-color:#FFF; color:#000; overflow:auto; max-height:500px;">
|
129
|
+
<div style="margin-bottom:10px; font-weight:bold;">
|
130
|
+
There are #{analysis.selects} queries on this page that return #{analysis.rows} rows and took #{(analysis.elapsed_time * 1000).round} ms to execute.
|
131
|
+
</div>
|
132
|
+
#{cache_html}
|
133
|
+
#{flagged_html}
|
134
|
+
</div>
|
135
|
+
</div>
|
136
|
+
EOS
|
137
|
+
end
|
138
|
+
|
139
|
+
def flagged_sql_text(analysis)
|
140
|
+
flagged_text = ''
|
141
|
+
cached_selects = 0
|
142
|
+
cached_rows = 0
|
143
|
+
cached_elapsed_time = 0.0
|
144
|
+
|
145
|
+
if analysis.flagged_queries?
|
146
|
+
flagged_text << "#{analysis.flagged_queries.size == 1 ? 'This query has' : "These #{analysis.flagged_queries.size} queries have"} flagged query plans:\n\n"
|
147
|
+
analysis.flagged_queries.each do |query|
|
148
|
+
if query[:cached]
|
149
|
+
cached_selects += 1
|
150
|
+
cached_rows += query[:rows]
|
151
|
+
cached_elapsed_time += query[:elapsed_time]
|
152
|
+
end
|
153
|
+
flagged_text << "#{query[:rows]} rows returned, #{(query[:elapsed_time] * 1000).round} ms#{ " (CACHED)" if query[:cached]}\n"
|
154
|
+
flagged_text << "#{query[:flags].join(', ')}\n\n"
|
155
|
+
flagged_text << "#{query[:sql]}\n\n"
|
156
|
+
flagged_text << "Query Plan: #{Rack::Utils.escape_html(query[:query_plan].inspect)}\n" if query[:query_plan]
|
157
|
+
flagged_text << "\n"
|
158
|
+
end
|
159
|
+
flagged_text << "\n"
|
160
|
+
end
|
161
|
+
|
162
|
+
if analysis.too_many_selects? || analysis.too_many_rows? || SqlSafetyNet.config.always_show?
|
163
|
+
flagged_text << "#{analysis.non_flagged_queries.size == 1 ? 'This query' : "These #{analysis.non_flagged_queries.size} queries"} did not have flagged query plans:\n\n"
|
164
|
+
analysis.non_flagged_queries.each do |query|
|
165
|
+
if query[:cached]
|
166
|
+
cached_selects += 1
|
167
|
+
cached_rows += query[:rows]
|
168
|
+
cached_elapsed_time += query[:elapsed_time]
|
169
|
+
end
|
170
|
+
flagged_text << "#{query[:rows]} rows returned, #{(query[:elapsed_time] * 1000).round} ms#{ " (CACHED)" if query[:cached]}\n\n"
|
171
|
+
flagged_text << "#{query[:sql]}\n\n"
|
172
|
+
flagged_text << "\n"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
label = (analysis.flagged?) ? 'SQL WARNING' : 'SQL INFO'
|
177
|
+
|
178
|
+
cache_text = ""
|
179
|
+
if cached_selects > 0
|
180
|
+
cache_text << "Some of the queries will be cached.\n\n"
|
181
|
+
cache_text << "Uncached: #{analysis.selects - cached_selects} selects, #{analysis.rows - cached_rows} rows, #{((analysis.elapsed_time - cached_elapsed_time) * 1000).round} ms\n"
|
182
|
+
cache_text << "Cached: #{cached_selects} selects, #{cached_rows} rows, #{(cached_elapsed_time * 1000).round} ms\n\n"
|
183
|
+
end
|
184
|
+
|
185
|
+
text = "SqlSafetyNet\n\n"
|
186
|
+
text << "There are #{analysis.selects} queries on this page that return #{analysis.rows} rows and took #{(analysis.elapsed_time * 1000).round} ms to execute.\n\n"
|
187
|
+
text << cache_text
|
188
|
+
text << flagged_text
|
189
|
+
text
|
190
|
+
end
|
191
|
+
|
192
|
+
def xml_comment(text)
|
193
|
+
"<!-- #{text.gsub('-->', '\\-\\->')} -->"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'action_controller'
|
3
|
+
|
4
|
+
# Collects and displays debugging information about SQL statements.
|
5
|
+
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__)
|
11
|
+
|
12
|
+
class << self
|
13
|
+
# Get the configuration for the safety net.
|
14
|
+
def config
|
15
|
+
@config ||= SqlSafetyNet::Configuration.new
|
16
|
+
end
|
17
|
+
|
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)
|
23
|
+
end
|
24
|
+
Rails.configuration.middleware.use(SqlSafetyNet::RackHandler)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SqlSafetyNet::CacheStore do
|
4
|
+
let(:cache){ ActiveSupport::Cache::MemoryStore.new }
|
5
|
+
|
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
|
16
|
+
|
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"
|
22
|
+
end
|
23
|
+
val.should == "woot"
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|