ocaml http/1, http/2 and websocket client and server library
1(** Logging module for HCS HTTP library.
2
3 Provides structured logging for HTTP events including requests, responses,
4 connections, and errors. The module is runtime-agnostic and uses a
5 callback-based approach for flexibility.
6
7 {1 Usage}
8
9 {[
10 (* Use built-in stderr logger *)
11 let logger = Hcs.Log.stderr ()
12
13 (* Use null logger (no output) *)
14 let logger = Hcs.Log.null
15
16 (* Use custom logger *)
17 let logger =
18 Hcs.Log.custom (fun level msg ->
19 match level with
20 | Hcs.Log.Error -> Printf.eprintf "[ERROR] %s\n%!" msg
21 | _ ->
22 Printf.printf "[%s] %s\n%!" (Hcs.Log.level_to_string level) msg)
23 ]} *)
24
25(** {1 Types} *)
26
27(** Log levels *)
28type level =
29 | Debug (** Detailed debugging information *)
30 | Info (** General information about operations *)
31 | Warn (** Warning conditions *)
32 | Error (** Error conditions *)
33
34(** HTTP method for logging (simplified) *)
35type http_method =
36 | GET
37 | POST
38 | PUT
39 | DELETE
40 | PATCH
41 | HEAD
42 | OPTIONS
43 | CONNECT
44 | TRACE
45 | Other of string
46
47(** Log events - structured events that can be logged *)
48type event =
49 | Request_start of {
50 id : string;
51 meth : http_method;
52 uri : string;
53 headers : (string * string) list;
54 } (** Request started *)
55 | Request_end of {
56 id : string;
57 status : int;
58 duration_ms : float;
59 body_size : int option;
60 } (** Request completed *)
61 | Connection_open of {
62 host : string;
63 port : int;
64 protocol : string; (** "http/1.1" or "h2" *)
65 } (** Connection opened *)
66 | Connection_close of { host : string; port : int; reason : string }
67 (** Connection closed *)
68 | Connection_reuse of { host : string; port : int }
69 (** Connection reused from pool *)
70 | Tls_handshake of {
71 host : string;
72 protocol : string; (** TLS version *)
73 cipher : string option;
74 } (** TLS handshake completed *)
75 | Retry of {
76 id : string;
77 attempt : int;
78 reason : string;
79 delay_ms : float option;
80 } (** Request retry *)
81 | Redirect of {
82 id : string;
83 from_uri : string;
84 to_uri : string;
85 status : int;
86 } (** Following redirect *)
87 | Error of { id : string option; error : string; context : string option }
88 (** Error occurred *)
89 | Custom of { name : string; data : (string * string) list }
90 (** Custom event *)
91
92type logger = level -> event -> unit
93(** Logger function type *)
94
95(** {1 Level operations} *)
96
97(** Convert level to string *)
98let level_to_string = function
99 | Debug -> "DEBUG"
100 | Info -> "INFO"
101 | Warn -> "WARN"
102 | Error -> "ERROR"
103
104(** Parse level from string *)
105let level_of_string = function
106 | "DEBUG" | "debug" -> Some Debug
107 | "INFO" | "info" -> Some Info
108 | "WARN" | "warn" | "WARNING" | "warning" -> Some Warn
109 | "ERROR" | "error" -> Some Error
110 | _ -> None
111
112(** Compare log levels (for filtering) *)
113let level_to_int = function Debug -> 0 | Info -> 1 | Warn -> 2 | Error -> 3
114
115let level_gte l1 l2 = level_to_int l1 >= level_to_int l2
116
117(** {1 HTTP method operations} *)
118
119let method_to_string = function
120 | GET -> "GET"
121 | POST -> "POST"
122 | PUT -> "PUT"
123 | DELETE -> "DELETE"
124 | PATCH -> "PATCH"
125 | HEAD -> "HEAD"
126 | OPTIONS -> "OPTIONS"
127 | CONNECT -> "CONNECT"
128 | TRACE -> "TRACE"
129 | Other s -> s
130
131let method_of_h1 (m : Httpun_types.Method.t) : http_method =
132 match m with
133 | `GET -> GET
134 | `POST -> POST
135 | `PUT -> PUT
136 | `DELETE -> DELETE
137 | `HEAD -> HEAD
138 | `OPTIONS -> OPTIONS
139 | `CONNECT -> CONNECT
140 | `TRACE -> TRACE
141 | `Other s -> Other s
142
143(** {1 Event formatting} *)
144
145(** Format event as a human-readable string *)
146let event_to_string = function
147 | Request_start { id; meth; uri; headers = _ } ->
148 Buf.build64 (fun b ->
149 Buf.string b "Request[";
150 Buf.string b id;
151 Buf.string b "] ";
152 Buf.string b (method_to_string meth);
153 Buf.char b ' ';
154 Buf.string b uri)
155 | Request_end { id; status; duration_ms; body_size } ->
156 Buf.build64 (fun b ->
157 Buf.string b "Request[";
158 Buf.string b id;
159 Buf.string b "] completed: status=";
160 Buf.int b status;
161 Buf.string b ", duration=";
162 Buf.float2 b duration_ms;
163 Buf.string b "ms";
164 match body_size with
165 | Some s ->
166 Buf.string b ", ";
167 Buf.int b s;
168 Buf.string b " bytes"
169 | None -> ())
170 | Connection_open { host; port; protocol } ->
171 Buf.build64 (fun b ->
172 Buf.string b "Connection opened: ";
173 Buf.string b host;
174 Buf.char b ':';
175 Buf.int b port;
176 Buf.string b " (";
177 Buf.string b protocol;
178 Buf.char b ')')
179 | Connection_close { host; port; reason } ->
180 Buf.build64 (fun b ->
181 Buf.string b "Connection closed: ";
182 Buf.string b host;
183 Buf.char b ':';
184 Buf.int b port;
185 Buf.string b " (";
186 Buf.string b reason;
187 Buf.char b ')')
188 | Connection_reuse { host; port } ->
189 Buf.build64 (fun b ->
190 Buf.string b "Connection reused: ";
191 Buf.string b host;
192 Buf.char b ':';
193 Buf.int b port)
194 | Tls_handshake { host; protocol; cipher } ->
195 Buf.build64 (fun b ->
196 Buf.string b "TLS handshake: ";
197 Buf.string b host;
198 Buf.string b " (";
199 Buf.string b protocol;
200 (match cipher with
201 | Some c ->
202 Buf.string b ", cipher=";
203 Buf.string b c
204 | None -> ());
205 Buf.char b ')')
206 | Retry { id; attempt; reason; delay_ms } ->
207 Buf.build64 (fun b ->
208 Buf.string b "Request[";
209 Buf.string b id;
210 Buf.string b "] retry #";
211 Buf.int b attempt;
212 Buf.string b ": ";
213 Buf.string b reason;
214 match delay_ms with
215 | Some d ->
216 Buf.string b ", delay=";
217 Buf.float0 b d;
218 Buf.string b "ms"
219 | None -> ())
220 | Redirect { id; from_uri; to_uri; status } ->
221 Buf.build64 (fun b ->
222 Buf.string b "Request[";
223 Buf.string b id;
224 Buf.string b "] redirect ";
225 Buf.int b status;
226 Buf.string b ": ";
227 Buf.string b from_uri;
228 Buf.string b " -> ";
229 Buf.string b to_uri)
230 | Error { id; error; context } ->
231 Buf.build64 (fun b ->
232 Buf.string b "Error";
233 (match id with
234 | Some i ->
235 Buf.char b '[';
236 Buf.string b i;
237 Buf.string b "] "
238 | None -> ());
239 Buf.string b ": ";
240 Buf.string b error;
241 match context with
242 | Some c ->
243 Buf.string b " (";
244 Buf.string b c;
245 Buf.char b ')'
246 | None -> ())
247 | Custom { name; data } ->
248 Buf.build64 (fun b ->
249 Buf.string b "Custom[";
250 Buf.string b name;
251 Buf.string b "]: ";
252 let first = ref true in
253 List.iter
254 (fun (k, v) ->
255 if !first then first := false else Buf.string b ", ";
256 Buf.string b k;
257 Buf.char b '=';
258 Buf.string b v)
259 data)
260
261(** Format event as JSON string *)
262let event_to_json = function
263 | Request_start { id; meth; uri; headers } ->
264 Buf.build64 (fun b ->
265 Buf.string b {|{"event":"request_start","id":|};
266 Buf.json_string_quoted b id;
267 Buf.string b {|,"method":|};
268 Buf.json_string_quoted b (method_to_string meth);
269 Buf.string b {|,"uri":|};
270 Buf.json_string_quoted b uri;
271 Buf.string b {|,"headers":{|};
272 let first = ref true in
273 List.iter
274 (fun (k, v) ->
275 if !first then first := false else Buf.char b ',';
276 Buf.json_string_quoted b k;
277 Buf.char b ':';
278 Buf.json_string_quoted b v)
279 headers;
280 Buf.string b "}}")
281 | Request_end { id; status; duration_ms; body_size } ->
282 Buf.build64 (fun b ->
283 Buf.string b {|{"event":"request_end","id":|};
284 Buf.json_string_quoted b id;
285 Buf.string b {|,"status":|};
286 Buf.int b status;
287 Buf.string b {|,"duration_ms":|};
288 Buf.float2 b duration_ms;
289 (match body_size with
290 | Some s ->
291 Buf.string b {|,"body_size":|};
292 Buf.int b s
293 | None -> ());
294 Buf.char b '}')
295 | Connection_open { host; port; protocol } ->
296 Buf.build64 (fun b ->
297 Buf.string b {|{"event":"connection_open","host":|};
298 Buf.json_string_quoted b host;
299 Buf.string b {|,"port":|};
300 Buf.int b port;
301 Buf.string b {|,"protocol":|};
302 Buf.json_string_quoted b protocol;
303 Buf.char b '}')
304 | Connection_close { host; port; reason } ->
305 Buf.build64 (fun b ->
306 Buf.string b {|{"event":"connection_close","host":|};
307 Buf.json_string_quoted b host;
308 Buf.string b {|,"port":|};
309 Buf.int b port;
310 Buf.string b {|,"reason":|};
311 Buf.json_string_quoted b reason;
312 Buf.char b '}')
313 | Connection_reuse { host; port } ->
314 Buf.build64 (fun b ->
315 Buf.string b {|{"event":"connection_reuse","host":|};
316 Buf.json_string_quoted b host;
317 Buf.string b {|,"port":|};
318 Buf.int b port;
319 Buf.char b '}')
320 | Tls_handshake { host; protocol; cipher } ->
321 Buf.build64 (fun b ->
322 Buf.string b {|{"event":"tls_handshake","host":|};
323 Buf.json_string_quoted b host;
324 Buf.string b {|,"protocol":|};
325 Buf.json_string_quoted b protocol;
326 (match cipher with
327 | Some c ->
328 Buf.string b {|,"cipher":|};
329 Buf.json_string_quoted b c
330 | None -> ());
331 Buf.char b '}')
332 | Retry { id; attempt; reason; delay_ms } ->
333 Buf.build64 (fun b ->
334 Buf.string b {|{"event":"retry","id":|};
335 Buf.json_string_quoted b id;
336 Buf.string b {|,"attempt":|};
337 Buf.int b attempt;
338 Buf.string b {|,"reason":|};
339 Buf.json_string_quoted b reason;
340 (match delay_ms with
341 | Some d ->
342 Buf.string b {|,"delay_ms":|};
343 Buf.float0 b d
344 | None -> ());
345 Buf.char b '}')
346 | Redirect { id; from_uri; to_uri; status } ->
347 Buf.build64 (fun b ->
348 Buf.string b {|{"event":"redirect","id":|};
349 Buf.json_string_quoted b id;
350 Buf.string b {|,"from":|};
351 Buf.json_string_quoted b from_uri;
352 Buf.string b {|,"to":|};
353 Buf.json_string_quoted b to_uri;
354 Buf.string b {|,"status":|};
355 Buf.int b status;
356 Buf.char b '}')
357 | Error { id; error; context } ->
358 Buf.build64 (fun b ->
359 Buf.string b {|{"event":"error"|};
360 (match id with
361 | Some i ->
362 Buf.string b {|,"id":|};
363 Buf.json_string_quoted b i
364 | None -> ());
365 Buf.string b {|,"error":|};
366 Buf.json_string_quoted b error;
367 (match context with
368 | Some c ->
369 Buf.string b {|,"context":|};
370 Buf.json_string_quoted b c
371 | None -> ());
372 Buf.char b '}')
373 | Custom { name; data } ->
374 Buf.build64 (fun b ->
375 Buf.string b {|{"event":"custom","name":|};
376 Buf.json_string_quoted b name;
377 Buf.string b {|,"data":{|};
378 let first = ref true in
379 List.iter
380 (fun (k, v) ->
381 if !first then first := false else Buf.char b ',';
382 Buf.json_string_quoted b k;
383 Buf.char b ':';
384 Buf.json_string_quoted b v)
385 data;
386 Buf.string b "}}")
387
388(** {1 Built-in Loggers} *)
389
390(** Null logger - discards all events *)
391let null : logger = fun _ _ -> ()
392
393(** Stderr logger with optional minimum level filter *)
394let stderr ?(min_level = Debug) ?(json = false) () : logger =
395 fun level event ->
396 if level_gte level min_level then
397 let formatted =
398 if json then event_to_json event else event_to_string event
399 in
400 Printf.eprintf "[%s] %s\n%!" (level_to_string level) formatted
401
402(** Stdout logger with optional minimum level filter *)
403let stdout ?(min_level = Debug) ?(json = false) () : logger =
404 fun level event ->
405 if level_gte level min_level then
406 let formatted =
407 if json then event_to_json event else event_to_string event
408 in
409 Printf.printf "[%s] %s\n%!" (level_to_string level) formatted
410
411(** Custom logger from a simple callback *)
412let custom (f : level -> string -> unit) : logger =
413 fun level event -> f level (event_to_string event)
414
415(** Custom logger with JSON output *)
416let custom_json (f : level -> string -> unit) : logger =
417 fun level event -> f level (event_to_json event)
418
419(** Combine multiple loggers *)
420let combine (loggers : logger list) : logger =
421 fun level event -> List.iter (fun logger -> logger level event) loggers
422
423(** Filter logger by minimum level *)
424let with_min_level (min_level : level) (logger : logger) : logger =
425 fun level event -> if level_gte level min_level then logger level event
426
427(** {1 Request ID generation} *)
428
429(** Counter for unique request IDs *)
430let request_id_counter = ref 0
431
432(** Generate a unique request ID *)
433let generate_request_id () =
434 incr request_id_counter;
435 let random = Random.int 0xFFFF in
436 let buf = Buffer.create 32 in
437 Buffer.add_string buf "req-";
438 let n = !request_id_counter in
439 let s = string_of_int n in
440 for _ = 1 to 6 - String.length s do
441 Buffer.add_char buf '0'
442 done;
443 Buffer.add_string buf s;
444 Buffer.add_char buf '-';
445 let hex_buf = Buffer.create 8 in
446 Buf.hex hex_buf random;
447 let hx = Buffer.contents hex_buf in
448 for _ = 1 to 4 - String.length hx do
449 Buffer.add_char buf '0'
450 done;
451 Buffer.add_string buf hx;
452 Buffer.contents buf