How to print filenames and line numbers with the Logs library

Hey,

I’m using the Logs library. I’d like log entries to contain the filename and the line number where it was created. This is a bit tricky though - I could use __FILE__ and __LINE__ but they’re hard to use in abstractions.

In Rust I’d either create a macro, or better I’d use #[track_caller] which makes available std::panic::Location::caller().line() to get the line number of the caller. Are either of these approaches viable in OCaml?

2 Likes

That’s not really a satisfactory answer but if already having the file improves things for you can define such a line at the top of your modules:

let[@inline] file m ?header ?tags fmt =
  m ?header ?tags ("%s: " ^^ fmt) __FILE__

(or add it in a tag if you want more structured access). Then:

let main () =
  Logs.(set_reporter (Logs.format_reporter ()));
  Logs.err (fun m -> (file m) "Hey %s!" "ho");
  ()

let () = main ()
2 Likes

Oh and I forgot to mention. I tend to play with these things with various degrees of success in my ad-hoc testing frameworks, so that when a test fails I can directly jump to the failing test in compilation mode. See for example here and the printing stuff here. Maybe you can try to play with that too.

1 Like

You could use something like ppx_here, which gives a Lexing.position wherever you write [%here]. But you would have to write [%here] at each call site of a logging function in order to get that callsite’s position.

1 Like

Note that the __POS__ value/macro also returns a source-code position 4-tuple that is roughly equivalent to Lexing.position, although it has the same problem w.r.t. abstraction as ppx_here and the other macros that have been mentioned.

@dbuenzli and @bcc32’s answers get as far as I think it’s possible to go in stock OCaml without PPX. With PPX – and some familiarity with Ppxlib – this feature is relatively straightforward to implement in ~20 LoC. (And while we’re at it, we can also generate the (fun f -> f ...) CPS wrapper that Logs requires.)

let () =
  Logs.(set_reporter (format_reporter ()));
  [%log err "Important numbers: %d, %f" 42 3.14];
  [%log warn "Lorem ipsum dolor sit amet"]

(* prints: *)

; dune exec ./main.exe
main.exe: [ERROR] [Line 3] Important numbers: 42, 3.140000
main.exe: [WARNING] [Line 4] Lorem ipsum dolor sit amet

This works as follows:

open Ppxlib

(* Input: [%log <log_fn> <fmt_string> <args...>]

   Output: Logs.(<log_fn>) (fun f -> 
             f ("[Line %d] " ^^ fmt_string) __LINE__ <args...>)
 *)
let expansion_function ~loc ~path:_ payload =
  let open Ast_builder.Default in
  let log_fn, fmt_string, args =
    match payload.pexp_desc with
    | Pexp_apply (fn, (Nolabel, fmt) :: args) -> (fn, fmt, List.map snd args)
    | _ ->
        Location.raise_errorf ~loc "ppx_logs: invalid payload %a"
          Pprintast.expression payload
  in
  let args =
    [%expr "[Line %d] " ^^ [%e fmt_string]] :: [%expr __LINE__] :: args
    |> List.map (fun x -> (Nolabel, x))
  in
  [%expr Logs.([%e log_fn]) (fun f -> [%e pexp_apply ~loc [%expr f] args])]

let extension =
  Extension.declare "log" Extension.Context.expression
    Ast_pattern.(single_expr_payload __)
    expansion_function

let rule = Context_free.Rule.extension extension
let () = Driver.register_transformation ~rules:[ rule ] "ppx_logs"

(Just a proof-of-concept. I expect an implementation that uses tags & handles shadowing and error reporting appropriately would be around twice this much code, so not too bad.)

6 Likes

After sending this, I remembered that Logs recently gained a PPX thanks to @ulrikstrid. This generates the (fun f -> f ...) wrapper but doesn’t attach any other information. Perhaps “add source positions via Logs tags” would be an appropriate feature request over there :slight_smile:

6 Likes

I also should update the ppx to use another name then the global Logs module, maybe have it configurable.

2 Likes