Erlang/OTP Event Tracer(ET)深度指南:用 et_collector 与 et_viewer 构建可视化序列图事件追踪
【免费下载链接】otpErlang/OTP项目地址: https://gitcode.com/gh_mirrors/ot/otp
ET(Event Tracer)是 Erlang/OTP 自带的轻量级分布式追踪工具,它把 Erlang 运行时产生的 trace 数据、或应用代码中显式上报的事件,统一采集进一个事件仓库,并渲染成可交互的序列图(sequence chart)。本文基于 OTP 仓库中lib/et的实现与文档,讲解 ET 的两大核心组件(et_collector事件仓库与et_viewer图形查看器)、过滤器(Filter)与字典机制、Trace Client 接入、全局追踪(global tracing)以及两套图形窗口的完整操作手册。读完本文,你将能够自己启动 ET、上报/接入各类事件源、编写自定义视图过滤器,并把 ET 嵌入到多节点系统的实时追踪中。
一、整体架构:Collector 与 Viewer 两大组件
从 et.erl 模块头注释 可以清晰看到,ET 应用由以下几部分构成:
et_collector:事件采集与后端存储。它是一个gen_server进程,接收 trace 数据、经过过滤器后按时间戳有序存放,并为多个订阅进程(如多个 Viewer)提供字典服务与组通信;et_viewer:图形化序列图查看器。它连接到一个 Collector,定期轮询获取新事件,并在显示前对每个事件应用用户自定义过滤器;et_contents_viewer:单个事件的细节查看窗口,通常由et_viewer启动;et_selector:底层库模块,负责把"细节级别(detail level)"翻译成 Erlang trace pattern(match spec),并实现默认过滤器——把原始 Erlang trace 数据转换成#event{}记录;et:面向业务应用的极轻量上报 API(trace_me/4,5等)。
文档中的架构要点是:一个 Collector 可以作为多个 Viewer 的共享后端存储,每个 Viewer 可以对同一份 trace 数据展示不同的视角(例如不同的过滤器和缩放比例)。Collector 与 Viewer 之间的接口是公开的,因此理论上可以编写其它类型的 Viewer,但本文聚焦et_viewer的标准用法。
1.1 启动:et_viewer:start/1
主要启动函数是et_viewer:start/1,默认情况下它会同时启动一个et_collector和一个et_viewer:
% erl -pa et/examples Erlang R13B03 (erts-5.7.4) [64-bit] [smp:4:4] [rq:4] [async-threads:0] [kernel-poll:false] Eshell V5.7.4 (abort with ^G) 1> {ok, Viewer} = et_viewer:start([]). {ok,<0.40.0>}1.2 事件如何流动:轮询 + report_event/6
Viewer 会定期轮询其 Collector,获取更多待显示的事件。事件本身可以由任何进程通过et_collector:report_event/6上报,例如:
2> Collector = et_viewer:get_collector_pid(Viewer). <0.39.0> 3> et_collector:report_event(Collector, 60, my_shell, mnesia_tm, start_outer, 3> "Start outer transaction"), 3> et_collector:report_event(Collector, 40, mnesia_tm, my_shell, new_tid, 3> "New transaction id is 4711"), 3> et_collector:report_event(Collector, 20, my_shell, mnesia_locker, try_write_lock, 3> "Acquire write lock for {my_tab, key}"), 3> et_collector:report_event(Collector, 10, mnesia_locker, my_shell, granted, 3> "You got the write lock for {my_tab, key}"), 3> et_collector:report_event(Collector, 60, my_shell, do_commit, 3> "Perform transaction commit"), 3> et_collector:report_event(Collector, 40, my_shell, mnesia_locker, release_tid, 3> "Release all locks for transaction 4711"), 3> et_collector:report_event(Collector, 60, my_shell, mnesia_tm, delete_transaction, 3> "End of outer transaction"), 3> et_collector:report_event(Collector, 20, my_shell, end_outer, 3> "Transaction returned {atomic, ok}"). {ok,{table_handle,<0.39.0>,16402,trace_ts, #Fun<et_collector.0.62831470>}}注意report_event/6的返回值:第一次调用以 Collector 的 pid 作为句柄,之后可以直接复用返回的table_handle记录(它内嵌了 Ets 表标识和过滤器 fun,从而省去每次向 Collector 进程请求句柄的往返开销,见下文源码走读)。
这段示例实际上是模拟了一次 Mnesia 本地表写事务产生的进程事件,真实事务等价于:
mnesia:transaction(fun() -> mnesia:write({my_tab, key, val}) end).在 et_demo.erl 的sim_trans/0,1中可以看到同样的事件序列被封装为可复用函数。有了这批事件,图形界面中即可看到序列图:
在序列图中,参与事件的**演员(actor)**显示为带名字的竖直条。你可以按住鼠标左键拖动某个 actor 的名字标签并放到其它位置,从而调整 actor 的左右排列顺序:
一个事件既可能是单个 actor 的动作(蓝色文字标签),也可能涉及两个 actor,此时会渲染成从一个 actor 指向另一个 actor 的箭头(红色文字标签)。单击事件标签文字或箭头(按下并松开鼠标左键),会弹出Contents Viewer窗口展示该事件的细节:
二、事件记录:#event{} 的结构
ET 内部统一使用#event{}记录作为事件的中介格式,其字段定义在 et.hrl:
| 字段 | 含义 |
|---|---|
detail_level | 细节级别,0..100。噪声(noise)事件的值偏高,关键事件值偏低 |
trace_ts | trace 数据被生成的时间戳;若原始 trace 数据未携带时间戳,则等于event_ts |
event_ts | 事件记录被创建(解析)的时间戳 |
from | 来源 actor,例如消息的发送方 |
to | 目标 actor,例如消息的接收方 |
label | 事件的简短摘要标签 |
contents | 事件的全部细节,建议使用[{Key, Value}]键值对列表 |
细节级别的上下限在 et_internal.hrl 中定义为0..100。
三、过滤器(Filter)与字典机制
3.1 过滤器接口:与 lists:filtermap/2 一致
ET 在各种场景中大量使用命名过滤器。一个事件 trace 过滤器是一个 Erlang fun,接收 trace 数据作为输入,返回其可能的修改版本:
filter(TraceData) -> false | true | {true, NewEvent} TraceData = Event | erlang_trace_data() Event = #event{} NewEvent = #event{}其语义与标准库lists:filtermap/2的过滤函数完全相同:
false:静默丢弃该 trace 数据;true:trace 数据本身已经是#event{}记录,原样保留;{true, NewEvent}:用NewEvent替换原始 trace 数据。
这样既可以去噪(丢弃不需要的事件),也可以改变事件在视图中的呈现方式。
3.2 Collector Filter 的特别之处
第一个接触到 trace 数据的过滤器是Collector Filter(字典中名字为all的那个过滤器,见 et_internal.hrl)。当事件通过et_collector:report/2(或report_event/5,6)上报时,首先会向 Collector 进程发送消息以获取一个句柄(其中包含 Collector Filter Fun 和 Ets 表标识);随后应用该过滤器,若返回true或{true, NewEvent},事件即被写入 Ets 表。作为优化,后续report调用可以直接使用这个句柄,而不再向 Collector 进程请求。
所有过滤器(无论注册在 Collector 还是 Viewer 中)都必须能处理#event{}记录作为输入;而 Collector Filter(名为all)略有特殊——它的输入还可能是原始的 Erlang trace 数据。
3.3 Collector 的字典服务与订阅传播
Collector 内部维护一个基于 key/value 的字典用于存放过滤器等条目。字典的每次更新都会被传播给所有订阅进程;当某个 Viewer 启动时,它会自动注册为字典更新的订阅者。相关 API(实现在 et_collector.erl):
et_collector:dict_insert(CollectorPid, Key, Val):插入条目并向所有订阅者发送{et, {dict_insert, Key, Val}}消息;若是新订阅者注册,会先把已存在的全部条目逐一发送给它;et_collector:dict_lookup(CollectorPid, Key):查询条目;et_collector:dict_delete(CollectorPid, Key):删除条目并广播;et_collector:dict_match(CollectorPid, Pattern):按 Ets 匹配模式批量查询。
每个 Viewer 同时只有一个激活过滤器,从 Collector 取回的所有事件都会流过该过滤器;编写巧妙的过滤器即可自定义事件在查看器中的形态。
3.4 实战示例:mgr_actors 过滤器
下面这个过滤器来自 et_demo.erl,它把 actor 名字mnesia_tm、mnesia_locker替换为语义化的trans_mgr、lock_mgr,并把原始值备份到 contents 中,其余字段保持不变:
mgr_actors(E) when is_record(E, event) -> Actor = fun(A) -> case A of mnesia_tm -> trans_mgr; mnesia_locker -> lock_mgr; _ -> A end end, {true, E#event{from = Actor(E#event.from), to = Actor(E#event.to), contents = [{orig_from, E#event.from}, {orig_to, E#event.to}, {orig_contents, E#event.contents}]}}.把它插入正在运行的 Collector:
4> Fun = fun(E) -> et_demo:mgr_actors(E) end. #Fun<erl_eval.6.13229925> 5> et_collector:dict_insert(Collector, {filter, mgr_actors}, Fun). ok此时所有 Viewer 的Filter菜单都会出现一个新条目mgr_actors。选中它,会弹出一个新的 Viewer 窗口,用该过滤器渲染同一份 trace 数据:
如果想查看事件的"细枝末节",可以单击事件打开Contents Viewer;该窗口内同样有过滤器菜单,允许从其它视角检视同一个事件。例如点击new_tid事件,会弹出以mgr_actors视角展示该事件的 Contents Viewer:
在 Filters 菜单中选择all,则会弹出以 Collector 视角(即原始视图)展示同一事件的窗口:
四、Trace clients:把各种数据源接入 Collector
除了手动调用report_event/5,6,ET 还提供了一系列现成的 trace client,把各种来源的 trace 数据转发进 Collector:
- 文件存取:Collector 中托管的事件可以保存到文件,之后通过 Viewer 的
File菜单中的save/load,或通过et_collectorAPI 重新载入。实现上文件由disk_log承载:save_event_file/3支持existing | write | append(事件范围/写模式)与keep | clear(写完后是否清空事件表)等选项,load_event_file/2按 chunk 读取并逐条report(见 et_collector.erl); - Erlang 内建 trace:可以利用 Erlang 虚拟机内建的 trace 机制对运行中的系统做实时追踪,trace 数据可输出到文件或端口。相关接口参见
erlang:trace/3、erlang:trace_pattern/3、dbg与ttb; - 对应的 trace client:
et_collector:start_trace_client/3会启动能读取上述 Erlang trace 文件/端口格式的客户端,并把数据重定向到 Collector(实现中对file类型会等待读到end_of_trace后返回file_loaded,对ip等端口类型返回{trace_client_pid, Pid})。
默认的 Collector Filter 负责把原始 Erlang trace 数据格式转换为#event{}记录。如果希望走不同的转换路径,可以完全自己编写 Collector Filter;但更省力的做法是先调用et_selector:parse_event/2应用默认过滤逻辑,再对其输出做二次加工(et_selector.erl 中parse_event/2即默认过滤器的实现核心)。
反过来,如果 trace 数据来自任何自有格式(文件、数据库、消息流……),你完全可以编写自定义 trace client 读取数据并喂给 Collector——只要在调用report/2之前把数据转成#event{}(或替换默认 Collector Filter 来完成转换)。
五、全局追踪(Global tracing):一键铺开多节点 trace
在多个节点上手工搭建 Erlang tracer、再把 trace client 接到各 tracer 的端口上,非常繁琐。ET 为此提供了全局追踪能力:
- 启用后,
et_collector进程会监控 Erlang 节点:每当有节点接入(nodeup),就在新节点上自动启动一个 Erlang tracer(通过 port tracer),同时在 Collector 所在节点启动对应的 trace client,把 trace 事件自动转发回 Collector; - 激活方式:给
et_collector或et_viewer设置布尔参数trace_global = true; - 并发匿名 Collector 的数量不受限制,但全局 Collector 全局唯一——因为它的名字注册在
global中(源码中trace_global为 true 时以{global, ?MODULE}注册,见 et_collector.erl)。可通过et_collector:get_global_pid/0获取其 pid。
5.1 et:trace_me/4,5:为应用埋点的"免费"函数
为了进一步简化追踪,可以借助et:trace_me/4,5系列函数。它们设计为从其它应用的关键位置调用:
trace_me(DetailLevel, From, To, Label, Contents) -> hopefully_traced从 et.erl 的实现 可见,这些函数极其轻量——除了返回原子hopefully_traced之外什么都不做。它们存在的意义就是被 trace:由于调用方显式提供了#event{}各字段的值,默认 Collector Filter 无需任何用户自定义过滤器,就能自动生成定制的事件记录。相关说明:
trace_me/4等价于trace_me/5且From、To都设为同一个 actor;phone_home/4,5与report_event/4,5目前为向后兼容保留,均委托给trace_me;DetailLevel取 0..100 的整数;Label建议用原子作简短摘要;Contents建议用[{Key, Value}]列表以方便后续加工;- 正常运行时这些调用几乎零开销;需要追踪时,可以显式对它们开启 trace,或组合使用
trace_global与trace_pattern。
5.2 trace_pattern:控制全局细节级别
设置trace_pattern后,它会自动在所有已连接节点上激活。它的好处是提供了一种非常简单的方式来压低产生的 trace 数据量:你可以在源头就控制追踪的细节级别。
这一点与 Viewer 中的"Detail Level"滑块互补——滑块只控制显示哪些事件;而如果把trace_pattern的细节级别调低,大量 trace 数据根本不会生成,自然也就不会经 socket 发到 trace client、存进 Collector。
从 et_selector.erl 的 make_pattern/1 可以看到细节级别如何翻译成 match spec:
min:空 match spec[],即关闭对et:trace_me/4,5的追踪;max:匹配所有调用;- 整数
X:匹配所有detail level < X的调用(match spec 条件{'<', '$1', DetailLevel})。
Collector 的change_pattern/2则通过rpc:multicall把新 pattern 同步到所有 trace 节点(et_collector.erl)。
5.3 实机演示:追踪真实的 Mnesia 事务
et_demo提供了两个级别的演示:sim_trans/0是纯模拟(手动report_event),而live_trans/0则做真实追踪:启动带trace_global的 Viewer、创建 Mnesia 表、调用trace_mnesia/0用dbg对 Mnesia 模块设置调用追踪并开启send/receive/procs/timestamp标志,然后执行一次真实事务(et_demo.erl)。et_demo:start/1的默认选项也展示了常用的 Viewer 配置:
Options = [{trace_global, true}, {parent_pid, undefined}, {max_actors, infinity}, {max_events, 1000}, {active_filter, module_as_actor}], et_viewer:start_link(filters() ++ Options ++ ExtraOptions).六、et_viewer 窗口:菜单与快捷键速查
et_viewer中几乎所有功能都有键盘快捷键,快捷键显示在菜单条目后的括号中。例如按键r等价于选择Viewer->Refresh。
6.1 File 菜单
| 条目 | 作用 |
|---|---|
Clear all events in the Collector | 删除 Collector 中存储的全部事件,并通知所有相连的 Viewer |
Load events to the Collector from file | 从文件向 Collector 装载事件,并通知所有相连 Viewer |
Save all events in the Collector to file | 把 Collector 中全部事件保存到文件 |
Print setup | 编辑打印设置(纸张、布局) |
Print current page | 打印当前页的事件(页大小取决于所选纸张类型) |
Print all pages | 打印全部事件 |
Close this Viewer | 关闭本 Viewer 窗口,保留其它 Viewer 窗口与 Collector 进程 |
Close other Viewers, but this | 保留本 Viewer 及其 Collector,关闭连接同一 Collector 的其它 Viewer |
Close all Viewers and the Collector | 关闭 Collector 及所有相连 Viewer |
6.2 Viewer 菜单
| 条目 | 作用 |
|---|---|
First/Last | 本 Viewer 滚动到 Collector 中的第一个/最后一个事件 |
Prev/Next | 本 Viewer 向后/向前翻一页 |
Refresh(快捷键r) | 清空本 Viewer 并重新从 Collector 读取事件 |
Up/Down | 向后/向前滚动少量事件 |
Display all actors. | 重置隐藏/高亮 actor 的设置 |
6.3 Collector 菜单
与 Viewer 菜单功能相同,但作用于所有Viewer:First/Last/Prev/Next让所有 Viewer 滚动,Refresh清空所有 Viewer 并重新读取。
6.4 Filters and scaling 菜单
| 条目 | 作用 |
|---|---|
ActiveFilter (=) | 以当前激活过滤器和缩放比例新开一个 Viewer 窗口 |
ActiveFilter (+) | 以当前激活过滤器、更大缩放比例新开 Viewer 窗口 |
ActiveFilter (-) | 以当前激活过滤器、更小缩放比例新开 Viewer 窗口 |
all (0) | 以 Collector Filter 作为激活过滤器新开 Viewer,查看 Collector 中全部事件 |
AnotherFilter (2) | 字典中插入的更多过滤器会依次出现在此菜单;第二个过滤器快捷键为 2,第三个为 3,依此类推;条目按名字排序 |
6.5 滑块与单选按钮
| 控件 | 作用 |
|---|---|
Hide From=To | 勾选后隐藏所有 from-actor 等于 to-actor 的事件(这类事件又称动作 actions) |
Hide (excluded actors) | 勾选后隐藏所有 actor 被标记为排除的事件。被排除的 actor 正常显示时名字带圆括号() |
Detail level | 控制 Viewer 的分辨率:只显示 detail level小于滑块值的事件(默认 100 = 最大,显示全部) |
6.6 其它交互特性
- 垂直滚动:鼠标滚轮与上下方向键小幅滚动;PageUp/PageDown、Home/End 大幅滚动;
- 显示事件细节:在事件标签或箭头上单击鼠标左键,弹出 Contents Viewer 显示事件内容;
- 高亮 actor(切换):在 actor 名字标签上单击左键,名字会被方括号
[]括起。当有一个或多个 actor 被高亮时,只显示与这些 actor 相关的事件,其余全部隐藏; - 排除 actor(切换):在 actor 名字标签上单击右键,名字被圆括号
()括起。被排除后,与该 actor 相关的所有事件被隐藏;若勾选了Hide (excluded actors),连名字标签和对应竖线也会被隐藏; - 移动 actor:在 actor 名字标签上按住鼠标左键拖动,到新位置松开即可;
- 显示全部 actor:按键
a,重置隐藏/高亮设置。
七、Contents Viewer 窗口:单事件检视与搜索
7.1 File 菜单
Close:关闭本窗口;Save:把本窗口内容保存到文件。
7.2 Filters 菜单
ActiveFilter:以相同激活过滤器新开一个 Contents Viewer 窗口;AnotherFilter (2):字典中新增的过滤器依次出现在此菜单(编号 2、3……,按名字排序)。
7.3 Hide 菜单
Hide actor in viewer:已知 actor 在 Viewer 中以命名竖条显示;隐藏 actor 后其竖条被移除,Viewer 自动刷新。注意:只有当max_actors阈值已到达时,隐藏 actor 才真正有意义——此时被隐藏的 actor 会以"UNKNOWN"形式显示;若阈值未到达,该 actor 仍会以竖条重新出现在 Viewer 中;Show actor in viewer:把 actor 加为 Viewer 的已知 actor,并赋予其独立竖条。
7.4 Search 菜单
Forward from this event:把当前事件设为 Viewer 的第一个事件,并切换到正向搜索模式;该事件的 actor(from、to 或两者)会被加入选定 actor 列表;Reverse from this event:把当前事件设为 Viewer 的第一个事件,并进入反向搜索模式;注意反向模式下事件按倒序显示;Abort search. Display all:无论是否正在搜索,切换到显示全部事件的模式并中止搜索。
八、配置参数速查:事件排序与 Collector 选项
Collector 中 Ets 表里的事件按时间戳排序,用哪个时间戳由event_order参数控制(见 et_collector.erl 的start_link/1注释与 make_key/2 的实现):
trace_ts(默认):按 trace 数据被生成的时间排序;event_ts:按 trace 数据被解析(转换为#event{})的时间排序。
若 trace 数据缺少时间戳,trace_ts会取event_ts的值。
et_collector:start_link/1支持的选项与默认值汇总如下:
| 选项 | 默认值 | 说明 |
|---|---|---|
{parent_pid, Pid} | self() | Collector 链接的父进程 |
{event_order, trace_ts \| event_ts} | trace_ts | 事件排序时间戳 |
{dict_insert, {filter, all}, Fun} | 默认et_selector:parse_event/2 | 设置 Collector Filter(可替换/追加命名过滤器) |
{dict_insert, {subscriber, Pid}, Val} | — | 注册字典订阅者 |
{dict_insert, Key, Val}/{dict_delete, Key} | — | 字典读写 |
{trace_client, {event_file, File}} | — | 启动文件 trace client |
{trace_client, {dbg_trace_type(), Params}} | — | 启动 dbg 格式 trace client(file/ip/follow_file 等) |
{trace_global, boolean()} | false | 是否启用全局追踪 |
{trace_pattern, Pattern} | undefined | 全局 trace pattern(min/max/整数或 match spec) |
{trace_port, integer()} | 4711 | 全局追踪使用的端口基址(冲突时自动递增) |
{trace_max_queue, integer()} | 50 | trace 端口队列上限 |
Viewer 侧常用选项(et_demo:start/1可作参考)还包括max_actors(如infinity)、max_events、active_filter、hide_actions、title等。
九、源码走读:Collector 内部是如何工作的
从 et_collector.erl 的实现可以确认文档描述的全部机制:
- 存储结构:
init_tables/1创建两张ordered_set类型的公共 Ets 表——et_events(事件表)与et_dict(字典表),键位置均为 1(L1128-L1131)。ordered_set保证事件可以按时间戳键前后迭代; - 上报路径:
report/2先从 Collector 获取table_handle(内含事件表 id、event_order与过滤器 fun),随后应用过滤器:false直接丢弃,true/{true, NewEvent}则用make_key/2生成排序键后ets:insert;若返回其它异常值,会构造一个bad_filter事件存入(L474-L524); - 句柄优化:
get_table_handle每次从字典中读取名为all的 Collector Filter 打包进句柄(L1188-L1194),这样后续report无需再与 Collector 进程同步交互; - 订阅通知:字典插入/删除会向订阅者广播
{et, Msg};新增订阅者会先收到全部既有条目再收到新条目;Collector 还会定期check_size/1,当事件表大小变化时向订阅者发送{more_events, Size}(L1526-L1536),这正是 Viewer"轮询到新事件"的通知机制; - 遍历接口:
iterate/3,5支持从first/last或任意事件键出发正向/反向遍历,Limit为正整数、infinity、负整数或'-infinity'(L888-L1034); - 全局追踪:
init_global/1在trace_global为 true 时启动本地 dbg tracer、net_kernel:monitor_nodes(true)并枚举已有节点;handle_info({nodeup, Node}, ...)通过rpc:call在远端节点启动monitor_trace_port/2(port tracer),再在本地start_trace_client(self(), ip, {Host, Port})建立消费端,并在该节点上应用 trace pattern(L1133-L1146、L1307-L1383)。默认的全局 trace pattern 为max。
十、进一步探索
- 完整示例与过滤器集合:et_demo.erl(
sim_trans模拟、live_trans实机 Mnesia 追踪、filters/0内置过滤器族); - 事件记录结构:et.hrl;
- 上报 API:et.erl、采集存储 API:et_collector.erl;
- pattern 与默认转换:et_selector.erl;
- 图形界面实现:et_wx_viewer.erl 与 et_wx_contents_viewer.erl。
ET 的价值在于把"事件采集—存储—过滤—可视化"整条链路做成标准件:业务代码埋点近乎零成本,多节点全局追踪一键开启,而过滤器机制又让同一份 trace 数据可以衍生出无数种观察视角——这正是调试分布式系统、分析 Mnesia 等内部协议行为时最实用的工具箱。
【免费下载链接】otpErlang/OTP项目地址: https://gitcode.com/gh_mirrors/ot/otp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考