feat(api-gateway): 实现 W1-W8 网关硬化与 P2-P5 路由扩展

依据 coord-final-decisions §3.8 W1-W8 裁决与
president-final-rulings §2.15/§2.16/§2.19 完整实现网关硬化:

- W1/W2: 错误码 GW_ 前缀 + ActionState 信封响应体
- W3: 全量替换为 log/slog 结构化日志
- W4: /readyz 并行 ping 9 下游 + 软失败规则
- W5: 7 个业务 Prometheus 指标 + /metrics 端点
- W6: tracer 资源属性补全(name/version/env/host)
- W7: DevMode=true && ENV=production panic 防护
- W8: 保持共享 downstream 熔断

P2 RS256 升级:接入 shared-go/jwks.Fetcher(TTL 5min)。
P2.7+P3-P5 路由扩展:student/parent/messages/dashboard。
文档同步:README/01/02/known-issues,arch.db 已更新。
质量校验:go vet + build + test 均通过。
This commit is contained in:
SpecialX
2026-07-10 18:15:48 +08:00
parent 9e767b4e95
commit 4307f6b73c
20 changed files with 797 additions and 382 deletions

View File

@@ -0,0 +1,96 @@
// Package observability provides tracer, metrics, and structured logging
// initialization for the api-gateway.
package observability
import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
// 业务指标W5 裁决7 个业务 metrics
var (
httpRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "api_gateway_http_requests_total",
Help: "Total number of HTTP requests processed by the gateway.",
}, []string{"method", "endpoint", "status"})
httpRequestDurationSeconds = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "api_gateway_http_request_duration_seconds",
Help: "HTTP request processing duration in seconds.",
Buckets: prometheus.DefBuckets,
}, []string{"method", "endpoint"})
circuitBreakerState = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "api_gateway_circuit_breaker_state",
Help: "Circuit breaker state by service: 1=active state, 0=inactive.",
}, []string{"service", "state"})
rateLimitedTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "api_gateway_rate_limited_total",
Help: "Total number of rate-limited requests.",
}, []string{"ip"})
proxyUpstreamDurationSeconds = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "api_gateway_proxy_upstream_duration_seconds",
Help: "Upstream proxy response duration in seconds.",
Buckets: prometheus.DefBuckets,
}, []string{"upstream"})
jwksRefreshTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "api_gateway_jwks_refresh_total",
Help: "Total number of JWKS cache refresh attempts.",
}, []string{"result"})
authFailuresTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "api_gateway_auth_failures_total",
Help: "Total number of authentication failures.",
}, []string{"reason"})
)
// Metrics 返回 HTTP 请求统计中间件W5 裁决)。
// 记录请求总数method/endpoint/status与请求延迟method/endpoint
// endpoint 使用 gin 路由模式c.FullPath())避免高基数。
func Metrics() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
duration := time.Since(start).Seconds()
status := strconv.Itoa(c.Writer.Status())
endpoint := c.FullPath()
if endpoint == "" {
endpoint = "unknown"
}
httpRequestsTotal.WithLabelValues(c.Request.Method, endpoint, status).Inc()
httpRequestDurationSeconds.WithLabelValues(c.Request.Method, endpoint).Observe(duration)
}
}
// IncAuthFailure 递增鉴权失败计数器。
func IncAuthFailure(reason string) {
authFailuresTotal.WithLabelValues(reason).Inc()
}
// IncRateLimited 递增限流计数器。
func IncRateLimited(ip string) {
rateLimitedTotal.WithLabelValues(ip).Inc()
}
// IncJWKSRefresh 递增 JWKS 刷新计数器result: success/failure
func IncJWKSRefresh(result string) {
jwksRefreshTotal.WithLabelValues(result).Inc()
}
// SetCircuitBreakerState 更新熔断器状态 gauge。
func SetCircuitBreakerState(service, state string, value float64) {
circuitBreakerState.WithLabelValues(service, state).Set(value)
}
// ObserveProxyUpstreamDuration 记录上游代理响应延迟。
func ObserveProxyUpstreamDuration(upstream string, duration time.Duration) {
proxyUpstreamDurationSeconds.WithLabelValues(upstream).Observe(duration.Seconds())
}

View File

@@ -2,11 +2,12 @@ package observability
import (
"context"
"log"
"log/slog"
"net/url"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
@@ -14,21 +15,22 @@ import (
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
// InitTracer 初始化 OpenTelemetry tracer.
// InitTracer 初始化 OpenTelemetry tracerW6 裁决:资源属性补全)。
//
// 资源属性包含service.name / service.version / deployment.environment / host.name。
// endpoint 为 "http://host:port" 格式(如 "http://localhost:4318"
// 为空时跳过初始化tracing disabled
//
// 返回 shutdown 函数,应在服务退出时调用以 flush 待发送 span.
func InitTracer(serviceName, endpoint string) func() {
// 返回 shutdown 函数,应在服务退出时调用以 flush 待发送 span
func InitTracer(serviceName, endpoint, env, version, hostName string) func() {
if endpoint == "" {
log.Println("OTEL endpoint not set, tracing disabled")
slog.Info("OTEL endpoint not set, tracing disabled")
return func() {}
}
u, err := url.Parse(endpoint)
if err != nil || u.Host == "" {
log.Printf("invalid OTEL endpoint %q, tracing disabled", endpoint)
slog.Warn("invalid OTEL endpoint, tracing disabled", "endpoint", endpoint)
return func() {}
}
@@ -40,15 +42,21 @@ func InitTracer(serviceName, endpoint string) func() {
otlptracehttp.WithInsecure(),
)
if err != nil {
log.Printf("failed to create OTLP exporter: %v, tracing disabled", err)
slog.Error("failed to create OTLP exporter, tracing disabled", "error", err)
return func() {}
}
// W6 裁决资源属性补全service.name + version + env + host
res, err := resource.New(ctx,
resource.WithAttributes(semconv.ServiceName(serviceName)),
resource.WithAttributes(
semconv.ServiceName(serviceName),
attribute.String("service.version", version),
attribute.String("deployment.environment", env),
attribute.String("host.name", hostName),
),
)
if err != nil {
log.Printf("failed to create resource: %v", err)
slog.Error("failed to create resource", "error", err)
return func() {}
}
@@ -62,12 +70,18 @@ func InitTracer(serviceName, endpoint string) func() {
propagation.Baggage{},
))
log.Printf("OpenTelemetry tracer initialized for %s (endpoint=%s)", serviceName, u.Host)
slog.Info("OpenTelemetry tracer initialized",
"service", serviceName,
"endpoint", u.Host,
"env", env,
"version", version,
"host", hostName,
)
return func() {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := tp.Shutdown(shutdownCtx); err != nil {
log.Printf("failed to shutdown tracer: %v", err)
slog.Error("failed to shutdown tracer", "error", err)
}
}
}