Adds langfuseMiddleware in internal/shared so every daemon's shared.Run gets free production-traffic trace visibility when LANGFUSE_URL + LANGFUSE_PUBLIC_KEY + LANGFUSE_SECRET_KEY are set. Same env names + file shape as the multi_coord_stress driver, so operators ship one /etc/lakehouse/langfuse.env across the deploy. Wiring is auth-gated: middleware runs INSIDE the RequireAuth group, so 401s from credential-stuffing don't pollute traces. /health is exempt so LB probes don't either. Missing env vars → nil client → middleware is a passthrough no-op (fail-open per ADR-005 5.1). Bundled deploy: - langfuse.env.example template (mode 0640, root:lakehouse) - 11 systemd units gain `EnvironmentFile=-/etc/lakehouse/langfuse.env` (leading - so missing file = OK) - REPLICATION.md bootstrap section documents setup Tests (4): nil passthrough, /health bypass, real-request emission, status-writer wrapping. All green. STATE_OF_PLAY OPEN list: 5 rows → 4 rows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
147 lines
4.3 KiB
Go
147 lines
4.3 KiB
Go
package shared
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
|
|
"git.agentview.dev/profit/golangLAKEHOUSE/internal/langfuse"
|
|
)
|
|
|
|
// TestLangfuseMiddleware_NilClientPassthrough locks the
|
|
// "no client → no-op" contract. Every daemon calls shared.Run;
|
|
// operators who don't set LANGFUSE_URL must not see middleware
|
|
// failures, latency, or behavior change of any kind.
|
|
func TestLangfuseMiddleware_NilClientPassthrough(t *testing.T) {
|
|
mw := langfuseMiddleware("test-service", nil)
|
|
called := false
|
|
h := mw(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
called = true
|
|
w.WriteHeader(http.StatusTeapot) // distinctive code
|
|
}))
|
|
srv := httptest.NewServer(h)
|
|
defer srv.Close()
|
|
resp, err := http.Get(srv.URL + "/anything")
|
|
if err != nil {
|
|
t.Fatalf("GET: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusTeapot {
|
|
t.Errorf("expected 418 (handler ran), got %d", resp.StatusCode)
|
|
}
|
|
if !called {
|
|
t.Error("handler should have run via passthrough")
|
|
}
|
|
}
|
|
|
|
// TestLangfuseMiddleware_HealthBypassed locks the /health-exempt
|
|
// rule (per langfuseMiddleware's doc comment): LB probes must not
|
|
// emit traces or the trace volume drowns out real signal.
|
|
func TestLangfuseMiddleware_HealthBypassed(t *testing.T) {
|
|
var (
|
|
mu sync.Mutex
|
|
captured []string // ingestion endpoint payloads
|
|
)
|
|
// Mock Langfuse ingestion endpoint that records every batch.
|
|
lfMock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
body, _ := io.ReadAll(r.Body)
|
|
mu.Lock()
|
|
captured = append(captured, string(body))
|
|
mu.Unlock()
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer lfMock.Close()
|
|
|
|
lf := langfuse.New(lfMock.URL, "test-pk", "test-sk", nil)
|
|
mw := langfuseMiddleware("test-service", lf)
|
|
h := mw(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
srv := httptest.NewServer(h)
|
|
defer srv.Close()
|
|
|
|
resp, err := http.Get(srv.URL + "/health")
|
|
if err != nil {
|
|
t.Fatalf("GET /health: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
_ = lf.Close() // force flush
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if len(captured) > 0 {
|
|
t.Errorf("expected zero ingestion calls for /health, got %d (%v)", len(captured), captured)
|
|
}
|
|
}
|
|
|
|
// TestLangfuseMiddleware_RealRequestEmitted locks the happy path:
|
|
// a real request through an authed route produces ingestion events
|
|
// (trace + span). We don't decode the payload here — the
|
|
// internal/langfuse client tests already verify the wire format.
|
|
// What this test asserts is the wiring: middleware → client → POST.
|
|
func TestLangfuseMiddleware_RealRequestEmitted(t *testing.T) {
|
|
var (
|
|
mu sync.Mutex
|
|
captured int
|
|
)
|
|
lfMock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = io.ReadAll(r.Body)
|
|
mu.Lock()
|
|
captured++
|
|
mu.Unlock()
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer lfMock.Close()
|
|
|
|
lf := langfuse.New(lfMock.URL, "test-pk", "test-sk", nil)
|
|
mw := langfuseMiddleware("test-service", lf)
|
|
h := mw(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
srv := httptest.NewServer(h)
|
|
defer srv.Close()
|
|
|
|
resp, err := http.Get(srv.URL + "/api/data")
|
|
if err != nil {
|
|
t.Fatalf("GET /api/data: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
_ = lf.Close() // force flush — sends the queued trace + span
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if captured == 0 {
|
|
t.Error("expected at least one ingestion call after real request")
|
|
}
|
|
}
|
|
|
|
// TestLangfuseMiddleware_StatusCaptured locks the status-writer
|
|
// wrapping: when the handler returns 500, the middleware must see
|
|
// 500 in the span output (otherwise error traces all show 200 and
|
|
// debugging gets harder).
|
|
func TestLangfuseMiddleware_StatusCaptured(t *testing.T) {
|
|
mw := langfuseMiddleware("test-service", nil) // nil client; just exercise wrapping
|
|
called := false
|
|
h := mw(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
called = true
|
|
http.Error(w, "boom", http.StatusInternalServerError)
|
|
}))
|
|
srv := httptest.NewServer(h)
|
|
defer srv.Close()
|
|
resp, err := http.Get(srv.URL + "/api/fail")
|
|
if err != nil {
|
|
t.Fatalf("GET: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusInternalServerError {
|
|
t.Errorf("expected 500, got %d", resp.StatusCode)
|
|
}
|
|
if !called {
|
|
t.Error("handler should have run")
|
|
}
|
|
}
|