Files
ai-gateway-go/internal/platform/httpserver/server.go
T
superidou 5759c1862e AI Gateway Go 0.10.0 源码快照 + 旗舰版需求规划报告
M0-M7 已完成:核心网关(身份/RBAC/TOTP/OIDC/SAML/Provider/配额/路由/内容策略/审计/定价)+ 资源市场(MCP/Skills/数字员工)。
含 22 个 PostgreSQL 迁移、管理端/门户端前端源码、OpenAPI 契约、部署 compose。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 11:45:54 +08:00

114 lines
3.8 KiB
Go

package httpserver
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"runtime/debug"
"sync/atomic"
"time"
"aigateway.local/core/internal/gateway"
"aigateway.local/core/internal/platform/config"
"aigateway.local/core/internal/platform/health"
)
type Dependencies struct {
Config config.Config
Logger *slog.Logger
Checker health.Checker
Gateway http.Handler
Control http.Handler
Version string
StartedAt time.Time
BootstrapUses func() uint64
ExtraMetrics func() string
}
type metrics struct {
requests atomic.Uint64
panics atomic.Uint64
}
func New(dependencies Dependencies) *http.Server {
mux := http.NewServeMux()
stats := &metrics{}
mux.HandleFunc("GET /healthz", func(writer http.ResponseWriter, _ *http.Request) {
writeJSON(writer, http.StatusOK, map[string]any{"status": "ok", "version": dependencies.Version})
})
mux.HandleFunc("GET /readyz", func(writer http.ResponseWriter, request *http.Request) {
report := dependencies.Checker.Readiness(request.Context())
status := http.StatusOK
if !report.Ready {
status = http.StatusServiceUnavailable
}
writeJSON(writer, status, report)
})
mux.HandleFunc("GET /metrics", func(writer http.ResponseWriter, _ *http.Request) {
writer.Header().Set("Content-Type", "text/plain; version=0.0.4")
bootstrapUses := uint64(0)
if dependencies.BootstrapUses != nil {
bootstrapUses = dependencies.BootstrapUses()
}
_, _ = fmt.Fprintf(writer, "gateway_http_requests_total %d\ngateway_http_panics_total %d\ngateway_bootstrap_api_key_uses_total %d\ngateway_uptime_seconds %.0f\n", stats.requests.Load(), stats.panics.Load(), bootstrapUses, time.Since(dependencies.StartedAt).Seconds())
if dependencies.ExtraMetrics != nil {
_, _ = fmt.Fprint(writer, dependencies.ExtraMetrics())
}
})
mux.Handle("/v1/", dependencies.Gateway)
if dependencies.Control != nil {
mux.Handle("/api/", dependencies.Control)
}
handler := recoverMiddleware(dependencies.Logger, stats, requestIDMiddleware(stats, mux))
return &http.Server{
Addr: dependencies.Config.Server.Address,
Handler: handler,
ReadHeaderTimeout: dependencies.Config.Server.ReadHeaderTimeout,
IdleTimeout: dependencies.Config.Server.IdleTimeout,
// WriteTimeout intentionally remains zero: SSE responses may be long lived.
}
}
func requestIDMiddleware(stats *metrics, next http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
stats.requests.Add(1)
requestID := request.Header.Get("X-Request-ID")
if requestID == "" || len(requestID) > 128 {
requestID = newRequestID()
}
writer.Header().Set("X-Request-ID", requestID)
next.ServeHTTP(writer, request.WithContext(gateway.WithRequestID(request.Context(), requestID)))
})
}
func recoverMiddleware(logger *slog.Logger, stats *metrics, next http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
defer func() {
if recovered := recover(); recovered != nil {
stats.panics.Add(1)
logger.Error("http handler panic", "request_id", gateway.RequestID(request.Context()), "panic", recovered, "stack", string(debug.Stack()))
writeJSON(writer, http.StatusInternalServerError, map[string]string{"error": "internal server error"})
}
}()
next.ServeHTTP(writer, request)
})
}
func writeJSON(writer http.ResponseWriter, status int, value any) {
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(status)
_ = json.NewEncoder(writer).Encode(value)
}
func newRequestID() string {
buffer := make([]byte, 12)
if _, err := rand.Read(buffer); err != nil {
return fmt.Sprintf("req_%x", time.Now().UnixNano())
}
return "req_" + hex.EncodeToString(buffer)
}