This is a preprocessing transformer (ppx) for OCaml projects that allows you to wrap functions in debugging and tracing metadata, without additional boilerplate at every callsite. This is largely intended for use with a pervasive tracing and debugging implementation such as ocaml-opentelemetry or trace.
With ppx_trace
, you can write:
module SomeComponent = struct
let foo%span _arg =
(* ... *)
let bar%span _arg =
(* ... *)
let baz%span _arg =
(* ... *)
end
... instead of verbosely annotating every function and manually duplicating names for tracing purposes:
module SomeComponent = struct
let foo _arg =
Dbg.trace ~name:"foo" ~file_name:__MODULE__ ~enclosing_module:"SomeComponent"
~module_path:Stdlib.__FUNCTION__ (fun () ->
(* ... *)
)
let bar _arg =
Dbg.trace ~name:"bar" ~file_name:__MODULE__ ~enclosing_module:"SomeComponent"
~module_path:Stdlib.__FUNCTION__ (fun () ->
(* ... *)
)
let baz _arg =
Dbg.trace ~name:"baz" ~file_name:__MODULE__ ~enclosing_module:"SomeComponent"
~module_path:Stdlib.__FUNCTION__ (fun () ->
(* ... *)
)
end
-
Install the
ppx_trace
package from opam in your project's opam switch:$ opam install ppx_trace
-
Add
ppx_trace
to either yourdune-project
file ...; dune-project (package (name my_package) (depends (ocaml (>= 4.08)) + ppx_trace (alcotest :with-test)))
... or, alternatively, to your manual
opam
file:# my_package.opam depends: [ "ocaml" {>= "4.08"} + "ppx_trace" "alcotest" {with-test} ]
-
Add
ppx_trace
to yourdune
build-instructions under thepreprocess
stanza:; src/dune (library (name my_package_lib) + (preprocess + (pps ppx_trace)) (libraries ...))
The extension-point for ppx_trace
is let%span
; which is supported on two syntactic forms:
-
"Top-level" functions in a module (known as 'structure items'), e.g.
let toplevel_func%span one two = (* ... *) module Functionality = struct let submodule_func%span arg = (* ... *) end
-
let%span ... in
at the expression level, e.g.let run () = (* ... *) let%span callback arg = (* ... *) in do_thing abc def callback
When transformed, ppx_trace
will wrap the function body in a call to the span
function, provided by a user-provided Trace_syntax
module. This module should implement the following signature:
sig
type code_path = {
file_path : string;
main_module_name : string;
submodule_path : string list;
enclosing_module : string;
enclosing_value : string option;
value : string option;
}
val span :
name:string ->
code_path:code_path ->
stdlib_name:string ->
(return -> 'ret) ->
'ret
end
The Trace_syntax.span
function is expected arguments, providing different ways to report on the instrumented function's location in the source code. For example, given the following widget.ml
file:
(* src/widget.ml *)
module Trace_syntax = struct
type code_path = { (* ... *) }
let span ~name ~code_path ~stdlib_name f = (* ... *)
end
module Foo = struct
module Bar = struct
let%span some_func arg1 arg2 = (* ... *)
end
end
... the Trace_syntax.span
function will be called with the following arguments:
-
~name
is the name of the binding being instrumented, e.g."some_func"
. -
~code_path
is a structured record of granular details (provided by ppxlib) about the source-code location, e.g.{ file_path = "src/widget.ml" ; (* the path to the .ml file *) main_module_name = "Widget" ; (* the module name corresponding to the file *) submodule_path = ["Foo"; "Bar"] ; (* the path within the main module, represented as a list of toplevel-module names (does not descend into expressions) *) enclosing_module = Some "Bar" ; (* the nearest enclosing module name ({b does} descend into subexpressions!) *) enclosing_value = "None" ; (* the nearest enclosing value name, if in a subexpression *) value = "None" (* the name of the value to which this code path leads - often {b not} the same as [~name] *) }
-
~stdlib_name
is the value ofStdlib.__FUNCTION__
at the point of instrumentation, for backwards compatibility; this often includes additional path-components, such as anonymous functions, invisible compilation units, etc, e.g."Dune__exe__Widget.Foo.Bar.run.X.some_func"
-
f
is the function body, wrapped in a "thunk" (or unit-argument continuation), e.g.fun () -> (* ... *)
Install a local switch with deps:
$ opam switch create . ocaml-base-compiler.4.14.0 --deps-only --no-install
$ opam install . --deps-only --with-test
Build:
$ dune build
Run unit tests:
$ dune build @runtest
Experiment manually with ./pp.sh
:
$ ./pp.sh --impl test/test_basic.ml
------ /home/me/code/ppx_trace/test/test_basic.ml
++++++ /home/me/code/ppx_trace/test/test_basic.pp.ml
@|-1,18 +1,55 ============================================================
+|open struct
+| module type PPX_TRACE_INTERNAL__TRACE_SYNTAX_SIG__ = sig
+| type code_path = {
+| file_path : string;
+| main_module_name : string;
+| submodule_path : string list;
+| enclosing_module : string;
+| enclosing_value : string option;
+| value : string option;
+| }
+| end
+|end
+|
|module Trace_syntax = struct
| type code_path = {
| file_path : string;
| main_module_name : string;
| submodule_path : string list;
| enclosing_module : string;
| enclosing_value : string option;
| value : string option;
| }
|
| let span ~name ~code_path:_ ~stdlib_name:_ f =
| let ret = f () in
| String.concat " " [ ret; "-"; name; "got wrapped btw" ]
|end
|
-|let%span greet name = "Hello, " ^ name ^ "!"
+|let greet name =
+| let module Ppx_trace_internal__Trace_syntax__ :
+| PPX_TRACE_INTERNAL__TRACE_SYNTAX_SIG__ =
+| Trace_syntax
+| in
+| (Trace_syntax.span
+| : name:string ->
+| code_path:Trace_syntax.code_path ->
+| stdlib_name:string ->
+| (unit -> 'ret) ->
+| 'ret)
+| ~name:"greet"
+| ~code_path:
+| (let open Trace_syntax in
+| {
+| file_path = "/home/me/code/ppx_trace/test/test_basic.ml";
+| main_module_name = "Test_basic";
+| submodule_path = [];
+| enclosing_module = "Test_basic";
+| enclosing_value = None;
+| value = None;
+| })
+| ~stdlib_name:Stdlib.__FUNCTION__
+| (fun () -> "Hello, " ^ name ^ "!")
+|
|
|let run () = ()