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