Packages

Sibyl is a library which augments the BEAM's default tracing capabilities by hooking into `:telemetry`, `:dbg` (the BEAM's built in tracing and debugging functionality), and `OpenTelemetry`.

Current section

Files

Jump to
sibyl lib sibyl decorator.ex
Raw

lib/sibyl/decorator.ex

defmodule Sibyl.Decorator do
@moduledoc """
Module encapsulating Sibyl's business logic for decorating functions for automated
tracing.
Should be used only via `use Sibyl`.
When used like this, provides the ability to decorate your function calls with:
- `@decorate trace()` to automatically trace a single function's execution, emitting
telemetry events for `:start`, `:end`, and `:exception` sub-events.
- `@decorate_all trace()` which does the same as the above, but automatically for
all functions in a module.
Automatically traced functions are available for reflection by via `Sibyl.Events.reflect/1`.
"""
use Decorator.Define, trace: 0
alias Sibyl.AST
require Sibyl.Events
@doc """
Decorator which wraps a given function with a standard telemetry span.
The name of the captured event will be determined by however `Sibyl` is configured
to generate event names.
Due to how anonymous functions are defined and executed in the BEAM, it ends up
being quite a bit more performant to build the span manually rather than using
`:telemetry.span/3`.
See [here](https://keathley.io/blog/telemetry-conventions.html) for an example of
how to emit the correct events.
See [here](https://github.com/beam-telemetry/telemetry/pull/43) for explanations
w.r.t. anonymous function perf.
"""
@spec trace(function_body :: AST.ast(), ctx :: map()) :: AST.ast() | no_return()
def trace(body, ctx) do
Application.ensure_all_started(:telemetry)
event = Sibyl.Events.build_event(ctx.module, ctx.name, ctx.arity)
quote do
args = binding() |> Keyword.values() |> Enum.reverse()
event = unquote(event)
module = unquote(inspect(ctx.module))
function = unquote(ctx.name)
arity = unquote(ctx.arity)
metadata = %{
args: args,
module: module,
function: function,
arity: arity,
mfa: "#{module}.#{function}/#{arity}",
node: node()
}
start = :os.perf_counter()
:ok = Sibyl.Events.emit(unquote(event ++ [:start]), %{timestamp: start}, metadata)
result =
try do
unquote(body)
rescue
exception ->
exception_type = Map.get(exception, :__struct__, Sibyl.UnknownExceptionType)
stop = :os.perf_counter()
:ok =
Sibyl.Events.emit(
unquote(event ++ [:exception]),
%{duration: stop - start, timestamp: stop},
Map.merge(metadata, %{
kind: :rescue,
reason: exception_type,
exception: exception,
stacktrace: __STACKTRACE__
})
)
reraise exception, __STACKTRACE__
catch
kind, reason ->
stop = :os.perf_counter()
:ok =
Sibyl.Events.emit(
unquote(event ++ [:exception]),
%{duration: stop - start, timestamp: stop},
Map.merge(metadata, %{kind: kind, reason: reason, stacktrace: __STACKTRACE__})
)
:erlang.raise(kind, reason, __STACKTRACE__)
end
stop = :os.perf_counter()
:ok =
Sibyl.Events.emit(
unquote(event ++ [:stop]),
%{duration: stop - start, timestamp: stop},
metadata
)
result
end
end
@doc false
@spec on_definition(env :: map(), term(), atom(), [term()], AST.ast(), AST.ast()) :: AST.ast()
def on_definition(%{module: module} = env, kind, function, args, guards, body) do
arity = length(args)
if has_decorate_all?(module) or has_decorate?(module, function) do
for event <- [:start, :stop, :exception] do
module
|> Sibyl.Events.build_event(function, arity, event)
|> Sibyl.Events.define_event(module)
end
end
Decorator.Decorate.on_definition(env, kind, function, args, guards, body)
end
defp has_decorate_all?(module), do: Module.get_attribute(module, :decorate_all) != []
defp has_decorate?(module, function) do
module
|> Module.get_attribute(:decorated)
|> Enum.find(fn node ->
is_tuple(node) && elem(node, 0) in [:def, :defp] && elem(node, 1) == function &&
{Sibyl.Decorator, :trace, []} in elem(node, 5)
end)
|> is_tuple()
end
end