5759c1862e
M0-M7 已完成:核心网关(身份/RBAC/TOTP/OIDC/SAML/Provider/配额/路由/内容策略/审计/定价)+ 资源市场(MCP/Skills/数字员工)。 含 22 个 PostgreSQL 迁移、管理端/门户端前端源码、OpenAPI 契约、部署 compose。 Co-Authored-By: Claude <noreply@anthropic.com>
114 lines
3.8 KiB
Go
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)
|
|
}
|