improvements mid apr 2025 (#26)

* General improvements and bug fixes.
* Admin dashboard
This commit is contained in:
2025-10-01 00:20:45 +01:00
committed by GitHub
parent cedee416a8
commit 0fc776228f
5 changed files with 219 additions and 8 deletions
+107 -7
View File
@@ -2,9 +2,11 @@ package main
import (
"embed"
"fmt"
"time"
"github.com/gofiber/fiber/v2"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
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
func (ad *AdminDashboard) getStats(c *fiber.Ctx) error {
uptimeSeconds := time.Since(startTime).Seconds()
stats := map[string]interface{}{
"timestamp": time.Now().Format(time.RFC3339),
"uptime": time.Since(startTime).Seconds(),
"version": "0.27.0", // TODO: Get from build info
"timestamp": time.Now().Format(time.RFC3339),
"uptime_seconds": uptimeSeconds,
"uptime_human": formatDuration(time.Since(startTime)),
"version": "0.27.0", // TODO: Get from build info
}
if cfg != nil && cfg.Monitoring != nil {
stats["metrics"] = map[string]interface{}{
"succeeded": getAdminMetricValue("graphql_proxy_succeeded_total"),
"failed": getAdminMetricValue("graphql_proxy_failed_total"),
"skipped": getAdminMetricValue("graphql_proxy_skipped_total"),
succeeded := getAdminMetricValue("graphql_proxy_succeeded_total")
failed := getAdminMetricValue("graphql_proxy_failed_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)
}
// 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
func (ad *AdminDashboard) getHealth(c *fiber.Ctx) error {
healthMgr := GetBackendHealthManager()
@@ -154,6 +225,35 @@ func (ad *AdminDashboard) getCacheStats(c *fiber.Ctx) error {
stats["ttl_seconds"] = cfg.Cache.CacheTTL
stats["max_memory_mb"] = cfg.Cache.CacheMaxMemorySize
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)
+12 -1
View File
@@ -110,8 +110,19 @@ func TestAdminDashboard_GetStats(t *testing.T) {
// Verify stats structure
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.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) {
+6
View File
@@ -387,6 +387,12 @@ func parseConfig() {
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
if err := loadRatelimitConfig(); err != nil {
// Log the error with clear guidance
+5
View File
@@ -236,6 +236,11 @@ func createFasthttpClient(clientConfig *config) *fasthttp.Client {
// proxyTheRequest handles the request proxying logic.
func proxyTheRequest(c *fiber.Ctx, currentEndpoint string) error {
// Record request for RPS tracking
if rpsTracker := GetRPSTracker(); rpsTracker != nil {
rpsTracker.RecordRequest()
}
// Setup tracing if enabled
var span trace.Span
var ctx context.Context
+89
View File
@@ -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
}