breaking: feat(trace): pass a string trace_id in manual spans

- in entering manual spans, we now pass an explicit span_ctx that
  contains a trace_id (bytes) and the parent span id (int64).
- this makes compat with OTEL easier as we want this sort
  of span_ctx to be passed around.
This commit is contained in:
Simon Cruanes 2025-04-09 09:28:09 -04:00
parent 7092217158
commit 151d80d0f1
No known key found for this signature in database
GPG key ID: EBFFF6F283F3A2B4
11 changed files with 93 additions and 80 deletions

View file

@ -8,9 +8,13 @@
open Types open Types
let dummy_span : span = Int64.min_int let dummy_span : span = Int64.min_int
let dummy_trace_id : trace_id = String.empty
let dummy_explicit_span : explicit_span = let dummy_explicit_span : explicit_span =
{ span = dummy_span; meta = Meta_map.empty } { span = dummy_span; trace_id = dummy_trace_id; meta = Meta_map.empty }
let dummy_explicit_span_ctx : explicit_span_ctx =
{ span = dummy_span; trace_id = dummy_trace_id }
(** Signature for a collector. (** Signature for a collector.
@ -49,7 +53,7 @@ module type S = sig
@since 0.6 *) @since 0.6 *)
val enter_manual_span : val enter_manual_span :
parent:explicit_span option -> parent:explicit_span_ctx option ->
flavor:[ `Sync | `Async ] option -> flavor:[ `Sync | `Async ] option ->
__FUNCTION__:string option -> __FUNCTION__:string option ->
__FILE__:string -> __FILE__:string ->
@ -61,6 +65,9 @@ module type S = sig
and this function can store as much metadata as it wants in the hmap and this function can store as much metadata as it wants in the hmap
in the {!explicit_span}'s [meta] field. in the {!explicit_span}'s [meta] field.
{b NOTE} the [parent] argument is now an {!explicit_span_ctx} and not
an {!explicit_span} since NEXT_RELEASE.
This means that the collector doesn't need to implement contextual This means that the collector doesn't need to implement contextual
storage mapping {!span} to scopes, metadata, etc. on its side; storage mapping {!span} to scopes, metadata, etc. on its side;
everything can be transmitted in the {!explicit_span}. everything can be transmitted in the {!explicit_span}.

View file

