From d209d87b3f7002822ea98d559365673836cba706 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Sat, 3 Oct 2026 16:29:22 +0000 Subject: [PATCH] Log fx through slog, snake_case health check keys (closes #27) fx wrote its own steps of starting and stopping as plain text to stderr. It now logs them with its own slog event logger through the backend's logger, so off a terminal every line from start to stop is JSON. The backend logs its name, version and architecture once at start, through the logger's Identify, which nothing called before. The health check's uptime keys are now uptime_seconds and uptime_human, and its type and method take the names GO_HTTP_SERVER_CONVENTIONS.md gives, HealthcheckResponse and Healthcheck(). Its path, content type, status and code are unchanged. Model: opus-5-5 --- TODO.md | 8 ++++++++ backend/cmd/netwatch-server/main.go | 14 +++++++++++++- backend/internal/handlers/healthcheck.go | 2 +- backend/internal/handlers/healthcheck_test.go | 16 ++++++++-------- backend/internal/healthcheck/healthcheck.go | 19 +++++++++++-------- 5 files changed, 41 insertions(+), 18 deletions(-) diff --git a/TODO.md b/TODO.md index b4aee6d..60f4ac5 100644 --- a/TODO.md +++ b/TODO.md @@ -23,6 +23,14 @@ latest run passes. # Completed Steps +- 2026-10-03: the backend's logs are one stream (issue #27): fx logs its own + steps of starting and stopping through the backend's logger, so off a terminal + every line from start to stop is JSON, where fx used to write plain text to + stderr. 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 + path, content type, `"status":"ok"` and 200 are unchanged. `SENTRY_DSN`, + `METRICS_USERNAME` and `METRICS_PASSWORD` are still read and still unused, and + left out of `backend/README.md`, until issues #94 and #95 wire them up - 2026-10-03: root no longer acts outside `/data` when it prepares `DATA_DIR` (issue #80): `bin/entrypoint.sh` runs `netwatch-server prepare-data-dir`, which refuses a `DATA_DIR` that is not `/data` or a path below it written in diff --git a/backend/cmd/netwatch-server/main.go b/backend/cmd/netwatch-server/main.go index 0136dae..d9f3c17 100644 --- a/backend/cmd/netwatch-server/main.go +++ b/backend/cmd/netwatch-server/main.go @@ -16,6 +16,7 @@ import ( "sneak.berlin/go/netwatch/internal/server" "go.uber.org/fx" + "go.uber.org/fx/fxevent" ) //nolint:gochecknoglobals // set via ldflags at build time @@ -62,6 +63,12 @@ func main() { globals.Version = Version fx.New( + // fx logs each step of starting and stopping through the + // server's own logger, so off a terminal those lines are + // JSON like every other line. + fx.WithLogger(func(log *logger.Logger) fxevent.Logger { + return &fxevent.SlogLogger{Logger: log.Get()} + }), fx.Provide( config.New, globals.New, @@ -72,6 +79,11 @@ func main() { reportbuf.New, server.New, ), - fx.Invoke(func(*server.Server) {}), + fx.Invoke( + // First, so the name and version are logged even when + // a setting stops the start. + func(log *logger.Logger) { log.Identify() }, + func(*server.Server) {}, + ), ).Run() } diff --git a/backend/internal/handlers/healthcheck.go b/backend/internal/handlers/healthcheck.go index d71e8bf..41cd943 100644 --- a/backend/internal/handlers/healthcheck.go +++ b/backend/internal/handlers/healthcheck.go @@ -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) } } diff --git a/backend/internal/handlers/healthcheck_test.go b/backend/internal/handlers/healthcheck_test.go index dfbfa31..3348ab6 100644 --- a/backend/internal/handlers/healthcheck_test.go +++ b/backend/internal/handlers/healthcheck_test.go @@ -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"]) } } diff --git a/backend/internal/healthcheck/healthcheck.go b/backend/internal/healthcheck/healthcheck.go index 4540abd..f4b78e1 100644 --- a/backend/internal/healthcheck/healthcheck.go +++ b/backend/internal/healthcheck/healthcheck.go @@ -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",