OCaml Logs 中如何传递多参数打印函数并避免 %a 类型错误?
可以把每条消息封装成一个 Format.formatter -> unit 闭包,再用 %t 输出。这样,一个日志函数就能处理不同数量的参数,具体参数由闭包绑定。也可以把多个参数装进元组,让打印函数符合 %a 的类型要求。
当前错误来自打印函数的类型和参数传递方式。Lwt/Eio 能否配合运行是另一个独立问题。
为什么 %a 会报类型错误?
Format 的 %a 需要两个参数:一个打印函数和一个交给它打印的值。打印函数必须具有这样的类型:
Format.formatter -> 'a -> unit
这里的 'a 可以是整数、元组或记录,但对 %a 来说,它始终是一个值。
原来的 pp_log6 类型是:
Format.formatter -> int -> int -> int -> unit
它接收 formatter 和三个整数。%a 提供 formatter 和一个整数之后,剩下的结果仍然是:
int -> int -> unit
而 %a 要求打印操作此时已经结束并返回 unit,因此类型不匹配。
log_message1 中的:
m ~tags:stamp " %a %a" pp k pp n
会调用两次单值打印函数,分别打印 k 和 n。它不会把两个参数一起交给同一次 pp 调用。
另外,log_message2 中的:
pp (k n n1 n2)
会把 k 当作函数,先计算 k n n1 n2。括号不会把这些变量打包成多个参数。要构造元组,应写成 (k, n, n1, n2)。
给出的调用传入了 term、candidate_id、index_at_last_log、term_at_last_log 四个值,而 pp_log6 只接收三个整数。当前模板也没有打印全部四个值的位置。
推荐写法:用 %t 接收消息闭包
%t 接收的打印函数类型是:
Format.formatter -> unit
保留现有的 pp_log6 和 pp_log1,定义一个公共日志函数:
(* stamp 沿用现有代码中的标签集合。 *)
let log_message print =
Logs_lwt.app (fun m ->
m ~tags:stamp " %t" print)
打印投票信息时,把三个参数绑定进闭包:
let log_voting =
log_message (fun out ->
pp_log6 out index_at_last_log term_at_last_log candidate_id)
对应关系是:
index使用index_at_last_log。term使用term_at_last_log。cmid使用candidate_id。
这里沿用了现有模板的三个字段;调用处额外提供的当前 term 没有对应字段。如果也需要记录它,就要修改消息模板,增加相应的参数和占位符。
打印另一种消息时,仍然使用同一个封装:
let log_follower =
log_message (fun out ->
pp_log1 out term)
pp_log1 中的 %ld 要求 term 是 int32;pp_log6 中的 %d 要求对应参数是 int。应按实际数据类型选择占位符。
这两个调用都会返回 Lwt promise,应按程序现有的异步流程组合或等待。例如,在已经使用 Lwt.Infix 的 Lwt 代码中:
log_message (fun out ->
pp_log6 out index_at_last_log term_at_last_log candidate_id)
>>= fun () ->
(* 后续异步操作 *)
Lwt.return_unit
原来的 let _ = ... in 只是丢弃返回值,没有表达等待日志完成的依赖关系。
如果保留 %a,可以使用元组
把打印函数改成接收一个三元组,就能满足 %a 的类型要求:
let pp_log6_tuple out (index, term, cmid) =
Format.fprintf out
"Votng procedure [Index=%d, Candidate ID =%d, term at last log=%d]"
index cmid term
let log_value pp value =
Logs_lwt.app (fun m ->
m ~tags:stamp " %a" pp value)
调用方式如下:
let log_voting =
log_value pp_log6_tuple
(index_at_last_log, term_at_last_log, candidate_id)
不同消息可以使用不同的元组或记录类型,公共的 log_value 不必随参数数量变化而修改。对于已有的多参数打印函数,前面的 %t 闭包写法改动更少。
如何保留延迟格式化?
把格式化操作放在传给 Logs_lwt 的消息回调内。前面的 %t 写法中,pp_log6 只有在消息回调执行并处理 %t 时才会被调用,创建闭包时不会生成字符串。
如果参数本身需要昂贵的计算,也应把计算放进去:
let log_message print =
Logs_lwt.app (fun m ->
m ~tags:stamp " %t" print)
let log_computed make_values =
log_message (fun out ->
let index, term, cmid = make_values () in
pp_log6 out index term cmid)
这样,创建 log_computed 对应的日志操作时,不会提前执行 make_values ()。
如果先在外面调用 make_values (),再把结果交给日志函数,计算就已经发生,日志级别无法省掉它。
当前封装使用的是 Logs_lwt.app,对应 Logs.App。如果这类消息应属于 Info 级别,应使用 Logs_lwt.info,并按所选级别检查过滤行为。
自定义 reporter 还需要检查什么?
修正打印函数类型后,现有 reporter 仍有几个独立的异步问题需要处理。
app_flush () 和 dst_flush () 在 Lwt_io.open_file 完成后才调用,而缓冲区由所有日志调用共享。如果打开文件期间又格式化了其他消息,一次写入就可能取走多条消息,另一条写入则取到空内容。应在当前消息格式化结束后、开始异步打开文件之前,先把该条消息取成独立字符串,再交给写入操作。如果对消息顺序有要求,还需要串行写入或使用队列。
当前 Lwt_io.close fd 只在写入成功后执行,写入失败时后面的关闭步骤不会执行。可以用 Lwt_io.with_file 管理通道生命周期,或者在成功打开文件后用另一个 Lwt.finalize 确保关闭。外层已有的 Lwt.finalize write unblock 负责调用 over (),不负责关闭文件。
Lwt.ignore_result 将写入 promise 从当前控制流中分离。需要明确写入异常如何处理,以及程序退出前如何等待尚未完成的写入。
这些代码没有展示 Lwt/Eio 的桥接和运行入口,因此无法据此判断混用是否正确。需要确认实际运行方式能持续驱动这些 Lwt I/O 操作,并在结束前等待必要的日志写入完成。
备注:内容仅供参考。
