# encoding: utf-8 require 'rack' require 'rack/request' require 'rack/response' require 'rack/file' require 'one_apm/support/collection_helper' require 'one_apm/rack/middleware_base' require 'one_apm/rack/middleware_wrapper' require 'one_apm/transaction/transaction_sample' require 'one_apm/transaction/transaction_analysis' require 'one_apm/rack/developer_mode/helper' require 'one_apm/collector/support/shell_poller' module OneApm module Rack class DeveloperMode < MiddlewareBase VIEW_PATH = File.expand_path('../developer_mode/views/', __FILE__) include OneApm::DeveloperModeHelper def traced_call(env) @req = ::Rack::Request.new(env) @start_memory_used = current_memory_used return dup._call(env) if /^\/oneapm/ =~ @req.path_info set_profiled status, headers, body = @app.call(env) if status == 200 && headers['Content-Type'] =~ /text\/html/ result = inject_profiler(env, status, headers, body) return result if result end return [status,headers,body] end def self.profiled? @profiled end def self.profiled=(profiled) @profiled = profiled end protected def _call(env) OneApm::Manager.ignore_transaction @rendered = false case @req.path_info when /assets/ ::Rack::File.new(VIEW_PATH).call(env) when /index/ index when /threads/ threads when /reset/ reset when /show_sample_summary/ show_sample_data when /show_sample_detail/ show_sample_data when /show_sample_sql/ show_sample_data when /show_sample_profile/ show_sample_profile when /explain_sql/ explain_sql when /^\/oneapm\/?$/ index else @app.call(env) end end private def set_profiled DeveloperMode.profiled = params['p'] == 'on' end def request_path @request_path ||= begin path = @req.path.gsub('/', '-') path.slice!(0) path end end def inject_profiler(env, status, headers, body) # mini profiler is meddling with stuff, we can not cache cause we will get incorrect data # Rack::ETag has already inserted some nonesense in the chain content_type = headers['Content-Type'] if content_type =~ /text\/html/ body = get_and_inject_profile(body) response = ::Rack::Response.new([], status, headers) if String === body response.write body else body.each { |fragment| response.write fragment } end body.close if body.respond_to? :close response.finish else nil end end def get_and_inject_profile body inject_page!(body, stylesheets, :postion => :head) inject_page!(body, javascripts(body)) inject_page!(body, slide_templates) body end def javascripts body script = "" script_files = ["javascript/jquery.min.js", "javascript/layer.js", "javascript/functions.js" ] script_files.shift if body.body[0] =~ /jquery(.min)?/ rescue false script_files.each do |sf| script << "\r\n" end script end def stylesheets stylesheets = [] stylesheets << "" stylesheets << "" stylesheets.join("\r\n") end def slide_templates template = read_script_file "#{VIEW_PATH}/oneapm/slide.tmpl" samples = "" duration = current_duration memory_used = current_memory_used - @start_memory_used samples = "
  • #{(duration * 1000).round(2)} ms
  • " template.gsub(/\{samples\}/, samples) end def get_transaction_samples return get_samples if last_sample_id <= 0 index = get_samples.find_index{|sample|sample.sample_id == last_sample_id} index.nil? ? get_samples : get_samples[0...index] end def current_sample_guid current_transaction = OneApm::TransactionState.tl_get.current_transaction current_transaction.guid end def last_sample_id Thread.current[:last_sample_id].to_i end def last_sample_id= sample_id Thread.current[:last_sample_id] = sample_id.to_i end def inject_page! body, script, options = {:postion => :body} if String === body inject!(body, script, options) else body.each { |fragment| inject!(fragment, script, options) } end end def read_script_file file_path IO.read(::File.expand_path(file_path, ::File.dirname(__FILE__))) end def inject!(fragment, script, options = {}) if options[:postion] == :head && fragment.match(/<\/head>/i) regex = /<\/head>/i close_tag = '' elsif options[:postion] == :body && fragment.match(/<\/body>/i) regex = /<\/body>/i close_tag = '' else return fragment end matches = fragment.scan(regex).length index = 1 fragment.gsub!(regex) do if index < matches index += 1 close_tag else if script.respond_to?(:encoding) && script.respond_to?(:force_encoding) (script + close_tag).force_encoding(fragment.encoding) else script + close_tag end end end end def index get_samples render(:index) end def reset OneApm::Manager.agent.transaction_sampler.reset! OneApm::Manager.agent.sql_sampler.reset! ::Rack::Response.new{|r| r.redirect('/oneapm/')}.finish end def explain_sql get_segment return render(:sample_not_found) unless @sample @sql = @segment[:sql] @trace = @segment[:backtrace] if OneApm::Manager.agent.record_sql == :obfuscated @obfuscated_sql = @segment.obfuscated_sql end _headers, explanations = @segment.explain_sql if explanations @explanation = explanations if !@explanation.blank? if @explanation.first.length < OneApm::MYSQL_EXPLAIN_COLUMNS.length @row_headers = nil else @row_headers = OneApm::MYSQL_EXPLAIN_COLUMNS end end end render(:explain_sql) end def threads render(:threads) end def render(view, layout=true) add_rack_array = true if view.is_a? Hash layout = false if view[:object] # object *is* used here, as it is capture in the binding below object = view[:object] end if view[:collection] return view[:collection].map do |obj| render({:partial => view[:partial], :object => obj}) end.join(' ') end if view[:partial] add_rack_array = false view = "_#{view[:partial]}" end end binding = Proc.new {}.binding if layout body = render_with_layout(view) do render_without_layout(view, binding) end else body = render_without_layout(view, binding) end if add_rack_array ::Rack::Response.new(body, 200, {'Content-Type' => 'text/html'}).finish else body end end # You have to call this with a block - the contents returned from # that block are interpolated into the layout def render_with_layout(view) body = ERB.new(File.read(File.join(VIEW_PATH, 'oneapm/layout.html.erb'))) body.result(Proc.new {}.binding) end # you have to pass a binding to this (a proc) so that ERB can have # access to helper functions and local variables def render_without_layout(view, binding) ERB.new(File.read(File.join(VIEW_PATH, 'oneapm', view.to_s + '.html.erb')), nil, nil, 'frobnitz').result(binding) end def content_tag(tag, contents, opts={}) opt_values = opts.map {|k, v| "#{k}=\"#{v}\"" }.join(' ') "<#{tag} #{opt_values}>#{contents}" end def sample @sample || @samples[0] end def params @req.params end def segment @segment end def show_sample_profile get_sample return render(:sample_not_found) unless @sample sort_method = params['sort'] || :total_time @profile_options = {:min_percent => 0.5, :sort_method => sort_method.to_sym} render(:show_sample_profile, false) end def show_sample_data get_sample return render(:sample_not_found) unless @sample controller_metric = @sample.transaction_name @sample_controller_name = controller_metric @sql_segments = @sample.sql_segments if params['d'] @sql_segments.sort!{|a,b| b.duration <=> a.duration } end render(:show_sample) end def get_samples @samples = OneApm::Manager.agent.transaction_sampler.dev_mode_sample_buffer.samples.select do |sample| sample.params[:path] != nil end return @samples = @samples.sort_by(&:duration).reverse if params['h'] return @samples = @samples.sort{|x,y| x.params[:uri] <=> y.params[:uri]} if params['u'] @samples = @samples.reverse end def get_sample get_samples @sample = @samples.select{|sample| sample.sample_id == params['id'].to_i || sample.guid == params['guid']}.first end def get_segment get_sample return unless @sample segment_id = params['segment'].to_i @segment = @sample.find_segment(segment_id) end def current_duration Time.now.to_f - OneApm::TransactionState.tl_get.transaction_sample_builder.sample_start end def current_memory_used @memory_sampler ||= OneApm::Collector::Samplers::MemorySampler.new @shell_poller ||= OneApm::Collector::ShellPoller.new @memory_sampler.set_poller(@shell_poller) @memory_sampler.get_sample end end end end