Skip to content

Latest commit

 

History

History
407 lines (289 loc) · 17.3 KB

2020-04-24-instrumenting-phoenix-with-live-dashboard.md

File metadata and controls

407 lines (289 loc) · 17.3 KB
author author_link categories date layout title excerpt
Sophie DeBenedetto
general
2020-04-24
post
Instrumenting Phoenix with Telemetry and LiveDashboard
The recent release of the [LiveDashboard](https://github.com/phoenixframework/phoenix_live_dashboard) library allows us to visualize our application metrics, performance and behavior in real-time. In this post, we'll add LiveDashboard to our Phoenix app, examine the out-of-the-box features and take a look under the hood to understand how LiveDashboard hooks into Telemetry events in order to visualize them.

使用 Telemetry 和 LiveDashboard 对 Phoenix 进行仪表化

最近发布的 LiveDashboard 库允许我们实时地可视化应用程序的指标、性能和行为。在这篇文章中,我们将把 LiveDashboard 添加到我们的 Phoenix 应用中,检查开箱即用的功能,并在了解 LiveDashboard 背后是如何钩入 Telemetry 事件以实现可视化。

应用

我们将使用我们为 Telemetry 系列博客文章建立的 Phoenix 应用,Quantum。Quantum 应用并没有做太多事情--它的存在其实只是为了被测量。在这篇文章中,我们将设置一个 Telemetry supervisor,为 Telemetry 事件实现一套度量定义。我们将定义的指标与 Phoenix 和 Ecto 发出的一些开箱即用的 Telemetry 事件相匹配。要深入了解 Telemetry 事件,请查看我们的系列文章 Instrumenting Phoenix with Telemetry。本系列即将发布的文章将仔细研究 Phoenix 和 Ecto 提供的开箱即用的 Telemetry 事件,演练如何添加我们自己的 Telemetry 事件等。

代码

本演示的最终代码可以在这里找到。

添加 LiveDashboard

首先,我们将把 LiveDashboard 依赖添加到我们的 Phoenix 应用中。

# mix.exs
def deps do
  [
    {:phoenix_live_dashboard, "~> 0.1"}
  ]
end

接下来,我们将确认 LiveView 已经配置了:

# config/config.exs
config :quantum, QuantumWeb.Endpoint,
  live_view: [signing_salt: "SECRET_SALT"]

然后呢,我们确认我们应用的 Endpoint 已经声明了 LiveView 的 socket

# lib/quantum_web/endpoint.ex
socket "/live", Phoenix.LiveView.Socket

最后,我们将设置从 /dashboard 端点到路由器 LiveDashboard 的请求转发。

use MyAppWeb, :router
import Phoenix.LiveDashboard.Router

...

if Mix.env() == :dev do
  scope "/" do
    pipe_through :browser
    live_dashboard "/dashboard"
  end
end

就是这样! 如果我们运行 mix deps.get,然后运行 mix phx.server,我们将看到我们的 LiveDashboard 和它开箱即用的监控可视化。

LiveDashboard 开箱即用

首页

live dashboard home

LiveView 为我们显示了一些开箱即用的监控功能。

Home 页面,我们可以看到系统信息,比如我们的 Erlang/OTP 版本,Phoenix 版本和 Elixir 版本。我们还可以看到我们的应用所负责的端口、进程和原子的数量等一些信息。

进程

live dashboard processes

Processes 页面允许我们对应用程序中运行的进程进行检查。我们可以看到一些有用的信息,比如每个进程占了多少内存,甚至某个进程当前正在执行的功能。检查一个给定的进程,我们可以看到更多的信息,包括它的状态,初始函数和堆栈跟踪。

端口

live dashboard ports

Ports 页面,我们可以看到我们的应用程序所暴露的端口(负责应用程序的 I/O)。

检查一个给定的端口,我们可以看到哪个进程负责暴露该端口并管理该端口的输入/输出。

live dashboard port-detail

Sockets

Sockets 页面公开了当前由应用程序管理的所有 socket 的信息。Phoenix 应用中的 socket 负责所有 UDP/TCP 流量。在这里,我们甚至可以看到负责监听端口 :4000 的 socket 连接。

![live dashboard port-4000-detail]({% asset ld-port-4000-detail.png @path %})

ETS

LiveDashboard 为我们提供的最后一个开箱即用的页面是 ETS 页面。ETS(Erlang Term Storage)是我们的内存存储。我们甚至可以看到 Telemetry handler 表的条目。

live dashboard ets-detail

LiveDashboard 指标

建立指标

有两个 LiveDashboard 功能,我们要做一点工作才能启用。我们将从 LiveDashboard Metrics 开始,它利用了 telemetry_metrics 库。

首先,我们将把 telemetry_metrics 添加到我们应用程序的依赖中:

# mix.exs
{:telemetry_metrics, "~> 0.4"},

然后运行 mix deps.get

Telemetry.Metrics 库提供了一个接口,用于将 Telemetry 事件转换为度量指标。我们将在稍后的一篇博文中更深入地了解这个库,现在只关注一个高级别的理解。

现在我们已经安装了这个库,我们准备好定义 Telemetry 监督器了。监督器将实现一个 metrics/0 函数,声明我们要处理的 Telemetry 事件集,并指定要为这些事件构建哪些度量指标。

# lib/quantum/telemetry.ex
defmodule Quantum.Telemetry do
  use Supervisor
  import Telemetry.Metrics

  def start_link(arg) do
    Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
  end

  def init(_arg) do
    Supervisor.init([], strategy: :one_for_one)
  end

  def metrics do
    [
      # Erlang VM Metrics - Formats `gauge` metric type
      last_value("vm.memory.total", unit: :byte),
      last_value("vm.total_run_queue_lengths.total"),
      last_value("vm.total_run_queue_lengths.cpu"),
      last_value("vm.system_counts.process_count"),

      # Database Time Metrics - Formats `timing` metric type
      summary(
        "quantum.repo.query.total_time",
        unit: {:native, :millisecond},
        tags: [:source, :command]
      ),

      # Database Count Metrics - Formats `count` metric type
      counter(
        "quantum.repo.query.count",
        tags: [:source, :command]
      ),

      # Phoenix Time Metrics - Formats `timing` metric type
      summary(
        "phoenix.router_dispatch.stop.duration",
        unit: {:native, :millisecond}
      ),

      # Phoenix Count Metrics - Formats `count` metric type
      counter(
        "phoenix.router_dispatch.stop.count"
      ),

      counter(
        "phoenix.error_rendered.count"
      )
    ]
  end
end

在这里,我们使用 Telemetry.Metrics 的度量函数(countersummarylast_value)来指定哪些 Telemetry 事件要被视为哪种度量指标。这些函数中的每一个都以 Telemetry 事件为参数,例如,quantum.repo.query.tquery.total_time。我们的 metrics/0 函数中列出的事件都是由 Phoenix 或 Ecto 源码为我们免费执行的。

metrics/0 函数之后会传递给 LiveDashboard LiveView。LiveDashboard 使用这个 Telemetry 事件- metrics 列表,为每个事件附加适当的处理程序。关于如何在 ETS 的帮助下执行和处理 Telemetry 事件,请查看我们的 Telemetry 介绍博客文章

我们稍后将重新审视它是如何工作的。首先,让我们将新的监督器添加到我们的应用程序的监督树中。

# lib/quantum/application.ex

children = [
  Quantum.Repo,
  Quantum.Telemetry,
  Quantum.Endpoint,
  ...
]

现在,我们已经准备好用我们新定义的指标来配置 LiveDashboard。

配置 LiveDashboard 指标

我们将在 live_dashboard 路由调用中添加以下选项。

# lib/quantum_web/router.ex
live_dashboard "/dashboard", metrics: Quantum.Telemetry

现在,如果我们访问浏览器中的 /dashboard,并点击 Metrics 标签,我们将看到我们的指标:

live dashboard metrics

让我们来一探究竟,以便更好地了解这个配置是如何工作的。

LiveDashboard Metrics 的背后

live_dashboard 宏包含下面这一行,它将实时 /metrics 请求路由到 Phoenix.LiveDashboard.MetricsLive LiveView,会话 payload 包含我们传递的 metrics.Quantum.Telemetry 选项:

# live_dashboard/router.ex
live "/:node/metrics", Phoenix.LiveDashboard.MetricsLive, :metrics, opts

Phoenix.LiveDashboard.MetricsLive LiveView 启动时,它会使用我们定义在 Quantum.Telemetry 中指标相关的参数调用 Phoenix.LiveDashboard.TelemetryListener.listen/2 函数。

Phoenix.LiveDashboard.TelemetryListener 模块通过在 ETS 中存储事件/处理程序组合,负责将一组处理程序附加到 Telemetry 事件。Phoenix.LiveDashboard.TelemetryListenerinit/1 函数对我们在Quantum.Telemetry 中定义的指标进行迭代,并在 ETS 中存储每个度量的 Telemetry 事件名称及其自己的 handle_metrics/4 函数的处理程序。

# live_dashboard/telemetry_listener.ex
def init({parent, metrics}) do
  metrics = Enum.with_index(metrics, 0)
  metrics_per_event = Enum.group_by(metrics, fn {metric, _} -> metric.event_name end)

  for {event_name, metrics} <- metrics_per_event do
    id = {__MODULE__, event_name, self()}
    :telemetry.attach(id, event_name, &handle_metrics/4, {parent, metrics})
  end

  {:ok, %{ref: ref, events: Map.keys(metrics_per_event)}}
end

回顾我们之前关于 Telemetry 的文章,:telemetry.attach/4 函数在 ETS 中存储了将 Telemetry 事件映射到处理程序的条目。之后,当给定的 Telemetry 事件被执行时(例如,当 Ecto 源代码执行 "quantum.repo.query.tquery.total_time" 事件时),Telemetry 将在 ETS 中查找这个名称的事件,并调用存储的处理函数,在本例中 Phoenix.LiveDashboard.TelemetryListener.handle_metrics/4

看看 Phoenix.LiveDashboard.TelemetryListener.handle_metrics/4 函数,我们可以看到它做了一些格式化事件度量的工作,然后发送消息到它的 父亲 -- Phoenix.LiveDashboard.MetricsLive LiveView。

# live_dashboard/telemetry_listener.ex
def handle_metrics(_event_name, measurements, metadata, {parent, metrics}) do
  time = System.system_time(:second)

  entries =
    for {metric, index} <- metrics do
      if measurement = extract_measurement(metric, measurements) do
        label = tags_to_label(metric, metadata)
        {index, label, measurement, time}
      end
    end

  send(parent, {:telemetry, entries})
end

Phoenix.LiveDashboard.MetricsLive LiveView 为这个 {:telemetry, entries} 事件实现了一个 handle_info/2 方法,通过更新 ChartComponent 作为响应,导致 UI 更新。

# live_dashboard/live/metrics_live.ex
def handle_info({:telemetry, entries}, socket) do
  for {id, label, measurement, time} <- entries do
    data = [{label, measurement, time}]
    send_update(ChartComponent, id: id, data: data)
  end

  {:noreply, socket}
end

放在一起

让我们来回顾一下所有这些活动部件是如何连接的。

  1. 我们定义了一个 Telemetry supervisor,Quantum.Telemetry,它实现了一个 metrics/0 函数。这个函数负责将一组 Telemetry 事件映射到各种类型的度量。
  2. 我们将 Telemetry 监督器 Quantum.Telemetry 作为一个选项传递给路由器的 LiveDashboard。
  3. LiveDashboard 启动一个 LiveView,Phoenix.LiveDashboard.MetricsLive,里面有我们在 Quantum.Telemetry 中定义的指标。
  4. Phoenix.LiveDashboard.MetricsLive 调用 Phoenix.LiveDashboard.TelemetryListener,它用自己的 handle_metrics/4 处理函数将我们在 Quantum.Telemetry 中定义的 Telemetry 事件作为指标存储在 ETS 中。
  5. 之后,当这些 Telemetry 事件之一被执行时,Telemetry 调用存储的处理函数 Phoenix.LiveDashboard.TelemetryListener.handle_metrics/4
  6. handle_metrics/4 函数将事件格式化为指定的度量,并向 Phoenix.LiveDashboard.MetricsLive LiveView 发送一条消息,然后更新 UI!

这个过程如果很酷,但这里有很多需要解读的地方。如果想深入了解 ETS 中如何存储 Telemetry 事件、执行和处理 Telemetry 事件,请查看本帖

LiveDashboard 请求记录

最后一个我们要设置的 LiveDashboard 功能是 RequestLogger。这个 LiveDashboard 功能可以让我们记录 LiveDashboard 中所有传入的请求。

添加 RequestLogger

我们需要做的就是将以下内容添加到我们的应用程序的 Endpoint 模块中,就在 Plug.RequestId 之前:

# lib/quantum_web/endpoint.ex
plug Phoenix.LiveDashboard.RequestLogger,
  param_key: "request_logger",
  cookie_key: "request_logger"

现在我们可以访问浏览器中的 /dashboard,点击 Request Logger 标签。我们将点击 "enable cookie" 来启用请求日志流的 cookie。现在我们应该看到我们的请求日志流:

live dashboard request-logger

让我们简单了解一下 LiveDashboard 背后 RequestLogger 的工作原理。

LiveDashboard RequestLogger 一探究竟

LiveDashboard 路由挂载一个 LiveView, Phoenix.LiveDashboard.RequestLoggerLive:

# live_dashboard/router.ex
live "/:node/request_logger",
     Phoenix.LiveDashboard.RequestLoggerLive,
     :request_logger,
     opts

RequestLoggerLive LiveView 从端点抓取主应用程序的 PubSub 服务器并订阅 "请求记录器" 主题:

# live_dashboard/live/request_logger_love.ex

def mount(%{"stream" => stream} = params, session, socket) do
  %{"request_logger" => {param_key, cookie_key}} = session

  if connected?(socket) do
    endpoint = socket.endpoint
    pubsub_server = endpoint.config(:pubsub_server) || endpoint.__pubsub_server__()
    Phoenix.PubSub.subscribe(pubsub_server, Phoenix.LiveDashboard.RequestLogger.topic(stream))
  end

  socket =
    socket
    |> assign_defaults(params, session)
    |> assign(
      stream: stream,
      param_key: param_key,
      cookie_key: cookie_key,
      cookie_enabled: false,
      autoscroll_enabled: true,
      messages_present: false
    )

  {:ok, socket, temporary_assigns: [messages: []]}
end

这意味着 RequestLoggerLive LiveView 进程订阅了通过 "请求记录器" 主题广播的任何事件。它为 {:logger, level, message} 事件实现了一个 handle_info/2 函数,并通过更新 socket 和 UI 做出响应:

# live_dashboard/live/request_logger_live.ex
def handle_info({:logger, level, message}, socket) do
  {:noreply, assign(socket, messages: [{message, level}], messages_present: true)}
end

什么时候 {:logger, level, message} 事件会通过 PubSub 广播到这个主题?

当 LiveDashboard 启动时,会给应用程序添加一个 的日志后台。

# live_dashboard/application.ex
defmodule Phoenix.LiveDashboard.Application do
  @moduledoc false
  use Application

  def start(_, _) do
    Logger.add_backend(Phoenix.LiveDashboard.LoggerPubSubBackend) # HERE!

    children = [
      {DynamicSupervisor, name: Phoenix.LiveDashboard.DynamicSupervisor, strategy: :one_for_one}
    ]

    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

Phoenix.LiveDashboard.LoggerPubSubBackend 广播了 log 信息到 RequestLogger PubSub topic 无论何时它收到一条 log 事件:

# live_dashboard/logger_pubsub_backend.ex
def handle_event({level, gl, {Logger, msg, ts, metadata}}, {format, keys} = state)
    when node(gl) == node() do
  with {pubsub, topic} <- metadata[:logger_pubsub_backend] do
    metadata = take_metadata(metadata, keys)
    formatted = Logger.Formatter.format(format, level, msg, ts, metadata)
    Phoenix.PubSub.broadcast(pubsub, topic, {:logger, level, formatted}) # HERE!
  end

  {:ok, state}
end

就是这样!

结语

LiveDashboard 为 Phoenix 开发者的工具箱增加了一个强大的工具。现在,我们可以轻松地可视化我们应用程序,并在开发过程中实时监控其性能和行为。LiveDashboard 代表了 "让开发者幸福" 工具这个不断增长的神殿中的又一个入口,它使 Phoenix 和 Elixir 越来越成为 Web 开发的一个引人注目的选择。

我希望这次功能之旅能让你对 LiveDashboard 感到兴奋,同时我们对它的窥探再次说明了 ETS 和消息传递等 Elixir 语言功能是如何让我们有可能构建强大的系统,并且仍然保持简单而优雅。