ocaml-opentelemetry/src/trace/opentelemetry_trace.ml
Simon Cruanes 1ebd474423 fix: critical bugs found in code review
Bug #1: Fix worker count logic in generic_consumer
- Was: min 2 (max 500 n_workers) - always created 2 workers
- Now: max 2 (min 500 n_workers) - properly clamps between 2-500
- Impact: Worker configuration was completely ignored

Bug #2: Handle missing dot in __FUNCTION__ name
- Added exception handling for String.rindex in trace span creation
- Prevents crash when tracing top-level or non-module functions
- Uses option type for module_path when no dot is present
- Scoped try/catch to only parsing logic
2026-02-08 06:16:05 +00:00

239 lines
7.9 KiB
OCaml

open Common_
module Extensions = struct
type Otrace.extension_event +=
| Ev_link_span of Otrace.span * OTEL.Span_ctx.t
| Ev_record_exn of {
sp: Otrace.span;
exn: exn;
bt: Printexc.raw_backtrace;
}
| Ev_set_span_kind of Otrace.span * OTEL.Span_kind.t
| Ev_set_span_status of Otrace.span * OTEL.Span_status.t
type Otrace.metric +=
| Metric_hist of OTEL.Metrics.histogram_data_point
| Metric_sum_int of int
| Metric_sum_float of float
end
open Extensions
open struct
type Otrace.span += Span_otel of OTEL.Span.t
end
open struct
type state = {
clock: Opentelemetry_core.Clock.t;
exporter: OTEL.Exporter.t;
}
let create_state ~(exporter : OTEL.Exporter.t) () : state =
let clock = exporter.clock in
{ clock; exporter }
(* sanity check: otrace meta-map must be the same as hmap *)
let () = ignore (fun (k : _ Hmap.key) : _ Ambient_context.Context.key -> k)
(** Key to access the current span context. *)
let k_span_ctx : OTEL.Span_ctx.t Ambient_context.Context.key =
Ambient_context.Context.new_key ()
let enter_span (self : state) ~__FUNCTION__ ~__FILE__ ~__LINE__ ~level:_
~params:_ ~(data : (_ * Otrace.user_data) list) ~parent name : Otrace.span
=
let start_time = OTEL.Clock.now self.clock in
let trace_id, parent_id =
match parent with
| Otrace.P_some (Span_otel sp) ->
OTEL.Span.trace_id sp, Some (OTEL.Span.id sp)
| _ ->
(match Ambient_context.get k_span_ctx with
| Some sp_ctx ->
OTEL.Span_ctx.trace_id sp_ctx, Some (OTEL.Span_ctx.parent_id sp_ctx)
| None -> OTEL.Trace_id.create (), None)
in
let span_id = OTEL.Span_id.create () in
let attrs =
("code.filepath", `String __FILE__)
:: ("code.lineno", `Int __LINE__)
:: data
in
let otel_sp : OTEL.Span.t =
OTEL.Span.make ~start_time ~id:span_id ~trace_id ~attrs ?parent:parent_id
~end_time:0L name
in
(* add more data if [__FUNCTION__] is present *)
(match __FUNCTION__ with
| Some __FUNCTION__ when OTEL.Span.is_not_dummy otel_sp ->
let function_name, module_path =
try
let last_dot = String.rindex __FUNCTION__ '.' in
let module_path = String.sub __FUNCTION__ 0 last_dot in
let function_name =
String.sub __FUNCTION__ (last_dot + 1)
(String.length __FUNCTION__ - last_dot - 1)
in
function_name, Some module_path
with Not_found ->
(* __FUNCTION__ has no dot, use it as-is *)
__FUNCTION__, None
in
let attrs =
("code.function", `String function_name)
::
(match module_path with
| Some module_path -> [ "code.namespace", `String module_path ]
| None -> [])
in
OTEL.Span.add_attrs otel_sp attrs
| _ -> ());
Span_otel otel_sp
let exit_span (self : state) sp =
match sp with
| Span_otel span when OTEL.Span.is_not_dummy span ->
(* emit the span after setting the end timestamp *)
let end_time = OTEL.Clock.now self.clock in
OTEL.Proto.Trace.span_set_end_time_unix_nano span end_time;
OTEL.Exporter.send_trace self.exporter [ span ]
| _ -> ()
let add_data_to_span _self span (data : (_ * Otrace.user_data) list) =
match span with
| Span_otel sp -> OTEL.Span.add_attrs sp data
| _ -> ()
let severity_of_level : Trace_core.Level.t -> _ = function
| Trace -> OTEL.Log_record.Severity_number_trace
| Debug1 -> OTEL.Log_record.Severity_number_debug
| Debug2 -> OTEL.Log_record.Severity_number_debug2
| Debug3 -> OTEL.Log_record.Severity_number_debug3
| Error -> OTEL.Log_record.Severity_number_error
| Info -> OTEL.Log_record.Severity_number_info
| Warning -> OTEL.Log_record.Severity_number_warn
let message (self : state) ~(level : Trace_core.Level.t) ~params:_ ~data ~span
msg : unit =
let observed_time_unix_nano = OTEL.Clock.now self.clock in
let trace_id, span_id =
match span with
| Some (Span_otel sp) ->
Some (OTEL.Span.trace_id sp), Some (OTEL.Span.id sp)
| _ ->
(match Ambient_context.get k_span_ctx with
| Some sp ->
Some (OTEL.Span_ctx.trace_id sp), Some (OTEL.Span_ctx.parent_id sp)
| _ -> None, None)
in
let severity = severity_of_level level in
let log =
OTEL.Log_record.make ~severity ?trace_id ?span_id ~attrs:data
~observed_time_unix_nano (`String msg)
in
OTEL.Exporter.send_logs self.exporter [ log ]
let metric (self : state) ~level:_ ~params:_ ~data:attrs name v : unit =
let now = OTEL.Clock.now self.clock in
let kind =
let open Trace_core.Core_ext in
match v with
| Metric_int i -> `gauge (OTEL.Metrics.int ~attrs ~now i)
| Metric_float v -> `gauge (OTEL.Metrics.float ~attrs ~now v)
| Metric_sum_int i -> `sum (OTEL.Metrics.int ~attrs ~now i)
| Metric_sum_float v -> `sum (OTEL.Metrics.float ~attrs ~now v)
| Metric_hist h -> `hist h
| _ -> `none
in
let m =
match kind with
| `none -> []
| `gauge v -> [ OTEL.Metrics.gauge ~name [ v ] ]
| `sum v -> [ OTEL.Metrics.sum ~name [ v ] ]
| `hist h -> [ OTEL.Metrics.histogram ~name [ h ] ]
in
if m <> [] then OTEL.Exporter.send_metrics self.exporter m
let extension (_self : state) ~level:_ ev =
match ev with
| Ev_link_span (Span_otel sp1, sc2) ->
OTEL.Span.add_links sp1 [ OTEL.Span_link.of_span_ctx sc2 ]
| Ev_link_span _ -> ()
| Ev_set_span_kind (Span_otel sp, k) -> OTEL.Span.set_kind sp k
| Ev_set_span_kind _ -> ()
| Ev_set_span_status (Span_otel sp, st) -> OTEL.Span.set_status sp st
| Ev_set_span_status _ -> ()
| Ev_record_exn { sp = Span_otel sp; exn; bt } ->
OTEL.Span.record_exception sp exn bt
| Ev_record_exn _ -> ()
| _ -> ()
let shutdown self = OTEL.Exporter.shutdown self.exporter
let callbacks : state Otrace.Collector.Callbacks.t =
Otrace.Collector.Callbacks.make ~enter_span ~exit_span ~add_data_to_span
~message ~metric ~extension ~shutdown ()
end
let collector_of_exporter (exporter : OTEL.Exporter.t) : Trace_core.collector =
let st = create_state ~exporter () in
Trace_core.Collector.C_some (st, callbacks)
let with_ambient_span (sp : Otrace.span) f =
match sp with
| Span_otel sp ->
Ambient_context.with_key_bound_to k_span_ctx (OTEL.Span.to_span_ctx sp) f
| _ -> f ()
let with_ambient_span_ctx (sp : OTEL.Span_ctx.t) f =
Ambient_context.with_key_bound_to k_span_ctx sp f
let link_span_to_otel_ctx (sp1 : Otrace.span) (sp2 : OTEL.Span_ctx.t) : unit =
if Otrace.enabled () then Otrace.extension_event @@ Ev_link_span (sp1, sp2)
let link_spans (sp1 : Otrace.span) (sp2 : Otrace.span) : unit =
if Otrace.enabled () then (
match sp2 with
| Span_otel sp2 ->
Otrace.extension_event @@ Ev_link_span (sp1, OTEL.Span.to_span_ctx sp2)
| _ -> ()
)
let[@inline] set_span_kind sp k : unit =
if Otrace.enabled () then Otrace.extension_event @@ Ev_set_span_kind (sp, k)
let[@inline] set_span_status sp status : unit =
if Otrace.enabled () then
Otrace.extension_event @@ Ev_set_span_status (sp, status)
let record_exception sp exn bt : unit =
if Otrace.enabled () then
Otrace.extension_event @@ Ev_record_exn { sp; exn; bt }
(** Collector that forwards to the {b currently installed} OTEL exporter. *)
let collector_main_otel_exporter () : Otrace.collector =
collector_of_exporter OTEL.Main_exporter.dynamic_forward_to_main_exporter
let (collector
[@deprecated "use collector_of_exporter or collector_main_otel_exporter"])
=
collector_main_otel_exporter
let setup () = Otrace.setup_collector @@ collector_main_otel_exporter ()
let setup_with_otel_exporter exp : unit =
let coll = collector_of_exporter exp in
OTEL.Main_exporter.set exp;
Otrace.setup_collector coll
let setup_with_otel_backend = setup_with_otel_exporter
module Well_known = struct end