feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers

This commit is contained in:
2026-04-02 11:19:50 +08:00
parent e59a77bc49
commit dcc1f186f8
298 changed files with 62603 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
package monitoring_test
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/user-management-system/internal/monitoring"
)
func TestPrometheusHandlerForRegistryExposesBusinessMetrics(t *testing.T) {
gin.SetMode(gin.TestMode)
metrics := monitoring.NewMetrics()
router := gin.New()
router.Use(monitoring.PrometheusMiddleware(metrics))
router.GET("/ready", func(c *gin.Context) {
c.Status(http.StatusNoContent)
})
router.GET("/metrics", gin.WrapH(promhttp.HandlerFor(metrics.GetRegistry(), promhttp.HandlerOpts{})))
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/ready", nil)
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNoContent {
t.Fatalf("expected 204, got %d", recorder.Code)
}
metricsRecorder := httptest.NewRecorder()
metricsRequest := httptest.NewRequest(http.MethodGet, "/metrics", nil)
router.ServeHTTP(metricsRecorder, metricsRequest)
if metricsRecorder.Code != http.StatusOK {
t.Fatalf("expected metrics endpoint to return 200, got %d", metricsRecorder.Code)
}
body := metricsRecorder.Body.String()
if !strings.Contains(body, `http_requests_total{method="GET",path="/ready",status="204"} 1`) {
t.Fatalf("expected recorded request metric in body, got %s", body)
}
if !strings.Contains(body, `http_request_duration_seconds_bucket{method="GET",path="/ready"`) {
t.Fatalf("expected recorded request duration metric in body, got %s", body)
}
}

View File

@@ -0,0 +1,107 @@
package monitoring
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// HealthStatus 健康状态
type HealthStatus string
const (
HealthStatusUP HealthStatus = "UP"
HealthStatusDOWN HealthStatus = "DOWN"
HealthStatusUNKNOWN HealthStatus = "UNKNOWN"
)
// HealthCheck 健康检查器
type HealthCheck struct {
db *gorm.DB
}
// NewHealthCheck 创建健康检查器
func NewHealthCheck(db *gorm.DB) *HealthCheck {
return &HealthCheck{db: db}
}
// Status 健康状态
type Status struct {
Status HealthStatus `json:"status"`
Checks map[string]CheckResult `json:"checks"`
}
// CheckResult 检查结果
type CheckResult struct {
Status HealthStatus `json:"status"`
Error string `json:"error,omitempty"`
}
// Check 执行健康检查
func (h *HealthCheck) Check() *Status {
status := &Status{
Status: HealthStatusUP,
Checks: make(map[string]CheckResult),
}
// 检查数据库
dbResult := h.checkDatabase()
status.Checks["database"] = dbResult
if dbResult.Status != HealthStatusUP {
status.Status = HealthStatusDOWN
}
return status
}
// checkDatabase 检查数据库
func (h *HealthCheck) checkDatabase() CheckResult {
if h == nil || h.db == nil {
return CheckResult{
Status: HealthStatusDOWN,
Error: "database not configured",
}
}
sqlDB, err := h.db.DB()
if err != nil {
return CheckResult{
Status: HealthStatusDOWN,
Error: err.Error(),
}
}
// Ping数据库
if err := sqlDB.Ping(); err != nil {
return CheckResult{
Status: HealthStatusDOWN,
Error: err.Error(),
}
}
return CheckResult{Status: HealthStatusUP}
}
// ReadinessHandler reports dependency readiness.
func (h *HealthCheck) ReadinessHandler(c *gin.Context) {
status := h.Check()
httpStatus := http.StatusOK
if status.Status != HealthStatusUP {
httpStatus = http.StatusServiceUnavailable
}
c.JSON(httpStatus, status)
}
// LivenessHandler reports process liveness without dependency checks.
func (h *HealthCheck) LivenessHandler(c *gin.Context) {
c.Status(http.StatusNoContent)
c.Writer.WriteHeaderNow()
}
// Handler keeps backward compatibility with the historical /health endpoint.
func (h *HealthCheck) Handler(c *gin.Context) {
h.ReadinessHandler(c)
}

