Skip to content

Commit 4f3723a

Browse files
committed
refactor: root GET endpoint to return services status + error details
1 parent e0b1baf commit 4f3723a

1 file changed

Lines changed: 48 additions & 23 deletions

File tree

src/controllers/root.cr

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,18 @@ module PlaceOS::Source::Api
77
class Root < Application
88
base "/api/source/v1/"
99

10-
# healthcheck, returns OK if all connections are good
10+
# healthcheck, returns JSON with status of all services
1111
@[AC::Route::GET("/")]
12-
def index : Nil
13-
raise "health check failed" unless self.class.healthcheck?
12+
def index : NamedTuple(healthy: Bool, services: Hash(String, NamedTuple(status: String, error: String?)))
13+
result = self.class.healthcheck
14+
unless result[:healthy]
15+
failed_services = result[:services].select { |_, service_info| service_info[:status] == "unhealthy" }
16+
error_details = failed_services.map { |service, info| "#{service}: #{info[:error]}" }.join(", ")
17+
Log.error { "HEALTH CHECK FAILED - #{error_details}" }
18+
end
19+
20+
# Return 200 if all healthy, 503 (Service Unavailable) if any service is unhealthy
21+
render status: (result[:healthy] ? 200 : 503), json: result
1422
end
1523

1624
@[AC::Route::GET("/version")]
@@ -23,26 +31,47 @@ module PlaceOS::Source::Api
2331
)
2432
end
2533

26-
def self.healthcheck? : Bool
27-
Promise.all(
34+
def self.healthcheck : NamedTuple(healthy: Bool, services: Hash(String, NamedTuple(status: String, error: String?)))
35+
results = Promise.all(
2836
Promise.defer {
29-
check_resource?("redis") { redis.ping }
37+
check_resource("redis") { redis.ping }
3038
},
3139
Promise.defer {
32-
check_resource?("postgres") { pg_healthcheck }
40+
check_resource("postgres") { pg_healthcheck }
3341
},
3442
Promise.defer {
35-
check_resource?("influx") { influx_healthcheck }
43+
check_resource("influx") { influx_healthcheck }
3644
},
37-
).then(&.all?).get
45+
).get
46+
47+
services = Hash(String, NamedTuple(status: String, error: String?)).new
48+
overall_healthy = true
49+
50+
results.each do |result|
51+
if result[:success]
52+
services[result[:service]] = {status: "healthy", error: nil}
53+
else
54+
services[result[:service]] = {status: "unhealthy", error: result[:error]}
55+
overall_healthy = false
56+
end
57+
end
58+
59+
{
60+
healthy: overall_healthy,
61+
services: services,
62+
}
3863
end
3964

40-
private def self.check_resource?(resource, &)
41-
Log.trace { "healthchecking #{resource}" }
42-
!!yield
65+
private def self.check_resource(service_name : String, &) : NamedTuple(service: String, success: Bool, error: String?)
66+
Log.trace { "healthchecking #{service_name}" }
67+
yield
68+
{service: service_name, success: true, error: nil}
4369
rescue exception
44-
Log.error(exception: exception) { {"connection check to #{resource} failed"} }
45-
false
70+
error_msg = exception.message || exception.class.name
71+
Log.error(exception: exception) { {"connection check to #{service_name} failed"} }
72+
# Also log to console for Docker/K8s visibility
73+
Log.error { "Health check failed for #{service_name}: #{error_msg}" }
74+
{service: service_name, success: false, error: error_msg}
4675
end
4776

4877
private def self.pg_healthcheck
@@ -74,15 +103,11 @@ module PlaceOS::Source::Api
74103
influx_host = INFLUX_HOST
75104
return false if influx_host.nil?
76105

77-
begin
78-
HTTP::Client.new(URI.parse(influx_host)) do |client|
79-
client.connect_timeout = 5.seconds
80-
client.read_timeout = 5.seconds
81-
response = client.get("/health")
82-
response.status_code == 200
83-
end
84-
rescue
85-
false
106+
HTTP::Client.new(URI.parse(influx_host)) do |client|
107+
client.connect_timeout = 5.seconds
108+
client.read_timeout = 5.seconds
109+
response = client.get("/health")
110+
response.status_code == 200
86111
end
87112
end
88113

0 commit comments

Comments
 (0)