@ -17,6 +17,9 @@ let current_level_ = A.make Level.Trace
(* ## implementation ## *) (* ## implementation ## *)
let[@inline] ctx_of_span (sp : explicit_span) : explicit_span_ctx =
{ span = sp.span; trace_id = sp.trace_id }
let data_empty_build_ () = [] let data_empty_build_ () = []
let[@inline] enabled () = let[@inline] enabled () =
@ -59,27 +62,19 @@ let[@inline] exit_span sp : unit =
| None -> () | None -> ()
| Some (module C) -> C.exit_span sp | Some (module C) -> C.exit_span sp
let enter_explicit_span_collector_ (module C : Collector.S) ~parent ~flavor let enter_manual_span_collector_ (module C : Collector.S) ~parent ~flavor
?__FUNCTION__ ~__FILE__ ~__LINE__ ?(data = data_empty_build_) name : ?__FUNCTION__ ~__FILE__ ~__LINE__ ?(data = data_empty_build_) name :
explicit_span = explicit_span =
let data = data () in let data = data () in
C.enter_manual_span ~parent ~flavor ~__FUNCTION__ ~__FILE__ ~__LINE__ ~data C.enter_manual_span ~parent ~flavor ~__FUNCTION__ ~__FILE__ ~__LINE__ ~data
name name
let[@inline] enter_manual_sub_span ~parent ?flavor ?level ?__FUNCTION__ let[@inline] enter_manual_span ~parent ?flavor ?level ?__FUNCTION__ ~__FILE__
~__FILE__ ~__LINE__ ?data name : explicit_span =
match A.get collector with
| Some coll when check_level ?level () ->
enter_explicit_span_collector_ coll ~parent:(Some parent) ~flavor
?__FUNCTION__ ~__FILE__ ~__LINE__ ?data name
| _ -> Collector.dummy_explicit_span
let[@inline] enter_manual_toplevel_span ?flavor ?level ?__FUNCTION__ ~__FILE__
~__LINE__ ?data name : explicit_span = ~__LINE__ ?data name : explicit_span =
match A.get collector with match A.get collector with
| Some coll when check_level ?level () -> | Some coll when check_level ?level () ->
enter_explicit_span_collector_ coll ~parent:None ~flavor ?__FUNCTION__ enter_manual_span_collector_ coll ~parent ~flavor ?__FUNCTION__ ~__FILE__
~__FILE__ ~__LINE__ ?data name ~__LINE__ ?data name
| _ -> Collector.dummy_explicit_span | _ -> Collector.dummy_explicit_span
let[@inline] exit_manual_span espan : unit = let[@inline] exit_manual_span espan : unit =

View file

@ -31,6 +31,10 @@ val set_default_level : Level.t -> unit
default value is [Level.Trace]. default value is [Level.Trace].
@since 0.7 *) @since 0.7 *)
val ctx_of_span : explicit_span -> explicit_span_ctx
(** Turn a span into a span context.
@since NEXT_RELEASE *)
val with_span : val with_span :
?level:Level.t -> ?level:Level.t ->
?__FUNCTION__:string -> ?__FUNCTION__:string ->
@ -80,8 +84,8 @@ val add_data_to_span : span -> (string * user_data) list -> unit
Behavior is not specified if the span has been exited. Behavior is not specified if the span has been exited.
@since 0.4 *) @since 0.4 *)
val enter_manual_sub_span : val enter_manual_span :
parent:explicit_span -> parent:explicit_span_ctx option ->
?flavor:[ `Sync | `Async ] -> ?flavor:[ `Sync | `Async ] ->
?level:Level.t -> ?level:Level.t ->
?__FUNCTION__:string -> ?__FUNCTION__:string ->
@ -93,6 +97,12 @@ val enter_manual_sub_span :
(** Like {!with_span} but the caller is responsible for (** Like {!with_span} but the caller is responsible for
obtaining the [parent] span from their {e own} caller, and carry the resulting obtaining the [parent] span from their {e own} caller, and carry the resulting
{!explicit_span} to the matching {!exit_manual_span}. {!explicit_span} to the matching {!exit_manual_span}.
{b NOTE} this replaces [enter_manual_sub_span] and [enter_manual_toplevel_span]
by just making [parent] an explicit option. It is breaking anyway because we now pass
an {!explicit_span_ctx} instead of a full {!explicit_span} (the reason being that we
might receive this explicit_span_ctx from another process or machine).
@param flavor a description of the span that can be used by the {!Collector.S} @param flavor a description of the span that can be used by the {!Collector.S}
to decide how to represent the span. Typically, [`Sync] spans to decide how to represent the span. Typically, [`Sync] spans
start and stop on one thread, and are nested purely by their timestamp; start and stop on one thread, and are nested purely by their timestamp;
@ -100,24 +110,7 @@ val enter_manual_sub_span :
Lwt, Eio, Async, etc.) which impacts how the collector might represent them. Lwt, Eio, Async, etc.) which impacts how the collector might represent them.
@param level optional level for this span. since 0.7. @param level optional level for this span. since 0.7.
Default is set via {!set_default_level}. Default is set via {!set_default_level}.
@since 0.3 *) @since NEXT_RELEASE *)
val enter_manual_toplevel_span :
?flavor:[ `Sync | `Async ] ->
?level:Level.t ->
?__FUNCTION__:string ->
__FILE__:string ->
__LINE__:int ->
?data:(unit -> (string * user_data) list) ->
string ->
explicit_span
(** Like {!with_span} but the caller is responsible for carrying this
[explicit_span] around until it's exited with {!exit_manual_span}.
The span can be used as a parent in {!enter_manual_sub_span}.
@param flavor see {!enter_manual_sub_span} for more details.
@param level optional level for this span. since 0.7.
Default is set via {!set_default_level}.
@since 0.3 *)
val exit_manual_span : explicit_span -> unit val exit_manual_span : explicit_span -> unit
(** Exit an explicit span. This can be on another thread, in a (** Exit an explicit span. This can be on another thread, in a

View file

@ -3,6 +3,11 @@ type span = int64
The meaning of the identifier depends on the collector. *) The meaning of the identifier depends on the collector. *)
type trace_id = string
(** A bytestring representing a (possibly distributed) trace made of async spans.
With opentelemetry this is 16 bytes.
@since NEXT_RELEASE *)
type user_data = type user_data =
[ `Int of int [ `Int of int
| `String of string | `String of string
@ -13,16 +18,26 @@ type user_data =
(** User defined data, generally passed as key/value pairs to (** User defined data, generally passed as key/value pairs to
whatever collector is installed (if any). *) whatever collector is installed (if any). *)
type explicit_span_ctx = {
span: span; (** The current span *)
trace_id: trace_id; (** The trace this belongs to *)
}
(** A context, passed around for async traces.
@since NEXT_RELEASE *)
type explicit_span = { type explicit_span = {
span: span; span: span;
(** Identifier for this span. Several explicit spans might share the same (** Identifier for this span. Several explicit spans might share the same
identifier since we can differentiate between them via [meta]. *) identifier since we can differentiate between them via [meta]. *)
trace_id: trace_id; (** The trace this belongs to *)
mutable meta: Meta_map.t; mutable meta: Meta_map.t;
(** Metadata for this span (and its context). This can be used by collectors to (** Metadata for this span (and its context). This can be used by collectors to
carry collector-specific information from the beginning carry collector-specific information from the beginning
of the span, to the end of the span. *) of the span, to the end of the span. *)
} }
(** Explicit span, with collector-specific metadata *) (** Explicit span, with collector-specific metadata.
This is richer than {!explicit_span_ctx} but not intended to be passed around
(or sent across the wire), unlike {!explicit_span_ctx}. *)
type extension_event = .. type extension_event = ..
(** An extension event, used to add features that are backend specific (** An extension event, used to add features that are backend specific

View file

@ -104,7 +104,6 @@ end = struct
end end
type async_span_info = { type async_span_info = {
async_id: int;
flavor: [ `Sync | `Async ] option; flavor: [ `Sync | `Async ] option;
name: string; name: string;
mutable data: (string * user_data) list; mutable data: (string * user_data) list;
@ -140,6 +139,12 @@ type state = {
at the end. This is a tid-sharded array of maps. *) at the end. This is a tid-sharded array of maps. *)
} }
let[@inline] mk_trace_id (self : state) : trace_id =
let n = A.fetch_and_add self.span_id_gen 1 in
let b = Bytes.create 8 in
Bytes.set_int64_le b 0 (Int64.of_int n);
Bytes.unsafe_to_string b
let key_thread_local_st : per_thread_state TLS.t = TLS.create () let key_thread_local_st : per_thread_state TLS.t = TLS.create ()
let[@inline never] mk_thread_local_st () = let[@inline never] mk_thread_local_st () =
@ -298,36 +303,33 @@ struct
| None -> !on_tracing_error (spf "unknown span %Ld" span) | None -> !on_tracing_error (spf "unknown span %Ld" span)
| Some idx -> Span_info_stack.add_data tls.spans idx data | Some idx -> Span_info_stack.add_data tls.spans idx data
let enter_manual_span ~(parent : explicit_span option) ~flavor ~__FUNCTION__:_ let enter_manual_span ~(parent : explicit_span_ctx option) ~flavor
~__FILE__:_ ~__LINE__:_ ~data name : explicit_span = ~__FUNCTION__:_ ~__FILE__:_ ~__LINE__:_ ~data name : explicit_span =
let out, tls = get_thread_output () in let out, tls = get_thread_output () in
let time_ns = Time.now_ns () in let time_ns = Time.now_ns () in
(* get the id, or make a new one *) (* get the id, or make a new one *)
let async_id = let trace_id =
match parent with match parent with
| Some m -> (Meta_map.find_exn key_async_data m.meta).async_id | Some m -> m.trace_id
| None -> A.fetch_and_add st.span_id_gen 1 | None -> mk_trace_id st
in in
FWrite.Event.Async_begin.encode out ~name ~args:data ~t_ref:tls.thread_ref FWrite.Event.Async_begin.encode out ~name ~args:data ~t_ref:tls.thread_ref
~time_ns ~async_id (); ~time_ns ~async_id:trace_id ();
{ {
span = 0L; span = 0L;
meta = trace_id;
Meta_map.( meta = Meta_map.(empty |> add key_async_data { name; flavor; data = [] });
empty |> add key_async_data { async_id; name; flavor; data = [] });
} }
let exit_manual_span (es : explicit_span) : unit = let exit_manual_span (es : explicit_span) : unit =
let { async_id; name; data; flavor = _ } = let { name; data; flavor = _ } = Meta_map.find_exn key_async_data es.meta in
Meta_map.find_exn key_async_data es.meta
in
let out, tls = get_thread_output () in let out, tls = get_thread_output () in
let time_ns = Time.now_ns () in let time_ns = Time.now_ns () in
FWrite.Event.Async_end.encode out ~name ~t_ref:tls.thread_ref ~time_ns FWrite.Event.Async_end.encode out ~name ~t_ref:tls.thread_ref ~time_ns
~args:data ~async_id () ~args:data ~async_id:es.trace_id ()
let add_data_to_manual_span (es : explicit_span) data = let add_data_to_manual_span (es : explicit_span) data =
let m = Meta_map.find_exn key_async_data es.meta in let m = Meta_map.find_exn key_async_data es.meta in

View file

@ -469,7 +469,7 @@ module Event = struct
+ Arguments.size_word args + 1 (* async id *) + Arguments.size_word args + 1 (* async id *)
let encode (out : Output.t) ~name ~(t_ref : Thread_ref.t) ~time_ns let encode (out : Output.t) ~name ~(t_ref : Thread_ref.t) ~time_ns
~(async_id : int) ~args () : unit = ~(async_id : Trace_core.trace_id) ~args () : unit =
let name = truncate_string name in let name = truncate_string name in
let size = size_word ~name ~t_ref ~args () in let size = size_word ~name ~t_ref ~args () in
let buf = Output.get_buf out ~available_word:size in let buf = Output.get_buf out ~available_word:size in
@ -494,7 +494,7 @@ module Event = struct
Buf.add_string buf name; Buf.add_string buf name;
Arguments.encode buf args; Arguments.encode buf args;
Buf.add_i64 buf (I64.of_int async_id); Buf.add_i64 buf (String.get_int64_le async_id 0);
() ()
end end
@ -505,7 +505,7 @@ module Event = struct
+ Arguments.size_word args + 1 (* async id *) + Arguments.size_word args + 1 (* async id *)
let encode (out : Output.t) ~name ~(t_ref : Thread_ref.t) ~time_ns let encode (out : Output.t) ~name ~(t_ref : Thread_ref.t) ~time_ns
~(async_id : int) ~args () : unit = ~(async_id : Trace_core.trace_id) ~args () : unit =
let name = truncate_string name in let name = truncate_string name in
let size = size_word ~name ~t_ref ~args () in let size = size_word ~name ~t_ref ~args () in
let buf = Output.get_buf out ~available_word:size in let buf = Output.get_buf out ~available_word:size in
@ -530,7 +530,7 @@ module Event = struct
Buf.add_string buf name; Buf.add_string buf name;
Arguments.encode buf args; Arguments.encode buf args;
Buf.add_i64 buf (I64.of_int async_id); Buf.add_i64 buf (String.get_int64_le async_id 0);
() ()
end end
end end

View file

@ -16,6 +16,9 @@
(* … other custom callbacks … *) (* … other custom callbacks … *)
end ]} end ]}
{b NOTE}: the [trace_id] passed alongside manual spans is guaranteed to be at
least 64 bits.
*) *)
open Trace_core open Trace_core
@ -88,7 +91,7 @@ module type S = sig
data:(string * user_data) list -> data:(string * user_data) list ->
name:string -> name:string ->
flavor:flavor option -> flavor:flavor option ->
trace_id:int -> trace_id:trace_id ->
span -> span ->
unit unit
(** Enter a manual (possibly async) span *) (** Enter a manual (possibly async) span *)
@ -100,7 +103,7 @@ module type S = sig
name:string -> name:string ->
data:(string * user_data) list -> data:(string * user_data) list ->
flavor:flavor option -> flavor:flavor option ->
trace_id:int -> trace_id:trace_id ->
span -> span ->
unit unit
(** Exit a manual span *) (** Exit a manual span *)

