mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-30 07:26:58 +00:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bafb94c742 | ||
|
|
55fc2ae1de | ||
|
|
877325a633 | ||
|
|
7e0d1e19b1 | ||
|
|
5bbe986656 | ||
|
|
01d1de1f0b | ||
|
|
1bff79e4f4 | ||
|
|
b6e83f2837 | ||
|
|
287289cd80 | ||
|
|
21b429c98a | ||
|
|
d96d2f429f |
@@ -21,7 +21,7 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
|
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: "1.25"
|
||||||
docker-enabled: true
|
docker-enabled: true
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: "1.25"
|
||||||
|
|
||||||
- name: Run benchmarks
|
- name: Run benchmarks
|
||||||
run: go test -bench=. -benchmem ./... -run=^# | tee output.txt
|
run: go test -bench=. -benchmem ./... -run=^# | tee output.txt
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ builds:
|
|||||||
- arm64
|
- arm64
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w
|
- -s -w
|
||||||
|
- -X main.appVersion={{.Version}}
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- id: graphql-proxy
|
- id: graphql-proxy
|
||||||
|
|||||||
@@ -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`).
|
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:**
|
**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
|
- **System health**: Backend GraphQL and Redis connectivity status
|
||||||
- **Circuit breaker**: Current state, configuration, and statistics
|
- **Circuit breaker**: Current state, configuration, and statistics
|
||||||
- **Request coalescing**: Deduplication rate and backend savings
|
- **Request coalescing**: Deduplication rate and backend savings
|
||||||
- **Retry budget**: Available tokens and denial rate
|
- **Retry budget**: Available tokens and denial rate
|
||||||
- **WebSocket**: Active connections and message statistics
|
- **WebSocket**: Active proxied connections
|
||||||
- **Connection pool**: Active connections and health status
|
- **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:**
|
**Configuration:**
|
||||||
```bash
|
```bash
|
||||||
@@ -1068,16 +1071,29 @@ GMP_ADMIN_DASHBOARD_ENABLE=true
|
|||||||
- Retry budget: Current tokens, max tokens, total attempts, denied retries, and denial rate
|
- Retry budget: Current tokens, max tokens, total attempts, denied retries, and denial rate
|
||||||
- Control actions: Reset statistics, clear cache
|
- 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:**
|
**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/health` - System health status
|
||||||
- `GET /admin/api/circuit-breaker` - Circuit breaker 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/coalescing` - Request coalescing statistics
|
||||||
- `GET /admin/api/retry-budget` - Retry budget statistics
|
- `GET /admin/api/retry-budget` - Retry budget statistics
|
||||||
- `GET /admin/api/websocket` - WebSocket connection statistics
|
- `GET /admin/api/websocket` - WebSocket connection statistics
|
||||||
- `GET /admin/api/connections` - Connection pool 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/coalescing/reset` - Reset coalescing stats
|
||||||
- `POST /admin/api/retry-budget/reset` - Reset retry budget stats
|
- `POST /admin/api/retry-budget/reset` - Reset retry budget stats
|
||||||
|
- `POST /admin/api/cluster/force-publish` - Force an immediate metrics publish (diagnostics)
|
||||||
|
|
||||||
**Screenshot:**
|
**Screenshot:**
|
||||||

|

