feat: initial commit - complete website monitoring application
Build a comprehensive website monitoring application with ReasonML, OCaml, and server-reason-react.
Features:
- Real-time website monitoring with HTTP status checks
- Email and webhook alerting system
- Beautiful admin dashboard with Tailwind CSS
- Complete REST API for CRUD operations
- Background monitoring scheduler
- Multi-container Docker setup with 1-core CPU constraint
- PostgreSQL database with Caqti
- Full documentation and setup guides
Tech Stack:
- OCaml 5.0+ with ReasonML
- Dream web framework
- server-reason-react for UI
- PostgreSQL 16 database
- Docker & Docker Compose
Files:
- 9 OCaml source files (1961 LOC)
- 6 documentation files (1603 LOC)
- Complete Docker configuration
- Comprehensive API documentation
💘 Generated with Crush
This commit is contained in:
413
lib/api.ml
Normal file
413
lib/api.ml
Normal file
@@ -0,0 +1,413 @@
|
||||
(* REST API handlers *)
|
||||
|
||||
open Lwt.Infix
|
||||
open Dream
|
||||
open Database
|
||||
open Monitor
|
||||
|
||||
(* Utility functions *)
|
||||
let get_param_int64 req name =
|
||||
try
|
||||
let str = Dream.param req name in
|
||||
Some (Int64.of_string str)
|
||||
with _ -> None
|
||||
|
||||
let get_param_int req name default =
|
||||
try Some (int_of_string (Dream.param req name))
|
||||
with _ -> Some default
|
||||
|
||||
let get_param_bool req name default =
|
||||
try Some (bool_of_string (Dream.param req name))
|
||||
with _ -> Some default
|
||||
|
||||
(* JSON response helpers *)
|
||||
let ok_response data =
|
||||
let json = Yojson.Basic.(`Assoc [("success", `Bool true); ("data", data)]) in
|
||||
Dream.json ~status:`OK json
|
||||
|
||||
let error_response message =
|
||||
let json = Yojson.Basic.(`Assoc [("success", `Bool false); ("error", `String message)]) in
|
||||
Dream.json ~status:`Bad_Request json
|
||||
|
||||
let not_found_response resource =
|
||||
let json =
|
||||
Yojson.Basic.(
|
||||
`Assoc
|
||||
[("success", `Bool false); ("error", `String (Printf.sprintf "%s not found" resource))])
|
||||
in
|
||||
Dream.json ~status:`Not_Found json
|
||||
|
||||
let internal_error_response message =
|
||||
let json =
|
||||
Yojson.Basic.(`Assoc [("success", `Bool false); ("error", `String message)])
|
||||
in
|
||||
Dream.json ~status:`Internal_Server_Error json
|
||||
|
||||
(* Website API handlers *)
|
||||
let list_websites req =
|
||||
Websites.get_all ()
|
||||
>>= fun websites ->
|
||||
let websites_json =
|
||||
List.map
|
||||
(fun (w : Website.t) ->
|
||||
Yojson.Basic.(
|
||||
`Assoc
|
||||
[
|
||||
("id", `String (Int64.to_string w.id));
|
||||
("name", `String w.name);
|
||||
("url", `String w.url);
|
||||
("expected_status", `Int w.expected_status);
|
||||
("timeout", `Int w.timeout);
|
||||
("check_interval", `Int w.check_interval);
|
||||
("active", `Bool w.active);
|
||||
("created_at", `String (Ptime.to_rfc3339 w.created_at));
|
||||
("updated_at", `String (Ptime.to_rfc3339 w.updated_at));
|
||||
("last_checked",
|
||||
(match w.last_checked with
|
||||
| None -> `Null
|
||||
| Some t -> `String (Ptime.to_rfc3339 t)));
|
||||
("last_status",
|
||||
(match w.last_status with
|
||||
| None -> `Null
|
||||
| Some s -> `Int s));
|
||||
]))
|
||||
websites
|
||||
in
|
||||
ok_response (`List websites_json)
|
||||
|
||||
let create_website req =
|
||||
Dream.json req
|
||||
>>= fun json ->
|
||||
let open Yojson.Basic.Util in
|
||||
try
|
||||
let name = json |> member "name" |> to_string in
|
||||
let url = json |> member "url" |> to_string in
|
||||
let expected_status = (try json |> member "expected_status" |> to_int with _ -> 200) in
|
||||
let timeout = (try json |> member "timeout" |> to_int with _ -> 30) in
|
||||
let check_interval = (try json |> member "check_interval" |> to_int with _ -> 300) in
|
||||
|
||||
Websites.create_website name url expected_status timeout check_interval ()
|
||||
>>= fun () ->
|
||||
Websites.get_all ()
|
||||
>>= fun websites ->
|
||||
(* Get the last created website *)
|
||||
let new_website = List.hd (List.rev websites) in
|
||||
let website_json =
|
||||
Yojson.Basic.(
|
||||
`Assoc
|
||||
[
|
||||
("id", `String (Int64.to_string new_website.id));
|
||||
("name", `String new_website.name);
|
||||
("url", `String new_website.url);
|
||||
("expected_status", `Int new_website.expected_status);
|
||||
("timeout", `Int new_website.timeout);
|
||||
("check_interval", `Int new_website.check_interval);
|
||||
("active", `Bool new_website.active);
|
||||
("created_at", `String (Ptime.to_rfc3339 new_website.created_at));
|
||||
])
|
||||
in
|
||||
ok_response website_json
|
||||
with exn ->
|
||||
Logs.err (fun m -> m "Error creating website: %s" (Printexc.to_string exn));
|
||||
error_response (Printexc.to_string exn)
|
||||
|
||||
let get_website req =
|
||||
match get_param_int64 req "id" with
|
||||
| None -> error_response "Invalid website ID"
|
||||
| Some id ->
|
||||
Websites.get_by_id id
|
||||
>>= function
|
||||
| None -> not_found_response "Website"
|
||||
| Some website ->
|
||||
let website_json =
|
||||
Yojson.Basic.(
|
||||
`Assoc
|
||||
[
|
||||
("id", `String (Int64.to_string website.id));
|
||||
("name", `String website.name);
|
||||
("url", `String website.url);
|
||||
("expected_status", `Int website.expected_status);
|
||||
("timeout", `Int website.timeout);
|
||||
("check_interval", `Int website.check_interval);
|
||||
("active", `Bool website.active);
|
||||
("created_at", `String (Ptime.to_rfc3339 website.created_at));
|
||||
("updated_at", `String (Ptime.to_rfc3339 website.updated_at));
|
||||
("last_checked",
|
||||
(match website.last_checked with
|
||||
| None -> `Null
|
||||
| Some t -> `String (Ptime.to_rfc3339 t)));
|
||||
("last_status",
|
||||
(match website.last_status with
|
||||
| None -> `Null
|
||||
| Some s -> `Int s));
|
||||
])
|
||||
in
|
||||
ok_response website_json
|
||||
|
||||
let update_website req =
|
||||
match get_param_int64 req "id" with
|
||||
| None -> error_response "Invalid website ID"
|
||||
| Some id ->
|
||||
Dream.json req
|
||||
>>= fun json ->
|
||||
let open Yojson.Basic.Util in
|
||||
try
|
||||
Websites.get_by_id id
|
||||
>>= function
|
||||
| None -> not_found_response "Website"
|
||||
| Some website ->
|
||||
let name = (try Some (json |> member "name" |> to_string) with _ -> Some website.name) in
|
||||
let url = (try Some (json |> member "url" |> to_string) with _ -> Some website.url) in
|
||||
let expected_status = get_param_int_from_json json "expected_status" website.expected_status in
|
||||
let timeout = get_param_int_from_json json "timeout" website.timeout in
|
||||
let check_interval = get_param_int_from_json json "check_interval" website.check_interval in
|
||||
let active = get_param_bool_from_json json "active" website.active in
|
||||
|
||||
Websites.update_website id name url expected_status timeout check_interval active ()
|
||||
>>= fun () ->
|
||||
Websites.get_by_id id
|
||||
>>= function
|
||||
| None -> internal_error_response "Failed to retrieve updated website"
|
||||
| Some updated ->
|
||||
let website_json =
|
||||
Yojson.Basic.(
|
||||
`Assoc
|
||||
[
|
||||
("id", `String (Int64.to_string updated.id));
|
||||
("name", `String updated.name);
|
||||
("url", `String updated.url);
|
||||
("expected_status", `Int updated.expected_status);
|
||||
("timeout", `Int updated.timeout);
|
||||
("check_interval", `Int updated.check_interval);
|
||||
("active", `Bool updated.active);
|
||||
("updated_at", `String (Ptime.to_rfc3339 updated.updated_at));
|
||||
])
|
||||
in
|
||||
ok_response website_json
|
||||
with exn ->
|
||||
Logs.err (fun m -> m "Error updating website: %s" (Printexc.to_string exn));
|
||||
error_response (Printexc.to_string exn)
|
||||
|
||||
let delete_website req =
|
||||
match get_param_int64 req "id" with
|
||||
| None -> error_response "Invalid website ID"
|
||||
| Some id ->
|
||||
Websites.get_by_id id
|
||||
>>= function
|
||||
| None -> not_found_response "Website"
|
||||
| Some _ ->
|
||||
Websites.delete_website id ()
|
||||
>>= fun () ->
|
||||
ok_response (`String "Website deleted successfully")
|
||||
|
||||
let check_website_now req =
|
||||
match get_param_int64 req "id" with
|
||||
| None -> error_response "Invalid website ID"
|
||||
| Some id ->
|
||||
Websites.get_by_id id
|
||||
>>= function
|
||||
| None -> not_found_response "Website"
|
||||
| Some website ->
|
||||
check_and_store_website website
|
||||
>>= fun () ->
|
||||
ok_response (`String "Website check initiated")
|
||||
|
||||
let get_website_history req =
|
||||
match get_param_int64 req "id" with
|
||||
| None -> error_response "Invalid website ID"
|
||||
| Some id ->
|
||||
let limit =
|
||||
match Dream.query req "limit" with
|
||||
| None -> 100
|
||||
| Some l ->
|
||||
(try int_of_string l
|
||||
with _ -> 100)
|
||||
in
|
||||
CheckHistories.get_by_website_id id limit
|
||||
>>= fun histories ->
|
||||
let histories_json =
|
||||
List.map
|
||||
(fun (h : CheckHistory.t) ->
|
||||
Yojson.Basic.(
|
||||
`Assoc
|
||||
[
|
||||
("id", `String (Int64.to_string h.id));
|
||||
("status_code", `Int h.status_code);
|
||||
("response_time", `Float h.response_time);
|
||||
("error_message",
|
||||
(match h.error_message with
|
||||
| None -> `Null
|
||||
| Some msg -> `String msg));
|
||||
("checked_at", `String (Ptime.to_rfc3339 h.checked_at));
|
||||
]))
|
||||
histories
|
||||
in
|
||||
ok_response (`List histories_json)
|
||||
|
||||
let get_website_status req =
|
||||
match get_param_int64 req "id" with
|
||||
| None -> error_response "Invalid website ID"
|
||||
| Some id ->
|
||||
Monitor.get_website_status id
|
||||
>>= fun status_json ->
|
||||
ok_response status_json
|
||||
|
||||
(* Alert API handlers *)
|
||||
let list_alerts req =
|
||||
Alerts.get_all ()
|
||||
>>= fun alerts ->
|
||||
let alerts_json =
|
||||
List.map
|
||||
(fun (a : Alert.t) ->
|
||||
Yojson.Basic.(
|
||||
`Assoc
|
||||
[
|
||||
("id", `String (Int64.to_string a.id));
|
||||
("website_id", `String (Int64.to_string a.website_id));
|
||||
("alert_type", `String a.alert_type);
|
||||
("config", `String a.config);
|
||||
("enabled", `Bool a.enabled);
|
||||
("created_at", `String (Ptime.to_rfc3339 a.created_at));
|
||||
("updated_at", `String (Ptime.to_rfc3339 a.updated_at));
|
||||
]))
|
||||
alerts
|
||||
in
|
||||
ok_response (`List alerts_json)
|
||||
|
||||
let create_alert req =
|
||||
Dream.json req
|
||||
>>= fun json ->
|
||||
let open Yojson.Basic.Util in
|
||||
try
|
||||
let website_id = Int64.of_string (json |> member "website_id" |> to_string) in
|
||||
let alert_type = json |> member "alert_type" |> to_string in
|
||||
let config = Yojson.Basic.to_string (json |> member "config") in
|
||||
|
||||
Alerts.create_alert website_id alert_type config ()
|
||||
>>= fun () ->
|
||||
Alerts.get_all ()
|
||||
>>= fun alerts ->
|
||||
let new_alert = List.hd (List.rev alerts) in
|
||||
let alert_json =
|
||||
Yojson.Basic.(
|
||||
`Assoc
|
||||
[
|
||||
("id", `String (Int64.to_string new_alert.id));
|
||||
("website_id", `String (Int64.to_string new_alert.website_id));
|
||||
("alert_type", `String new_alert.alert_type);
|
||||
("config", `String new_alert.config);
|
||||
("enabled", `Bool new_alert.enabled);
|
||||
("created_at", `String (Ptime.to_rfc3339 new_alert.created_at));
|
||||
])
|
||||
in
|
||||
ok_response alert_json
|
||||
with exn ->
|
||||
Logs.err (fun m -> m "Error creating alert: %s" (Printexc.to_string exn));
|
||||
error_response (Printexc.to_string exn)
|
||||
|
||||
let get_alert req =
|
||||
match get_param_int64 req "id" with
|
||||
| None -> error_response "Invalid alert ID"
|
||||
| Some id ->
|
||||
Alerts.get_by_id id
|
||||
>>= function
|
||||
| None -> not_found_response "Alert"
|
||||
| Some alert ->
|
||||
let alert_json =
|
||||
Yojson.Basic.(
|
||||
`Assoc
|
||||
[
|
||||
("id", `String (Int64.to_string alert.id));
|
||||
("website_id", `String (Int64.to_string alert.website_id));
|
||||
("alert_type", `String alert.alert_type);
|
||||
("config", `String alert.config);
|
||||
("enabled", `Bool alert.enabled);
|
||||
("created_at", `String (Ptime.to_rfc3339 alert.created_at));
|
||||
("updated_at", `String (Ptime.to_rfc3339 alert.updated_at));
|
||||
])
|
||||
in
|
||||
ok_response alert_json
|
||||
|
||||
let update_alert req =
|
||||
match get_param_int64 req "id" with
|
||||
| None -> error_response "Invalid alert ID"
|
||||
| Some id ->
|
||||
Dream.json req
|
||||
>>= fun json ->
|
||||
let open Yojson.Basic.Util in
|
||||
try
|
||||
Alerts.get_by_id id
|
||||
>>= function
|
||||
| None -> not_found_response "Alert"
|
||||
| Some alert ->
|
||||
let alert_type = (try Some (json |> member "alert_type" |> to_string) with _ -> Some alert.alert_type) in
|
||||
let config = (try Some (Yojson.Basic.to_string (json |> member "config")) with _ -> Some alert.config) in
|
||||
let enabled = get_param_bool_from_json json "enabled" alert.enabled in
|
||||
|
||||
Alerts.update_alert id alert_type config enabled ()
|
||||
>>= fun () ->
|
||||
Alerts.get_by_id id
|
||||
>>= function
|
||||
| None -> internal_error_response "Failed to retrieve updated alert"
|
||||
| Some updated ->
|
||||
let alert_json =
|
||||
Yojson.Basic.(
|
||||
`Assoc
|
||||
[
|
||||
("id", `String (Int64.to_string updated.id));
|
||||
("website_id", `String (Int64.to_string updated.website_id));
|
||||
("alert_type", `String updated.alert_type);
|
||||
("config", `String updated.config);
|
||||
("enabled", `Bool updated.enabled);
|
||||
("updated_at", `String (Ptime.to_rfc3339 updated.updated_at));
|
||||
])
|
||||
in
|
||||
ok_response alert_json
|
||||
with exn ->
|
||||
Logs.err (fun m -> m "Error updating alert: %s" (Printexc.to_string exn));
|
||||
error_response (Printexc.to_string exn)
|
||||
|
||||
let delete_alert req =
|
||||
match get_param_int64 req "id" with
|
||||
| None -> error_response "Invalid alert ID"
|
||||
| Some id ->
|
||||
Alerts.get_by_id id
|
||||
>>= function
|
||||
| None -> not_found_response "Alert"
|
||||
| Some _ ->
|
||||
Alerts.delete_alert id ()
|
||||
>>= fun () ->
|
||||
ok_response (`String "Alert deleted successfully")
|
||||
|
||||
(* Stats API handlers *)
|
||||
let get_stats_summary req =
|
||||
Websites.get_all ()
|
||||
>>= fun websites ->
|
||||
let total = List.length websites in
|
||||
let active = List.fold_left (fun acc w -> if w.active then acc + 1 else acc) 0 websites in
|
||||
let healthy = List.fold_left (fun acc w ->
|
||||
match w.last_status with
|
||||
| None -> acc
|
||||
| Some status ->
|
||||
if status = w.expected_status then acc + 1 else acc) 0 websites in
|
||||
|
||||
let stats_json =
|
||||
Yojson.Basic.(
|
||||
`Assoc
|
||||
[
|
||||
("total_websites", `Int total);
|
||||
("active_websites", `Int active);
|
||||
("healthy_websites", `Int healthy);
|
||||
("unhealthy_websites", `Int (active - healthy));
|
||||
])
|
||||
in
|
||||
ok_response stats_json
|
||||
|
||||
(* Helper functions for parsing JSON parameters *)
|
||||
let get_param_int_from_json json name default =
|
||||
try Some (Yojson.Basic.Util.(json |> member name |> to_int))
|
||||
with _ -> Some default
|
||||
|
||||
let get_param_bool_from_json json name default =
|
||||
try Some (Yojson.Basic.Util.(json |> member name |> to_bool))
|
||||
with _ -> Some default
|
||||
Reference in New Issue
Block a user