View file

@ -1,3 +1,3 @@
let get_time_ns () : float = let[@inline] get_time_ns () : float =
let t = Mtime_clock.now () in let t = Mtime_clock.now () in
Int64.to_float (Mtime.to_uint64_ns t) Int64.to_float (Mtime.to_uint64_ns t)

View file

@ -33,9 +33,6 @@ open struct
(** Key used to carry some information between begin and end of (** Key used to carry some information between begin and end of
manual spans, by way of the meta map *) manual spans, by way of the meta map *)
let key_manual_info : manual_span_info Meta_map.key = Meta_map.Key.create () let key_manual_info : manual_span_info Meta_map.key = Meta_map.Key.create ()
(** key used to carry a unique "id" for all spans in an async context *)
let key_async_trace_id : int Meta_map.key = Meta_map.Key.create ()
end end
let[@inline] conv_flavor = function let[@inline] conv_flavor = function
@ -64,6 +61,12 @@ let collector (Sub { st; callbacks = (module CB) } : Subscriber.t) : collector =
let module M = struct let module M = struct
let trace_id_gen_ = A.make 0 let trace_id_gen_ = A.make 0
let[@inline] mk_trace_id () : trace_id =
let n = A.fetch_and_add trace_id_gen_ 1 in
let b = Bytes.create 8 in
Bytes.set_int64_le b 0 (Int64.of_int n);
Bytes.unsafe_to_string b
(** generator for span ids *) (** generator for span ids *)
let new_span_ : unit -> int = let new_span_ : unit -> int =
let span_id_gen_ = A.make 0 in let span_id_gen_ = A.make 0 in
@ -100,8 +103,8 @@ let collector (Sub { st; callbacks = (module CB) } : Subscriber.t) : collector =
CB.on_add_data st ~data span CB.on_add_data st ~data span
) )
let enter_manual_span ~(parent : explicit_span option) ~flavor ~__FUNCTION__ let enter_manual_span ~(parent : explicit_span_ctx option) ~flavor
~__FILE__ ~__LINE__ ~data name : explicit_span = ~__FUNCTION__ ~__FILE__ ~__LINE__ ~data name : explicit_span =
let span = Int64.of_int (new_span_ ()) in let span = Int64.of_int (new_span_ ()) in
let tid = tid_ () in let tid = tid_ () in
let time_ns = now_ns () in let time_ns = now_ns () in
@ -111,8 +114,8 @@ let collector (Sub { st; callbacks = (module CB) } : Subscriber.t) : collector =
(* get the common trace id, or make a new one *) (* get the common trace id, or make a new one *)
let trace_id, parent = let trace_id, parent =
match parent with match parent with
| Some m -> Meta_map.find_exn key_async_trace_id m.meta, Some m.span | Some m -> m.trace_id, Some m.span
| None -> A.fetch_and_add trace_id_gen_ 1, None | None -> mk_trace_id (), None
in in
CB.on_enter_manual_span st ~__FUNCTION__ ~__FILE__ ~__LINE__ ~parent ~data CB.on_enter_manual_span st ~__FUNCTION__ ~__FILE__ ~__LINE__ ~parent ~data
@ -120,18 +123,13 @@ let collector (Sub { st; callbacks = (module CB) } : Subscriber.t) : collector =
let meta = let meta =
Meta_map.empty Meta_map.empty
|> Meta_map.add key_manual_info { name; flavor; data = [] } |> Meta_map.add key_manual_info { name; flavor; data = [] }
|> Meta_map.add key_async_trace_id trace_id
in in
{ span; meta } { span; trace_id; meta }
let exit_manual_span (es : explicit_span) : unit = let exit_manual_span (es : explicit_span) : unit =
let time_ns = now_ns () in let time_ns = now_ns () in
let tid = tid_ () in let tid = tid_ () in
let trace_id = let trace_id = es.trace_id in
match Meta_map.find key_async_trace_id es.meta with
| None -> assert false
| Some id -> id
in
let minfo = let minfo =
match Meta_map.find key_manual_info es.meta with match Meta_map.find key_manual_info es.meta with
| None -> assert false | None -> assert false

