Erlang Elixir编译时代码注入/AOP

Erlang Elixir编译时代码注入/AOP,erlang,aop,elixir,Erlang,Aop,Elixir,我以前使用过AOP风格的代码来分离逻辑和日志记录,并且对结果非常满意。我知道人们对AOP的看法各不相同,但我想在Elixir中找到一个解决方案,即使我最终没有在prod中使用它 我见过的最接近的例子是ExUnit内部的setup回调,它允许在每次测试运行之前执行代码;我想做一些类似的事情,但是还没能通过ExUnit源来理解那里的直觉 以代码形式: defmodule Project.Logic do LoggingInjection.inject Project.Logging

我以前使用过AOP风格的代码来分离逻辑和日志记录,并且对结果非常满意。我知道人们对AOP的看法各不相同,但我想在Elixir中找到一个解决方案,即使我最终没有在prod中使用它

我见过的最接近的例子是ExUnit内部的setup回调,它允许在每次测试运行之前执行代码;我想做一些类似的事情,但是还没能通过ExUnit源来理解那里的直觉

以代码形式:

defmodule Project.Logic do
    LoggingInjection.inject Project.Logging

    def work_do_stuff(arg) do
        #...
        #returns some_result
    end
end
在单独的代码文件中:

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
最后,真正的问题是:启用此魔法的代码是什么

defmodule LoggingInjection do
    defmacro inject(logging_module) do
        #What goes here?
    end
end

我想提出一种完全不同的方法来解决这个问题:

ExUnit方法适用于ExUnit,因为它是一个测试框架,可以对测试的运行方式以及代码的编写方式施加限制。对于您的实际应用程序(包括日志记录和其他内容),基于事件的系统似乎是更健壮的解决方案,它也可以轻松地从并发中获益


这个想法是,您将启动一个GenEvent并向其发送事件。您的记录器将是安装在GenEvent中的处理程序。您可以选择将发布的事件设置为同步或异步。我们的文档中的例子正好是这种情况。

它不是AOP,但如果您对运行时的函数调用感兴趣,可以考虑查看Erlang跟踪。

例如,可以使用
:dbg
设置函数调用的动态跟踪。下面是如何跟踪对
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)> ">>)
要使用它,只需启动它并提供要跟踪的模块列表:

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)> ")
iex(2)>跟踪程序启动([IO])
{:好的,#PID}
称为Elixir.IO.puts(:stdio,“\e[33m{:ok,#PID}\e[0m”)
Elixir.IO.puts/2返回正常
调用Elixir.IO.get(:stdio,“iex(3)>”)

跟踪功能非常强大,你可以用它做各种事情。我自己没有把它用于日志系统,所以我不能说它会有多大的问题,所以如果你走这条路,我建议你小心观察性能,因为太多的跟踪可能会使你的系统过载。

我用同样的方法偶然发现了这个问题ed.不是我的解决方案,把它放在这里供大家参考,我发现了一个关于宏以及如何提取函数def信息的极好的方法

基本上:

  • 为了修饰函数声明,可以从内核模块中排除“def”宏的默认导入

    import Kernel, except: [def: 2] 
    
  • 提供“def”宏的自定义实现,该宏将提取AST节点的函数声明信息(名称和参数),并生成执行实际“Kernel.def”的代码,例如在其中执行日志记录并执行函数的原始代码

    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
    
  • 我将这两个解决方案放在一起,并创建了一个实现这些解决方案的工具。仅用于试验。因此,您可以:

    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"
    
    defmodule用户操作
    使用功能装饰
    用LogDecorator装饰
    def说(词)做
    单词
    结束
    结束
    iex>User.say(“你好”)
    #PID[x]Elixir.User.say([“你好])->“你好”
    “你好”
    
    如果您不介意用属性装饰函数,可以使用:

    这很不显眼。你可以做这样的LogDecorator:

    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
    
    defmodule MyModule do
      use LogDecorator
    
      @decorate log()
      def square(a) do
        a * a
      end
    
      @decorate log()
      def add(a, b) do
        a + b
      end
    end
    
    要使用它,您需要将属性放置在要跟踪的函数上,如下所示:

    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
    
    defmodule MyModule do
      use LogDecorator
    
      @decorate log()
      def square(a) do
        a * a
      end
    
      @decorate log()
      def add(a, b) do
        a + b
      end
    end
    

    并发是这个方案的一个极好的补充,我很乐意采用GenEvent方法;我需要什么才能使它满足主要需求(日志记录与逻辑分离)是一种在每个方法调用的开始和结束时分派事件的方法;使这些事件包含参数/返回/方法信息;并在每个函数的开始/结束处隐藏所有样板文件。替换
    GenEvent.notify
    for
    Logging.log
    的代码不能解决主要需求。好吧,您的代码does对日志记录一无所知。GenEvent旨在提供一个通用扩展点,该扩展点可供任何人使用,且不与实现耦合。在任何情况下,要直接回答您的问题,目前无法在编译时按照您的要求进行分离(我不知道库或示例)。:)