sql_safety_net 1.1.11 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,19 +1,40 @@
1
- = SQL Safety Net
1
+ = SqlSafetyNet
2
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.
3
+ ActiveRecord makes it very easy and seamless to access data from a database. A downside of this is you often don't realize what kind of load you are putting on the database either by the number or the type of queries generated because "it just works." This can lead to performance problems in production because databases are notoriously hard and expensive to scale.
4
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.
5
+ This gem exposes debugging information about SQL queries generated by ActiveRecord in a Rails application. It is intended to be used in development mode to allow developers to see what queries are being generated so issues can be caught before code goes to production.
6
6
 
7
- == Example
7
+ It works by injecting code into the connection adapter to count and analyze SELECT queries. It does not collect any information on INSERT, UPDATE, or DELETE queries. The analysis is exposed by a Rack middleware handler in a variety of ways.
8
8
 
9
- Add this code to an initializer:
9
+ == Features
10
10
 
11
- SqlSafetyNet.init_rails
11
+ SqlSafetyNet will track data about each query in your request and analyze them individually and as a group.
12
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:
13
+ * Rows returned by each query
14
+ * Estimated number of bytes returned by each query
15
+ * Time taken to execute each query
16
+ * Total number of queries
17
+ * Total number or rows returned for all queries
18
+ * Total estimated bytes returned for all queries
19
+ * Total time taken to execute all queries
20
+ * Query plan analysis for each query (if supported)
14
21
 
15
- SqlSafetyNet.init_rails do |config|
16
- config.query_limit = 20
17
- end
22
+ == Debugging Output
18
23
 
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.
24
+ A summary of the queries will be added to all responses in the X-SqlSafetyNet header. This will include the number of queries, the number of rows returned, the approximate amount of data returned from the database, and the elapsed time to make the queries.
25
+
26
+ When issues are found with queries in a request, this information will be logged.
27
+
28
+ If the response is an HTML document and the request was not from Ajax, a debug info window will be inserted into the document if there were any queries flagged as problematic. This is the most effective way to insure that the analysis is always visible to the developers. The box can also be expanded to details about each query. The debug box will always be displayed if the request queries are flagged with issues. There is also a configuration setting to always show the debug box. The box will be green if there are no issues, red if there are issues, or orange if there are issues but the queries that generate them are cached in Rails.cache.
29
+
30
+ == Configuration
31
+
32
+ There are variety of configuration options where you can specify the thresholds which you'd consider excess database usage. See SqlSafetyNet::Config for details.
33
+
34
+ == Query Plan Analysis
35
+
36
+ If you are using MySQL or PostgreSQL, then each query will also get the query plan from the database and analyze it for problems like table scans on large tables.
37
+
38
+ The query analysis for PostgreSQL is much less detailed than MySQL because the MySQL plans are much more straightforward to understand programatically. Reading PostgreSQL query plans is more of an art. In addition, take the PostgreSQL warnings with a grain of salt. It only looks for large table scans or large number of rows examined in a query. However, PostgreSQL will only estimate these numbers on a simple EXPLAIN plan and sometimes it gets the number very wrong on small tables. For the query plan may estimate the query will do a table scan on 300 rows even though the table only has 10 rows in it.
39
+
40
+ For details on enabling query plan analysis see SqlSafteyNet::ExplainPlan.
data/Rakefile CHANGED
@@ -27,12 +27,10 @@ begin
27
27
  gem.has_rdoc = true
28
28
  gem.rdoc_options << '--line-numbers' << '--inline-source' << '--main' << 'README.rdoc'
29
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')
30
+ gem.add_dependency('activesupport', '>= 3.0.0')
31
+ gem.add_dependency('activerecord', '>= 3.0.0')
32
+ gem.add_dependency('actionpack', '>= 3.0.0')
33
33
  gem.add_development_dependency('rspec', '>= 2.0.0')
34
- gem.add_development_dependency('mysql')
35
- gem.add_development_dependency('pg')
36
34
  gem.add_development_dependency('sqlite3-ruby')
37
35
  end
38
36
  Jeweler::RubygemsDotOrgTasks.new
@@ -1,18 +1,28 @@
1
1
  module SqlSafetyNet