View file

@ -30,7 +30,7 @@ type t =
tid: int; tid: int;
name: string; name: string;
time_us: float; time_us: float;
id: int; id: trace_id;
flavor: Sub.flavor option; flavor: Sub.flavor option;
fun_name: string option; fun_name: string option;
data: (string * Sub.user_data) list; data: (string * Sub.user_data) list;
@ -41,7 +41,7 @@ type t =
time_us: float; time_us: float;
flavor: Sub.flavor option; flavor: Sub.flavor option;
data: (string * Sub.user_data) list; data: (string * Sub.user_data) list;
id: int; id: trace_id;
} }
| E_counter of { | E_counter of {
name: string; name: string;

View file

@ -142,12 +142,12 @@ module Writer = struct
args; args;
Buffer.output_buffer self.oc self.buf Buffer.output_buffer self.oc self.buf
let emit_manual_begin ~tid ~name ~id ~ts ~args ~(flavor : Sub.flavor option) let emit_manual_begin ~tid ~name ~(id : trace_id) ~ts ~args
(self : t) : unit = ~(flavor : Sub.flavor option) (self : t) : unit =
emit_sep_and_start_ self; emit_sep_and_start_ self;
Printf.bprintf self.buf Printf.bprintf self.buf
{json|{"pid":%d,"cat":"trace","id":%d,"tid": %d,"ts": %.2f,"name":%a,"ph":"%c"%a}|json} {json|{"pid":%d,"cat":"trace","id":%Ld,"tid": %d,"ts": %.2f,"name":%a,"ph":"%c"%a}|json}
self.pid id tid ts str_val name self.pid (String.get_int64_le id 0) tid ts str_val name
(match flavor with (match flavor with
| None | Some Async -> 'b' | None | Some Async -> 'b'
| Some Sync -> 'B') | Some Sync -> 'B')
@ -155,12 +155,12 @@ module Writer = struct
args; args;
Buffer.output_buffer self.oc self.buf Buffer.output_buffer self.oc self.buf
let emit_manual_end ~tid ~name ~id ~ts ~(flavor : Sub.flavor option) ~args let emit_manual_end ~tid ~name ~(id : trace_id) ~ts
(self : t) : unit = ~(flavor : Sub.flavor option) ~args (self : t) : unit =
emit_sep_and_start_ self; emit_sep_and_start_ self;
Printf.bprintf self.buf Printf.bprintf self.buf
{json|{"pid":%d,"cat":"trace","id":%d,"tid": %d,"ts": %.2f,"name":%a,"ph":"%c"%a}|json} {json|{"pid":%d,"cat":"trace","id":%Ld,"tid": %d,"ts": %.2f,"name":%a,"ph":"%c"%a}|json}
self.pid id tid ts str_val name self.pid (String.get_int64_le id 0) tid ts str_val name
(match flavor with (match flavor with
| None | Some Async -> 'e' | None | Some Async -> 'e'
| Some Sync -> 'E') | Some Sync -> 'E')