wip: migrate to trace 0.11 and ambient-context 0.2

This commit is contained in:
Simon Cruanes 2026-02-17 19:54:38 -05:00
parent 98a364b046
commit 573e35fec9
No known key found for this signature in database
GPG key ID: EBFFF6F283F3A2B4
26 changed files with 189 additions and 894 deletions

View file

@ -1,4 +1,12 @@
## 0.13
- feat: adapt to trace 0.11 (callbacks-based collector API, extensible span type, no more manual spans)
- breaking: remove vendored `opentelemetry.ambient-context`; use the
`ambient-context` 0.2 package directly instead. To configure the
storage backend, call `Ambient_context.set_current_storage` (e.g.
`Ambient_context.set_current_storage Ambient_context_lwt.storage`).
## 0.12 ## 0.12
- breaking: change `Collector.cleanup` so it takes a callback - breaking: change `Collector.cleanup` so it takes a callback

View file

@ -1,5 +1,5 @@
# Opentelemetry [![build](https://github.com/imandra-ai/ocaml-opentelemetry/actions/workflows/main.yml/badge.svg)](https://github.com/imandra-ai/ocaml-opentelemetry/actions/workflows/main.yml) # Opentelemetry [![build](https://github.com/ocaml-tracing/ocaml-opentelemetry/actions/workflows/main.yml/badge.svg)](https://github.com/ocaml-tracing/ocaml-opentelemetry/actions/workflows/main.yml)
This project provides an API for instrumenting server software This project provides an API for instrumenting server software
using [opentelemetry](https://opentelemetry.io/docs), as well as using [opentelemetry](https://opentelemetry.io/docs), as well as
@ -28,7 +28,7 @@ MIT
* [x] batching, perf, etc. * [x] batching, perf, etc.
- [ ] async collector relying on ocurl-multi - [ ] async collector relying on ocurl-multi
- [ ] interface with `logs` (carry context around) - [ ] interface with `logs` (carry context around)
- [x] implicit scope (via vendored `ambient-context`, see `opentelemetry.ambient-context`) - [x] implicit scope (via [`ambient-context`](https://github.com/ocaml-tracing/ambient-context))
## Use ## Use
@ -65,16 +65,17 @@ let main () =
Otel.Globals.service_name := "my_service"; Otel.Globals.service_name := "my_service";
Otel.GC_metrics.basic_setup(); Otel.GC_metrics.basic_setup();
Opentelemetry_ambient_context.set_storage_provider (Opentelemetry_ambient_context_lwt.storage ()); (* install Lwt's fiber-local storage as the ambient context backend *)
Ambient_context.set_current_storage Ambient_context_lwt.storage;
Opentelemetry_client_ocurl.with_setup () @@ fun () -> Opentelemetry_client_ocurl.with_setup () @@ fun () ->
(* … *) (* … *)
foo (); foo ();
(* … *) (* … *)
``` ```
[`service_name`]: <https://v3.ocaml.org/p/opentelemetry/0.5/doc/Opentelemetry/Globals/index.html#val-service_name> [`service_name`]: <https://github.com/ocaml-tracing/ocaml-opentelemetry>
[`Collector`]: <https://v3.ocaml.org/p/opentelemetry/0.5/doc/Opentelemetry/Collector/index.html> [`Collector`]: <https://github.com/ocaml-tracing/ocaml-opentelemetry>
[ambient-context]: now vendored as `opentelemetry.ambient-context`, formerly <https://v3.ocaml.org/p/ambient-context> [ambient-context]: <https://github.com/ocaml-tracing/ambient-context>
## Configuration ## Configuration
@ -93,7 +94,7 @@ or the datadog agent).
Do note that this backend uses a thread pool and is incompatible Do note that this backend uses a thread pool and is incompatible
with uses of `fork` on some Unixy systems. with uses of `fork` on some Unixy systems.
See [#68](https://github.com/imandra-ai/ocaml-opentelemetry/issues/68) for a possible workaround. See [#68](https://github.com/ocaml-tracing/ocaml-opentelemetry/issues/68) for a possible workaround.
## Collector opentelemetry-client-cohttp-lwt ## Collector opentelemetry-client-cohttp-lwt
@ -103,7 +104,7 @@ inside a `Lwt_main.run` scope.
## Opentelemetry-trace ## Opentelemetry-trace
The optional library `opentelemetry.trace`, present if [trace](https://github.com/c-cube/trace) is The optional library `opentelemetry.trace`, present if [trace](https://github.com/ocaml-tracing/ocaml-trace) is
installed, provides a collector for `trace`. This collector forwards and translates installed, provides a collector for `trace`. This collector forwards and translates
events from `trace` into `opentelemetry`. It's only useful if there also is also a OTEL collector. events from `trace` into `opentelemetry`. It's only useful if there also is also a OTEL collector.

View file

@ -40,6 +40,7 @@
(>= 3.0) (>= 3.0)
(< 4.0))) (< 4.0)))
(ocaml-lsp-server :with-dev-setup) (ocaml-lsp-server :with-dev-setup)
(ambient-context (and (>= 0.2) (< 0.3)))
(ocamlformat (ocamlformat
(and (and
:with-dev-setup :with-dev-setup
@ -48,7 +49,7 @@
(depopts trace lwt eio) (depopts trace lwt eio)
(conflicts (conflicts
(trace (trace
(< 0.10))) (< 0.11)))
(tags (tags
(instrumentation tracing opentelemetry datadog jaeger))) (instrumentation tracing opentelemetry datadog jaeger)))

View file

@ -23,11 +23,12 @@ depends: [
"alcotest" {with-test} "alcotest" {with-test}
"pbrt" {>= "3.0" & < "4.0"} "pbrt" {>= "3.0" & < "4.0"}
"ocaml-lsp-server" {with-dev-setup} "ocaml-lsp-server" {with-dev-setup}
"ambient-context" {>= "0.2" & < "0.3"}
"ocamlformat" {with-dev-setup & >= "0.27" & < "0.28"} "ocamlformat" {with-dev-setup & >= "0.27" & < "0.28"}
] ]
depopts: ["trace" "lwt" "eio"] depopts: ["trace" "lwt" "eio"]
conflicts: [ conflicts: [
"trace" {< "0.10"} "trace" {< "0.11"}
] ]
build: [ build: [
["dune" "subst"] {dev} ["dune" "subst"] {dev}

View file

@ -1,15 +0,0 @@
(library
(name opentelemetry_ambient_context)
(public_name opentelemetry.ambient-context)
(synopsis
"Abstraction over thread-local storage and fiber-local storage mechanisms")
(private_modules hmap_key_)
(libraries
thread-local-storage
threads
atomic
opentelemetry.ambient-context.types
(select
hmap_key_.ml
from
(-> hmap_key_.new.ml))))

View file

@ -1,7 +0,0 @@
(library
(name opentelemetry_ambient_context_eio)
(public_name opentelemetry.ambient-context.eio)
(synopsis
"Storage backend for ambient-context using Eio's fibre-local storage")
(optional) ; eio
(libraries eio hmap opentelemetry.ambient-context thread-local-storage))

View file

@ -1,39 +0,0 @@
module Fiber = Eio.Fiber
open struct
let _internal_key : Hmap.t Fiber.key = Fiber.create_key ()
let ( let* ) = Option.bind
end
module M = struct
let name = "Storage_eio"
let[@inline] get_map () = Fiber.get _internal_key
let[@inline] with_map m cb = Fiber.with_binding _internal_key m cb
let create_key = Hmap.Key.create
let get k =
let* context = get_map () in
Hmap.find k context
let with_binding k v cb =
let new_context =
match get_map () with
| None -> Hmap.singleton k v
| Some old_context -> Hmap.add k v old_context
in
with_map new_context cb
let without_binding k cb =
let new_context =
match get_map () with
| None -> Hmap.empty
| Some old_context -> Hmap.rem k old_context
in
with_map new_context cb
end
let storage () : Opentelemetry_ambient_context.storage = (module M)

View file

@ -1,2 +0,0 @@
val storage : unit -> Opentelemetry_ambient_context.storage
(** Storage using Eio's fibers local storage *)

View file

@ -1 +0,0 @@
let key : Hmap.t Thread_local_storage.t = Thread_local_storage.create ()

View file

@ -1,7 +0,0 @@
(library
(name opentelemetry_ambient_context_lwt)
(public_name opentelemetry.ambient-context.lwt)
(optional) ; lwt
(synopsis
"Storage backend for ambient-context using Lwt's sequence-associated storage")
(libraries lwt opentelemetry.ambient-context thread-local-storage))

View file

@ -1,37 +0,0 @@
open struct
let _internal_key : Hmap.t Lwt.key = Lwt.new_key ()
let ( let* ) = Option.bind
end
module M = struct
let name = "Storage_lwt"
let[@inline] get_map () = Lwt.get _internal_key
let[@inline] with_map m cb = Lwt.with_value _internal_key (Some m) cb
let create_key = Hmap.Key.create
let get k =
let* context = get_map () in
Hmap.find k context
let with_binding k v cb =
let new_context =
match get_map () with
| None -> Hmap.singleton k v
| Some old_context -> Hmap.add k v old_context
in
with_map new_context cb
let without_binding k cb =
let new_context =
match get_map () with
| None -> Hmap.empty
| Some old_context -> Hmap.rem k old_context
in
with_map new_context cb
end
let storage () : Opentelemetry_ambient_context.storage = (module M)

View file

@ -1,2 +0,0 @@
val storage : unit -> Opentelemetry_ambient_context.storage
(** Storage using Lwt keys *)

View file

@ -1,124 +0,0 @@
module TLS = Thread_local_storage
include Opentelemetry_ambient_context_types
type 'a key = int * 'a Hmap.key
let debug =
match Sys.getenv_opt "OCAML_AMBIENT_CONTEXT_DEBUG" with
| Some ("1" | "true") -> true
| _ -> false
let _debug_id_ = Atomic.make 0
let[@inline] generate_debug_id () = Atomic.fetch_and_add _debug_id_ 1
let compare_key : int -> int -> int = Stdlib.compare
module Storage_tls_hmap = struct
let[@inline] ( let* ) o f =
match o with
| None -> None
| Some x -> f x
let key : Hmap.t TLS.t = Hmap_key_.key
let name = "Storage_tls"
let[@inline] get_map () = TLS.get_opt key
let[@inline] with_map m cb =
let old = TLS.get_opt key |> Option.value ~default:Hmap.empty in
TLS.set key m;
Fun.protect ~finally:(fun () -> TLS.set key old) cb
let create_key = Hmap.Key.create
let get k =
let* context = get_map () in
Hmap.find k context
let with_binding k v cb =
let new_context =
match get_map () with
| None -> Hmap.singleton k v
| Some old_context -> Hmap.add k v old_context
in
with_map new_context @@ fun _context -> cb ()
let without_binding k cb =
match get_map () with
| None -> cb ()
| Some old_context ->
let new_context = Hmap.rem k old_context in
with_map new_context @@ fun _context -> cb ()
end
let default_storage : storage = (module Storage_tls_hmap)
let k_current_storage : storage TLS.t = TLS.create ()
let get_current_storage () =
match TLS.get_exn k_current_storage with
| v -> v
| exception TLS.Not_set ->
let v = default_storage in
TLS.set k_current_storage v;
v
let create_key () =
let (module Store : STORAGE) = get_current_storage () in
if not debug then
0, Store.create_key ()
else (
let id = generate_debug_id () in
Printf.printf "%s: create_key %i\n%!" Store.name id;
id, Store.create_key ()
)
let get (id, k) =
let (module Store : STORAGE) = get_current_storage () in
if not debug then
Store.get k
else (
let rv = Store.get k in
(match rv with
| Some _ -> Printf.printf "%s: get %i -> Some\n%!" Store.name id
| None -> Printf.printf "%s: get %i -> None\n%!" Store.name id);
rv
)
let with_binding : 'a key -> 'a -> (unit -> 'r) -> 'r =
fun (id, k) v cb ->
let (module Store : STORAGE) = get_current_storage () in
if not debug then
Store.with_binding k v cb
else (
Printf.printf "%s: with_binding %i enter\n%!" Store.name id;
let rv = Store.with_binding k v cb in
Printf.printf "%s: with_binding %i exit\n%!" Store.name id;
rv
)
let without_binding (id, k) cb =
let (module Store : STORAGE) = get_current_storage () in
if not debug then
Store.without_binding k cb
else (
Printf.printf "%s: without_binding %i enter\n%!" Store.name id;
let rv = Store.without_binding k cb in
Printf.printf "%s: without_binding %i exit\n%!" Store.name id;
rv
)
let set_storage_provider store_new =
let store_before = get_current_storage () in
if store_new == store_before then
()
else
TLS.set k_current_storage store_new;
if debug then (
let (module Store_before : STORAGE) = store_before in
let (module Store_new : STORAGE) = store_new in
Printf.printf "set_storage_provider %s (previously %s)\n%!" Store_new.name
Store_before.name
)

View file

@ -1,55 +0,0 @@
(** Ambient context.
The ambient context, like the Matrix, is everywhere around you.
It is responsible for keeping track of that context in a manner that's
consistent with the program's choice of control flow paradigm:
- for synchronous/threaded/direct style code, {b TLS} ("thread local
storage") keeps track of a global variable per thread. Each thread has its
own copy of the variable and updates it independently of other threads.
- for Lwt, any ['a Lwt.t] created inside the [with_binding k v (fun _ -> )]
will inherit the [k := v] assignment.
- for Eio, fibers created inside [with_binding k v (fun () -> )] will
inherit the [k := v] assignment. This is consistent with the structured
concurrency approach of Eio.
The only data stored by this storage is a {!Hmap.t}, ie a heterogeneous map.
Various users (libraries, user code, etc.) can create their own {!key} to
store what they are interested in, without affecting other parts of the
storage. *)
module Types := Opentelemetry_ambient_context_types
module type STORAGE = Types.STORAGE
type storage = (module STORAGE)
val default_storage : storage
val get_current_storage : unit -> storage
val set_storage_provider : storage -> unit
type 'a key
(** A key that can be mapped to values of type ['a] in the ambient context. *)
val compare_key : int -> int -> int
(** Total order on keys *)
val create_key : unit -> 'a key
(** Create a new fresh key, distinct from any previously created key. *)
val get : 'a key -> 'a option
(** Get the current value for a given key, or [None] if no value was associated
with the key in the ambient context. *)
val with_binding : 'a key -> 'a -> (unit -> 'r) -> 'r
(** [with_binding k v cb] calls [cb()] in a context in which [k] is bound to
[v]. This does not affect storage outside of [cb()]. *)
val without_binding : 'a key -> (unit -> 'b) -> 'b
(** [without_binding k cb] calls [cb()] in a context where [k] has no binding
(possibly shadowing the current ambient binding of [k] if it exists). *)

View file

@ -1,4 +0,0 @@
(library
(name opentelemetry_ambient_context_types)
(public_name opentelemetry.ambient-context.types)
(libraries hmap thread-local-storage))

View file

@ -1,19 +0,0 @@
type 'a key = 'a Hmap.key
module type STORAGE = sig
val name : string
val get_map : unit -> Hmap.t option
val with_map : Hmap.t -> (unit -> 'b) -> 'b
val create_key : unit -> 'a key
val get : 'a key -> 'a option
val with_binding : 'a key -> 'a -> (unit -> 'b) -> 'b
val without_binding : 'a key -> (unit -> 'b) -> 'b
end
type storage = (module STORAGE)

View file

@ -1,32 +0,0 @@
(** Storage implementation.
There is a singleton storage for a given program, responsible for providing
ambient context to the rest of the program. *)
type 'a key = 'a Hmap.key
module type STORAGE = sig
val name : string
(** Name of the storage implementation. *)
val get_map : unit -> Hmap.t option
(** Get the hmap from the current ambient context, or [None] if there is no
ambient context. *)
val with_map : Hmap.t -> (unit -> 'b) -> 'b
(** [with_hmap h cb] calls [cb()] in an ambient context in which [get_map()]
will return [h]. Once [cb()] returns, the storage is reset to its previous
value. *)
val create_key : unit -> 'a key
(** Create a new storage key, guaranteed to be distinct from any previously
created key. *)
val get : 'a key -> 'a option
val with_binding : 'a key -> 'a -> (unit -> 'b) -> 'b
val without_binding : 'a key -> (unit -> 'b) -> 'b
end
type storage = (module STORAGE)

View file

@ -47,6 +47,6 @@ val pop_if_ready : ?force:bool -> now:Mtime.t -> 'a t -> 'a list option
batches before exit or because the user asks for it *) batches before exit or because the user asks for it *)
val push : 'a t -> 'a list -> [ `Dropped | `Ok ] val push : 'a t -> 'a list -> [ `Dropped | `Ok ]
(** [push b xs] is [`Ok] if it succeeds in pushing the values in [xs] into the batch (** [push b xs] is [`Ok] if it succeeds in pushing the values in [xs] into the
[b], or [`Dropped] if the current size of the batch has exceeded the high water batch [b], or [`Dropped] if the current size of the batch has exceeded the
mark determined by the [batch] argument to [{!make}]. ) *) high water mark determined by the [batch] argument to [{!make}]. ) *)

View file

@ -4,7 +4,8 @@
(flags :standard -warn-error -a+8) (flags :standard -warn-error -a+8)
(libraries (libraries
opentelemetry.proto opentelemetry.proto
opentelemetry.ambient-context ambient-context.core
ambient-context
ptime ptime
ptime.clock.os ptime.clock.os
pbrt pbrt

View file

@ -4,7 +4,7 @@ open struct
let spf = Printf.sprintf let spf = Printf.sprintf
module Atomic = Opentelemetry_atomic.Atomic module Atomic = Opentelemetry_atomic.Atomic
module Ambient_context = Opentelemetry_ambient_context module Ambient_context = Ambient_context
end end
module Lock = Lock module Lock = Lock
@ -961,7 +961,7 @@ module Scope : sig
(** Set the span's kind. (** Set the span's kind.
@since 0.11 *) @since 0.11 *)
val ambient_scope_key : t Ambient_context.key val ambient_scope_key : t Ambient_context.Context.key
(** The opaque key necessary to access/set the ambient scope with (** The opaque key necessary to access/set the ambient scope with
{!Ambient_context}. *) {!Ambient_context}. *)
@ -1097,7 +1097,8 @@ end = struct
let set_kind (scope : t) (k : Span_kind.t) : unit = let set_kind (scope : t) (k : Span_kind.t) : unit =
if Collector.has_backend () then scope.items <- Span_kind (k, scope.items) if Collector.has_backend () then scope.items <- Span_kind (k, scope.items)
let ambient_scope_key : t Ambient_context.key = Ambient_context.create_key () let ambient_scope_key : t Ambient_context.Context.key =
Ambient_context.new_key ()
let get_ambient_scope ?scope () : t option = let get_ambient_scope ?scope () : t option =
match scope with match scope with
@ -1105,7 +1106,7 @@ end = struct
| None -> Ambient_context.get ambient_scope_key | None -> Ambient_context.get ambient_scope_key
let[@inline] with_ambient_scope (sc : t) (f : unit -> 'a) : 'a = let[@inline] with_ambient_scope (sc : t) (f : unit -> 'a) : 'a =
Ambient_context.with_binding ambient_scope_key sc (fun _ -> f ()) Ambient_context.with_key_bound_to ambient_scope_key sc f
end end
(** {2 Traces} *) (** {2 Traces} *)

View file

@ -145,12 +145,19 @@ let client ?(scope : Otel.Scope.t option) (module C : Cohttp_lwt.S.Client) =
(* These types and values are not customized by our client, but are required to satisfy (* These types and values are not customized by our client, but are required to satisfy
[Cohttp_lwt.S.Client]. *) [Cohttp_lwt.S.Client]. *)
include (C : sig include (
C :
sig
type ctx = C.ctx type ctx = C.ctx
type 'a io = 'a C.io type 'a io = 'a C.io
type 'a with_context = 'a C.with_context type 'a with_context = 'a C.with_context
type body = C.body type body = C.body
val map_context : 'a with_context -> ('a -> 'b) -> 'b with_context val map_context : 'a with_context -> ('a -> 'b) -> 'b with_context
val set_cache : Cohttp_lwt.S.call -> unit val set_cache : Cohttp_lwt.S.call -> unit
end) end)

View file

@ -3,4 +3,4 @@
(public_name opentelemetry.trace) (public_name opentelemetry.trace)
(synopsis "Use opentelemetry as a collector for trace") (synopsis "Use opentelemetry as a collector for trace")
(optional) (optional)
(libraries opentelemetry.ambient-context trace.core opentelemetry)) (libraries ambient-context.core ambient-context trace.core opentelemetry))

View file

@ -6,149 +6,32 @@ open struct
let spf = Printf.sprintf let spf = Printf.sprintf
end end
module Conv = struct module Well_known = struct end
let[@inline] trace_id_of_otel (id : Otel.Trace_id.t) : Otrace.trace_id =
if id == Otel.Trace_id.dummy then
Otrace.Collector.dummy_trace_id
else
Bytes.unsafe_to_string (Otel.Trace_id.to_bytes id)
let[@inline] trace_id_to_otel (id : Otrace.trace_id) : Otel.Trace_id.t =
if id == Otrace.Collector.dummy_trace_id then
Otel.Trace_id.dummy
else
Otel.Trace_id.of_bytes @@ Bytes.unsafe_of_string id
let[@inline] span_id_of_otel (id : Otel.Span_id.t) : Otrace.span =
if id == Otel.Span_id.dummy then
Otrace.Collector.dummy_span
else
Bytes.get_int64_le (Otel.Span_id.to_bytes id) 0
let[@inline] span_id_to_otel (id : Otrace.span) : Otel.Span_id.t =
if id == Otrace.Collector.dummy_span then
Otel.Span_id.dummy
else (
let b = Bytes.create 8 in
Bytes.set_int64_le b 0 id;
Otel.Span_id.of_bytes b
)
let[@inline] ctx_to_otel (self : Otrace.explicit_span_ctx) : Otel.Span_ctx.t =
Otel.Span_ctx.make
~trace_id:(trace_id_to_otel self.trace_id)
~parent_id:(span_id_to_otel self.span)
()
let[@inline] ctx_of_otel (ctx : Otel.Span_ctx.t) : Otrace.explicit_span_ctx =
{
trace_id = trace_id_of_otel (Otel.Span_ctx.trace_id ctx);
span = span_id_of_otel (Otel.Span_ctx.parent_id ctx);
}
end
open Conv
module Well_known = struct
let spankind_key = "otrace.spankind"
let internal = `String "INTERNAL"
let server = `String "SERVER"
let client = `String "CLIENT"
let producer = `String "PRODUCER"
let consumer = `String "CONSUMER"
let spankind_of_string =
let open Otel.Span in
function
| "INTERNAL" -> Span_kind_internal
| "SERVER" -> Span_kind_server
| "CLIENT" -> Span_kind_client
| "PRODUCER" -> Span_kind_producer
| "CONSUMER" -> Span_kind_consumer
| _ -> Span_kind_unspecified
let otel_attrs_of_otrace_data data =
let kind : Otel.Span.kind ref = ref Otel.Span.Span_kind_unspecified in
let data =
List.filter_map
(function
| name, `String v when name = "otrace.spankind" ->
kind := spankind_of_string v;
None
| x -> Some x)
data
in
!kind, data
(** Key to store an error [Otel.Span.status] with the message. Set
["otrace.error" = "mymsg"] in a span data to set the span's status to
[{message="mymsg"; code=Error}]. *)
let status_error_key = "otrace.error"
end
open Well_known
let on_internal_error = let on_internal_error =
ref (fun msg -> Printf.eprintf "error in Opentelemetry_trace: %s\n%!" msg) ref (fun msg -> Printf.eprintf "error in Opentelemetry_trace: %s\n%!" msg)
type Otrace.extension_event += module Span_info = struct
| Ev_link_span of Otrace.explicit_span * Otrace.explicit_span type t = {
| Ev_set_span_kind of Otrace.explicit_span * Otel.Span_kind.t
| Ev_record_exn of Otrace.explicit_span * exn * Printexc.raw_backtrace
module Internal = struct
type span_begin = {
start_time: int64; start_time: int64;
name: string; name: string;
__FILE__: string;
__LINE__: int;
__FUNCTION__: string option;
scope: Otel.Scope.t; scope: Otel.Scope.t;
parent: Otel.Span_ctx.t option; parent: Otel.Span_ctx.t option;
} }
module Active_span_tbl = Hashtbl.Make (struct
include Int64
let hash : t -> int = Hashtbl.hash
end)
(** key to access a OTEL scope from an explicit span *)
let k_explicit_scope : Otel.Scope.t Otrace.Meta_map.key =
Otrace.Meta_map.Key.create ()
(** Per-thread set of active spans. *)
module Active_spans = struct
type t = { tbl: span_begin Active_span_tbl.t } [@@unboxed]
let create () : t = { tbl = Active_span_tbl.create 32 }
let k_tls : t TLS.t = TLS.create ()
let[@inline] get () : t =
try TLS.get_exn k_tls
with TLS.Not_set ->
let self = create () in
TLS.set k_tls self;
self
end end
let otrace_of_otel (id : Otel.Span_id.t) : int64 = type Otrace.span += Span_otel of Span_info.t
let bs = Otel.Span_id.to_bytes id in
(* lucky that it coincides! *)
assert (Bytes.length bs = 8);
Bytes.get_int64_le bs 0
let enter_span' ?(explicit_parent : Otrace.explicit_span_ctx option) type Otrace.extension_event +=
~__FUNCTION__ ~__FILE__ ~__LINE__ ~data name = | Ev_link_span of Otrace.span * Otrace.span
| Ev_set_span_kind of Otrace.span * Otel.Span_kind.t
| Ev_record_exn of Otrace.span * exn * Printexc.raw_backtrace
module Internal = struct
let enter_span' ?(parent_span : Otrace.span option) ~__FUNCTION__ ~__FILE__
~__LINE__ ~data name : Span_info.t =
let open Otel in let open Otel in
let otel_id = Span_id.create () in let span_id = Span_id.create () in
let otrace_id = otrace_of_otel otel_id in
let parent_scope = Scope.get_ambient_scope () in let parent_scope = Scope.get_ambient_scope () in
let trace_id = let trace_id =
@ -157,52 +40,21 @@ module Internal = struct
| None -> Trace_id.create () | None -> Trace_id.create ()
in in
let parent = let parent =
match explicit_parent, parent_scope with match parent_span, parent_scope with
| Some p, _ -> | Some (Span_otel parent_span), _ ->
Some Some (Otel.Scope.to_span_ctx parent_span.scope)
(Otel.Span_ctx.make ~trace_id ~parent_id:(span_id_to_otel p.span) ()) | _, Some sc -> Some (Otel.Scope.to_span_ctx sc)
| None, Some parent -> Some (Otel.Scope.to_span_ctx parent) | _, None -> None
| None, None -> None
in in
let new_scope = Otel.Scope.make ~trace_id ~span_id:otel_id ~attrs:data () in let new_scope = Otel.Scope.make ~trace_id ~span_id ~attrs:data () in
let start_time = Timestamp_ns.now_unix_ns () in let start_time = Timestamp_ns.now_unix_ns () in
let sb =
{
start_time;
name;
__FILE__;
__LINE__;
__FUNCTION__;
scope = new_scope;
parent;
}
in
let active_spans = Active_spans.get () in let attrs_function =
Active_span_tbl.add active_spans.tbl otrace_id sb;
otrace_id, sb
let exit_span_
{ start_time; name; __FILE__; __LINE__; __FUNCTION__; scope; parent } =
let open Otel in
let end_time = Timestamp_ns.now_unix_ns () in
let kind, attrs = otel_attrs_of_otrace_data (Scope.attrs scope) in
let status : Span_status.t =
match List.assoc_opt Well_known.status_error_key attrs with
| Some (`String message) -> { message; code = Status_code_error }
| _ -> { message = ""; code = Status_code_ok }
in
let attrs =
match __FUNCTION__ with match __FUNCTION__ with
| None -> | None -> []
[ "code.filepath", `String __FILE__; "code.lineno", `Int __LINE__ ]
@ attrs
| Some __FUNCTION__ -> | Some __FUNCTION__ ->
(try
let last_dot = String.rindex __FUNCTION__ '.' in let last_dot = String.rindex __FUNCTION__ '.' in
let module_path = String.sub __FUNCTION__ 0 last_dot in let module_path = String.sub __FUNCTION__ 0 last_dot in
let function_name = let function_name =
@ -210,162 +62,99 @@ module Internal = struct
(String.length __FUNCTION__ - last_dot - 1) (String.length __FUNCTION__ - last_dot - 1)
in in
[ [
"code.filepath", `String __FILE__;
"code.lineno", `Int __LINE__;
"code.function", `String function_name; "code.function", `String function_name;
"code.namespace", `String module_path; "code.namespace", `String module_path;
] ]
@ attrs with Not_found -> [])
in in
let parent_id = Option.map Otel.Span_ctx.parent_id parent in (* directly store file, line, etc in scope *)
Span.create ~kind ~trace_id:scope.trace_id ?parent:parent_id ~status Otel.Scope.add_attrs new_scope (fun () ->
("code.filepath", `String __FILE__)
:: ("code.lineno", `Int __LINE__)
:: attrs_function);
{ Span_info.start_time; name; scope = new_scope; parent }
let exit_span_ ({ name; start_time; scope; parent } : Span_info.t) :
Otel.Span.t =
let open Otel in
let end_time = Timestamp_ns.now_unix_ns () in
let attrs = Scope.attrs scope in
let parent_id = Option.map Span_ctx.parent_id parent in
let kind = Scope.kind scope in
let status = Scope.status scope in
Span.create ?kind ~trace_id:scope.trace_id ?parent:parent_id ?status
~id:scope.span_id ~start_time ~end_time ~attrs ~id:scope.span_id ~start_time ~end_time ~attrs
~events:(Scope.events scope) name ~events:(Scope.events scope) ~links:(Scope.links scope) name
|> fst |> fst
let exit_span' otrace_id otel_span_begin = let enter_span _st ~__FUNCTION__ ~__FILE__ ~__LINE__ ~level:_ ~params:_ ~data
let active_spans = Active_spans.get () in ~parent name : Otrace.span =
Active_span_tbl.remove active_spans.tbl otrace_id; let parent_span =
exit_span_ otel_span_begin
let exit_span_from_id otrace_id =
let active_spans = Active_spans.get () in
match Active_span_tbl.find_opt active_spans.tbl otrace_id with
| None -> None
| Some otel_span_begin ->
Active_span_tbl.remove active_spans.tbl otrace_id;
Some (exit_span_ otel_span_begin)
let[@inline] get_scope (span : Otrace.explicit_span) : Otel.Scope.t option =
Otrace.Meta_map.find k_explicit_scope span.meta
module M = struct
let with_span ~__FUNCTION__ ~__FILE__ ~__LINE__ ~data name cb =
let otrace_id, sb =
enter_span' ~__FUNCTION__ ~__FILE__ ~__LINE__ ~data name
in
Otel.Scope.with_ambient_scope sb.scope @@ fun () ->
match cb otrace_id with
| res ->
let otel_span = exit_span' otrace_id sb in
Otel.Trace.emit [ otel_span ];
res
| exception e ->
let bt = Printexc.get_raw_backtrace () in
Otel.Scope.record_exception sb.scope e bt;
let otel_span = exit_span' otrace_id sb in
Otel.Trace.emit [ otel_span ];
Printexc.raise_with_backtrace e bt
let enter_span ~__FUNCTION__ ~__FILE__ ~__LINE__ ~data name :
Trace_core.span =
let otrace_id, _sb =
enter_span' ~__FUNCTION__ ~__FILE__ ~__LINE__ ~data name
in
(* NOTE: we cannot enter ambient scope in a disjoint way
with the exit, because we only have [Ambient_context.with_binding],
no [set_binding] *)
otrace_id
let exit_span otrace_id =
match exit_span_from_id otrace_id with
| None -> ()
| Some otel_span -> Otel.Trace.emit [ otel_span ]
let enter_manual_span ~(parent : Otrace.explicit_span_ctx option) ~flavor:_
~__FUNCTION__ ~__FILE__ ~__LINE__ ~data name : Otrace.explicit_span =
let otrace_id, sb =
match parent with match parent with
| None -> enter_span' ~__FUNCTION__ ~__FILE__ ~__LINE__ ~data name | Otrace.P_some sp -> Some sp
| Some parent -> | _ -> None
enter_span' ~explicit_parent:parent ~__FUNCTION__ ~__FILE__ ~__LINE__
~data name
in in
let span_info =
enter_span' ?parent_span ~__FUNCTION__ ~__FILE__ ~__LINE__ ~data name
in
Span_otel span_info
let active_spans = Active_spans.get () in let exit_span _st (span : Otrace.span) =
Active_span_tbl.add active_spans.tbl otrace_id sb;
Otrace.
{
span = otrace_id;
trace_id = trace_id_of_otel sb.scope.trace_id;
meta = Meta_map.(empty |> add k_explicit_scope sb.scope);
}
let exit_manual_span Otrace.{ span = otrace_id; _ } =
let active_spans = Active_spans.get () in
match Active_span_tbl.find_opt active_spans.tbl otrace_id with
| None -> !on_internal_error (spf "no active span with ID %Ld" otrace_id)
| Some sb ->
let otel_span = exit_span' otrace_id sb in
Otel.Trace.emit [ otel_span ]
let add_data_to_span otrace_id data =
let active_spans = Active_spans.get () in
match Active_span_tbl.find_opt active_spans.tbl otrace_id with
| None -> !on_internal_error (spf "no active span with ID %Ld" otrace_id)
| Some sb -> Otel.Scope.add_attrs sb.scope (fun () -> data)
let add_data_to_manual_span (span : Otrace.explicit_span) data : unit =
match get_scope span with
| None ->
!on_internal_error (spf "manual span does not a contain an OTEL scope")
| Some scope -> Otel.Scope.add_attrs scope (fun () -> data)
let message ?span ~data:_ msg : unit =
(* gather information from context *)
let old_scope = Otel.Scope.get_ambient_scope () in
let trace_id = Option.map (fun sc -> sc.Otel.Scope.trace_id) old_scope in
let span_id =
match span with match span with
| Some id -> Some (span_id_to_otel id) | Span_otel span_info ->
| None -> Option.map (fun sc -> sc.Otel.Scope.span_id) old_scope let otel_span = exit_span_ span_info in
Otel.Trace.emit [ otel_span ]
| _ -> ()
let add_data_to_span _st (span : Otrace.span) data =
match span with
| Span_otel span_info ->
Otel.Scope.add_attrs span_info.scope (fun () -> data)
| _ -> ()
let message _st ~level:_ ~params:_ ~data:_ ~span msg : unit =
let trace_id, span_id =
match span with
| Some (Span_otel si) -> Some si.scope.trace_id, Some si.scope.span_id
| _ ->
(match Otel.Scope.get_ambient_scope () with
| None -> None, None
| Some scope -> Some scope.trace_id, Some scope.span_id)
in in
let log = Otel.Logs.make_str ?trace_id ?span_id msg in let log = Otel.Logs.make_str ?trace_id ?span_id msg in
Otel.Logs.emit [ log ] Otel.Logs.emit [ log ]
let shutdown () = () let metric _st ~level:_ ~params:_ ~data:attrs name v =
let open Otrace.Core_ext in
let name_process _name = () match v with
| Metric_int i ->
let name_thread _name = () let m = Otel.Metrics.(gauge ~name [ int ~attrs i ]) in
let counter_int ~data name cur_val : unit =
let _kind, attrs = otel_attrs_of_otrace_data data in
let m = Otel.Metrics.(gauge ~name [ int ~attrs cur_val ]) in
Otel.Metrics.emit [ m ] Otel.Metrics.emit [ m ]
| Metric_float f ->
let counter_float ~data name cur_val : unit = let m = Otel.Metrics.(gauge ~name [ float ~attrs f ]) in
let _kind, attrs = otel_attrs_of_otrace_data data in
let m = Otel.Metrics.(gauge ~name [ float ~attrs cur_val ]) in
Otel.Metrics.emit [ m ] Otel.Metrics.emit [ m ]
let extension_event = function
| Ev_link_span (sp1, sp2) ->
(match get_scope sp1, get_scope sp2 with
| Some sc1, Some sc2 ->
Otel.Scope.add_links sc1 (fun () -> [ Otel.Scope.to_span_link sc2 ])
| _ -> !on_internal_error "could not find scope for OTEL span")
| Ev_set_span_kind (sp, k) ->
(match get_scope sp with
| None -> !on_internal_error "could not find scope for OTEL span"
| Some sc -> Otel.Scope.set_kind sc k)
| Ev_record_exn (sp, exn, bt) ->
(match get_scope sp with
| None -> !on_internal_error "could not find scope for OTEL span"
| Some sc -> Otel.Scope.record_exception sc exn bt)
| _ -> () | _ -> ()
end
let extension _st ~level:_ ev =
match ev with
| Ev_link_span (Span_otel sb1, Span_otel sb2) ->
Otel.Scope.add_links sb1.scope (fun () ->
[ Otel.Scope.to_span_link sb2.scope ])
| Ev_set_span_kind (Span_otel sb, k) -> Otel.Scope.set_kind sb.scope k
| Ev_record_exn (Span_otel sb, exn, bt) ->
Otel.Scope.record_exception sb.scope exn bt
| _ -> ()
let callbacks : unit Otrace.Collector.Callbacks.t =
Otrace.Collector.Callbacks.make ~enter_span ~exit_span ~add_data_to_span
~message ~metric ~extension ()
end end
let link_spans (sp1 : Otrace.explicit_span) (sp2 : Otrace.explicit_span) : unit let link_spans (sp1 : Otrace.span) (sp2 : Otrace.span) : unit =
=
if Otrace.enabled () then Otrace.extension_event @@ Ev_link_span (sp1, sp2) if Otrace.enabled () then Otrace.extension_event @@ Ev_link_span (sp1, sp2)
let set_span_kind sp k : unit = let set_span_kind sp k : unit =
@ -374,7 +163,13 @@ let set_span_kind sp k : unit =
let record_exception sp exn bt : unit = let record_exception sp exn bt : unit =
if Otrace.enabled () then Otrace.extension_event @@ Ev_record_exn (sp, exn, bt) if Otrace.enabled () then Otrace.extension_event @@ Ev_record_exn (sp, exn, bt)
let collector () : Otrace.collector = (module Internal.M) let with_ambient_span (sp : Otrace.span) f =
match sp with
| Span_otel sb -> Otel.Scope.with_ambient_scope sb.scope f
| _ -> f ()
let collector () : Otrace.collector =
Trace_core.Collector.C_some ((), Internal.callbacks)
let setup () = Otrace.setup_collector @@ collector () let setup () = Otrace.setup_collector @@ collector ()

View file

@ -1,21 +1,3 @@
module Otel := Opentelemetry
module Otrace := Trace_core
module TLS := Thread_local_storage
module Conv : sig
val trace_id_of_otel : Otel.Trace_id.t -> string
val trace_id_to_otel : string -> Otel.Trace_id.t
val span_id_of_otel : Otel.Span_id.t -> int64
val span_id_to_otel : int64 -> Otel.Span_id.t
val ctx_to_otel : Otrace.explicit_span_ctx -> Otel.Span_ctx.t
val ctx_of_otel : Otel.Span_ctx.t -> Otrace.explicit_span_ctx
end
(** [opentelemetry.trace] implements a {!Trace_core.Collector} for (** [opentelemetry.trace] implements a {!Trace_core.Collector} for
{{:https://v3.ocaml.org/p/trace} ocaml-trace}. {{:https://v3.ocaml.org/p/trace} ocaml-trace}.
@ -23,33 +5,27 @@ end
that use [ocaml-trace], and they will automatically emit OpenTelemetry spans that use [ocaml-trace], and they will automatically emit OpenTelemetry spans
and logs. and logs.
Both explicit scope (in the [_manual] functions such as [enter_manual_span]) [Ambient_context] is used to propagate the current span to child spans.
and implicit scope (in {!Internal.M.with_span}, via {!Ambient_context}) are
supported; see the detailed notes on {!Internal.M.enter_manual_span}.
{1:wellknown Well-known identifiers} [Trace_core.extension_event] is used to expose OTEL-specific features on top
of the common tracing interface, e.g. to set the span kind:
Because [ocaml-trace]'s API is a subset of OpenTelemetry functionality, this
interface allows for a few 'well-known' identifiers to be used in
[Trace]-instrumented libraries that wish to further support OpenTelemetry
usage.
(These strings will not change in subsequent versions of this library, so
you do not need to depend on [opentelemetry.trace] to use them.)
- If a key of exactly ["otrace.spankind"] is included in the
{!Trace_core.user_data} passed to [with_span] et al., it will be used as
the {!Opentelemetry.Span.kind} of the emitted span. (See
{!Internal.spankind_of_string} for the list of supported values.)
{[ {[
ocaml let@ span = Trace_core.with_span ~__FILE__ ~__LINE__ "my-span" in
let describe () = [ Opentelemetry_trace.(spankind_key, client) ] in Opentelemetry_trace.set_span_kind span Span_kind_client
Trace_core.with_span ~__FILE__ ~__LINE__ ~data:describe "my-span"
@@ fun _ ->
(* ... *) (* ... *)
]} *) ]} *)
module Otel := Opentelemetry
module Otrace := Trace_core
type Otrace.extension_event +=
| Ev_link_span of Otrace.span * Otrace.span
(** Link the two spans together. Both must be currently active spans. *)
| Ev_set_span_kind of Otrace.span * Otel.Span_kind.t
| Ev_record_exn of Otrace.span * exn * Printexc.raw_backtrace
(** Record exception and potentially turn span to an error *)
val on_internal_error : (string -> unit) ref val on_internal_error : (string -> unit) ref
(** Callback to print errors in the library itself (ie bugs) *) (** Callback to print errors in the library itself (ie bugs) *)
@ -62,166 +38,23 @@ val setup_with_otel_backend : Opentelemetry.Collector.backend -> unit
val collector : unit -> Trace_core.collector val collector : unit -> Trace_core.collector
(** Make a Trace collector that uses the OTEL backend to send spans and logs *) (** Make a Trace collector that uses the OTEL backend to send spans and logs *)
val link_spans : Otrace.explicit_span -> Otrace.explicit_span -> unit val link_spans : Otrace.span -> Otrace.span -> unit
(** [link_spans sp1 sp2] modifies [sp1] by adding a span link to [sp2]. (** [link_spans sp1 sp2] modifies [sp1] by adding a span link to [sp2].
@since 0.11 *) @since 0.11 *)
val set_span_kind : Otrace.explicit_span -> Otel.Span.kind -> unit val set_span_kind : Otrace.span -> Otel.Span_kind.t -> unit
(** [set_span_kind sp k] sets the span's kind. (** [set_span_kind sp k] sets the span's kind.
@since 0.11 *) @since 0.11 *)
val record_exception : val record_exception : Otrace.span -> exn -> Printexc.raw_backtrace -> unit
Otrace.explicit_span -> exn -> Printexc.raw_backtrace -> unit
(** Record exception in the current span. (** Record exception in the current span.
@since 0.11 *) @since 0.11 *)
(** Static references for well-known identifiers; see {!label-wellknown}. *) val with_ambient_span : Otrace.span -> (unit -> 'a) -> 'a
module Well_known : sig (** [with_ambient_span sp f] calls [f()] in an ambient context where [sp] is the
val spankind_key : string current span. *)
val internal : Otrace.user_data module Well_known : sig end
[@@deprecated
val server : Otrace.user_data "use the regular functions such as `link_spans` or `set_span_kind` for this"]
(** Static references for well-known identifiers *)
val client : Otrace.user_data
val producer : Otrace.user_data
val consumer : Otrace.user_data
val spankind_of_string : string -> Otel.Span.kind
val otel_attrs_of_otrace_data :
(string * Otrace.user_data) list ->
Otel.Span.kind * Otel.Span.key_value list
end
[@@deprecated "use the regular functions for this"]
(**/**)
(** Internal implementation details; do not consider these stable. *)
module Internal : sig
module M : sig
val with_span :
__FUNCTION__:string option ->
__FILE__:string ->
__LINE__:int ->
data:(string * Otrace.user_data) list ->
string (* span name *) ->
(Otrace.span -> 'a) ->
'a
(** Implements {!Trace_core.Collector.S.with_span}, with the OpenTelemetry
collector as the backend. Invoked via {!Trace_core.with_span}.
Notably, this has the same implicit-scope semantics as
{!Opentelemetry.Trace.with_}, and requires configuration of
{!Ambient_context}.
@see <https://github.com/ELLIOTTCABLE/ocaml-ambient-context>
ambient-context docs *)
val enter_manual_span :
parent:Otrace.explicit_span_ctx option ->
flavor:'a ->
__FUNCTION__:string option ->
__FILE__:string ->
__LINE__:int ->
data:(string * Otrace.user_data) list ->
string (* span name *) ->
Otrace.explicit_span
(** Implements {!Trace_core.Collector.S.enter_manual_span}, with the
OpenTelemetry collector as the backend. Invoked at
{!Trace_core.enter_manual_toplevel_span} and
{!Trace_core.enter_manual_sub_span}; requires an eventual call to
{!Trace_core.exit_manual_span}.
These 'manual span' functions {e do not} implement the same implicit-
scope semantics of {!with_span}; and thus don't need to wrap a single
stack-frame / callback; you can freely enter a span at any point, store
the returned {!Trace_core.explicit_span}, and exit it at any later point
with {!Trace_core.exit_manual_span}.
However, for that same reason, they also cannot update the
{!Ambient_context} that is, when you invoke the various [manual]
functions, if you then invoke other functions that use
{!Trace_core.with_span}, those callees {e will not} see the span you
entered manually as their [parent].
Generally, the best practice is to only use these [manual] functions at
the 'leaves' of your callstack: that is, don't invoke user callbacks
from within them; or if you do, make sure to pass the [explicit_span]
you recieve from this function onwards to the user callback, so they can
create further child-spans. *)
val exit_manual_span : Otrace.explicit_span -> unit
(** Implements {!Trace_core.Collector.S.exit_manual_span}, with the
OpenTelemetry collector as the backend. Invoked at
{!Trace_core.exit_manual_span}. Expects the [explicit_span] returned
from an earlier call to {!Trace_core.enter_manual_toplevel_span} or
{!Trace_core.enter_manual_sub_span}.
(See the notes at {!enter_manual_span} about {!Ambient_context}.) *)
val add_data_to_span :
Otrace.span -> (string * Otrace.user_data) list -> unit
val add_data_to_manual_span :
Otrace.explicit_span -> (string * Otrace.user_data) list -> unit
val message :
?span:Otrace.span ->
data:(string * Otrace.user_data) list ->
string ->
unit
val shutdown : unit -> unit
val name_process : string -> unit
val name_thread : string -> unit
val counter_int :
data:(string * Otrace.user_data) list -> string -> int -> unit
val counter_float :
data:(string * Otrace.user_data) list -> string -> float -> unit
end
type span_begin = {
start_time: int64;
name: string;
__FILE__: string;
__LINE__: int;
__FUNCTION__: string option;
scope: Otel.Scope.t;
parent: Otel.Span_ctx.t option;
}
module Active_span_tbl : Hashtbl.S with type key = Otrace.span
(** Table indexed by ocaml-trace spans. *)
module Active_spans : sig
type t = private { tbl: span_begin Active_span_tbl.t } [@@unboxed]
val create : unit -> t
val k_tls : t TLS.t
val get : unit -> t
end
val otrace_of_otel : Otel.Span_id.t -> Otrace.span
val enter_span' :
?explicit_parent:Otrace.explicit_span_ctx ->
__FUNCTION__:string option ->
__FILE__:string ->
__LINE__:int ->
data:(string * Otrace.user_data) list ->
string ->
Otrace.span * span_begin
val exit_span' : Otrace.span -> span_begin -> Otel.Span.t
end
(**/**)

View file

@ -48,8 +48,7 @@
opentelemetry-client-cohttp-lwt opentelemetry-client-cohttp-lwt
opentelemetry-cohttp-lwt opentelemetry-cohttp-lwt
opentelemetry-logs opentelemetry-logs
logs logs))
))
(executable (executable
(name cohttp_client) (name cohttp_client)
@ -59,5 +58,3 @@
opentelemetry opentelemetry
opentelemetry-client-cohttp-lwt opentelemetry-client-cohttp-lwt
opentelemetry-cohttp-lwt)) opentelemetry-cohttp-lwt))

View file

@ -3,16 +3,10 @@
; Make the binaries for the test emitters available on the path for the components defined in this dir. ; Make the binaries for the test emitters available on the path for the components defined in this dir.
; See https://dune.readthedocs.io/en/stable/reference/dune/env.html ; See https://dune.readthedocs.io/en/stable/reference/dune/env.html
(binaries (binaries
(../bin/emit_logs_cohttp.exe as emit_logs_cohttp) (../bin/emit_logs_cohttp.exe as emit_logs_cohttp))))
)))
(tests (tests
(names test_logs_e2e) (names test_logs_e2e)
(package opentelemetry-logs) (package opentelemetry-logs)
(libraries (libraries opentelemetry opentelemetry-logs signal_gatherer alcotest)
opentelemetry (deps %{bin:emit_logs_cohttp}))
opentelemetry-logs
signal_gatherer
alcotest)
(deps %{bin:emit_logs_cohttp})
)