Log fx through slog, snake_case health check keys (closes #27)
check / check (push) Successful in 4m40s
check / check (push) Successful in 4m40s
fx wrote its own steps of starting and stopping as plain text to stderr. It now logs them with its slog event logger through the backend's logger, so off a terminal every line the backend's own logger and fx write is JSON. A malformed config file now makes config.New return the error instead of panicking. A test runs the server as a child process and checks that all it writes is JSON, on a normal start and stop and with such a config file. The backend logs its name, version and architecture once at start. The health check's uptime keys are now uptime_seconds and uptime_human; its type and method take the names GO_HTTP_SERVER_CONVENTIONS.md gives. Rules suppressed: revive and tagliatelle on HealthcheckResponse, whose name and keys come from the conventions; gosec where the test starts its own binary. Model: opus-5-5
This commit was merged in pull request #98.
This commit is contained in:
@@ -73,7 +73,8 @@ type Config struct {
|
||||
|
||||
// New loads configuration from env, .env files, and config
|
||||
// files, returning a fully resolved Config. It fails, with an error
|
||||
// naming the setting, on a value the server cannot use.
|
||||
// naming the setting, on a value the server cannot use, and on a
|
||||
// config file it finds but cannot read.
|
||||
func New(
|
||||
_ fx.Lifecycle,
|
||||
params Params,
|
||||
@@ -106,8 +107,8 @@ func New(
|
||||
if err != nil {
|
||||
var notFound viper.ConfigFileNotFoundError
|
||||
if !errors.As(err, ¬Found) {
|
||||
log.Error("config file malformed", "error", err)
|
||||
panic(err)
|
||||
return nil, fmt.Errorf("config file %s: %w",
|
||||
viper.ConfigFileUsed(), err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@ import "net/http"
|
||||
// endpoint.
|
||||
func (s *Handlers) HandleHealthCheck() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
s.respondJSON(w, r, s.hc.Check(), http.StatusOK)
|
||||
s.respondJSON(w, r, s.hc.Healthcheck(), http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ func newStartedHandlers(t *testing.T, g *globals.Globals) *handlers.Handlers {
|
||||
|
||||
// TestHandleHealthCheck checks the health check's answer: 200, a JSON
|
||||
// content type, and a JSON object with exactly the fields of
|
||||
// healthcheck.Response, carrying this server's name and version and
|
||||
// an uptime counted from its start.
|
||||
// healthcheck.HealthcheckResponse, carrying this server's name and
|
||||
// version and an uptime counted from its start.
|
||||
func TestHandleHealthCheck(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -82,7 +82,7 @@ func TestHandleHealthCheck(t *testing.T) {
|
||||
}
|
||||
|
||||
fields := []string{
|
||||
"appname", "now", "status", "uptimeHuman", "uptimeSeconds", "version",
|
||||
"appname", "now", "status", "uptime_human", "uptime_seconds", "version",
|
||||
}
|
||||
if got := slices.Sorted(maps.Keys(body)); !slices.Equal(got, fields) {
|
||||
t.Fatalf("fields = %v, want %v", got, fields)
|
||||
@@ -104,17 +104,17 @@ func TestHandleHealthCheck(t *testing.T) {
|
||||
}
|
||||
|
||||
// Started just now, so the uptime is well under a minute.
|
||||
human, _ := body["uptimeHuman"].(string)
|
||||
human, _ := body["uptime_human"].(string)
|
||||
|
||||
uptime, err := time.ParseDuration(human)
|
||||
if err != nil || uptime > time.Minute {
|
||||
t.Errorf("uptimeHuman = %q, want a duration under a minute (%v)",
|
||||
t.Errorf("uptime_human = %q, want a duration under a minute (%v)",
|
||||
human, err)
|
||||
}
|
||||
|
||||
seconds, ok := body["uptimeSeconds"].(float64)
|
||||
seconds, ok := body["uptime_seconds"].(float64)
|
||||
if !ok || seconds < 0 || seconds > time.Minute.Seconds() {
|
||||
t.Errorf("uptimeSeconds = %v, want a number of seconds under a minute",
|
||||
body["uptimeSeconds"])
|
||||
t.Errorf("uptime_seconds = %v, want a number of seconds under a minute",
|
||||
body["uptime_seconds"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,14 +30,17 @@ type Healthcheck struct {
|
||||
params *Params
|
||||
}
|
||||
|
||||
// Response is the JSON payload returned by the health check
|
||||
// endpoint.
|
||||
type Response struct {
|
||||
// HealthcheckResponse is the JSON payload returned by the health
|
||||
// check endpoint. Its name and its snake_case keys are the ones
|
||||
// GO_HTTP_SERVER_CONVENTIONS.md gives.
|
||||
//
|
||||
//nolint:revive,tagliatelle // name and keys from the conventions
|
||||
type HealthcheckResponse struct {
|
||||
Appname string `json:"appname"`
|
||||
Now string `json:"now"`
|
||||
Status string `json:"status"`
|
||||
UptimeHuman string `json:"uptimeHuman"`
|
||||
UptimeSeconds int64 `json:"uptimeSeconds"`
|
||||
UptimeHuman string `json:"uptime_human"`
|
||||
UptimeSeconds int64 `json:"uptime_seconds"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
@@ -65,9 +68,9 @@ func New(
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Check returns the current health status of the application.
|
||||
func (s *Healthcheck) Check() *Response {
|
||||
return &Response{
|
||||
// Healthcheck returns the current health status of the application.
|
||||
func (s *Healthcheck) Healthcheck() *HealthcheckResponse {
|
||||
return &HealthcheckResponse{
|
||||
Appname: s.params.Globals.Appname,
|
||||
Now: time.Now().UTC().Format(time.RFC3339Nano),
|
||||
Status: "ok",
|
||||
|
||||
Reference in New Issue
Block a user