Major changes: - Add internal/logging package with field constants, context propagation, sensitive data auto-redaction, and per-component log levels - Add worker timeout constants (TimeoutQuickOp, TimeoutHealthCheck, etc.) - Extend SDLC with callback handlers, generate endpoints, and executor - Add new cookbook trees for aeries and slackpath progression - Add skeleton templates for queue, realtime, and microservices - Add worker component template with async job processing - Refactor services and handlers to use new logging infrastructure - Split component.go into component_infra.go and component_listing.go Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
273 lines
7.1 KiB
Go
273 lines
7.1 KiB
Go
package logging
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestMiddleware(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
cfg := DefaultConfig()
|
|
cfg.RedactEnabled = false
|
|
logger := NewWithWriter(cfg, &buf)
|
|
|
|
middlewareCfg := DefaultMiddlewareConfig()
|
|
middlewareCfg.Logger = logger
|
|
|
|
handler := Middleware(middlewareCfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("ok"))
|
|
}))
|
|
|
|
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d", rec.Code)
|
|
}
|
|
|
|
output := buf.String()
|
|
|
|
// Should log request completion
|
|
if !strings.Contains(output, "request completed") {
|
|
t.Errorf("expected 'request completed' in log, got: %s", output)
|
|
}
|
|
|
|
// Should include HTTP fields
|
|
if !strings.Contains(output, FieldHTTPMethod) {
|
|
t.Errorf("expected http_method field, got: %s", output)
|
|
}
|
|
if !strings.Contains(output, FieldHTTPPath) {
|
|
t.Errorf("expected http_path field, got: %s", output)
|
|
}
|
|
if !strings.Contains(output, FieldHTTPStatus) {
|
|
t.Errorf("expected http_status field, got: %s", output)
|
|
}
|
|
if !strings.Contains(output, FieldDuration) {
|
|
t.Errorf("expected duration_ms field, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestMiddlewareGeneratesRequestID(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
cfg := DefaultConfig()
|
|
cfg.RedactEnabled = false
|
|
logger := NewWithWriter(cfg, &buf)
|
|
|
|
middlewareCfg := DefaultMiddlewareConfig()
|
|
middlewareCfg.Logger = logger
|
|
middlewareCfg.GenerateRequestID = true
|
|
|
|
handler := Middleware(middlewareCfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
|
|
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
// Should set request ID header in response
|
|
requestID := rec.Header().Get(RequestIDHeader)
|
|
if requestID == "" {
|
|
t.Error("expected X-Request-ID header to be set")
|
|
}
|
|
|
|
// Should include request ID in log
|
|
output := buf.String()
|
|
if !strings.Contains(output, FieldRequestID) {
|
|
t.Errorf("expected request_id field, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestMiddlewarePropagatesRequestID(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
cfg := DefaultConfig()
|
|
cfg.RedactEnabled = false
|
|
logger := NewWithWriter(cfg, &buf)
|
|
|
|
middlewareCfg := DefaultMiddlewareConfig()
|
|
middlewareCfg.Logger = logger
|
|
|
|
handler := Middleware(middlewareCfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
|
|
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
req.Header.Set(RequestIDHeader, "test-request-id-123")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
// Should echo back the request ID
|
|
requestID := rec.Header().Get(RequestIDHeader)
|
|
if requestID != "test-request-id-123" {
|
|
t.Errorf("expected X-Request-ID to be echoed, got: %s", requestID)
|
|
}
|
|
|
|
// Should use the provided request ID in log
|
|
output := buf.String()
|
|
if !strings.Contains(output, "test-request-id-123") {
|
|
t.Errorf("expected provided request_id in log, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestMiddlewareSkipPaths(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
cfg := DefaultConfig()
|
|
cfg.RedactEnabled = false
|
|
logger := NewWithWriter(cfg, &buf)
|
|
|
|
middlewareCfg := DefaultMiddlewareConfig()
|
|
middlewareCfg.Logger = logger
|
|
middlewareCfg.SkipPaths = map[string]bool{"/health": true}
|
|
|
|
handler := Middleware(middlewareCfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
|
|
req := httptest.NewRequest("GET", "/health", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
// Should not log anything for skipped paths
|
|
output := buf.String()
|
|
if output != "" {
|
|
t.Errorf("expected no log output for skipped path, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestMiddlewareLogLevelByStatus(t *testing.T) {
|
|
tests := []struct {
|
|
status int
|
|
logLevel string
|
|
}{
|
|
{200, "INFO"},
|
|
{201, "INFO"},
|
|
{204, "INFO"},
|
|
{301, "INFO"},
|
|
{400, "WARN"},
|
|
{401, "WARN"},
|
|
{404, "WARN"},
|
|
{500, "ERROR"},
|
|
{502, "ERROR"},
|
|
{503, "ERROR"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(http.StatusText(tt.status), func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
cfg := DefaultConfig()
|
|
cfg.RedactEnabled = false
|
|
logger := NewWithWriter(cfg, &buf)
|
|
|
|
middlewareCfg := DefaultMiddlewareConfig()
|
|
middlewareCfg.Logger = logger
|
|
|
|
handler := Middleware(middlewareCfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(tt.status)
|
|
}))
|
|
|
|
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
output := buf.String()
|
|
if !strings.Contains(output, tt.logLevel) {
|
|
t.Errorf("expected %s level for status %d, got: %s", tt.logLevel, tt.status, output)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMiddlewareContextLogger(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
cfg := DefaultConfig()
|
|
cfg.RedactEnabled = false
|
|
logger := NewWithWriter(cfg, &buf)
|
|
|
|
middlewareCfg := DefaultMiddlewareConfig()
|
|
middlewareCfg.Logger = logger
|
|
|
|
var ctxLogger *Logger
|
|
handler := Middleware(middlewareCfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Handler can get logger from context
|
|
ctxLogger = FromContext(r.Context())
|
|
ctxLogger.Info("handler logging")
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
|
|
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if ctxLogger == nil {
|
|
t.Fatal("expected logger in context")
|
|
}
|
|
|
|
output := buf.String()
|
|
// Handler's log should include the request context
|
|
if !strings.Contains(output, "handler logging") {
|
|
t.Errorf("expected handler log in output, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestMiddlewareJSONOutput(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
cfg := DefaultConfig()
|
|
cfg.Format = FormatJSON
|
|
cfg.RedactEnabled = false
|
|
logger := NewWithWriter(cfg, &buf)
|
|
|
|
middlewareCfg := DefaultMiddlewareConfig()
|
|
middlewareCfg.Logger = logger
|
|
|
|
handler := Middleware(middlewareCfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("test"))
|
|
}))
|
|
|
|
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
// Should have two JSON log entries (debug start + info complete)
|
|
lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
|
|
if len(lines) < 1 {
|
|
t.Fatalf("expected at least 1 log line, got: %d", len(lines))
|
|
}
|
|
|
|
// Parse the completion log (last line)
|
|
var entry map[string]any
|
|
if err := json.Unmarshal([]byte(lines[len(lines)-1]), &entry); err != nil {
|
|
t.Fatalf("expected valid JSON, got error: %v, line: %s", err, lines[len(lines)-1])
|
|
}
|
|
|
|
// Verify expected fields
|
|
if entry[FieldHTTPMethod] != "GET" {
|
|
t.Errorf("expected http_method=GET, got: %v", entry[FieldHTTPMethod])
|
|
}
|
|
if entry[FieldHTTPPath] != "/api/test" {
|
|
t.Errorf("expected http_path=/api/test, got: %v", entry[FieldHTTPPath])
|
|
}
|
|
if entry[FieldHTTPStatus] != float64(200) {
|
|
t.Errorf("expected http_status=200, got: %v", entry[FieldHTTPStatus])
|
|
}
|
|
if _, ok := entry[FieldDuration]; !ok {
|
|
t.Error("expected duration_ms field")
|
|
}
|
|
if _, ok := entry[FieldRequestID]; !ok {
|
|
t.Error("expected request_id field")
|
|
}
|
|
}
|