mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-05 23:03:48 +00:00
c2c75d69c0
Performance / resource usage: - circuit_breaker_metrics: fix data race on failCounters map (RWMutex + double-checked locking) - server.go: drop user_id and op_name metric labels (Prometheus cardinality bound); de-duplicate extractUserInfo - graphql.go: gate runtime.ReadMemStats per-request behind ENABLE_ALLOCATION_TRACKING flag (default off) - graphql.go: collapse two-pass AST scan into single pass; lower-case once - sanitization.go: cache compiled redaction regexes per pattern via sync.Map; hoist inner constants to pkg vars - proxy.go: hoist connection/timeout substrings to pkg vars; sentinel errors for static error paths; drop dead Headers map alloc - metrics_aggregator.go: log-field allocation guarded by Logger.IsLevelEnabled - logging/logger.go: add IsLevelEnabled helper - lru_cache.go: 16-shard sharding, FNV-1a routing (concurrent throughput +22%) - cache/memory/lru_memory_cache.go: gzip compress/decompress moved outside mu.Lock - rps_tracker.go: RWMutex+uint64 -> atomic.Uint64 - retry_budget.go: drop unused mutex - api.go: bannedUsersIDs map+RWMutex -> sync.Map (+ snapshot/replace helpers) - tracing/tracing.go: pkg-level constSpanAttrs, copy-then-append in StartSpanWithAttributes - admin_dashboard.go: handleStatsWebSocket reuses bytes.Buffer + json.Encoder per connection Build / runtime: - Makefile: -ldflags="-s -w" -trimpath, CGO_ENABLED=0 for build (=1 for test recipes) - Dockerfile + Dockerfile.goreleaser: ENV GOMEMLIMIT=512MiB - main.go: blank import go.uber.org/automaxprocs (cgroup-aware GOMAXPROCS) - main.go: PPROF_PORT env var wires net/http/pprof on 127.0.0.1 only with full server timeouts - README.md: env-var docs + metric-label docs updated; cardinality note Test coverage push (per package): - main 51.2% -> 74.7% - cache 66.3% -> 93.7% - cache/redis 45.5% -> 98.2% - tracing 66.7% -> 72.9% - (cache/memory 91.6%, logging 91.9%, monitoring 77.6%, pkg/pools 100% unchanged) New test files: coverage_micro_test, coverage_extras_test, server_handlers_test, api_health_test, admin_dashboard_cluster_test, metrics_aggregator_test, concerns_test, cache/cache_coverage_test, cache/redis/redis_coverage_test, tracing/tracing_coverage_test. Bug fix: connection_resilience_test.go TestIntegratedHealthManagement.health_manager_startup was sync.Once-coupled to InitializeBackendHealth and panicked when another test (e.g. via parseConfig) had already triggered Once. Use NewBackendHealthManager directly.
248 lines
7.2 KiB
Go
248 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
|
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// newClusterApp registers all cluster + control routes on a fresh Fiber app.
|
|
func newClusterApp(t *testing.T) (*fiber.App, *AdminDashboard) {
|
|
t.Helper()
|
|
app := fiber.New()
|
|
logger := libpack_logger.New()
|
|
dashboard := NewAdminDashboard(logger)
|
|
dashboard.RegisterRoutes(app)
|
|
return app, dashboard
|
|
}
|
|
|
|
// ensureNilAggregator guarantees no metrics aggregator is active for the test
|
|
// and restores the original value after.
|
|
func ensureNilAggregator(t *testing.T) {
|
|
t.Helper()
|
|
aggregatorMutex.Lock()
|
|
orig := metricsAggregator
|
|
metricsAggregator = nil
|
|
aggregatorMutex.Unlock()
|
|
t.Cleanup(func() {
|
|
aggregatorMutex.Lock()
|
|
metricsAggregator = orig
|
|
aggregatorMutex.Unlock()
|
|
})
|
|
}
|
|
|
|
// ---- getClusterStats -------------------------------------------------------
|
|
|
|
func TestGetClusterStats_NoAggregator_Returns503(t *testing.T) {
|
|
ensureNilAggregator(t)
|
|
app, _ := newClusterApp(t)
|
|
|
|
req := httptest.NewRequest("GET", "/admin/api/cluster/stats", nil)
|
|
resp, err := app.Test(req)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 503, resp.StatusCode)
|
|
|
|
var body map[string]any
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
assert.NoError(t, json.Unmarshal(raw, &body))
|
|
assert.Equal(t, false, body["cluster_mode"])
|
|
assert.NotEmpty(t, body["error"])
|
|
}
|
|
|
|
// ---- getClusterInstances ---------------------------------------------------
|
|
|
|
func TestGetClusterInstances_NoAggregator_Returns503(t *testing.T) {
|
|
ensureNilAggregator(t)
|
|
app, _ := newClusterApp(t)
|
|
|
|
req := httptest.NewRequest("GET", "/admin/api/cluster/instances", nil)
|
|
resp, err := app.Test(req)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 503, resp.StatusCode)
|
|
|
|
var body map[string]any
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
assert.NoError(t, json.Unmarshal(raw, &body))
|
|
assert.Equal(t, false, body["cluster_mode"])
|
|
assert.NotEmpty(t, body["error"])
|
|
}
|
|
|
|
// ---- getClusterDebug -------------------------------------------------------
|
|
|
|
func TestGetClusterDebug_NoAggregator_Returns200WithFalseFlag(t *testing.T) {
|
|
ensureNilAggregator(t)
|
|
// also set cfg so the redis_cache_enabled branch is exercised
|
|
cfg = &config{
|
|
Logger: libpack_logger.New(),
|
|
}
|
|
cfg.Cache.CacheEnable = true
|
|
cfg.Cache.CacheRedisEnable = false
|
|
|
|
app, _ := newClusterApp(t)
|
|
|
|
req := httptest.NewRequest("GET", "/admin/api/cluster/debug", nil)
|
|
resp, err := app.Test(req)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 200, resp.StatusCode)
|
|
|
|
var body map[string]any
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
assert.NoError(t, json.Unmarshal(raw, &body))
|
|
assert.Equal(t, false, body["aggregator_initialized"])
|
|
assert.Equal(t, false, body["redis_cache_enabled"])
|
|
assert.Equal(t, true, body["cache_enabled"])
|
|
}
|
|
|
|
func TestGetClusterDebug_NilCfg_Returns200WithDefaults(t *testing.T) {
|
|
ensureNilAggregator(t)
|
|
orig := cfg
|
|
cfg = nil
|
|
defer func() { cfg = orig }()
|
|
|
|
app, _ := newClusterApp(t)
|
|
|
|
req := httptest.NewRequest("GET", "/admin/api/cluster/debug", nil)
|
|
resp, err := app.Test(req)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 200, resp.StatusCode)
|
|
|
|
var body map[string]any
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
assert.NoError(t, json.Unmarshal(raw, &body))
|
|
assert.Equal(t, false, body["aggregator_initialized"])
|
|
assert.Equal(t, false, body["redis_cache_enabled"])
|
|
}
|
|
|
|
// ---- forcePublish ----------------------------------------------------------
|
|
|
|
func TestForcePublish_NoAggregator_Returns503(t *testing.T) {
|
|
ensureNilAggregator(t)
|
|
app, _ := newClusterApp(t)
|
|
|
|
req := httptest.NewRequest("POST", "/admin/api/cluster/force-publish", nil)
|
|
resp, err := app.Test(req)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 503, resp.StatusCode)
|
|
|
|
var body map[string]any
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
assert.NoError(t, json.Unmarshal(raw, &body))
|
|
assert.Equal(t, false, body["success"])
|
|
assert.NotEmpty(t, body["error"])
|
|
}
|
|
|
|
// ---- gatherAllStats / gatherAllStatsWithMode / gatherAllStatsClusterAware --
|
|
|
|
func newDashboardForGather(t *testing.T) *AdminDashboard {
|
|
t.Helper()
|
|
logger := libpack_logger.New()
|
|
monitoring := libpack_monitoring.NewMonitoring(&libpack_monitoring.InitConfig{})
|
|
cfg = &config{
|
|
Logger: logger,
|
|
Monitoring: monitoring,
|
|
}
|
|
return NewAdminDashboard(logger)
|
|
}
|
|
|
|
func TestGatherAllStats_ReturnsExpectedTopLevelKeys(t *testing.T) {
|
|
ensureNilAggregator(t)
|
|
ad := newDashboardForGather(t)
|
|
|
|
result := ad.gatherAllStats()
|
|
assert.NotNil(t, result)
|
|
|
|
// cluster_mode must be false when no aggregator
|
|
assert.Equal(t, false, result["cluster_mode"])
|
|
|
|
// stats sub-map must exist
|
|
statsRaw, ok := result["stats"]
|
|
assert.True(t, ok, "stats key must be present")
|
|
stats, ok := statsRaw.(map[string]any)
|
|
assert.True(t, ok)
|
|
assert.NotEmpty(t, stats["timestamp"])
|
|
assert.NotNil(t, stats["uptime_seconds"])
|
|
assert.NotNil(t, stats["uptime_human"])
|
|
assert.NotEmpty(t, stats["version"])
|
|
assert.NotNil(t, stats["requests"])
|
|
|
|
// health sub-map must exist
|
|
healthRaw, ok := result["health"]
|
|
assert.True(t, ok, "health key must be present")
|
|
health, ok := healthRaw.(map[string]any)
|
|
assert.True(t, ok)
|
|
assert.NotNil(t, health["status"])
|
|
assert.NotNil(t, health["backend"])
|
|
}
|
|
|
|
func TestGatherAllStatsWithMode_FalseMode_ReturnsLocalStats(t *testing.T) {
|
|
ensureNilAggregator(t)
|
|
ad := newDashboardForGather(t)
|
|
|
|
result := ad.gatherAllStatsWithMode(false)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, false, result["cluster_mode"])
|
|
assert.NotNil(t, result["stats"])
|
|
assert.NotNil(t, result["health"])
|
|
}
|
|
|
|
func TestGatherAllStatsWithMode_TrueModeNoAggregator_FallsBackToLocal(t *testing.T) {
|
|
ensureNilAggregator(t)
|
|
ad := newDashboardForGather(t)
|
|
|
|
// With no aggregator, cluster mode request must fall back to local stats.
|
|
result := ad.gatherAllStatsWithMode(true)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, false, result["cluster_mode"])
|
|
}
|
|
|
|
func TestGatherAllStatsClusterAware_NoAggregator_FallsBackToLocal(t *testing.T) {
|
|
ensureNilAggregator(t)
|
|
ad := newDashboardForGather(t)
|
|
|
|
result := ad.gatherAllStatsClusterAware()
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, false, result["cluster_mode"])
|
|
}
|
|
|
|
func TestGatherAllStats_NilCfg_ReturnsStatsWithoutRequests(t *testing.T) {
|
|
ensureNilAggregator(t)
|
|
origCfg := cfg
|
|
cfg = nil
|
|
defer func() { cfg = origCfg }()
|
|
|
|
ad := NewAdminDashboard(nil)
|
|
|
|
result := ad.gatherAllStats()
|
|
assert.NotNil(t, result)
|
|
stats, ok := result["stats"].(map[string]any)
|
|
assert.True(t, ok)
|
|
// when cfg is nil, "requests" key must NOT be present
|
|
_, hasRequests := stats["requests"]
|
|
assert.False(t, hasRequests)
|
|
}
|
|
|
|
func TestGatherAllStats_RequestStatsShape(t *testing.T) {
|
|
ensureNilAggregator(t)
|
|
ad := newDashboardForGather(t)
|
|
|
|
result := ad.gatherAllStats()
|
|
stats := result["stats"].(map[string]any)
|
|
requests, ok := stats["requests"].(map[string]any)
|
|
assert.True(t, ok, "requests must be a map")
|
|
assert.NotNil(t, requests["total"])
|
|
assert.NotNil(t, requests["succeeded"])
|
|
assert.NotNil(t, requests["failed"])
|
|
assert.NotNil(t, requests["skipped"])
|
|
assert.NotNil(t, requests["success_rate_pct"])
|
|
assert.NotNil(t, requests["failure_rate_pct"])
|
|
assert.NotNil(t, requests["skip_rate_pct"])
|
|
assert.NotNil(t, requests["avg_requests_per_second"])
|
|
assert.NotNil(t, requests["current_requests_per_second"])
|
|
}
|