interceptor v0.2.0 Interceptor View Source
The Interceptor library allows you to intercept function calls, by configuring
the interception functions and using the Interceptor.intercept/1 macro.
Create a module with a get_intercept_config/0 function that returns the
interception configuration map.
defmodule Interception.Config do
def get_intercept_config, do: %{
{Intercepted, :abc, 1} => [
before: {MyInterceptor, :intercept_before, 1},
after: {MyInterceptor, :intercept_after, 2},
on_success: {MyInterceptor, :intercept_on_success, 3},
on_error: {MyInterceptor, :intercept_on_error, 3}
# there's also a `wrapper` callback available!
]
}
end
Point to the previous configuration module in your configuration:
# [...]
config :interceptor,
configuration: Interception.Config
Define your interceptor module:
defmodule MyInterceptor do
def intercept_before(mfa),
do: IO.puts "Intercepted #{inspect(mfa)} before it started."
def intercept_after(mfa, result),
do: IO.puts "Intercepted #{inspect(mfa)} after it completed. Its result: #{inspect(result)}"
def intercept_on_success(mfa, result, _start_timestamp),
do: IO.puts "Intercepted #{inspect(mfa)} after it completed successfully. Its result: #{inspect(result)}"
def intercept_on_error(mfa, error, _start_timestamp),
do: IO.puts "Intercepted #{inspect(mfa)} after it raised an error. Here's the error: #{inspect(error)}"
end
In the module that you want to intercept (in our case, Intercepted), place the
functions that you want to intercept inside an Interceptor.intercept/1 block.
Notice that if your functions are placed out of this block or if they don’t have a
corresponding interceptor configuration, they won’t be intercepted. In the following
example, the not_intercepted/3 function can’t be intercepted because it isn’t enclosed
in the Interceptor.intercept/1 block.
defmodule Intercepted do
require Interceptor, as: I
I.intercept do
def abc(x), do: "Got #{inspect(x)}"
end
def not_intercepted(f, g, h), do: f+g+h
end
Now when you run your code, whenever the Intercepted.abc/1 function is
called, it will be intercepted before it starts and after it completes.
In the previous example, we defined four callbacks: one before callback, that
will be called before the intercepted function starts; one after callback, that
will be called after the intercepted function completes. And we also defined the
on_success and on_error callbacks, that will be called when the
Intercepted.abc/1 function completes successfully or raises any error,
respectively.
If none of the previous callbacks suits your needs, you can use the wrapper
callback. This way, the intercepted function will be wrapped in a lambda and
passed to your callback function.
Note: When you use a wrapper callback, you can’t use any other callback,
i.e., the before, after, on_success and on_error callbacks can’t be
used for a function that is already being intercepted by a wrapper callback.
If you try so, an exception in compile-time will be raised.
Note 2: When you use the wrapper callback, it’s the responsibility of the
callback function to invoke the lambda and return the result. If you don’t
return the result from your callback, the return value of the intercepted
function will be whatever value your wrapper callback function returns.
Note 3: You can intercept private functions in exactly the same way you intercept
public functions. You just need to configure the callbacks that should be invoked for
the given private function, and the private function definition needs to be enclosed in
an Interceptor.intercept/1 macro.
Possible callbacks
before- The callback function that you use to intercept your function will be passed the MFA ({intercepted_module, intercepted_function, intercepted_args}) of the intercepted function, hence it needs to receive one argument. E.g.:
defmodule BeforeInterceptor do
def called_before_your_function({module, function, args}) do
...
end
end
after- The callback function that you use to intercept your function will be passed the MFA ({intercepted_module, intercepted_function, intercepted_args}) of the intercepted function and its result, hence it needs to receive two arguments. E.g.:
defmodule AfterInterceptor do
def called_after_your_function({module, function, args}, result) do
...
end
end
on_success- The callback function that you use to intercept your function on success will be passed the MFA ({intercepted_module, intercepted_function, intercepted_args}) of the intercepted function, its success result and the start timestamp (in microseconds, obtained with:os.system_time(:microsecond)), hence it needs to receive three arguments. E.g.:
defmodule SuccessInterceptor do
def called_when_your_function_completes_successfully(
{module, function, args}, result, start_timestamp) do
...
end
end
on_error- The callback function that you use to intercept your function on error will be passed the MFA ({intercepted_module, intercepted_function, intercepted_args}) of the intercepted function, the raised error and the start timestamp (in microseconds, obtained with:os.system_time(:microsecond)), hence it needs to receive three arguments. E.g.:
defmodule ErrorInterceptor do
def called_when_your_function_raises_an_error(
{module, function, args}, error, start_timestamp) do
...
end
end
wrapper- The callback function that you use to intercept your function will be passed the MFA ({intercepted_module, intercepted_function, intercepted_args}) of the intercepted function and its body wrapped in a lambda, hence it needs to receive two argument. E.g.:
defmodule WrapperInterceptor do
def called_instead_of_your_function(
{module, function, args}, intercepted_function_lambda) do
# do something with the result, or measure how long the lambda call took
result = intercepted_function_lambda.()
result
end
end
Streamlined configuration
If you think defining a get_intercept_config/0 function on the configuration module or
using the {module, function, arity} format is too verbose, you can use the
Interceptor.Configurator that will allow you to use its intercept/2 macro and the
"Module.function/arity" streamlined format.
Using the Configurator and the new streamlined format, the previous configuration
would become:
defmodule Interception.Config do
use Interceptor.Configurator
intercept "Intercepted.abc/1",
before: "MyInterceptor.intercept_before/1",
after: "MyInterceptor.intercept_after/2"
on_success: "MyInterceptor.intercept_on_success/3",
on_error: "MyInterceptor.intercept_on_error/3"
# there's also a `wrapper` callback available!
intercept "OtherModule.another_function/2",
on_success: "OtherInterceptor.success_callback/3"
# ...
end
The Configurator is defining the needed get_intercept_config/0 for you, and
converting those string MFAs into tuple-based MFAs. If you want to intercept another
function, it’s just a matter of adding other intercept
"OtherModule.another_function/2", ... entry.
Intercept configuration on the intercepted module
If you don’t want to place the intercept configuration on the application configuration
file, you can set it directly on the intercepted module, just add use Interceptor,
config: <config_module>, instead of requiring the Interceptor module. Using the
previous Intercepted module as an example:
defmodule Intercepted do
use Interceptor, config: Interception.Config
Interceptor.intercept do
def abc(x), do: "Got #{inspect(x)}"
end
def not_intercepted(f, g, h), do: f+g+h
end
Note: If the configuration you set on the intercepted module overlaps with a
configuration set on the application configuration file, the former will take
precedence, i.e., if both the intercepted module configuration and the application
configuration set the rules to intercept the Intercepted.abc/1 function, the former
will prevail, overriding the latter.
Instead of pointing to the intercept configuration module, you may also pass the
intercept configuration directly via the config keyword. E.g:
defmodule Intercepted do
use Interceptor, config: %{
"Intercepted.abc/1" => [
before: "MyInterceptor.intercept_before/1",
after: "MyInterceptor.intercept_after/2"
]
}
Interceptor.intercept do
def abc(x), do: "Got #{inspect(x)}"
end
def not_intercepted(f, g, h), do: f+g+h
end
Notice that we’re using the streamlined format for the MFAs, but we could also use the more verbose tuple-based MFAs.
Link to this section Summary
Functions
Use this macro to wrap all the function definitions of your modules that you want to intercept. Remember that you need to configure how the interception
This function will be called as the error callback, in those cases when you only define a success callback for your intercepted function
This function will be called as the success callback, in those cases when you only define an error callback for your intercepted function
Link to this section Functions
Use this macro to wrap all the function definitions of your modules that you want to intercept. Remember that you need to configure how the interception
Here’s an example of a module that we want to intercept, using the
Interceptor.intercept/1 macro:
defmodule ModuleToBeIntercepted do
require Interceptor, as: I
I.intercept do
def foo(x), do: "Got #{inspect(x)}"
def bar, do: "Hi"
def baz(a, b, c, d), do: a + b + c + d
end
end
This function will be called as the error callback, in those cases when you only define a success callback for your intercepted function.
This function will be called as the success callback, in those cases when you only define an error callback for your intercepted function.