require "erb" require "rack/insight/logging" require 'rack/insight/database' require 'rack/insight/instrumentation' require 'rack/insight/render' module Rack::Insight # Panels are also Rack middleware class Panel include ERB::Util include Rack::Insight::Logging include Rack::Insight::Render include Rack::Insight::Database::RequestDataClient include Rack::Insight::Instrumentation::Client attr_reader :request # has table defaults to true for panels. def self.has_table self.has_table.nil? ? true : self.class.table.nil? end class << self include Rack::Insight::Logging # This will allow the following: # p = Panel.new # p.class.is_probing = true include Rack::Insight::Instrumentation::EigenClient include Rack::Insight::Database::EigenClient def file_index return @file_index ||= Hash.new do |h,k| h[k] = [] end end def panel_exclusion return @panel_exclusion ||= [] end def from_file(rel_path) old_rel, Thread::current['rack-panel_file'] = Thread::current['rack-panel_file'], rel_path num_load_paths_to_check = Rack::Insight::Config.config[:panel_load_paths].length Rack::Insight::Config.config[:panel_load_paths].each_with_index do |load_path, index| begin require File::join(load_path, rel_path) break # once found rescue LoadError => e # TODO: If probes are defined for this panel, instantiate a magic panel # if self.has_custom_probes? if !verbose(:high) && (index + 1) == num_load_paths_to_check # You have failed me for the last time! warn "Rack::Insight #{e.class} while attempting to load '#{rel_path}' from :panel_load_paths #{Rack::Insight::Config.config[:panel_load_paths].inspect}." elsif verbose(:high) warn "Rack::Insight #{e.class} #{e.message} while attempting to load '#{rel_path}' from :panel_load_paths #{Rack::Insight::Config.config[:panel_load_paths].inspect} (just checked: #{load_path})." end end end return (file_index[rel_path] - panel_exclusion) ensure Thread::current['rack-panel_file'] = old_rel end def set_sub_class_template_root(sub_class, path) sub_class.template_root = path end def current_panel_file(sub) file_name = nil matched_line = nil caller.each do |line| # First make sure we are not matching rack-insight's own panel class, which will be in the caller stack, # and which may match some custom load path added (try adding 'rack' as a custom load path!) # .*panel because the panels that ship with rack-insight also do not need custom template roots. next if line =~ /rack-insight.*\/lib\/rack\/insight\/.*panel.rb:/ Rack::Insight::Config.config[:panel_load_paths].each do |load_path| regex = %r{^[^:]*#{load_path}/([^:]*)\.rb:} md = regex.match line file_name = md[1] unless md.nil? matched_line = line unless file_name.nil? break unless file_name.nil? end break unless file_name.nil? end set_sub_class_template_root(sub, File.dirname(matched_line.split(':')[0])) if matched_line.respond_to?(:split) return Thread::current['rack-panel_file'] || file_name end def inherited(sub) if filename = current_panel_file(sub) logger.debug("panel inherited by #{sub.inspect} with template_root: #{sub.template_root}") if verbose(:high) Panel::file_index[filename] << sub else warn "Rack::Insight::Panel inherited by #{sub.name} outside rack-insight's :panel_load_paths. Discarded. Configured panel load paths are: #{Rack::Insight::Config.config[:panel_load_paths].inspect}" end end def excluded(klass = nil) Panel::panel_exclusion << klass || self end end def initialize(app) if panel_app #XXX use mappings @app = Rack::Cascade.new([panel_app, app]) else @app = app end # User has explicitly declared what classes/methods to probe: # Rack::Insight::Config.configure do |config| # config[:panel_configs][:log] = {:probes => {'Logger' => [:instance, :add] } } # # OR EQUIVALENTLY # config[:panel_configs][:log] = {:probes => ['Logger', :instance, :add] } # end panel_name = self.underscored_name.to_sym if self.has_custom_probes?(panel_name) # Both formats are valid and must be supported #config[:panel_configs][:log] = {:probes => {'Logger' => [:instance, :add]}} #config[:panel_configs][:log] = {:probes => ['Logger', :instance, :add]} custom_probes = Rack::Insight::Config.config[:panel_configs][panel_name][:probes] if custom_probes.kind_of?(Hash) probe(self) do custom_probes.each do |klass, method_probes| instrument klass do self.send("#{method_probes[0]}_probe", *(method_probes[1..-1])) end end end elsif custom_probes.kind_of?(Array) && custom_probes.length >=3 probe(self) do custom_probes.each do |probe| klass = probe.shift probe_type = probe.shift instrument klass do self.send("#{probe_type}_probe", *probe) end end end else raise "Expected Rack::Insight::Config.config[:panel_configs][#{panel_name}][:probes] to be a kind of Hash or an Array with length >= 3, but is a #{Rack::Insight::Config.config[:panel_configs][self.as_sym][:probes].class}" end end # Setup a table for the panel unless # 1. self.has_table = false has been set for the Panel class # 2. class instance variable @has_table has been set to false # 3. table_setup has already been called by the sub class' initializer if !has_table? table_setup(self.name) end end def inspect "#{self.underscored_name} Magic:#{self.bool_prop(:is_magic?)} Table:#{self.bool_prop(:has_table?)} Probe:#{self.bool_prop(:is_probing?)} Custom:#{self.bool_prop(:has_custom_probes?)}" rescue "XXX inspect failed" end def bool_prop(prop) self.send(prop) ? 'Y' : 'N' end def call(env) @env = env logger.debug{ "Before call: #{self.name}" } if verbose(:debug) before(env) status, headers, body = @app.call(env) @request = Rack::Request.new(env) logger.debug{ "After call: #{self.name}" } if verbose(:debug) after(env, status, headers, body) env["rack-insight.panels"] << self return [status, headers, body] end def panel_app nil end def self.panel_mappings {} end def has_table? !!self.class.has_table end def is_magic? !!self.class.is_magic end def has_content? true end def is_probing? !!self.class.is_probing end def has_custom_probes?(panel_name = self.underscored_name.to_sym) Rack::Insight::Config.config[:panel_configs][panel_name].respond_to?(:[]) && !Rack::Insight::Config.config[:panel_configs][panel_name][:probes].nil? end # The name informs the table name, and the panel_configs hash among other things. # Override in subclass panels if you want a custom name def name self.underscored_name end # Mostly stolen from Rails' ActiveSupport' underscore method: # See activesupport/lib/active_support/inflector/methods.rb, line 77 # HTTPClientPanel => http_client # LogPanel => log # ActiveRecordPanel => active_record def underscored_name(word = self.class.to_s) @underscored_name ||= begin words = word.dup.split('::') word = words.last if word == 'Panel' word = words[-2] # Panel class is Panel... and this won't do. end # This bit from rails probably isn't needed here, and wouldn't work anyways. #word.gsub!(/(?:([A-Za-z\d])|^)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" } word.gsub!(/Panel$/,'') word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') word.tr!("-", "_") word.downcase! word end end def camelized_name(str = self.underscored_name) str.split('_').map {|w| w.capitalize}.join end def heading_for_request(number) if !self.has_table? heading else num = count(number) if num.kind_of?(Numeric) if num == 0 heading else "#{self.camelized_name} (#{num})" end else heading end end rescue StandardError => exception handle_error_for('heading_for_request', exception) end def content_for_request(number) logger.info("Rack::Insight is using default content_for_request for #{self.class}") if verbose(:med) if !self.has_table? logger.info("#{self.class} is being used without a table") if verbose(:med) content elsif self.is_probing? # Checking probed because we only get here when the subclass panel hasn't overridden this method invocations = retrieve(number) if invocations.length > 0 logger.info("Rack::Insight is using #{self.is_magic? ? 'magic' : 'default'} content for #{self.class}, which is probed")# if verbose(:med) render_template 'magic_panel', :magic_insights => invocations, :name => self.camelized_name else logger.info("Rack::Insight has no data for #{self.is_magic? ? 'magic' : 'default'} content for #{self.class}, which is probed") render_template 'no_data', :name => self.camelized_name end else content end rescue StandardError => exception handle_error_for('content_for_request', exception) end def heading self.camelized_name rescue StandardError => exception handle_error_for('heading', exception) end def content logger.info("Rack::Insight is using default content for #{self.class}") if verbose(:med) render_template 'no_content', :name => self.camelized_name rescue StandardError => exception handle_error_for('content', exception) end def handle_error_for(method_name, exception) nom = self.name rescue "xxx" msg = ["#{self.class}##{method_name} failed","#{exception.class}: #{exception.message}"] + exception.backtrace logger.error(msg.join("\n")) # return HTML "Error in #{nom} " end # Override in subclasses. # This is to make magic classes work. def after_detect(method_call, timing, args, result) if self.is_magic? && self.has_table? && self.is_probing? store(@env, Rack::Insight::DefaultInvocation.new(method_call.method.to_s, timing, args, result, method_call.backtrace[2..-1])) end end def before(env) end def after(env, status, headers, body) end def render(template) end end end