erlang - top - Inyección de código en tiempo de compilación Elixir/AOP
owasp seguridad (4)
Me gustaría proponer un enfoque completamente diferente al problema: GenEvent .
La forma ExUnit funciona para ExUnit porque es un marco de prueba y puede imponer restricciones sobre cómo se ejecuta la prueba y, por lo tanto, cómo se debe escribir su código. Para su aplicación actual, que incluye el registro y otras cosas, un sistema basado en eventos parece ser una solución mucho más sólida que también puede beneficiarse fácilmente de la concurrencia.
La idea es que inicie GenEvent y le envíe eventos. Su registrador será un controlador instalado en GenEvent. Puede elegir que los eventos publicados sean sincronizados o asincrónicos. El ejemplo en nuestra documentación cubre exactamente este caso.
Anteriormente utilicé el código de estilo AOP para separar Logic de Logging y estoy muy satisfecho con los resultados. Reconozco que las opiniones sobre AOP varían, pero me gustaría encontrar una solución en Elixir, incluso si no termino usándola en prod.
El ejemplo más cercano que he visto es la devolución de llamada de configuración dentro de ExUnit, que permite la ejecución del código antes de ejecutar cada prueba; Me gustaría hacer algo similar, pero no he podido confundir la fuente ExUnit para comprender las intuiciones allí.
En forma de código:
defmodule Project.Logic do
LoggingInjection.inject Project.Logging
def work_do_stuff(arg) do
#...
#returns some_result
end
end
en un archivo de código separado:
defmodule Project.Logging do
#called just before Project.Logic.work_do_stuff with the same args
def before_work_do_stuff(arg) do
Log.write("about to work_do_stuff with #{inspect arg}")
end
# def after_work_do_stuff(some_result) implicitly defined as no-op,
# but could be overridden.
end
y finalmente, la verdadera pregunta: ¿Cuál es el código para habilitar esta magia?
defmodule LoggingInjection do
defmacro inject(logging_module) do
#What goes here?
end
end
No es AOP, pero si está interesado en observar las llamadas a funciones en tiempo de ejecución, puede considerar buscar en el trazo de Erlang.
Por ejemplo, puede usar :dbg
para configurar huellas dinámicas de llamadas a funciones. A continuación, le mostramos cómo rastrear todas las llamadas a IO
:
iex> :dbg.tracer
iex> :dbg.p(:all, [:call])
iex> :dbg.tp(IO, [{:_, [], [{:return_trace}]}])
(<0.53.0>) call ''Elixir.IO'':puts(stdio,<<"/e[33m{:ok, [{:matched, :nonode@nohost, 28}, {:saved, 1}]}/e[0m">>)
(<0.53.0>) returned from ''Elixir.IO'':puts/2 -> ok
(<0.59.0>) call ''Elixir.IO'':gets(stdio,<<"iex(4)> ">>)
De vez en cuando uso esta característica para conectarme a un nodo BEAM en ejecución y analizar el sistema en ejecución. Asegúrese de detener los rastros con :dbg.stop_clear
una vez que haya terminado.
Si desea manejar manualmente los mensajes de seguimiento y hacer algo específico sobre ellos (por ejemplo, registrarlos en un archivo), puede usar :erlang.trace
. Aquí hay un gen_server
simple que rastrea las llamadas a varios módulos:
defmodule Tracer do
use GenServer
def start(modules), do: GenServer.start(__MODULE__, modules)
def init(modules) do
:erlang.trace(:all, true, [:call])
for module <- modules do
:erlang.trace_pattern({module, :_, :_}, [{:_, [], [{:return_trace}]}])
end
{:ok, nil}
end
def handle_info({:trace, _, :call, {mod, fun, args}}, state) do
IO.puts "called #{mod}.#{fun}(#{Enum.join(Enum.map(args, &inspect/1), ",")})"
{:noreply, state}
end
def handle_info({:trace, _, :return_from, {mod, fun, arity}, res}, state) do
IO.puts "#{mod}.#{fun}/#{arity} returned #{res}"
{:noreply, state}
end
def handle_info(_, state), do: {:noreply, state}
end
Para usarlo, simplemente inícielo y proporcione la lista de módulos que desea rastrear:
iex(2)> Tracer.start([IO])
{:ok, #PID<0.84.0>}
called Elixir.IO.puts(:stdio,"/e[33m{:ok, #PID<0.84.0>}/e[0m")
Elixir.IO.puts/2 returned ok
call Elixir.IO.gets(:stdio,"iex(3)> ")
El rastreo es muy poderoso y puedes hacer todo tipo de cosas con él. No lo utilicé para un sistema de registro, así que no puedo decir qué tan problemático será, así que si tomas este camino, te aconsejo que seas cuidadoso y observes el rendimiento, ya que un exceso de rastreo podría sobrecargar su sistema
Si no te importa decorar tus funciones con atributos, puedes utilizar: https://github.com/arjan/decorator
Es bastante discreto. Puede hacer un LogDecorator así:
defmodule LogDecorator do
use Decorator.Define, [log: 0]
require Logger
def log(body, context) do
quote do
self = unquote(__MODULE__)
data =
unquote(Macro.escape(context))
|> Map.delete(:__struct__)
|> Map.merge(%{
args: self.listify(unquote(context.args)),
returned: unquote(body)
})
self.trace_function(data)
data.returned
end
end
def listify(arg) when is_list(arg), do: arg
def listify(arg) when is_map(arg), do: Enum.into(arg, [])
def listify(arg), do: [arg]
def trace_function(ctx) do
Logger.info("Function Invocation #{inspect ctx}")
end
end
Para usarlo colocaría atributos en las funciones que desea rastrear de esta manera:
defmodule DecoratorSpike do
use LogDecorator
@decorate log()
def square(a) do
a * a
end
@decorate log()
def add(a, b) do
a + b
end
end
Me encontré con esta pregunta con la misma necesidad. No es mi solución, ponerlo aquí para cualquier referencia, encontré una excelente serie de tutoriales sobre macros y cómo extraer la información de la función por Saša Jurić y una forma de anular la macro ''def'' también por él.
Básicamente:
Para decorar declaración de función, puede excluir la importación predeterminada de la macro ''def'' desde el módulo Kernel.
import Kernel, except: [def: 2]
Proporcione una implementación personalizada de la macro ''def'' que extraerá la información de declaración de función para el nodo AST - nombre y argumentos - y generará un código que haga el ''Kernel.def'' real, dentro de él haga el registro, por ejemplo, y haga la código original para la función.
defmacro def(fn_call_ast, fn_opts_ast) do result_fn_call_ast = process_call_ast fn_call_ast result_fn_opts_ast = process_opts_ast fn_opts_ast quote do Kernel.def( unquote(result_fn_call_ast), unquote(result_fn_opts_ast)) end end
He reunido esas dos soluciones y he creado un paquete para implementarlas. Destinado solo para experimentar Entonces puedes hacer:
defmodule User do
use FunctionDecorating
decorate_fn_with LogDecorator
def say(word) do
word
end
end
iex>User.say("hello")
#PID<0.86.0> [x] Elixir.User.say(["hello"]) -> "hello"
"hello"