I am writing the Elixir integration for AppSignal, an application metrics solution. As of such, I am looking for a way to decorate functions in a developer-friendly way, so that these functions are automagically wrapped with library calls to measure the time it took to execute them (and send that info off to the backend).
As instrumenting functions is a common task while analyzing an application’s performance I want to minimize the amount of work the developer has to do to add instrumentation. As of such I have found two ways to do the instrumentation, both of which have its drawbacks:
1 - an instrumented do .... end block, in which you define the functions example here. Drawback of this method is that inside the block, everything is indented one level deeper, causing all the code to change while you have in fact just added 2 lines of code;making merging code changes harder;
2 - replace def foo() by def_instrument foo() (or similar) and use Kernel.def (like suggested here); the downside of this method is that it just “feels” weird to not read def, plus editor syntax highlighting breaks.
Are there any alternatives to tackle this? Ideally my solution would be a (Python / Java) decorator kind of syntax like this:
@instrumented
def foo(bar) do
...
But I am not sure that this is technically possible. I would love some input from the community on this!
I’d probably just make something that the user could use just via:
defmodule SomeUserModule do
use Instrumented,
funs [
(:hello, 0)
]
def hello, do: "world"
# .. lots of more functions
end
In your use you can hook all the functions that are listed by the tuple of the funName and funArity and use a macro to hook the def’s and instrument the ones that match the list. That seems like the easiest way to hook that. Could do an :all or so for the funs to say to do everything
Or you could do something like this if you want to mark ‘at’ the function site:
defmodule SomeUserModule do
use Instrumented
@instrumented
def hello, do: "world"
# .. lots of more functions
end
Where that would basically do the same thing but you’d walk the ast looking for that attribute than an immediately following function declaration or so.
It would be entirely possible for someone to make a generic ‘function decorator’ macro module that works like that, could do things like @decorate SomeModule.functionWrapper or so before the functions. Unsure if anyone’s made one yet, but eh?
Yes this latest idea was how I envisioned implementing it, walking the AST looking for module attributes next to a def. However, I dont know where I should hook in to, @before_compile does not allow you to alter the AST, iirc… and __using__ only allows you to add code to the end of the module.
You can always hook def itself within the module with a macro and have it either call to the normal def or do special things based on information too (that would work well with the first style especially), or at least using it for the second style and renaming the function (adding a NONINSTRUMENTED_ or so to everything) then setting up trampolines with instrumenting or not at the end based on the annotations.
Yes, overriding def with a macro seems possible however I still don’t know how to get the @ attributes preceding the def, because I’m not sure there is a way to get the entire AST tree in some kind of hook and alter it.
Use a generic callback for def, when it gets called, check if @instrumented is set as a module-attribute, if so, do your stuff and unset it. If it is not set just do nothing.
There are some handy functions in the Module-module to work with module attributes ((get|put|delete|register)_attribute/*)
Thanks. But I guess I also need to check its position in the AST - basically misuse module attributes to check whether an attribute occurs just before a def.
Maybe if I put the decorators inside the function as the first thing, I can get away with just overriding def:
def some_function(bar) do
@instrumented
# ... do stuff
end
Why do you need it to be the very last thing before the function or the very first in it? If you are too strict, it will be impossible to compose such annotations.
Also, why should it matter if one writes variant A or B?
A:
@instrumented
@doc "documented!"
def foo(bar), do: bar
B:
@doc "documented!"
@instrumented
def foo(bar), do: bar
Of course you can exchange @doc with @spec or any other function level attribute or combinations thereof.
PS: AFAIK you cant do just an empty attribute you need to assign something to it.
It doesnt need to be strictly the last attribute before the def but the attribute should be scoped to the function somehow, e.g. some defs need instrumentation, others dont:
def foo(bar) do
# not instrumented
@instrumented
def i_am_instrumented(bar) do
# ...
def foo2(bar) do
# not instrumented, again
I know about attribute definitions needing definition but inside the __using__ the attribute can get defined.
When you define @instrumented in the using, it will get replaced by that value in the sourcecode on every occurence, this can make your sourcecode invalid.
My proposal was to explicitely set it before functions, maybe specifying some extra stuff (similar to ExUnits @tags). And unset it in the callback.
Ah! That makes sense suddenly.
So @instrumented “blablah” just before a def (which is an overridden macro); and then the macro does its AST magic, and unsets the attribute. Neat! I’ll try that.
Nah, that is fine if it is only for the AST to go over, it only matters to set something if you want to access it post-ast.
You could always do something like this too:
defmodule SomeUserModule do
use Instrumented
@instrumented hello: 0
@instrumented anotherFun: 4
# ...
def hello, do: "world"
# .. lots of more functions
end
Doing just that! Right now I’m generating macros. Does not look so nice (no @) but it works pretty well. Let me know if you have a better way of doing this
One this thread hasn’t really talked about is whether the decorator pattern is actually a good idea in Elixir. I’m not entirely convinced that it is.
With instrumentation for example very frequently the caller site is the thing that should care about whether or not a given function call is instrumented. If you mark a function as instrumented with a decorator then every call to it is gonna have some kind of side effect whether that is actually desired or not.
I think it was the :trace module could allow you to hook functions, so you could define things to instrument based on module name and function name from the config or so?