My agentic slop goes here. Not intended for anyone else!
at main 11 kB view raw
1open Dancer_logs 2open Cmdliner 3 4let style_ok = Fmt.styled `Green 5let style_error = Fmt.styled `Red 6let style_warning = Fmt.styled `Yellow 7let style_info = Fmt.styled `Blue 8let style_debug = Fmt.styled `Cyan 9let style_dim = Fmt.styled `Faint 10let style_bold = Fmt.styled `Bold 11 12let pp_level ppf = function 13 | Logs.App -> Fmt.pf ppf "%a" style_bold "APP" 14 | Logs.Error -> Fmt.pf ppf "%a" style_error "ERR" 15 | Logs.Warning -> Fmt.pf ppf "%a" style_warning "WRN" 16 | Logs.Info -> Fmt.pf ppf "%a" style_info "INF" 17 | Logs.Debug -> Fmt.pf ppf "%a" style_debug "DBG" 18 19let pp_timestamp ppf timestamp = 20 let ptime = Ptime.of_float_s timestamp |> Option.get in 21 Fmt.pf ppf "%a" (Ptime.pp_human ~frac_s:3 ()) ptime 22 23let pp_source ppf source = 24 Fmt.pf ppf "%a" style_bold source 25 26let pp_duration ppf ms = 27 if ms > 1000.0 then 28 Fmt.pf ppf "%a" style_warning (Printf.sprintf "%.1fs" (ms /. 1000.0)) 29 else if ms > 100.0 then 30 Fmt.pf ppf "%a" style_warning (Printf.sprintf "%.0fms" ms) 31 else 32 Fmt.pf ppf "%a" style_ok (Printf.sprintf "%.1fms" ms) 33 34let pp_log_row ppf row = 35 match row with 36 | timestamp :: level :: source :: message :: rest -> 37 let timestamp = match timestamp with 38 | Sqlite3.Data.FLOAT f -> f 39 | _ -> 0.0 40 in 41 let level = match level with 42 | Sqlite3.Data.INT i -> 43 (match Int64.to_int i with 44 | 0 -> Logs.App 45 | 1 -> Logs.Error 46 | 2 -> Logs.Warning 47 | 3 -> Logs.Info 48 | _ -> Logs.Debug) 49 | _ -> Logs.Info 50 in 51 let source = match source with 52 | Sqlite3.Data.TEXT s -> s 53 | _ -> "unknown" 54 in 55 let message = match message with 56 | Sqlite3.Data.TEXT s -> s 57 | _ -> "" 58 in 59 60 Fmt.pf ppf "@[<h>%a %a %a@]@. %s@." 61 pp_timestamp timestamp 62 pp_level level 63 pp_source source 64 message; 65 66 List.iteri (fun i data -> 67 match data with 68 | Sqlite3.Data.TEXT s when String.length s > 0 -> 69 Fmt.pf ppf " %a: %s@." style_dim 70 (match i with 71 | 0 -> "error" 72 | 1 -> "code" 73 | 2 -> "trace" 74 | _ -> string_of_int i) s 75 | Sqlite3.Data.INT i -> 76 Fmt.pf ppf " %a: %Ld@." style_dim (string_of_int i) i 77 | Sqlite3.Data.FLOAT f -> 78 Fmt.pf ppf " %a: %f@." style_dim (string_of_int i) f 79 | _ -> () 80 ) rest 81 | _ -> () 82 83let pp_pattern ppf pattern = 84 match pattern with 85 | [id; hash; pattern_text; first_seen; last_seen; count; severity] -> 86 let count = match count with 87 | Sqlite3.Data.INT i -> Int64.to_int i 88 | _ -> 0 89 in 90 let severity_color = 91 match severity with 92 | Sqlite3.Data.FLOAT f when f > 7.0 -> style_error 93 | Sqlite3.Data.FLOAT f when f > 4.0 -> style_warning 94 | _ -> style_info 95 in 96 97 Fmt.pf ppf "@[<v>%a Pattern (×%d) %a@. %s@. First: %a | Last: %a@]@." 98 severity_color "" 99 count 100 style_dim 101 (match hash with Sqlite3.Data.TEXT s -> String.sub s 0 8 | _ -> "") 102 (match pattern_text with Sqlite3.Data.TEXT s -> s | _ -> "") 103 pp_timestamp (match first_seen with Sqlite3.Data.FLOAT f -> f | _ -> 0.0) 104 pp_timestamp (match last_seen with Sqlite3.Data.FLOAT f -> f | _ -> 0.0) 105 | _ -> () 106 107let pp_error_summary ppf row = 108 match row with 109 | [hash; error_type; error_code; count; sessions; users; last; first; sources; avg_dur; sample] -> 110 let count = match count with Sqlite3.Data.INT i -> Int64.to_int i | _ -> 0 in 111 let sessions = match sessions with Sqlite3.Data.INT i -> Int64.to_int i | _ -> 0 in 112 let error_type = match error_type with Sqlite3.Data.TEXT s -> s | _ -> "unknown" in 113 114 Fmt.pf ppf "@[<v>%a %s (×%d in %d sessions)@. %s@. Sources: %s@]@." 115 style_error "ERROR" 116 error_type count sessions 117 (match sample with Sqlite3.Data.TEXT s -> 118 if String.length s > 80 then String.sub s 0 77 ^ "..." else s 119 | _ -> "") 120 (match sources with Sqlite3.Data.TEXT s -> s | _ -> "") 121 | _ -> () 122 123let list_cmd = 124 let list db_path level source since limit follow = 125 Eio_main.run @@ fun _env -> 126 let db = Dancer_logs.init ~path:db_path () in 127 128 let filter = { 129 Query.default_filter with 130 level; 131 source; 132 since = Option.map (fun h -> Unix.gettimeofday () -. (h *. 3600.0)) since; 133 limit; 134 } in 135 136 let rows = Query.list db filter in 137 138 Fmt.pr "@[<v>%a@]@." 139 (Fmt.list ~sep:Fmt.cut pp_log_row) rows; 140 141 Dancer_logs.close db 142 in 143 144 let db_path = Arg.(value & opt string "dancer_logs.db" & 145 info ["d"; "db"] ~doc:"Database path") in 146 147 let level = Arg.(value & opt (some (enum [ 148 "error", Logs.Error; 149 "warning", Logs.Warning; 150 "info", Logs.Info; 151 "debug", Logs.Debug; 152 ])) None & info ["l"; "level"] ~doc:"Filter by log level") in 153 154 let source = Arg.(value & opt (some string) None & 155 info ["s"; "source"] ~doc:"Filter by source module") in 156 157 let since = Arg.(value & opt (some float) None & 158 info ["since"] ~doc:"Show logs from last N hours") in 159 160 let limit = Arg.(value & opt int 100 & 161 info ["n"; "limit"] ~doc:"Maximum number of results") in 162 163 let follow = Arg.(value & flag & 164 info ["f"; "follow"] ~doc:"Follow mode (like tail -f)") in 165 166 let doc = "List log entries" in 167 let info = Cmd.info "list" ~doc in 168 Cmd.v info Term.(const list $ db_path $ level $ source $ since $ limit $ follow) 169 170let search_cmd = 171 let search db_path query limit = 172 Eio_main.run @@ fun _env -> 173 let db = Dancer_logs.init ~path:db_path () in 174 175 let rows = Query.search db query ~limit () in 176 177 Fmt.pr "@[<v>%a Found %d matches@.@.%a@]@." 178 style_bold "🔍" (List.length rows) 179 (Fmt.list ~sep:(Fmt.any "@.---@.") pp_log_row) rows; 180 181 Dancer_logs.close db 182 in 183 184 let db_path = Arg.(value & opt string "dancer_logs.db" & 185 info ["d"; "db"] ~doc:"Database path") in 186 187 let query = Arg.(required & pos 0 (some string) None & 188 info [] ~docv:"QUERY" ~doc:"Search query") in 189 190 let limit = Arg.(value & opt int 100 & 191 info ["n"; "limit"] ~doc:"Maximum number of results") in 192 193 let doc = "Full-text search in logs" in 194 let info = Cmd.info "search" ~doc in 195 Cmd.v info Term.(const search $ db_path $ query $ limit) 196 197let patterns_cmd = 198 let patterns db_path min_count since = 199 Eio_main.run @@ fun _env -> 200 let db = Dancer_logs.init ~path:db_path () in 201 202 let patterns = Pattern.list db ~min_count ?since () in 203 204 Fmt.pr "@[<v>%a Detected Patterns@.@.%a@]@." 205 style_bold "📊" 206 (Fmt.list ~sep:Fmt.cut (fun ppf p -> 207 pp_pattern ppf [ 208 Sqlite3.Data.INT (Int64.of_int p.Pattern.id); 209 Sqlite3.Data.TEXT p.pattern_hash; 210 Sqlite3.Data.TEXT p.pattern; 211 Sqlite3.Data.FLOAT p.first_seen; 212 Sqlite3.Data.FLOAT p.last_seen; 213 Sqlite3.Data.INT (Int64.of_int p.occurrence_count); 214 match p.severity_score with 215 | Some s -> Sqlite3.Data.FLOAT s 216 | None -> Sqlite3.Data.NULL 217 ] 218 )) patterns; 219 220 Dancer_logs.close db 221 in 222 223 let db_path = Arg.(value & opt string "dancer_logs.db" & 224 info ["d"; "db"] ~doc:"Database path") in 225 226 let min_count = Arg.(value & opt int 5 & 227 info ["c"; "min-count"] ~doc:"Minimum occurrence count") in 228 229 let since = Arg.(value & opt (some float) None & 230 info ["since"] ~doc:"Patterns from last N hours") in 231 232 let doc = "Show detected error patterns" in 233 let info = Cmd.info "patterns" ~doc in 234 Cmd.v info Term.(const patterns $ db_path $ min_count $ since) 235 236let errors_cmd = 237 let errors db_path limit = 238 Eio_main.run @@ fun _env -> 239 let db = Dancer_logs.init ~path:db_path () in 240 241 let errors = Query.recent_errors db ~limit () in 242 243 Fmt.pr "@[<v>%a Recent Errors (last 24h)@.@.%a@]@." 244 style_error "⚠️" 245 (Fmt.list ~sep:(Fmt.any "@.") pp_error_summary) errors; 246 247 Dancer_logs.close db 248 in 249 250 let db_path = Arg.(value & opt string "dancer_logs.db" & 251 info ["d"; "db"] ~doc:"Database path") in 252 253 let limit = Arg.(value & opt int 50 & 254 info ["n"; "limit"] ~doc:"Maximum number of results") in 255 256 let doc = "Show recent errors with aggregation" in 257 let info = Cmd.info "errors" ~doc in 258 Cmd.v info Term.(const errors $ db_path $ limit) 259 260let stats_cmd = 261 let stats db_path = 262 Eio_main.run @@ fun _env -> 263 let db = Dancer_logs.init ~path:db_path () in 264 265 let count_by_level = Query.count_by_level db ~since:(Unix.gettimeofday () -. 86400.0) () in 266 267 Fmt.pr "@[<v>%a Database Statistics@.@." style_bold "📈"; 268 269 List.iter (fun (level, count) -> 270 Fmt.pr " %a: %d@." pp_level level count 271 ) count_by_level; 272 273 let db_stats = Dancer_logs.stats db in 274 List.iter (fun (k, v) -> 275 Fmt.pr " %a: %s@." style_dim k v 276 ) db_stats; 277 278 Fmt.pr "@]@."; 279 280 Dancer_logs.close db 281 in 282 283 let db_path = Arg.(value & opt string "dancer_logs.db" & 284 info ["d"; "db"] ~doc:"Database path") in 285 286 let doc = "Show database statistics" in 287 let info = Cmd.info "stats" ~doc in 288 Cmd.v info Term.(const stats $ db_path) 289 290let export_cmd = 291 let export db_path format output since = 292 Eio_main.run @@ fun _env -> 293 let db = Dancer_logs.init ~path:db_path () in 294 295 let filter = { 296 Query.default_filter with 297 since = Option.map (fun h -> Unix.gettimeofday () -. (h *. 3600.0)) since; 298 limit = 10000; 299 } in 300 301 let rows = Query.list db filter in 302 303 let content = match format with 304 | "json" -> Yojson.Safe.to_string (Export.to_json rows) 305 | "csv" -> Export.to_csv rows 306 | "claude" -> Export.for_claude db () 307 | _ -> failwith "Unknown format" 308 in 309 310 let oc = open_out output in 311 output_string oc content; 312 close_out oc; 313 314 Fmt.pr "%a Exported %d logs to %s@." 315 style_ok "" (List.length rows) output; 316 317 Dancer_logs.close db 318 in 319 320 let db_path = Arg.(value & opt string "dancer_logs.db" & 321 info ["d"; "db"] ~doc:"Database path") in 322 323 let format = Arg.(value & opt (enum [ 324 "json", "json"; 325 "csv", "csv"; 326 "claude", "claude"; 327 ]) "json" & info ["f"; "format"] ~doc:"Export format") in 328 329 let output = Arg.(required & opt (some string) None & 330 info ["o"; "output"] ~doc:"Output file") in 331 332 let since = Arg.(value & opt (some float) None & 333 info ["since"] ~doc:"Export logs from last N hours") in 334 335 let doc = "Export logs to file" in 336 let info = Cmd.info "export" ~doc in 337 Cmd.v info Term.(const export $ db_path $ format $ output $ since) 338 339let main_cmd = 340 let doc = "Dancer Logs CLI - Query and analyze structured logs" in 341 let info = Cmd.info "dancer-logs" ~version:"0.1.0" ~doc in 342 let default = Term.(ret (const (`Help (`Pager, None)))) in 343 Cmd.group info ~default [ 344 list_cmd; 345 search_cmd; 346 patterns_cmd; 347 errors_cmd; 348 stats_cmd; 349 export_cmd; 350 ] 351 352let () = exit (Cmd.eval main_cmd)