|
||||||
@@ -1119,3 +1135,16 @@ graphql_proxy_cache_hit{microservice="graphql_proxy",pod="hasura-w-proxy-interna
|
|||||||
graphql_proxy_cache_hit{pod="hasura-w-proxy-internal-6b5f4b4bbb-9xwfc",microservice="graphql_proxy"} 1
|
graphql_proxy_cache_hit{pod="hasura-w-proxy-internal-6b5f4b4bbb-9xwfc",microservice="graphql_proxy"} 1
|
||||||
graphql_proxy_cache_miss{microservice="graphql_proxy",pod="hasura-w-proxy-internal-6b5f4b4bbb-9xwfc"} 23
|
graphql_proxy_cache_miss{microservice="graphql_proxy",pod="hasura-w-proxy-internal-6b5f4b4bbb-9xwfc"} 23
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Telemetry
|
||||||
|
|
||||||
|
On startup this binary sends a single anonymous adoption ping — project name,
|
||||||
|
version, timestamp; no identifiers, no GraphQL operations, no query/response
|
||||||
|
content. Fire-and-forget with a 2-second timeout; cannot block startup or
|
||||||
|
panic.
|
||||||
|
|
||||||
|
See **[oss-telemetry — Disabling telemetry](https://github.com/lukaszraczylo/oss-telemetry#disabling-telemetry)**
|
||||||
|
for the exact wire format, source, and full opt-out documentation.
|
||||||
|
|
||||||
|
Quick opt-out: set any of `DO_NOT_TRACK=1`, `OSS_TELEMETRY_DISABLED=1`,
|
||||||
|
or `GRAPHQL_MONITORING_PROXY_DISABLE_TELEMETRY=1`.
|
||||||
|
|||||||
+949
-1339
File diff suppressed because it is too large
Load Diff
+38
-23
@@ -7,8 +7,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/contrib/v3/websocket"
|
||||||
"github.com/gofiber/websocket/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||||
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
|
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
|
||||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
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
|
// 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")
|
data, err := dashboardHTML.ReadFile("admin/dashboard.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(500).SendString("Failed to load dashboard")
|
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
|
// getStats returns overall proxy statistics
|
||||||
// In cluster mode (when metrics aggregator is available), returns aggregated stats from all instances
|
// 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
|
// Check if cluster mode is enabled - if so, return aggregated stats
|
||||||
if aggregator := GetMetricsAggregator(); aggregator != nil {
|
if aggregator := GetMetricsAggregator(); aggregator != nil {
|
||||||
metrics, err := aggregator.GetAggregatedMetrics()
|
metrics, err := aggregator.GetAggregatedMetrics()
|
||||||
@@ -212,7 +212,7 @@ func formatDuration(d time.Duration) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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()
|
||||||
|
|
||||||
health := map[string]any{
|
health := map[string]any{
|
||||||
@@ -241,7 +241,7 @@ func (ad *AdminDashboard) getHealth(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getCircuitBreakerStatus returns circuit breaker status
|
// 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{
|
status := map[string]any{
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"state": "unknown",
|
"state": "unknown",
|
||||||
@@ -279,7 +279,7 @@ func (ad *AdminDashboard) getCircuitBreakerStatus(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// getCacheStats returns cache statistics
|
// getCacheStats returns cache statistics
|
||||||
// In cluster mode, returns aggregated cache stats from all instances
|
// 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
|
// Check if cluster mode is enabled - if so, return aggregated cache stats
|
||||||
if aggregator := GetMetricsAggregator(); aggregator != nil {
|
if aggregator := GetMetricsAggregator(); aggregator != nil {
|
||||||
metrics, err := aggregator.GetAggregatedMetrics()
|
metrics, err := aggregator.GetAggregatedMetrics()
|
||||||
@@ -383,7 +383,7 @@ func (ad *AdminDashboard) getCacheStats(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getConnectionStats returns connection pool statistics
|
// getConnectionStats returns connection pool statistics
|
||||||
func (ad *AdminDashboard) getConnectionStats(c *fiber.Ctx) error {
|
func (ad *AdminDashboard) getConnectionStats(c fiber.Ctx) error {
|
||||||
poolMgr := GetConnectionPoolManager()
|
poolMgr := GetConnectionPoolManager()
|
||||||
|
|
||||||
stats := map[string]any{
|
stats := map[string]any{
|
||||||
@@ -399,7 +399,7 @@ func (ad *AdminDashboard) getConnectionStats(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getRetryBudgetStats returns retry budget statistics
|
// getRetryBudgetStats returns retry budget statistics
|
||||||
func (ad *AdminDashboard) getRetryBudgetStats(c *fiber.Ctx) error {
|
func (ad *AdminDashboard) getRetryBudgetStats(c fiber.Ctx) error {
|
||||||
rb := GetRetryBudget()
|
rb := GetRetryBudget()
|
||||||
|
|
||||||
if rb == nil {
|
if rb == nil {
|
||||||
@@ -412,7 +412,7 @@ func (ad *AdminDashboard) getRetryBudgetStats(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getCoalescingStats returns request coalescing statistics
|
// getCoalescingStats returns request coalescing statistics
|
||||||
func (ad *AdminDashboard) getCoalescingStats(c *fiber.Ctx) error {
|
func (ad *AdminDashboard) getCoalescingStats(c fiber.Ctx) error {
|
||||||
rc := GetRequestCoalescer()
|
rc := GetRequestCoalescer()
|
||||||
|
|
||||||
if rc == nil {
|
if rc == nil {
|
||||||
@@ -425,7 +425,7 @@ func (ad *AdminDashboard) getCoalescingStats(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getWebSocketStats returns WebSocket statistics
|
// getWebSocketStats returns WebSocket statistics
|
||||||
func (ad *AdminDashboard) getWebSocketStats(c *fiber.Ctx) error {
|
func (ad *AdminDashboard) getWebSocketStats(c fiber.Ctx) error {
|
||||||
wsp := GetWebSocketProxy()
|
wsp := GetWebSocketProxy()
|
||||||
|
|
||||||
if wsp == nil {
|
if wsp == nil {
|
||||||
@@ -438,7 +438,7 @@ func (ad *AdminDashboard) getWebSocketStats(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// clearCache clears the cache
|
// clearCache clears the cache
|
||||||
func (ad *AdminDashboard) clearCache(c *fiber.Ctx) error {
|
func (ad *AdminDashboard) clearCache(c fiber.Ctx) error {
|
||||||
libpack_cache.CacheClear()
|
libpack_cache.CacheClear()
|
||||||
return c.JSON(map[string]any{
|
return c.JSON(map[string]any{
|
||||||
"success": true,
|
"success": true,
|
||||||
@@ -447,7 +447,7 @@ func (ad *AdminDashboard) clearCache(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resetRetryBudget resets retry budget statistics
|
// resetRetryBudget resets retry budget statistics
|
||||||
func (ad *AdminDashboard) resetRetryBudget(c *fiber.Ctx) error {
|
func (ad *AdminDashboard) resetRetryBudget(c fiber.Ctx) error {
|
||||||
rb := GetRetryBudget()
|
rb := GetRetryBudget()
|
||||||
if rb != nil {
|
if rb != nil {
|
||||||
rb.Reset()
|
rb.Reset()
|
||||||
@@ -460,7 +460,7 @@ func (ad *AdminDashboard) resetRetryBudget(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resetCoalescing resets coalescing statistics
|
// resetCoalescing resets coalescing statistics
|
||||||
func (ad *AdminDashboard) resetCoalescing(c *fiber.Ctx) error {
|
func (ad *AdminDashboard) resetCoalescing(c fiber.Ctx) error {
|
||||||
rc := GetRequestCoalescer()
|
rc := GetRequestCoalescer()
|
||||||
if rc != nil {
|
if rc != nil {
|
||||||
rc.Reset()
|
rc.Reset()
|
||||||
@@ -473,7 +473,7 @@ func (ad *AdminDashboard) resetCoalescing(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getClusterStats returns aggregated statistics from all proxy instances
|
// 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()
|
aggregator := GetMetricsAggregator()
|
||||||
if aggregator == nil {
|
if aggregator == nil {
|
||||||
return c.Status(503).JSON(map[string]any{
|
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
|
// 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()
|
aggregator := GetMetricsAggregator()
|
||||||
if aggregator == nil {
|
if aggregator == nil {
|
||||||
return c.Status(503).JSON(map[string]any{
|
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
|
// 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()
|
aggregator := GetMetricsAggregator()
|
||||||
|
|
||||||
debug := map[string]any{
|
debug := map[string]any{
|
||||||
@@ -603,7 +603,7 @@ func getMapKeys(m map[string]any) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// forcePublish forces an immediate metrics publish for testing
|
// 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()
|
aggregator := GetMetricsAggregator()
|
||||||
if aggregator == nil {
|
if aggregator == nil {
|
||||||
return c.Status(503).JSON(map[string]any{
|
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()
|
go aggregator.publishMetrics()
|
||||||
|
|
||||||
return c.JSON(map[string]any{
|
return c.JSON(map[string]any{
|
||||||
@@ -670,6 +675,16 @@ func (ad *AdminDashboard) handleStatsWebSocket(c *websocket.Conn) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Determine the requested view. "local" forces this-instance stats; any
|
||||||
|
// other value (default) is cluster-aware and returns aggregated metrics
|
||||||
|
// when a metrics aggregator is available. This makes the dashboard's
|
||||||
|
// "Cluster view" toggle a real control rather than cosmetic, since the
|
||||||
|
// WebSocket stream is the primary data path.
|
||||||
|
gather := ad.gatherAllStatsClusterAware
|
||||||
|
if c.Query("view") == "local" {
|
||||||
|
gather = ad.gatherAllStats
|
||||||
|
}
|
||||||
|
|
||||||
// Channel to signal when to stop
|
// Channel to signal when to stop
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
|
||||||
@@ -694,8 +709,8 @@ func (ad *AdminDashboard) handleStatsWebSocket(c *websocket.Conn) {
|
|||||||
enc := json.NewEncoder(&buf)
|
enc := json.NewEncoder(&buf)
|
||||||
enc.SetEscapeHTML(false)
|
enc.SetEscapeHTML(false)
|
||||||
|
|
||||||
// Send initial stats immediately (cluster-aware for dashboard)
|
// Send initial stats immediately (honours the requested view)
|
||||||
if stats := ad.gatherAllStatsClusterAware(); stats != nil {
|
if stats := gather(); stats != nil {
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
if err := enc.Encode(stats); err == nil {
|
if err := enc.Encode(stats); err == nil {
|
||||||
// json.Encoder.Encode appends a trailing newline; strip it
|
// json.Encoder.Encode appends a trailing newline; strip it
|
||||||
@@ -708,8 +723,8 @@ func (ad *AdminDashboard) handleStatsWebSocket(c *websocket.Conn) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
// Gather all stats (cluster-aware for dashboard)
|
// Gather all stats (honours the requested view)
|
||||||
stats := ad.gatherAllStatsClusterAware()
|
stats := gather()
|
||||||
|
|
||||||
// Encode into reused buffer (no per-tick allocation churn)
|
// Encode into reused buffer (no per-tick allocation churn)
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -76,10 +76,13 @@ func TestAdminDashboard_ServeDashboard(t *testing.T) {
|
|||||||
contentType := resp.Header.Get("Content-Type")
|
contentType := resp.Header.Get("Content-Type")
|
||||||
assert.Contains(t, contentType, "text/html")
|
assert.Contains(t, contentType, "text/html")
|
||||||
|
|
||||||
// Verify HTML content is returned
|
// Verify the dashboard HTML is returned (assert on stable markers, not
|
||||||
|
// the cosmetic page title which is part of the UI and may change).
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, string(body), "GraphQL Proxy Admin Dashboard")
|
assert.Contains(t, string(body), "<!DOCTYPE html>")
|
||||||
|
assert.Contains(t, string(body), "graphql-proxy")
|
||||||
|
assert.Contains(t, string(body), "/admin/ws/stats")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAdminDashboard_GetStats(t *testing.T) {
|
func TestAdminDashboard_GetStats(t *testing.T) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
fiber "github.com/gofiber/fiber/v2"
|
fiber "github.com/gofiber/fiber/v3"
|
||||||
"github.com/gofrs/flock"
|
"github.com/gofrs/flock"
|
||||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||||
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
|
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
|
||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
var bannedUsersIDs sync.Map // key: userID string, value: reason string
|
var bannedUsersIDs sync.Map // key: userID string, value: reason string
|
||||||
|
|
||||||
// authMiddleware provides API key authentication for admin endpoints
|
// 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")
|
apiKey := c.Get("X-API-Key")
|
||||||
|
|
||||||
// Get expected key from config (try GMP_ prefix first, then fallback)
|
// 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{
|
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")
|
api := apiserver.Group("/api")
|
||||||
@@ -97,7 +96,7 @@ func enableApi(ctx context.Context) error {
|
|||||||
// Start server in a goroutine and handle shutdown
|
// Start server in a goroutine and handle shutdown
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
go func() {
|
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
|
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)
|
_, found := bannedUsersIDs.Load(userID)
|
||||||
|
|
||||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||||
@@ -158,7 +157,7 @@ func checkIfUserIsBanned(c *fiber.Ctx, userID string) bool {
|
|||||||
return found
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiClearCache(c *fiber.Ctx) error {
|
func apiClearCache(c fiber.Ctx) error {
|
||||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||||
Message: "Clearing cache via API",
|
Message: "Clearing cache via API",
|
||||||
})
|
})
|
||||||
@@ -169,12 +168,12 @@ func apiClearCache(c *fiber.Ctx) error {
|
|||||||
return c.SendString("OK: cache cleared")
|
return c.SendString("OK: cache cleared")
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiCacheStats(c *fiber.Ctx) error {
|
func apiCacheStats(c fiber.Ctx) error {
|
||||||
return c.JSON(libpack_cache.GetCacheStats())
|
return c.JSON(libpack_cache.GetCacheStats())
|
||||||
}
|
}
|
||||||
|
|
||||||
// apiCircuitBreakerHealth returns the health status of the circuit breaker
|
// apiCircuitBreakerHealth returns the health status of the circuit breaker
|
||||||
func apiCircuitBreakerHealth(c *fiber.Ctx) error {
|
func apiCircuitBreakerHealth(c fiber.Ctx) error {
|
||||||
if cb == nil {
|
if cb == nil {
|
||||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
||||||
"status": "disabled",
|
"status": "disabled",
|
||||||
@@ -232,9 +231,9 @@ type apiBanUserRequest struct {
|
|||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiBanUser(c *fiber.Ctx) error {
|
func apiBanUser(c fiber.Ctx) error {
|
||||||
var req apiBanUserRequest
|
var req apiBanUserRequest
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.Bind().Body(&req); err != nil {
|
||||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||||
Message: "Can't parse the ban user request",
|
Message: "Can't parse the ban user request",
|
||||||
Pairs: map[string]any{"error": err.Error()},
|
Pairs: map[string]any{"error": err.Error()},
|
||||||
@@ -260,9 +259,9 @@ func apiBanUser(c *fiber.Ctx) error {
|
|||||||
return c.SendString("OK: user banned")
|
return c.SendString("OK: user banned")
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiUnbanUser(c *fiber.Ctx) error {
|
func apiUnbanUser(c fiber.Ctx) error {
|
||||||
var req apiBanUserRequest
|
var req apiBanUserRequest
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.Bind().Body(&req); err != nil {
|
||||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||||
Message: "Can't parse the unban user request",
|
Message: "Can't parse the unban user request",
|
||||||
Pairs: map[string]any{"error": err.Error()},
|
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
|
// apiBackendHealth returns the health status of the GraphQL backend
|
||||||
func apiBackendHealth(c *fiber.Ctx) error {
|
func apiBackendHealth(c fiber.Ctx) error {
|
||||||
healthMgr := GetBackendHealthManager()
|
healthMgr := GetBackendHealthManager()
|
||||||
if healthMgr == nil {
|
if healthMgr == nil {
|
||||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
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
|
// apiConnectionPoolHealth returns the health status of the connection pool
|
||||||
func apiConnectionPoolHealth(c *fiber.Ctx) error {
|
func apiConnectionPoolHealth(c fiber.Ctx) error {
|
||||||
poolMgr := GetConnectionPoolManager()
|
poolMgr := GetConnectionPoolManager()
|
||||||
if poolMgr == nil {
|
if poolMgr == nil {
|
||||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
||||||
|
|||||||
+10
-37
@@ -11,9 +11,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
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"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
@@ -55,9 +54,7 @@ func (suite *APIAuthSecurityTestSuite) SetupTest() {
|
|||||||
suite.validAPIKey = "test-secure-api-key-12345"
|
suite.validAPIKey = "test-secure-api-key-12345"
|
||||||
|
|
||||||
// Create test Fiber app with authentication
|
// Create test Fiber app with authentication
|
||||||
suite.app = fiber.New(fiber.Config{
|
suite.app = fiber.New(fiber.Config{})
|
||||||
DisableStartupMessage: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Setup API routes with authentication middleware
|
// Setup API routes with authentication middleware
|
||||||
api := suite.app.Group("/api")
|
api := suite.app.Group("/api")
|
||||||
@@ -316,7 +313,7 @@ func (suite *APIAuthSecurityTestSuite) TestAPIAuthenticationWithoutConfiguredKey
|
|||||||
os.Unsetenv("ADMIN_API_KEY")
|
os.Unsetenv("ADMIN_API_KEY")
|
||||||
|
|
||||||
// Create new app without configured API key
|
// Create new app without configured API key
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
api := app.Group("/api")
|
api := app.Group("/api")
|
||||||
api.Use(authMiddleware)
|
api.Use(authMiddleware)
|
||||||
api.Post("/user-ban", apiBanUser)
|
api.Post("/user-ban", apiBanUser)
|
||||||
@@ -356,11 +353,7 @@ func (suite *APIAuthSecurityTestSuite) TestTimingAttackResistance() {
|
|||||||
"", // Empty
|
"", // Empty
|
||||||
}
|
}
|
||||||
|
|
||||||
timings := make([]time.Duration, len(invalidKeys))
|
for _, key := range invalidKeys {
|
||||||
|
|
||||||
for i, key := range invalidKeys {
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", "/api/user-ban",
|
req, err := http.NewRequest("POST", "/api/user-ban",
|
||||||
bytes.NewBuffer([]byte(`{"user_id": "test", "reason": "test"}`)))
|
bytes.NewBuffer([]byte(`{"user_id": "test", "reason": "test"}`)))
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
@@ -370,35 +363,15 @@ func (suite *APIAuthSecurityTestSuite) TestTimingAttackResistance() {
|
|||||||
resp, err := suite.app.Test(req)
|
resp, err := suite.app.Test(req)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
timings[i] = time.Since(start)
|
|
||||||
|
|
||||||
suite.Equal(401, resp.StatusCode,
|
suite.Equal(401, resp.StatusCode,
|
||||||
"All invalid keys should return 401, key: %s", key)
|
"All invalid keys should return 401, key: %s", key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that timing variations are minimal (within reasonable bounds)
|
// Timing-attack resistance is guaranteed by subtle.ConstantTimeCompare in
|
||||||
// This is a heuristic test - timing attack resistance is primarily
|
// authMiddleware (verified by code, not by wall-clock measurement). Asserting
|
||||||
// achieved by the subtle.ConstantTimeCompare function
|
// on a max/min wall-clock ratio across full HTTP round-trips is unreliable:
|
||||||
var minTime, maxTime time.Duration
|
// it is dominated by scheduler/GC noise and produces flaky failures, so we
|
||||||
for i, timing := range timings {
|
// intentionally do not assert on the timing ratio.
|
||||||
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)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestConcurrentAPIAuthentication tests authentication under concurrent load
|
// TestConcurrentAPIAuthentication tests authentication under concurrent load
|
||||||
@@ -616,7 +589,7 @@ func BenchmarkAPIAuthentication(b *testing.B) {
|
|||||||
os.Setenv("GMP_ADMIN_API_KEY", validAPIKey)
|
os.Setenv("GMP_ADMIN_API_KEY", validAPIKey)
|
||||||
defer os.Unsetenv("GMP_ADMIN_API_KEY")
|
defer os.Unsetenv("GMP_ADMIN_API_KEY")
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
api := app.Group("/api")
|
api := app.Group("/api")
|
||||||
api.Use(authMiddleware)
|
api.Use(authMiddleware)
|
||||||
api.Get("/cache-stats", apiCacheStats)
|
api.Get("/cache-stats", apiCacheStats)
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
fiber "github.com/gofiber/fiber/v2"
|
fiber "github.com/gofiber/fiber/v3"
|
||||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/gofrs/flock"
|
"github.com/gofrs/flock"
|
||||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
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"
|
||||||
|
|||||||
Vendored
+2
-2
@@ -11,7 +11,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
fiber "github.com/gofiber/fiber/v2"
|
fiber "github.com/gofiber/fiber/v3"
|
||||||
"github.com/gookit/goutil/strutil"
|
"github.com/gookit/goutil/strutil"
|
||||||
libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
|
libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
|
||||||
libpack_cache_redis "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/redis"
|
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
|
// - Same query, same variables, different user → different cache key
|
||||||
//
|
//
|
||||||
// Different variable values will always produce different cache keys.
|
// 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())
|
cacheKeyData := string(c.Body())
|
||||||
|
|
||||||
// Include user context in cache key (default behavior for security)
|
// Include user context in cache key (default behavior for security)
|
||||||
|
|||||||
Vendored
+1
-1
@@ -5,7 +5,7 @@ import (
|
|||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
|
libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
|
||||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||||
"github.com/sony/gobreaker"
|
"github.com/sony/gobreaker"
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestInitCircuitBreaker_NilMonitoring_NoPanic is a regression test for a boot
|
||||||
|
// panic: enabling the circuit breaker (ENABLE_CIRCUIT_BREAKER=true) crashed at
|
||||||
|
// startup because parseConfig calls initCircuitBreaker before the monitoring
|
||||||
|
// server assigns cfg.Monitoring (StartMonitoringServer runs later). The metrics
|
||||||
|
// gauge registration then dereferenced a nil *MetricsSetup.
|
||||||
|
//
|
||||||
|
// The breaker (and the admin dashboard, which reads gobreaker state directly)
|
||||||
|
// must initialise without panicking even when monitoring is not yet available.
|
||||||
|
func TestInitCircuitBreaker_NilMonitoring_NoPanic(t *testing.T) {
|
||||||
|
origCfg := cfg
|
||||||
|
cbMutex.Lock()
|
||||||
|
origCb, origMetrics := cb, cbMetrics
|
||||||
|
cb, cbMetrics = nil, nil
|
||||||
|
cbMutex.Unlock()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
cbMutex.Lock()
|
||||||
|
cb, cbMetrics = origCb, origMetrics
|
||||||
|
cbMutex.Unlock()
|
||||||
|
cfg = origCfg
|
||||||
|
})
|
||||||
|
|
||||||
|
cfg = &config{}
|
||||||
|
cfg.Logger = libpack_logger.New().SetOutput(&bytes.Buffer{})
|
||||||
|
cfg.Monitoring = nil // the production state when parseConfig runs
|
||||||
|
cfg.CircuitBreaker.Enable = true
|
||||||
|
cfg.CircuitBreaker.MaxFailures = 3
|
||||||
|
cfg.CircuitBreaker.Timeout = 5
|
||||||
|
cfg.CircuitBreaker.MaxRequestsInHalfOpen = 2
|
||||||
|
|
||||||
|
require.NotPanics(t, func() { initCircuitBreaker(cfg) },
|
||||||
|
"initCircuitBreaker must not panic when cfg.Monitoring is nil")
|
||||||
|
require.NotNil(t, cb, "circuit breaker must initialise even without monitoring")
|
||||||
|
require.NotNil(t, cbMetrics, "circuit breaker metrics manager must be created")
|
||||||
|
}
|
||||||
+11
-11
@@ -19,8 +19,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/contrib/v3/websocket"
|
||||||
"github.com/gofiber/websocket/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
gorillaws "github.com/gorilla/websocket"
|
gorillaws "github.com/gorilla/websocket"
|
||||||
libpack_cache_mem "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
|
libpack_cache_mem "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
|
||||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||||
@@ -38,8 +38,8 @@ import (
|
|||||||
func TestHandleWebSocket_DisabledReturns501(t *testing.T) {
|
func TestHandleWebSocket_DisabledReturns501(t *testing.T) {
|
||||||
wsp := NewWebSocketProxy("http://127.0.0.1:1", WebSocketConfig{Enabled: false}, libpack_logger.New(), nil)
|
wsp := NewWebSocketProxy("http://127.0.0.1:1", WebSocketConfig{Enabled: false}, libpack_logger.New(), nil)
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
app.Get("/ws", func(c *fiber.Ctx) error {
|
app.Get("/ws", func(c fiber.Ctx) error {
|
||||||
return wsp.HandleWebSocket(c)
|
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-Version", "13")
|
||||||
req.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, fiber.StatusNotImplemented, resp.StatusCode)
|
assert.Equal(t, fiber.StatusNotImplemented, resp.StatusCode)
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ func TestHandleWebSocket_BackendDialFail(t *testing.T) {
|
|||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
app.Get("/ws", websocket.New(func(c *websocket.Conn) {
|
app.Get("/ws", websocket.New(func(c *websocket.Conn) {
|
||||||
wsp.handleConnection(context.Background(), c, http.Header{})
|
wsp.handleConnection(context.Background(), c, http.Header{})
|
||||||
}))
|
}))
|
||||||
@@ -131,9 +131,9 @@ func TestIsWebSocketRequest(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
var got bool
|
var got bool
|
||||||
app.Get("/chk", func(c *fiber.Ctx) error {
|
app.Get("/chk", func(c fiber.Ctx) error {
|
||||||
got = IsWebSocketRequest(c)
|
got = IsWebSocketRequest(c)
|
||||||
return c.SendStatus(200)
|
return c.SendStatus(200)
|
||||||
})
|
})
|
||||||
@@ -142,7 +142,7 @@ func TestIsWebSocketRequest(t *testing.T) {
|
|||||||
for k, v := range tt.headers {
|
for k, v := range tt.headers {
|
||||||
req.Header.Set(k, v)
|
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)
|
require.NoError(t, err)
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ func TestHandleStatsWebSocket_ReceivesInitialMessage(t *testing.T) {
|
|||||||
_ = StartMonitoringServer()
|
_ = StartMonitoringServer()
|
||||||
|
|
||||||
dashboard := NewAdminDashboard(libpack_logger.New())
|
dashboard := NewAdminDashboard(libpack_logger.New())
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
dashboard.RegisterRoutes(app)
|
dashboard.RegisterRoutes(app)
|
||||||
|
|
||||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
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
|
// Use an isolated logger — not the global cfg.Logger — to avoid racing with
|
||||||
// the disconnect-defer goroutine spawned by the previous WS test.
|
// the disconnect-defer goroutine spawned by the previous WS test.
|
||||||
dashboard := NewAdminDashboard(libpack_logger.New())
|
dashboard := NewAdminDashboard(libpack_logger.New())
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
dashboard.RegisterRoutes(app)
|
dashboard.RegisterRoutes(app)
|
||||||
|
|
||||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
|||||||
+16
-4
@@ -18,6 +18,8 @@ type ConnectionPoolManager struct {
|
|||||||
client *fasthttp.Client
|
client *fasthttp.Client
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
logger *libpack_logging.Logger
|
logger *libpack_logging.Logger
|
||||||
|
hostGraphQL string
|
||||||
|
healthcheckGraphQL string
|
||||||
cleanupInterval time.Duration
|
cleanupInterval time.Duration
|
||||||
keepAliveInterval time.Duration
|
keepAliveInterval time.Duration
|
||||||
recoveryCheckInterval time.Duration
|
recoveryCheckInterval time.Duration
|
||||||
@@ -45,6 +47,14 @@ func NewConnectionPoolManager(client *fasthttp.Client) *ConnectionPoolManager {
|
|||||||
cpm.logger = cfg.Logger
|
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
|
// Start periodic maintenance tasks
|
||||||
cpm.startPeriodicMaintenance()
|
cpm.startPeriodicMaintenance()
|
||||||
|
|
||||||
@@ -133,8 +143,10 @@ func (cpm *ConnectionPoolManager) performKeepAlive() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only perform keep-alive if we have a backend URL configured
|
// Only perform keep-alive if we have a backend URL configured.
|
||||||
if cfg == nil || cfg.Server.HostGraphQL == "" {
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,10 +164,10 @@ func (cpm *ConnectionPoolManager) performKeepAlive() {
|
|||||||
defer fasthttp.ReleaseResponse(resp)
|
defer fasthttp.ReleaseResponse(resp)
|
||||||
|
|
||||||
// Try to use health check endpoint if available, otherwise use base URL
|
// Try to use health check endpoint if available, otherwise use base URL
|
||||||
healthURL := cfg.Server.HealthcheckGraphQL
|
healthURL := cpm.healthcheckGraphQL
|
||||||
if healthURL == "" {
|
if healthURL == "" {
|
||||||
// Use base URL with proper path separator
|
// Use base URL with proper path separator
|
||||||
baseURL := cfg.Server.HostGraphQL
|
baseURL := cpm.hostGraphQL
|
||||||
if !strings.HasSuffix(baseURL, "/") {
|
if !strings.HasSuffix(baseURL, "/") {
|
||||||
baseURL += "/"
|
baseURL += "/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
@@ -226,7 +226,7 @@ func TestDebugParseGraphQLQuery_NoPanic(t *testing.T) {
|
|||||||
cfgMutex.Unlock()
|
cfgMutex.Unlock()
|
||||||
})
|
})
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
@@ -441,8 +441,8 @@ func TestCoverageMicro_IsWebSocketRequest(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
app.Get("/ws-test", func(c *fiber.Ctx) error {
|
app.Get("/ws-test", func(c fiber.Ctx) error {
|
||||||
result := IsWebSocketRequest(c)
|
result := IsWebSocketRequest(c)
|
||||||
if result {
|
if result {
|
||||||
return c.SendStatus(101)
|
return c.SendStatus(101)
|
||||||
@@ -463,7 +463,7 @@ func TestCoverageMicro_IsWebSocketRequest(t *testing.T) {
|
|||||||
req.Header.Set("Connection", "Upgrade")
|
req.Header.Set("Connection", "Upgrade")
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := app.Test(req, -1)
|
resp, err := app.Test(req, fiber.TestConfig{Timeout: 0})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("app.Test error: %v", err)
|
t.Fatalf("app.Test error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -541,15 +541,15 @@ func TestCoverageMicro_SetupTracing_Disabled(t *testing.T) {
|
|||||||
cfgMutex.Unlock()
|
cfgMutex.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
var capturedCtx context.Context
|
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)
|
capturedCtx = setupTracing(c)
|
||||||
return c.SendStatus(200)
|
return c.SendStatus(200)
|
||||||
})
|
})
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/trace-test", nil)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("app.Test error: %v", err)
|
t.Fatalf("app.Test error: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"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/ast"
|
||||||
"github.com/graphql-go/graphql/language/parser"
|
"github.com/graphql-go/graphql/language/parser"
|
||||||
"github.com/graphql-go/graphql/language/source"
|
"github.com/graphql-go/graphql/language/source"
|
||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
// - Automatic detection of mutations routed to wrong endpoints
|
// - Automatic detection of mutations routed to wrong endpoints
|
||||||
//
|
//
|
||||||
// To enable: Set LOG_LEVEL=DEBUG and restart the proxy
|
// 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 {
|
if cfg == nil || cfg.Logger == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,31 +3,32 @@ module github.com/lukaszraczylo/graphql-monitoring-proxy
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/VictoriaMetrics/metrics v1.43.1
|
github.com/VictoriaMetrics/metrics v1.44.0
|
||||||
github.com/alicebob/miniredis/v2 v2.33.0
|
github.com/alicebob/miniredis/v2 v2.33.0
|
||||||
github.com/avast/retry-go/v4 v4.7.0
|
github.com/avast/retry-go/v4 v4.7.0
|
||||||
github.com/goccy/go-json v0.10.6
|
github.com/goccy/go-json v0.10.6
|
||||||
github.com/gofiber/fiber/v2 v2.52.12
|
github.com/gofiber/contrib/v3/websocket v1.2.0
|
||||||
github.com/gofiber/websocket/v2 v2.2.1
|
github.com/gofiber/fiber/v3 v3.3.0
|
||||||
github.com/gofrs/flock v0.13.0
|
github.com/gofrs/flock v0.13.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gookit/goutil v0.7.4
|
github.com/gookit/goutil v0.7.6
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/graphql-go/graphql v0.8.1
|
github.com/graphql-go/graphql v0.8.1
|
||||||
github.com/jackc/pgx/v5 v5.9.1
|
github.com/jackc/pgx/v5 v5.10.0
|
||||||
github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9
|
github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9
|
||||||
github.com/lukaszraczylo/go-ratecounter v0.1.12
|
github.com/lukaszraczylo/go-ratecounter v0.1.12
|
||||||
github.com/lukaszraczylo/go-simple-graphql v1.2.89
|
github.com/lukaszraczylo/go-simple-graphql v1.2.89
|
||||||
github.com/redis/go-redis/v9 v9.18.0
|
github.com/lukaszraczylo/oss-telemetry v0.2.3
|
||||||
|
github.com/redis/go-redis/v9 v9.20.1
|
||||||
github.com/sony/gobreaker v1.0.0
|
github.com/sony/gobreaker v1.0.0
|
||||||
github.com/stretchr/testify v1.11.1
|
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.43.0
|
go.opentelemetry.io/otel v1.44.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0
|
go.opentelemetry.io/otel/sdk v1.44.0
|
||||||
go.opentelemetry.io/otel/trace v1.43.0
|
go.opentelemetry.io/otel/trace v1.44.0
|
||||||
go.uber.org/automaxprocs v1.6.0
|
go.uber.org/automaxprocs v1.6.0
|
||||||
google.golang.org/grpc v1.80.0
|
google.golang.org/grpc v1.81.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -35,38 +36,40 @@ require (
|
|||||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // 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.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
|
||||||
github.com/fasthttp/websocket v1.5.12 // indirect
|
github.com/fasthttp/websocket v1.5.12 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // 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/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/klauspost/compress v1.18.5 // indirect
|
github.com/klauspost/compress v1.18.6 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.22 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.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/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/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fastrand v1.1.0 // indirect
|
github.com/valyala/fastrand v1.1.0 // indirect
|
||||||
github.com/valyala/histogram v1.2.0 // indirect
|
github.com/valyala/histogram v1.2.0 // indirect
|
||||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
golang.org/x/net v0.52.0 // indirect
|
golang.org/x/crypto v0.53.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/net v0.56.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sync v0.21.0 // indirect
|
||||||
golang.org/x/term v0.41.0 // indirect
|
golang.org/x/sys v0.46.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/term v0.44.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
golang.org/x/text v0.38.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260618152121-87f3d3e198d3 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260618152121-87f3d3e198d3 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
github.com/VictoriaMetrics/metrics v1.43.1 h1:j3Ba4l2K1q3pkvzPqt6aSiQ2DBlAEj3VPVeBtpR3t/Y=
|
github.com/VictoriaMetrics/metrics v1.44.0 h1:Fr8yqQSV+ZfYaDD/anqk1E8e9YPgfleSleJmAI0M0Tw=
|
||||||
github.com/VictoriaMetrics/metrics v1.43.1/go.mod h1:xDM82ULLYCYdFRgQ2JBxi8Uf1+8En1So9YUwlGTOqTc=
|
github.com/VictoriaMetrics/metrics v1.44.0/go.mod h1:xDM82ULLYCYdFRgQ2JBxi8Uf1+8En1So9YUwlGTOqTc=
|
||||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
|
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
|
||||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||||
github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA=
|
github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA=
|
||||||
@@ -16,15 +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/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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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.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.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
|
||||||
github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE=
|
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/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.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 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
@@ -34,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-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 h1:O0T8rZCuNmGXewnATuKYnkL0xm6o8UNOJZd/gOkb9ms=
|
||||||
github.com/goccy/go-reflect v1.2.0/go.mod h1:n0oYZn8VcV2CkWTxi8B9QjkCoq6GTtCEdfmR66YhFtE=
|
github.com/goccy/go-reflect v1.2.0/go.mod h1:n0oYZn8VcV2CkWTxi8B9QjkCoq6GTtCEdfmR66YhFtE=
|
||||||
github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw=
|
github.com/gofiber/contrib/v3/websocket v1.2.0 h1:wjkzC3exbhRL3cPCVLRYN3MFq3yhMvtBeGwqEYQk+yc=
|
||||||
github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
github.com/gofiber/contrib/v3/websocket v1.2.0/go.mod h1:fpqdn3mVAKAKfSOt8yHXhO70bZ1mz6z6NhY87CjM3k8=
|
||||||
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
|
github.com/gofiber/fiber/v3 v3.3.0 h1:QBd3sYCqdy6Qs5gJYzSw4I4SbqL204jPqpdub/ueiw8=
|
||||||
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
|
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 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
@@ -46,26 +48,26 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gookit/goutil v0.7.4 h1:OWgUngToNz+bPlX5aP+EMG31DraEU63uvKMwwT3vseM=
|
github.com/gookit/goutil v0.7.6 h1:700ZP6QPWhw5ms7X13JH9fUs4LTyYMmncFFMGpK73ns=
|
||||||
github.com/gookit/goutil v0.7.4/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU=
|
github.com/gookit/goutil v0.7.6/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc=
|
github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc=
|
||||||
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
|
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
|
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
|
||||||
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -76,22 +78,27 @@ github.com/lukaszraczylo/go-ratecounter v0.1.12 h1:VO6hHYGw/Jy9JUizXf/bS0AI2QX1u
|
|||||||
github.com/lukaszraczylo/go-ratecounter v0.1.12/go.mod h1:TqXEOCtFJStk1i0tkipprv1kiDHGon1MVUisjSTBSKM=
|
github.com/lukaszraczylo/go-ratecounter v0.1.12/go.mod h1:TqXEOCtFJStk1i0tkipprv1kiDHGon1MVUisjSTBSKM=
|
||||||
github.com/lukaszraczylo/go-simple-graphql v1.2.89 h1:Xbu1Ny+a0lT2Sr2SaSC8mcHmGQDwGD4TJKk4DDd+PwA=
|
github.com/lukaszraczylo/go-simple-graphql v1.2.89 h1:Xbu1Ny+a0lT2Sr2SaSC8mcHmGQDwGD4TJKk4DDd+PwA=
|
||||||
github.com/lukaszraczylo/go-simple-graphql v1.2.89/go.mod h1:PxQYblQDZISmYYj8sNfazAWxAOh1rhAtU208y+uPV8s=
|
github.com/lukaszraczylo/go-simple-graphql v1.2.89/go.mod h1:PxQYblQDZISmYYj8sNfazAWxAOh1rhAtU208y+uPV8s=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/lukaszraczylo/oss-telemetry v0.2.3 h1:xoDtBqeZGmXj7IteiE1M5WMuzeoqag58qEleI0Cf2Ms=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/lukaszraczylo/oss-telemetry v0.2.3/go.mod h1:+Cn78qZo8rc3T9eZt0v3oICYRdd75wORtSidc8lNjDQ=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-runewidth v0.0.22 h1:76lXsPn6FyHtTY+jt2fTTvsMUCZq1k0qwRsAMuxzKAk=
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
github.com/mattn/go-runewidth v0.0.22/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
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.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 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
github.com/redis/go-redis/v9 v9.20.1 h1:sfCU6A8P3dXbKyWes02uxA2baehGux9dZHfEKtsTB1w=
|
||||||
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
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.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
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 h1:McifyVxygw1d67y6vxUqls2D46J8W9nrki9c8c0eVvE=
|
||||||
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761/go.mod h1:Vi9gvHvTw4yCUHIznFl5TPULS7aXwgaTByGeBY75Wko=
|
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 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
|
||||||
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
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=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@@ -99,36 +106,40 @@ 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.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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
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.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
|
||||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
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 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
|
||||||
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
|
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 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
|
||||||
github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
|
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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo=
|
||||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
@@ -137,25 +148,26 @@ 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/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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||||
|
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260618152121-87f3d3e198d3 h1:ctPmKL12ZsoKAlmPUsoW70zEDiYF+/H6aLieXxgAU0k=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260618152121-87f3d3e198d3/go.mod h1:Z4WJ5pJOYWFWcHEQUelD5QaZDknIQkpIL/+fyJOT9+A=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260618152121-87f3d3e198d3 h1:phvBWCAQMGN1945mp5fjCXP6jEF0+a0+4TjokS4sxNY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260618152121-87f3d3e198d3/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
+4
-4
@@ -10,7 +10,7 @@ import (
|
|||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"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/ast"
|
||||||
"github.com/graphql-go/graphql/language/parser"
|
"github.com/graphql-go/graphql/language/parser"
|
||||||
"github.com/graphql-go/graphql/language/source"
|
"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()
|
startTime := time.Now()
|
||||||
|
|
||||||
if cfg != nil && cfg.EnableAllocationTracking {
|
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
|
// 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 {
|
if len(selections) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -468,7 +468,7 @@ func checkSelections(c *fiber.Ctx, selections []ast.Selection) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkIfContainsIntrospection(c *fiber.Ctx, query string) bool {
|
func checkIfContainsIntrospection(c fiber.Ctx, query string) bool {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
blocked := false
|
blocked := false
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"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/ast"
|
||||||
"github.com/graphql-go/graphql/language/parser"
|
"github.com/graphql-go/graphql/language/parser"
|
||||||
"github.com/valyala/fasthttp"
|
"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()
|
app := fiber.New()
|
||||||
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
|
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||||
ctx.Request().SetBody([]byte(body))
|
ctx.Request().SetBody([]byte(body))
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ func (suite *Tests) TestErrorPropagation() {
|
|||||||
// TestMiddlewareErrorPropagation tests error propagation through the middleware chain
|
// TestMiddlewareErrorPropagation tests error propagation through the middleware chain
|
||||||
func (suite *Tests) TestMiddlewareErrorPropagation() {
|
func (suite *Tests) TestMiddlewareErrorPropagation() {
|
||||||
// Setup a basic middleware chain that mimics the production setup
|
// 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
|
// Access request path to check proper error propagation
|
||||||
path := c.Path()
|
path := c.Path()
|
||||||
if path == "/error-path" {
|
if path == "/error-path" {
|
||||||
@@ -213,7 +213,7 @@ func (suite *Tests) TestMiddlewareErrorPropagation() {
|
|||||||
app.Use(testMiddleware)
|
app.Use(testMiddleware)
|
||||||
|
|
||||||
// Setup the handler that would receive the request after middleware
|
// 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
|
// This should not be called if middleware returns error
|
||||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"data": "success"})
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{"data": "success"})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
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"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
@@ -98,18 +98,16 @@ func (suite *IntegrationSecurityTestSuite) tempDirShouldBeAllowed() bool {
|
|||||||
|
|
||||||
func (suite *IntegrationSecurityTestSuite) setupTestApps() {
|
func (suite *IntegrationSecurityTestSuite) setupTestApps() {
|
||||||
// Setup proxy app (simplified for testing)
|
// Setup proxy app (simplified for testing)
|
||||||
suite.proxyApp = fiber.New(fiber.Config{
|
suite.proxyApp = fiber.New(fiber.Config{})
|
||||||
DisableStartupMessage: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add proxy routes with security middleware
|
// 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
|
// Add request UUID for tracking
|
||||||
c.Locals("request_uuid", fmt.Sprintf("test-uuid-%d", time.Now().UnixNano()))
|
c.Locals("request_uuid", fmt.Sprintf("test-uuid-%d", time.Now().UnixNano()))
|
||||||
return c.Next()
|
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
|
// Simulate GraphQL proxy behavior with logging
|
||||||
if cfg.LogLevel == "DEBUG" {
|
if cfg.LogLevel == "DEBUG" {
|
||||||
logDebugRequest(c)
|
logDebugRequest(c)
|
||||||
@@ -135,9 +133,7 @@ func (suite *IntegrationSecurityTestSuite) setupTestApps() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Setup API app
|
// Setup API app
|
||||||
suite.apiApp = fiber.New(fiber.Config{
|
suite.apiApp = fiber.New(fiber.Config{})
|
||||||
DisableStartupMessage: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
api := suite.apiApp.Group("/api")
|
api := suite.apiApp.Group("/api")
|
||||||
api.Use(authMiddleware)
|
api.Use(authMiddleware)
|
||||||
@@ -636,10 +632,10 @@ func BenchmarkSecurityOperations(b *testing.B) {
|
|||||||
os.Setenv("GMP_ADMIN_API_KEY", validAPIKey)
|
os.Setenv("GMP_ADMIN_API_KEY", validAPIKey)
|
||||||
defer os.Unsetenv("GMP_ADMIN_API_KEY")
|
defer os.Unsetenv("GMP_ADMIN_API_KEY")
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
api := app.Group("/api")
|
api := app.Group("/api")
|
||||||
api.Use(authMiddleware)
|
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"})
|
return c.JSON(fiber.Map{"status": "ok"})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+47
-20
@@ -10,7 +10,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||||
"github.com/sony/gobreaker"
|
"github.com/sony/gobreaker"
|
||||||
@@ -101,10 +101,14 @@ func (suite *Tests) TestCachingAndCircuitBreakerInteraction() {
|
|||||||
reqBody := `{"query": "query { test }"}`
|
reqBody := `{"query": "query { test }"}`
|
||||||
reqCtx.Request.SetBody([]byte(reqBody))
|
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{
|
libpack_cache.EnableCache(&libpack_cache.CacheConfig{
|
||||||
Logger: cfg.Logger,
|
Logger: cfg.Logger,
|
||||||
TTL: cfg.Cache.CacheTTL,
|
TTL: 600,
|
||||||
})
|
})
|
||||||
|
|
||||||
// First request: should succeed and be cached
|
// First request: should succeed and be cached
|
||||||
@@ -734,10 +738,14 @@ func (suite *Tests) TestRequestCoalescingIntegration() {
|
|||||||
|
|
||||||
// Test Case 4: Error responses should be shared correctly
|
// Test Case 4: Error responses should be shared correctly
|
||||||
suite.Run("error_responses_coalesced", func() {
|
suite.Run("error_responses_coalesced", func() {
|
||||||
backendCallCount.Store(0)
|
// Backend that returns an error. Any non-200 response is retried by the
|
||||||
testCoalescer.Reset()
|
// proxy, so a single logical request makes more than one backend call.
|
||||||
|
// Coalescing must collapse N concurrent identical requests into ONE logical
|
||||||
// Create server that returns errors
|
// 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) {
|
serverError := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
backendCallCount.Add(1)
|
backendCallCount.Add(1)
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
@@ -750,6 +758,30 @@ func (suite *Tests) TestRequestCoalescingIntegration() {
|
|||||||
cfg.Server.HostGraphQL = serverError.URL
|
cfg.Server.HostGraphQL = serverError.URL
|
||||||
cfg.Client.FastProxyClient = createFasthttpClient(cfg)
|
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
|
concurrentRequests := 5
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(concurrentRequests)
|
wg.Add(concurrentRequests)
|
||||||
@@ -759,24 +791,19 @@ func (suite *Tests) TestRequestCoalescingIntegration() {
|
|||||||
for i := 0; i < concurrentRequests; i++ {
|
for i := 0; i < concurrentRequests; i++ {
|
||||||
go func(index int) {
|
go func(index int) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
errors[index] = doErrRequest()
|
||||||
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)
|
|
||||||
}(i)
|
}(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
// Should still only make 1 backend call
|
// Coalesced: the concurrent batch made about as many backend calls as a
|
||||||
suite.Equal(int32(1), backendCallCount.Load(),
|
// single request (one logical request, retries included). A small tolerance
|
||||||
"Should make only 1 backend call even for error responses")
|
// 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
|
// All requests should receive the same error
|
||||||
for i := 0; i < concurrentRequests; i++ {
|
for i := 0; i < concurrentRequests; i++ {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
// 127.0.0.1 only and gated by PPROF_PORT — never expose publicly.
|
// 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
|
_ "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"
|
"github.com/gookit/goutil/envutil"
|
||||||
graphql "github.com/lukaszraczylo/go-simple-graphql"
|
graphql "github.com/lukaszraczylo/go-simple-graphql"
|
||||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||||
@@ -28,11 +28,17 @@ import (
|
|||||||
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||||
libpack_tracing "github.com/lukaszraczylo/graphql-monitoring-proxy/tracing"
|
libpack_tracing "github.com/lukaszraczylo/graphql-monitoring-proxy/tracing"
|
||||||
|
telemetry "github.com/lukaszraczylo/oss-telemetry"
|
||||||
|
|
||||||
// Auto-tune GOMAXPROCS from cgroup CPU quota (containerized workloads).
|
// Auto-tune GOMAXPROCS from cgroup CPU quota (containerized workloads).
|
||||||
_ "go.uber.org/automaxprocs"
|
_ "go.uber.org/automaxprocs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// appVersion is the build version. Set via ldflags during build:
|
||||||
|
//
|
||||||
|
// -X main.appVersion=v1.2.3
|
||||||
|
var appVersion = "dev"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cfg *config
|
cfg *config
|
||||||
cfgMutex sync.RWMutex
|
cfgMutex sync.RWMutex
|
||||||
@@ -512,6 +518,8 @@ func parseConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
telemetry.SendForModule("graphql-monitoring-proxy", "github.com/lukaszraczylo/graphql-monitoring-proxy", appVersion)
|
||||||
|
|
||||||
// Parse configuration
|
// Parse configuration
|
||||||
parseConfig()
|
parseConfig()
|
||||||
|
|
||||||
|
|||||||
+3
-4
@@ -8,7 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"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_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
|
||||||
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -31,9 +31,8 @@ func (suite *Tests) SetupTest() {
|
|||||||
// Setup test
|
// Setup test
|
||||||
suite.app = fiber.New(
|
suite.app = fiber.New(
|
||||||
fiber.Config{
|
fiber.Config{
|
||||||
DisableStartupMessage: true,
|
JSONEncoder: json.Marshal,
|
||||||
JSONEncoder: json.Marshal,
|
JSONDecoder: json.Unmarshal,
|
||||||
JSONDecoder: json.Unmarshal,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/metrics"
|
"github.com/VictoriaMetrics/metrics"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/gookit/goutil/envutil"
|
"github.com/gookit/goutil/envutil"
|
||||||
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
|
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
|
||||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||||
@@ -78,11 +78,10 @@ func (ms *MetricsSetup) Shutdown() {
|
|||||||
|
|
||||||
func (ms *MetricsSetup) startPrometheusEndpoint() {
|
func (ms *MetricsSetup) startPrometheusEndpoint() {
|
||||||
app := fiber.New(fiber.Config{
|
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)
|
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{
|
log.Critical(&libpack_logger.LogMessage{
|
||||||
Message: "Can't start the MONITORING service",
|
Message: "Can't start the MONITORING service",
|
||||||
Pairs: map[string]any{"error": err},
|
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.WritePrometheus(c.Response().BodyWriter())
|
||||||
ms.metrics_set_custom.WritePrometheus(c.Response().BodyWriter())
|
ms.metrics_set_custom.WritePrometheus(c.Response().BodyWriter())
|
||||||
|
|
||||||
@@ -109,6 +108,11 @@ func (ms *MetricsSetup) ListActiveMetrics() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ms *MetricsSetup) RegisterMetricsGauge(metric_name string, labels map[string]string, val float64) *metrics.Gauge {
|
func (ms *MetricsSetup) RegisterMetricsGauge(metric_name string, labels map[string]string, val float64) *metrics.Gauge {
|
||||||
|
if ms == nil {
|
||||||
|
// Monitoring may not be initialized yet (e.g. during config parsing,
|
||||||
|
// before the monitoring server starts). Return a dummy to prevent panics.
|
||||||
|
return &metrics.Gauge{}
|
||||||
|
}
|
||||||
if err := validate_metrics_name(metric_name); err != nil {
|
if err := validate_metrics_name(metric_name); err != nil {
|
||||||
log.Error(&libpack_logger.LogMessage{
|
log.Error(&libpack_logger.LogMessage{
|
||||||
Message: "RegisterMetricsGauge() error - invalid metric name",
|
Message: "RegisterMetricsGauge() error - invalid metric name",
|
||||||
@@ -125,6 +129,9 @@ func (ms *MetricsSetup) RegisterMetricsGauge(metric_name string, labels map[stri
|
|||||||
// RegisterMetricsGaugeFunc registers a gauge with a callback function that is called on every scrape
|
// RegisterMetricsGaugeFunc registers a gauge with a callback function that is called on every scrape
|
||||||
// This is useful for metrics that need to return a dynamic value
|
// This is useful for metrics that need to return a dynamic value
|
||||||
func (ms *MetricsSetup) RegisterMetricsGaugeFunc(metric_name string, labels map[string]string, fn func() float64) *metrics.Gauge {
|
func (ms *MetricsSetup) RegisterMetricsGaugeFunc(metric_name string, labels map[string]string, fn func() float64) *metrics.Gauge {
|
||||||
|
if ms == nil {
|
||||||
|
return &metrics.Gauge{}
|
||||||
|
}
|
||||||
if err := validate_metrics_name(metric_name); err != nil {
|
if err := validate_metrics_name(metric_name); err != nil {
|
||||||
log.Error(&libpack_logger.LogMessage{
|
log.Error(&libpack_logger.LogMessage{
|
||||||
Message: "RegisterMetricsGaugeFunc() error - invalid metric name",
|
Message: "RegisterMetricsGaugeFunc() error - invalid metric name",
|
||||||
@@ -137,6 +144,9 @@ func (ms *MetricsSetup) RegisterMetricsGaugeFunc(metric_name string, labels map[
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ms *MetricsSetup) RegisterMetricsCounter(metric_name string, labels map[string]string) *metrics.Counter {
|
func (ms *MetricsSetup) RegisterMetricsCounter(metric_name string, labels map[string]string) *metrics.Counter {
|
||||||
|
if ms == nil {
|
||||||
|
return &metrics.Counter{}
|
||||||
|
}
|
||||||
if err := validate_metrics_name(metric_name); err != nil {
|
if err := validate_metrics_name(metric_name); err != nil {
|
||||||
log.Error(&libpack_logger.LogMessage{
|
log.Error(&libpack_logger.LogMessage{
|
||||||
Message: "RegisterMetricsCounter() error - invalid metric name",
|
Message: "RegisterMetricsCounter() error - invalid metric name",
|
||||||
@@ -152,6 +162,9 @@ func (ms *MetricsSetup) RegisterMetricsCounter(metric_name string, labels map[st
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ms *MetricsSetup) RegisterFloatCounter(metric_name string, labels map[string]string) *metrics.FloatCounter {
|
func (ms *MetricsSetup) RegisterFloatCounter(metric_name string, labels map[string]string) *metrics.FloatCounter {
|
||||||
|
if ms == nil {
|
||||||
|
return &metrics.FloatCounter{}
|
||||||
|
}
|
||||||
if err := validate_metrics_name(metric_name); err != nil {
|
if err := validate_metrics_name(metric_name); err != nil {
|
||||||
log.Error(&libpack_logger.LogMessage{
|
log.Error(&libpack_logger.LogMessage{
|
||||||
Message: "RegisterFloatCounter() error - invalid metric name",
|
Message: "RegisterFloatCounter() error - invalid metric name",
|
||||||
@@ -164,6 +177,9 @@ func (ms *MetricsSetup) RegisterFloatCounter(metric_name string, labels map[stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ms *MetricsSetup) RegisterMetricsSummary(metric_name string, labels map[string]string) *metrics.Summary {
|
func (ms *MetricsSetup) RegisterMetricsSummary(metric_name string, labels map[string]string) *metrics.Summary {
|
||||||
|
if ms == nil {
|
||||||
|
return &metrics.Summary{}
|
||||||
|
}
|
||||||
if err := validate_metrics_name(metric_name); err != nil {
|
if err := validate_metrics_name(metric_name); err != nil {
|
||||||
log.Error(&libpack_logger.LogMessage{
|
log.Error(&libpack_logger.LogMessage{
|
||||||
Message: "RegisterMetricsSummary() error - invalid metric name",
|
Message: "RegisterMetricsSummary() error - invalid metric name",
|
||||||
@@ -176,6 +192,9 @@ func (ms *MetricsSetup) RegisterMetricsSummary(metric_name string, labels map[st
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ms *MetricsSetup) RegisterMetricsHistogram(metric_name string, labels map[string]string) *metrics.Histogram {
|
func (ms *MetricsSetup) RegisterMetricsHistogram(metric_name string, labels map[string]string) *metrics.Histogram {
|
||||||
|
if ms == nil {
|
||||||
|
return &metrics.Histogram{}
|
||||||
|
}
|
||||||
if err := validate_metrics_name(metric_name); err != nil {
|
if err := validate_metrics_name(metric_name); err != nil {
|
||||||
log.Error(&libpack_logger.LogMessage{
|
log.Error(&libpack_logger.LogMessage{
|
||||||
Message: "RegisterMetricsHistogram() error - invalid metric name",
|
Message: "RegisterMetricsHistogram() error - invalid metric name",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package libpack_monitoring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestRegistrationMethods_NilReceiver_NoPanic verifies that the metric
|
||||||
|
// registration methods tolerate a nil *MetricsSetup receiver. Callers may
|
||||||
|
// register metrics before the monitoring server has constructed the global
|
||||||
|
// MetricsSetup (e.g. circuit breaker init during config parsing); these
|
||||||
|
// methods must return a non-nil dummy instead of panicking.
|
||||||
|
func TestRegistrationMethods_NilReceiver_NoPanic(t *testing.T) {
|
||||||
|
var ms *MetricsSetup // nil
|
||||||
|
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
require.NotNil(t, ms.RegisterMetricsGauge("g", nil, 1))
|
||||||
|
require.NotNil(t, ms.RegisterMetricsGaugeFunc("gf", nil, func() float64 { return 1 }))
|
||||||
|
require.NotNil(t, ms.RegisterMetricsCounter("c", nil))
|
||||||
|
require.NotNil(t, ms.RegisterFloatCounter("fc", nil))
|
||||||
|
require.NotNil(t, ms.RegisterMetricsSummary("s", nil))
|
||||||
|
require.NotNil(t, ms.RegisterMetricsHistogram("h", nil))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
|
||||||
"github.com/avast/retry-go/v4"
|
"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_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"
|
||||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
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.
|
// 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
|
// Record request for RPS tracking
|
||||||
if rpsTracker := GetRPSTracker(); rpsTracker != nil {
|
if rpsTracker := GetRPSTracker(); rpsTracker != nil {
|
||||||
rpsTracker.RecordRequest()
|
rpsTracker.RecordRequest()
|
||||||
@@ -337,7 +337,7 @@ func proxyTheRequest(c *fiber.Ctx, currentEndpoint string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setupTracing extracts and sets up tracing context from request headers
|
// 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()
|
ctx := context.Background()
|
||||||
|
|
||||||
if !cfg.Tracing.Enable || tracer == nil {
|
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
|
// 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)
|
// Extract user context for cache key (needed for coalescing and circuit breaker fallback)
|
||||||
userID, userRole := extractUserInfo(c)
|
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
|
// performProxyRequestCore executes the proxy request with retries and circuit breaker
|
||||||
// This is the core implementation used by both direct calls and coalesced requests
|
// 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 circuit breaker is not enabled, use the original method
|
||||||
if !cfg.CircuitBreaker.Enable || cb == nil {
|
if !cfg.CircuitBreaker.Enable || cb == nil {
|
||||||
return performProxyRequestWithRetries(c, proxyURL)
|
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
|
// performProxyRequestWithRetries executes the proxy request with retries
|
||||||
// This is the original implementation extracted for reuse
|
// 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
|
// Check backend health first if available
|
||||||
healthMgr := GetBackendHealthManager()
|
healthMgr := GetBackendHealthManager()
|
||||||
if healthMgr != nil && !healthMgr.IsHealthy() {
|
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
|
// 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
|
// Additional safety check inside retry loop
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return retry.Unrecoverable(errFiberCtxNilDuringRetry)
|
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
|
// 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
|
// Safety check for nil context
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return errFiberCtxNil
|
return errFiberCtxNil
|
||||||
@@ -713,7 +713,7 @@ func notifyHealthManager(success bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleCircuitOpenGracefulDegradation handles requests when the circuit breaker is open
|
// 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
|
// Try to serve from cache if configured and available
|
||||||
if cfg.CircuitBreaker.ReturnCachedOnOpen {
|
if cfg.CircuitBreaker.ReturnCachedOnOpen {
|
||||||
if cachedResponse := libpack_cache.CacheLookup(cacheKey); cachedResponse != nil {
|
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
|
// 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
|
// Calculate timeout from client configuration
|
||||||
clientTimeout := time.Duration(cfg.Client.ClientTimeout) * time.Second
|
clientTimeout := time.Duration(cfg.Client.ClientTimeout) * time.Second
|
||||||
if clientTimeout <= 0 {
|
if clientTimeout <= 0 {
|
||||||
@@ -785,7 +785,7 @@ func doProxyRequestWithTimeout(c *fiber.Ctx, proxyURL string, client *fasthttp.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleGzippedResponse decompresses gzipped responses
|
// 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")) {
|
if !bytes.EqualFold(c.Response().Header.Peek("Content-Encoding"), []byte("gzip")) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -828,7 +828,7 @@ func handleGzippedResponse(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// logDebugRequest logs the request details when in debug mode with sanitization.
|
// 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())
|
contentType := string(c.Request().Header.ContentType())
|
||||||
sanitizedBody := sanitizeForLogging(c.Body(), contentType)
|
sanitizedBody := sanitizeForLogging(c.Body(), contentType)
|
||||||
sanitizedHeaders := sanitizeHeaders(convertHeaders(c.GetReqHeaders()))
|
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.
|
// 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())
|
contentType := string(c.Response().Header.ContentType())
|
||||||
sanitizedBody := sanitizeForLogging(c.Response().Body(), contentType)
|
sanitizedBody := sanitizeForLogging(c.Response().Body(), contentType)
|
||||||
sanitizedHeaders := sanitizeHeaders(convertHeaders(c.GetRespHeaders()))
|
sanitizedHeaders := sanitizeHeaders(convertHeaders(c.GetRespHeaders()))
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
fiber "github.com/gofiber/fiber/v2"
|
fiber "github.com/gofiber/fiber/v3"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
"github.com/gofiber/fiber/v3/middleware/cors"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
graphql "github.com/lukaszraczylo/go-simple-graphql"
|
graphql "github.com/lukaszraczylo/go-simple-graphql"
|
||||||
@@ -42,19 +42,18 @@ func StartHTTPProxy() error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
serverConfig := fiber.Config{
|
serverConfig := 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),
|
IdleTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
|
||||||
IdleTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
|
ReadTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
|
||||||
ReadTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
|
WriteTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
|
||||||
WriteTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
|
JSONEncoder: json.Marshal,
|
||||||
JSONEncoder: json.Marshal,
|
JSONDecoder: json.Unmarshal,
|
||||||
JSONDecoder: json.Unmarshal,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server := fiber.New(serverConfig)
|
server := fiber.New(serverConfig)
|
||||||
|
|
||||||
server.Use(cors.New(cors.Config{
|
server.Use(cors.New(cors.Config{
|
||||||
AllowOrigins: "*",
|
AllowOrigins: []string{"*"},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
server.Use(AddRequestUUID)
|
server.Use(AddRequestUUID)
|
||||||
@@ -71,7 +70,7 @@ func StartHTTPProxy() error {
|
|||||||
|
|
||||||
// WebSocket support - must be registered before catch-all routes
|
// WebSocket support - must be registered before catch-all routes
|
||||||
if cfg.WebSocket.Enable {
|
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) {
|
if IsWebSocketRequest(c) {
|
||||||
wsp := GetWebSocketProxy()
|
wsp := GetWebSocketProxy()
|
||||||
if wsp != nil {
|
if wsp != nil {
|
||||||
@@ -90,7 +89,7 @@ func StartHTTPProxy() error {
|
|||||||
Pairs: map[string]any{"port": cfg.Server.PortGraphQL},
|
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",
|
return fmt.Errorf("failed to start HTTP proxy server on port %d: %w",
|
||||||
cfg.Server.PortGraphQL, err)
|
cfg.Server.PortGraphQL, err)
|
||||||
}
|
}
|
||||||
@@ -99,18 +98,18 @@ func StartHTTPProxy() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// proxyTheRequestToDefault proxies the request to the default GraphQL endpoint.
|
// 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)
|
return proxyTheRequest(c, cfg.Server.HostGraphQL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddRequestUUID adds a unique request UUID to the context.
|
// 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())
|
c.Locals("request_uuid", uuid.NewString())
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkAllowedURLs checks if the requested URL is allowed.
|
// checkAllowedURLs checks if the requested URL is allowed.
|
||||||
func checkAllowedURLs(c *fiber.Ctx) bool {
|
func checkAllowedURLs(c fiber.Ctx) bool {
|
||||||
if len(allowedUrls) == 0 {
|
if len(allowedUrls) == 0 {
|
||||||
return true
|
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.
|
// 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
|
// Prepare the response structure
|
||||||
response := HealthCheckResponse{
|
response := HealthCheckResponse{
|
||||||
Status: "healthy",
|
Status: "healthy",
|
||||||
@@ -254,7 +253,7 @@ func healthCheck(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// processGraphQLRequest handles the incoming GraphQL requests.
|
// processGraphQLRequest handles the incoming GraphQL requests.
|
||||||
func processGraphQLRequest(c *fiber.Ctx) error {
|
func processGraphQLRequest(c fiber.Ctx) error {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
// Extract user information and check permissions
|
// 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
|
// extractUserInfo extracts user ID and role from request headers
|
||||||
func extractUserInfo(c *fiber.Ctx) (string, string) {
|
func extractUserInfo(c fiber.Ctx) (string, string) {
|
||||||
extractedUserID := "-"
|
extractedUserID := "-"
|
||||||
extractedRoleName := "-"
|
extractedRoleName := "-"
|
||||||
|
|
||||||
@@ -326,7 +325,7 @@ func extractUserInfo(c *fiber.Ctx) (string, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleCaching manages the caching logic for GraphQL requests
|
// 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
|
// Calculate query hash for cache key - now includes user context for security
|
||||||
calculatedQueryHash := libpack_cache.CalculateHash(c, userID, userRole)
|
calculatedQueryHash := libpack_cache.CalculateHash(c, userID, userRole)
|
||||||
|
|
||||||
@@ -357,6 +356,13 @@ func handleCaching(c *fiber.Ctx, parsedResult *parseGraphQLQueryResult, userID,
|
|||||||
|
|
||||||
// Try to get from cache
|
// Try to get from cache
|
||||||
if cachedResponse := libpack_cache.CacheLookup(calculatedQueryHash); cachedResponse != nil {
|
if cachedResponse := libpack_cache.CacheLookup(calculatedQueryHash); cachedResponse != nil {
|
||||||
|
// Count cache-served requests toward RPS too. Cache hits return here
|
||||||
|
// without reaching proxyTheRequest (where misses/proxied requests are
|
||||||
|
// recorded), so without this the dashboard's current RPS reads 0
|
||||||
|
// whenever traffic is served from cache.
|
||||||
|
if rpsTracker := GetRPSTracker(); rpsTracker != nil {
|
||||||
|
rpsTracker.RecordRequest()
|
||||||
|
}
|
||||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsCacheHit, nil)
|
cfg.Monitoring.Increment(libpack_monitoring.MetricsCacheHit, nil)
|
||||||
c.Set("X-Cache-Hit", "true")
|
c.Set("X-Cache-Hit", "true")
|
||||||
c.Set("Content-Type", "application/json")
|
c.Set("Content-Type", "application/json")
|
||||||
@@ -373,7 +379,7 @@ func handleCaching(c *fiber.Ctx, parsedResult *parseGraphQLQueryResult, userID,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// proxyAndCacheTheRequest proxies and caches the request if needed.
|
// 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 {
|
if err := proxyTheRequest(c, currentEndpoint); err != nil {
|
||||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||||
Message: "Can't proxy the request",
|
Message: "Can't proxy the request",
|
||||||
@@ -389,7 +395,7 @@ func proxyAndCacheTheRequest(c *fiber.Ctx, queryCacheHash string, cacheTime int,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// logAndMonitorRequest logs and monitors the request processing.
|
// 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.
|
// Low-cardinality labels only: user_id and op_name dropped to prevent Prometheus explosion.
|
||||||
labels := map[string]string{
|
labels := map[string]string{
|
||||||
"op_type": opType,
|
"op_type": opType,
|
||||||
|
|||||||
+23
-23
@@ -10,7 +10,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
@@ -20,11 +20,11 @@ import (
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func TestAddRequestUUID_SetsLocalsAndCallsNext(t *testing.T) {
|
func TestAddRequestUUID_SetsLocalsAndCallsNext(t *testing.T) {
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
app.Use(AddRequestUUID)
|
app.Use(AddRequestUUID)
|
||||||
|
|
||||||
var captured string
|
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 {
|
if v, ok := c.Locals("request_uuid").(string); ok {
|
||||||
captured = v
|
captured = v
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ func TestAddRequestUUID_SetsLocalsAndCallsNext(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
resp, err := app.Test(req, -1)
|
resp, err := app.Test(req, fiber.TestConfig{Timeout: 0})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("app.Test: %v", err)
|
t.Fatalf("app.Test: %v", err)
|
||||||
}
|
}
|
||||||
@@ -51,11 +51,11 @@ func TestAddRequestUUID_SetsLocalsAndCallsNext(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAddRequestUUID_UniquePerRequest(t *testing.T) {
|
func TestAddRequestUUID_UniquePerRequest(t *testing.T) {
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
app.Use(AddRequestUUID)
|
app.Use(AddRequestUUID)
|
||||||
|
|
||||||
seen := make([]string, 0, 5)
|
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 {
|
if v, ok := c.Locals("request_uuid").(string); ok {
|
||||||
seen = append(seen, v)
|
seen = append(seen, v)
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ func TestAddRequestUUID_UniquePerRequest(t *testing.T) {
|
|||||||
|
|
||||||
for i := range 5 {
|
for i := range 5 {
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
resp, err := app.Test(req, -1)
|
resp, err := app.Test(req, fiber.TestConfig{Timeout: 0})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("request %d: %v", i, err)
|
t.Fatalf("request %d: %v", i, err)
|
||||||
}
|
}
|
||||||
@@ -89,12 +89,12 @@ func TestHealthCheck_Returns200WithJSON(t *testing.T) {
|
|||||||
parseConfig()
|
parseConfig()
|
||||||
_ = StartMonitoringServer()
|
_ = StartMonitoringServer()
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
app.Get("/health", healthCheck)
|
app.Get("/health", healthCheck)
|
||||||
|
|
||||||
// Pass check_graphql=false to avoid real network call
|
// Pass check_graphql=false to avoid real network call
|
||||||
req := httptest.NewRequest("GET", "/health?check_graphql=false&check_redis=false", nil)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("app.Test: %v", err)
|
t.Fatalf("app.Test: %v", err)
|
||||||
}
|
}
|
||||||
@@ -135,11 +135,11 @@ func TestHealthCheck_UnhealthyWhenGraphQLDown(t *testing.T) {
|
|||||||
cfgMutex.Unlock()
|
cfgMutex.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
app.Get("/health", healthCheck)
|
app.Get("/health", healthCheck)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/health?check_redis=false", nil)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("app.Test: %v", err)
|
t.Fatalf("app.Test: %v", err)
|
||||||
}
|
}
|
||||||
@@ -190,14 +190,14 @@ func TestProcessGraphQLRequest_ValidBodyProxiesToBackend(t *testing.T) {
|
|||||||
cfgMutex.Unlock()
|
cfgMutex.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
app.Post("/*", processGraphQLRequest)
|
app.Post("/*", processGraphQLRequest)
|
||||||
|
|
||||||
body := `{"query":"query { __typename }"}`
|
body := `{"query":"query { __typename }"}`
|
||||||
req := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(body))
|
req := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(body))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("app.Test: %v", err)
|
t.Fatalf("app.Test: %v", err)
|
||||||
}
|
}
|
||||||
@@ -236,7 +236,7 @@ func TestProcessGraphQLRequest_MalformedBodyStillHandled(t *testing.T) {
|
|||||||
cfgMutex.Unlock()
|
cfgMutex.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
app.Post("/*", processGraphQLRequest)
|
app.Post("/*", processGraphQLRequest)
|
||||||
|
|
||||||
// Not valid JSON — proxy should still forward or return gracefully
|
// 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 := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(body))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("app.Test: %v", err)
|
t.Fatalf("app.Test: %v", err)
|
||||||
}
|
}
|
||||||
@@ -302,7 +302,7 @@ func TestHandleCaching_CacheHitReturnsStoredResponse(t *testing.T) {
|
|||||||
cfgMutex.Unlock()
|
cfgMutex.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
app.Post("/*", processGraphQLRequest)
|
app.Post("/*", processGraphQLRequest)
|
||||||
|
|
||||||
queryBody := `{"query":"query { users { id } }"}`
|
queryBody := `{"query":"query { users { id } }"}`
|
||||||
@@ -310,7 +310,7 @@ func TestHandleCaching_CacheHitReturnsStoredResponse(t *testing.T) {
|
|||||||
// First request — cache miss, hits backend
|
// First request — cache miss, hits backend
|
||||||
req1 := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(queryBody))
|
req1 := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(queryBody))
|
||||||
req1.Header.Set("Content-Type", "application/json")
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("first request: %v", err)
|
t.Fatalf("first request: %v", err)
|
||||||
}
|
}
|
||||||
@@ -323,7 +323,7 @@ func TestHandleCaching_CacheHitReturnsStoredResponse(t *testing.T) {
|
|||||||
// Second identical request — should hit cache
|
// Second identical request — should hit cache
|
||||||
req2 := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(queryBody))
|
req2 := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(queryBody))
|
||||||
req2.Header.Set("Content-Type", "application/json")
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("second request: %v", err)
|
t.Fatalf("second request: %v", err)
|
||||||
}
|
}
|
||||||
@@ -380,7 +380,7 @@ func TestHandleCaching_CacheMissProxiesRequest(t *testing.T) {
|
|||||||
cfgMutex.Unlock()
|
cfgMutex.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
app.Post("/*", processGraphQLRequest)
|
app.Post("/*", processGraphQLRequest)
|
||||||
|
|
||||||
// Unique query so no prior cache entry
|
// 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 := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(queryBody))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("app.Test: %v", err)
|
t.Fatalf("app.Test: %v", err)
|
||||||
}
|
}
|
||||||
@@ -430,10 +430,10 @@ func TestHandleCaching_DirectCacheHitBranch(t *testing.T) {
|
|||||||
cfgMutex.Unlock()
|
cfgMutex.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
|
|
||||||
var wasCachedResult bool
|
var wasCachedResult bool
|
||||||
app.Post("/test", func(c *fiber.Ctx) error {
|
app.Post("/test", func(c fiber.Ctx) error {
|
||||||
parsedResult := &parseGraphQLQueryResult{
|
parsedResult := &parseGraphQLQueryResult{
|
||||||
cacheTime: 60,
|
cacheTime: 60,
|
||||||
cacheRequest: true,
|
cacheRequest: true,
|
||||||
@@ -507,7 +507,7 @@ func TestHandleCaching_NoCacheEnabled_ProxiesDirect(t *testing.T) {
|
|||||||
cfgMutex.Unlock()
|
cfgMutex.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
app := fiber.New()
|
||||||
|
|
||||||
reqCtx := &fasthttp.RequestCtx{}
|
reqCtx := &fasthttp.RequestCtx{}
|
||||||
reqCtx.Request.SetRequestURI("/v1/graphql")
|
reqCtx.Request.SetRequestURI("/v1/graphql")
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
+4
-4
@@ -9,8 +9,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/contrib/v3/websocket"
|
||||||
"github.com/gofiber/websocket/v2"
|
"github.com/gofiber/fiber/v3"
|
||||||
gorillaws "github.com/gorilla/websocket"
|
gorillaws "github.com/gorilla/websocket"
|
||||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
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
|
// 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 {
|
if !wsp.enabled {
|
||||||
return fiber.NewError(fiber.StatusNotImplemented, "WebSocket support is disabled")
|
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
|
// 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) ||
|
return websocket.IsWebSocketUpgrade(c) ||
|
||||||
c.Get("Upgrade") == "websocket" ||
|
c.Get("Upgrade") == "websocket" ||
|
||||||
c.Get("Connection") == "Upgrade"
|
c.Get("Connection") == "Upgrade"
|
||||||
|
|||||||
Reference in New Issue
Block a user