Erlang/Elixir 可观测性:日志、Telemetry 指标与分布式追踪

Erlang/Elixir 可观测性:日志系统(Logger 配置/处理器/格式化/结构化日志)、Telemetry 事件与指标(attach/detach/execute/计数器/直方图/最后值)、分布式追踪(OpenTelemetry/Span 上下文传播)、Observer/CLI 诊断工具(observer_cli/recon/vmstats)、监控告警(Prometheus/Grafana 集成)、BEAM VM 内部指标(reductions/内存/进程/ETS)、日志聚合与 ELK/Loki 集成、可观测性最佳实践。

引言

BEAM 虚拟机运行着数十万甚至数百万个轻量进程,每个进程都有自己的堆、邮箱和状态。监控这样一个「进程海洋」需要不同于传统系统的工具:不是看 CPU 和内存的宏观曲线,而是看 reductions、消息队列长度、ETS 表大小、GC 频率等 BEAM 特有指标。Elixir 的 Telemetry 库提供了标准化的指标发射机制,配合 Logger 的结构化输出和 OpenTelemetry 的分布式追踪,构成了 Erlang/Elixir 系统的完整可观测栈。本文从日志到指标、从诊断工具到告警集成,给 BEAM 系统的观测一张地图。

前置:/elixir-otp-supervision-tasks/(OTP 监督树)、/erlang-concurrency-actors/(并发与容错)。


目录


1. Logger 日志系统:配置与处理器

1.1 Elixir Logger 配置

# config/config.exs
config :logger,
  level: :info,
  backends: [:console]

config :logger, :console,
  format: "$time $metadata[$level] $message\n",
  metadata: [:request_id, :user_id]

1.2 日志级别

级别用途
:debug开发调试
:info正常运行信息
:warning需要注意但非错误
:error错误需处理

1.3 自定义后端

# 写入文件
config :logger,
  backends: [{LoggerFileBackend, :error_log}]

config :logger, :error_log,
  path: "/var/log/app/error.log",
  level: :error

# 或使用 structured logging backend(如 logger_json)

记忆 Elixir Logger 分 debug/info/warning/error 四级,config 配置 backends(console/file/自定义)和 metadata;生产环境推荐 logger_json backend 输出结构化 JSON。


2. 结构化日志与 JSON 输出

2.1 为什么结构化

非结构化:"User 42 logged in from 192.168.1.1 at 10:00"
结构化:{"@timestamp":"2024-01-15T10:00:00Z","event":"login","user_id":42,"ip":"192.168.1.1"}
# JSON 可被 Elasticsearch/Loki 索引和查询

2.2 logger_json 配置

# mix.exs: {:logger_json, "~> 5.0"}
config :logger_json, :backend,
  metadata: :all,
  formatter: LoggerJSON.Formatters.BasicLogger

config :logger,
  backends: [LoggerJSON.Backend]

2.3 日志最佳实践

# 用 metadata 而非字符串拼接
Logger.info("User login", user_id: user.id, ip: conn.remote_ip)

# 统一 request_id 贯穿请求
Logger.metadata(request_id: request_id)

# 错误日志带异常和堆栈
Logger.error("Payment failed", error: inspect(e), stacktrace: __STACKTRACE__)

记忆 结构化日志 = logger_json backend + metadata 传键值对(不用字符串拼接);metadata 设 request_id 贯穿请求链路;错误日志带 inspect(e) 和 STACKTRACE。


3. Telemetry 事件与指标体系

3.1 Telemetry 核心概念

Telemetry 是「标准化事件发射库」
不是监控工具本身,而是数据 producer
第三方库(Ecto/Phoenix/Redix)内置 Telemetry 事件
应用代码 attach 处理器来消费事件 → 生成指标

3.2 基本用法

# 定义事件(通常在库代码中)
:telemetry.execute([:web, :request], %{duration: 230}, %{path: "/users"})

# 附加处理器(应用代码中订阅)
:telemetry.attach(
  "web-request-handler",
  [:web, :request],
  &handle_event/4,
  nil
)

def handle_event([:web, :request], measurements, metadata, _config) do
  # measurements = %{duration: 230}
  # metadata = %{path: "/users"}
  Statsd histogram("web.request.duration", measurements.duration,
    tags: ["path:#{metadata.path}"])
end

3.3 常用内置事件

