Home

Awesome

Trace

Build and Test

This small library provides basic types that can be used to instrument a library or application, either by hand or via a ppx.

Features

Usage

To instrument your code, you can simply add trace to your dune/opam files, and then write code like such:

let f x =
  Trace.with_span ~__FILE__ ~__LINE__ "inside-f" @@ fun _sp ->
  (* … code for f *)

let g x =
  Trace.with_span ~__FILE__ ~__LINE__ "inside-g" @@ fun _sp ->
  let y = f x in
  (* … code for f *)

let () =
  Some_trace_backend.setup () @@ fun () ->
  let result = g 42 in
  print_result result

The file test/t1.ml follows this pattern, using trace-tef as a simple backend that emits one JSON object per span/message:

let run () =
  Trace.set_process_name "main";
  Trace.set_thread_name "t1";

  let n = ref 0 in

  for _i = 1 to 50 do
    Trace.with_span ~__FILE__ ~__LINE__ "outer.loop" @@ fun _sp ->
    for _j = 2 to 5 do
      incr n;
      Trace.with_span ~__FILE__ ~__LINE__ "inner.loop" @@ fun _sp ->
      Trace.messagef (fun k -> k "hello %d %d" _i _j);
      Trace.message "world";
      Trace.counter_int "n" !n
    done
  done

let () =
  Trace_tef.with_setup ~out:(`File "trace.json") () @@ fun () ->
  run ()

After running this, the file "trace.json" will contain something like:

[{"pid":2,"name":"process_name","ph":"M","args": {"name":"main"}},
{"pid":2,"tid": 3,"name":"thread_name","ph":"M","args": {"name":"t1"}},
{"pid":2,"cat":"","tid": 3,"ts": 2.00,"name":"hello 1 2","ph":"I"},
{"pid":2,"cat":"","tid": 3,"ts": 3.00,"name":"world","ph":"I"},
{"pid":2,"tid":3,"ts":4.00,"name":"c","ph":"C","args": {"n":1}},
…

Opening it in https://ui.perfetto.dev we get something like this:

screenshot of perfetto UI

ppx_trace

On OCaml >= 4.12, and with ppxlib installed, you can install ppx_trace. This is a preprocessor that will rewrite like so:

let%trace f x y z =
  do_sth x;
  do_sth y;
  begin
    let%trace () = "sub-span" in
    do_sth z
  end

This more or less corresponds to:

let f x y z =
  let _trace_span = Trace_core.enter_span ~__FILE__ ~__LINE__ "Foo.f" in
  match
    do_sth x;
    do_sth y;
    begin
      let _trace_span = Trace_core.enter_span ~__FILE__ ~__LINE__ "sub-span" in
      match do_sth z with
      | res ->
        Trace_core.exit_span _trace_span;
        res
      | exception e ->
        Trace_core.exit_span _trace_span
        raise e
    end;
  with
  | res ->
    Trace_core.exit_span _trace_span
    res
  | exception e ->
    Trace_core.exit_span _trace_span
    raise e

Alternatively, a name can be provided for the span, which is useful if you want to access it and use functions like Trace.add_data_to_span:

let%trace f x y z =
  do_sth x;
  do_sth y;
  begin
    let%trace _sp = "sub-span" in
    do_sth z;
    Trace.add_data_to_span _sp ["x", `Int 42]
  end

Dune configuration

In your library or executable stanza, add: (preprocess (pps ppx_trace)). The dependency on trace.core is automatically added. You still need to configure a backend to actually do collection.

Backends

Concrete tracing or observability formats such as:

Subscribers

The library trace.subscriber defines composable subscribers, which are sets of callbacks that consume tracing events. Multiple subscribers can be aggregated together (with events being dispatched to all of them) and be installed as a normal collector.