module BuilderApm module Methods class Instrumenter def initialize(root_path: Rails.root.to_s) @root_path = root_path @call_times = {} end def start @trace = setup_trace @trace.enable end def stop @trace.disable unless @trace.nil? end def setup_trace me = self TracePoint.new(:call, :return) do |tp| me.process_trace_point(tp) if me.valid_trace_point?(tp) end end def valid_trace_point?(tp) !Thread.current[:request_id].nil? && tp.path.start_with?(@root_path) end def process_trace_point(tp) if tp.event == :call process_call_event(tp) elsif tp.event == :return process_return_event(tp) end end private def process_call_event(tp) method_id = "#{tp.defined_class}##{tp.method_id}" @call_times[method_id] = Process.clock_gettime(Process::CLOCK_MONOTONIC) caller_info = caller_locations(4,1).first calling_file_path = caller_info.absolute_path calling_line_number = caller_info.lineno method_call = { method: method_id, method_line: "#{tp.path.gsub(@root_path, '')}:#{tp.lineno}", triggering_line: "#{calling_file_path.gsub(@root_path, '')}:#{calling_line_number}", children: [], start_time: Time.now.to_f * 1000, sql_events: [] } (Thread.current[:stack] ||= []).push(method_call) end def process_return_event(tp) method_id = "#{tp.defined_class}##{tp.method_id}" if @call_times.key?(method_id) elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @call_times[method_id] elapsed_time_in_ms = (elapsed_time * 1000).round(3) @call_times.delete(method_id) method_call = (Thread.current[:stack] ||= []).pop method_call[:end_time] = Time.now.to_f * 1000 method_call[:duration] = elapsed_time_in_ms if Thread.current[:stack]&.any? Thread.current[:stack].last[:children].push(method_call) else Thread.current[:stack].push(method_call) end end end end end end