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:
parent
df2253398b
commit
36722940eb
7 changed files with 506 additions and 14 deletions
55
internal/log/log_test.go
Normal file
55
internal/log/log_test.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package log_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
brokerlog "kode.naiv.no/olemd/forgejo-mcp-broker/internal/log"
|
||||
)
|
||||
|
||||
func TestNew_WritesJSON(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
l := brokerlog.New(&buf, false)
|
||||
l.Info("hello", "key", "value")
|
||||
|
||||
var rec map[string]any
|
||||
if err := json.Unmarshal(buf.Bytes(), &rec); err != nil {
|
||||
t.Fatalf("output is not valid JSON: %v\ngot: %s", err, buf.String())
|
||||
}
|
||||
if rec["msg"] != "hello" {
|
||||
t.Errorf("msg = %v, want hello", rec["msg"])
|
||||
}
|
||||
if rec["service"] != "fjmcp-broker" {
|
||||
t.Errorf("service = %v, want fjmcp-broker", rec["service"])
|
||||
}
|
||||
if rec["key"] != "value" {
|
||||
t.Errorf("key = %v, want value", rec["key"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_DebugSuppressedByDefault(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
l := brokerlog.New(&buf, false)
|
||||
l.Debug("debug-only")
|
||||
if buf.Len() > 0 {
|
||||
t.Errorf("debug record should be suppressed at info level, got: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_DebugIncludedWhenEnabled(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
l := brokerlog.New(&buf, true)
|
||||
l.Debug("debug-enabled")
|
||||
if !strings.Contains(buf.String(), "debug-enabled") {
|
||||
t.Errorf("debug record missing with debug=true, got: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscard_NoOutput(t *testing.T) {
|
||||
l := brokerlog.Discard()
|
||||
l.Info("ignored")
|
||||
l.Error("also ignored")
|
||||
// Nothing to assert except that these calls don't panic.
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue