require "time" require "socket" module PlainApm module EventAttributes SOURCES_WITH_EXTRA_ATTRIBUTES = %w[ action_controller action_mailer active_job exception ].freeze # FIXME: This is duplicated for ErrorReporter IGNORED_EXCEPTIONS = [ "Sidekiq::JobRetry::Skip", # Sidekiq uses exceptions for control flow. "ActionController::RoutingError" # Rails unmapped route, raised before the request hits the app. ].freeze def attributes_from_exception(e, context, error_source) name = "exception" return [name, nil] if IGNORED_EXCEPTIONS.include?(e.class.name) attrs = context_attributes attrs[:class] = e.class.name attrs[:message] = e.message attrs[:backtrace] = e.backtrace attrs[:event_time] = now attrs[:event_utc_time] = utc_now if error_source attrs[:event_source] = error_source end if context[:controller]&.is_a?(ActionController::Base) attrs[:controller] = context[:controller].class.name end if context[:job]&.is_a?(ActiveJob::Base) attrs[:job_class] = context[:job].class.name attrs[:queue_name] = context[:job].queue_name end if context[:env] attrs[:params] = context.dig(:env, "action_dispatch.request.parameters") attrs[:action] = context.dig(:env, "action_dispatch.request.parameters", :action) attrs[:controller] = context.dig(:env, "action_controller.instance")&.class.name end # https://bugs.ruby-lang.org/issues/19197 root_cause = e root_cause = root_cause.cause while root_cause.cause if root_cause != e attrs[:root_cause_class] = root_cause.class.name attrs[:root_cause_message] = root_cause.message attrs[:root_cause_backtrace] = root_cause.backtrace loc = source_location(root_cause.backtrace) if !loc.nil? attrs[:root_cause_location] = loc end end loc = source_location(e.backtrace) if !loc.nil? attrs[:source_location] = loc end add_trace_attributes(attrs) [name, attrs] end def attributes_from_notification(event) name, source = *event.name.split(".") loc = source_location attrs = context_attributes attrs[:source] = source attrs[:name] = name attrs[:allocations] = event.allocations attrs[:thread_cpu_time] = event.cpu_time attrs[:event_time] = event.time attrs[:duration] = event.duration if event.respond_to?(:utc_time) attrs[:event_utc_time] = event.utc_time end if event.respond_to?(:gc_time) attrs[:gc_time] = event.gc_time end if event.respond_to?(:gc_major_count) attrs[:gc_major_count] = event.gc_major_count end if event.respond_to?(:gc_minor_count) attrs[:gc_minor_count] = event.gc_minor_count end if event.respond_to?(:thread_allocations) attrs[:thread_allocations] = event.thread_allocations end if !loc.nil? attrs[:source_location] = loc end add_trace_attributes(attrs) [name, attrs] end private # TODO: There is a perf cost in Rubies for Process.pid until 3.3, so it # might be a good idea to cache this. See # https://bugs.ruby-lang.org/issues/19443 for the feature. def add_trace_attributes(attrs) attrs[:thread_id] = Thread.current.object_id attrs[:collected_at] = utc_now attrs[:pid] = Process.pid attrs[:version] = PlainApm::VERSION attrs[:hostname] = cached_attributes[:hostname] attrs[:revision] = cached_attributes[:revision] end def cached_attributes @cached_attributes ||= { hostname: Socket.gethostname, revision: PlainApm::DeployTracking.revision } end ## # Context contains the trace ID (which comes from either # HTTP_X_REQUEST_ID header, the deserialized job, # or is generated by the trace_id middleware). # It can also carry user inserted app data. def context_attributes if defined?(PlainApm::Extensions::Context) PlainApm::Extensions::Context.current.dup else {} end end def source_location(backtrace = nil) return if self.class.rails_root.nil? root_length = self.class.rails_root.length (backtrace || caller).each do |frame| if frame.start_with?(self.class.rails_root) path = frame[(root_length + 1)..-1] return path if path.start_with?("app") || path.start_with?("lib") end end nil end def now Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) end def utc_now Time.now.to_f end def self.included(other) other.class_eval do def self.rails_root return @rails_root if defined?(@rails_root) @rails_root = (defined?(Rails) && Rails.root.to_s.present?) ? Rails.root.to_s.freeze : nil end end end end end