A batteries included HTTP/1.1 client in OCaml
at main 167 lines 5.3 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6(** HTTP request timing metrics 7 8 Per Recommendation #12: Detailed timing breakdown for requests, similar to 9 curl's --write-out timing variables. 10 11 Timing phases: 12 {[ 13 |--DNS--|--Connect--|--TLS--|--Request--|--Wait--|--Content--| 14 ^ ^ ^ ^ ^ ^ 15 namelookup connect ssl_handsh send ttfb total 16 ]} *) 17 18let src = Logs.Src.create "requests.timing" ~doc:"HTTP request timing" 19 20module Log = (val Logs.src_log src : Logs.LOG) 21 22type t = { 23 dns_lookup : float option; (** Time for DNS resolution *) 24 tcp_connect : float option; (** Time to establish TCP connection *) 25 tls_handshake : float option; (** Time for TLS handshake (HTTPS only) *) 26 request_sent : float option; (** Time to send request *) 27 time_to_first_byte : float option; 28 (** Time from request sent to first byte received *) 29 content_transfer : float option; (** Time to transfer response body *) 30 total : float; (** Total request time *) 31} 32(** Timing metrics for a single request *) 33 34let empty = 35 { 36 dns_lookup = None; 37 tcp_connect = None; 38 tls_handshake = None; 39 request_sent = None; 40 time_to_first_byte = None; 41 content_transfer = None; 42 total = 0.0; 43 } 44 45let v ?dns_lookup ?tcp_connect ?tls_handshake ?request_sent ?time_to_first_byte 46 ?content_transfer ~total () = 47 { 48 dns_lookup; 49 tcp_connect; 50 tls_handshake; 51 request_sent; 52 time_to_first_byte; 53 content_transfer; 54 total; 55 } 56 57let dns_lookup t = t.dns_lookup 58let tcp_connect t = t.tcp_connect 59let tls_handshake t = t.tls_handshake 60let request_sent t = t.request_sent 61let time_to_first_byte t = t.time_to_first_byte 62let content_transfer t = t.content_transfer 63let total t = t.total 64 65(** Connection setup time (DNS + TCP + TLS) *) 66let connection_time t = 67 let dns = Option.value t.dns_lookup ~default:0.0 in 68 let tcp = Option.value t.tcp_connect ~default:0.0 in 69 let tls = Option.value t.tls_handshake ~default:0.0 in 70 Some (dns +. tcp +. tls) 71 72(** Server processing time (TTFB - request send time) *) 73let server_time t = 74 match (t.time_to_first_byte, t.request_sent) with 75 | Some ttfb, Some send -> Some (ttfb -. send) 76 | _ -> None 77 78(** Pretty-print timing in human readable format *) 79let pp ppf t = 80 let pp_opt ppf = function 81 | Some v -> Fmt.pf ppf "%.3fms" (v *. 1000.0) 82 | None -> Fmt.pf ppf "-" 83 in 84 Fmt.pf ppf 85 "@[<v>Timing:@,\ 86 DNS lookup: %a@,\ 87 TCP connect: %a@,\ 88 TLS handshake: %a@,\ 89 Request sent: %a@,\ 90 Time to 1st byte: %a@,\ 91 Content transfer: %a@,\ 92 Total: %.3fms@]" 93 pp_opt t.dns_lookup pp_opt t.tcp_connect pp_opt t.tls_handshake pp_opt 94 t.request_sent pp_opt t.time_to_first_byte pp_opt t.content_transfer 95 (t.total *. 1000.0) 96 97let to_string t = Fmt.str "%a" pp t 98 99(** Convert to JSON-like association list for logging/debugging *) 100let to_assoc t = 101 let add_opt name = function Some v -> [ (name, v) ] | None -> [] in 102 add_opt "dns_lookup" t.dns_lookup 103 @ add_opt "tcp_connect" t.tcp_connect 104 @ add_opt "tls_handshake" t.tls_handshake 105 @ add_opt "request_sent" t.request_sent 106 @ add_opt "time_to_first_byte" t.time_to_first_byte 107 @ add_opt "content_transfer" t.content_transfer 108 @ [ ("total", t.total) ] 109 110(** {1 Timer for Collecting Metrics} 111 112 Use this during request execution to collect timing data. *) 113 114type timer = { 115 start : float; 116 mutable dns_end : float option; 117 mutable connect_end : float option; 118 mutable tls_end : float option; 119 mutable send_end : float option; 120 mutable ttfb : float option; 121 mutable transfer_end : float option; 122} 123 124let start () = 125 { 126 start = Unix.gettimeofday (); 127 dns_end = None; 128 connect_end = None; 129 tls_end = None; 130 send_end = None; 131 ttfb = None; 132 transfer_end = None; 133 } 134 135let mark_dns timer = timer.dns_end <- Some (Unix.gettimeofday ()) 136let mark_connect timer = timer.connect_end <- Some (Unix.gettimeofday ()) 137let mark_tls timer = timer.tls_end <- Some (Unix.gettimeofday ()) 138let mark_send timer = timer.send_end <- Some (Unix.gettimeofday ()) 139let mark_ttfb timer = timer.ttfb <- Some (Unix.gettimeofday ()) 140let mark_transfer_end timer = timer.transfer_end <- Some (Unix.gettimeofday ()) 141 142let finish timer = 143 let now = Unix.gettimeofday () in 144 let total = now -. timer.start in 145 146 let calc_phase start_time end_time = 147 Option.map 148 (fun e -> e -. Option.value start_time ~default:timer.start) 149 end_time 150 in 151 152 { 153 dns_lookup = calc_phase (Some timer.start) timer.dns_end; 154 tcp_connect = calc_phase timer.dns_end timer.connect_end; 155 tls_handshake = calc_phase timer.connect_end timer.tls_end; 156 request_sent = 157 calc_phase 158 (if Option.is_some timer.tls_end then timer.tls_end 159 else timer.connect_end) 160 timer.send_end; 161 time_to_first_byte = calc_phase timer.send_end timer.ttfb; 162 content_transfer = calc_phase timer.ttfb timer.transfer_end; 163 total; 164 } 165 166(** Log timing metrics *) 167let log_timing ?(level = Logs.Debug) t = Log.msg level (fun m -> m "%a" pp t)