View File

@@ -0,0 +1,78 @@
package monitoring_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"github.com/user-management-system/internal/monitoring"
)
func TestHealthCheckReadinessHandlerReturnsServiceUnavailableWhenDatabaseMissing(t *testing.T) {
gin.SetMode(gin.TestMode)
healthCheck := monitoring.NewHealthCheck(nil)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodGet, "/health/ready", nil)
healthCheck.ReadinessHandler(ctx)
if recorder.Code != http.StatusServiceUnavailable {
t.Fatalf("expected 503, got %d", recorder.Code)
}
var status monitoring.Status
if err := json.Unmarshal(recorder.Body.Bytes(), &status); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if status.Status != monitoring.HealthStatusDOWN {
t.Fatalf("expected DOWN, got %s", status.Status)
}
if check := status.Checks["database"]; check.Status != monitoring.HealthStatusDOWN {
t.Fatalf("expected database check to be DOWN, got %s", check.Status)
}
}
func TestHealthCheckReadinessHandlerReturnsOKWhenDatabaseIsReady(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("failed to open sqlite database: %v", err)
}
healthCheck := monitoring.NewHealthCheck(db)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodGet, "/health/ready", nil)
healthCheck.ReadinessHandler(ctx)
if recorder.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", recorder.Code)
}
}
func TestHealthCheckLivenessHandlerReturnsNoContent(t *testing.T) {
gin.SetMode(gin.TestMode)
healthCheck := monitoring.NewHealthCheck(nil)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodGet, "/health/live", nil)
healthCheck.LivenessHandler(ctx)
if recorder.Code != http.StatusNoContent {
t.Fatalf("expected 204, got %d", recorder.Code)
}
if recorder.Body.Len() != 0 {
t.Fatalf("expected empty body, got %q", recorder.Body.String())
}
}

View File

