Files
graphql-monitoring-proxy/debug_routing.go
T
lukaszraczylo 55fc2ae1de feat(deps)!: migrate from fiber v2 to fiber v3
BREAKING CHANGE: upgrades the HTTP framework from gofiber/fiber/v2 to gofiber/fiber/v3, and gofiber/websocket/v2 to gofiber/contrib/v3/websocket.

- Handlers now take fiber.Ctx (interface) instead of *fiber.Ctx. - DisableStartupMessage moved from fiber.Config to fiber.ListenConfig on the proxy/api/monitoring Listen calls. - cors AllowOrigins is now []string; c.BodyParser -> c.Bind().Body(). - app.Test takes fiber.TestConfig instead of an int timeout (tests updated). - WebSocket swapped to contrib/v3/websocket (same Conn/New/Config/IsCloseError/IsWebSocketUpgrade API; Conn.Query preserved). - Adopts fasthttp v1.71.0 (pulled by fiber v3), resolving the v1.71 Host-header enforcement that broke the prior pinned-v1.69 workaround.

fix(connpool): the background keep-alive goroutine no longer reads the mutable global cfg (captures HostGraphQL/HealthcheckGraphQL at pool creation). Under v3 timing it raced with parseConfig/test cfg reassignment; go test -race ./... is now clean (0 data races).

test: make the circuit-breaker cache-fallback and request-coalescing integration tests deterministic (cache TTL outlives the retry loop; coalescing asserts against a measured single-request baseline instead of a hard-coded count); drop the flaky timing-ratio assertion in TestTimingAttackResistance (constant-time guarantee comes from subtle.ConstantTimeCompare, not wall-clock timing).

Verified: go build, go vet, golangci-lint (0 issues), go test -race ./... (all packages pass, 0 races), govulncheck (no vulnerabilities). App + dashboard + WebSocket verified live under v3 in single-node and cluster modes.
2026-06-21 13:15:14 +01:00

144 lines
4.3 KiB
Go

package main
import (
"fmt"
"strings"
fiber "github.com/gofiber/fiber/v3"
"github.com/graphql-go/graphql/language/ast"
"github.com/graphql-go/graphql/language/parser"
"github.com/graphql-go/graphql/language/source"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
)
// debugParseGraphQLQuery provides detailed logging for mutation routing analysis
// This is automatically called when LOG_LEVEL=DEBUG to help identify routing issues
//
// It logs:
// - GraphQL query structure (operations, selections, directives)
// - Final routing decision (which endpoint was chosen)
// - Automatic detection of mutations routed to wrong endpoints
//
// To enable: Set LOG_LEVEL=DEBUG and restart the proxy
func debugParseGraphQLQuery(c fiber.Ctx, query string) {
if cfg == nil || cfg.Logger == nil {
return
}
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "=== DEBUG: Parsing GraphQL Query ===",
Pairs: map[string]any{
"query_length": len(query),
"query_preview": truncateString(query, 100),
},
})
// Parse the query
src := source.NewSource(&source.Source{
Body: []byte(query),
Name: "Debug GraphQL request",
})
p, err := parser.Parse(parser.ParseParams{Source: src})
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "DEBUG: Failed to parse query",
Pairs: map[string]any{"error": err.Error()},
})
return
}
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "DEBUG: Query parsed successfully",
Pairs: map[string]any{
"definitions_count": len(p.Definitions),
},
})
// Analyze each definition
for i, d := range p.Definitions {
if oper, ok := d.(*ast.OperationDefinition); ok {
operationType := strings.ToLower(oper.Operation)
operationName := "unnamed"
if oper.Name != nil {
operationName = oper.Name.Value
}
// Count selections
selectionCount := 0
if oper.SelectionSet != nil {
selectionCount = len(oper.GetSelectionSet().Selections)
}
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: fmt.Sprintf("DEBUG: Definition #%d (OperationDefinition)", i),
Pairs: map[string]any{
"operation_type": operationType,
"operation_name": operationName,
"selection_count": selectionCount,
"is_mutation": operationType == "mutation",
"directive_count": len(oper.Directives),
},
})
// Log selections for mutations
if operationType == "mutation" && oper.SelectionSet != nil {
for j, sel := range oper.GetSelectionSet().Selections {
if field, ok := sel.(*ast.Field); ok {
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: fmt.Sprintf("DEBUG: Mutation field #%d", j),
Pairs: map[string]any{
"field_name": field.Name.Value,
},
})
}
}
}
} else if frag, ok := d.(*ast.FragmentDefinition); ok {
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: fmt.Sprintf("DEBUG: Definition #%d (FragmentDefinition)", i),
Pairs: map[string]any{
"fragment_name": frag.Name.Value,
},
})
}
}
// Now run the actual parsing to see the result
result := parseGraphQLQuery(c)
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "DEBUG: Final routing decision",
Pairs: map[string]any{
"operation_type": result.operationType,
"operation_name": result.operationName,
"active_endpoint": result.activeEndpoint,
"should_block": result.shouldBlock,
"should_ignore": result.shouldIgnore,
"write_endpoint": cfg.Server.HostGraphQL,
"read_endpoint": cfg.Server.HostGraphQLReadOnly,
"is_using_write": result.activeEndpoint == cfg.Server.HostGraphQL,
},
})
// Check for potential issues
if result.operationType == "mutation" && result.activeEndpoint != cfg.Server.HostGraphQL {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "DEBUG: ⚠️ BUG DETECTED: Mutation routed to wrong endpoint!",
Pairs: map[string]any{
"expected_endpoint": cfg.Server.HostGraphQL,
"actual_endpoint": result.activeEndpoint,
},
})
}
if result.operationType == "mutation" && strings.Contains(strings.ToLower(result.activeEndpoint), "read") {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "DEBUG: ⚠️ CRITICAL: Mutation endpoint contains 'read' in URL!",
Pairs: map[string]any{
"endpoint": result.activeEndpoint,
},
})
}
}