Recently I’ve needed to update our handling of logs to emit “structured” log messages, i.e. JSON-formatted objects with one field being the primary string log message. I’m using @dbuenzli’s logs
library, so I looked to write a log reporter that assembled a Yojson
value containing (in part) the formatted log message that would otherwise just be rendered mostly as-is on stderr
or whatever.
Along the way, I ran into what looks to me like some truly spooky behaviour. I thought at first I must have been hitting a bug in logs
, but my minimal repro appears to demonstrate that the problem might actually be either in Format
or perhaps even the compiler.
let indirect =
{ Logs.report =
(fun src level ~over k msgf ->
msgf
@@ fun ?header ?tags fmt ->
let msg = ref "INIT" in
let k =
Format.kasprintf
(fun s ->
(* A *) print_endline @@ "in k, got msg: " ^ s;
msg := s;
k ())
fmt
in
(* in real usage, the log messages aren't going to stdout/stderr *)
(* B *) print_endline @@ "Log message: " ^ !msg;
over ();
k)
}
let () =
Logs.set_level ~all:true @@ Some Logs.Debug;
Logs.set_reporter indirect;
Logs.info (fun m -> m "No format annotations");
print_endline "------------------------";
Logs.info (fun m -> m "this won't %s" "print")
It seems obvious that the print on line *A*
should always, always happen before the print on line *B*
. (I’m eschewing Format
usage in this example outside of the actual log handling just to avoid complicating the interaction with that module in any way.) Unfortunately, that’s not the case:
$ ocamlc -I `ocamlfind query logs` logs.cma -o loggingtest loggingtest.ml && ./loggingtest
in k, got msg: No format annotations
Log message: No format annotations
------------------------
Log message: INIT
in k, got msg: this won't print
As you can see, the formatted message that includes any sort of annotation provokes some kind of bizarre out-of-order execution that results in the actual captured log message string effectively going missing.
FWIW:
- I’ve tried variations of this that use e.g.
kfprintf
with both the providedstr_formatter
and aBuffer
-backed formatter; though spooky actions happen in those scenarios as well, I think the above example demonstrates the problem most tangibly (using thek
fun to either alter the ref or not before the later log message “output” @*B*
) - Outcomes are the same with OCaml 4.14 or 5.0 (I’m currently using 5 at the moment, but thought to check to see if the problem was somehow domain-related)
What in the world is going on here?