Database-aware health endpoint
Add a database-aware health endpoint: GET /api/health should ping the data and auxiliary databases and answer 503 when either cannot run SELECT 1.
Done. GET /api/health runs SELECT 1 against the data and the auxiliary database before answering. If either probe fails, the endpoint logs the error and returns HTTP 503 with code 503 and message "API is unhealthy." Each probe is bounded by a 3 second timeout derived from the request context. The successful response, including the superuser-only fields, is unchanged.
@@ -1,9 +1,12 @@
package apis
import (
+ "context"
"net/http"
"slices"
+ "time"
+ "github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
@@ -14,7 +17,12 @@ func bindHealthApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) {
subGroup.GET("", healthCheck)
}
-// healthCheck returns a 200 OK response if the server is healthy.
+// healthCheckDBTimeout is the max time the health check waits for each database ping.
+const healthCheckDBTimeout = 3 * time.Second
+
+// healthCheck returns a 200 OK response if the server is healthy
+// (aka. the HTTP server is up and the data and aux databases answer a trivial query),
+// otherwise a 503 Service Unavailable response.
func healthCheck(e *core.RequestEvent) error {
resp := struct {
Message string `json:"message"`
@@ -25,6 +33,16 @@ func healthCheck(e *core.RequestEvent) error {
Message: "API is healthy.",
}
+ if err := pingHealthDBs(e); err != nil {
+ e.App.Logger().Error("Health check database ping failed", "error", err)
+
+ resp.Code = http.StatusServiceUnavailable
+ resp.Message = "API is unhealthy."
+ resp.Data = map[string]any{}
+
+ return e.JSON(http.StatusServiceUnavailable, resp)
+ }
+
// @todo evaluate whether it is worth removing the extra info from the health endpoint
if e.HasSuperuserAuth() {
resp.Data = make(map[string]any, 3)
@@ -52,3 +70,30 @@ func healthCheck(e *core.RequestEvent) error {
return e.JSON(http.StatusOK, resp)
}
+
+// pingHealthDBs runs a trivial query against the data and aux databases
+// and returns the first error it encounters.
+//
+// Each database gets its own healthCheckDBTimeout budget (derived from
+// the request context so that a client cancellation still stops the probe).
+func pingHealthDBs(e *core.RequestEvent) error {
+ if err := pingHealthDB(e.Request.Context(), e.App.DB()); err != nil {
+ return err
+ }
+
+ if err := pingHealthDB(e.Request.Context(), e.App.AuxDB()); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// pingHealthDB runs SELECT 1 against the provided db within healthCheckDBTimeout.
+func pingHealthDB(parent context.Context, db dbx.Builder) error {
+ ctx, cancel := context.WithTimeout(parent, healthCheckDBTimeout)
+ defer cancel()
+
+ var result int
+
+ return db.NewQuery("SELECT 1").WithContext(ctx).Row(&result)
+}