[:phoenix, :endpoint, :stop]          # Phoenix 请求完成
e[:phoenix, :router_dispatch, :stop]   # 路由分派完成
e[:phoenix, :socket_connected]         # WebSocket 连接
e[:ecto, :query]                       # Ecto 查询
e[:vm_memory, :total]                  # VM 内存

记忆 Telemetry = 事件发射标准库(execute 发射、attach 订阅处理);不是监控工具是数据桥梁;Phoenix/Ecto 内置大量事件;处理器收到 measurements(数值)和 metadata(标签)后转指标。


4. 指标类型:计数器、直方图与最后值

4.1 Telemetry.Metrics 定义

# 定义指标规格
metrics = [
  counter("web.request.count"),
  sum("web.request.body_bytes", unit: :byte),
  last_value("vm.memory.total", unit: :byte),
  summary("web.request.duration",
    unit: {:native, :millisecond},
    tags: [:path, :method]
  ),
  distribution("web.request.duration",
    buckets: [10, 50, 100, 200, 500],
    unit: {:native, :millisecond}
  )
]

4.2 指标类型对比

类型描述用例
counter只增计数请求数、错误数
sum累加值传输字节数
last_value最新值内存使用量
summary采样统计响应时间分位
distribution分桶分布P50/P95/P99

记忆 五种指标——counter(只增计数)、sum(累加)、last_value(最新状态)、summary(分位采样)、distribution(分桶直方图);Telemetry.Metrics 定义规格,Reporter(如 StatsD/Prometheus)负责导出。


5. Prometheus 与 Grafana 集成

5.1 Prometheus 导出

# mix.exs: {:prometheus_ex, "~> 3.0"}, {:prometheus_plugs, "~> 1.0"}

# 定义 metrics
defmodule MyApp.Metrics do
  use Prometheus.Metric

  defsetup do
    Counter.declare(name: :http_requests_total, labels: [:method, :path])
    Histogram.declare(name: :http_request_duration_seconds, labels: [:method])
  end
end

# 更新
def handle_request(conn) do
  Counter.inc(name: :http_requests_total, labels: [conn.method, conn.path_info])
  # ...
end

5.2 /metrics 端点

# Phoenix 路由
get "/metrics", Prometheus.PlugsExporter

# Prometheus scrape 配置
scrape_configs:
  - job_name: 'myapp'
    static_configs:
      - targets: ['localhost:4000']

5.3 Grafana 看板

# 导入 BEAM VM 看板模板
# 关键面板:
#   - 请求 QPS / 错误率
#   - P50/P95/P99 响应时间
#   - BEAM 进程数 / reductions 每秒
#   - 内存使用(atom / binary / ets / process)
#   - ETS 表大小

记忆 Prometheus 集成——prometheus_ex 定义 Counter/Histogram、PlugsExporter 暴露 /metrics、Prometheus scrape 拉取、Grafana 看板展示;BEAM 特有面板看 reductions/进程/内存/ETS。


6. OpenTelemetry 分布式链路追踪

6.1 核心概念

Trace:一次请求的完整链路
Span:链路中的一个操作(如 DB 查询)
SpanContext:Trace ID + Span ID + Flags(跨进程传递)
Baggage:跨 Span 传递的键值对

6.2 Elixir 集成

# mix.exs: {:opentelemetry, "~> 1.0"}, {:opentelemetry_exporter, "~> 1.0"}

# 初始化 Trace
require OpenTelemetry.Tracer

OpenTelemetry.Tracer.with_span "process_order" do
  OpenTelemetry.Tracer.set_attribute("order.id", order_id)

  OpenTelemetry.Tracer.with_span "db_query" do
    Repo.get(Order, order_id)
  end
end

6.3 Phoenix 自动追踪

# mix.exs: {:opentelemetry_phoenix, "~> 1.0"}
# 自动追踪 HTTP 请求、DB 查询、外部调用
# 导出到 Jaeger/Tempo/Datadog

记忆 OpenTelemetry 在 Elixir 中用 with_span 嵌套追踪;opentelemetry_phoenix 自动埋点;Span 包含 attribute(标签)和 event(时间点);导出到 Jaeger/Tempo 查看链路。


7. Observer 与 CLI 诊断工具

7.1 observer_cli(终端版 Observer)

$ mix deps.get observer_cli
$ iex -S mix
iex> :observer_cli.start()

# 实时显示:
# - 进程列表(内存/ reductions /消息队列)
# - ETS 表
# - 端口 / 调度器负载
# - Mnesia 表

