add Trace_lwt optional library (#47)

* add Trace_lwt optional library

provides a with_span that's lwt friendly, as well as a ambient span
provider.

* test: extract recorder used for ambient tests

* tests for trace.lwt

* opam
This commit is contained in:
Simon Cruanes 2026-04-14 14:10:32 -04:00 committed by GitHub
parent 83d408355b
commit 42bf837113
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 169 additions and 40 deletions

View file

@ -22,13 +22,14 @@
(synopsis (synopsis
"A lightweight stub for tracing/observability, agnostic in how data is collected") "A lightweight stub for tracing/observability, agnostic in how data is collected")
(description (description
"ocaml-trace can be used to instrument libraries and programs with low overhead.\n\n It doesn't do any IO unless a collector is plugged in, which only\n the final executable should do.") "ocaml-trace can be used to instrument libraries and programs with low overhead. It doesn't do any IO unless a collector is plugged in, which only the final executable should do.")
(depends (depends
(ocaml (ocaml
(>= 4.08)) (>= 4.08))
dune) dune)
(depopts (depopts
unix unix
lwt
(thread-local-storage (>= 0.2)) (thread-local-storage (>= 0.2))
(mtime (mtime
(>= 2.0))) (>= 2.0)))

6
src/lwt/dune Normal file
View file

@ -0,0 +1,6 @@
(library
(name trace_lwt)
(public_name trace.lwt)
(optional) ; lwt
(libraries trace.core lwt)
(synopsis "Interface with lwt"))

30
src/lwt/trace_lwt.ml Normal file
View file

@ -0,0 +1,30 @@
(** Optional interface with lwt.
@since NEXT_RELEASE *)
open Trace_core
let k_ambient_span : span Lwt.key = Lwt.new_key ()
let ambient_span_provider : Trace_core.Ambient_span_provider.t =
ASP_some
( (),
{
get_current_span = (fun () -> Lwt.get k_ambient_span);
with_current_span_set_to =
(fun () span f ->
Lwt.with_value k_ambient_span (Some span) (fun () -> f span));
} )
let with_span ?level ?__FUNCTION__ ~__FILE__ ~__LINE__ ?parent ?params ?data
name (f : span -> 'a Lwt.t) : 'a Lwt.t =
if Trace_core.enabled () then (
let span =
Trace_core.enter_span ?level ?__FUNCTION__ ~__FILE__ ~__LINE__ ?parent
?params ?data name
in
let fut = Trace_core.with_current_span_set_to span f in
Lwt.on_termination fut (fun () -> Trace_core.exit_span span);
fut
) else
f Trace_core.Collector.dummy_span

3
test/common/dune Normal file
View file

@ -0,0 +1,3 @@
(library
(name test_trace_common)
(libraries trace.core))

View file

@ -0,0 +1,32 @@
(** Shared test utilities for ambient span tests. *)
type Trace_core.span += Named of string
let make_recorder () =
let open Trace_core.Collector in
let cbs =
Callbacks.make
~enter_span:(fun
()
~__FUNCTION__:_
~__FILE__:_
~__LINE__:_
~level:_
~params:_
~data:_
~parent:_
name
-> Named name)
~exit_span:(fun () _sp -> ())
~add_data_to_span:(fun () _sp _data -> ())
~message:(fun () ~level:_ ~params:_ ~data:_ ~span:_ _msg -> ())
~metric:(fun () ~level:_ ~params:_ ~data:_ _name _m -> ())
()
in
C_some ((), cbs)
let current_name () =
match Trace_core.current_span () with
| None -> "none"
| Some (Named s) -> s
| Some _ -> "<other>"

21
test/lwt/dune Normal file
View file

@ -0,0 +1,21 @@
(executable
(name t_lwt)
(modules t_lwt)
(libraries
trace
test_trace_common
(select
t_lwt.ml
from
(trace.lwt lwt.unix -> t_lwt.real.ml)
(-> t_lwt.quine.ml))))
(rule
(alias runtest)
(package trace)
(action
(progn
(with-stdout-to
t_lwt.output
(run %{exe:t_lwt.exe} %{dep:t_lwt.expected}))
(diff t_lwt.expected t_lwt.output))))

10
test/lwt/t_lwt.expected Normal file
View file

@ -0,0 +1,10 @@
=== ambient span (Lwt) ===
before any span: none
sync outer: sync-outer
sync inner: sync-inner
after sync: none
lwt outer: lwt-outer
lwt inner: lwt-inner
after lwt inner: lwt-outer
after lwt outer: none
after binds: bind-test

10
test/lwt/t_lwt.quine.ml Normal file
View file

@ -0,0 +1,10 @@
(* When trace.lwt is unavailable, echo the expected file
so the diff always passes. The file is passed as argv[1] by the dune rule. *)
let () =
let ic = open_in Sys.argv.(1) in
(try
while true do
print_char (input_char ic)
done
with End_of_file -> ());
close_in ic

49
test/lwt/t_lwt.real.ml Normal file
View file

@ -0,0 +1,49 @@
(* Test ambient span tracking via Lwt key provider *)
open Test_trace_common
let ( let@ ) = ( @@ )
let () =
print_endline "=== ambient span (Lwt) ===";
Trace_core.set_ambient_context_provider Trace_lwt.ambient_span_provider;
let coll = make_recorder () in
let@ () = Trace_core.with_setup_collector coll in
(* sync with_span works with the Lwt provider *)
Printf.printf "before any span: %s\n" (current_name ());
(let@ _outer = Trace_core.with_span ~__FILE__ ~__LINE__ "sync-outer" in
Printf.printf "sync outer: %s\n" (current_name ());
let@ _inner = Trace_core.with_span ~__FILE__ ~__LINE__ "sync-inner" in
Printf.printf "sync inner: %s\n" (current_name ()));
Printf.printf "after sync: %s\n" (current_name ());
(* Lwt-specific tests *)
Lwt_main.run
(let open Lwt.Syntax in
(* nested Trace_lwt.with_span *)
let* () =
Trace_lwt.with_span ~__FILE__ ~__LINE__ "lwt-outer" (fun _outer ->
Printf.printf "lwt outer: %s\n" (current_name ());
let* () =
Trace_lwt.with_span ~__FILE__ ~__LINE__ "lwt-inner" (fun _inner ->
Printf.printf "lwt inner: %s\n" (current_name ());
Lwt.return_unit)
in
Printf.printf "after lwt inner: %s\n" (current_name ());
Lwt.return_unit)
in
Printf.printf "after lwt outer: %s\n" (current_name ());
(* context survives Lwt.bind chain *)
let* () =
Trace_lwt.with_span ~__FILE__ ~__LINE__ "bind-test" (fun _sp ->
let* () = Lwt.return_unit in
let* () = Lwt.return_unit in
Printf.printf "after binds: %s\n" (current_name ());
Lwt.return_unit)
in
Lwt.return_unit)

View file

@ -3,8 +3,7 @@
(modules t_tls) (modules t_tls)
(libraries (libraries
trace trace
; avoid using a recent dune with `enabled-if`, by just using `select` instead. test_trace_common
; the dummy case just echoes back the expected file.
(select (select
t_tls.ml t_tls.ml
from from

View file

@ -1,39 +1,9 @@
(* Test ambient span tracking via thread-local-storage provider *) (* Test ambient span tracking via thread-local-storage provider *)
open Test_trace_common
let ( let@ ) = ( @@ ) let ( let@ ) = ( @@ )
(* Unique span type so we can identify spans by name *)
type Trace_core.span += Named of string
let make_recorder () =
let open Trace_core.Collector in
let cbs =
Callbacks.make
~enter_span:(fun
()
~__FUNCTION__:_
~__FILE__:_
~__LINE__:_
~level:_
~params:_
~data:_
~parent:_
name
-> Named name)
~exit_span:(fun () _sp -> ())
~add_data_to_span:(fun () _sp _data -> ())
~message:(fun () ~level:_ ~params:_ ~data:_ ~span:_ _msg -> ())
~metric:(fun () ~level:_ ~params:_ ~data:_ _name _m -> ())
()
in
C_some ((), cbs)
let current_name () =
match Trace_core.current_span () with
| None -> "none"
| Some (Named s) -> s
| Some _ -> "<other>"
let () = let () =
print_endline "=== ambient span (TLS) ==="; print_endline "=== ambient span (TLS) ===";
Trace_thread_local_storage.setup (); Trace_thread_local_storage.setup ();

View file

@ -3,11 +3,8 @@ opam-version: "2.0"
version: "0.12" version: "0.12"
synopsis: synopsis:
"A lightweight stub for tracing/observability, agnostic in how data is collected" "A lightweight stub for tracing/observability, agnostic in how data is collected"
description: """ description:
ocaml-trace can be used to instrument libraries and programs with low overhead. "ocaml-trace can be used to instrument libraries and programs with low overhead. It doesn't do any IO unless a collector is plugged in, which only the final executable should do."
It doesn't do any IO unless a collector is plugged in, which only
the final executable should do."""
maintainer: ["Simon Cruanes"] maintainer: ["Simon Cruanes"]
authors: ["Simon Cruanes"] authors: ["Simon Cruanes"]
license: "MIT" license: "MIT"
@ -21,6 +18,7 @@ depends: [
] ]
depopts: [ depopts: [
"unix" "unix"
"lwt"
"thread-local-storage" {>= "0.2"} "thread-local-storage" {>= "0.2"}
"mtime" {>= "2.0"} "mtime" {>= "2.0"}
] ]