From f0a48cc91ce4d0c1cce96c15fbc34098059227df Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 6 Oct 2025 21:45:23 +0300 Subject: [PATCH] feat: add health check command --- Dockerfile | 2 - cmd/health.go | 109 +++++++++++++++++++++++ cmd/root.go | 1 + internal/controller/health_controller.go | 4 +- 4 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 cmd/health.go diff --git a/Dockerfile b/Dockerfile index a0ebc0d..f61a32a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,8 +45,6 @@ FROM alpine:3.22 AS runner WORKDIR /tinyauth -RUN apk add --no-cache curl - COPY --from=builder /tinyauth/tinyauth ./ EXPOSE 3000 diff --git a/cmd/health.go b/cmd/health.go new file mode 100644 index 0000000..214076e --- /dev/null +++ b/cmd/health.go @@ -0,0 +1,109 @@ +package cmd + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "tinyauth/internal/config" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +type healthzResponse struct { + Status string `json:"status"` + Message string `json:"message"` +} + +type healthCmd struct { + root *cobra.Command + cmd *cobra.Command + + viper *viper.Viper + appUrl string +} + +func newHealthCmd(root *cobra.Command) *healthCmd { + return &healthCmd{ + root: root, + viper: viper.New(), + } +} + +func (c *healthCmd) Register() { + c.cmd = &cobra.Command{ + Use: "health", + Short: "Health check", + Long: `Use the health check endpoint to verify that Tinyauth is running and it's healthy.`, + Run: c.run, + } + + c.viper.AutomaticEnv() + c.cmd.Flags().StringVar(&c.appUrl, "app-url", "http://localhost:3000", "The URL where the Tinyauth server is running on.") + c.viper.BindEnv("app-url", "APP_URL") + c.viper.BindPFlags(c.cmd.Flags()) + + if c.root != nil { + c.root.AddCommand(c.cmd) + } +} + +func (c *healthCmd) GetCmd() *cobra.Command { + return c.cmd +} + +func (c *healthCmd) run(cmd *cobra.Command, args []string) { + log.Logger = log.Level(zerolog.InfoLevel) + + appUrl := c.viper.GetString("app-url") + + if appUrl == "" { + log.Fatal().Err(errors.New("app-url is required")).Msg("App URL is required") + } + + if config.Version == "development" { + log.Warn().Msg("Running in development mode. Overriding the app-url to http://localhost:3000") + appUrl = "http://localhost:3000" + } + + log.Info().Msgf("Health check endpoint is available at %s/api/healthz", appUrl) + + client := http.Client{} + + req, err := http.NewRequest("GET", appUrl+"/api/healthz", nil) + + if err != nil { + log.Fatal().Err(err).Msg("Failed to create request") + } + + resp, err := client.Do(req) + + if err != nil { + log.Fatal().Err(err).Msg("Failed to perform request") + } + + if resp.StatusCode != http.StatusOK { + log.Fatal().Err(errors.New("service is not healthy")).Msgf("Service is not healthy. Status code: %d", resp.StatusCode) + } + + defer resp.Body.Close() + + var healthResp healthzResponse + + body, err := io.ReadAll(resp.Body) + + if err != nil { + log.Fatal().Err(err).Msg("Failed to read response") + } + + err = json.Unmarshal(body, &healthResp) + + if err != nil { + log.Fatal().Err(err).Msg("Failed to decode response") + } + + log.Info().Interface("response", healthResp).Msg("Service is healthy") +} diff --git a/cmd/root.go b/cmd/root.go index e7bbb13..92abab3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -140,6 +140,7 @@ func Run() { newVerifyUserCmd(userCmd).Register() newGenerateTotpCmd(totpCmd).Register() newVersionCmd(root).Register() + newHealthCmd(root).Register() root.AddCommand(userCmd) root.AddCommand(totpCmd) diff --git a/internal/controller/health_controller.go b/internal/controller/health_controller.go index 8f0aa42..8ad67b5 100644 --- a/internal/controller/health_controller.go +++ b/internal/controller/health_controller.go @@ -13,8 +13,8 @@ func NewHealthController(router *gin.RouterGroup) *HealthController { } func (controller *HealthController) SetupRoutes() { - controller.router.GET("/health", controller.healthHandler) - controller.router.HEAD("/health", controller.healthHandler) + controller.router.GET("/healthz", controller.healthHandler) + controller.router.HEAD("/healthz", controller.healthHandler) } func (controller *HealthController) healthHandler(c *gin.Context) {