Home > Enterprise >  Use ActiveSupport Around-Callbacks to execute code (tracing/logging) around when my services are inv
Use ActiveSupport Around-Callbacks to execute code (tracing/logging) around when my services are inv

Time:04-16

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)

CodePudding user response:

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
  • Related