Erlang 版 CAT 客户端(erlcat)接入与使用指南:NIF 封装 C 客户端的完整实践

📅 发布时间:2026/9/20 12:04:24
Erlang 版 CAT 客户端(erlcat)接入与使用指南:NIF 封装 C 客户端的完整实践
可观测性指标监控告警APM后端链路追踪【免费下载链接】catCAT 作为服务端项目基础组件提供了 Java, C/C, Node.js, Python, Go 等多语言客户端已经在美团点评的基础架构中间件框架MVC框架RPC框架数据库框架缓存框架等消息队列配置系统等深度集成为美团点评各业务线提供系统丰富的性能指标、健康状况、实时告警等。项目地址https://gitcode.com/gh_mirrors/ca/cat点击查看免费下载本文基于美团点评 CAT 监控体系中 Erlang 客户端的官方中文文档系统讲解如何在 Erlang/OTP 项目中接入 CAT 分布式监控从 rebar3 依赖引入与 NIF 编译到#cat_config{}各配置项的语义与源码级实现再到 Event、Metric、Transaction、Heartbeat、远程调用链路等核心 API 的完整用法。读完本文你将能够在自己的 Erlang 服务中完成 CAT 客户端的初始化、上下文隔离、业务埋点与跨进程/跨服务调用链追踪并理解每个 API 在底层 C 客户端ccat中的真实行为。一、erlcat 是什么一份覆盖 C 客户端全部 API 的 Erlang 封装在 lib/erlang/README_zh_CN.md 的定位中erlcat 是 CAT 监控体系的 Erlang 客户端其设计目标是实现几乎所有 C 客户端的 API文档未涉及的方法可参考 C 端即 lib/c/README.md说明。这意味着 erlcat 并非从零实现而是通过Erlang NIFNative Implemented Function直接桥接 ccat 这一 C 语言客户端内核。从仓库结构可以清晰看到这条技术链路Erlang 层erlcat.erl 声明全部对外 API 并通过-on_load(init/0)在模块加载时定位并加载 NIF 动态库erlcat.hrl 定义#cat_config{}记录与编码器常量。NIF 桥接层erlcat.c 将 Erlang 侧的erlcat:xxx调用翻译为 ccat 的 C API 调用并管理 Transaction、Context 两类资源句柄erlcat_transaction、erlcat_context。C 内核c_src/src/ccat/下的client.c、context.c、message_aggregator_*.c、message_sender.c、router_json_parser.c等实现消息树构建、聚合、采样、发送与路由发现。因此理解 erlcat 的配置项与 API 行为本质上是在理解 ccat 的配置模型——两者共享同一份CatClientConfig结构与默认值。二、安装rebar3 依赖引入与 NIF 编译2.1 在 rebar.config 中声明依赖官方文档给出的方式是在项目的rebar.config中直接通过 git 依赖引入 erlcat同时配置 shell 启动的应用列表{erl_opts, [debug_info]}. {deps, [ % 添加 erlcat 依赖 {erlcat,{git,https://github.com/glasses1989/erlcat.git,master}} ]}. {shell, [ % {config, [{config, config/sys.config}]}, {apps, [demo]} ]}.需要说明的是上述 git 地址来自官方文档的历史示例在当前仓库中 erlcat 已经以源码形态合入见 lib/erlang/实际使用时也可以将本仓库的lib/erlang目录作为本地依赖或子项目引入效果等价。2.2 编译与 NIF 构建依赖引入后执行$ rebar3 compilerebar3 compile触发两段构建流水线。第一段是 Erlang 源码编译第二段是 C 代码编译——这由 rebar.config 中的 hooks 驱动{pre_hooks, [{(linux|darwin|solaris), compile, make -C c_src}, {(freebsd), compile, gmake -C c_src}]}. {post_hooks, [{(linux|darwin|solaris), clean, make -C c_src clean}, {(freebsd), clean, gmake -C c_src clean}]}.其中 Linux/macOS 使用make -C c_srcFreeBSD 使用gmake。真正的编译规则在 c_src/Makefile 中它通过erl -noshell -s init stop -eval ...动态探测当前 OTP 的ERTS_INCLUDE_DIRerts- /include与ERL_INTERFACE_INCLUDE_DIR用 gcc/cc 以-O3 -stdc99 -fPIC编译全部.c源文件最终链接为共享库priv/erlcat.so。2.3 动态库的加载时机NIF 库的加载是自动完成的erlcat.erl 中的init/0通过code:priv_dir(erlcat)定位priv/erlcat.so再调用erlang:load_nif(SoName, 0)完成加载。模块中所有 API 都声明为?NOT_LOADED桩即erlang:nif_error一旦 NIF 加载失败任何调用都会立即抛出{not_loaded, ...}错误便于快速定位编译问题。三、初始化init_cat与#cat_config{}全配置详解3.1 最小初始化%% 采用默认配置 erlcat:init_cat(appkey, #cat_config{})appkey即 CAT 服务端为你的业务应用分配的标识有严格的字符限制appkey 只能包含英文字母 (a-z, A-Z)、数字 (0-9)、下划线 (_) 和中划线 (-)3.2 配置项默认值源码为准官方文档列出的默认值与 erlcat.hrl 中#cat_config{}记录的定义完全一致配置字段默认值含义encoder_type1消息编码器类型1 二进制0 文本enable_heartbeat1是否启用内置心跳上报enable_sampling1是否启用采样聚合enable_multiprocessing0是否启用 ccat 上下文管理器消息树功能enable_debugLog0是否输出调试日志-record(cat_config, {encoder_type1, enable_heartbeat1, enable_sampling1, enable_multiprocessing0, enable_debugLog0}).当传入#cat_config{}时erlcat.erl 的init_cat/2会把记录解构为六个原子参数再委托给 NIF 函数init_cat/6。而 erlcat.c 的initCatClient依次将 6 个整数写入CatClientConfig结构体默认值来自DEFAULT_CCAT_CONFIG最终调用catClientInitWithConfig(appKey, config)。也就是说Erlang 侧的 5 个配置项在底层就是 ccat 的CatClientConfig五个字段其定义可见 client.h。3.3 采样聚合enable_sampling采样聚合在默认情况下是开启的erlcat:init_cat(appkey, #cat_config{enable_sampling1})从源码看enableSampling直接影响三层行为聚合器判断message_aggregator.c中if (!g_config.enableSampling) { ... }决定是否把同秒同 key 的消息聚合后一次性上报发送侧采样message_sender.c中if (g_config.enableSampling hitSample())决定消息树是否被随机采样丢弃以控制上报量问题消息豁免client.h注释说明包含problemstatus 非 0消息的消息树不会被采样但高并发下也可能因此造成网络压力。3.4 编码器encoder_type二进制与文本的选择erlcat:init_cat(appkey, #cat_config{encoder_type1})encoder_type有两个取值对应 erlcat.hrl 中的常量?ENCODE_TEXT, 0与?ENCODE_BINARY, 11二进制编码器默认值推荐用于高版本 CAT 服务端编码效率高、传输体积小0文本编码器仅用于适配早期版本的 CAT 服务端。官方文档特别强调高版本 CAT 服务端必须采用默认的二进制编码文本方式会造成数据传输失败。底层对应encoder_binary.c与encoder_text.c两个实现NIF 层将encoderType写入配置后由 ccat 选择对应编码器。3.5 协程模式enable_multiprocessing与消息树erlcat:init_cat(appkey, #cat_config{enable_multiprocessing0})这一项与 Erlang 的并发模型直接相关。官方文档说明ccat 内部使用ThreadLocal存储 Transaction 栈以构建消息树因此默认禁用 ccat 的上下文管理器即禁用消息树功能enable_multiprocessing 0。从源码看message_id.c中if (g_config.enableMultiprocessing)会改变 MessageId 的生成/复用策略。这里的工程背景是Erlang 进程天然轻量、数量庞大与 C 层的线程模型并不一一对应如果强行依赖 ThreadLocal 栈构建消息树多个 Erlang 进程共享底层线程时会发生数据串扰。因此 erlcat 采用每个 Erlang 进程显式持有上下文对象见下文第四节的方式隔离消息树这也是enable_multiprocessing默认关闭的原因。3.6 调试日志enable_debugLogerlcat:init_cat(appkey, #cat_config{enable_debugLog1})开启后 ccat 的_CLog_debugInfo日志会被输出到控制台client_config.c 会打印 encoder、sampling、multiprocessing、heartbeat 等配置值适合在接入初期排查路由发现、消息发送问题生产环境建议关闭以免日志刷屏。四、进程上下文解决多进程数据混乱的关键4.1 为什么需要显式上下文官方文档指出每个 Erlang 进程都需要初始化上下文对象用于解决多个进程共享上下文造成的事务等数据混乱的问题。因为 NIF 底层 ccat 用 ThreadLocal 存储消息树状态而 Erlang 调度器会把大量轻量进程复用到少量调度线程上——若不显式隔离进程 A 的 Transaction 栈可能被进程 B 的数据污染。4.2 上下文的标准用法%% 创建上下文对象放入进程字典 ErlCatContext erlcat:new_context(), put(erlcat_process_context, ErlCatContext).在 erlcat.c 中new_context会先检查isCatEnabled()未初始化则返回cat_unable原子然后为当前调用分配一个erlcatContext_res资源并调用 ccat 的newCatContext()创建独立的CatContext包含独立的 message tree 与 transactionStack。所有后续 API如log_event、new_transaction的第一个参数都是这个上下文句柄NIF 层通过switchCatContext在调用前把 ccat 的g_cat_context切换到该进程自己的上下文erlcat.c。从测试用例 cat_test.erl 可以看到典型实践spawn出的每个子进程都先init_context()再执行埋点保证各进程互不干扰erlcat_tests.erl 中同样如此。实践经验建议在每个需要埋点的进程入口spawn/proc_lib回调/gen_server初始化统一调用一次new_context/0并存入进程字典后续所有 API 通过get(erlcat_process_context)取用。五、Event记录事件与错误5.1 记录一个完整事件%% 记录一个完整的事件 erlcat:log_event(ErlCatContext, Event, E4, 0, some debug info),log_event/5的参数依次为上下文、事件 type、事件 name、状态码、附加数据。在底层 erlcat.c 会先做isCatEnabled()检查未初始化返回cat_unable再校验参数数量少于 5 个返回badarg随后调用 ccat 的logEvent(type, name, status, data)。这里 type/name/status 的最大长度限制为MAXKEYLEN 128data 最大为MAXVALLEN 1024。5.2 记录错误事件%% 记录一个错误事件 erlcat:log_error(ErlCatContext, failed, error info),错误是 Event 的一种特殊形态默认情况下 type Exception 且 name errorname 可以通过第二个参数复写错误堆栈会被收集并存放在 data 属性中。对应 ccat 的logError(msg, errStr)其语义等价于logEvent(Exception, msg, CAT_ERROR, errStr)见 client.h。需要注意的是状态码CAT_SUCCESS定义为0任何不等于0的状态都会被视作 problem 进入问题报表client.h因此错误事件的 status 会自动置为ERROR。六、Metric秒级聚合的三种上报语义Metric 的核心理念是每秒钟对相同 name 的指标做聚合然后一次性上报。例如同一秒内以相同 name 调用三次底层会对这些值求和后只上报一次对应 message_aggregator_metric.c 的按秒聚合逻辑。%% 每秒求指标总和 erlcat:log_metric_for_count(ErlCatContext, metric-1, 3), %% 每秒求指标平均值 erlcat:log_metric_for_duration(ErlCatContext, metric-2, 3), %% 每秒求指标总和和log_metric_for_count方法效果一样 erlcat:log_metric_for_sum(ErlCatContext, metric-3, 3),三个 API 的语义差异均以毫秒/数值为单位的聚合维度不同API聚合语义底层实现log_metric_for_count/3同秒同 name 求和logMetricForCount(name, count)参数为 intlog_metric_for_duration/3求平均值而非求和专用于耗时指标logMetricForDuration(name, duration)参数为 longlog_metric_for_sum/3求和效果与 count 完全一致NIF 层直接复用logMetricForCounterlcat.c从 NIF 源码可以确认log_metric_for_sum在 C 层就是logMetricForCount(name, count)的别名erlcat.c而log_metric_for_duration使用enif_get_long解析 64 位耗时值适合记录毫秒级耗时。七、Transaction消息树的核心构建 API7.1 API 清单与典型用法Transaction 是 CAT 消息树的核心节点erlcat 提供了一整套创建与修改 APInew_transaction/3— 创建事务add_data/3— 追加数据add_kv/4— 追加键值对set_status/3— 设置状态set_duration/3— 设置耗时毫秒set_duration_start/3— 设置起始时间set_timestamp/3— 设置创建时间戳complete/2— 完成事务官方文档示例注意Tran1是 NIF 资源句柄由new_transaction返回Tran1 erlcat:new_transaction(ErlCatContext, MSG.send, send), erlcat:add_kv(ErlCatContext, Tran1, key, val), erlcat:set_status(ErlCatContext, Tran1, error), erlcat:set_duration(ErlCatContext, Tran1, 500), erlcat:set_duration_start(ErlCatContext, Tran1, time.time() * 1000 - 30 * 1000), erlcat:set_timestamp(ErlCatContext, Tran1, time.time() * 1000 - 30 * 1000), erlcat:complete(ErlCatContext, Tran1),其中new_transaction(ErlCatContext, MSG.send, send)的语义是创建 type 为MSG.send、name 为send的事务注意 NIF 参数顺序为 type、name见 erlcat.c。set_duration对应 C 层的setDurationInMillisset_duration_start对应setDurationStartadd_kv对应addKVcomplete完成事务后会释放 NIF 资源句柄erlcat.c。7.2 使用注意事项官方文档原文要点add_data可多次调用多次数据会被连接起来最终呈现在 LogView 页面不要同时指定duration和durationStart二者互斥没有意义尽管示例代码两者都写了。底层逻辑是setDurationStart只在 duration 未被显式指定时才生效——事务完成时耗时按当前时间戳 - durationStart计算见 client.h 注释务必调用complete完成事务否则会得到毁坏的消息树并造成内存泄漏——因为未完成的 Transaction 一直悬挂在上下文的 transactionStack 上无法回收。7.3 简化场景log_transaction_with_duration/4除了手工创建/修改/完成erlcat 还提供了log_transaction_with_duration/4一次调用完成创建-设置耗时-完成的便捷 APIerlcat.erlerlcat:log_transaction_with_duration(TEST, testDuration, rand:uniform(200)),对应 NIF 的newCompletedTransactionWithDuration(type, name, duration)——它会自动回拨时间戳并补全 duration 后直接上报适合不需要中间过程数据的场景测试用例cat_test.erl的trans_count即采用此写法。7.4 嵌套事务与消息树验证事务支持嵌套以构建父子层级。测试用例 erlcat_tests.erl 展示了经典嵌套模式先创建外层事务T1在其内部创建并完成子事务T2、T3最后完成T1T1 erlcat:new_transaction(ErlCatContext, MSG.send, send), T2 erlcat:new_transaction(ErlCatContext, MSG.send, check), erlcat:complete(ErlCatContext, T2), T3 erlcat:new_transaction(ErlCatContext, MSG.send, del111111), erlcat:complete(ErlCatContext, T3), erlcat:complete(ErlCatContext, T1),配合enable_multiprocessing开启时ccat 会依据 Transaction 栈自动把这些节点组织成消息树。八、进阶 APIHeartbeat、MessageId 与远程调用链原文档开头声明实现几乎所有 C 客户端的 API未涉及的方法请参考 C 端说明以下 API 虽然未在中文文档中展开但在 erlcat.erl 与 erlcat.c 中均已实现可从源码确认其行为。8.1 Heartbeat 自定义上报erlcat:log_heartbeat(ErlCatContext, titleh1, #{ userinfo integer_to_list(rand:uniform(1000)), test22 integer_to_list(rand:uniform(1000)) }),log_heartbeat/3接收一个 Erlang mapNIF 层erlcat.c会遍历 map 键值对用 ezxml 拼装成extension id...extensionDetail id... value...//extension结构的 XML作为自定义状态上报。需要注意的是内置心跳由 ccat 按 60 秒周期自动发送monitor.c中runCount % 60 1 g_config.enableHeartbeat如需完全自定义心跳应先通过enable_heartbeat0关闭内置心跳再调用本 API。8.2 MessageId 与消息树 ID 操作create_message_id/1 %% 生成一个新的 MessageId create_remote_message_id/2 %% 为远程服务生成 MessageId携带目标 appkey get_message_tree_id/1 %% 获取当前上下文的 treeId get_message_tree_root_id/1 %% 获取 rootId get_message_tree_parent_id/1 %% 获取 parentId set_message_tree_id/2 %% 设置 treeId set_message_tree_root_id/2 %% 设置 rootId set_message_tree_parent_id/2 %% 设置 parentId这些 API 与 ccat 的createMessageId、getThreadLocalMessageTreeId等一一对应client.h用于跨进程/跨线程恢复或改写消息树标识。8.3 远程调用链路client / server 两端erlcat 提供了 RPC 场景的链路串联能力%% 调用方客户端侧生成当前链路的 {RootId, ParentId, ChildId} {R, P, C} erlcat:log_remote_call_client(Content), %% 服务端侧消费上游透传的三元组恢复消息树关系 erlcat:log_remote_call_server(Content, R, P, C),从 erlcat.c 的实现看log_remote_call_client会生成当前 treeId/rootId 并额外创建一个 childId返回三元组{rootId, messageId, childId}供调用方透传给对端log_remote_call_server则把收到的 rootId/parentId/childId 写入当前上下文的消息树并生成下一个 childId 返回。测试用例erlcat_tests.erl的remote_call_client_test与cat_test.erl的l1都覆盖了这条链路。九、验证与排错仓库自带的测试用例仓库在 lib/erlang/test/erlcat_tests.erl 提供了基于 EUnit 的测试集覆盖了文档中的全部核心 APIsetup/0统一初始化使用#cat_config{enable_heartbeat 0, enable_debugLog 1, encoder_type 1}即关闭内置心跳、开启调试日志、使用二进制编码器transaction_test_/0多进程并发发送嵌套事务验证上下文隔离与消息树完整性logevent_test_/0、logerror_test_/0验证 Event 与 Error 记录测试以spawn方式模拟多进程同时上报heart_test_/0验证自定义 Heartbeat 上报remote_call_client_test/0验证远程调用链客户端侧消息 ID 生成。同时 src/cat_test.erl 提供了可直接在 shell 中手工验证的入口例如cat_test:init(), cat_test:trans(). %% 初始化并发送 2 轮嵌套事务 cat_test:l1(). %% 验证远程调用链 ID 生成运行测试需保证 NIF 已成功编译priv/erlcat.so存在且与 OTP 版本匹配否则会得到{not_loaded, ...}异常。十、总结erlcat 以NIF 桥接 ccat的方式把 CAT C 客户端的能力完整地带入 Erlang 生态。接入时的三个要点值得牢记初始化即定全局init_cat/2的五个配置项直接映射底层CatClientConfig其中编码器默认二进制高版本服务端必须保持、采样默认开启、上下文管理器默认关闭上下文即隔离每个 Erlang 进程都必须new_context/0并存入进程字典所有 API 显式传入该上下文这是避免多进程数据串扰的根本手段事务必须 complete忘记complete会损坏消息树并泄漏内存同时避免同时设置duration与durationStart。掌握上述内容后你就可以在 Erlang 服务中实现与 Java/Go/Python 等客户端一致的 CAT 埋点能力记录 Event/Error、聚合 Metric、构建嵌套 Transaction 消息树并通过log_remote_call_client/server串联跨服务调用链最终在 CAT 服务端统一查看性能指标、健康状况与实时告警。相关源码参考erlcat.erl、erlcat.hrl、erlcat.c、client.h、rebar.config、cat_test.erl、erlcat_tests.erl、lib/c/README.md。赞分享可观测性指标监控告警APM后端链路追踪【免费下载链接】catCAT 作为服务端项目基础组件提供了 Java, C/C, Node.js, Python, Go 等多语言客户端已经在美团点评的基础架构中间件框架MVC框架RPC框架数据库框架缓存框架等消息队列配置系统等深度集成为美团点评各业务线提供系统丰富的性能指标、健康状况、实时告警等。项目地址https://gitcode.com/gh_mirrors/ca/cat点击查看免费下载相关推荐Phoenix Channels 客户端协议实战手写 WebSocket 客户端接入 Phoenix 的完整指南Phoenix Channels 客户端协议实战手写 WebSocket 客户端接入 Phoenix 的完整指南 本指南以 Phoenix 官方文档 Writ后端Appium 客户端生态完全指南官方客户端与社区客户端的选型、安装与使用Appium 客户端生态完全指南官方客户端与社区客户端的选型、安装与使用 Appium 采用基于 W3C WebDriver 规范的客户端—服务器架构测试脚测试移动开发质量保障Apache Ignite客户端连接管理终极指南厚客户端与瘦客户端完整对比Apache Ignite客户端连接管理终极指南厚客户端与瘦客户端完整对比 Apache Ignite作为高性能分布式内存计算平台提供了两种不同类型的 客户分布式数据库缓存创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考