I have a log backend that doesn’t use stdout (it uploads JSON to Datadog), and I’m trying to figure out the best way to make Logs tags show up.
Basically I have a Format.formatter -> 'a -> unit which prints the value into a formatter, and I have a value, and I want to convert the value to a string.
let tag_to_key_value tag =
let Logs.Tag.V (def, value) = tag in
let name = Logs.Tag.name def
and value =
let printer = Logs.Tag.printer def
and buffer =
Buffer.create 80
in
let formatter = Format.formatter_of_buffer buffer in
printer formatter value;
Format.pp_print_flush formatter ();
Buffer.contents buffer
in
name, value
This seems… really complicated, so I’m wondering if there’s a Format.print_to_string printer value or something that I’m missing.
Ah, someone else at my company came up with a solution right after I posted this. The function I wanted was asprintf
let tag_to_key_value tag =
let (Logs.Tag.V (def, value)) = tag in
let name = Logs.Tag.name def
and value =
let printer = Logs.Tag.printer def in
Format.asprintf "%a" printer value
in
name, value
Although the "%a" argument still makes me think I’m doing something wrong…
If you wanted to avoid using asprintf (and the performance cost of interpreting a format string), it’s also possible to print a value to Format.str_formatter and then get the string with Format.flush_str_formatter. The implementation of this would then be nearly identical to your original solution, but more ergonomic.
The str_formatter is occasionally useful when you don’t have a convenient handle on a specific pretty-printer and value. I also think you are right to be suspicious of the single "%a" format string; I’ve come to see this as a code-smell that indicates there’s a nicer solution somewhere.
Our log integration is currently tied into a lot of stuff. I’m hoping to make it a lot more generic where we’re just setting up outputs for other log libraries but right now it’s not really something we can easily separate.
We basically have our own entire log system and just add integrations for others that redirect to it. We also have a thread-local tag system so we can do something like:
Logger.with_tags [ "url", "https://www.example.com" ] @@ fun () ->
Log.Global.info "This will have the tag created above"
In case it’s useful to you, our Async.Log integration looks like this:
Log.Global.set_transform
(Some
(fun msg ->
(* synchronously add thread-local tags to the message. If we do this later, we will frequently
get tags from the wrong async context.
See https://github.com/janestreet/async_unix/issues/16#issuecomment-620830885 *)
let open Log.Message in
let level = level msg
and raw_message = raw_message msg
and tags = tags msg |> Logger0.merge_thread_local_tags
and time = time msg in
Log.Message.create ?level ~tags ~time raw_message));
Log.Global.set_level Config.(get log_level |> log_level_to_async_log_level);
Log.Output.create
~flush:(fun () -> Lazy.force Writer.stdout |> Writer.flushed)
(fun msgs ->
Queue.iter msgs ~f:(fun msg ->
let module Message = Log.Message in
let time = Message.time msg
and message = Message.message msg
and type_ =
Message.level msg
|> Option.map ~f:log_level_to_async_log_level
|> Option.value ~default:Info
and tags = Message.tags msg in
save_and_log ~type_ ~time ~tags "%s" message)
|> return)
|> List.return
|> Log.Global.set_output;
And our Logs integration looks like:
let report src level ~over k msgf =
msgf
@@ fun ?header:_ ?(tags = Logs.Tag.empty) fmt ->
Format.kfprintf
(fun fmt ->
Format.pp_print_flush fmt ();
over ();
k ())
(Format.make_formatter
(fun str pos len ->
let type_ =
match level with
| Logs.Error -> Error
| Warning -> Warn
| App | Info -> Info
| Debug -> Debug
and tags =
let tags =
Logs.Tag.fold
(fun (Logs.Tag.V (def, value)) acc ->
let name = Logs.Tag.name def
and value =
let printer = Logs.Tag.printer def in
Format.asprintf "%a" printer value
in
(name, value) :: acc)
tags
[]
in
if List.exists tags ~f:(fun (key, _) -> String.(key = "source"))
then tags
else ("source", Logs.Src.name src) :: tags
in
save_and_log ~type_ ~tags "%s" (String.sub str ~pos ~len))
ignore)
fmt
in
Logs.set_reporter { report };
Config.(get log_level)
|> type_to_logs_level
|> Option.return
|> Logs.set_level ~all:true
At some point we’ll probably change our log system to be based on Logs and just have an integration for Async.Log, but it’s not a high priority for us at the moment.