@@ -0,0 +1,206 @@
package monitoring
import (
"strconv"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
)
// Metrics 监控指标
type Metrics struct {
// HTTP请求指标
httpRequestsTotal *prometheus.CounterVec
httpRequestDuration *prometheus.HistogramVec
// 数据库指标
dbQueriesTotal *prometheus.CounterVec
dbQueryDuration *prometheus.HistogramVec
// 用户指标
userRegistrations *prometheus.CounterVec
userLogins *prometheus.CounterVec
activeUsers *prometheus.GaugeVec
// 系统指标
systemMemoryUsage prometheus.Gauge
systemGoroutines prometheus.Gauge
// 私有注册表(测试时互不干扰)
registry *prometheus.Registry
}
// globalMetrics 全局单例(生产使用)
var (
globalMetrics *Metrics
globalMetricsOnce sync.Once
)
// NewMetrics 创建监控指标(每次创建使用独立 registry避免重复注册 panic
func NewMetrics() *Metrics {
reg := prometheus.NewRegistry()
m := &Metrics{registry: reg}
m.httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "path", "status"},
)
m.httpRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "path"},
)
m.dbQueriesTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "db_queries_total",
Help: "Total number of database queries",
},
[]string{"operation", "table"},
)
m.dbQueryDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "db_query_duration_seconds",
Help: "Database query duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"operation", "table"},
)
m.userRegistrations = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "user_registrations_total",
Help: "Total number of user registrations",
},
[]string{"type"},
)
m.userLogins = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "user_logins_total",
Help: "Total number of user logins",
},
[]string{"type", "status"},
)
m.activeUsers = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "active_users",
Help: "Number of active users",
},
[]string{"period"},
)
m.systemMemoryUsage = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "system_memory_usage_bytes",
Help: "Current memory usage in bytes",
},
)
m.systemGoroutines = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "system_goroutines",
Help: "Number of goroutines",
},
)
// 注册到私有 registry
reg.MustRegister(
m.httpRequestsTotal,
m.httpRequestDuration,
m.dbQueriesTotal,
m.dbQueryDuration,
m.userRegistrations,
m.userLogins,
m.activeUsers,
m.systemMemoryUsage,
m.systemGoroutines,
)
return m
}
// GetGlobalMetrics 获取全局单例 Metrics生产使用同时注册到默认 registry
func GetGlobalMetrics() *Metrics {
globalMetricsOnce.Do(func() {
m := NewMetrics()
// 将私有 registry 的指标也注册到默认 registry
prometheus.DefaultRegisterer.Register(m.httpRequestsTotal) //nolint:errcheck
prometheus.DefaultRegisterer.Register(m.httpRequestDuration) //nolint:errcheck
prometheus.DefaultRegisterer.Register(m.dbQueriesTotal) //nolint:errcheck
prometheus.DefaultRegisterer.Register(m.dbQueryDuration) //nolint:errcheck
prometheus.DefaultRegisterer.Register(m.userRegistrations) //nolint:errcheck
prometheus.DefaultRegisterer.Register(m.userLogins) //nolint:errcheck
prometheus.DefaultRegisterer.Register(m.activeUsers) //nolint:errcheck
prometheus.DefaultRegisterer.Register(m.systemMemoryUsage) //nolint:errcheck
prometheus.DefaultRegisterer.Register(m.systemGoroutines) //nolint:errcheck
globalMetrics = m
})
return globalMetrics
}
// GetRegistry 获取私有 Prometheus registry
func (m *Metrics) GetRegistry() *prometheus.Registry {
return m.registry
}
// IncHTTPRequest 记录HTTP请求
func (m *Metrics) IncHTTPRequest(method, path string, status int) {
m.httpRequestsTotal.WithLabelValues(method, path, strconv.Itoa(status)).Inc()
}
// ObserveHTTPRequestDuration 记录HTTP请求耗时
func (m *Metrics) ObserveHTTPRequestDuration(method, path string, duration time.Duration) {
m.httpRequestDuration.WithLabelValues(method, path).Observe(duration.Seconds())
}
// IncDBQuery 记录数据库查询
func (m *Metrics) IncDBQuery(operation, table string) {
m.dbQueriesTotal.WithLabelValues(operation, table).Inc()
}
// ObserveDBQueryDuration 记录数据库查询耗时
func (m *Metrics) ObserveDBQueryDuration(operation, table string, duration time.Duration) {
m.dbQueryDuration.WithLabelValues(operation, table).Observe(duration.Seconds())
}
// IncUserRegistration 记录用户注册
func (m *Metrics) IncUserRegistration(userType string) {
m.userRegistrations.WithLabelValues(userType).Inc()
}
// IncUserLogin 记录用户登录
func (m *Metrics) IncUserLogin(loginType, status string) {
m.userLogins.WithLabelValues(loginType, status).Inc()
}
// SetActiveUsers 设置活跃用户数
func (m *Metrics) SetActiveUsers(period string, count float64) {
m.activeUsers.WithLabelValues(period).Set(count)
}
// SetMemoryUsage 设置内存使用量
func (m *Metrics) SetMemoryUsage(bytes float64) {
m.systemMemoryUsage.Set(bytes)
}
// SetGoroutines 设置协程数
func (m *Metrics) SetGoroutines(count float64) {
m.systemGoroutines.Set(count)
}
// GetMetrics 获取Prometheus指标收集器
func (m *Metrics) GetMetrics() []prometheus.Collector {
return []prometheus.Collector{
m.httpRequestsTotal,
m.httpRequestDuration,
m.dbQueriesTotal,
m.dbQueryDuration,
m.userRegistrations,
m.userLogins,
m.activeUsers,
m.systemMemoryUsage,
m.systemGoroutines,
}
}

View File

