http clients: carry a description of the export attempt into error message

This commit is contained in:
Simon Cruanes 2026-03-09 17:59:15 -04:00
parent aa86fc455d
commit ce3c85869b
No known key found for this signature in database
GPG key ID: EBFFF6F283F3A2B4
6 changed files with 39 additions and 25 deletions

View file

@ -89,8 +89,8 @@ struct
let cleanup = ignore
(* send the content to the remote endpoint/path *)
let send (client : t) ~url ~headers:user_headers ~decode (body : string) :
('a, Export_error.t) result =
let send (client : t) ~attempt_descr ~url ~headers:user_headers ~decode
(body : string) : ('a, Export_error.t) result =
Eio.Switch.run @@ fun sw ->
let uri = Uri.of_string url in
@ -138,7 +138,7 @@ struct
let r =
try
let status = Status.decode_pb_status dec in
Error (`Status (code, status))
Error (`Status (code, status, attempt_descr))
with e ->
let bt = Printexc.get_backtrace () in
Error

View file

@ -27,8 +27,8 @@ module Httpc : Generic_http_consumer.HTTPC with module IO = IO = struct
let cleanup _self = ()
(* send the content to the remote endpoint/path *)
let send (_self : t) ~url ~headers:user_headers ~decode (bod : string) :
('a, error) result Lwt.t =
let send (_self : t) ~attempt_descr ~url ~headers:user_headers ~decode
(bod : string) : ('a, error) result Lwt.t =
let uri = Uri.of_string url in
let open Cohttp in
@ -74,7 +74,7 @@ module Httpc : Generic_http_consumer.HTTPC with module IO = IO = struct
let r =
try
let status = Status.decode_pb_status dec in
Error (`Status (code, status))
Error (`Status (code, status, attempt_descr))
with e ->
let bt = Printexc.get_backtrace () in
Error

View file

@ -26,8 +26,8 @@ module Httpc : Generic_http_consumer.HTTPC with module IO = IO = struct
let cleanup self = Ezcurl_lwt.delete self
(** send the content to the remote endpoint/path *)
let send (self : t) ~url ~headers:user_headers ~decode (bod : string) :
('a, error) result Lwt.t =
let send (self : t) ~attempt_descr ~url ~headers:user_headers ~decode
(bod : string) : ('a, error) result Lwt.t =
let* r =
let headers = user_headers in
Ezcurl_lwt.post ~client:self ~headers ~params:[] ~url
@ -61,7 +61,9 @@ module Httpc : Generic_http_consumer.HTTPC with module IO = IO = struct
in
Lwt.return r)
| Ok { code; body; _ } ->
let err = Export_error.decode_invalid_http_response ~url ~code body in
let err =
Export_error.decode_invalid_http_response ~attempt_descr ~url ~code body
in
Lwt.return (Error err)
end

View file

@ -24,8 +24,8 @@ module Httpc : OTELC.Generic_http_consumer.HTTPC with module IO = IO = struct
let cleanup = Ezcurl.delete
let send (self : t) ~url ~headers:user_headers ~decode (bod : string) :
('a, error) result =
let send (self : t) ~attempt_descr ~url ~headers:user_headers ~decode
(bod : string) : ('a, error) result =
let r =
let headers = user_headers in
Ezcurl.post ~client:self ~headers ~params:[] ~url ~content:(`String bod)
@ -57,7 +57,8 @@ module Httpc : OTELC.Generic_http_consumer.HTTPC with module IO = IO = struct
(spf "decoding failed with:\n%s\n%s" (Printexc.to_string e) bt))))
| Ok { code; body; _ } ->
let err =
OTELC.Export_error.decode_invalid_http_response ~url ~code body
OTELC.Export_error.decode_invalid_http_response ~attempt_descr ~url
~code body
in
Error err
end

View file

@ -1,7 +1,9 @@
(** Error that can occur during export *)
type attempt_descr = string
type t =
[ `Status of int * Opentelemetry.Proto.Status.status
[ `Status of int * Opentelemetry.Proto.Status.status * attempt_descr
| `Failure of string
| `Sysbreak
]
@ -24,25 +26,27 @@ let report_err : t -> unit = function
message;
details;
_presence = _;
} ) ->
},
descr ) ->
Opentelemetry.Self_debug.log Opentelemetry.Self_debug.Error (fun () ->
let pp_details out l =
List.iter
(fun s -> Format.fprintf out "%S;@ " (Bytes.unsafe_to_string s))
l
in
Format.asprintf
"@[<2>opentelemetry: export failed with@ http code=%d@ status \
{@[code=%ld;@ message=%S;@ details=[@[%a@]]@]}@]"
code scode
"@[<2>opentelemetry: export failed with@ http code=%d@ attempt: %s@ \
status {@[code=%ld;@ message=%S;@ details=[@[%a@]]@]}@]"
code descr scode
(Bytes.unsafe_to_string message)
pp_details details)
let decode_invalid_http_response ~code ~url (body : string) : t =
let decode_invalid_http_response ~attempt_descr ~code ~url (body : string) : t =
try
let dec = Pbrt.Decoder.of_string body in
let status = Opentelemetry.Proto.Status.decode_pb_status dec in
`Status (code, status)
`Status (code, status, attempt_descr)
with e ->
let bt = Printexc.get_backtrace () in
`Failure

View file

@ -15,11 +15,14 @@ module type HTTPC = sig
val send :
t ->
attempt_descr:string ->
url:string ->
headers:(string * string) list ->
decode:[ `Dec of Pbrt.Decoder.t -> 'a | `Ret of 'a ] ->
string ->
('a, error) result IO.t
(** Send a HTTP request.
@param attempt_descr included in error message if this fails *)
end
module Make
@ -61,15 +64,19 @@ end = struct
(** Should we retry, based on the HTTP response code? *)
let should_retry = function
| `Failure _ -> true (* Network errors, connection issues *)
| `Status (code, _) ->
| `Status (code, _, _) ->
(* Retry on server errors, rate limits, timeouts *)
code >= 500 || code = 429 || code = 408
| `Sysbreak -> false (* User interrupt, don't retry *)
(** Retry loop over [f()] with exponential backoff *)
let rec retry_loop_ (self : t) attempt delay_ms ~f =
let rec retry_loop_ (self : t) attempt delay_ms
~(f : attempt_descr:string -> unit -> _ result IO.t) : _ result IO.t =
let open IO in
let* result = f () in
let attempt_descr =
spf "try(%d/%d)" attempt self.config.retry_max_attempts
in
let* result = f ~attempt_descr () in
match result with
| Ok x -> return (Ok x)
| Error err
@ -114,14 +121,14 @@ end = struct
~protocol:self.config.protocol res
in
let do_once () =
Httpc.send self.http ~url ~headers ~decode:(`Ret ()) data
let do_once ~attempt_descr () =
Httpc.send self.http ~attempt_descr ~url ~headers ~decode:(`Ret ()) data
in
if self.config.retry_max_attempts > 0 then
retry_loop_ self 0 self.config.retry_initial_delay_ms ~f:do_once
else
do_once ()
do_once ~attempt_descr:"single_attempt" ()
end
module C = Generic_consumer.Make (IO) (Notifier) (Sender)