package centrifugo import ( "context" "encoding/json" "errors" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bonfire" ) // HealthCommandName is the name of the Centrifugo health check command. const HealthCommandName = "websockets:health" // healthFailed is returned by the health command after it has printed why // the check failed: bonfire.Reported makes the binary exit 1 without // repeating the message. func healthFailed() error { return bonfire.Reported(errors.New(HealthCommandName + ": check failed")) } // healthJSON is websockets:health --json. The API key is never included. type healthJSON struct { Healthy bool `json:"healthy"` APIURL string `json:"api_url"` Enabled bool `json:"enabled"` APIKeySet bool `json:"api_key_set"` Error string `json:"error,omitempty"` } // Commands returns the Centrifugo console commands for an application // binary: the HealthCommandName check. An application appends them to the // commands its plugin returns from Commands. func Commands(app *backpack.App) []bonfire.Command { return []bonfire.Command{healthCommand(app)} } // healthCommand ports the WinterCMS CentrifugoHealthCheck command. The // WinterCMS command only read the local configuration; this one also calls // the Centrifugo info API, so a wrong URL or key fails the check. The API // key is never printed. func healthCommand(app *backpack.App) bonfire.Command { return bonfire.Command{ Name: HealthCommandName, Description: "Check Centrifugo connection health", Long: `Reads the Centrifugo settings (realtime.centrifugo.*) and calls the Centrifugo info API with them, so a wrong URL or API key fails the check. The API key is never printed. It exits 1 when Centrifugo is not configured or does not answer. --json prints {healthy, api_url, enabled, api_key_set, error} for monitoring scripts.`, Flags: []bonfire.Flag{{Name: "json", Description: "Print the result as JSON", Bare: true}}, Examples: []bonfire.Example{ {Command: HealthCommandName, Description: "Check the connection"}, {Command: HealthCommandName + " --json", Description: "Machine-readable result for a monitoring check"}, }, Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { var cfg Config if app != nil { cfg = LoadConfig(app.Config) } else { cfg = LoadConfig(nil) } client := NewClient(cfg, nil) asJSON := false if in != nil { v, _ := in.Flag("json") asJSON = v == "true" } if asJSON { info := client.DebugInfo() res := healthJSON{APIURL: info.APIURL, Enabled: info.Enabled, APIKeySet: info.APIKeySet} if !client.Enabled() { res.Error = "Centrifugo not configured (API key missing)" } else if _, err := client.Info(ctx); err != nil { res.Error = err.Error() } else { res.Healthy = true } b, err := json.MarshalIndent(res, "", " ") if err != nil { return err } out.Println(string(b)) if !res.Healthy { return healthFailed() } return nil } if !client.Enabled() { out.Error("Centrifugo not configured (API key missing)") out.Println("Set SUMMER_REALTIME__CENTRIFUGO__API_KEY in the environment") return healthFailed() } out.Info("Checking Centrifugo connection...") out.Println("API URL: " + cfg.APIURL) if _, err := client.Info(ctx); err != nil { out.Error("Connection check failed: " + err.Error()) return healthFailed() } info := client.DebugInfo() out.Success("Configuration OK") out.Table([]string{"Setting", "Value"}, [][]string{ {"API URL", info.APIURL}, {"Enabled", yesNo(info.Enabled)}, {"API Key Set", yesNo(info.APIKeySet)}, }) return nil }, } } func yesNo(b bool) string { if b { return "Yes" } return "No" }