lib/sentry/rails/log_subscribers/active_job_subscriber.rb



# frozen_string_literal: true

require "sentry/rails/log_subscriber"
require "sentry/rails/log_subscribers/parameter_filter"

module Sentry
  module Rails
    module LogSubscribers
      # LogSubscriber for ActiveJob events that captures background job execution
      # and logs them using Sentry's structured logging system.
      #
      # This subscriber captures various ActiveJob events including job execution,
      # enqueueing, retries, and failures with relevant job information.
      #
      # @example Usage
      #   # Enable structured logging for ActiveJob
      #   Sentry.init do |config|
      #     config.enable_logs = true
      #     config.rails.structured_logging = true
      #     config.rails.structured_logging.subscribers = { active_job: Sentry::Rails::LogSubscribers::ActiveJobSubscriber }
      #   end
      class ActiveJobSubscriber < Sentry::Rails::LogSubscriber
        include ParameterFilter

        # Handle perform.active_job events
        #
        # @param event [ActiveSupport::Notifications::Event] The job performance event
        def perform(event)
          return unless Sentry.initialized?

          job = event.payload[:job]
          duration = duration_ms(event)

          attributes = {
            job_class: job.class.name,
            job_id: job.job_id,
            queue_name: job.queue_name,
            duration_ms: duration,
            executions: job.executions,
            priority: job.priority
          }

          attributes[:adapter] = job.class.queue_adapter.class.name

          if job.scheduled_at
            attributes[:scheduled_at] = job.scheduled_at.iso8601
            attributes[:delay_ms] = ((Time.current - job.scheduled_at) * 1000).round(2)
          end

          if Sentry.configuration.send_default_pii && job.arguments.present?
            filtered_args = filter_sensitive_arguments(job.arguments)
            attributes[:arguments] = filtered_args unless filtered_args.empty?
          end

          message = "Job performed: #{job.class.name}"

          log_structured_event(
            message: message,
            level: :info,
            attributes: attributes
          )
        end

        # Handle enqueue.active_job events
        #
        # @param event [ActiveSupport::Notifications::Event] The job enqueue event
        def enqueue(event)
          return unless Sentry.initialized?

          job = event.payload[:job]

          attributes = {
            job_class: job.class.name,
            job_id: job.job_id,
            queue_name: job.queue_name,
            priority: job.priority
          }

          attributes[:adapter] = job.class.queue_adapter.class.name if job.class.respond_to?(:queue_adapter)

          if job.scheduled_at
            attributes[:scheduled_at] = job.scheduled_at.iso8601
            attributes[:delay_seconds] = (job.scheduled_at - Time.current).round(2)
          end

          message = "Job enqueued: #{job.class.name}"

          log_structured_event(
            message: message,
            level: :info,
            attributes: attributes
          )
        end

        def retry_stopped(event)
          return unless Sentry.initialized?

          job = event.payload[:job]
          error = event.payload[:error]

          attributes = {
            job_class: job.class.name,
            job_id: job.job_id,
            queue_name: job.queue_name,
            executions: job.executions,
            error_class: error.class.name,
            error_message: error.message
          }

          message = "Job retry stopped: #{job.class.name}"

          log_structured_event(
            message: message,
            level: :error,
            attributes: attributes
          )
        end

        def discard(event)
          return unless Sentry.initialized?

          job = event.payload[:job]
          error = event.payload[:error]

          attributes = {
            job_class: job.class.name,
            job_id: job.job_id,
            queue_name: job.queue_name,
            executions: job.executions
          }

          attributes[:error_class] = error.class.name if error
          attributes[:error_message] = error.message if error

          message = "Job discarded: #{job.class.name}"

          log_structured_event(
            message: message,
            level: :warn,
            attributes: attributes
          )
        end

        private

        def filter_sensitive_arguments(arguments)
          return [] unless arguments.is_a?(Array)

          arguments.map do |arg|
            case arg
            when Hash
              filter_sensitive_params(arg)
            when String
              arg.length > 100 ? "[FILTERED: #{arg.length} chars]" : arg
            else
              arg
            end
          end
        end
      end
    end
  end
end