require "logger" require "socket" require "etc" module RorVsWild class Agent def self.default_config { api_url: "https://www.rorvswild.com/api/v1", ignore_exceptions: default_ignored_exceptions, ignore_requests: [], ignore_plugins: [], ignore_jobs: [], } end def self.default_ignored_exceptions if defined?(Rails) ActionDispatch::ExceptionWrapper.rescue_responses.keys else [] end end attr_reader :config, :locator, :client, :queue def initialize(config) @config = self.class.default_config.merge(config) load_features @client = Client.new(@config) @queue = config[:queue] || Queue.new(client) @locator = RorVsWild::Locator.new Host.load_config(config) Deployment.load_config(config) RorVsWild.logger.debug("Start RorVsWild #{RorVsWild::VERSION}") setup_plugins cleanup_data end def load_features features = config[:features] || [] RorVsWild.logger.info("Server metrics are now monitored enabled by default") if features.include?("server_metrics") end def setup_plugins for name in RorVsWild::Plugin.constants next if config[:ignore_plugins] && config[:ignore_plugins].include?(name.to_s) if (plugin = RorVsWild::Plugin.const_get(name)).respond_to?(:setup) RorVsWild.logger.debug("Setup RorVsWild::Plugin::#{name}") plugin.setup end end end def measure_code(code) measure_block(code) { eval(code) } end def measure_block(name = nil, kind = "code".freeze, &block) current_data ? measure_section(name, kind: kind, &block) : measure_job(name, &block) end def measure_method(method) return if method.name.end_with?("_measured_by_rorvswild") if method.is_a?(Method) method_full_name = [method.receiver, method.name].join(".") # Method => class method else method_full_name = [method.owner, method.name].join("#") # UnboundMethod => instance method end method_alias = :"#{method.name}_measured_by_rorvswild" return if method.owner.method_defined?(method_alias) method.owner.alias_method(method_alias, method.name) method_file, method_line = method.source_location method_file = locator.relative_path(File.expand_path(method_file)) method.owner.define_method(method.name) do |*args| section = Section.start section.file = method_file section.line = method_line section.commands << method_full_name result = send(method_alias, *args) Section.stop result end end def measure_section(name, kind: "code", &block) return block.call unless current_data begin RorVsWild::Section.start do |section| section.commands << name section.kind = kind end block.call ensure RorVsWild::Section.stop end end def measure_job(name, parameters: nil, &block) return measure_section(name, &block) if current_data # For recursive jobs return block.call if ignored_job?(name) initialize_data[:name] = name begin block.call rescue Exception => ex push_exception(ex, parameters: parameters, job: {name: name}) raise ensure gc = Section.stop_gc_timing(current_data[:gc_section]) current_data[:sections] << gc if gc.calls > 0 current_data[:runtime] = RorVsWild.clock_milliseconds - current_data[:started_at] queue_job end end def start_request current_data || initialize_data end def stop_request return unless data = current_data gc = Section.stop_gc_timing(data[:gc_section]) data[:sections] << gc if gc.calls > 0 && gc.total_ms > 0 data[:runtime] = RorVsWild.clock_milliseconds - current_data[:started_at] queue_request end def catch_error(context = nil, &block) begin block.call rescue Exception => ex record_error(ex, context) ex end end def record_error(exception, context = nil) queue_error(exception_to_hash(exception, context)) if !ignored_exception?(exception) end def push_exception(exception, options = nil) return if ignored_exception?(exception) return unless current_data current_data[:error] = exception_to_hash(exception) current_data[:error].merge!(options) if options current_data[:error] end def merge_error_context(hash) self.error_context = error_context ? error_context.merge(hash) : hash end def error_context current_data[:error_context] if current_data end def error_context=(hash) current_data[:error_context] = hash if current_data end def send_server_timing=(boolean) current_data[:send_server_timing] = boolean if current_data end def current_data Thread.current[:rorvswild_data] end def add_section(section) return unless current_data[:sections] if sibling = current_data[:sections].find { |s| s.sibling?(section) } sibling.merge(section) else current_data[:sections] << section end end def ignored_request?(name) config[:ignore_requests].any? { |str_or_regex| str_or_regex === name } end def ignored_job?(name) config[:ignore_jobs].any? { |str_or_regex| str_or_regex === name } end def ignored_exception?(exception) return false unless config[:ignore_exceptions] config[:ignore_exceptions].any? { |str_or_regex| str_or_regex === exception.class.to_s } end ####################### ### Private methods ### ####################### private def initialize_data Thread.current[:rorvswild_data] = { started_at: RorVsWild.clock_milliseconds, gc_section: Section.start_gc_timing, environment: Host.to_h, section_stack: [], sections: [], } end def cleanup_data result = Thread.current[:rorvswild_data] Thread.current[:rorvswild_data] = nil result end def queue_request (data = cleanup_data) && data[:name] && queue.push_request(data) data end def queue_job queue.push_job(cleanup_data) end def queue_error(hash) queue.push_error(hash) end def exception_to_hash(exception, context = nil) file, line = locator.find_most_relevant_file_and_line_from_exception(exception) context = context ? error_context.merge(context) : error_context if error_context { line: line.to_i, file: locator.relative_path(file), message: exception.message[0,1_000_000], backtrace: exception.backtrace || ["No backtrace"], exception: exception.class.to_s, context: context, environment: Host.to_h, } end end end