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
