feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
This commit is contained in:
47
internal/monitoring/exposure_test.go
Normal file
47
internal/monitoring/exposure_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
107
internal/monitoring/health.go
Normal file
107
internal/monitoring/health.go
Normal 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)
|
||||
}
|
||||
78
internal/monitoring/health_test.go
Normal file
78
internal/monitoring/health_test.go
Normal 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())
|
||||
}
|
||||
}
|
||||
206
internal/monitoring/metrics.go
Normal file
206
internal/monitoring/metrics.go
Normal 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,
|
||||
}
|
||||
}
|
||||
27
internal/monitoring/middleware.go
Normal file
27
internal/monitoring/middleware.go
Normal 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)
|
||||
}
|
||||
}
|
||||
91
internal/monitoring/monitoring_test.go
Normal file
91
internal/monitoring/monitoring_test.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user