an atproto pds written in F# (.NET 9) 馃
pds
fsharp
giraffe
dotnet
atproto
1namespace PDSharp.Core
2
3open System
4open System.IO
5open System.Runtime.InteropServices
6
7/// Health monitoring module for guardrails and uptime checks
8module Health =
9
10 /// Health status response
11 type HealthStatus = {
12 /// Version of the PDS
13 Version : string
14 /// Uptime in seconds
15 UptimeSeconds : int64
16 /// Server start time in ISO8601
17 StartTime : string
18 /// Database status
19 DatabaseStatus : DatabaseStatus
20 /// Disk usage information
21 DiskUsage : DiskUsage option
22 /// Backup status
23 BackupStatus : BackupStatus option
24 }
25
26 /// Database connectivity status
27 and DatabaseStatus = {
28 /// Whether the database is reachable
29 IsHealthy : bool
30 /// Optional error message
31 Message : string option
32 }
33
34 /// Disk usage metrics
35 and DiskUsage = {
36 /// Total disk space in bytes
37 TotalBytes : int64
38 /// Free disk space in bytes
39 FreeBytes : int64
40 /// Used disk space in bytes
41 UsedBytes : int64
42 /// Percentage of disk used
43 UsedPercent : float
44 /// Whether disk pressure is critical (>90%)
45 IsCritical : bool
46 }
47
48 /// Backup status tracking
49 and BackupStatus = {
50 /// Timestamp of last successful backup
51 LastBackupTime : DateTimeOffset option
52 /// Age of last backup in hours
53 BackupAgeHours : float option
54 /// Whether backup is too old (>24 hours)
55 IsStale : bool
56 }
57
58 /// Get disk usage for a given path
59 let getDiskUsage (path : string) : DiskUsage option =
60 try
61 let driveInfo =
62 if RuntimeInformation.IsOSPlatform OSPlatform.Windows then
63 let driveLetter = Path.GetPathRoot path
64 DriveInfo driveLetter
65 else
66 DriveInfo(if Directory.Exists path then path else "/")
67
68 if driveInfo.IsReady then
69 let total = driveInfo.TotalSize
70 let free = driveInfo.TotalFreeSpace
71 let used = total - free
72 let usedPercent = float used / float total * 100.0
73
74 Some {
75 TotalBytes = total
76 FreeBytes = free
77 UsedBytes = used
78 UsedPercent = Math.Round(usedPercent, 2)
79 IsCritical = usedPercent >= 90.0
80 }
81 else
82 None
83 with _ ->
84 None
85
86
87
88 /// Check if a SQLite database file is accessible
89 let checkDatabaseHealth (connectionString : string) : DatabaseStatus =
90 try
91 let dbPath =
92 connectionString.Split ';'
93 |> Array.tryFind (fun s -> s.Trim().StartsWith("Data Source=", StringComparison.OrdinalIgnoreCase))
94 |> Option.map (fun s -> s.Split('=').[1].Trim())
95
96 match dbPath with
97 | Some path when File.Exists path -> { IsHealthy = true; Message = None }
98 | Some path -> {
99 IsHealthy = false
100 Message = Some $"Database file not found: {path}"
101 }
102 | None -> {
103 IsHealthy = false
104 Message = Some "Could not parse connection string"
105 }
106 with ex -> { IsHealthy = false; Message = Some ex.Message }
107
108 /// Calculate backup status from last backup time
109 let getBackupStatus (lastBackupTime : DateTimeOffset option) : BackupStatus =
110 match lastBackupTime with
111 | Some time ->
112 let age = DateTimeOffset.UtcNow - time
113 let ageHours = age.TotalHours
114
115 {
116 LastBackupTime = Some time
117 BackupAgeHours = Some(Math.Round(ageHours, 2))
118 IsStale = ageHours > 24.0
119 }
120 | None -> {
121 LastBackupTime = None
122 BackupAgeHours = None
123 IsStale = true
124 }
125
126 /// Mutable state for tracking server state
127 type HealthState() =
128 let mutable startTime = DateTimeOffset.UtcNow
129 let mutable lastBackupTime : DateTimeOffset option = None
130
131 member _.StartTime = startTime
132 member _.SetStartTime(time : DateTimeOffset) = startTime <- time
133 member _.LastBackupTime = lastBackupTime
134
135 member _.RecordBackup() =
136 lastBackupTime <- Some DateTimeOffset.UtcNow
137
138 member _.RecordBackup(time : DateTimeOffset) = lastBackupTime <- Some time
139
140 member _.GetUptime() : int64 =
141 int64 (DateTimeOffset.UtcNow - startTime).TotalSeconds
142
143 /// Build a complete health status
144 let buildHealthStatus
145 (version : string)
146 (healthState : HealthState)
147 (connectionString : string)
148 (dataPath : string)
149 : HealthStatus =
150 {
151 Version = version
152 UptimeSeconds = healthState.GetUptime()
153 StartTime = healthState.StartTime.ToString("o")
154 DatabaseStatus = checkDatabaseHealth connectionString
155 DiskUsage = getDiskUsage dataPath
156 BackupStatus = Some(getBackupStatus healthState.LastBackupTime)
157 }