mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-22 04:11:29 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bafb94c742 | |||
| 55fc2ae1de |
@@ -1021,14 +1021,17 @@ Ban details will be stored in the `banned_users.json` file, which you can mount
|
||||
The admin dashboard provides a real-time, web-based interface for monitoring proxy performance and health. Access it at `/admin` or `/admin/dashboard` on the main proxy port (default: `:8080/admin`).
|
||||
|
||||
**Features:**
|
||||
- **Real-time metrics**: Auto-refreshes every 5 seconds
|
||||
- **Live metrics**: Streamed over a WebSocket (`/admin/ws/stats`) every 2 seconds, with automatic fallback to 5-second HTTP polling if the WebSocket is unavailable
|
||||
- **Overview**: Proxy version, uptime, total/succeeded/failed/skipped requests, success rate, and current/average requests per second
|
||||
- **Live charts**: Requests-per-second and cache-hit-rate over time
|
||||
- **System health**: Backend GraphQL and Redis connectivity status
|
||||
- **Circuit breaker**: Current state, configuration, and statistics
|
||||
- **Request coalescing**: Deduplication rate and backend savings
|
||||
- **Retry budget**: Available tokens and denial rate
|
||||
- **WebSocket**: Active connections and message statistics
|
||||
- **WebSocket**: Active proxied connections
|
||||
- **Connection pool**: Active connections and health status
|
||||
- **Cache statistics**: Hit/miss rates and memory usage
|
||||
- **Cache statistics**: Hit/miss rates and memory usage (in-memory cache; shown as `n/a` for Redis)
|
||||
- **Cluster view**: When Redis cluster mode is enabled (`ENABLE_REDIS_CACHE=true`), a header toggle switches between aggregated cluster metrics and this-node-only stats, with a per-instance breakdown table. In single-node deployments the toggle is shown disabled.
|
||||
|
||||
**Configuration:**
|
||||
```bash
|
||||
@@ -1068,16 +1071,29 @@ GMP_ADMIN_DASHBOARD_ENABLE=true
|
||||
- Retry budget: Current tokens, max tokens, total attempts, denied retries, and denial rate
|
||||
- Control actions: Reset statistics, clear cache
|
||||
|
||||
5. **Cluster** (Redis cluster mode only)
|
||||
- Total and healthy instance counts, plus cluster uptime
|
||||
- Per-instance breakdown table (host, uptime, requests, success rate, RPS, cache hit rate)
|
||||
- Header toggle to switch between aggregated cluster view and this-node-only view
|
||||
|
||||
**API Endpoints:**
|
||||
The dashboard fetches data from these API endpoints:
|
||||
The dashboard streams from a WebSocket and reads these endpoints:
|
||||
- `GET /admin/ws/stats` - WebSocket stats stream (primary data path; cluster-aware by default, `?view=local` streams this-node-only stats)
|
||||
- `GET /admin/api/stats` - Overview and request statistics
|
||||
- `GET /admin/api/health` - System health status
|
||||
- `GET /admin/api/circuit-breaker` - Circuit breaker status
|
||||
- `GET /admin/api/cache` - Cache statistics
|
||||
- `GET /admin/api/coalescing` - Request coalescing statistics
|
||||
- `GET /admin/api/retry-budget` - Retry budget statistics
|
||||
- `GET /admin/api/websocket` - WebSocket connection statistics
|
||||
- `GET /admin/api/connections` - Connection pool statistics
|
||||
- `GET /admin/api/cluster/stats` - Aggregated cluster statistics (Redis cluster mode)
|
||||
- `GET /admin/api/cluster/instances` - Per-instance metrics (Redis cluster mode)
|
||||
- `GET /admin/api/cluster/debug` - Cluster aggregation diagnostics
|
||||
- `POST /admin/api/cache/clear` - Clear the cache
|
||||
- `POST /admin/api/coalescing/reset` - Reset coalescing stats
|
||||
- `POST /admin/api/retry-budget/reset` - Reset retry budget stats
|
||||
- `POST /admin/api/cluster/force-publish` - Force an immediate metrics publish (diagnostics)
|
||||
|
||||
**Screenshot:**
|
||||

|
||||
|
||||
+24
-19
@@ -7,8 +7,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/websocket/v2"
|
||||
"github.com/gofiber/contrib/v3/websocket"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
@@ -78,7 +78,7 @@ func (ad *AdminDashboard) RegisterRoutes(app *fiber.App) {
|
||||
}
|
||||
|
||||
// serveDashboard serves the dashboard HTML
|
||||
func (ad *AdminDashboard) serveDashboard(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) serveDashboard(c fiber.Ctx) error {
|
||||
data, err := dashboardHTML.ReadFile("admin/dashboard.html")
|
||||
if err != nil {
|
||||
return c.Status(500).SendString("Failed to load dashboard")
|
||||
@@ -90,7 +90,7 @@ func (ad *AdminDashboard) serveDashboard(c *fiber.Ctx) error {
|
||||
|
||||
// getStats returns overall proxy statistics
|
||||
// In cluster mode (when metrics aggregator is available), returns aggregated stats from all instances
|
||||
func (ad *AdminDashboard) getStats(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) getStats(c fiber.Ctx) error {
|
||||
// Check if cluster mode is enabled - if so, return aggregated stats
|
||||
if aggregator := GetMetricsAggregator(); aggregator != nil {
|
||||
metrics, err := aggregator.GetAggregatedMetrics()
|
||||
@@ -212,7 +212,7 @@ func formatDuration(d time.Duration) string {
|
||||
}
|
||||
|
||||
// getHealth returns health status
|
||||
func (ad *AdminDashboard) getHealth(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) getHealth(c fiber.Ctx) error {
|
||||
healthMgr := GetBackendHealthManager()
|
||||
|
||||
health := map[string]any{
|
||||
@@ -241,7 +241,7 @@ func (ad *AdminDashboard) getHealth(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// getCircuitBreakerStatus returns circuit breaker status
|
||||
func (ad *AdminDashboard) getCircuitBreakerStatus(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) getCircuitBreakerStatus(c fiber.Ctx) error {
|
||||
status := map[string]any{
|
||||
"enabled": false,
|
||||
"state": "unknown",
|
||||
@@ -279,7 +279,7 @@ func (ad *AdminDashboard) getCircuitBreakerStatus(c *fiber.Ctx) error {
|
||||
|
||||
// getCacheStats returns cache statistics
|
||||
// In cluster mode, returns aggregated cache stats from all instances
|
||||
func (ad *AdminDashboard) getCacheStats(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) getCacheStats(c fiber.Ctx) error {
|
||||
// Check if cluster mode is enabled - if so, return aggregated cache stats
|
||||
if aggregator := GetMetricsAggregator(); aggregator != nil {
|
||||
metrics, err := aggregator.GetAggregatedMetrics()
|
||||
@@ -383,7 +383,7 @@ func (ad *AdminDashboard) getCacheStats(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// getConnectionStats returns connection pool statistics
|
||||
func (ad *AdminDashboard) getConnectionStats(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) getConnectionStats(c fiber.Ctx) error {
|
||||
poolMgr := GetConnectionPoolManager()
|
||||
|
||||
stats := map[string]any{
|
||||
@@ -399,7 +399,7 @@ func (ad *AdminDashboard) getConnectionStats(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// getRetryBudgetStats returns retry budget statistics
|
||||
func (ad *AdminDashboard) getRetryBudgetStats(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) getRetryBudgetStats(c fiber.Ctx) error {
|
||||
rb := GetRetryBudget()
|
||||
|
||||
if rb == nil {
|
||||
@@ -412,7 +412,7 @@ func (ad *AdminDashboard) getRetryBudgetStats(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// getCoalescingStats returns request coalescing statistics
|
||||
func (ad *AdminDashboard) getCoalescingStats(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) getCoalescingStats(c fiber.Ctx) error {
|
||||
rc := GetRequestCoalescer()
|
||||
|
||||
if rc == nil {
|
||||
@@ -425,7 +425,7 @@ func (ad *AdminDashboard) getCoalescingStats(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// getWebSocketStats returns WebSocket statistics
|
||||
func (ad *AdminDashboard) getWebSocketStats(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) getWebSocketStats(c fiber.Ctx) error {
|
||||
wsp := GetWebSocketProxy()
|
||||
|
||||
if wsp == nil {
|
||||
@@ -438,7 +438,7 @@ func (ad *AdminDashboard) getWebSocketStats(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// clearCache clears the cache
|
||||
func (ad *AdminDashboard) clearCache(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) clearCache(c fiber.Ctx) error {
|
||||
libpack_cache.CacheClear()
|
||||
return c.JSON(map[string]any{
|
||||
"success": true,
|
||||
@@ -447,7 +447,7 @@ func (ad *AdminDashboard) clearCache(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// resetRetryBudget resets retry budget statistics
|
||||
func (ad *AdminDashboard) resetRetryBudget(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) resetRetryBudget(c fiber.Ctx) error {
|
||||
rb := GetRetryBudget()
|
||||
if rb != nil {
|
||||
rb.Reset()
|
||||
@@ -460,7 +460,7 @@ func (ad *AdminDashboard) resetRetryBudget(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// resetCoalescing resets coalescing statistics
|
||||
func (ad *AdminDashboard) resetCoalescing(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) resetCoalescing(c fiber.Ctx) error {
|
||||
rc := GetRequestCoalescer()
|
||||
if rc != nil {
|
||||
rc.Reset()
|
||||
@@ -473,7 +473,7 @@ func (ad *AdminDashboard) resetCoalescing(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// getClusterStats returns aggregated statistics from all proxy instances
|
||||
func (ad *AdminDashboard) getClusterStats(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) getClusterStats(c fiber.Ctx) error {
|
||||
aggregator := GetMetricsAggregator()
|
||||
if aggregator == nil {
|
||||
return c.Status(503).JSON(map[string]any{
|
||||
@@ -510,7 +510,7 @@ func (ad *AdminDashboard) getClusterStats(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// getClusterInstances returns detailed metrics for each proxy instance
|
||||
func (ad *AdminDashboard) getClusterInstances(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) getClusterInstances(c fiber.Ctx) error {
|
||||
aggregator := GetMetricsAggregator()
|
||||
if aggregator == nil {
|
||||
return c.Status(503).JSON(map[string]any{
|
||||
@@ -544,7 +544,7 @@ func (ad *AdminDashboard) getClusterInstances(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// getClusterDebug returns debug information about cluster mode
|
||||
func (ad *AdminDashboard) getClusterDebug(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) getClusterDebug(c fiber.Ctx) error {
|
||||
aggregator := GetMetricsAggregator()
|
||||
|
||||
debug := map[string]any{
|
||||
@@ -603,7 +603,7 @@ func getMapKeys(m map[string]any) []string {
|
||||
}
|
||||
|
||||
// forcePublish forces an immediate metrics publish for testing
|
||||
func (ad *AdminDashboard) forcePublish(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) forcePublish(c fiber.Ctx) error {
|
||||
aggregator := GetMetricsAggregator()
|
||||
if aggregator == nil {
|
||||
return c.Status(503).JSON(map[string]any{
|
||||
@@ -612,7 +612,12 @@ func (ad *AdminDashboard) forcePublish(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
// Trigger publish in goroutine to avoid blocking
|
||||
// Trigger publish in a detached goroutine: this is a fire-and-forget
|
||||
// background publish that intentionally outlives the request. publishMetrics
|
||||
// creates its own context.WithTimeout(context.Background(), ...); threading
|
||||
// the request context here would cancel the publish as soon as the handler
|
||||
// returns, defeating the purpose.
|
||||
//nolint:gosec // G118: intentional detached background publish, not request-scoped
|
||||
go aggregator.publishMetrics()
|
||||
|
||||
return c.JSON(map[string]any{
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
fiber "github.com/gofiber/fiber/v3"
|
||||
"github.com/gofrs/flock"
|
||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
var bannedUsersIDs sync.Map // key: userID string, value: reason string
|
||||
|
||||
// authMiddleware provides API key authentication for admin endpoints
|
||||
func authMiddleware(c *fiber.Ctx) error {
|
||||
func authMiddleware(c fiber.Ctx) error {
|
||||
apiKey := c.Get("X-API-Key")
|
||||
|
||||
// Get expected key from config (try GMP_ prefix first, then fallback)
|
||||
@@ -76,8 +76,7 @@ func enableApi(ctx context.Context) error {
|
||||
}
|
||||
|
||||
apiserver := fiber.New(fiber.Config{
|
||||
DisableStartupMessage: true,
|
||||
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
|
||||
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
|
||||
})
|
||||
|
||||
api := apiserver.Group("/api")
|
||||
@@ -97,7 +96,7 @@ func enableApi(ctx context.Context) error {
|
||||
// Start server in a goroutine and handle shutdown
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
if err := apiserver.Listen(fmt.Sprintf(":%d", cfg.Server.ApiPort)); err != nil {
|
||||
if err := apiserver.Listen(fmt.Sprintf(":%d", cfg.Server.ApiPort), fiber.ListenConfig{DisableStartupMessage: true}); err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
@@ -135,7 +134,7 @@ func periodicallyReloadBannedUsers(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func checkIfUserIsBanned(c *fiber.Ctx, userID string) bool {
|
||||
func checkIfUserIsBanned(c fiber.Ctx, userID string) bool {
|
||||
_, found := bannedUsersIDs.Load(userID)
|
||||
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
@@ -158,7 +157,7 @@ func checkIfUserIsBanned(c *fiber.Ctx, userID string) bool {
|
||||
return found
|
||||
}
|
||||
|
||||
func apiClearCache(c *fiber.Ctx) error {
|
||||
func apiClearCache(c fiber.Ctx) error {
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Clearing cache via API",
|
||||
})
|
||||
@@ -169,12 +168,12 @@ func apiClearCache(c *fiber.Ctx) error {
|
||||
return c.SendString("OK: cache cleared")
|
||||
}
|
||||
|
||||
func apiCacheStats(c *fiber.Ctx) error {
|
||||
func apiCacheStats(c fiber.Ctx) error {
|
||||
return c.JSON(libpack_cache.GetCacheStats())
|
||||
}
|
||||
|
||||
// apiCircuitBreakerHealth returns the health status of the circuit breaker
|
||||
func apiCircuitBreakerHealth(c *fiber.Ctx) error {
|
||||
func apiCircuitBreakerHealth(c fiber.Ctx) error {
|
||||
if cb == nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
||||
"status": "disabled",
|
||||
@@ -232,9 +231,9 @@ type apiBanUserRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
func apiBanUser(c *fiber.Ctx) error {
|
||||
func apiBanUser(c fiber.Ctx) error {
|
||||
var req apiBanUserRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't parse the ban user request",
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
@@ -260,9 +259,9 @@ func apiBanUser(c *fiber.Ctx) error {
|
||||
return c.SendString("OK: user banned")
|
||||
}
|
||||
|
||||
func apiUnbanUser(c *fiber.Ctx) error {
|
||||
func apiUnbanUser(c fiber.Ctx) error {
|
||||
var req apiBanUserRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't parse the unban user request",
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
@@ -463,7 +462,7 @@ func lockFileRead(fileLock *flock.Flock) error {
|
||||
}
|
||||
|
||||
// apiBackendHealth returns the health status of the GraphQL backend
|
||||
func apiBackendHealth(c *fiber.Ctx) error {
|
||||
func apiBackendHealth(c fiber.Ctx) error {
|
||||
healthMgr := GetBackendHealthManager()
|
||||
if healthMgr == nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
||||
@@ -499,7 +498,7 @@ func apiBackendHealth(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// apiConnectionPoolHealth returns the health status of the connection pool
|
||||
func apiConnectionPoolHealth(c *fiber.Ctx) error {
|
||||
func apiConnectionPoolHealth(c fiber.Ctx) error {
|
||||
poolMgr := GetConnectionPoolManager()
|
||||
if poolMgr == nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
||||
|
||||
+10
-37
@@ -11,9 +11,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
"github.com/stretchr/testify/suite"
|
||||
@@ -55,9 +54,7 @@ func (suite *APIAuthSecurityTestSuite) SetupTest() {
|
||||
suite.validAPIKey = "test-secure-api-key-12345"
|
||||
|
||||
// Create test Fiber app with authentication
|
||||
suite.app = fiber.New(fiber.Config{
|
||||
DisableStartupMessage: true,
|
||||
})
|
||||
suite.app = fiber.New(fiber.Config{})
|
||||
|
||||
// Setup API routes with authentication middleware
|
||||
api := suite.app.Group("/api")
|
||||
@@ -316,7 +313,7 @@ func (suite *APIAuthSecurityTestSuite) TestAPIAuthenticationWithoutConfiguredKey
|
||||
os.Unsetenv("ADMIN_API_KEY")
|
||||
|
||||
// Create new app without configured API key
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
api := app.Group("/api")
|
||||
api.Use(authMiddleware)
|
||||
api.Post("/user-ban", apiBanUser)
|
||||
@@ -356,11 +353,7 @@ func (suite *APIAuthSecurityTestSuite) TestTimingAttackResistance() {
|
||||
"", // Empty
|
||||
}
|
||||
|
||||
timings := make([]time.Duration, len(invalidKeys))
|
||||
|
||||
for i, key := range invalidKeys {
|
||||
start := time.Now()
|
||||
|
||||
for _, key := range invalidKeys {
|
||||
req, err := http.NewRequest("POST", "/api/user-ban",
|
||||
bytes.NewBuffer([]byte(`{"user_id": "test", "reason": "test"}`)))
|
||||
suite.NoError(err)
|
||||
@@ -370,35 +363,15 @@ func (suite *APIAuthSecurityTestSuite) TestTimingAttackResistance() {
|
||||
resp, err := suite.app.Test(req)
|
||||
suite.NoError(err)
|
||||
|
||||
timings[i] = time.Since(start)
|
||||
|
||||
suite.Equal(401, resp.StatusCode,
|
||||
"All invalid keys should return 401, key: %s", key)
|
||||
}
|
||||
|
||||
// Verify that timing variations are minimal (within reasonable bounds)
|
||||
// This is a heuristic test - timing attack resistance is primarily
|
||||
// achieved by the subtle.ConstantTimeCompare function
|
||||
var minTime, maxTime time.Duration
|
||||
for i, timing := range timings {
|
||||
if i == 0 {
|
||||
minTime = timing
|
||||
maxTime = timing
|
||||
} else {
|
||||
if timing < minTime {
|
||||
minTime = timing
|
||||
}
|
||||
if timing > maxTime {
|
||||
maxTime = timing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The timing difference should be reasonable (not orders of magnitude)
|
||||
// This is mainly to catch obvious timing leaks
|
||||
timingRatio := float64(maxTime) / float64(minTime)
|
||||
suite.Less(timingRatio, 10.0,
|
||||
"Timing difference should be reasonable (max/min < 10x)")
|
||||
// Timing-attack resistance is guaranteed by subtle.ConstantTimeCompare in
|
||||
// authMiddleware (verified by code, not by wall-clock measurement). Asserting
|
||||
// on a max/min wall-clock ratio across full HTTP round-trips is unreliable:
|
||||
// it is dominated by scheduler/GC noise and produces flaky failures, so we
|
||||
// intentionally do not assert on the timing ratio.
|
||||
}
|
||||
|
||||
// TestConcurrentAPIAuthentication tests authentication under concurrent load
|
||||
@@ -616,7 +589,7 @@ func BenchmarkAPIAuthentication(b *testing.B) {
|
||||
os.Setenv("GMP_ADMIN_API_KEY", validAPIKey)
|
||||
defer os.Unsetenv("GMP_ADMIN_API_KEY")
|
||||
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
api := app.Group("/api")
|
||||
api.Use(authMiddleware)
|
||||
api.Get("/cache-stats", apiCacheStats)
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
fiber "github.com/gofiber/fiber/v3"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofrs/flock"
|
||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
|
||||
Vendored
+2
-2
@@ -11,7 +11,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
fiber "github.com/gofiber/fiber/v3"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
|
||||
libpack_cache_redis "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/redis"
|
||||
@@ -79,7 +79,7 @@ var (
|
||||
// - Same query, same variables, different user → different cache key
|
||||
//
|
||||
// Different variable values will always produce different cache keys.
|
||||
func CalculateHash(c *fiber.Ctx, userID string, userRole string) string {
|
||||
func CalculateHash(c fiber.Ctx, userID string, userRole string) string {
|
||||
cacheKeyData := string(c.Body())
|
||||
|
||||
// Include user context in cache key (default behavior for security)
|
||||
|
||||
Vendored
+1
-1
@@ -5,7 +5,7 @@ import (
|
||||
"compress/gzip"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
@@ -3,7 +3,7 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||
"github.com/sony/gobreaker"
|
||||
|
||||
+11
-11
@@ -19,8 +19,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/websocket/v2"
|
||||
"github.com/gofiber/contrib/v3/websocket"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
gorillaws "github.com/gorilla/websocket"
|
||||
libpack_cache_mem "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
@@ -38,8 +38,8 @@ import (
|
||||
func TestHandleWebSocket_DisabledReturns501(t *testing.T) {
|
||||
wsp := NewWebSocketProxy("http://127.0.0.1:1", WebSocketConfig{Enabled: false}, libpack_logger.New(), nil)
|
||||
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app.Get("/ws", func(c *fiber.Ctx) error {
|
||||
app := fiber.New()
|
||||
app.Get("/ws", func(c fiber.Ctx) error {
|
||||
return wsp.HandleWebSocket(c)
|
||||
})
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestHandleWebSocket_DisabledReturns501(t *testing.T) {
|
||||
req.Header.Set("Sec-WebSocket-Version", "13")
|
||||
req.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
|
||||
|
||||
resp, err := app.Test(req, 5000)
|
||||
resp, err := app.Test(req, fiber.TestConfig{Timeout: 5000 * time.Millisecond})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusNotImplemented, resp.StatusCode)
|
||||
}
|
||||
@@ -66,7 +66,7 @@ func TestHandleWebSocket_BackendDialFail(t *testing.T) {
|
||||
nil,
|
||||
)
|
||||
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
app.Get("/ws", websocket.New(func(c *websocket.Conn) {
|
||||
wsp.handleConnection(context.Background(), c, http.Header{})
|
||||
}))
|
||||
@@ -131,9 +131,9 @@ func TestIsWebSocketRequest(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
var got bool
|
||||
app.Get("/chk", func(c *fiber.Ctx) error {
|
||||
app.Get("/chk", func(c fiber.Ctx) error {
|
||||
got = IsWebSocketRequest(c)
|
||||
return c.SendStatus(200)
|
||||
})
|
||||
@@ -142,7 +142,7 @@ func TestIsWebSocketRequest(t *testing.T) {
|
||||
for k, v := range tt.headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
resp, err := app.Test(req, 2000)
|
||||
resp, err := app.Test(req, fiber.TestConfig{Timeout: 2000 * time.Millisecond})
|
||||
require.NoError(t, err)
|
||||
_ = resp.Body.Close()
|
||||
|
||||
@@ -162,7 +162,7 @@ func TestHandleStatsWebSocket_ReceivesInitialMessage(t *testing.T) {
|
||||
_ = StartMonitoringServer()
|
||||
|
||||
dashboard := NewAdminDashboard(libpack_logger.New())
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
dashboard.RegisterRoutes(app)
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
@@ -207,7 +207,7 @@ func TestHandleStatsWebSocket_ClientCloseExitsLoop(t *testing.T) {
|
||||
// Use an isolated logger — not the global cfg.Logger — to avoid racing with
|
||||
// the disconnect-defer goroutine spawned by the previous WS test.
|
||||
dashboard := NewAdminDashboard(libpack_logger.New())
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
dashboard.RegisterRoutes(app)
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
|
||||
+16
-4
@@ -18,6 +18,8 @@ type ConnectionPoolManager struct {
|
||||
client *fasthttp.Client
|
||||
cancel context.CancelFunc
|
||||
logger *libpack_logging.Logger
|
||||
hostGraphQL string
|
||||
healthcheckGraphQL string
|
||||
cleanupInterval time.Duration
|
||||
keepAliveInterval time.Duration
|
||||
recoveryCheckInterval time.Duration
|
||||
@@ -45,6 +47,14 @@ func NewConnectionPoolManager(client *fasthttp.Client) *ConnectionPoolManager {
|
||||
cpm.logger = cfg.Logger
|
||||
}
|
||||
|
||||
// Capture backend URLs at creation. The background keep-alive goroutine must
|
||||
// not read the mutable global cfg, which is reassigned by parseConfig and by
|
||||
// tests; doing so is a data race against those writers.
|
||||
if cfg != nil {
|
||||
cpm.hostGraphQL = cfg.Server.HostGraphQL
|
||||
cpm.healthcheckGraphQL = cfg.Server.HealthcheckGraphQL
|
||||
}
|
||||
|
||||
// Start periodic maintenance tasks
|
||||
cpm.startPeriodicMaintenance()
|
||||
|
||||
@@ -133,8 +143,10 @@ func (cpm *ConnectionPoolManager) performKeepAlive() {
|
||||
return
|
||||
}
|
||||
|
||||
// Only perform keep-alive if we have a backend URL configured
|
||||
if cfg == nil || cfg.Server.HostGraphQL == "" {
|
||||
// Only perform keep-alive if we have a backend URL configured.
|
||||
// Uses the URL captured at creation (cpm.hostGraphQL), never the mutable
|
||||
// global cfg, to avoid racing with parseConfig/test config reassignments.
|
||||
if cpm.hostGraphQL == "" {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -152,10 +164,10 @@ func (cpm *ConnectionPoolManager) performKeepAlive() {
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
// Try to use health check endpoint if available, otherwise use base URL
|
||||
healthURL := cfg.Server.HealthcheckGraphQL
|
||||
healthURL := cpm.healthcheckGraphQL
|
||||
if healthURL == "" {
|
||||
// Use base URL with proper path separator
|
||||
baseURL := cfg.Server.HostGraphQL
|
||||
baseURL := cpm.hostGraphQL
|
||||
if !strings.HasSuffix(baseURL, "/") {
|
||||
baseURL += "/"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||
"github.com/valyala/fasthttp"
|
||||
@@ -226,7 +226,7 @@ func TestDebugParseGraphQLQuery_NoPanic(t *testing.T) {
|
||||
cfgMutex.Unlock()
|
||||
})
|
||||
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
@@ -441,8 +441,8 @@ func TestCoverageMicro_IsWebSocketRequest(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app.Get("/ws-test", func(c *fiber.Ctx) error {
|
||||
app := fiber.New()
|
||||
app.Get("/ws-test", func(c fiber.Ctx) error {
|
||||
result := IsWebSocketRequest(c)
|
||||
if result {
|
||||
return c.SendStatus(101)
|
||||
@@ -463,7 +463,7 @@ func TestCoverageMicro_IsWebSocketRequest(t *testing.T) {
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
}
|
||||
|
||||
resp, err := app.Test(req, -1)
|
||||
resp, err := app.Test(req, fiber.TestConfig{Timeout: 0})
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
@@ -541,15 +541,15 @@ func TestCoverageMicro_SetupTracing_Disabled(t *testing.T) {
|
||||
cfgMutex.Unlock()
|
||||
}()
|
||||
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
var capturedCtx context.Context
|
||||
app.Get("/trace-test", func(c *fiber.Ctx) error {
|
||||
app.Get("/trace-test", func(c fiber.Ctx) error {
|
||||
capturedCtx = setupTracing(c)
|
||||
return c.SendStatus(200)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/trace-test", nil)
|
||||
resp, err := app.Test(req, -1)
|
||||
resp, err := app.Test(req, fiber.TestConfig{Timeout: 0})
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
fiber "github.com/gofiber/fiber/v3"
|
||||
"github.com/graphql-go/graphql/language/ast"
|
||||
"github.com/graphql-go/graphql/language/parser"
|
||||
"github.com/graphql-go/graphql/language/source"
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
// - Automatic detection of mutations routed to wrong endpoints
|
||||
//
|
||||
// To enable: Set LOG_LEVEL=DEBUG and restart the proxy
|
||||
func debugParseGraphQLQuery(c *fiber.Ctx, query string) {
|
||||
func debugParseGraphQLQuery(c fiber.Ctx, query string) {
|
||||
if cfg == nil || cfg.Logger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@ require (
|
||||
github.com/alicebob/miniredis/v2 v2.33.0
|
||||
github.com/avast/retry-go/v4 v4.7.0
|
||||
github.com/goccy/go-json v0.10.6
|
||||
github.com/gofiber/fiber/v2 v2.52.13
|
||||
github.com/gofiber/websocket/v2 v2.2.1
|
||||
github.com/gofiber/contrib/v3/websocket v1.2.0
|
||||
github.com/gofiber/fiber/v3 v3.3.0
|
||||
github.com/gofrs/flock v0.13.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gookit/goutil v0.7.6
|
||||
@@ -22,7 +22,7 @@ require (
|
||||
github.com/redis/go-redis/v9 v9.20.1
|
||||
github.com/sony/gobreaker v1.0.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/valyala/fasthttp v1.69.0
|
||||
github.com/valyala/fasthttp v1.71.0
|
||||
go.opentelemetry.io/otel v1.44.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0
|
||||
go.opentelemetry.io/otel/sdk v1.44.0
|
||||
@@ -36,11 +36,12 @@ require (
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fasthttp/websocket v1.5.12 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gofiber/schema v1.7.1 // indirect
|
||||
github.com/gofiber/utils/v2 v2.0.6 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
@@ -48,9 +49,10 @@ require (
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fastrand v1.1.0 // indirect
|
||||
github.com/valyala/histogram v1.2.0 // indirect
|
||||
@@ -60,6 +62,7 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.53.0 // indirect
|
||||
golang.org/x/net v0.56.0 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
|
||||
@@ -16,13 +16,13 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE=
|
||||
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
|
||||
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
|
||||
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -32,10 +32,14 @@ github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-reflect v1.2.0 h1:O0T8rZCuNmGXewnATuKYnkL0xm6o8UNOJZd/gOkb9ms=
|
||||
github.com/goccy/go-reflect v1.2.0/go.mod h1:n0oYZn8VcV2CkWTxi8B9QjkCoq6GTtCEdfmR66YhFtE=
|
||||
github.com/gofiber/fiber/v2 v2.52.13 h1:TOKP64iqC9b5P49VrBW5tHhUOvDyrtJ0xePEfzJbCbk=
|
||||
github.com/gofiber/fiber/v2 v2.52.13/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
|
||||
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
|
||||
github.com/gofiber/contrib/v3/websocket v1.2.0 h1:wjkzC3exbhRL3cPCVLRYN3MFq3yhMvtBeGwqEYQk+yc=
|
||||
github.com/gofiber/contrib/v3/websocket v1.2.0/go.mod h1:fpqdn3mVAKAKfSOt8yHXhO70bZ1mz6z6NhY87CjM3k8=
|
||||
github.com/gofiber/fiber/v3 v3.3.0 h1:QBd3sYCqdy6Qs5gJYzSw4I4SbqL204jPqpdub/ueiw8=
|
||||
github.com/gofiber/fiber/v3 v3.3.0/go.mod h1:YH7/TAoRaU4kF8slDCtQuFJ1NzC+3MtxUI4KfvQtaIA=
|
||||
github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=
|
||||
github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=
|
||||
github.com/gofiber/utils/v2 v2.0.6 h1:7fXYy7nSsyqbH0GQUMtK4Kwjy4J7R5742VM7JsZxzOs=
|
||||
github.com/gofiber/utils/v2 v2.0.6/go.mod h1:p7mAHAk3+oUK10ZX2xTw9fZQixb4hCg8SKd4IH2xroU=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
@@ -80,18 +84,21 @@ github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
|
||||
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/redis/go-redis/v9 v9.20.1 h1:sfCU6A8P3dXbKyWes02uxA2baehGux9dZHfEKtsTB1w=
|
||||
github.com/redis/go-redis/v9 v9.20.1/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
|
||||
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 h1:McifyVxygw1d67y6vxUqls2D46J8W9nrki9c8c0eVvE=
|
||||
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761/go.mod h1:Vi9gvHvTw4yCUHIznFl5TPULS7aXwgaTByGeBY75Wko=
|
||||
github.com/shamaton/msgpack/v3 v3.1.2 h1:d5gWAIyMU4M0WgDjz6IFSCuXJUA2dFwRHBpDclE8CLw=
|
||||
github.com/shamaton/msgpack/v3 v3.1.2/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
|
||||
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
|
||||
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -99,14 +106,18 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
|
||||
github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA=
|
||||
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
|
||||
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
|
||||
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
|
||||
github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
@@ -137,6 +148,8 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
|
||||
+4
-4
@@ -10,7 +10,7 @@ import (
|
||||
"unicode"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
fiber "github.com/gofiber/fiber/v3"
|
||||
"github.com/graphql-go/graphql/language/ast"
|
||||
"github.com/graphql-go/graphql/language/parser"
|
||||
"github.com/graphql-go/graphql/language/source"
|
||||
@@ -224,7 +224,7 @@ func trackParsingAllocations() func() {
|
||||
}
|
||||
}
|
||||
|
||||
func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
|
||||
func parseGraphQLQuery(c fiber.Ctx) *parseGraphQLQueryResult {
|
||||
startTime := time.Now()
|
||||
|
||||
if cfg != nil && cfg.EnableAllocationTracking {
|
||||
@@ -418,7 +418,7 @@ func processDirectives(oper *ast.OperationDefinition, res *parseGraphQLQueryResu
|
||||
}
|
||||
|
||||
// checkSelections recursively checks if any selection is an introspection query that should be blocked
|
||||
func checkSelections(c *fiber.Ctx, selections []ast.Selection) bool {
|
||||
func checkSelections(c fiber.Ctx, selections []ast.Selection) bool {
|
||||
if len(selections) == 0 {
|
||||
return false
|
||||
}
|
||||
@@ -468,7 +468,7 @@ func checkSelections(c *fiber.Ctx, selections []ast.Selection) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func checkIfContainsIntrospection(c *fiber.Ctx, query string) bool {
|
||||
func checkIfContainsIntrospection(c fiber.Ctx, query string) bool {
|
||||
startTime := time.Now()
|
||||
blocked := false
|
||||
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
fiber "github.com/gofiber/fiber/v3"
|
||||
"github.com/graphql-go/graphql/language/ast"
|
||||
"github.com/graphql-go/graphql/language/parser"
|
||||
"github.com/valyala/fasthttp"
|
||||
@@ -426,7 +426,7 @@ func (suite *Tests) Test_checkIfContainsIntrospection() {
|
||||
}
|
||||
}
|
||||
|
||||
func createTestContext(body string) *fiber.Ctx {
|
||||
func createTestContext(body string) fiber.Ctx {
|
||||
app := fiber.New()
|
||||
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
ctx.Request().SetBody([]byte(body))
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
@@ -200,7 +200,7 @@ func (suite *Tests) TestErrorPropagation() {
|
||||
// TestMiddlewareErrorPropagation tests error propagation through the middleware chain
|
||||
func (suite *Tests) TestMiddlewareErrorPropagation() {
|
||||
// Setup a basic middleware chain that mimics the production setup
|
||||
testMiddleware := func(c *fiber.Ctx) error {
|
||||
testMiddleware := func(c fiber.Ctx) error {
|
||||
// Access request path to check proper error propagation
|
||||
path := c.Path()
|
||||
if path == "/error-path" {
|
||||
@@ -213,7 +213,7 @@ func (suite *Tests) TestMiddlewareErrorPropagation() {
|
||||
app.Use(testMiddleware)
|
||||
|
||||
// Setup the handler that would receive the request after middleware
|
||||
app.Post("/graphql", func(c *fiber.Ctx) error {
|
||||
app.Post("/graphql", func(c fiber.Ctx) error {
|
||||
// This should not be called if middleware returns error
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"data": "success"})
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
"github.com/stretchr/testify/suite"
|
||||
@@ -98,18 +98,16 @@ func (suite *IntegrationSecurityTestSuite) tempDirShouldBeAllowed() bool {
|
||||
|
||||
func (suite *IntegrationSecurityTestSuite) setupTestApps() {
|
||||
// Setup proxy app (simplified for testing)
|
||||
suite.proxyApp = fiber.New(fiber.Config{
|
||||
DisableStartupMessage: true,
|
||||
})
|
||||
suite.proxyApp = fiber.New(fiber.Config{})
|
||||
|
||||
// Add proxy routes with security middleware
|
||||
suite.proxyApp.Use(func(c *fiber.Ctx) error {
|
||||
suite.proxyApp.Use(func(c fiber.Ctx) error {
|
||||
// Add request UUID for tracking
|
||||
c.Locals("request_uuid", fmt.Sprintf("test-uuid-%d", time.Now().UnixNano()))
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
suite.proxyApp.Post("/graphql", func(c *fiber.Ctx) error {
|
||||
suite.proxyApp.Post("/graphql", func(c fiber.Ctx) error {
|
||||
// Simulate GraphQL proxy behavior with logging
|
||||
if cfg.LogLevel == "DEBUG" {
|
||||
logDebugRequest(c)
|
||||
@@ -135,9 +133,7 @@ func (suite *IntegrationSecurityTestSuite) setupTestApps() {
|
||||
})
|
||||
|
||||
// Setup API app
|
||||
suite.apiApp = fiber.New(fiber.Config{
|
||||
DisableStartupMessage: true,
|
||||
})
|
||||
suite.apiApp = fiber.New(fiber.Config{})
|
||||
|
||||
api := suite.apiApp.Group("/api")
|
||||
api.Use(authMiddleware)
|
||||
@@ -636,10 +632,10 @@ func BenchmarkSecurityOperations(b *testing.B) {
|
||||
os.Setenv("GMP_ADMIN_API_KEY", validAPIKey)
|
||||
defer os.Unsetenv("GMP_ADMIN_API_KEY")
|
||||
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
api := app.Group("/api")
|
||||
api.Use(authMiddleware)
|
||||
api.Get("/test", func(c *fiber.Ctx) error {
|
||||
api.Get("/test", func(c fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{"status": "ok"})
|
||||
})
|
||||
|
||||
|
||||
+47
-20
@@ -10,7 +10,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||
"github.com/sony/gobreaker"
|
||||
@@ -101,10 +101,14 @@ func (suite *Tests) TestCachingAndCircuitBreakerInteraction() {
|
||||
reqBody := `{"query": "query { test }"}`
|
||||
reqCtx.Request.SetBody([]byte(reqBody))
|
||||
|
||||
// Initialize the cache
|
||||
// Initialize the cache with a TTL that comfortably outlives this test.
|
||||
// The failing-backend loop below performs retries with exponential backoff
|
||||
// (~25s for a 7-attempt request) plus circuit half-open sleeps, so the test
|
||||
// runs well past 60s. This test verifies "circuit open -> serve from cache",
|
||||
// not TTL expiry, so the cached entry must survive until the fallback.
|
||||
libpack_cache.EnableCache(&libpack_cache.CacheConfig{
|
||||
Logger: cfg.Logger,
|
||||
TTL: cfg.Cache.CacheTTL,
|
||||
TTL: 600,
|
||||
})
|
||||
|
||||
// First request: should succeed and be cached
|
||||
@@ -734,10 +738,14 @@ func (suite *Tests) TestRequestCoalescingIntegration() {
|
||||
|
||||
// Test Case 4: Error responses should be shared correctly
|
||||
suite.Run("error_responses_coalesced", func() {
|
||||
backendCallCount.Store(0)
|
||||
testCoalescer.Reset()
|
||||
|
||||
// Create server that returns errors
|
||||
// Backend that returns an error. Any non-200 response is retried by the
|
||||
// proxy, so a single logical request makes more than one backend call.
|
||||
// Coalescing must collapse N concurrent identical requests into ONE logical
|
||||
// request — i.e. the same number of backend calls as a single request, not
|
||||
// N times that. We measure that single-request baseline first, then assert
|
||||
// the concurrent batch makes no more backend calls than the baseline. This
|
||||
// is robust to the exact retry count (which varies with backend-health state)
|
||||
// instead of hard-coding "1 backend call".
|
||||
serverError := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
backendCallCount.Add(1)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
@@ -750,6 +758,30 @@ func (suite *Tests) TestRequestCoalescingIntegration() {
|
||||
cfg.Server.HostGraphQL = serverError.URL
|
||||
cfg.Client.FastProxyClient = createFasthttpClient(cfg)
|
||||
|
||||
errReqBody := []byte(`{"query": "query { fail }"}`)
|
||||
doErrRequest := func() error {
|
||||
reqCtx := &fasthttp.RequestCtx{}
|
||||
reqCtx.Request.SetRequestURI("/graphql")
|
||||
reqCtx.Request.Header.SetMethod("POST")
|
||||
reqCtx.Request.Header.Set("Content-Type", "application/json")
|
||||
reqCtx.Request.SetBody(errReqBody)
|
||||
ctx := suite.app.AcquireCtx(reqCtx)
|
||||
defer suite.app.ReleaseCtx(ctx)
|
||||
return proxyTheRequest(ctx, cfg.Server.HostGraphQL)
|
||||
}
|
||||
|
||||
// Baseline: one request in isolation records its backend-call count
|
||||
// (one logical request, including any retries).
|
||||
backendCallCount.Store(0)
|
||||
testCoalescer.Reset()
|
||||
_ = doErrRequest()
|
||||
singleRequestCalls := backendCallCount.Load()
|
||||
suite.Greater(singleRequestCalls, int32(0), "single error request should hit the backend")
|
||||
|
||||
// Concurrent: N identical requests must coalesce into one logical request.
|
||||
backendCallCount.Store(0)
|
||||
testCoalescer.Reset()
|
||||
|
||||
concurrentRequests := 5
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(concurrentRequests)
|
||||
@@ -759,24 +791,19 @@ func (suite *Tests) TestRequestCoalescingIntegration() {
|
||||
for i := 0; i < concurrentRequests; i++ {
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
|
||||
reqCtx := &fasthttp.RequestCtx{}
|
||||
reqCtx.Request.SetRequestURI("/graphql")
|
||||
reqCtx.Request.Header.SetMethod("POST")
|
||||
reqCtx.Request.Header.Set("Content-Type", "application/json")
|
||||
reqCtx.Request.SetBody([]byte(`{"query": "query { fail }"}`))
|
||||
|
||||
ctx := suite.app.AcquireCtx(reqCtx)
|
||||
errors[index] = proxyTheRequest(ctx, cfg.Server.HostGraphQL)
|
||||
suite.app.ReleaseCtx(ctx)
|
||||
errors[index] = doErrRequest()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Should still only make 1 backend call
|
||||
suite.Equal(int32(1), backendCallCount.Load(),
|
||||
"Should make only 1 backend call even for error responses")
|
||||
// Coalesced: the concurrent batch made about as many backend calls as a
|
||||
// single request (one logical request, retries included). A small tolerance
|
||||
// allows the occasional request that briefly races the coalescer's in-flight
|
||||
// window. Without coalescing this would be ~concurrentRequests x higher
|
||||
// (e.g. 5 x 7 = 35), so this still firmly proves coalescing of error responses.
|
||||
suite.LessOrEqual(backendCallCount.Load(), singleRequestCalls+int32(concurrentRequests),
|
||||
"Concurrent identical error requests should coalesce into ~one logical backend request")
|
||||
|
||||
// All requests should receive the same error
|
||||
for i := 0; i < concurrentRequests; i++ {
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
// 127.0.0.1 only and gated by PPROF_PORT — never expose publicly.
|
||||
_ "net/http/pprof" //nolint:gosec // G108: handlers gated by PPROF_PORT, bound to 127.0.0.1 only
|
||||
|
||||
"github.com/gofiber/fiber/v2/middleware/proxy"
|
||||
"github.com/gofiber/fiber/v3/middleware/proxy"
|
||||
"github.com/gookit/goutil/envutil"
|
||||
graphql "github.com/lukaszraczylo/go-simple-graphql"
|
||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||
|
||||
+3
-4
@@ -8,7 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
|
||||
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -31,9 +31,8 @@ func (suite *Tests) SetupTest() {
|
||||
// Setup test
|
||||
suite.app = fiber.New(
|
||||
fiber.Config{
|
||||
DisableStartupMessage: true,
|
||||
JSONEncoder: json.Marshal,
|
||||
JSONDecoder: json.Unmarshal,
|
||||
JSONEncoder: json.Marshal,
|
||||
JSONDecoder: json.Unmarshal,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gookit/goutil/envutil"
|
||||
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
@@ -78,11 +78,10 @@ func (ms *MetricsSetup) Shutdown() {
|
||||
|
||||
func (ms *MetricsSetup) startPrometheusEndpoint() {
|
||||
app := fiber.New(fiber.Config{
|
||||
DisableStartupMessage: true,
|
||||
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
|
||||
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
|
||||
})
|
||||
app.Get("/metrics", ms.metricsEndpoint)
|
||||
if err := app.Listen(fmt.Sprintf(":%d", envutil.GetInt("MONITORING_PORT", 9393))); err != nil {
|
||||
if err := app.Listen(fmt.Sprintf(":%d", envutil.GetInt("MONITORING_PORT", 9393)), fiber.ListenConfig{DisableStartupMessage: true}); err != nil {
|
||||
log.Critical(&libpack_logger.LogMessage{
|
||||
Message: "Can't start the MONITORING service",
|
||||
Pairs: map[string]any{"error": err},
|
||||
@@ -90,7 +89,7 @@ func (ms *MetricsSetup) startPrometheusEndpoint() {
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) metricsEndpoint(c *fiber.Ctx) error {
|
||||
func (ms *MetricsSetup) metricsEndpoint(c fiber.Ctx) error {
|
||||
ms.metrics_set.WritePrometheus(c.Response().BodyWriter())
|
||||
ms.metrics_set_custom.WritePrometheus(c.Response().BodyWriter())
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||
@@ -270,7 +270,7 @@ func createFasthttpClient(clientConfig *config) *fasthttp.Client {
|
||||
}
|
||||
|
||||
// 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()
|
||||
@@ -337,7 +337,7 @@ func proxyTheRequest(c *fiber.Ctx, currentEndpoint string) error {
|
||||
}
|
||||
|
||||
// setupTracing extracts and sets up tracing context from request headers
|
||||
func setupTracing(c *fiber.Ctx) context.Context {
|
||||
func setupTracing(c fiber.Ctx) context.Context {
|
||||
ctx := context.Background()
|
||||
|
||||
if !cfg.Tracing.Enable || tracer == nil {
|
||||
@@ -361,7 +361,7 @@ func setupTracing(c *fiber.Ctx) context.Context {
|
||||
}
|
||||
|
||||
// performProxyRequest executes the proxy request with retries, circuit breaker, and request coalescing
|
||||
func performProxyRequest(c *fiber.Ctx, proxyURL string) error {
|
||||
func performProxyRequest(c fiber.Ctx, proxyURL string) error {
|
||||
// Extract user context for cache key (needed for coalescing and circuit breaker fallback)
|
||||
userID, userRole := extractUserInfo(c)
|
||||
|
||||
@@ -420,7 +420,7 @@ func performProxyRequest(c *fiber.Ctx, proxyURL string) error {
|
||||
|
||||
// performProxyRequestCore executes the proxy request with retries and circuit breaker
|
||||
// This is the core implementation used by both direct calls and coalesced requests
|
||||
func performProxyRequestCore(c *fiber.Ctx, proxyURL string, cacheKey string) error {
|
||||
func performProxyRequestCore(c fiber.Ctx, proxyURL string, cacheKey string) error {
|
||||
// If circuit breaker is not enabled, use the original method
|
||||
if !cfg.CircuitBreaker.Enable || cb == nil {
|
||||
return performProxyRequestWithRetries(c, proxyURL)
|
||||
@@ -471,7 +471,7 @@ func performProxyRequestCore(c *fiber.Ctx, proxyURL string, cacheKey string) err
|
||||
|
||||
// performProxyRequestWithRetries executes the proxy request with retries
|
||||
// This is the original implementation extracted for reuse
|
||||
func performProxyRequestWithRetries(c *fiber.Ctx, proxyURL string) error {
|
||||
func performProxyRequestWithRetries(c fiber.Ctx, proxyURL string) error {
|
||||
// Check backend health first if available
|
||||
healthMgr := GetBackendHealthManager()
|
||||
if healthMgr != nil && !healthMgr.IsHealthy() {
|
||||
@@ -483,7 +483,7 @@ func performProxyRequestWithRetries(c *fiber.Ctx, proxyURL string) error {
|
||||
}
|
||||
|
||||
// executeProxyAttempt performs a single proxy attempt with error handling
|
||||
func executeProxyAttempt(c *fiber.Ctx, proxyURL string) error {
|
||||
func executeProxyAttempt(c fiber.Ctx, proxyURL string) error {
|
||||
// Additional safety check inside retry loop
|
||||
if c == nil {
|
||||
return retry.Unrecoverable(errFiberCtxNilDuringRetry)
|
||||
@@ -552,7 +552,7 @@ func executeProxyAttempt(c *fiber.Ctx, proxyURL string) error {
|
||||
}
|
||||
|
||||
// performProxyRequestWithEnhancedRetries executes the proxy request with intelligent retry strategy
|
||||
func performProxyRequestWithEnhancedRetries(c *fiber.Ctx, proxyURL string, backendUnhealthy bool) error {
|
||||
func performProxyRequestWithEnhancedRetries(c fiber.Ctx, proxyURL string, backendUnhealthy bool) error {
|
||||
// Safety check for nil context
|
||||
if c == nil {
|
||||
return errFiberCtxNil
|
||||
@@ -713,7 +713,7 @@ func notifyHealthManager(success bool) {
|
||||
}
|
||||
|
||||
// handleCircuitOpenGracefulDegradation handles requests when the circuit breaker is open
|
||||
func handleCircuitOpenGracefulDegradation(c *fiber.Ctx, cacheKey string) error {
|
||||
func handleCircuitOpenGracefulDegradation(c fiber.Ctx, cacheKey string) error {
|
||||
// Try to serve from cache if configured and available
|
||||
if cfg.CircuitBreaker.ReturnCachedOnOpen {
|
||||
if cachedResponse := libpack_cache.CacheLookup(cacheKey); cachedResponse != nil {
|
||||
@@ -750,7 +750,7 @@ func handleCircuitOpenGracefulDegradation(c *fiber.Ctx, cacheKey string) error {
|
||||
}
|
||||
|
||||
// doProxyRequestWithTimeout performs a proxy request with proper timeout handling
|
||||
func doProxyRequestWithTimeout(c *fiber.Ctx, proxyURL string, client *fasthttp.Client) error {
|
||||
func doProxyRequestWithTimeout(c fiber.Ctx, proxyURL string, client *fasthttp.Client) error {
|
||||
// Calculate timeout from client configuration
|
||||
clientTimeout := time.Duration(cfg.Client.ClientTimeout) * time.Second
|
||||
if clientTimeout <= 0 {
|
||||
@@ -785,7 +785,7 @@ func doProxyRequestWithTimeout(c *fiber.Ctx, proxyURL string, client *fasthttp.C
|
||||
}
|
||||
|
||||
// handleGzippedResponse decompresses gzipped responses
|
||||
func handleGzippedResponse(c *fiber.Ctx) error {
|
||||
func handleGzippedResponse(c fiber.Ctx) error {
|
||||
if !bytes.EqualFold(c.Response().Header.Peek("Content-Encoding"), []byte("gzip")) {
|
||||
return nil
|
||||
}
|
||||
@@ -828,7 +828,7 @@ func handleGzippedResponse(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// logDebugRequest logs the request details when in debug mode with sanitization.
|
||||
func logDebugRequest(c *fiber.Ctx) {
|
||||
func logDebugRequest(c fiber.Ctx) {
|
||||
contentType := string(c.Request().Header.ContentType())
|
||||
sanitizedBody := sanitizeForLogging(c.Body(), contentType)
|
||||
sanitizedHeaders := sanitizeHeaders(convertHeaders(c.GetReqHeaders()))
|
||||
@@ -845,7 +845,7 @@ func logDebugRequest(c *fiber.Ctx) {
|
||||
}
|
||||
|
||||
// logDebugResponse logs the response details when in debug mode with sanitization.
|
||||
func logDebugResponse(c *fiber.Ctx) {
|
||||
func logDebugResponse(c fiber.Ctx) {
|
||||
contentType := string(c.Response().Header.ContentType())
|
||||
sanitizedBody := sanitizeForLogging(c.Response().Body(), contentType)
|
||||
sanitizedHeaders := sanitizeHeaders(convertHeaders(c.GetRespHeaders()))
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
fiber "github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/middleware/cors"
|
||||
"github.com/google/uuid"
|
||||
|
||||
graphql "github.com/lukaszraczylo/go-simple-graphql"
|
||||
@@ -42,19 +42,18 @@ func StartHTTPProxy() error {
|
||||
})
|
||||
|
||||
serverConfig := fiber.Config{
|
||||
DisableStartupMessage: true,
|
||||
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
|
||||
IdleTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
|
||||
ReadTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
|
||||
WriteTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
|
||||
JSONEncoder: json.Marshal,
|
||||
JSONDecoder: json.Unmarshal,
|
||||
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
|
||||
IdleTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
|
||||
ReadTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
|
||||
WriteTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
|
||||
JSONEncoder: json.Marshal,
|
||||
JSONDecoder: json.Unmarshal,
|
||||
}
|
||||
|
||||
server := fiber.New(serverConfig)
|
||||
|
||||
server.Use(cors.New(cors.Config{
|
||||
AllowOrigins: "*",
|
||||
AllowOrigins: []string{"*"},
|
||||
}))
|
||||
|
||||
server.Use(AddRequestUUID)
|
||||
@@ -71,7 +70,7 @@ func StartHTTPProxy() error {
|
||||
|
||||
// WebSocket support - must be registered before catch-all routes
|
||||
if cfg.WebSocket.Enable {
|
||||
server.Get("/v1/graphql", func(c *fiber.Ctx) error {
|
||||
server.Get("/v1/graphql", func(c fiber.Ctx) error {
|
||||
if IsWebSocketRequest(c) {
|
||||
wsp := GetWebSocketProxy()
|
||||
if wsp != nil {
|
||||
@@ -90,7 +89,7 @@ func StartHTTPProxy() error {
|
||||
Pairs: map[string]any{"port": cfg.Server.PortGraphQL},
|
||||
})
|
||||
|
||||
if err := server.Listen(fmt.Sprintf(":%d", cfg.Server.PortGraphQL)); err != nil {
|
||||
if err := server.Listen(fmt.Sprintf(":%d", cfg.Server.PortGraphQL), fiber.ListenConfig{DisableStartupMessage: true}); err != nil {
|
||||
return fmt.Errorf("failed to start HTTP proxy server on port %d: %w",
|
||||
cfg.Server.PortGraphQL, err)
|
||||
}
|
||||
@@ -99,18 +98,18 @@ func StartHTTPProxy() error {
|
||||
}
|
||||
|
||||
// proxyTheRequestToDefault proxies the request to the default GraphQL endpoint.
|
||||
func proxyTheRequestToDefault(c *fiber.Ctx) error {
|
||||
func proxyTheRequestToDefault(c fiber.Ctx) error {
|
||||
return proxyTheRequest(c, cfg.Server.HostGraphQL)
|
||||
}
|
||||
|
||||
// AddRequestUUID adds a unique request UUID to the context.
|
||||
func AddRequestUUID(c *fiber.Ctx) error {
|
||||
func AddRequestUUID(c fiber.Ctx) error {
|
||||
c.Locals("request_uuid", uuid.NewString())
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// checkAllowedURLs checks if the requested URL is allowed.
|
||||
func checkAllowedURLs(c *fiber.Ctx) bool {
|
||||
func checkAllowedURLs(c fiber.Ctx) bool {
|
||||
if len(allowedUrls) == 0 {
|
||||
return true
|
||||
}
|
||||
@@ -120,7 +119,7 @@ func checkAllowedURLs(c *fiber.Ctx) bool {
|
||||
}
|
||||
|
||||
// healthCheck performs a comprehensive health check on the GraphQL server and its dependencies.
|
||||
func healthCheck(c *fiber.Ctx) error {
|
||||
func healthCheck(c fiber.Ctx) error {
|
||||
// Prepare the response structure
|
||||
response := HealthCheckResponse{
|
||||
Status: "healthy",
|
||||
@@ -254,7 +253,7 @@ func healthCheck(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// processGraphQLRequest handles the incoming GraphQL requests.
|
||||
func processGraphQLRequest(c *fiber.Ctx) error {
|
||||
func processGraphQLRequest(c fiber.Ctx) error {
|
||||
startTime := time.Now()
|
||||
|
||||
// Extract user information and check permissions
|
||||
@@ -305,7 +304,7 @@ func processGraphQLRequest(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// extractUserInfo extracts user ID and role from request headers
|
||||
func extractUserInfo(c *fiber.Ctx) (string, string) {
|
||||
func extractUserInfo(c fiber.Ctx) (string, string) {
|
||||
extractedUserID := "-"
|
||||
extractedRoleName := "-"
|
||||
|
||||
@@ -326,7 +325,7 @@ func extractUserInfo(c *fiber.Ctx) (string, string) {
|
||||
}
|
||||
|
||||
// handleCaching manages the caching logic for GraphQL requests
|
||||
func handleCaching(c *fiber.Ctx, parsedResult *parseGraphQLQueryResult, userID, userRole string) (bool, error) {
|
||||
func handleCaching(c fiber.Ctx, parsedResult *parseGraphQLQueryResult, userID, userRole string) (bool, error) {
|
||||
// Calculate query hash for cache key - now includes user context for security
|
||||
calculatedQueryHash := libpack_cache.CalculateHash(c, userID, userRole)
|
||||
|
||||
@@ -380,7 +379,7 @@ func handleCaching(c *fiber.Ctx, parsedResult *parseGraphQLQueryResult, userID,
|
||||
}
|
||||
|
||||
// proxyAndCacheTheRequest proxies and caches the request if needed.
|
||||
func proxyAndCacheTheRequest(c *fiber.Ctx, queryCacheHash string, cacheTime int, currentEndpoint string) error {
|
||||
func proxyAndCacheTheRequest(c fiber.Ctx, queryCacheHash string, cacheTime int, currentEndpoint string) error {
|
||||
if err := proxyTheRequest(c, currentEndpoint); err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't proxy the request",
|
||||
@@ -396,7 +395,7 @@ func proxyAndCacheTheRequest(c *fiber.Ctx, queryCacheHash string, cacheTime int,
|
||||
}
|
||||
|
||||
// logAndMonitorRequest logs and monitors the request processing.
|
||||
func logAndMonitorRequest(c *fiber.Ctx, userID, opType, opName string, wasCached bool, duration time.Duration, startTime time.Time) {
|
||||
func logAndMonitorRequest(c fiber.Ctx, userID, opType, opName string, wasCached bool, duration time.Duration, startTime time.Time) {
|
||||
// Low-cardinality labels only: user_id and op_name dropped to prevent Prometheus explosion.
|
||||
labels := map[string]string{
|
||||
"op_type": opType,
|
||||
|
||||
+23
-23
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
@@ -20,11 +20,11 @@ import (
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestAddRequestUUID_SetsLocalsAndCallsNext(t *testing.T) {
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
app.Use(AddRequestUUID)
|
||||
|
||||
var captured string
|
||||
app.Get("/", func(c *fiber.Ctx) error {
|
||||
app.Get("/", func(c fiber.Ctx) error {
|
||||
if v, ok := c.Locals("request_uuid").(string); ok {
|
||||
captured = v
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func TestAddRequestUUID_SetsLocalsAndCallsNext(t *testing.T) {
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
resp, err := app.Test(req, -1)
|
||||
resp, err := app.Test(req, fiber.TestConfig{Timeout: 0})
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
@@ -51,11 +51,11 @@ func TestAddRequestUUID_SetsLocalsAndCallsNext(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAddRequestUUID_UniquePerRequest(t *testing.T) {
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
app.Use(AddRequestUUID)
|
||||
|
||||
seen := make([]string, 0, 5)
|
||||
app.Get("/", func(c *fiber.Ctx) error {
|
||||
app.Get("/", func(c fiber.Ctx) error {
|
||||
if v, ok := c.Locals("request_uuid").(string); ok {
|
||||
seen = append(seen, v)
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func TestAddRequestUUID_UniquePerRequest(t *testing.T) {
|
||||
|
||||
for i := range 5 {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
resp, err := app.Test(req, -1)
|
||||
resp, err := app.Test(req, fiber.TestConfig{Timeout: 0})
|
||||
if err != nil {
|
||||
t.Fatalf("request %d: %v", i, err)
|
||||
}
|
||||
@@ -89,12 +89,12 @@ func TestHealthCheck_Returns200WithJSON(t *testing.T) {
|
||||
parseConfig()
|
||||
_ = StartMonitoringServer()
|
||||
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
app.Get("/health", healthCheck)
|
||||
|
||||
// Pass check_graphql=false to avoid real network call
|
||||
req := httptest.NewRequest("GET", "/health?check_graphql=false&check_redis=false", nil)
|
||||
resp, err := app.Test(req, 10000)
|
||||
resp, err := app.Test(req, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
@@ -135,11 +135,11 @@ func TestHealthCheck_UnhealthyWhenGraphQLDown(t *testing.T) {
|
||||
cfgMutex.Unlock()
|
||||
}()
|
||||
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
app.Get("/health", healthCheck)
|
||||
|
||||
req := httptest.NewRequest("GET", "/health?check_redis=false", nil)
|
||||
resp, err := app.Test(req, 15000)
|
||||
resp, err := app.Test(req, fiber.TestConfig{Timeout: 15000 * time.Millisecond})
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
@@ -190,14 +190,14 @@ func TestProcessGraphQLRequest_ValidBodyProxiesToBackend(t *testing.T) {
|
||||
cfgMutex.Unlock()
|
||||
}()
|
||||
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
app.Post("/*", processGraphQLRequest)
|
||||
|
||||
body := `{"query":"query { __typename }"}`
|
||||
req := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := app.Test(req, 10000)
|
||||
resp, err := app.Test(req, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
@@ -236,7 +236,7 @@ func TestProcessGraphQLRequest_MalformedBodyStillHandled(t *testing.T) {
|
||||
cfgMutex.Unlock()
|
||||
}()
|
||||
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
app.Post("/*", processGraphQLRequest)
|
||||
|
||||
// Not valid JSON — proxy should still forward or return gracefully
|
||||
@@ -244,7 +244,7 @@ func TestProcessGraphQLRequest_MalformedBodyStillHandled(t *testing.T) {
|
||||
req := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := app.Test(req, 10000)
|
||||
resp, err := app.Test(req, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
@@ -302,7 +302,7 @@ func TestHandleCaching_CacheHitReturnsStoredResponse(t *testing.T) {
|
||||
cfgMutex.Unlock()
|
||||
}()
|
||||
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
app.Post("/*", processGraphQLRequest)
|
||||
|
||||
queryBody := `{"query":"query { users { id } }"}`
|
||||
@@ -310,7 +310,7 @@ func TestHandleCaching_CacheHitReturnsStoredResponse(t *testing.T) {
|
||||
// First request — cache miss, hits backend
|
||||
req1 := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(queryBody))
|
||||
req1.Header.Set("Content-Type", "application/json")
|
||||
resp1, err := app.Test(req1, 10000)
|
||||
resp1, err := app.Test(req1, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
|
||||
if err != nil {
|
||||
t.Fatalf("first request: %v", err)
|
||||
}
|
||||
@@ -323,7 +323,7 @@ func TestHandleCaching_CacheHitReturnsStoredResponse(t *testing.T) {
|
||||
// Second identical request — should hit cache
|
||||
req2 := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(queryBody))
|
||||
req2.Header.Set("Content-Type", "application/json")
|
||||
resp2, err := app.Test(req2, 10000)
|
||||
resp2, err := app.Test(req2, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
|
||||
if err != nil {
|
||||
t.Fatalf("second request: %v", err)
|
||||
}
|
||||
@@ -380,7 +380,7 @@ func TestHandleCaching_CacheMissProxiesRequest(t *testing.T) {
|
||||
cfgMutex.Unlock()
|
||||
}()
|
||||
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
app.Post("/*", processGraphQLRequest)
|
||||
|
||||
// Unique query so no prior cache entry
|
||||
@@ -388,7 +388,7 @@ func TestHandleCaching_CacheMissProxiesRequest(t *testing.T) {
|
||||
req := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(queryBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := app.Test(req, 10000)
|
||||
resp, err := app.Test(req, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
@@ -430,10 +430,10 @@ func TestHandleCaching_DirectCacheHitBranch(t *testing.T) {
|
||||
cfgMutex.Unlock()
|
||||
}()
|
||||
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
|
||||
var wasCachedResult bool
|
||||
app.Post("/test", func(c *fiber.Ctx) error {
|
||||
app.Post("/test", func(c fiber.Ctx) error {
|
||||
parsedResult := &parseGraphQLQueryResult{
|
||||
cacheTime: 60,
|
||||
cacheRequest: true,
|
||||
@@ -507,7 +507,7 @@ func TestHandleCaching_NoCacheEnabled_ProxiesDirect(t *testing.T) {
|
||||
cfgMutex.Unlock()
|
||||
}()
|
||||
|
||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
app := fiber.New()
|
||||
|
||||
reqCtx := &fasthttp.RequestCtx{}
|
||||
reqCtx.Request.SetRequestURI("/v1/graphql")
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
+4
-4
@@ -9,8 +9,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/websocket/v2"
|
||||
"github.com/gofiber/contrib/v3/websocket"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
gorillaws "github.com/gorilla/websocket"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||
@@ -79,7 +79,7 @@ func NewWebSocketProxy(backendURL string, config WebSocketConfig, logger *libpac
|
||||
}
|
||||
|
||||
// HandleWebSocket upgrades the connection and proxies WebSocket traffic
|
||||
func (wsp *WebSocketProxy) HandleWebSocket(c *fiber.Ctx) error {
|
||||
func (wsp *WebSocketProxy) HandleWebSocket(c fiber.Ctx) error {
|
||||
if !wsp.enabled {
|
||||
return fiber.NewError(fiber.StatusNotImplemented, "WebSocket support is disabled")
|
||||
}
|
||||
@@ -477,7 +477,7 @@ func (wsp *WebSocketProxy) GetStats() map[string]any {
|
||||
}
|
||||
|
||||
// IsWebSocketRequest checks if the request is a WebSocket upgrade request
|
||||
func IsWebSocketRequest(c *fiber.Ctx) bool {
|
||||
func IsWebSocketRequest(c fiber.Ctx) bool {
|
||||
return websocket.IsWebSocketUpgrade(c) ||
|
||||
c.Get("Upgrade") == "websocket" ||
|
||||
c.Get("Connection") == "Upgrade"
|
||||
|
||||
Reference in New Issue
Block a user