2
- # Hook into ActiveSupport::Cache to set a caching flag on the QueryAnalysis whenever +fetch+ is called with a block.
2
+ # This module provides a hook into ActiveSupport::Cache::Store caches to keep
3
+ # track of when a query happens inside a cache fetch block. This will be reported
4
+ # in the analysis.
3
5
  module CacheStore
4
- def self.included(base)
5
- base.alias_method_chain(:fetch, :sql_safety_net)
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ alias_method_chain :fetch, :sql_safety_net
6
10
  end
7
11
 
8
12
  def fetch_with_sql_safety_net(*args, &block)
9
- analysis = QueryAnalysis.current
10
- saved_val = analysis.caching? if analysis
13
+ save_val = Thread.current[:sql_safety_net_in_cache_store_fetch_block]
11
14
  begin
12
- analysis.caching = true if analysis
15
+ Thread.current[:sql_safety_net_in_cache_store_fetch_block] = true
13
16
  fetch_without_sql_safety_net(*args, &block)
14
17
  ensure
15
- analysis.caching = saved_val if analysis
18
+ Thread.current[:sql_safety_net_in_cache_store_fetch_block] = save_val
19
+ end
20
+ end
21
+
22
+ class << self
23
+ # Return +true+ if called from within a +fetch+ block.
24
+ def in_fetch_block?
25
+ !!Thread.current[:sql_safety_net_in_cache_store_fetch_block]
16
26
  end
17
27
  end
18
28
  end
@@ -1,57 +1,46 @@
1
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.
2
+ # This class provides configuration options for SQL analysis.
3
+ #
4
+ # These options specify when a warning will be triggered based on the totals from all queries
5
+ # in a single request:
6
+ #
7
+ # * query_limit - the total number of queries (default to 10)
8
+ # * returned_rows_limit - the total number of rows returned (defaults to 100)
9
+ # * result_size_limit - the number of bytes returned by all queries (defaults to 16K)
10
+ # * elapsed_time_limit - the number of seconds taken for all queries (defaults to 0.3)
11
+ #
12
+ # These options specify when a warning will be triggered on a single query. These options are only
13
+ # available when using MySQL:
14
+ #
15
+ # * table_scan_limit - the number of rows in a table scan that will trigger a warning (defaults to 100)
16
+ # * temporary_table_limit - the number of temporary table rows that will trigger a warning (defaults to 100)
17
+ # * filesort_limit - the number of rows in a filesort operation that will trigger a warning (defaults to 100)
18
+ # * examined_rows_limit - the number of rows examined in a query that will trigger a warning (defaults to 5000)
19
+ #
20
+ # These options specify details about embedding debugging info in HTML pages
21
+ #
22
+ # * always_show - set to true to always show debugging info; otherwise only shown if the request is flagged (defaults to false)
23
+ # * style - set to a hash of CSS styles used to style the debugging info; defaults to appearing in the upper right corner
4
24
  class Configuration
5
- attr_accessor :table_scan_limit, :temporary_table_limit, :filesort_limit, :return_rows_limit, :examine_rows_limit, :query_limit, :time_limit, :position
25
+ attr_accessor :query_limit, :returned_rows_limit, :result_size_limit, :elapsed_time_limit
26
+ attr_accessor :table_scan_limit, :temporary_table_limit, :filesort_limit, :examined_rows_limit
27
+ attr_accessor :always_show, :style
6
28
 
7
29
  def initialize
8
- @debug = false
9
- @header = false
30
+ @query_limit = 10
31
+ @returned_rows_limit = 100
32
+ @result_size_limit = 16 * 1024
33
+ @elapsed_time_limit = 0.3
34
+
10
35
  @table_scan_limit = 100
11
36
  @temporary_table_limit = 100
12
37
  @filesort_limit = 100
13
- @return_rows_limit = 100
14
- @examine_rows_limit = 5000
15
- @query_limit = 10
38
+ @examined_rows_limit = 5000
39
+
16
40
  @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
41
+ @style = {}
42
+
43
+ yield(self) if block_given?
55
44
  end
56
45
  end
57
46
  end
@@ -1,103 +1,72 @@
1
1
  module SqlSafetyNet
