rack-insight 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. data/.gitignore +13 -0
  2. data/.rspec +1 -0
  3. data/.simplecov +4 -0
  4. data/.travis.yml +8 -0
  5. data/CHANGELOG +58 -0
  6. data/Gemfile +3 -0
  7. data/Gemfile.lock +82 -0
  8. data/LICENSE +24 -0
  9. data/README.md +189 -0
  10. data/Rakefile +27 -0
  11. data/TODO +7 -0
  12. data/lib/rack-insight.rb +1 -0
  13. data/lib/rack/insight.rb +19 -0
  14. data/lib/rack/insight/app.rb +198 -0
  15. data/lib/rack/insight/config.rb +30 -0
  16. data/lib/rack/insight/database.rb +193 -0
  17. data/lib/rack/insight/enable-button.rb +43 -0
  18. data/lib/rack/insight/filtered_backtrace.rb +45 -0
  19. data/lib/rack/insight/instrumentation.rb +9 -0
  20. data/lib/rack/insight/instrumentation/backstage.rb +10 -0
  21. data/lib/rack/insight/instrumentation/client.rb +20 -0
  22. data/lib/rack/insight/instrumentation/instrument.rb +109 -0
  23. data/lib/rack/insight/instrumentation/package-definition.rb +58 -0
  24. data/lib/rack/insight/instrumentation/probe-definition.rb +20 -0
  25. data/lib/rack/insight/instrumentation/probe.rb +196 -0
  26. data/lib/rack/insight/instrumentation/setup.rb +32 -0
  27. data/lib/rack/insight/logger.rb +53 -0
  28. data/lib/rack/insight/options.rb +116 -0
  29. data/lib/rack/insight/panel.rb +135 -0
  30. data/lib/rack/insight/panel_app.rb +31 -0
  31. data/lib/rack/insight/panels-content.rb +22 -0
  32. data/lib/rack/insight/panels-header.rb +18 -0
  33. data/lib/rack/insight/panels/active_record_panel.rb +46 -0
  34. data/lib/rack/insight/panels/active_resource_panel.rb +48 -0
  35. data/lib/rack/insight/panels/active_resource_panel/query.rb +27 -0
  36. data/lib/rack/insight/panels/cache_panel.rb +68 -0
  37. data/lib/rack/insight/panels/cache_panel/panel_app.rb +46 -0
  38. data/lib/rack/insight/panels/cache_panel/stats.rb +90 -0
  39. data/lib/rack/insight/panels/log_panel.rb +53 -0
  40. data/lib/rack/insight/panels/memory_panel.rb +36 -0
  41. data/lib/rack/insight/panels/mongo_panel.rb +41 -0
  42. data/lib/rack/insight/panels/mongo_panel/mongo_extension.rb +24 -0
  43. data/lib/rack/insight/panels/mongo_panel/stats.rb +46 -0
  44. data/lib/rack/insight/panels/rails_info_panel.rb +19 -0
  45. data/lib/rack/insight/panels/redis_panel.rb +42 -0
  46. data/lib/rack/insight/panels/redis_panel/redis_extension.rb +23 -0
  47. data/lib/rack/insight/panels/redis_panel/stats.rb +50 -0
  48. data/lib/rack/insight/panels/request_variables_panel.rb +70 -0
  49. data/lib/rack/insight/panels/speedtracer_panel.rb +89 -0
  50. data/lib/rack/insight/panels/speedtracer_panel/profiling.rb +29 -0
  51. data/lib/rack/insight/panels/speedtracer_panel/trace-app.rb +52 -0
  52. data/lib/rack/insight/panels/speedtracer_panel/tracer.rb +213 -0
  53. data/lib/rack/insight/panels/sphinx_panel.rb +41 -0
  54. data/lib/rack/insight/panels/sphinx_panel/stats.rb +94 -0
  55. data/lib/rack/insight/panels/sql_panel.rb +53 -0
  56. data/lib/rack/insight/panels/sql_panel/panel_app.rb +37 -0
  57. data/lib/rack/insight/panels/sql_panel/query.rb +94 -0
  58. data/lib/rack/insight/panels/templates_panel.rb +58 -0
  59. data/lib/rack/insight/panels/templates_panel/rendering.rb +81 -0
  60. data/lib/rack/insight/panels/timer_panel.rb +40 -0
  61. data/lib/rack/insight/params_signature.rb +61 -0
  62. data/lib/rack/insight/path-filter.rb +23 -0
  63. data/lib/rack/insight/public/__insight__/bookmarklet.html +10 -0
  64. data/lib/rack/insight/public/__insight__/bookmarklet.js +223 -0
  65. data/lib/rack/insight/public/__insight__/insight.css +235 -0
  66. data/lib/rack/insight/public/__insight__/insight.js +127 -0
  67. data/lib/rack/insight/public/__insight__/jquery-1.3.2.js +4376 -0
  68. data/lib/rack/insight/public/__insight__/jquery.tablesorter.min.js +1 -0
  69. data/lib/rack/insight/public/__insight__/spinner.gif +0 -0
  70. data/lib/rack/insight/rack_static_bug_avoider.rb +16 -0
  71. data/lib/rack/insight/redirect_interceptor.rb +25 -0
  72. data/lib/rack/insight/render.rb +72 -0
  73. data/lib/rack/insight/request-recorder.rb +22 -0
  74. data/lib/rack/insight/rspec_matchers.rb +33 -0
  75. data/lib/rack/insight/toolbar.rb +69 -0
  76. data/lib/rack/insight/version.rb +7 -0
  77. data/lib/rack/insight/views/enable-button.html.erb +21 -0
  78. data/lib/rack/insight/views/error.html.erb +17 -0
  79. data/lib/rack/insight/views/headers_fragment.html.erb +20 -0
  80. data/lib/rack/insight/views/panels/active_record.html.erb +17 -0
  81. data/lib/rack/insight/views/panels/active_resource.html.erb +47 -0
  82. data/lib/rack/insight/views/panels/cache.html.erb +93 -0
  83. data/lib/rack/insight/views/panels/execute_sql.html.erb +32 -0
  84. data/lib/rack/insight/views/panels/explain_sql.html.erb +32 -0
  85. data/lib/rack/insight/views/panels/log.html.erb +21 -0
  86. data/lib/rack/insight/views/panels/mongo.html.erb +32 -0
  87. data/lib/rack/insight/views/panels/profile_sql.html.erb +32 -0
  88. data/lib/rack/insight/views/panels/rails_info.html.erb +19 -0
  89. data/lib/rack/insight/views/panels/redis.html.erb +46 -0
  90. data/lib/rack/insight/views/panels/request_variables.html.erb +25 -0
  91. data/lib/rack/insight/views/panels/speedtracer/serverevent.html.erb +10 -0
  92. data/lib/rack/insight/views/panels/speedtracer/servertrace.html.erb +12 -0
  93. data/lib/rack/insight/views/panels/speedtracer/traces.html.erb +18 -0
  94. data/lib/rack/insight/views/panels/sphinx.html.erb +32 -0
  95. data/lib/rack/insight/views/panels/sql.html.erb +43 -0
  96. data/lib/rack/insight/views/panels/templates.html.erb +6 -0
  97. data/lib/rack/insight/views/panels/timer.html.erb +19 -0
  98. data/lib/rack/insight/views/panels/view_cache.html.erb +19 -0
  99. data/lib/rack/insight/views/redirect.html.erb +16 -0
  100. data/lib/rack/insight/views/request_fragment.html.erb +25 -0
  101. data/lib/rack/insight/views/toolbar.html.erb +29 -0
  102. data/rack-insight.gemspec +40 -0
  103. data/spec/custom_matchers.rb +0 -0
  104. data/spec/fixtures/config.ru +8 -0
  105. data/spec/fixtures/dummy_panel.rb +2 -0
  106. data/spec/fixtures/sample_app.rb +72 -0
  107. data/spec/fixtures/star_trek_panel.rb +1 -0
  108. data/spec/insight_spec.rb +163 -0
  109. data/spec/instrumentation_spec.rb +188 -0
  110. data/spec/rack/insight/config_spec.rb +20 -0
  111. data/spec/rack/insight/panels/active_record_panel_spec.rb +43 -0
  112. data/spec/rack/insight/panels/active_resource_panel_spec.rb +40 -0
  113. data/spec/rack/insight/panels/cache_panel_spec.rb +178 -0
  114. data/spec/rack/insight/panels/log_panel_spec.rb +44 -0
  115. data/spec/rack/insight/panels/memory_panel_spec.rb +21 -0
  116. data/spec/rack/insight/panels/mongo_panel_spec_pending.rb +52 -0
  117. data/spec/rack/insight/panels/rails_info_panel_spec.rb +29 -0
  118. data/spec/rack/insight/panels/redis_panel_spec.rb +67 -0
  119. data/spec/rack/insight/panels/speedtracer_panel_spec.rb +86 -0
  120. data/spec/rack/insight/panels/sql_panel_spec.rb +146 -0
  121. data/spec/rack/insight/panels/templates_panel_spec.rb +86 -0
  122. data/spec/rack/insight/panels/timer_panel_spec.rb +38 -0
  123. data/spec/rcov.opts +1 -0
  124. data/spec/spec.opts +1 -0
  125. data/spec/spec_helper.rb +111 -0
  126. metadata +380 -0
