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",