refactor(log): unified schema — mandatory type/class/method, request_id removed

Every log record now carries type ('http' for the request middleware
line, 'wrapper' for garmin execute() calls and forwarded wrapper.py
stderr, 'app' for everything else), class (Go type with package, or the
package/module for free functions), and method (the emitting Go/Python
function -- wrapper.py stamps it automatically, so its msg prefixes are
gone). msg is optional and omitted when empty; the redundant messages
('HTTP request', 'wrapper call', 'garmin wrapper stderr') are dropped,
the HTTP verb moves to http_method, source=garmin-wrapper is replaced by
type=wrapper, and the request_id middleware plumbing (applog
WithLogger/FromContext) is removed. applog.App(class, method) is the
tagging helper for app records.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 19:45:30 +02:00
parent 78c494affb
commit 6effb79097
16 changed files with 267 additions and 152 deletions

View File

@@ -1146,10 +1146,11 @@ func TestSessionCallback_MintsSessionCookieCarryingIDToken(t *testing.T) {
func TestRequestLoggingMiddleware_LogsMethodPathStatusDuration(t *testing.T) {
s, _, _ := newTestServer(t)
var buf bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&buf, nil))
prev := slog.Default()
slog.SetDefault(applog.NewLogger("info", &buf))
defer slog.SetDefault(prev)
req := httptest.NewRequest(http.MethodGet, "/api/health", nil)
req = req.WithContext(applog.WithLogger(req.Context(), logger))
rec := httptest.NewRecorder()
s.Router().ServeHTTP(rec, req)
@@ -1157,11 +1158,14 @@ func TestRequestLoggingMiddleware_LogsMethodPathStatusDuration(t *testing.T) {
if err := json.Unmarshal(buf.Bytes(), &entry); err != nil {
t.Fatalf("log output is not valid JSON: %v (%q)", err, buf.String())
}
if entry["msg"] != "HTTP request" {
t.Errorf("msg = %v, want \"http request\"", entry["msg"])
if entry["type"] != "http" || entry["class"] != "api" || entry["method"] != "loggingMiddleware" {
t.Errorf("schema fields = %v, want type=http class=api method=loggingMiddleware", entry)
}
if entry["method"] != "GET" || entry["path"] != "/api/health" {
t.Errorf("method/path = %v/%v, want GET//api/health", entry["method"], entry["path"])
if _, hasMsg := entry["msg"]; hasMsg {
t.Errorf("expected no msg on the http record, got %v", entry["msg"])
}
if entry["http_method"] != "GET" || entry["path"] != "/api/health" {
t.Errorf("http_method/path = %v/%v, want GET//api/health", entry["http_method"], entry["path"])
}
if entry["status"] != float64(http.StatusOK) {
t.Errorf("status = %v, want 200", entry["status"])
@@ -1176,10 +1180,11 @@ func TestRequestLoggingMiddleware_5xxLogsAtWarnLevel(t *testing.T) {
db.Close() // force a downstream DB call to fail with a 500
var buf bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&buf, nil))
prev := slog.Default()
slog.SetDefault(applog.NewLogger("info", &buf))
defer slog.SetDefault(prev)
req := httptest.NewRequest(http.MethodGet, "/api/profile", nil)
req = req.WithContext(applog.WithLogger(req.Context(), logger))
cookie, err := auth.MintSessionCookie(auth.Claims{Sub: "test-user", Name: "Test User", Email: "test@example.com"}, "", testSessionConfig.Secret, testSessionConfig.Duration, testSessionConfig.Secure)
if err != nil {
t.Fatalf("mint session cookie: %v", err)