diff --git a/README.md b/README.md index 3b1233b5..24dd9af5 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,11 @@ MIT * [x] batching, perf, etc. - [ ] async collector relying on ocurl-multi - [ ] interface with `logs` (carry context around) +- [x] implicit scope (via [ambient-context][]) ## Use -For now, instrument manually: +For now, instrument traces/spans, logs, and metrics manually: ```ocaml module Otel = Opentelemetry @@ -45,16 +46,34 @@ let foo () = ]); do_more_work(); () +``` +### Setup + +If you're writing a top-level application, you need to perform some initial configuration. + +1. Set the [`service_name`][]; +2. configure our [ambient-context][] dependency with the appropriate storage for your environment — TLS, Lwt, Eio ... (see [their docs][install-ambient-storage] for more details); +3. and install a [`Collector`][] (usually by calling your collector's `with_setup` function.) + +For example, if your application is using Lwt, and you're using `ocurl` as your collector, you might do something like this: + +```ocaml let main () = Otel.Globals.service_name := "my_service"; Otel.GC_metrics.basic_setup(); + Ambient_context.with_storage_provider (Ambient_context_lwt.storage ()) @@ fun () -> Opentelemetry_client_ocurl.with_setup () @@ fun () -> (* … *) foo (); (* … *) -``` +``` + + [`service_name`]: + [`Collector`]: + [ambient-context]: + [install-ambient-storage]: ## Configuration diff --git a/src/opentelemetry.ml b/src/opentelemetry.ml index 9994ba9b..fac821c9 100644 --- a/src/opentelemetry.ml +++ b/src/opentelemetry.ml @@ -528,8 +528,11 @@ module Scope = struct | Some _ -> scope | None -> Ambient_context.get ambient_scope_key - (** [with_ambient_scope sc f] calls [f()] in a context where [sc] is the - (thread)-local scope, then reverts to the previous local scope, if any. *) + (** [with_ambient_scope sc thunk] calls [thunk()] in a context where [sc] is + the (thread|continuation)-local scope, then reverts to the previous local + scope, if any. + + @see ambient-context docs *) let[@inline] with_ambient_scope (sc : t) (f : unit -> 'a) : 'a = Ambient_context.with_binding ambient_scope_key sc (fun _ -> f ()) end @@ -761,12 +764,20 @@ module Trace = struct (** Sync span guard. - @param force_new_trace_id if true (default false), the span will not use a - ambient scope, [scope], or [trace_id], but will always - create a fresh new trace ID. + Notably, this includes {e implicit} scope-tracking: if called without a + [~scope] argument (or [~parent]/[~trace_id]), it will check in the + {!Ambient_context} for a surrounding environment, and use that as the + scope. Similarly, it uses {!Scope.with_ambient_scope} to {e set} a new + scope in the ambient context, so that any logically-nested calls to + {!with_} will use this span as their parent. {b NOTE} be careful not to call this inside a Gc alarm, as it can - cause deadlocks. *) + cause deadlocks. + + @param force_new_trace_id if true (default false), the span will not use a + ambient scope, the [~scope] argument, nor [~trace_id], but will instead + always create fresh identifiers for this span *) + let with_ ?force_new_trace_id ?trace_state ?service_name ?attrs ?kind ?trace_id ?parent ?scope ?links name (cb : Scope.t -> 'a) : 'a = let thunk, finally = diff --git a/src/trace/opentelemetry_trace.ml b/src/trace/opentelemetry_trace.ml index 34010ef9..6873fdd7 100644 --- a/src/trace/opentelemetry_trace.ml +++ b/src/trace/opentelemetry_trace.ml @@ -17,7 +17,6 @@ module Internal = struct parent_scope: Otel.Scope.t option; } - (** Table indexed by ocaml-trace spans *) module Active_span_tbl = Hashtbl.Make (struct include Int64 diff --git a/src/trace/opentelemetry_trace.mli b/src/trace/opentelemetry_trace.mli index e506d674..ca41b29c 100644 --- a/src/trace/opentelemetry_trace.mli +++ b/src/trace/opentelemetry_trace.mli @@ -2,6 +2,16 @@ module Otel := Opentelemetry module Otrace := Trace module TLS := Ambient_context_tls.Thread_local +(** [ocaml-opentelemetry.trace] implements a {!Trace_core.Collector} for {{:https://v3.ocaml.org/p/trace} ocaml-trace}. + + After installing this collector with {!setup}, you can consume libraries + that use ocaml-trace, and they will automatically emit OpenTelemetry spans + and logs. + + Both explicit scope (in the [_manual] functions such as [enter_manual_span]) + and implicit scope (in {!Internal.M.with_span}, via {!Ambient_context}) are + supported; see the detailed notes on {!Internal.M.enter_manual_span}. *) + val setup : unit -> unit (** Install the OTEL backend as a Trace collector *) @@ -22,6 +32,14 @@ module Internal : sig string (* span name *) -> (Otrace.span -> 'a) -> 'a + (** Implements {!Trace_core.Collector.S.with_span}, with the OpenTelemetry + collector as the backend. Invoked via {!Trace.with_span}. + + Notably, this has the same implicit-scope semantics as + {!Opentelemetry.Trace.with_}, and requires configuration of + {!Ambient_context}. + + @see ambient-context docs *) val enter_manual_span : parent:Otrace.explicit_span option -> @@ -32,8 +50,37 @@ module Internal : sig 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.enter_manual_toplevel_span} + and {!Trace.enter_manual_sub_span}; requires an eventual call to + {!Trace.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.explicit_span}, and exit it at any later point with + {!Trace.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.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.exit_manual_span}. Expects the [explicit_span] returned from an + earlier call to {!Trace.enter_manual_toplevel_span} or + {!Trace.enter_manual_sub_span}. + + (See the notes at {!enter_manual_span} about {!Ambient_context}.) *) val message : ?span:Otrace.span -> @@ -68,6 +115,7 @@ module Internal : sig 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]