mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-22 06:38:13 +00:00
improvements mid apr 2025 (#26)
* General improvements and bug fixes. * Admin dashboard
This commit is contained in:
+107
-7
@@ -2,9 +2,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,23 +69,92 @@ func (ad *AdminDashboard) serveDashboard(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// getStats returns overall proxy statistics
|
// getStats returns overall proxy statistics
|
||||||
func (ad *AdminDashboard) getStats(c *fiber.Ctx) error {
|
func (ad *AdminDashboard) getStats(c *fiber.Ctx) error {
|
||||||
|
uptimeSeconds := time.Since(startTime).Seconds()
|
||||||
stats := map[string]interface{}{
|
stats := map[string]interface{}{
|
||||||
"timestamp": time.Now().Format(time.RFC3339),
|
"timestamp": time.Now().Format(time.RFC3339),
|
||||||
"uptime": time.Since(startTime).Seconds(),
|
"uptime_seconds": uptimeSeconds,
|
||||||
"version": "0.27.0", // TODO: Get from build info
|
"uptime_human": formatDuration(time.Since(startTime)),
|
||||||
|
"version": "0.27.0", // TODO: Get from build info
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg != nil && cfg.Monitoring != nil {
|
if cfg != nil && cfg.Monitoring != nil {
|
||||||
stats["metrics"] = map[string]interface{}{
|
succeeded := getAdminMetricValue("graphql_proxy_succeeded_total")
|
||||||
"succeeded": getAdminMetricValue("graphql_proxy_succeeded_total"),
|
failed := getAdminMetricValue("graphql_proxy_failed_total")
|
||||||
"failed": getAdminMetricValue("graphql_proxy_failed_total"),
|
skipped := getAdminMetricValue("graphql_proxy_skipped_total")
|
||||||
"skipped": getAdminMetricValue("graphql_proxy_skipped_total"),
|
total := succeeded + failed + skipped
|
||||||
|
|
||||||
|
// Request statistics
|
||||||
|
requestStats := map[string]interface{}{
|
||||||
|
"total": total,
|
||||||
|
"succeeded": succeeded,
|
||||||
|
"failed": failed,
|
||||||
|
"skipped": skipped,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate rates and percentages
|
||||||
|
if total > 0 {
|
||||||
|
requestStats["success_rate_pct"] = float64(succeeded) / float64(total) * 100
|
||||||
|
requestStats["failure_rate_pct"] = float64(failed) / float64(total) * 100
|
||||||
|
requestStats["skip_rate_pct"] = float64(skipped) / float64(total) * 100
|
||||||
|
} else {
|
||||||
|
requestStats["success_rate_pct"] = 0.0
|
||||||
|
requestStats["failure_rate_pct"] = 0.0
|
||||||
|
requestStats["skip_rate_pct"] = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate average requests per second (lifetime)
|
||||||
|
if uptimeSeconds > 0 {
|
||||||
|
requestStats["avg_requests_per_second"] = float64(total) / uptimeSeconds
|
||||||
|
} else {
|
||||||
|
requestStats["avg_requests_per_second"] = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current requests per second (last 1 second)
|
||||||
|
if rpsTracker := GetRPSTracker(); rpsTracker != nil {
|
||||||
|
requestStats["current_requests_per_second"] = rpsTracker.GetCurrentRPS()
|
||||||
|
} else {
|
||||||
|
requestStats["current_requests_per_second"] = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
stats["requests"] = requestStats
|
||||||
|
|
||||||
|
// Get cache statistics summary
|
||||||
|
cacheStats := libpack_cache.GetCacheStats()
|
||||||
|
if cacheStats != nil {
|
||||||
|
totalCacheRequests := cacheStats.CacheHits + cacheStats.CacheMisses
|
||||||
|
hitRate := 0.0
|
||||||
|
if totalCacheRequests > 0 {
|
||||||
|
hitRate = float64(cacheStats.CacheHits) / float64(totalCacheRequests) * 100
|
||||||
|
}
|
||||||
|
stats["cache_summary"] = map[string]interface{}{
|
||||||
|
"hits": cacheStats.CacheHits,
|
||||||
|
"misses": cacheStats.CacheMisses,
|
||||||
|
"hit_rate_pct": hitRate,
|
||||||
|
"total_cached": cacheStats.CachedQueries,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(stats)
|
return c.JSON(stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatDuration formats a duration into human-readable format
|
||||||
|
func formatDuration(d time.Duration) string {
|
||||||
|
days := int(d.Hours() / 24)
|
||||||
|
hours := int(d.Hours()) % 24
|
||||||
|
minutes := int(d.Minutes()) % 60
|
||||||
|
seconds := int(d.Seconds()) % 60
|
||||||
|
|
||||||
|
if days > 0 {
|
||||||
|
return fmt.Sprintf("%dd %dh %dm %ds", days, hours, minutes, seconds)
|
||||||
|
} else if hours > 0 {
|
||||||
|
return fmt.Sprintf("%dh %dm %ds", hours, minutes, seconds)
|
||||||
|
} else if minutes > 0 {
|
||||||
|
return fmt.Sprintf("%dm %ds", minutes, seconds)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%ds", seconds)
|
||||||
|
}
|
||||||
|
|
||||||
// getHealth returns health status
|
// getHealth returns health status
|
||||||
func (ad *AdminDashboard) getHealth(c *fiber.Ctx) error {
|
func (ad *AdminDashboard) getHealth(c *fiber.Ctx) error {
|
||||||
healthMgr := GetBackendHealthManager()
|
healthMgr := GetBackendHealthManager()
|
||||||
@@ -154,6 +225,35 @@ func (ad *AdminDashboard) getCacheStats(c *fiber.Ctx) error {
|
|||||||
stats["ttl_seconds"] = cfg.Cache.CacheTTL
|
stats["ttl_seconds"] = cfg.Cache.CacheTTL
|
||||||
stats["max_memory_mb"] = cfg.Cache.CacheMaxMemorySize
|
stats["max_memory_mb"] = cfg.Cache.CacheMaxMemorySize
|
||||||
stats["max_entries"] = cfg.Cache.CacheMaxEntries
|
stats["max_entries"] = cfg.Cache.CacheMaxEntries
|
||||||
|
|
||||||
|
// Get runtime cache statistics
|
||||||
|
cacheStats := libpack_cache.GetCacheStats()
|
||||||
|
if cacheStats != nil {
|
||||||
|
stats["cached_queries"] = cacheStats.CachedQueries
|
||||||
|
stats["cache_hits"] = cacheStats.CacheHits
|
||||||
|
stats["cache_misses"] = cacheStats.CacheMisses
|
||||||
|
|
||||||
|
// Calculate hit rate
|
||||||
|
totalRequests := cacheStats.CacheHits + cacheStats.CacheMisses
|
||||||
|
hitRate := 0.0
|
||||||
|
if totalRequests > 0 {
|
||||||
|
hitRate = float64(cacheStats.CacheHits) / float64(totalRequests) * 100
|
||||||
|
}
|
||||||
|
stats["hit_rate_pct"] = hitRate
|
||||||
|
|
||||||
|
// Get memory usage
|
||||||
|
memoryUsage := libpack_cache.GetCacheMemoryUsage()
|
||||||
|
maxMemory := libpack_cache.GetCacheMaxMemorySize()
|
||||||
|
stats["memory_usage_bytes"] = memoryUsage
|
||||||
|
stats["memory_usage_mb"] = float64(memoryUsage) / (1024 * 1024)
|
||||||
|
|
||||||
|
// Calculate memory usage percentage
|
||||||
|
memoryUsagePct := 0.0
|
||||||
|
if maxMemory > 0 {
|
||||||
|
memoryUsagePct = float64(memoryUsage) / float64(maxMemory) * 100
|
||||||
|
}
|
||||||
|
stats["memory_usage_pct"] = memoryUsagePct
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(stats)
|
return c.JSON(stats)
|
||||||
|
|||||||
+12
-1
@@ -110,8 +110,19 @@ func TestAdminDashboard_GetStats(t *testing.T) {
|
|||||||
|
|
||||||
// Verify stats structure
|
// Verify stats structure
|
||||||
assert.NotEmpty(t, stats["timestamp"])
|
assert.NotEmpty(t, stats["timestamp"])
|
||||||
assert.NotNil(t, stats["uptime"])
|
assert.NotNil(t, stats["uptime_seconds"])
|
||||||
|
assert.NotNil(t, stats["uptime_human"])
|
||||||
assert.NotEmpty(t, stats["version"])
|
assert.NotEmpty(t, stats["version"])
|
||||||
|
assert.NotNil(t, stats["requests"])
|
||||||
|
|
||||||
|
// Verify request stats structure
|
||||||
|
requests := stats["requests"].(map[string]interface{})
|
||||||
|
assert.NotNil(t, requests["total"])
|
||||||
|
assert.NotNil(t, requests["succeeded"])
|
||||||
|
assert.NotNil(t, requests["failed"])
|
||||||
|
assert.NotNil(t, requests["success_rate_pct"])
|
||||||
|
assert.NotNil(t, requests["avg_requests_per_second"])
|
||||||
|
assert.NotNil(t, requests["current_requests_per_second"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAdminDashboard_GetHealth(t *testing.T) {
|
func TestAdminDashboard_GetHealth(t *testing.T) {
|
||||||
|
|||||||
@@ -387,6 +387,12 @@ func parseConfig() {
|
|||||||
healthMgr.StartHealthChecking()
|
healthMgr.StartHealthChecking()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize RPS tracker for real-time requests per second monitoring
|
||||||
|
InitializeRPSTracker()
|
||||||
|
cfg.Logger.Info(&libpack_logging.LogMessage{
|
||||||
|
Message: "RPS tracker initialized",
|
||||||
|
})
|
||||||
|
|
||||||
// Load rate limit configuration with improved error handling
|
// Load rate limit configuration with improved error handling
|
||||||
if err := loadRatelimitConfig(); err != nil {
|
if err := loadRatelimitConfig(); err != nil {
|
||||||
// Log the error with clear guidance
|
// Log the error with clear guidance
|
||||||
|
|||||||
@@ -236,6 +236,11 @@ func createFasthttpClient(clientConfig *config) *fasthttp.Client {
|
|||||||
|
|
||||||
// proxyTheRequest handles the request proxying logic.
|
// proxyTheRequest handles the request proxying logic.
|
||||||
func proxyTheRequest(c *fiber.Ctx, currentEndpoint string) error {
|
func proxyTheRequest(c *fiber.Ctx, currentEndpoint string) error {
|
||||||
|
// Record request for RPS tracking
|
||||||
|
if rpsTracker := GetRPSTracker(); rpsTracker != nil {
|
||||||
|
rpsTracker.RecordRequest()
|
||||||
|
}
|
||||||
|
|
||||||
// Setup tracing if enabled
|
// Setup tracing if enabled
|
||||||
var span trace.Span
|
var span trace.Span
|
||||||
var ctx context.Context
|
var ctx context.Context
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RPSTracker tracks requests per second using periodic sampling
|
||||||
|
type RPSTracker struct {
|
||||||
|
lastCount atomic.Int64
|
||||||
|
lastSampleTime atomic.Int64 // Unix nano
|
||||||
|
currentRPS uint64 // stored as uint64, accessed with atomic operations
|
||||||
|
mu sync.RWMutex // for currentRPS updates
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRPSTracker creates a new RPS tracker
|
||||||
|
func NewRPSTracker() *RPSTracker {
|
||||||
|
tracker := &RPSTracker{}
|
||||||
|
tracker.lastSampleTime.Store(time.Now().UnixNano())
|
||||||
|
go tracker.updateLoop()
|
||||||
|
return tracker
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordRequest increments the request counter
|
||||||
|
func (r *RPSTracker) RecordRequest() {
|
||||||
|
// Just increment the counter, sampling happens in background
|
||||||
|
r.lastCount.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateLoop periodically calculates current RPS
|
||||||
|
func (r *RPSTracker) updateLoop() {
|
||||||
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
r.sample()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sample calculates RPS since last sample
|
||||||
|
func (r *RPSTracker) sample() {
|
||||||
|
now := time.Now()
|
||||||
|
nowNano := now.UnixNano()
|
||||||
|
|
||||||
|
currentCount := r.lastCount.Load()
|
||||||
|
lastSampleNano := r.lastSampleTime.Load()
|
||||||
|
|
||||||
|
if lastSampleNano == 0 {
|
||||||
|
r.lastSampleTime.Store(nowNano)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed := float64(nowNano-lastSampleNano) / float64(time.Second)
|
||||||
|
if elapsed > 0 {
|
||||||
|
rps := float64(currentCount) / elapsed
|
||||||
|
// Store RPS as centirps for precision (multiply by 100)
|
||||||
|
r.mu.Lock()
|
||||||
|
atomic.StoreUint64(&r.currentRPS, uint64(rps*100))
|
||||||
|
r.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset for next sample
|
||||||
|
r.lastCount.Store(0)
|
||||||
|
r.lastSampleTime.Store(nowNano)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentRPS returns the current requests per second
|
||||||
|
func (r *RPSTracker) GetCurrentRPS() float64 {
|
||||||
|
r.mu.RLock()
|
||||||
|
centirps := atomic.LoadUint64(&r.currentRPS)
|
||||||
|
r.mu.RUnlock()
|
||||||
|
return float64(centirps) / 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalRPSTracker *RPSTracker
|
||||||
|
|
||||||
|
// InitializeRPSTracker initializes the global RPS tracker
|
||||||
|
func InitializeRPSTracker() *RPSTracker {
|
||||||
|
if globalRPSTracker == nil {
|
||||||
|
globalRPSTracker = NewRPSTracker()
|
||||||
|
}
|
||||||
|
return globalRPSTracker
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRPSTracker returns the global RPS tracker
|
||||||
|
func GetRPSTracker() *RPSTracker {
|
||||||
|
return globalRPSTracker
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user