2
- # Logic to be mixed into connection adapters allowing them to analyze queries.
2
+ # This module needs to be included with the specific ActiveRecord::ConnectionAdapter class
3
+ # to collect data about all SELECT queries.
3
4
  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
5
+ extend ActiveSupport::Concern
23
6
 
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
7
+ SELECT_SQL_PATTERN = /\A\s*SELECT\b/im.freeze
8
+ IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE).freeze
9
+
10
+ included do
11
+ alias_method_chain :select_rows, :sql_safety_net
12
+ alias_method_chain :select, :sql_safety_net
28
13
  end
29
14
 
30
15
  def select_rows_with_sql_safety_net(sql, name = nil, *args)
31
- analyze_sql_safety_net_query(sql, name, *args) do
16
+ analyze_query(sql, name, []) do
32
17
  select_rows_without_sql_safety_net(sql, name, *args)
33
18
  end
34
19
  end
35
20
 
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
21
  protected
56
-
22
+
57
23
  def select_with_sql_safety_net(sql, name = nil, *args)
58
- analyze_sql_safety_net_query(sql, name, *args) do
24
+ binds = args.first || []
25
+ analyze_query(sql, name, binds) do
59
26
  select_without_sql_safety_net(sql, name, *args)
60
27
  end
61
28
  end
62
29
 
63
- private
64
30
 
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
31
+ def analyze_query(sql, name, binds)
32
+ queries = QueryAnalysis.current
33
+ if queries && sql.match(SELECT_SQL_PATTERN) && !IGNORED_PAYLOADS.include?(name)
34
+ start_time = Time.now
35
+ results = yield
36
+ elapsed_time = Time.now - start_time
37
+
38
+ expanded_sql = sql
39
+ unless binds.empty?
40
+ sql = "#{sql} #{binds.collect{|col, val| [col.name, val]}.inspect}"
73
41
  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
42
+ rows = results.size
43
+ result_size = 0
44
+ results.each do |row|
45
+ values = row.is_a?(Hash) ? row.values : row
46
+ values.each{|val| result_size += val.to_s.size if val}
97
47
  end
98
-
99
- query_results
48
+ cached = CacheStore.in_fetch_block?
49
+ sql_str = nil
50
+ if method(:to_sql).arity == 1
51
+ sql_str = (sql.is_a?(String) ? sql : to_sql(sql))
52
+ else
53
+ sql_str = to_sql(sql, binds)
54
+ end
55
+ query_info = QueryInfo.new(sql_str, :elapsed_time => elapsed_time, :rows => rows, :result_size => result_size, :cached => cached)
56
+ queries << query_info
57
+
58
+ # If connection includes a query plan analyzer then alert on issues in the query plan.
59
+ if respond_to?(:sql_safety_net_analyze_query_plan)
60
+ query_info.alerts.concat(sql_safety_net_analyze_query_plan(sql, binds))
61
+ end
62
+
63
+ query_info.alerts.each{|alert| ActiveRecord::Base.logger.debug(alert)} if ActiveRecord::Base.logger
64
+
65
+ results
66
+ else
67
+ yield
100
68
  end
101
69
  end
70
+
102
71
  end
103
72
  end
