feat: close v3 governance evidence and slo metrics wiring
Some checks failed
CI / Build & Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Release (push) Has been cancelled

This commit is contained in:
phamnazage-jpg
2026-06-08 13:59:03 +08:00
parent 13b88ba2e7
commit dbbb313a36
15 changed files with 347 additions and 39 deletions

View File

@@ -591,7 +591,7 @@ func NewAPIHandlerWithAuth(adminAuth AdminAuthConfig, actions ActionSet, dsn ...
mux.Handle("DELETE /api/hosts/{hostID}", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handleDeleteHost(w, r, actions.DeleteHost)
})))
return mux
return metrics.Middleware(mux)
}
func healthz(w http.ResponseWriter, _ *http.Request) {
@@ -2692,12 +2692,14 @@ func handlePublicV1ChatCompletions(w http.ResponseWriter, r *http.Request, dsn s
// 1. Extract bearer key
auth := strings.TrimSpace(r.Header.Get("Authorization"))
if !strings.HasPrefix(auth, "Bearer ") {
metrics.RecordUserKeyChatRequest("unauthorized")
writeHTTPError(w, &httpError{StatusCode: http.StatusUnauthorized, Code: "unauthorized", Message: "missing or invalid Authorization header"})
return
}
keyToken := strings.TrimPrefix(auth, "Bearer ")
keyToken = strings.TrimSpace(keyToken)
if keyToken == "" {
metrics.RecordUserKeyChatRequest("unauthorized")
writeHTTPError(w, &httpError{StatusCode: http.StatusUnauthorized, Code: "unauthorized", Message: "empty bearer token"})
return
}
@@ -2707,6 +2709,7 @@ func handlePublicV1ChatCompletions(w http.ResponseWriter, r *http.Request, dsn s
store, err := sqlite.Open(r.Context(), dsn)
if err != nil {
metrics.RecordUserKeyChatRequest("db_error")
writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "db_error", Message: "database error"})
return
}
@@ -2714,6 +2717,7 @@ func handlePublicV1ChatCompletions(w http.ResponseWriter, r *http.Request, dsn s
keys, err := store.UserKeys().ListByFingerprint(r.Context(), keyFingerprint)
if err != nil || len(keys) == 0 {
metrics.RecordUserKeyChatRequest("invalid_api_key")
writeHTTPError(w, &httpError{StatusCode: http.StatusUnauthorized, Code: "unauthorized", Message: "invalid API key"})
return
}
@@ -2723,17 +2727,25 @@ func handlePublicV1ChatCompletions(w http.ResponseWriter, r *http.Request, dsn s
// Governance check
log.Printf("gateway: key %s admin_status=%s paused=%v fingerprint=%s", key.KeyID, key.AdminStatus, key.AdminStatus == "paused", keyFingerprint)
if key.AdminStatus == "paused" {
metrics.RecordUserKeyChatRequest("key_paused")
writeHTTPError(w, &httpError{StatusCode: http.StatusForbidden, Code: "key_paused", Message: "API key is paused"})
return
}
if key.AdminStatus == "retired" || key.AdminStatus == "deleted" {
metrics.RecordUserKeyChatRequest("key_retired")
writeHTTPError(w, &httpError{StatusCode: http.StatusForbidden, Code: "key_retired", Message: "API key is no longer active"})
return
}
if key.QuotaStatus == "exhausted" {
metrics.RecordUserKeyChatRequest("quota_exhausted")
writeHTTPError(w, &httpError{StatusCode: http.StatusForbidden, Code: "quota_exhausted", Message: "API key quota exhausted"})
return
}
// 4. Parse request body (OpenAI-compatible)
body, err := io.ReadAll(io.LimitReader(r.Body, maxJSONBodyBytes))
if err != nil {
metrics.RecordUserKeyChatRequest("bad_request")
writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "cannot read request body"})
return
}
@@ -2745,12 +2757,14 @@ func handlePublicV1ChatCompletions(w http.ResponseWriter, r *http.Request, dsn s
Stream bool `json:"stream,omitempty"`
}
if err := json.Unmarshal(body, &openAIReq); err != nil {
metrics.RecordUserKeyChatRequest("bad_request")
writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "invalid request body"})
return
}
model := strings.TrimSpace(openAIReq.Model)
if model == "" {
metrics.RecordUserKeyChatRequest("bad_request")
writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "model is required"})
return
}
@@ -2771,6 +2785,7 @@ func handlePublicV1ChatCompletions(w http.ResponseWriter, r *http.Request, dsn s
result, err := proxyChat(r.Context(), proxyReq)
if err != nil {
metrics.RecordUserKeyChatRequest("proxy_error")
writeHTTPError(w, classifyError(err))
return
}
@@ -2835,6 +2850,7 @@ func handlePublicV1ChatCompletions(w http.ResponseWriter, r *http.Request, dsn s
}
}
metrics.RecordUserKeyChatRequest("ok")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(respPayload)