在 Elixir 中编写函数来测量另一个函数的好方法是啥

Posted

技术标签:

【中文标题】在 Elixir 中编写函数来测量另一个函数的好方法是啥【英文标题】:What is a good way for writing a function to measure another function in Elixir在 Elixir 中编写函数来测量另一个函数的好方法是什么 【发布时间】:2021-10-16 07:01:28 【问题描述】:

我是 elixir 新手,我正在尝试寻找类似于 Python 的 ContextManager 的东西。

问题:

我有一堆函数,我想在它们周围添加延迟指标。

现在我们有了:

def method_1 do
  ...
end

def method_2 do
  ...
end

... more methods

我想要:


def method_1 do
  start = System.monotonic_time()
  ...
  end = System.monotonic_time()
  emit_metric(end-start)
end

def method_2 do
  start = System.monotonic_time()
  ...
  end = System.monotonic_time()
  emit_metric(end-start)
end

... more methods

现在代码重复是个问题

  start = System.monotonic_time()
  ...
  end = System.monotonic_time()
  emit_metric(end-start)

那么在这种情况下,避免代码重复的更好方法是什么?我喜欢 python 中的context manager 想法。但现在确定如何在 Elixir 中实现类似的功能,提前感谢您的帮助!

【问题讨论】:

【参考方案1】:

在 Erlang/Elixir 中,这是通过高阶函数完成的,请查看 BEAM telemetry。它是用于收集指标和检测代码的 Erlang 和 Elixir 库/标准——它被 Pheonix、Ecto、cowboy 和其他库广泛采用。具体来说,您会对 :telemetry.span/3 函数感兴趣,因为它默认发出开始时间和持续时间测量值:

def some_function(args) do
  :telemetry.span([:my_app, :my_function], %metadata: "Some data", fn ->
    result = do_some_work(args)
    result, %more_metadata: "Some data here"
  end)
end

def do_some_work(args) # actual work goes here

然后,在您的其他代码中,您侦听这些事件并将它们记录/发送到 APM:

:telemetry.attach_many(
  "test-telemetry", 
  [[:my_app, :my_function, :start], 
   [:my_app, :my_function, :stop], 
   [:my_app, :my_function, :exception]],
  fn event, measurements, metadata, config -> 
    # Handle the actual event.
  end)
  nil
)

【讨论】:

【参考方案2】:

我认为最接近 python 上下文管理器的是使用高阶函数,即以函数作为参数的函数。 所以你可以有类似的东西:

def measure(fun) do
  start = System.monotonic_time()
  result = fun.()  
  stop = System.monotonic_time()
  emit_metric(stop - start)
  result
end

你可以像这样使用它:

measure(fn ->
  do_stuff()
  ...
end)

注意:还有其他类似的实例,您可以在 python 中使用上下文管理器,这将以类似的方式完成,在我的脑海中:Django 有一个context manager for transactions,但Ecto uses a higher order function 用于同样的事情。

PS:要测量经过的时间,您可能想使用:timer.tc/1

def measure(fun) do
  elapsed, result = :timer.tc(fun)
  emit_metric(elapsed)
  result
end

【讨论】:

【参考方案3】:

实际上有一个非常漂亮的库,名为 Decorator,其中可以使用宏来“包装”您的函数来执行各种操作。

在您的情况下,您可以编写一个装饰器模块(感谢@maciej-szlosarczyk 的telemetry 示例):

defmodule MyApp.Measurements do
  use Decorator.Define, measure: 0

  def measure(body, context) do
    meta = Map.take(context, [:name, :module, :arity])
    quote do
      # Pass the metadata information about module/name/arity as metadata to be accessed later
      :telemetry.span([:my_app, :measurements, :function_call], unquote(meta), fn ->
        unquote(body), %
      end) 
    end
  end
end

您可以在 Application.start 定义中设置遥测侦听器:

:telemetry.attach_many(
  "my-app-measurements", 
  [[:my_app, :measurements, :function_call, :start], 
   [:my_app, :measurements, :function_call, :stop], 
   [:my_app, :measurements, :function_call, :exception]],
  &MyApp.MeasurementHandler.handle_telemetry/4)
  nil
)

然后在任何带有您想要测量的函数调用的模块中,您可以像这样“装饰”这些函数:

defmodule MyApp.Domain.DoCoolStuff do
  use MyApp.Measurements
  
  @decorate measure()
  def awesome_function(a, b, c) do
    # regular function logic
  end
end

虽然此示例使用遥测,但您可以在装饰器定义中轻松打印出时差。

【讨论】:

以上是关于在 Elixir 中编写函数来测量另一个函数的好方法是啥的主要内容,如果未能解决你的问题,请参考以下文章

我可以在这个 Elixir 匿名函数中避免元组参数吗?

为啥需要 Elixir 捕获运算符来将函数绑定到值

Elixir/Erlang 中的命名函数是不是有等效于 __MODULE__ 的方法?

的重要性 。在 Elixir 函数式编程中的匿名函数中 [重复]

Elixir:来自函数的结构默认值

为啥在 elixir 中定义命名函数有两种方式?