@@ -0,0 +1,33 @@
1
+ module SqlSafetyNet
2
+ module ExplainPlan
3
+ # Include this module in your MySQL connection class to analyze the query plan generated by MySQL.
4
+ module Mysql
5
+ def sql_safety_net_analyze_query_plan(sql, binds)
6
+ alerts = []
7
+ config = SqlSafetyNet.config
8
+ explain_results = select("EXPLAIN #{sql}", "EXPLAIN", binds)
9
+
10
+ explain_results.each do |row|
11
+ select_type = (row['select_type'] || '').downcase
12
+ type = (row['type'] || '').downcase
13
+ rows = row['rows'].to_i
14
+ extra = (row['Extra'] || '').downcase
15
+ key = row['key']
16
+ possible_keys = row['possible_keys']
17
+
18
+ alerts << "table scan on #{rows} rows" if (type.include?('all') && rows > config.table_scan_limit)
19
+ alerts << "no index used" if (key.blank? && rows > config.table_scan_limit)
20
+ alerts << "no index possible" if (possible_keys.blank? && rows > config.table_scan_limit)
21
+ alerts << "dependent subquery" if select_type.include?('dependent')
22
+ alerts << "uncacheable subquery" if select_type.include?('uncacheable')
23
+ alerts << "full scan on null key" if extra.include?('full scan on null key')
24
+ alerts << "uses temporary table for #{rows} rows" if extra.include?('using temporary') && rows > config.temporary_table_limit
25
+ alerts << "uses filesort for #{rows} rows" if extra.include?('filesort') && rows > config.filesort_limit
26
+ alerts << "examined #{rows} rows" if rows > config.examined_rows_limit
27
+ end
28
+
29
+ alerts
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ module SqlSafetyNet
2
+ module ExplainPlan
3
+ # Include this module in your PostgreSQL connection class to analyze the query plan. It will just
4
+ # look for excess row counts in the number of rows examined or scanned. The row counts provided by
5
+ # PostgreSQL are just estimates, so take any alerts with a grain of salt.
6
+ module Postgresql
7
+ def sql_safety_net_analyze_query_plan(sql, binds)
8
+ alerts = []
9
+ config = SqlSafetyNet.config
10
+ explain_results = select("EXPLAIN #{sql}", "EXPLAIN", binds)
11
+ query_plan = explain_results.collect{|r| r.values.first}
12
+ limit = nil
13
+
14
+ query_plan.each do |row|
15
+ row_count = row.match(/\brows=(\d+)/) ? $~[1].to_i : 0
16
+ row_count = [limit, row_count].min if limit
17
+ if row =~ /^(\s|(->))*Limit\s/
18
+ limit = row_count
19
+ elsif row =~ /^(\s|(->))*Seq Scan/
20
+ alerts << "table scan on ~#{row_count} rows" if row_count > config.table_scan_limit
21
+ elsif row_count > config.examined_rows_limit
22
+ alerts << "examined ~#{row_count} rows"
23
+ end
24
+ end
25
+
26
+ alerts
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,24 @@
1
+ module SqlSafetyNet
2
+ # Query plan analysis is supported out of the box for MySQL and PostgreSQL.
3
+ #
4
+ # If you wish to implement it for another database, you'll need to create a module that defines
5
+ # the +sql_safety_net_analyze_query_plan+ method and takes arguments for the sql to execute and
6
+ # an array of bind values.
7
+ module ExplainPlan
8
+ autoload :Mysql, File.expand_path("../explain_plan/mysql.rb", __FILE__)
9
+ autoload :Postgresql, File.expand_path("../explain_plan/postgresql.rb", __FILE__)
10
+
11
+ class << self
12
+ # Enable query plan analysize on a connection adapter class. The explain_plan_analyzer argument
13
+ # can either be <tt>:mysql</tt>, <tt>:postgresql</tt> or a module that defines a
14
+ # <tt>sql_safety_net_analyze_query_plan(sql, binds)</tt> method.
15
+ def enable_on_connection_adapter!(connection_adapter_class, explain_plan_analyzer)
16
+ if explain_plan_analyzer.is_a?(Symbol)
17
+ class_name = explain_plan_analyzer.to_s.camelize
18
+ explain_plan_analyzer = ExplainPlan.const_get(class_name)
19
+ end
20
+ connection_adapter_class.send(:include, explain_plan_analyzer) unless connection_adapter_class.include?(explain_plan_analyzer)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,178 @@
1
+ require 'rack'
2
+
3
+ module SqlSafetyNet
4
+ # Formatter to output information from a query analysis in various formats.
5
+ class Formatter
6
+ attr_reader :analysis
7
+
8
+ def initialize(analysis)
9
+ @analysis = analysis
10
+ end
11
+
12
+ def to_html
13
+ uncached_analysis = QueryAnalysis.new
14
+ cached_analysis = QueryAnalysis.new
15
+ analysis.queries.each{ |query| query.cached? ? cached_analysis << query : uncached_analysis << query }
16
+
17
+ ok_color = "#060"
18
+ warn_color = "#900"
19
+ cache_warn_color = "#A80"
20
+ theme_color = ok_color
21
+ theme_color = cache_warn_color if uncached_analysis.flagged?
22
+ theme_color = warn_color if analysis.flagged?
23
+ close_js = "document.getElementById('_sql_safety_net_').style.display = 'none'"
24
+ toggle_queries_js = "document.getElementById('_sql_safety_net_queries_').style.display = (document.getElementById('_sql_safety_net_queries_').style.display == 'block' ? 'none' : 'block')"
25
+
26
+ tag(:div, :id => "_sql_safety_net_", :style => div_style(SqlSafetyNet.config.style)) do
27
+ tag(:div, :style => "padding:4px; background-color:#{theme_color}; font-weight:bold; color:#FFF;") do
28
+ tag(:div) do
29
+ tag(:a, :href => "javascript:void(#{close_js})", :style => "float:right; display:block; text-decoration:none") do
30
+ tag(:span, "&times;", :style => "color:#FFF; text-decoration:none; font-weight:bold;")
31
+ end
32
+ tag(:a, :href => "javascript:void(#{toggle_queries_js})", :style => "text-decoration:none;") do
33
+ tag(:span, "#{analysis.flagged? ? 'SQL WARNING' : 'SQL INFO'} &raquo;", :style => "color:#FFF; text-decoration:none; font-weight:bold;")
34
+ end
35
+ end
36
+ tag(:div, summary, :style => "font-weight:normal;")
37
+ end
38
+
39
+ tag(:div, :id => "_sql_safety_net_queries_", :style => "display:none; border:1px solid #{theme_color}; background-color:#FFF; color:#000; overflow:auto; max-height:500px;") do
40
+ tag(:div, :style => "padding-left:4px; padding-right:4px;") do
41
+ if cached_analysis.total_queries > 0
42
+ tag(:div, :style => "margin-top:5px; margin-bottom:5px;") do
43
+ tag(:div, "Uncached", :style => "font-weight:bold;")
44
+ tag(:div, Formatter.new(uncached_analysis).summary)
45
+ end
46
+
47
+ tag(:div, :style => "margin-top:5px; margin-bottom:5px; color:#066;") do
48
+ tag(:div, "Cached", :style => "font-weight:bold;")
49
+ tag(:div, Formatter.new(cached_analysis).summary)
50
+ end
51
+ end
52
+
53
+ warning_style = "color:#{warn_color}; margin-top:5px; margin-bottom:5px;"
54
+ tag(:div, "WARNING: #{analysis.total_queries} queries", :style => warning_style) if analysis.too_many_queries?
55
+ tag(:div, "WARNING: #{analysis.rows} rows returned", :style => warning_style) if analysis.too_many_rows?
56
+ tag(:div, "WARNING: #{sprintf('%0.1f', analysis.result_size / 1024.0)}K returned", :style => warning_style) if analysis.results_too_big?
57
+ tag(:div, "WARNING: queries took #{(analysis.elapsed_time * 1000).round} ms", :style => warning_style) if analysis.too_much_time?
58
+ tag(:div, "WARNING: alerts on #{analysis.alerted_queries} queries", :style => warning_style) if analysis.alerts?
59
+ end
60
+
61
+ analysis.queries.each do |query|
62
+ color = ok_color
63
+ if query.alerts?
64
+ color = (query.cached? ? cache_warn_color : warn_color)
65
+ end
66
+ tag(:div, :style => "color:#{color}; border-top:1px solid #CCC; padding:8px 4px;#{' background-color:#DEE;' if query.cached?}") do
67
+ tag(:div, "CACHED", :style => "color:#066;") if query.cached?
68
+ query_info = "#{query.rows} row#{'s' if query.rows != 1} returned (#{sprintf('%0.1f', query.result_size / 1024.0)}K) in #{(query.elapsed_time * 1000).round} ms"
69
+ tag(:div, query_info, :style => "margin-bottom:5px;")
70
+ if query.alerts?
71
+ tag(:div, :style => "margin-bottom:5px;") do
72
+ query.alerts.each do |alert|
73
+ tag(:div, alert, :style => "margin-bottom:2px;")
74
+ end
75
+ end
76
+ end
77
+ tag(:div, query.sql, :style => "color:#666")
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ def to_s
85
+ uncached_analysis = QueryAnalysis.new
86
+ cached_analysis = QueryAnalysis.new
87
+ analysis.queries.each{ |query| query.cached? ? cached_analysis << query : uncached_analysis << query }
88
+ lines = []
89
+ lines << "#{analysis.flagged? ? 'SQL WARNING' : 'SQL INFO'}: #{summary}"
90
+ if cached_analysis.total_queries > 0
91
+ lines << "UNCACHED: #{Formatter.new(uncached_analysis).summary}"
92
+ lines << "CACHED: #{Formatter.new(cached_analysis).summary}"
93
+ end
94
+ lines << "WARNING: #{analysis.total_queries} queries" if analysis.too_many_queries?
95
+ lines << "WARNING: #{analysis.rows} rows returned" if analysis.too_many_rows?
96
+ lines << "WARNING: #{analysis.result_size}K returned" if analysis.results_too_big?
97
+ lines << "WARNING: queries took #{(analysis.elapsed_time * 1000).round} ms" if analysis.too_much_time?
98
+ lines << "WARNING: alerts on #{analysis.alerted_queries} queries" if analysis.alerts?
99
+ analysis.queries.each do |query|
100
+ lines << "-----------------"
101
+ lines << "CACHED" if query.cached?
102
+ lines << "#{query.rows} row#{'s' if query.rows != 1} returned (#{sprintf('%0.1f', query.result_size / 1024.0)}K) in #{(query.elapsed_time * 1000).round} ms"
103
+ lines.concat(query.alerts)
104
+ lines << "#{query.sql}"
105
+ end
106
+ lines.join("\n")
107
+ end
108
+
109
+ def summary
110
+ queries = analysis.total_queries
111
+ rows = analysis.rows
112
+ kilobytes = analysis.result_size / 1024.0
113
+ "#{queries} #{queries == 1 ? 'query' : 'queries'}, #{analysis.rows} row#{rows == 1 ? '' : 's'}, #{sprintf("%0.1f", kilobytes)}K, #{(analysis.elapsed_time * 1000).round}ms"
114
+ end
115
+
116
+ # Turn a hash of styles into a proper CSS style attribute. The style will use specified defaults
117
+ # to make the div appear in the top, right corner of the page at 160px wide.
118
+ def div_style(style)
119
+ default_style = {
120
+ "font-family" => "sans-serif",
121
+ "font-size" => "10px",
122
+ "font-weight" => "normal",
123
+ "position" => "fixed",
124
+ "z-index" => "999999",
125
+ "text-align" => "left",
126
+ "line-height" => "100%",
127
+ "width" => "200px"
128
+ }
129
+
130
+ style = default_style.merge(style)
131
+ style.delete_if{|key, value| value.blank? }
132
+
133
+ # Ensure a default positioning
134
+ if %w(fixed static absolute).include?(style["position"])
135
+ style["top"] = "5px" if style["top"].blank? && style["bottom"].blank?
136
+ style["right"] = "5px" if style["right"].blank? && style["left"].blank?
137
+ end
138
+
139
+ css_style = ""
140
+ style.each do |name, value|
141
+ css_style << "#{name}:#{value};"
142
+ end
143
+ css_style
144
+ end
145
+
146
+ private
147
+
148
+ # Helper to generate an HTML tag.
149
+ def tag(name, *args)
150
+ attributes = args.extract_options!
151
+ body = args.first
152
+
153
+ @buffer ||= []
154
+ output = ""
155
+ @buffer.push(output)
156
+ output << "<#{name}"
157
+ if attributes
158
+ attributes.each do |key, value|
159
+ output << " #{key}=\"#{Rack::Utils.escape_html(value)}\""
160
+ end
161
+ end
162
+ if block_given? || body
163
+ output << ">"
164
+ if block_given?
165
+ yield
166
+ else
167
+ output << body if body
168
+ end
169
+ output << "</#{name}>"
170
+ else
171
+ output << "/>"
172
+ end
173
+ @buffer.pop
174
+ @buffer.last << output unless @buffer.empty?
175
+ output
176
+ end
177
+ end
178
+ end