7.2 recon

# 内存分析
:recon.proc_count(:memory, 10)   # 内存占用最高的 10 个进程
:recon.proc_count(:message_queue_len, 10)  # 消息队列最长的进程

# 节点状态
:recon.node_stats(1000, 10)       # 每秒采样,共 10 次

7.3 vmstats

# 定期输出 VM 统计到 StatsD
# 进程数 / reductions / atom / binary / ets / port
# 适合长期趋势监控

记忆 诊断工具——observer_cli 终端实时监控(进程/ETS/调度器)、recon 内存和消息队列排查、vmstats 长期趋势输出;生产环境首选 CLI 工具(observer GUI 需 wx)。


8. BEAM VM 内部指标解析

8.1 核心指标

指标含义健康阈值
reductions函数调用次数(调度单位)平稳增长
process_count进程数< 系统上限
message_queue_len消息队列长度< 1000
memory.total总内存< 物理内存 80%
memory.binary堆外 binary 内存不泄漏增长
memory.etsETS 表内存不泄漏增长
gc.countGC 次数不激增

8.2 获取方式

# 进程信息
Process.info(pid, [:message_queue_len, :memory, :reductions])

# VM 内存
:erlang.memory()      # => [total: ..., processes: ..., binary: ..., ets: ...]

# 统计
:erlang.statistics(:reductions)
:erlang.system_info(:process_count)

记忆 BEAM 核心指标——reductions(调度单位)、process_count(进程数)、message_queue_len(消息队列 >1000 危险)、memory 分 total/processes/binary/ets(binary/ets 泄漏增长要排查)、gc.count(激增说明内存抖动)。


9. 日志聚合与告警体系

9.1 日志流架构

App (JSON 日志) → Filebeat/Fluent Bit → Logstash → Elasticsearch → Kibana
                                          ↓
                                        Loki (轻量替代)

# BEAM 应用日志通过 stdout/stderr → 容器收集器 → 聚合存储

9.2 告警规则

# Prometheus Alertmanager
- alert: HighErrorRate
  expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05
  for: 5m
  annotations:
    summary: "错误率 > 5%"

- alert: MemoryLeaking
  expr: rate(vm_memory_total_bytes[10m]) > 1000000
  for: 10m
  annotations:
    summary: "内存持续泄漏"

- alert: MessageQueueBlocked
  expr: max_over_time(process_message_queue_length[5m]) > 10000
  annotations:
    summary: "进程消息队列阻塞"

记忆 日志聚合——App 输出 JSON → Filebeat → ES/Loki → Kibana/Grafana;告警用 Prometheus 规则——错误率>5%、内存持续泄漏(rate>阈值)、消息队列阻塞(>10000);BEAM 特有告警:进程数接近上限、binary 内存泄漏。


10. 速查表与一句话记忆

概念一句话
LoggerElixir 日志系统,分四级
logger_json结构化 JSON 日志
Telemetry标准化事件发射
execute发射事件
attach订阅处理
counter只增计数
histogram时间分布
last_value最新状态
Prometheus/metrics 端点
OpenTelemetrywith_span 追踪
observer_cli终端实时监控
recon内存/队列排查
reductionsBEAM 调度单位
message_queue消息队列阻塞指标

一句话记忆:Elixir 可观测性三支柱——Logger(结构化 JSON 日志 + metadata/request_id)、Telemetry(execute 发射 + attach 消费转指标,Phoenix/Ecto 内置大量事件)、OpenTelemetry(with_span 嵌套追踪);指标五种类型——counter/sum/last_value/summary/distribution,Prometheus 拉取 /metrics、Grafana 看 BEAM 特有面板;诊断工具 observer_cli 实时看进程/ETS/调度器、recon 排查内存和消息队列;BEAM 核心指标 reductions(调度)、message_queue_len(>1000 危险)、memory binary/ets(泄漏排查);日志走 JSON → Filebeat → Loki/ES,告警用 Prometheus 规则(错误率/内存泄漏/队列阻塞)——「百万进程的世界,reductions 和消息队列长度是生命线」。


延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「erlang」更多文章

  1. OTP 应用设计模式:监督树结构、release 打包与热升级
  2. Erlang/Elixir 安全加固:加密、认证与分布式信任
  3. Ecto 高级查询与数据库工程:关联、多态与性能优化