@@ -0,0 +1,27 @@
package monitoring
import (
"time"
"github.com/gin-gonic/gin"
)
// PrometheusMiddleware Prometheus监控中间件
func PrometheusMiddleware(metrics *Metrics) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
duration := time.Since(start)
method := c.Request.Method
path := c.FullPath()
status := c.Writer.Status()
// 记录请求数
metrics.IncHTTPRequest(method, path, status)
// 记录请求耗时
metrics.ObserveHTTPRequestDuration(method, path, duration)
}
}

View File

@@ -0,0 +1,91 @@
package monitoring_test
import (
"testing"
"time"
"github.com/user-management-system/internal/monitoring"
)
// TestNewMetrics 测试监控指标初始化
func TestNewMetrics(t *testing.T) {
m := monitoring.NewMetrics()
if m == nil {
t.Fatal("NewMetrics() returned nil")
}
}
// TestMetricsGetCollectors 测试获取 Prometheus 收集器列表不为空
func TestMetricsGetCollectors(t *testing.T) {
m := monitoring.NewMetrics()
collectors := m.GetMetrics()
if len(collectors) == 0 {
t.Error("GetMetrics() should return non-empty collector list")
}
}
// TestIncHTTPRequest 测试HTTP请求计数不 panic
func TestIncHTTPRequest(t *testing.T) {
m := monitoring.NewMetrics()
m.IncHTTPRequest("GET", "/api/v1/users", 200)
m.IncHTTPRequest("POST", "/api/v1/users", 201)
m.IncHTTPRequest("GET", "/api/v1/users", 500)
}
// TestObserveHTTPRequestDuration 测试HTTP请求耗时记录不 panic
func TestObserveHTTPRequestDuration(t *testing.T) {
m := monitoring.NewMetrics()
m.ObserveHTTPRequestDuration("GET", "/api/v1/users", 50*time.Millisecond)
m.ObserveHTTPRequestDuration("POST", "/api/v1/auth/login", 200*time.Millisecond)
}
// TestIncDBQuery 测试数据库查询计数不 panic
func TestIncDBQuery(t *testing.T) {
m := monitoring.NewMetrics()
m.IncDBQuery("SELECT", "users")
m.IncDBQuery("INSERT", "users")
m.IncDBQuery("UPDATE", "users")
m.IncDBQuery("DELETE", "users")
}
// TestObserveDBQueryDuration 测试数据库查询耗时记录不 panic
func TestObserveDBQueryDuration(t *testing.T) {
m := monitoring.NewMetrics()
m.ObserveDBQueryDuration("SELECT", "users", 5*time.Millisecond)
m.ObserveDBQueryDuration("INSERT", "users", 10*time.Millisecond)
}
// TestIncUserRegistration 测试用户注册计数不 panic
func TestIncUserRegistration(t *testing.T) {
m := monitoring.NewMetrics()
m.IncUserRegistration("normal")
m.IncUserRegistration("oauth")
}
// TestIncUserLogin 测试用户登录计数不 panic
func TestIncUserLogin(t *testing.T) {
m := monitoring.NewMetrics()
m.IncUserLogin("password", "success")
m.IncUserLogin("password", "fail")
m.IncUserLogin("oauth", "success")
}
// TestSetActiveUsers 测试活跃用户数设置不 panic
func TestSetActiveUsers(t *testing.T) {
m := monitoring.NewMetrics()
m.SetActiveUsers("daily", 1000)
m.SetActiveUsers("weekly", 5000)
}
// TestSetMemoryUsage 测试内存使用量设置不 panic
func TestSetMemoryUsage(t *testing.T) {
m := monitoring.NewMetrics()
m.SetMemoryUsage(1024 * 1024 * 100) // 100MB
}
// TestSetGoroutines 测试协程数设置不 panic
func TestSetGoroutines(t *testing.T) {
m := monitoring.NewMetrics()
m.SetGoroutines(50)
m.SetGoroutines(100)
}