feat(httpserver,log): /healthz, graceful shutdown, slog constructor

Implements internal/httpserver and internal/log.

httpserver (forgejo-mcp-broker-8ei):
- Server struct owns the HTTP lifecycle; Run(ctx) blocks, Handler() returns
  the composed handler for unit tests
- GET /healthz returns JSON with status, version, git_revision, build_date,
  and store probe result. Returns 503 when the store reports unhealthy
- Signal handling delegated to the caller via ctx cancellation — main wires
  signal.NotifyContext, httpserver just responds to Done()
- Graceful shutdown with a configurable deadline (default 10s). When the
  deadline expires, falls back to http.Server.Close() so lingering
  connections are forcibly terminated — http.Server.Shutdown alone never
  interrupts active connections
- ExtraHandler extension point for the OAuth + MCP routes that land in
  phase 2 and phase 5, so the server doesn't need to be re-plumbed later

log:
- Small slog wrapper: New(w, debug) returns a JSON logger that stamps every
  record with service/version/git_rev for correlation across deployments
- Discard() helper for tests

Tests: 97.9% coverage on httpserver (all health states, wrong-method,
ExtraHandler dispatch, ctx-cancel shutdown, shutdown-deadline force-close
of hanging requests, missing-field errors), 100% on log.

Closes forgejo-mcp-broker-8ei.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-04-24 17:26:32 +02:00
commit 36722940eb
7 changed files with 506 additions and 14 deletions

32
internal/log/log.go Normal file
View file

@ -0,0 +1,32 @@
// Package log constructs the process-wide structured logger.
//
// Every record carries the broker's build info so aggregated log backends can
// correlate events across versions and deployments.
package log
import (
"io"
"log/slog"
"kode.naiv.no/olemd/forgejo-mcp-broker/internal/buildinfo"
)
// New returns a JSON slog.Logger writing to w. When debug is true the
// logger starts at Debug level, otherwise at Info.
func New(w io.Writer, debug bool) *slog.Logger {
level := slog.LevelInfo
if debug {
level = slog.LevelDebug
}
h := slog.NewJSONHandler(w, &slog.HandlerOptions{Level: level})
return slog.New(h).With(
slog.String("service", "fjmcp-broker"),
slog.String("version", buildinfo.Version),
slog.String("git_rev", buildinfo.GitRevision),
)
}
// Discard returns a logger that drops every record. Useful in tests.
func Discard() *slog.Logger {
return slog.New(slog.DiscardHandler)
}