├── .gitignore ├── History.txt ├── MIT-LICENSE.txt ├── README.md ├── Rakefile ├── Thorfile ├── lib └── rack │ ├── bug.rb │ └── bug │ ├── autoloading.rb │ ├── filtered_backtrace.rb │ ├── options.rb │ ├── panel.rb │ ├── panel_app.rb │ ├── panels │ ├── active_record_panel.rb │ ├── active_record_panel │ │ └── activerecord_extensions.rb │ ├── cache_panel.rb │ ├── cache_panel │ │ ├── dalli_extension.rb │ │ ├── memcache_extension.rb │ │ ├── panel_app.rb │ │ └── stats.rb │ ├── log_panel.rb │ ├── log_panel │ │ └── logger_extension.rb │ ├── memory_panel.rb │ ├── mongo_panel.rb │ ├── mongo_panel │ │ ├── mongo_extension.rb │ │ └── stats.rb │ ├── rails_info_panel.rb │ ├── redis_panel.rb │ ├── redis_panel │ │ ├── redis_extension.rb │ │ └── stats.rb │ ├── request_variables_panel.rb │ ├── sphinx_panel.rb │ ├── sphinx_panel │ │ ├── sphinx_extension.rb │ │ └── stats.rb │ ├── sql_panel.rb │ ├── sql_panel │ │ ├── panel_app.rb │ │ ├── query.rb │ │ └── sql_extension.rb │ ├── templates_panel.rb │ ├── templates_panel │ │ ├── actionview_extension.rb │ │ ├── rendering.rb │ │ └── trace.rb │ └── timer_panel.rb │ ├── params_signature.rb │ ├── public │ └── __rack_bug__ │ │ ├── bookmarklet.html │ │ ├── bookmarklet.js │ │ ├── bug.css │ │ ├── bug.js │ │ ├── jquery-1.3.2.js │ │ ├── jquery.tablesorter.min.js │ │ └── spinner.gif │ ├── rack_static_bug_avoider.rb │ ├── redirect_interceptor.rb │ ├── render.rb │ ├── toolbar.rb │ └── views │ ├── error.html.erb │ ├── panels │ ├── active_record.html.erb │ ├── cache.html.erb │ ├── execute_sql.html.erb │ ├── explain_sql.html.erb │ ├── log.html.erb │ ├── mongo.html.erb │ ├── profile_sql.html.erb │ ├── rails_info.html.erb │ ├── redis.html.erb │ ├── request_variables.html.erb │ ├── sphinx.html.erb │ ├── sql.html.erb │ ├── templates.html.erb │ ├── timer.html.erb │ └── view_cache.html.erb │ ├── redirect.html.erb │ └── toolbar.html.erb ├── rack-bug.gemspec └── spec ├── custom_matchers.rb ├── fixtures ├── config.ru ├── dummy_panel.rb └── sample_app.rb ├── rack ├── bug │ └── panels │ │ ├── active_record_panel_spec.rb │ │ ├── cache_panel_spec.rb │ │ ├── log_panel_spec.rb │ │ ├── memory_panel_spec.rb │ │ ├── mongo_panel_spec.rb │ │ ├── rails_info_panel_spec.rb │ │ ├── redis_panel_spec.rb │ │ ├── sql_panel_spec.rb │ │ ├── templates_panel_spec.rb │ │ └── timer_panel_spec.rb └── bug_spec.rb ├── rcov.opts ├── spec.opts └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | pkg 3 | TODO -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | == HEAD 2 | 3 | * New features 4 | 5 | * Can use LoggerPanel on ruby stdlib Logger in non-rails app (Tim Connor) 6 | 7 | * Bug fixes 8 | 9 | * Fix profile, explain and select in the queries tab, fixes issue #22 (ebertech) 10 | 11 | * Minor fixes 12 | 13 | * Explicitly require 'digest/sha1' (Jérémy Lecour) 14 | * Eliminate unreachable code in params signature validation (Tim Connor) 15 | 16 | * Compatibilty 17 | 18 | * Make Redis panel compatible with latest redis-rb gem, without breaking older redis-rb versions (Luke Melia) 19 | * Fix issues with Ruby 1.9.2 and with mysql2 (George Ogota) 20 | 21 | * Other 22 | 23 | * Refactoring and code cleanup (Tim Connor) 24 | * Testing cleanup - better isolation of Rails vs. non-Rails in tests (Tim Connor) 25 | 26 | == 0.3.0 / 2010-05-28 27 | 28 | * New features 29 | 30 | * Log panel includes log level and timestamp (Tim Connor) 31 | * Sphinx panel (George Chatzigeorgiou) 32 | * Backtraces for Redis panel (Luke Melia & Joey Aghion) 33 | 34 | * Minor fixes 35 | 36 | * Don't "enable" rack bug if you hit cancel on the bookmarklet prompt (Mischa Fierer) 37 | 38 | * Compatibilty 39 | 40 | * backtrace filtering now supports more than just Rails (Alex Chaffee) 41 | * compatibility with current rack-test (Luke Melia & Joey Aghion) 42 | * update Sinatra sample app (Tim Conner) 43 | 44 | == 0.2.1 45 | 46 | * The beginning of recorded history 47 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Bryan Helmkamp 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rack::Bug [](https://codeclimate.com/github/brynary/rack-bug) 2 | ========= 3 | 4 | * Repository: [http://github.com/brynary/rack-bug](http://github.com/brynary/rack-bug) 5 | 6 | Description 7 | ----------- 8 | 9 | Rack::Bug adds a diagnostics toolbar to Rack apps. When enabled, it injects a floating div 10 | allowing exploration of logging, database queries, template rendering times, etc. 11 | 12 | Features 13 | -------- 14 | 15 | * Password-based security 16 | * IP-based security 17 | * Rack::Bug instrumentation/reporting is broken up into panels. 18 | * Panels in default configuration: 19 | * Rails Info 20 | * Timer 21 | * Request Variables 22 | * SQL 23 | * Active Record 24 | * Cache 25 | * Templates 26 | * Log 27 | * Memory 28 | * Other bundled panels: 29 | * Redis 30 | * Sphinx 31 | * The API for adding your own panels is simple and powerful 32 | 33 | Rails quick start 34 | --------------------------- 35 | 36 | script/plugin install git://github.com/brynary/rack-bug.git 37 | 38 | In config/environments/development.rb, add: 39 | 40 | config.middleware.use "Rack::Bug", 41 | :secret_key => "someverylongandveryhardtoguesspreferablyrandomstring" 42 | 43 | Add the bookmarklet to your browser: 44 | 45 | open http://RAILS_APP/__rack_bug__/bookmarklet.html 46 | 47 | Using with non-Rails Rack apps 48 | ------------------------------ 49 | 50 | Just 'use Rack::Bug' as any other middleware. See the SampleApp in the spec/fixtures folder for an example Sinatra app. 51 | 52 | If you wish to use the logger panel define the LOGGER constant that is a ruby Logger or ActiveSupport::BufferedLogger 53 | 54 | Configuring custom panels 55 | ------------------------- 56 | 57 | Specify the set of panels you want, in the order you want them to appear: 58 | 59 | require "rack/bug" 60 | 61 | ActionController::Dispatcher.middleware.use Rack::Bug, 62 | :secret_key => "someverylongandveryhardtoguesspreferablyrandomstring", 63 | :panel_classes => [ 64 | Rack::Bug::TimerPanel, 65 | Rack::Bug::RequestVariablesPanel, 66 | Rack::Bug::RedisPanel, 67 | Rack::Bug::TemplatesPanel, 68 | Rack::Bug::LogPanel, 69 | Rack::Bug::MemoryPanel 70 | ] 71 | 72 | 73 | Running Rack::Bug in staging or production 74 | ------------------------------------------ 75 | 76 | We have have found that Rack::Bug is fast enough to run in production for specific troubleshooting efforts. 77 | 78 | ### Configuration #### 79 | 80 | Add the middleware configuration to an initializer or the appropriate environment files, taking the rest of this section into consideration. 81 | 82 | ### Security #### 83 | 84 | Restrict access to particular IP addresses: 85 | 86 | require "ipaddr" 87 | 88 | ActionController::Dispatcher.middleware.use "Rack::Bug" 89 | :secret_key => "someverylongandveryhardtoguesspreferablyrandomstring", 90 | :ip_masks => [IPAddr.new("2.2.2.2/0")] 91 | 92 | Restrict access using a password: 93 | 94 | ActionController::Dispatcher.middleware.use "Rack::Bug", 95 | :secret_key => "someverylongandveryhardtoguesspreferablyrandomstring", 96 | :password => "yourpassword" 97 | 98 | 99 | Authors 100 | ------- 101 | 102 | - Maintained by [Bryan Helmkamp](mailto:bryan@brynary.com) 103 | - Contributions from Luke Melia, Joey Aghion, Tim Connor, and more 104 | 105 | Thanks 106 | ------ 107 | Inspiration for Rack::Bug is primarily from the Django debug toolbar. Additional ideas from Rails footnotes, Rack's ShowException middleware, Oink, and Rack::Cache 108 | 109 | Thanks to Weplay.com for supporting the development of Rack::Bug 110 | 111 | Development 112 | ----------- 113 | For development, you'll need to install the following gems: rspec, rack-test, webrat, sinatra 114 | 115 | License 116 | ------- 117 | 118 | See MIT-LICENSE.txt in this directory. 119 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "spec/rake/spectask" 3 | 4 | Spec::Rake::SpecTask.new do |t| 5 | t.spec_opts = ['--options', "\"#{File.dirname(__FILE__)}/spec/spec.opts\""] 6 | end 7 | 8 | desc "Run all specs in spec directory with RCov" 9 | Spec::Rake::SpecTask.new(:rcov) do |t| 10 | t.spec_opts = ['--options', "\"#{File.dirname(__FILE__)}/spec/spec.opts\""] 11 | t.rcov = true 12 | t.rcov_opts = lambda do 13 | IO.readlines(File.dirname(__FILE__) + "/spec/rcov.opts").map {|l| l.chomp.split " "}.flatten 14 | end 15 | end 16 | 17 | desc "Run the specs" 18 | task :default => :spec 19 | 20 | desc 'Removes trailing whitespace' 21 | task :whitespace do 22 | sh %{find . -name '*.rb' -exec sed -i '' 's/ *$//g' {} \\;} 23 | end 24 | -------------------------------------------------------------------------------- /Thorfile: -------------------------------------------------------------------------------- 1 | module GemHelpers 2 | 3 | def generate_gemspec 4 | $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), "lib"))) 5 | require "rack/bug" 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "rack-bug" 9 | s.version = Rack::Bug::VERSION 10 | s.author = "Bryan Helmkamp" 11 | s.email = "bryan@brynary.com" 12 | s.homepage = "http://github.com/brynary/rack-bug" 13 | s.summary = "Debugging toolbar for Rack applications implemented as middleware" 14 | # s.description = "TODO" 15 | s.rubyforge_project = "rack-bug" 16 | 17 | require "git" 18 | repo = Git.open(".") 19 | 20 | s.files = normalize_files(repo.ls_files.keys - repo.lib.ignored_files) 21 | s.test_files = normalize_files(Dir['spec/**/*.rb'] - repo.lib.ignored_files) 22 | 23 | s.has_rdoc = true 24 | s.extra_rdoc_files = %w[README.md MIT-LICENSE.txt] 25 | 26 | s.add_dependency "rack", ">= 1.0" 27 | s.add_development_dependency "webrat" 28 | s.add_development_dependency "rspec" 29 | s.add_development_dependency "sinatra" 30 | s.add_development_dependency "git" 31 | end 32 | end 33 | 34 | def normalize_files(array) 35 | # only keep files, no directories, and sort 36 | array.select do |path| 37 | File.file?(path) 38 | end.sort 39 | end 40 | 41 | # Adds extra space when outputting an array. This helps create better version 42 | # control diffs, because otherwise it is all on the same line. 43 | def prettyify_array(gemspec_ruby, array_name) 44 | gemspec_ruby.gsub(/s\.#{array_name.to_s} = \[.+?\]/) do |match| 45 | leadin, files = match[0..-2].split("[") 46 | leadin + "[\n #{files.split(",").join(",\n ")}\n ]" 47 | end 48 | end 49 | 50 | def read_gemspec 51 | @read_gemspec ||= eval(File.read("rack-bug.gemspec")) 52 | end 53 | 54 | def sh(command) 55 | puts command 56 | system command 57 | end 58 | end 59 | 60 | class Default < Thor 61 | include GemHelpers 62 | 63 | desc "gemspec", "Regenerate rack-bug.gemspec" 64 | def gemspec 65 | File.open("rack-bug.gemspec", "w") do |file| 66 | gemspec_ruby = generate_gemspec.to_ruby 67 | gemspec_ruby = prettyify_array(gemspec_ruby, :files) 68 | gemspec_ruby = prettyify_array(gemspec_ruby, :test_files) 69 | gemspec_ruby = prettyify_array(gemspec_ruby, :extra_rdoc_files) 70 | 71 | file.write gemspec_ruby 72 | end 73 | 74 | puts "Wrote gemspec to rack-bug.gemspec" 75 | read_gemspec.validate 76 | end 77 | 78 | desc "build", "Build a rack-bug gem" 79 | def build 80 | sh "gem build rack-bug.gemspec" 81 | FileUtils.mkdir_p "pkg" 82 | FileUtils.mv read_gemspec.file_name, "pkg" 83 | end 84 | 85 | desc "install", "Install the latest built gem" 86 | def install 87 | sh "gem install --local pkg/#{read_gemspec.file_name}" 88 | end 89 | 90 | desc "release", "Release the current branch to GitHub and Gemcutter" 91 | def release 92 | gemspec 93 | build 94 | Release.new.tag 95 | Release.new.gem 96 | end 97 | end 98 | 99 | class Release < Thor 100 | include GemHelpers 101 | 102 | desc "tag", "Tag the gem on the origin server" 103 | def tag 104 | release_tag = "v#{read_gemspec.version}" 105 | sh "git tag -a #{release_tag} -m 'Tagging #{release_tag}'" 106 | sh "git push origin #{release_tag}" 107 | end 108 | 109 | desc "gem", "Push the gem to Gemcutter" 110 | def gem 111 | sh "gem push pkg/#{read_gemspec.file_name}" 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/rack/bug.rb: -------------------------------------------------------------------------------- 1 | require "ipaddr" 2 | require "digest" 3 | require "rack" 4 | require "digest/sha1" 5 | require "rack/bug/autoloading" 6 | 7 | class Rack::Bug 8 | include Options 9 | 10 | VERSION = "0.3.0" 11 | 12 | class SecurityError < StandardError 13 | end 14 | 15 | def self.enable 16 | Thread.current["rack-bug.enabled"] = true 17 | end 18 | 19 | def self.disable 20 | Thread.current["rack-bug.enabled"] = false 21 | end 22 | 23 | def self.enabled? 24 | Thread.current["rack-bug.enabled"] == true 25 | end 26 | 27 | def initialize(app, options = {}, &block) 28 | @app = asset_server(app) 29 | initialize_options options 30 | instance_eval(&block) if block_given? 31 | 32 | @toolbar = Toolbar.new(RedirectInterceptor.new(@app)) 33 | end 34 | 35 | 36 | def call(env) 37 | env.replace @default_options.merge(env) 38 | @env = env 39 | @original_request = Rack::Request.new(@env) 40 | 41 | if toolbar_requested? && ip_authorized? && password_authorized? && toolbar_xhr? 42 | @toolbar.call(env) 43 | else 44 | @app.call(env) 45 | end 46 | end 47 | 48 | private 49 | 50 | def toolbar_xhr? 51 | !@original_request.xhr? || @original_request.path =~ /^\/__rack_bug__/ 52 | end 53 | 54 | def asset_server(app) 55 | RackStaticBugAvoider.new(app, Rack::Static.new(app, :urls => ["/__rack_bug__"], :root => public_path)) 56 | end 57 | 58 | def public_path 59 | ::File.expand_path(::File.dirname(__FILE__) + "/bug/public") 60 | end 61 | 62 | def toolbar_requested? 63 | @original_request.cookies["rack_bug_enabled"] 64 | end 65 | 66 | def ip_authorized? 67 | return true unless options["rack-bug.ip_masks"] 68 | 69 | options["rack-bug.ip_masks"].any? do |ip_mask| 70 | ip_mask.include?(IPAddr.new(@original_request.ip)) 71 | end 72 | end 73 | 74 | def password_authorized? 75 | return true unless options["rack-bug.password"] 76 | 77 | expected_sha = Digest::SHA1.hexdigest ["rack_bug", options["rack-bug.password"]].join(":") 78 | actual_sha = @original_request.cookies["rack_bug_password"] 79 | 80 | actual_sha == expected_sha 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/rack/bug/autoloading.rb: -------------------------------------------------------------------------------- 1 | class Rack::Bug 2 | autoload :FilteredBacktrace, "rack/bug/filtered_backtrace" 3 | autoload :Options, "rack/bug/options" 4 | autoload :Panel, "rack/bug/panel" 5 | autoload :PanelApp, "rack/bug/panel_app" 6 | autoload :ParamsSignature, "rack/bug/params_signature" 7 | autoload :RackStaticBugAvoider, "rack/bug/rack_static_bug_avoider" 8 | autoload :RedirectInterceptor, "rack/bug/redirect_interceptor" 9 | autoload :Render, "rack/bug/render" 10 | autoload :Toolbar, "rack/bug/toolbar" 11 | 12 | # Panels 13 | autoload :ActiveRecordPanel, "rack/bug/panels/active_record_panel" 14 | autoload :CachePanel, "rack/bug/panels/cache_panel" 15 | autoload :LogPanel, "rack/bug/panels/log_panel" 16 | autoload :MemoryPanel, "rack/bug/panels/memory_panel" 17 | autoload :RailsInfoPanel, "rack/bug/panels/rails_info_panel" 18 | autoload :RedisPanel, "rack/bug/panels/redis_panel" 19 | autoload :MongoPanel, "rack/bug/panels/mongo_panel" 20 | autoload :RequestVariablesPanel, "rack/bug/panels/request_variables_panel" 21 | autoload :SQLPanel, "rack/bug/panels/sql_panel" 22 | autoload :TemplatesPanel, "rack/bug/panels/templates_panel" 23 | autoload :TimerPanel, "rack/bug/panels/timer_panel" 24 | autoload :SphinxPanel, "rack/bug/panels/sphinx_panel" 25 | end 26 | -------------------------------------------------------------------------------- /lib/rack/bug/filtered_backtrace.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Bug 3 | module FilteredBacktrace 4 | 5 | def backtrace 6 | @backtrace 7 | end 8 | 9 | def has_backtrace? 10 | filtered_backtrace.any? 11 | end 12 | 13 | def filtered_backtrace 14 | @filtered_backtrace ||= @backtrace.map{|l| l.to_s.strip }.select do |line| 15 | root_for_backtrace_filtering.nil? || 16 | (line.index(root_for_backtrace_filtering) == 0) && !(line.index(root_for_backtrace_filtering("vendor")) == 0) 17 | end 18 | end 19 | 20 | def root_for_backtrace_filtering(sub_path = nil) 21 | if defined?(Rails) && Rails.respond_to?(:root) 22 | (sub_path ? Rails.root.join(sub_path) : Rails.root).to_s 23 | else 24 | root = if defined?(RAILS_ROOT) 25 | RAILS_ROOT 26 | elsif defined?(ROOT) 27 | ROOT 28 | elsif defined?(Sinatra::Application) 29 | Sinatra::Application.root 30 | else 31 | nil 32 | end 33 | sub_path ? ::File.join(root, sub_path) : root 34 | end.to_s 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/rack/bug/options.rb: -------------------------------------------------------------------------------- 1 | class Rack::Bug 2 | 3 | module Options 4 | class << self 5 | private 6 | def option_accessor(key) 7 | define_method(key) { || read_option(key) } 8 | define_method("#{key}=") { |value| write_option(key, value) } 9 | define_method("#{key}?") { || !! read_option(key) } 10 | end 11 | end 12 | 13 | option_accessor :secret_key 14 | option_accessor :ip_masks 15 | option_accessor :password 16 | option_accessor :panel_classes 17 | option_accessor :intercept_redirects 18 | 19 | # The underlying options Hash. During initialization (or outside of a 20 | # request), this is a default values Hash. During a request, this is the 21 | # Rack environment Hash. The default values Hash is merged in underneath 22 | # the Rack environment before each request is processed. 23 | def options 24 | @env || @default_options 25 | end 26 | 27 | # Set multiple options. 28 | def options=(hash={}) 29 | hash.each { |key,value| write_option(key, value) } 30 | end 31 | 32 | # Set an option. When +option+ is a Symbol, it is set in the Rack 33 | # Environment as "rack-cache.option". When +option+ is a String, it 34 | # exactly as specified. The +option+ argument may also be a Hash in 35 | # which case each key/value pair is merged into the environment as if 36 | # the #set method were called on each. 37 | def set(option, value=self, &block) 38 | if block_given? 39 | write_option option, block 40 | elsif value == self 41 | self.options = option.to_hash 42 | else 43 | write_option option, value 44 | end 45 | end 46 | 47 | private 48 | 49 | def read_option(key) 50 | options[option_name(key)] 51 | end 52 | 53 | def write_option(key, value) 54 | options[option_name(key)] = value 55 | end 56 | 57 | def option_name(key) 58 | case key 59 | when Symbol ; "rack-bug.#{key}" 60 | when String ; key 61 | else raise ArgumentError 62 | end 63 | end 64 | 65 | def initialize_options(options={}) 66 | @default_options = { 67 | 'rack-bug.ip_masks' => [IPAddr.new("127.0.0.1")], 68 | 'rack-bug.password' => nil, 69 | 'rack-bug.verbose' => nil, 70 | 'rack-bug.secret_key' => nil, 71 | 'rack-bug.intercept_redirects' => false, 72 | 'rack-bug.panels' => [], 73 | 'rack-bug.panel_classes' => [ 74 | RailsInfoPanel, 75 | TimerPanel, 76 | RequestVariablesPanel, 77 | SQLPanel, 78 | ActiveRecordPanel, 79 | CachePanel, 80 | TemplatesPanel, 81 | LogPanel, 82 | MemoryPanel 83 | ] 84 | } 85 | self.options = options 86 | end 87 | 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/rack/bug/panel.rb: -------------------------------------------------------------------------------- 1 | require "erb" 2 | 3 | module Rack 4 | class Bug 5 | 6 | # Panels are also Rack middleware 7 | class Panel 8 | include Render 9 | include ERB::Util 10 | 11 | attr_reader :request 12 | 13 | def initialize(app) 14 | if panel_app 15 | @app = Rack::Cascade.new([panel_app, app]) 16 | else 17 | @app = app 18 | end 19 | end 20 | 21 | def call(env) 22 | before(env) 23 | status, headers, body = @app.call(env) 24 | @request = Request.new(env) 25 | after(env, status, headers, body) 26 | env["rack-bug.panels"] << self 27 | return [status, headers, body] 28 | end 29 | 30 | def panel_app 31 | nil 32 | end 33 | 34 | def has_content? 35 | true 36 | end 37 | 38 | def before(env) 39 | end 40 | 41 | def after(env, status, headers, body) 42 | end 43 | 44 | def render(template) 45 | end 46 | 47 | end 48 | 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/rack/bug/panel_app.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Bug 3 | 4 | class PanelApp 5 | include Rack::Bug::Render 6 | 7 | attr_reader :request 8 | 9 | def call(env) 10 | @request = Rack::Request.new(env) 11 | dispatch 12 | end 13 | 14 | def render_template(*args) 15 | Rack::Response.new([super]).to_a 16 | end 17 | 18 | def params 19 | @request.GET 20 | end 21 | 22 | def not_found 23 | [404, {}, []] 24 | end 25 | 26 | def validate_params 27 | ParamsSignature.new(request).validate! 28 | end 29 | 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/active_record_panel.rb: -------------------------------------------------------------------------------- 1 | require "rack/bug/panels/active_record_panel/activerecord_extensions" 2 | 3 | module Rack 4 | class Bug 5 | 6 | class ActiveRecordPanel < Panel 7 | 8 | def self.record(class_name) 9 | return unless Rack::Bug.enabled? 10 | records[class_name] += 1 11 | end 12 | 13 | def self.reset 14 | Thread.current["rack.bug.active_records"] = Hash.new { 0 } 15 | end 16 | 17 | def self.records 18 | Thread.current["rack.bug.active_records"] ||= Hash.new { 0 } 19 | end 20 | 21 | def self.total 22 | records.inject(0) do |memo, (key, value)| 23 | memo + value 24 | end 25 | end 26 | 27 | def name 28 | "active_record" 29 | end 30 | 31 | def heading 32 | "#{self.class.total} AR Objects" 33 | end 34 | 35 | def content 36 | records = self.class.records.to_a.sort_by { |key, value| value }.reverse 37 | result = render_template "panels/active_record", :records => records 38 | self.class.reset 39 | result 40 | end 41 | 42 | end 43 | 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/active_record_panel/activerecord_extensions.rb: -------------------------------------------------------------------------------- 1 | if defined?(ActiveRecord) 2 | ActiveRecord::Base.class_eval do 3 | 4 | if instance_methods.include?("after_initialize") 5 | def after_initialize_with_rack_bug 6 | Rack::Bug::ActiveRecordPanel.record(self.class.base_class.name) 7 | after_initialize_without_rack_bug 8 | end 9 | 10 | alias_method_chain :after_initialize, :rack_bug 11 | else 12 | def after_initialize 13 | Rack::Bug::ActiveRecordPanel.record(self.class.base_class.name) 14 | end 15 | end 16 | 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/cache_panel.rb: -------------------------------------------------------------------------------- 1 | require "rack/bug/panels/cache_panel/memcache_extension" 2 | require "rack/bug/panels/cache_panel/dalli_extension" 3 | 4 | module Rack 5 | class Bug 6 | 7 | class CachePanel < Panel 8 | autoload :PanelApp, "rack/bug/panels/cache_panel/panel_app" 9 | autoload :Stats, "rack/bug/panels/cache_panel/stats" 10 | 11 | def self.record(method, *keys, &block) 12 | return block.call unless Rack::Bug.enabled? 13 | 14 | start_time = Time.now 15 | result = block.call 16 | total_time = Time.now - start_time 17 | hit = result.nil? ? false : true 18 | stats.record_call(method, total_time * 1_000, hit, *keys) 19 | return result 20 | end 21 | 22 | def self.reset 23 | Thread.current["rack.bug.cache"] = Stats.new 24 | end 25 | 26 | def self.stats 27 | Thread.current["rack.bug.cache"] ||= Stats.new 28 | end 29 | 30 | def panel_app 31 | PanelApp.new 32 | end 33 | 34 | def name 35 | "cache" 36 | end 37 | 38 | def heading 39 | "Cache: %.2fms (#{self.class.stats.queries.size} calls)" % self.class.stats.time 40 | end 41 | 42 | def content 43 | result = render_template "panels/cache", :stats => self.class.stats 44 | self.class.reset 45 | return result 46 | end 47 | 48 | end 49 | 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/cache_panel/dalli_extension.rb: -------------------------------------------------------------------------------- 1 | begin 2 | 3 | require 'dalli' 4 | 5 | Dalli::Client.class_eval do 6 | def perform_with_rack_bug(op, *args) 7 | Rack::Bug::CachePanel.record(op, args.first) do 8 | perform_without_rack_bug(op, *args) 9 | end 10 | end 11 | 12 | alias_method_chain :perform, :rack_bug 13 | end 14 | 15 | rescue NameError, LoadError 16 | end -------------------------------------------------------------------------------- /lib/rack/bug/panels/cache_panel/memcache_extension.rb: -------------------------------------------------------------------------------- 1 | if defined?(Memcached) 2 | Memcached.class_eval do 3 | 4 | def set_with_rack_bug(key, value, timeout=0, marshal=true) 5 | Rack::Bug::CachePanel.record(:set, key) do 6 | set_without_rack_bug(key, value, timeout, marshal) 7 | end 8 | end 9 | 10 | def add_with_rack_bug(key, value, timeout=0, marshal=true) 11 | Rack::Bug::CachePanel.record(:add, key) do 12 | add_without_rack_bug(key, value, timeout, marshal) 13 | end 14 | end 15 | 16 | def increment_with_rack_bug(key, offset=1) 17 | Rack::Bug::CachePanel.record(:incr, key) do 18 | increment_without_rack_bug(key, offset) 19 | end 20 | end 21 | 22 | def decrement_with_rack_bug(key, offset=1) 23 | Rack::Bug::CachePanel.record(:decr, key) do 24 | decrement_without_rack_bug(key, offset) 25 | end 26 | end 27 | 28 | def replace_with_rack_bug(key, value, timeout=0, marshal=true) 29 | Rack::Bug::CachePanel.record(:replace, key) do 30 | replace_without_rack_bug(key, value, timeout, marshal) 31 | end 32 | end 33 | 34 | def append_with_rack_bug(key, value) 35 | Rack::Bug::CachePanel.record(:append, key) do 36 | append_without_rack_bug(key, value) 37 | end 38 | end 39 | 40 | def prepend_with_rack_bug(key, value) 41 | Rack::Bug::CachePanel.record(:prepend, key) do 42 | prepend_without_rack_bug(key, value) 43 | end 44 | end 45 | 46 | def delete_with_rack_bug(key) 47 | Rack::Bug::CachePanel.record(:delete, key) do 48 | delete_without_rack_bug(key) 49 | end 50 | end 51 | 52 | def get_with_rack_bug(keys, marshal=true) 53 | if keys.is_a? Array 54 | Rack::Bug::CachePanel.record(:get_multi, *keys) do 55 | get_without_rack_bug(keys, marshal) 56 | end 57 | else 58 | Rack::Bug::CachePanel.record(:get, keys) do 59 | get_without_rack_bug(keys, marshal) 60 | end 61 | end 62 | end 63 | 64 | alias_method_chain :decrement, :rack_bug 65 | alias_method_chain :get, :rack_bug 66 | alias_method_chain :increment, :rack_bug 67 | alias_method_chain :set, :rack_bug 68 | alias_method_chain :add, :rack_bug 69 | alias_method_chain :replace, :rack_bug 70 | alias_method_chain :delete, :rack_bug 71 | alias_method_chain :prepend, :rack_bug 72 | alias_method_chain :append, :rack_bug 73 | end 74 | end 75 | 76 | if defined?(MemCache) 77 | MemCache.class_eval do 78 | 79 | def decr_with_rack_bug(key, amount = 1) 80 | Rack::Bug::CachePanel.record(:decr, key) do 81 | decr_without_rack_bug(key, amount) 82 | end 83 | end 84 | 85 | def get_with_rack_bug(key, raw = false) 86 | Rack::Bug::CachePanel.record(:get, key) do 87 | get_without_rack_bug(key, raw) 88 | end 89 | end 90 | 91 | def get_multi_with_rack_bug(*keys) 92 | Rack::Bug::CachePanel.record(:get_multi, *keys) do 93 | get_multi_without_rack_bug(*keys) 94 | end 95 | end 96 | 97 | def incr_with_rack_bug(key, amount = 1) 98 | Rack::Bug::CachePanel.record(:incr, key) do 99 | incr_without_rack_bug(key, amount) 100 | end 101 | end 102 | 103 | def set_with_rack_bug(key, value, expiry = 0, raw = false) 104 | Rack::Bug::CachePanel.record(:set, key) do 105 | set_without_rack_bug(key, value, expiry, raw) 106 | end 107 | end 108 | 109 | def add_with_rack_bug(key, value, expiry = 0, raw = false) 110 | Rack::Bug::CachePanel.record(:add, key) do 111 | add_without_rack_bug(key, value, expiry, raw) 112 | end 113 | end 114 | 115 | def delete_with_rack_bug(key, expiry = 0) 116 | Rack::Bug::CachePanel.record(:delete, key) do 117 | delete_without_rack_bug(key, expiry) 118 | end 119 | end 120 | 121 | alias_method_chain :decr, :rack_bug 122 | alias_method_chain :get, :rack_bug 123 | alias_method_chain :get_multi, :rack_bug 124 | alias_method_chain :incr, :rack_bug 125 | alias_method_chain :set, :rack_bug 126 | alias_method_chain :add, :rack_bug 127 | alias_method_chain :delete, :rack_bug 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/cache_panel/panel_app.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Bug 3 | class CachePanel 4 | 5 | class PanelApp < ::Rack::Bug::PanelApp 6 | 7 | def dispatch 8 | case request.path_info 9 | when "/__rack_bug__/view_cache" then view_cache 10 | when "/__rack_bug__/delete_cache" then delete_cache 11 | when "/__rack_bug__/delete_cache_list" then delete_cache_list 12 | else not_found 13 | end 14 | end 15 | 16 | def ok 17 | Rack::Response.new(["OK"]).to_a 18 | end 19 | 20 | def view_cache 21 | validate_params 22 | render_template "panels/view_cache", :key => params["key"], :value => Rails.cache.read(params["key"]) 23 | end 24 | 25 | def delete_cache 26 | validate_params 27 | raise "Rails not found... can't delete key" unless defined?(Rails) 28 | Rails.cache.delete(params["key"]) 29 | ok 30 | end 31 | 32 | def delete_cache_list 33 | validate_params 34 | raise "Rails not found... can't delete key" unless defined?(Rails) 35 | 36 | params.each do |key, value| 37 | next unless key =~ /^keys_/ 38 | Rails.cache.delete(value) 39 | end 40 | 41 | ok 42 | end 43 | 44 | end 45 | 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/cache_panel/stats.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Bug 3 | class CachePanel 4 | 5 | class Stats 6 | class Query 7 | attr_reader :method, :time, :hit, :keys 8 | 9 | def initialize(method, time, hit, keys) 10 | @method = method 11 | @time = time 12 | @hit = hit 13 | @keys = keys 14 | end 15 | 16 | def display_time 17 | "%.2fms" % time 18 | end 19 | 20 | def display_keys 21 | if keys.size == 1 22 | keys.first 23 | else 24 | keys.join(", ") 25 | end 26 | end 27 | end 28 | 29 | attr_reader :calls 30 | attr_reader :keys 31 | attr_reader :queries 32 | 33 | def initialize 34 | @queries = [] 35 | @misses = 36 | @calls = 0 37 | @time = 0.0 38 | @keys = [] 39 | end 40 | 41 | def record_call(method, time, hit, *keys) 42 | @queries << Query.new(method, time, hit, keys) 43 | @calls += 1 44 | @time += time 45 | @keys += keys 46 | end 47 | 48 | def display_time 49 | "%.2fms" % time 50 | end 51 | 52 | def time 53 | @queries.inject(0) do |memo, query| 54 | memo + query.time 55 | end 56 | end 57 | 58 | def gets 59 | count_queries(:get) 60 | end 61 | 62 | def sets 63 | count_queries(:set) 64 | end 65 | 66 | def deletes 67 | count_queries(:delete) 68 | end 69 | 70 | def get_multis 71 | count_queries(:get_multi) 72 | end 73 | 74 | def hits 75 | @queries.select { |q| [:get, :get_multi].include?(q.method) && q.hit }.size 76 | end 77 | 78 | def misses 79 | @queries.select { |q| [:get, :get_multi].include?(q.method) && !q.hit }.size 80 | end 81 | 82 | def count_queries(method) 83 | @queries.select { |q| q.method == method }.size 84 | end 85 | 86 | def queries_to_param 87 | params = {} 88 | @queries.each_with_index do |query, index| 89 | params["keys_#{index}"] = query.keys.first 90 | end 91 | params 92 | end 93 | end 94 | 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/log_panel.rb: -------------------------------------------------------------------------------- 1 | require "rack/bug/panels/log_panel/logger_extension" 2 | 3 | module Rack 4 | class Bug 5 | 6 | class LogPanel < Panel 7 | class LogEntry 8 | attr_reader :level, :time, :message 9 | LEVELS = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'] 10 | 11 | def initialize(level, time, message) 12 | @level = LEVELS[level] 13 | @time = time 14 | @message = message 15 | end 16 | 17 | def cleaned_message 18 | @message.to_s.gsub(/\e\[[;\d]+m/, "") 19 | end 20 | end 21 | 22 | def self.record(message, log_level) 23 | return unless Rack::Bug.enabled? 24 | return unless message 25 | Thread.current["rack.bug.logs.start"] ||= Time.now 26 | timestamp = ((Time.now - Thread.current["rack.bug.logs.start"]) * 1000).to_i 27 | logs << LogEntry.new(log_level, timestamp, message) 28 | end 29 | 30 | def self.reset 31 | Thread.current["rack.bug.logs"] = [] 32 | Thread.current["rack.bug.logs.start"] = nil 33 | end 34 | 35 | def self.logs 36 | Thread.current["rack.bug.logs"] ||= [] 37 | end 38 | 39 | def name 40 | "log" 41 | end 42 | 43 | def heading 44 | "Log" 45 | end 46 | 47 | def content 48 | result = render_template "panels/log", :logs => self.class.logs 49 | self.class.reset 50 | return result 51 | end 52 | 53 | end 54 | 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/log_panel/logger_extension.rb: -------------------------------------------------------------------------------- 1 | module Rack::Bug::LoggerExtension 2 | def self.included(target) 3 | target.send :alias_method, :add_without_rack_bug, :add 4 | target.send :alias_method, :add, :add_with_rack_bug 5 | end 6 | 7 | def add_with_rack_bug(*args, &block) 8 | logger_return = add_without_rack_bug(*args, &block) 9 | logged_message = logger_return 10 | logged_message = args[1] || args[2] unless logged_message.is_a?(String) 11 | Rack::Bug::LogPanel.record(logged_message, args[0]) 12 | return logger_return 13 | end 14 | end 15 | 16 | if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger 17 | logger = Rails.logger 18 | elsif defined?(LOGGER) 19 | logger = LOGGER 20 | end 21 | 22 | if logger 23 | logger.class.send :include, Rack::Bug::LoggerExtension 24 | end -------------------------------------------------------------------------------- /lib/rack/bug/panels/memory_panel.rb: -------------------------------------------------------------------------------- 1 | # 2 | module Rack 3 | class Bug 4 | 5 | class MemoryPanel < Panel 6 | 7 | def before(env) 8 | @original_memory = `ps -o rss= -p #{$$}`.to_i 9 | end 10 | 11 | def after(env, status, headers, body) 12 | @total_memory = `ps -o rss= -p #{$$}`.to_i 13 | @memory_increase = @total_memory - @original_memory 14 | end 15 | 16 | def heading 17 | "#{@memory_increase} KB Δ, #{@total_memory} KB total" 18 | end 19 | 20 | def has_content? 21 | false 22 | end 23 | 24 | end 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/mongo_panel.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Bug 3 | 4 | class MongoPanel < Panel 5 | require "rack/bug/panels/mongo_panel/mongo_extension" 6 | 7 | autoload :Stats, "rack/bug/panels/mongo_panel/stats" 8 | 9 | def self.record(command, &block) 10 | return block.call unless Rack::Bug.enabled? 11 | 12 | start_time = Time.now 13 | result = block.call 14 | total_time = Time.now - start_time 15 | stats.record_call(total_time * 1_000, command) 16 | return result 17 | end 18 | 19 | def self.reset 20 | Thread.current["rack.bug.mongo"] = Stats.new 21 | end 22 | 23 | def self.stats 24 | Thread.current["rack.bug.mongo"] ||= Stats.new 25 | end 26 | 27 | def name 28 | "mongo" 29 | end 30 | 31 | def heading 32 | "Mongo: %.2fms (#{self.class.stats.queries.size} calls)" % self.class.stats.time 33 | end 34 | 35 | def content 36 | result = render_template "panels/mongo", :stats => self.class.stats 37 | self.class.reset 38 | return result 39 | end 40 | 41 | end 42 | 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/mongo_panel/mongo_extension.rb: -------------------------------------------------------------------------------- 1 | require 'mongo' 2 | 3 | if defined?(Mongo) 4 | Mongo::Connection.class_eval do 5 | 6 | def send_message_with_rack_bug(operation, message, log_message=nil) 7 | Rack::Bug::MongoPanel.record(log_message || message) do 8 | send_message_without_rack_bug(operation, message, log_message) 9 | end 10 | end 11 | alias_method_chain :send_message, :rack_bug 12 | 13 | def send_message_with_safe_check_with_rack_bug(operation, message, db_name, log_message=nil, last_error_params=false) 14 | Rack::Bug::MongoPanel.record(log_message || message) do 15 | send_message_with_safe_check_without_rack_bug(operation, message, db_name, log_message, last_error_params) 16 | end 17 | end 18 | alias_method_chain :send_message_with_safe_check, :rack_bug 19 | 20 | def receive_message_with_rack_bug(operation, message, log_message=nil, socket=nil) 21 | Rack::Bug::MongoPanel.record(log_message || "A logger must be configured to catch receive message commands") do 22 | receive_message_without_rack_bug(operation, message, log_message, socket) 23 | end 24 | end 25 | alias_method_chain :receive_message, :rack_bug 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/mongo_panel/stats.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Bug 3 | class MongoPanel 4 | 5 | class Stats 6 | class Query 7 | attr_reader :time 8 | attr_reader :command 9 | 10 | def initialize(time, command) 11 | @time = time 12 | @command = command 13 | end 14 | 15 | def display_time 16 | "%.2fms" % time 17 | end 18 | end 19 | 20 | attr_reader :calls 21 | attr_reader :queries 22 | 23 | def initialize 24 | @queries = [] 25 | @calls = 0 26 | @time = 0.0 27 | end 28 | 29 | def record_call(time, command) 30 | @queries << Query.new(time, command) 31 | @calls += 1 32 | @time += time 33 | end 34 | 35 | def display_time 36 | "%.2fms" % time 37 | end 38 | 39 | def time 40 | @queries.inject(0) do |memo, query| 41 | memo + query.time 42 | end 43 | end 44 | end 45 | 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/rails_info_panel.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Bug 3 | 4 | class RailsInfoPanel < Panel 5 | 6 | def name 7 | "rails_info" 8 | end 9 | 10 | def heading 11 | return unless (defined?(Rails) && defined?(Rails::Info)) 12 | "Rails #{Rails.version}" 13 | end 14 | 15 | def content 16 | return unless (defined?(Rails) && defined?(Rails::Info)) 17 | render_template "panels/rails_info" 18 | end 19 | 20 | end 21 | 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/redis_panel.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Bug 3 | 4 | class RedisPanel < Panel 5 | require "rack/bug/panels/redis_panel/redis_extension" 6 | 7 | autoload :Stats, "rack/bug/panels/redis_panel/stats" 8 | 9 | def self.record(redis_command_args, backtrace, &block) 10 | return block.call unless Rack::Bug.enabled? 11 | 12 | start_time = Time.now 13 | result = block.call 14 | total_time = Time.now - start_time 15 | stats.record_call(total_time * 1_000, redis_command_args, backtrace) 16 | return result 17 | end 18 | 19 | def self.reset 20 | Thread.current["rack.bug.redis"] = Stats.new 21 | end 22 | 23 | def self.stats 24 | Thread.current["rack.bug.redis"] ||= Stats.new 25 | end 26 | 27 | def name 28 | "redis" 29 | end 30 | 31 | def heading 32 | "Redis: %.2fms (#{self.class.stats.queries.size} calls)" % self.class.stats.time 33 | end 34 | 35 | def content 36 | result = render_template "panels/redis", :stats => self.class.stats 37 | self.class.reset 38 | return result 39 | end 40 | 41 | end 42 | 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/redis_panel/redis_extension.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | 3 | if defined?(Redis) 4 | Redis.class_eval do 5 | if Redis.methods.include?('call_command') # older versions of redis-rb 6 | def call_command_with_rack_bug(*argv) 7 | Rack::Bug::RedisPanel.record(argv, Kernel.caller) do 8 | call_command_without_rack_bug(*argv) 9 | end 10 | end 11 | 12 | alias_method_chain :call_command, :rack_bug 13 | 14 | elsif defined?(Redis::Client) # newer versions of redis-rb 15 | 16 | Redis::Client.class_eval do 17 | def call_with_rack_bug(*argv) 18 | Rack::Bug::RedisPanel.record(argv, Kernel.caller) do 19 | call_without_rack_bug(*argv) 20 | end 21 | end 22 | 23 | alias_method_chain :call, :rack_bug 24 | 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/redis_panel/stats.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Bug 3 | class RedisPanel 4 | 5 | class Stats 6 | class Query 7 | include Rack::Bug::FilteredBacktrace 8 | 9 | attr_reader :time 10 | attr_reader :command 11 | 12 | def initialize(time, command_args, backtrace) 13 | @time = time 14 | @command = command_args.inspect 15 | @backtrace = backtrace 16 | end 17 | 18 | def display_time 19 | "%.2fms" % time 20 | end 21 | end 22 | 23 | attr_reader :calls 24 | attr_reader :queries 25 | 26 | def initialize 27 | @queries = [] 28 | @calls = 0 29 | @time = 0.0 30 | end 31 | 32 | def record_call(time, command_args, backtrace) 33 | @queries << Query.new(time, command_args, backtrace) 34 | @calls += 1 35 | @time += time 36 | end 37 | 38 | def display_time 39 | "%.2fms" % time 40 | end 41 | 42 | def time 43 | @queries.inject(0) do |memo, query| 44 | memo + query.time 45 | end 46 | end 47 | 48 | end 49 | 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/request_variables_panel.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Bug 3 | 4 | class RequestVariablesPanel < Panel 5 | 6 | def name 7 | "request_variables" 8 | end 9 | 10 | def before(env) 11 | @env = env 12 | end 13 | 14 | def heading 15 | "Rack Env" 16 | end 17 | 18 | def content 19 | sections = {} 20 | sections["GET"] = sort(@request.GET) if @request.GET.any? 21 | sections["POST"] = sort(@request.GET) if @request.POST.any? 22 | sections["Session"] = sort(@request.env["rack.session"]) if @request.env["rack.session"] && @request.env["rack.session"].any? 23 | sections["Cookies"] = sort(@request.env["rack.request.cookie_hash"]) if @request.env["rack.request.cookie_hash"] && @request.env["rack.request.cookie_hash"].any? 24 | server, rack = split_and_filter_env(@env) 25 | sections["SERVER VARIABLES"] = sort(server) 26 | sections["Rack ENV"] = sort(rack) 27 | render_template "panels/request_variables", :sections => sections 28 | end 29 | 30 | private 31 | def sort(hash) 32 | hash.sort_by { |k, v| k.to_s } 33 | end 34 | 35 | def split_and_filter_env(env) 36 | server, rack = {}, {} 37 | env.each do |k,v| 38 | if k.index("rack.") == 0 39 | rack[k] = v 40 | elsif k.index("rack-bug.") == 0 41 | #don't output the rack-bug variables - especially secret_key 42 | else 43 | server[k] = v 44 | end 45 | end 46 | return server, rack 47 | end 48 | 49 | end 50 | 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/sphinx_panel.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Bug 3 | 4 | class SphinxPanel < Panel 5 | require "rack/bug/panels/sphinx_panel/sphinx_extension" 6 | 7 | autoload :Stats, "rack/bug/panels/sphinx_panel/stats" 8 | 9 | def self.record(*sphinx_command_args, &block) 10 | return block.call unless Rack::Bug.enabled? 11 | 12 | start_time = Time.now 13 | result = block.call 14 | total_time = Time.now - start_time 15 | stats.record_call(total_time * 1_000, sphinx_command_args) 16 | return result 17 | end 18 | 19 | def self.reset 20 | Thread.current["rack.bug.sphinx"] = Stats.new 21 | end 22 | 23 | def self.stats 24 | Thread.current["rack.bug.sphinx"] ||= Stats.new 25 | end 26 | 27 | def name 28 | "sphinx" 29 | end 30 | 31 | def heading 32 | "Sphinx: %.2fms (#{self.class.stats.queries.size} calls)" % self.class.stats.time 33 | end 34 | 35 | def content 36 | result = render_template "panels/sphinx", :stats => self.class.stats 37 | self.class.reset 38 | return result 39 | end 40 | 41 | end 42 | 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/sphinx_panel/sphinx_extension.rb: -------------------------------------------------------------------------------- 1 | require 'riddle' 2 | 3 | if defined?(Riddle) 4 | Riddle::Client.class_eval do 5 | def request_with_rack_bug(command, messages) 6 | Rack::Bug::SphinxPanel.record(command, messages) do 7 | request_without_rack_bug(command, messages) 8 | end 9 | end 10 | 11 | alias_method_chain :request, :rack_bug 12 | end 13 | end 14 | 15 | if defined?(Sphinx::Client) 16 | Sphinx::Client.class_eval do 17 | def PerformRequest_with_rack_bug(command, request, additional = nil) 18 | Rack::Bug::SphinxPanel.record(command, request) do 19 | PerformRequest_without_rack_bug(command, request, additional) 20 | end 21 | end 22 | 23 | alias_method_chain :PerformRequest, :rack_bug 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/sphinx_panel/stats.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Bug 3 | class SphinxPanel 4 | 5 | class Stats 6 | class Query 7 | attr_reader :time 8 | attr_reader :command 9 | 10 | def initialize(time, *command_args) 11 | @time = time 12 | if command_args.flatten.first == :search 13 | @command = "search: " + decode_message(command_args.first.flatten.last).collect{|k,v| "#{k} => #{v}"}.join(", ") 14 | else 15 | @command = command_args.flatten.first.to_s + ": No more info is available for this Sphinx request type" 16 | end 17 | end 18 | 19 | def display_time 20 | "%.2fms" % time 21 | end 22 | 23 | def decode_message(m) 24 | @m = m.clone 25 | params = ActiveSupport::OrderedHash.new 26 | 27 | params[:offset] = consume_int 28 | params[:limit] = consume_int 29 | params[:match_mode] = consume_int 30 | params[:rank_mode] = consume_int 31 | params[:sort_mode] = consume_int 32 | params[:sort_by] = consume_string 33 | params[:query] = consume_string 34 | wl = consume_int 35 | weights = [] 36 | wl.times do weights << consume_int end 37 | params[:weights] = weights 38 | 39 | params[:index] = consume_string 40 | 41 | consume_string 42 | 43 | params[:id_range] = [consume_64int, consume_64int] 44 | params 45 | end 46 | 47 | def consume_int 48 | i = @m.unpack("N").first 49 | @m = @m.slice(4, @m.length - 4) 50 | i 51 | end 52 | 53 | def consume_64int 54 | i = @m.unpack("NN").first 55 | @m = @m.slice(8, @m.length - 8) 56 | i 57 | end 58 | 59 | def consume_string 60 | len = consume_int 61 | s = @m.slice(0, len) 62 | @m = @m.slice(len, @m.length - len) 63 | s 64 | end 65 | end 66 | 67 | attr_reader :calls 68 | attr_reader :queries 69 | 70 | def initialize 71 | @queries = [] 72 | @calls = 0 73 | @time = 0.0 74 | end 75 | 76 | def record_call(time, *command_args) 77 | 78 | @queries << Query.new(time, command_args) 79 | @calls += 1 80 | @time += time 81 | end 82 | 83 | def display_time 84 | "%.2fms" % time 85 | end 86 | 87 | def time 88 | @queries.inject(0) do |memo, query| 89 | memo + query.time 90 | end 91 | end 92 | end 93 | 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/sql_panel.rb: -------------------------------------------------------------------------------- 1 | require "digest" 2 | 3 | module Rack 4 | class Bug 5 | 6 | class SQLPanel < Panel 7 | require "rack/bug/panels/sql_panel/sql_extension" 8 | 9 | autoload :PanelApp, "rack/bug/panels/sql_panel/panel_app" 10 | autoload :Query, "rack/bug/panels/sql_panel/query" 11 | 12 | def panel_app 13 | PanelApp.new 14 | end 15 | 16 | def self.record(sql, backtrace = [], &block) 17 | return block.call unless Rack::Bug.enabled? 18 | 19 | start_time = Time.now 20 | result = block.call 21 | queries << Query.new(sql, Time.now - start_time, backtrace) 22 | 23 | return result 24 | end 25 | 26 | def self.reset 27 | Thread.current["rack.test.queries"] = [] 28 | end 29 | 30 | def self.queries 31 | Thread.current["rack.test.queries"] ||= [] 32 | end 33 | 34 | def self.total_time 35 | (queries.inject(0) { |memo, query| memo + query.time}) * 1_000 36 | end 37 | 38 | def name 39 | "sql" 40 | end 41 | 42 | def heading 43 | "#{self.class.queries.size} Queries (%.2fms)" % self.class.total_time 44 | end 45 | 46 | def content 47 | result = render_template "panels/sql", :queries => self.class.queries 48 | self.class.reset 49 | return result 50 | end 51 | 52 | end 53 | 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/sql_panel/panel_app.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Bug 3 | class SQLPanel 4 | 5 | class PanelApp < ::Rack::Bug::PanelApp 6 | 7 | def dispatch 8 | case request.path_info 9 | when "/__rack_bug__/explain_sql" then explain_sql 10 | when "/__rack_bug__/profile_sql" then profile_sql 11 | when "/__rack_bug__/execute_sql" then execute_sql 12 | else not_found 13 | end 14 | end 15 | 16 | def explain_sql 17 | validate_params 18 | query = Query.new(params["query"], params["time"].to_f) 19 | render_template "panels/explain_sql", :result => query.explain, :query => query.sql, :time => query.time 20 | end 21 | 22 | def profile_sql 23 | validate_params 24 | query = Query.new(params["query"], params["time"].to_f) 25 | render_template "panels/profile_sql", :result => query.profile, :query => query.sql, :time => query.time 26 | end 27 | 28 | def execute_sql 29 | validate_params 30 | query = Query.new(params["query"], params["time"].to_f) 31 | render_template "panels/execute_sql", :result => query.execute, :query => query.sql, :time => query.time 32 | end 33 | 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/sql_panel/query.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Bug 3 | class SQLPanel 4 | 5 | class Query 6 | include Rack::Bug::FilteredBacktrace 7 | 8 | attr_reader :sql 9 | attr_reader :time 10 | 11 | def initialize(sql, time, backtrace = []) 12 | @sql = sql 13 | @time = time 14 | @backtrace = backtrace 15 | end 16 | 17 | def human_time 18 | "%.2fms" % (@time * 1_000) 19 | end 20 | 21 | def inspectable? 22 | sql.strip =~ /^SELECT /i 23 | end 24 | 25 | def with_profiling 26 | self.class.execute("SET PROFILING=1") 27 | result = yield 28 | self.class.execute("SET PROFILING=0") 29 | return result 30 | end 31 | 32 | def explain 33 | self.class.execute "EXPLAIN #{@sql}" 34 | end 35 | 36 | def profile 37 | with_profiling do 38 | execute 39 | self.class.execute <<-SQL 40 | SELECT * 41 | FROM information_schema.profiling 42 | WHERE query_id = (SELECT query_id FROM information_schema.profiling ORDER BY query_id DESC LIMIT 1) 43 | SQL 44 | end 45 | end 46 | 47 | def execute 48 | self.class.execute(@sql) 49 | end 50 | 51 | def valid_hash?(secret_key, possible_hash) 52 | hash = Digest::SHA1.hexdigest [secret_key, @sql].join(":") 53 | possible_hash == hash 54 | end 55 | 56 | def self.execute(sql) 57 | ActiveRecord::Base.connection.execute(sql) 58 | end 59 | end 60 | 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/sql_panel/sql_extension.rb: -------------------------------------------------------------------------------- 1 | if defined?(ActiveRecord) && defined?(ActiveRecord::ConnectionAdapters) 2 | ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do 3 | def log_with_rack_bug(sql, name, &block) 4 | Rack::Bug::SQLPanel.record(sql, Kernel.caller) do 5 | log_without_rack_bug(sql, name, &block) 6 | end 7 | end 8 | 9 | alias_method_chain :log, :rack_bug 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/templates_panel.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Bug 3 | 4 | class TemplatesPanel < Panel 5 | require "rack/bug/panels/templates_panel/actionview_extension" 6 | 7 | autoload :Trace, "rack/bug/panels/templates_panel/trace" 8 | autoload :Rendering, "rack/bug/panels/templates_panel/rendering" 9 | 10 | def self.record(template) 11 | return yield unless Rack::Bug.enabled? 12 | 13 | template_trace.start(template) 14 | begin 15 | result = yield 16 | ensure 17 | template_trace.finished(template) 18 | end 19 | return result 20 | end 21 | 22 | def self.reset 23 | Thread.current["rack.bug.template_trace"] = Trace.new 24 | end 25 | 26 | def self.template_trace 27 | Thread.current["rack.bug.template_trace"] ||= Trace.new 28 | end 29 | 30 | def name 31 | "templates" 32 | end 33 | 34 | def heading 35 | "Templates: %.2fms" % (self.class.template_trace.total_time * 1_000) 36 | end 37 | 38 | def content 39 | result = render_template "panels/templates", :template_trace => self.class.template_trace 40 | self.class.reset 41 | return result 42 | end 43 | 44 | end 45 | 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/templates_panel/actionview_extension.rb: -------------------------------------------------------------------------------- 1 | if defined?(ActionView) && defined?(ActionView::Template) 2 | ActionView::Template.class_eval do 3 | 4 | def render_template_with_rack_bug(*args, &block) 5 | Rack::Bug::TemplatesPanel.record(path_without_format_and_extension) do 6 | render_template_without_rack_bug(*args, &block) 7 | end 8 | end 9 | 10 | alias_method_chain :render_template, :rack_bug 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/rack/bug/panels/templates_panel/rendering.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Bug 3 | class TemplatesPanel 4 | 5 | class Rendering 6 | attr_accessor :name 7 | attr_accessor :start_time 8 | attr_accessor :end_time 9 | attr_accessor :parent 10 | attr_reader :children 11 | 12 | 13 | def initialize(name) 14 | @name = name 15 | @children = [] 16 | end 17 | 18 | def add(rendering) 19 | @children << rendering 20 | rendering.parent = self 21 | end 22 | 23 | def time 24 | @end_time - @start_time 25 | end 26 | 27 | def exclusive_time 28 | time - child_time 29 | end 30 | 31 | def child_time 32 | children.inject(0.0) { |memo, c| memo + c.time } 33 | end 34 | 35 | def time_summary 36 | if children.any? 37 | "%.2fms, %.2f exclusive" % [time * 1_000, exclusive_time * 1_000] 38 | else 39 | "%.2fms" % (time * 1_000) 40 | end 41 | end 42 | def html 43 | <<-HTML 44 |
#{name} (#{time_summary})
46 | 47 | #{children_html} 48 |Location: <%= redirect_to %>
8 | 9 |
10 | Rack::Bug has intercepted a redirect to the above URL for debug viewing
11 | purposes. You can click the above link to continue with the redirect as
12 | normal. If you'd like to disable this feature, set the Rack::Bug
13 | internal_redirects
option to false
.
14 |
" 47 | 48 | @response["Content-Length"] = full_body.size.to_s 49 | 50 | # Ensure that browser does 51 | @response["Etag"] = "" 52 | @response["Cache-Control"] = "no-cache" 53 | 54 | @response.body = [full_body] 55 | end 56 | 57 | def render 58 | render_template("toolbar", :panels => @env["rack-bug.panels"].reverse) 59 | end 60 | 61 | end 62 | 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/rack/bug/views/error.html.erb: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 |
There was an error within Rack::Bug!
15 |17 | -------------------------------------------------------------------------------- /lib/rack/bug/views/panels/active_record.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Count | 6 |Class | 7 |
---|---|
<%= count %> | 13 |<%= class_name %> | 14 |
-------------------------------------------------------------------------------- /lib/rack/bug/views/panels/cache.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Total Calls | 15 |<%= stats.calls %> | 16 | 17 |Total Time | 18 |<%= stats.display_time %> | 19 | 20 |Hits | 21 |<%= stats.hits %> | 22 | 23 |Misses | 24 |<%= stats.misses %> | 25 |
---|---|---|---|---|---|---|---|
gets | 28 |<%= stats.gets %> | 29 | 30 |sets | 31 |<%= stats.sets %> | 32 | 33 |deletes | 34 |<%= stats.deletes %> | 35 | 36 |get_multis | 37 |<%= stats.get_multis %> | 38 |
40 | 41 | <% if stats.queries.any? %> 42 |
43 |
Time (ms) | 47 |Type | 48 |Parameters | 49 |Function | 50 |51 | 52 | Delete All 53 | 54 | | 55 |
---|---|---|---|---|
<%= query.display_time %> | 62 |<%= query.method %> | 63 |<%= query.display_keys %> | 64 |65 | | 66 | query.keys.first) %>" class="remote_call">View | 67 | query.keys.first) %>" class="rb_delete_cache">Delete 68 | | 69 |
74 | <% end %> 75 | 76 | -------------------------------------------------------------------------------- /lib/rack/bug/views/panels/execute_sql.html.erb: -------------------------------------------------------------------------------- 1 | « Back 2 | 3 |
4 | 5 |
<%=h query %>
12 | 13 |
<%= field.upcase %> | 19 | <% end %> 20 | <% else %> 21 | <% result.fetch_fields.each do |field| %> 22 |<%= field.name.upcase %> | 23 | <% end %> 24 | <% end %> 25 |
---|---|
<%= value %> | 33 | <% end %> 34 |
39 | -------------------------------------------------------------------------------- /lib/rack/bug/views/panels/explain_sql.html.erb: -------------------------------------------------------------------------------- 1 | « Back 2 | 3 |
4 | 5 |
<%=h query %>
12 | 13 |
<%= field.upcase %> | 19 | <% end %> 20 | <% else %> 21 | <% result.fetch_fields.each do |field| %> 22 |<%= field.name.upcase %> | 23 | <% end %> 24 | <% end %> 25 |
---|---|
<%= value %> | 33 | <% end %> 34 |
39 | -------------------------------------------------------------------------------- /lib/rack/bug/views/panels/log.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Level | 6 |Time | 7 |Message | 8 |
---|---|---|
<%= entry.level %> | 15 |<%= entry.time %>ms | 16 |<%= entry.cleaned_message %> | 17 |
-------------------------------------------------------------------------------- /lib/rack/bug/views/panels/mongo.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Total Calls | 5 |<%= stats.calls %> | 6 | 7 |Total Time | 8 |<%= stats.display_time %> | 9 |
---|
11 | 12 | <% if stats.queries.any? %> 13 |
14 |
Time (ms) | 18 |Command | 19 |
---|---|
<%= query.display_time %> | 26 |<%= query.command %> | 27 |
32 | <% end %> 33 | -------------------------------------------------------------------------------- /lib/rack/bug/views/panels/profile_sql.html.erb: -------------------------------------------------------------------------------- 1 | « Back 2 | 3 |
4 | 5 |
<%=h query %>
12 | 13 |
<%= field.upcase %> | 19 | <% end %> 20 | <% else %> 21 | <% result.fetch_fields.each do |field| %> 22 |<%= field.name.upcase %> | 23 | <% end %> 24 | <% end %> 25 |
---|---|
<%= value %> | 33 | <% end %> 34 |
39 | -------------------------------------------------------------------------------- /lib/rack/bug/views/panels/rails_info.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Variable | 6 |Value | 7 |
---|---|
<%=h key %> | 14 |<%=h val %> |
15 |
-------------------------------------------------------------------------------- /lib/rack/bug/views/panels/redis.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Total Calls | 5 |<%= stats.calls %> | 6 | 7 |Total Time | 8 |<%= stats.display_time %> | 9 |
---|
11 | 12 | <% if stats.queries.any? %> 13 |
14 |
Time (ms) | 18 |Command | 19 |20 | |
---|---|---|
<%= query.display_time %> | 27 |<%= query.command %> | 28 |<%= "Show Backtrace" if query.has_backtrace? %> | 29 |
46 | <% end %> 47 | -------------------------------------------------------------------------------- /lib/rack/bug/views/panels/request_variables.html.erb: -------------------------------------------------------------------------------- 1 | <% ["GET", "POST", "Session", "Cookies", "SERVER VARIABLES", "Rack ENV"].each do |header| 2 | next unless sections.has_key?(header) 3 | %> 4 |
5 |
Variable | 9 |Value | 10 |
---|---|
<%=h key %> | 17 |
18 | <% if val.is_a?(Hash) %>
19 | <%=h val.inspect %>
20 | <% else %>
21 | <%=h val.to_s %>
22 | <% end %>
23 | |
24 |
29 | <% end %> -------------------------------------------------------------------------------- /lib/rack/bug/views/panels/sphinx.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Total Calls | 5 |<%= stats.calls %> | 6 | 7 |Total Time | 8 |<%= stats.display_time %> | 9 |
---|
11 | 12 | <% if stats.queries.any? %> 13 |
14 |
Time (ms) | 18 |Command | 19 |
---|---|
<%= query.display_time %> | 26 |<%= query.command %> | 27 |
32 | <% end %> -------------------------------------------------------------------------------- /lib/rack/bug/views/panels/sql.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Time (ms) | 6 |Query | 7 |8 | | 9 | |
---|---|---|---|
<%= query.human_time %> | 16 |
17 | <%= query.sql %>
18 |
19 | <% if query.has_backtrace? %>
20 | Show Backtrace
21 | <% end %>
22 | <% if query.inspectable? && query.has_backtrace? %>
23 | |
24 | <% end %>
25 | <% if query.inspectable? %>
26 | query.sql, "time" => query.time) %>" class="remote_call">SELECT |
27 | query.sql, "time" => query.time) %>" class="remote_call">EXPLAIN |
28 | query.sql, "time" => query.time) %>" class="remote_call">Profile
29 | <% end %>
30 |
31 |
36 | |
37 |
-------------------------------------------------------------------------------- /lib/rack/bug/views/panels/templates.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 |
8 | -------------------------------------------------------------------------------- /lib/rack/bug/views/panels/timer.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Key | 6 |Value | 7 |
---|---|
<%=h key %> | 14 |<%=h val %> | 15 |
-------------------------------------------------------------------------------- /lib/rack/bug/views/panels/view_cache.html.erb: -------------------------------------------------------------------------------- 1 | « Back 2 | 3 |
4 | 5 |
<%=h key %>
12 | 13 |
14 | <% if value.is_a?(String )%> 15 | <%=h value %> 16 | <% else %> 17 | <%=h value.inspect %> 18 | <% end %> 19 |
-------------------------------------------------------------------------------- /lib/rack/bug/views/redirect.html.erb: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
16 |