@@ -0,0 +1,198 @@
1
+ require 'rack'
2
+ require "digest/sha1"
3
+ require "rack/insight/config"
4
+ require "rack/insight/logger"
5
+ require "rack/insight/filtered_backtrace"
6
+ require "rack/insight/options"
7
+ require "rack/insight/panel"
8
+ require "rack/insight/panel_app"
9
+ require "rack/insight/params_signature"
10
+ require "rack/insight/rack_static_bug_avoider"
11
+ require "rack/insight/redirect_interceptor"
12
+ require "rack/insight/render"
13
+ require "rack/insight/toolbar"
14
+ require "rack/insight/enable-button"
15
+ require "rack/insight/path-filter"
16
+ require 'rack/insight/request-recorder'
17
+ require 'rack/insight/instrumentation/setup'
18
+ require 'rack/insight/panels-content'
19
+ require 'rack/insight/panels-header'
20
+
21
+ module Rack::Insight
22
+ class App
23
+ include Options
24
+ INSIGHT_ROOT = "/__insight__"
25
+ INSIGHT_REGEX = %r{^#{INSIGHT_ROOT}}
26
+
27
+ VERSION = "0.4.4"
28
+
29
+ class SecurityError < StandardError
30
+ end
31
+
32
+ def initialize(app, options = {}, &block)
33
+ initialize_options options
34
+ @base_app = app
35
+ @panels = []
36
+ instance_eval(&block) if block_given?
37
+
38
+ @logger = Logger.new(read_option(:log_level), read_option(:log_path))
39
+ Thread.current['rack-insight.logger'] = @logger
40
+ build_normal_stack
41
+ build_debug_stack
42
+ if options[:on_initialize]
43
+ options[:on_initialize].call(self)
44
+ end
45
+ end
46
+ attr_reader :logger
47
+ attr_accessor :panels
48
+
49
+ def call(env)
50
+ @original_request = Rack::Request.new(env)
51
+ if insight_active?
52
+ @env = env
53
+ self.options = @default_options
54
+
55
+ env['rack-insight.logger'] = @logger
56
+ Thread.current['rack-insight.logger'] = @logger
57
+
58
+ Rack::Insight.enable
59
+ env["rack-insight.panels"] = []
60
+ @debug_stack.call(env)
61
+ else
62
+ @normal_stack.call(env)
63
+ end
64
+ end
65
+
66
+
67
+ def reset(new_options=nil)
68
+ @env = nil
69
+ initialize_options(new_options)
70
+
71
+ Rack::Insight::Instrumentation::ClassProbe::all_probes.each do |probe|
72
+ probe.clear_collectors
73
+ end
74
+ Rack::Insight::Instrumentation::InstanceProbe::all_probes.each do |probe|
75
+ probe.clear_collectors
76
+ end
77
+ Rack::Insight::Instrumentation::PackageDefinition.clear_collectors
78
+
79
+ build_debug_stack
80
+ end
81
+
82
+ private
83
+
84
+ def insight_active?
85
+ return (toolbar_requested? && ip_authorized? && password_authorized?)
86
+ end
87
+
88
+ def build_normal_stack
89
+ builder = Rack::Builder.new
90
+ builder.use EnableButton, self
91
+ builder.run Rack::Cascade.new([ asset_mapped(Rack::Builder.new), @base_app ])
92
+ @normal_stack = builder.to_app
93
+ end
94
+
95
+ def build_debug_stack
96
+ @panels.clear
97
+ builder = Rack::Builder.new
98
+ builder.use Toolbar, self
99
+ builder.run Rack::Cascade.new([panel_mappings, shortcut_stack(@base_app), collection_stack(@base_app)])
100
+
101
+ @debug_stack = builder.to_app
102
+ end
103
+
104
+ def panel_mappings
105
+ classes = read_option(:panel_classes)
106
+ root = INSIGHT_ROOT
107
+ insight = self
108
+ builder = Rack::Builder.new do
109
+ classes.each do |panel_class|
110
+ panel_class.panel_mappings.each do |path, app|
111
+ map [root, path].join("/") do
112
+ run app
113
+ end
114
+ end
115
+ end
116
+ map root + "/panels_content" do
117
+ run PanelsContent.new(insight)
118
+ end
119
+ map root + "/panels_header" do
120
+ run PanelsHeader.new(insight)
121
+ end
122
+ end
123
+ return asset_mapped(builder)
124
+ end
125
+
126
+ def shortcut_stack(app)
127
+ Rack::Builder.app do
128
+ use PathFilter
129
+ run app
130
+ end
131
+ end
132
+
133
+ def collection_stack(app)
134
+ classes = read_option(:panel_classes)
135
+ insight_id = self.object_id
136
+ panels = self.panels
137
+
138
+ #Builder makes it impossible to access the panels
139
+
140
+ app = Instrumentation::Setup.new(app)
141
+ app = RedirectInterceptor.new(app)
142
+ #Reversed? Does it matter?
143
+ app = classes.inject(app) do |app, panel_class|
144
+ panel = panel_class.new(app)
145
+ panels << panel
146
+ panel
147
+ end
148
+ app = RequestRecorder.new(app)
149
+ return app
150
+ end
151
+
152
+ def asset_mapped(builder)
153
+ path = public_path
154
+ builder.map INSIGHT_ROOT do
155
+ run Rack::File.new(path)
156
+ end
157
+ builder.to_app
158
+ end
159
+
160
+ def public_path
161
+ ::File.expand_path("../../insight/public/__insight__", __FILE__)
162
+ end
163
+
164
+ def toolbar_requested?
165
+ @original_request.cookies["rack-insight_enabled"]
166
+ end
167
+
168
+ def ip_authorized?
169
+ return true unless options["rack-insight.ip_masks"]
170
+
171
+ logger.debug{ "Checking #{@original_request.ip} against ip_masks" }
172
+ ip = IPAddr.new(@original_request.ip)
173
+
174
+ mask = options["rack-insight.ip_masks"].find do |ip_mask|
175
+ ip_mask.include?(ip)
176
+ end
177
+ if mask
178
+ logger.debug{ "Matched #{mask}" }
179
+ return true
180
+ else
181
+ logger.debug{ "Matched no masks" }
182
+ return false
183
+ end
184
+ end
185
+
186
+ def password_authorized?
187
+ return true unless options["rack-insight.password"]
188
+
189
+ logger.debug{"Checking password"}
190
+
191
+ expected_sha = Digest::SHA1.hexdigest ["rack-insight", options["rack-insight.password"]].join(":")
192
+ actual_sha = @original_request.cookies["rack-insight_password"]
193
+
194
+ logger.debug{"Password result: #{actual_sha == expected_sha}"}
195
+ actual_sha == expected_sha
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,30 @@
1
+ module Rack::Insight
2
+ class Config
3
+ class << self
4
+ attr_accessor :config
5
+ end
6
+
7
+ DEFAULTS = {
8
+ # You can augment or replace the default set of panel load paths.
9
+ # These are the paths where rack-insight will look for panels.
10
+ # A rack-insight extension gem could place panels in:
11
+ # lib/foo/bar/
12
+ # Since gems' lib/ is automatically shifted onto Ruby load path, this will make the custom panels discoverable:
13
+ # Rack::Insight::Config.configure do |config|
14
+ # config[:panel_load_paths] << File::join('foo', 'bar')
15
+ # end
16
+ :panel_load_paths => [File::join('rack', 'insight', 'panels')]
17
+ }
18
+
19
+ #cattr_reader :config
20
+ #cattr_writer :config
21
+
22
+ @config ||= DEFAULTS
23
+ def self.configure &block
24
+ yield @config
25
+ unless config[:panel_load_paths].kind_of?(Array)
26
+ raise "Rack::Insight::Config.config[:panel_load_paths] is invalid: Expected kind of Array but got #{config[:panel_load_paths].class}"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,193 @@
1
+ #require 'rack-insight'
2
+ require 'sqlite3'
3
+ require 'base64'
4
+
5
+ module Rack::Insight
6
+ class Database
7
+ module RequestDataClient
8
+ def key_sql_template(sql)
9
+ @key_sql_template = sql
10
+ end
11
+
12
+ def table_setup(name, *keys)
13
+ @table = DataTable.new(name, *keys)
14
+ if keys.empty?
15
+ @key_sql_template = ""
16
+ end
17
+ end
18
+
19
+ def store(env, *keys_and_value)
20
+ return if env.nil?
21
+ request_id = env["rack-insight.request-id"]
22
+ return if request_id.nil?
23
+
24
+ value = keys_and_value[-1]
25
+ keys = keys_and_value[0...-1]
26
+
27
+ @table.store(request_id, value, @key_sql_template % keys)
28
+ end
29
+
30
+ def retrieve(request_id)
31
+ @table.for_request(request_id)
32
+ end
33
+ alias retreive retrieve #JDL cannot spell
34
+
35
+ def table_length
36
+ @table.length
37
+ end
38
+ end
39
+
40
+ class << self
41
+ include Logging
42
+
43
+ def database_path=(value)
44
+ @database_path = value || "rack-insight.sqlite"
45
+ end
46
+
47
+ def database_path
48
+ @database_path
49
+ end
50
+
51
+ def db
52
+ @db ||= open_database
53
+ end
54
+
55
+ def reset
56
+ @db = nil
57
+ end
58
+
59
+ def open_database
60
+ @db = SQLite3::Database.new(database_path)
61
+ @db.execute("pragma foreign_keys = on")
62
+ @db
63
+ rescue Object => ex
64
+ msg = "Issue while loading SQLite DB:" + [ex.class, ex.message, ex.backtrace[0..4]].inspect
65
+ logger.error{ msg }
66
+
67
+ return {}
68
+ end
69
+
70
+ if defined?(PhusionPassenger)
71
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
72
+ Rack::Insight::Database::open_database if forked
73
+ end
74
+ end
75
+ end
76
+
77
+ class Table
78
+ include Logging
79
+
80
+ def db
81
+ Rack::Insight::Database.db
82
+ end
83
+
84
+ def create_keys_clause
85
+ "#{@keys.map{|key| "#{key} varchar"}.join(", ")}"
86
+ end
87
+
88
+ def create_sql
89
+ "create table #@table_name ( id integer primary key, #{create_keys_clause} )"
90
+ end
91
+
92
+ def execute(*args)
93
+ logger.info{ ins_args = args.inspect; "(#{[ins_args.length,120].min}/#{ins_args.length})" + ins_args[0..120] }
94
+ db.execute(*args)
95
+ end
96
+
97
+ def initialize(table_name, *keys)
98
+ @table_name = table_name
99
+ @keys = keys
100
+ if(execute("select * from sqlite_master where name = ?", table_name).empty?)
101
+ logger.warn{ "Initializing a table called #{table_name}" }
102
+ execute(create_sql)
103
+ end
104
+ end
105
+
106
+ def select(which_sql, condition_sql)
107
+ execute("select #{which_sql} from #@table_name where #{condition_sql}")
108
+ end
109
+
110
+ def fields_sql
111
+ "#{@keys.join(",")}"
112
+ end
113
+
114
+ def insert(values_sql)
115
+ execute("insert into #@table_name(#{fields_sql}) values (#{values_sql})")
116
+ end
117
+
118
+ def keys(name)
119
+ execute("select #{name} from #@table_name").flatten
120
+ end
121
+
122
+ def length(where = "1 = 1")
123
+ execute("select count(1) from #@table_name where #{where}").first.first
124
+ end
125
+
126
+ def to_a
127
+ execute("select * from #@table_name")
128
+ end
129
+ end
130
+
131
+ class RequestTable < Table
132
+ def initialize()
133
+ super("requests", "method", "url", "date")
134
+ end
135
+
136
+ def store(method, url)
137
+ result = insert("'#{method}', '#{url}', #{Time.now.to_i}")
138
+ db.last_insert_row_id
139
+ end
140
+
141
+ def last_request_id
142
+ execute("select max(id) from #@table_name").first.first
143
+ end
144
+
145
+ def sweep
146
+ execute("delete from #@table_name where date < #{Time.now.to_i - (60 * 60 * 12)}")
147
+ end
148
+ end
149
+
150
+ require 'yaml'
151
+ class DataTable < Table
152
+ def initialize(name, *keys)
153
+ super(name, *(%w{request_id} + keys + %w{value}))
154
+ end
155
+
156
+ def create_keys_clause
157
+ non_request_keys = @keys - %w"request_id"
158
+ sql = non_request_keys.map{|key| "#{key} varchar"}.join(", ")
159
+ sql += ", request_id references requests(id) on delete cascade"
160
+ sql
161
+ end
162
+
163
+ def store(request_id, value, keys_sql = "")
164
+ sql = "'#{encode_value(value)}'"
165
+ sql = keys_sql + ", " + sql unless keys_sql.empty?
166
+ sql = "#{request_id}, #{sql}"
167
+ insert(sql)
168
+ end
169
+
170
+ def encode_value(value)
171
+ Base64.encode64(YAML.dump(value))
172
+ end
173
+
174
+ def decode_value(value)
175
+ YAML.load(Base64.decode64(value))
176
+ end
177
+
178
+ def retrieve(key_sql)
179
+ select("value", key_sql).map{|value| decode_value(value.first)}
180
+ end
181
+
182
+ def for_request(id)
183
+ retrieve("request_id = #{id}")
184
+ end
185
+
186
+ def to_a
187
+ super.map do |row|
188
+ row[-1] = decode_value(row[-1])
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,43 @@
1
+ module Rack::Insight
2
+ class EnableButton
3
+ include Render
4
+
5
+ MIME_TYPES = ["text/html", "application/xhtml+xml"]
6
+
7
+ def initialize(app, insight)
8
+ @app = app
9
+ @insight = insight
10
+ end
11
+
12
+ def call(env)
13
+ @env = env
14
+ status, headers, body = @app.call(@env)
15
+
16
+ response = Rack::Response.new(body, status, headers)
17
+
18
+ inject_button(response) if okay_to_modify?(env, response)
19
+
20
+ return response.to_a
21
+ end
22
+
23
+ def okay_to_modify?(env, response)
24
+ req = Rack::Request.new(env)
25
+ content_type, charset = response.content_type.split(";")
26
+
27
+ response.ok? && MIME_TYPES.include?(content_type) && !req.xhr?
28
+ end
29
+
30
+ def inject_button(response)
31
+ full_body = response.body.join
32
+ full_body.sub! /<\/body>/, render + "</body>"
33
+
34
+ response["Content-Length"] = full_body.bytesize.to_s
35
+
36
+ response.body = [full_body]
37
+ end
38
+
39
+ def render
40
+ render_template("enable-button")
41
+ end
42
+ end
43
+ end