Search code examples
ruby-on-railscallbacktraceactivesupport

Use ActiveSupport Around-Callbacks to execute code (tracing/logging) around when my services are invoked?


In my app/services directory, I have a bunch of services that each have an initialize method and a public perform method that takes no arguments.

A simple example service to show the structure might look like this:

module Users
  class CreateService

  def initialize(name)
    @name = name
  end

  def perform
    # code to create the user
  end
end

My goal is to add tracing to my application so that I can have traces for when each of my services in invoked, without needing to significantly modify the code of each of my services. In the end, I am hoping to make it so that when any of my services has its perform() method called, traces are automatically generated.

I believe this should be possible using ActiveSupport::Callbacks, similarly to how ActiveJob lets us define, say, around_perform callbacks for our jobs. I attempted to create a concern that I could include in my services which would instrument the desired behavior

module Traceable
  extend ActiveSupport::Concern
  include ActiveSupport::Callbacks

  included do
    include ActiveSupport::Callbacks

    define_callbacks :perform

    set_callback :perform, :around, lambda { |r, block|
      puts "start the trace"
      result = block.call
      puts "end the trace"
    }
  end
end

However, after including this concern, the callback I'd like to define (uniform for all services) still isn't getting called. How can I make this work? Thank you!

References: https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html https://api.rubyonrails.org/classes/ActiveSupport/Callbacks.html https://edgeapi.rubyonrails.org/classes/ActiveJob/Callbacks/ClassMethods.html#method-i-around_perform (view the source code for the around_perform class method)


Solution

  • The cleanest way would be to make use of prepended.

    It'll look like this:

    module Traceable
      extend ActiveSupport::Concern
    
      prepended do
        include ActiveSupport::Callbacks
    
        define_callbacks :trace
    
        set_callback :trace, :around do |_r, block|
          puts "start the trace"
          block.call
          puts "end the trace"
        end
      end
    
      def perform
        run_callbacks :trace do
          super
        end
      end
    end
    
    module Users
      class CreateService
        prepend Traceable
    
        attr_accessor :name
    
        def initialize(name)
          @name = name
        end
    
        def perform
          # code to create the user
          puts "perform"
        end
      end
    end
    

    The link has more details. The prepend in the service means that Users::CreateService.perform will first look for its definition in the Traceable module. That allows Traceable to wrap perform in the callback(s).

    Since we're using prepend instead of include in the service, we need to call define_callbacks and set_callback inside of ActiveSupport::Concern's prepended block.

    If you prepend the Traceable module in your service, you don't really need callbacks or concerns at all. The Traceable module could just be this, and it would have the same outcome:

    module Traceable
      def perform
        puts "before"
        super
        puts "after"
      end
    end
    
    module Users
      class CreateService
        prepend Traceable
        ...
      end
    end