Skip to content
Simple code instrumentation utilities
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Type Name Latest commit message Commit time
Failed to load latest commit information.

Annotations - MetaData and Code Instrumentation for Erlang/OTP

This library provides additional features for annotating code with metadata, as well as support for compile time code instrumentation via a parse transform. See here for a concrete usage example.


Annotating specific functions:

-export([find/2, add/2]).

find(DbHandle, What) ->
    qlc:q([I || I <- table_source(DbHandle), I.key == What]).

add(DbHandle, Item) ->
    db_backend:insert(DbHandle, Item).

Note that the -transactional attribute is not usually allowed at this scope, but the annotations parse transform moves the metadata around and tags the annotation so that it maps the right function.

Adding behaviour to Annotations

An annotation can be backed by a user-defined module, in which case the module may implement certain callback functions that can be used to provide instrumentation at runtime. For example, consider this logging annotation:

-annotation('function').    %% the scope of this annotation

before_advice(#annotation{data=Lvl}, M, F, Inputs) ->
    log(Lvl, "before_advice - M: ~p, F: ~p, Inputs: ~p~n", [M, F, Inputs]),

after_advice(#annotation{data=Lvl}, M, F, _Inputs, Result) ->
    log(Lvl, "after_advice - M: ~p, F: ~p, Result: ~p~n", [M, F, Result]),

log(Lvl, Message, Args) ->
    case get(loglevel) of
        Lvl ->
            io:format(Message, Args);
        _ ->

Naturally we'd normally want to use a proper logging framework and pass on the log level instead, but we've kept our example simple to use/understand here.

We can now use the logging annotated in our actual code:


-spec(foo/1 :: (term()) -> {foo, string(), term()}).
foo(T) ->
    {foo, T}.

Building with rebar based project

We'll need to export the logging annotation config in order for the parse transform to work, which we can do using the supplied rebar plugin:

%% rebar.config

{deps, [
    {parse_trans, ".*",
        {git, ""}},
    {annotations, ".*",
        {git, ""}}

{plugins, [rebar_annotations_plugin]}.
{annotations, [{registered, [logging]}]}.

{erl_first_files, ["logging.erl"]}.

Building with based project

An plugin is provided for easy integration with your project.

DEPS = annotations
DEP_PLUGINS = annotations



And now for an example session:

t4@malachi:simple $ rebar clean compile
# output snipped...
t4@malachi:simple $ erl -pa ebin/
Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:2:2] [rq:2] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.8.2  (abort with ^G)
1> annotated:foo("Hello world").
{foo,"Hello world"}
2> put(loglevel, info).
3> annotated:foo("Hello world").
before_advice - M: logging, F: foo, Inputs: ["Hello world"]
after_advice - M: logging, F: foo, Result: {foo,"Hello world"}
{foo,"Hello world"}
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution
t4@malachi:simple $ 

For an example of using around advice take a look in the examples directory.

Generating code at build time

Annotations can also opt to export additional code at runtime. Currently there are two supported methods for doing this:

  1. Generate a function that calls back into your (annotation) module
  2. Generate a function by hand

To generate code, you should export a function codegen/3 which takes three inputs: the annotation record, the module in which the target function resides and the current function AST (as handled by erl_syntax). This function must return a list of targets, which can contain one of the following two kinds of specification:

In order to utilise (1), your codegen/3 function should return tuples which contains {AdviceFunctionName, TargetFunctionName, TargetFuncArity, Data}. The AdviceFunctionName is the name of a function which will be called by the generated function, the target function name and arity indicate what you wish to generate and the Data is a literal/AST containing the input(s) you want sent to your AdviceFunctionName at runtime. The result of this will be a function which calls back into your module directly.

In order to utilise (2), your codegen/3 should return tuples which contain {Name, fun()} where Name is the name you wish to export and the corresponding fun is the function you wish to generate. The idea of this API is that you can use the codegen module from parse_trans to generate your function conveniently, and have the annotation processing engine deal with the rest.

For an example of code generation in practise, take a look at the delegate library.

%% delegate.erl
-export([codegen/3, delegate_advice/4]).

codegen(A=#annotation{data=Data}, _Mod, AST) ->
    io:format("Annotation Data: ~p~n",[lists:keyfind(arity, 1, Data)]),
    case lists:keyfind(delegate, 1, Data) of
        {delegate, Delegates} when is_list(Delegates) ->
            %% NB: you need a *little* understanding of erl_syntax here
            {_FN, FA} = erl_syntax_lib:analyze_function(AST),
            Arity = case lists:keyfind(arity, 1, Data) of
                {arity, N} when is_integer(N) -> N;
                _ -> FA
            [ build_spec(D, Arity, A) || D <- Delegates ];
        Other ->
            %% TODO: clearer error handling API
            io:format("Other: ~p~n", [Other]),
            {error, "no delegates defined"}

delegate_advice(A, M, F, Inputs) ->
    Argv = make_args(A, M, F, Inputs),
    erlang:apply(M, F, Argv).

%% etc....

And the corresponding usage pattern:

-compile({no_auto_import, [error/2]}).

-delegate([{delegate, ["info", "warn", "error"]},
           {args, ['$T', '$I']},
           {arity, 2}]).
log(Level, Message, Args) ->
    case erlang:get({?MODULE, loglevel}) of
        Level ->
            io:format(Message, Args);
        _ ->

Which when the annotation processing has finished, looks like this:

% escript deps/parse_trans/ebin/parse_trans_pp.beam ebin/simple_log.beam
%% snip....
-annotation({annotation, delegate,
	     {function, {simple_log, log, 3}},
	     [{delegate, ["info", "warn", "error"]},
	      {args, ['$T', '$I']}, {arity, 2}]}).




info(V73, V45) ->
    delegate:delegate_advice({annotation, delegate,
			      {function, {simple_log, log, 3}},
			      [{target, info},
			       {delegate, ["info", "warn", "error"]},
			       {args, ['$T', '$I']}, {arity, 2}]},
			     simple_log, log, [V73, V45]).

warn(V51, V95) ->
    delegate:delegate_advice({annotation, delegate,
			      {function, {simple_log, log, 3}},
			      [{target, warn},
			       {delegate, ["info", "warn", "error"]},
			       {args, ['$T', '$I']}, {arity, 2}]},
			     simple_log, log, [V51, V95]).

error(V60, V32) ->
    delegate:delegate_advice({annotation, delegate,
			      {function, {simple_log, log, 3}},
			      [{target, error},
			       {delegate, ["info", "warn", "error"]},
			       {args, ['$T', '$I']}, {arity, 2}]},
			     simple_log, log, [V60, V32]).

log(Level, Message, Args) ->
    case erlang:get({simple_log, loglevel}) of
      Level -> io:format(Message, Args);
      _ -> ok
%% snip....


This work is distributed under a permissive BSD-style license.


This project will adhere to the principles of semantic versioning once a first public API is declared.


See the file for upcoming plans/changes.

You can’t perform that action at this time.