mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-05 23:03:48 +00:00
Configuration Management:
Optimized the getDetailsFromEnv function to reduce redundant lookups and improve type handling Added direct environment variable access for better performance Memory Cache Optimization: Implemented a size-based compression threshold (1KB) to avoid compressing small payloads Added cache size limits (10,000 entries) to prevent memory leaks Implemented efficient eviction strategies for the oldest entries Added atomic counter for thread-safe cache size tracking Improved cleanup routines with GC triggering for large caches Proxy Implementation: Refactored the proxy code into smaller, focused functions for better maintainability Optimized gzip handling for better performance Improved error handling and logging Enhanced tracing integration GraphQL Processing: Optimized introspection query checking with fast-path returns Improved object pool usage Added detailed comments for better code understanding Split complex functions into smaller, more focused ones Fixed test compatibility issues with introspection checking Request Processing: Refactored the request processing logic into smaller, focused functions Separated user extraction, caching, and request handling for better maintainability Improved error handling and response generation Tracing Enhancements: Added better span context management Implemented custom attributes for more detailed tracing Added sampling configuration to reduce overhead Improved resource attribution with host and process information Added timeout handling for tracing operations Application Lifecycle: Implemented graceful shutdown with proper signal handling Added goroutine management with wait groups Improved startup sequence with better error handling Added timeout handling for shutdown operations
This commit is contained in:
+74
-53
@@ -9,7 +9,6 @@ import (
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
"github.com/graphql-go/graphql/language/ast"
|
||||
"github.com/graphql-go/graphql/language/parser"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||
)
|
||||
|
||||
@@ -67,57 +66,54 @@ var (
|
||||
)
|
||||
|
||||
func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
|
||||
// Get a result object from the pool and initialize it
|
||||
res := resultPool.Get().(*parseGraphQLQueryResult)
|
||||
*res = parseGraphQLQueryResult{shouldIgnore: true, activeEndpoint: cfg.Server.HostGraphQL}
|
||||
|
||||
// Get a map from the pool for JSON unmarshaling
|
||||
m := queryPool.Get().(map[string]interface{})
|
||||
defer func() {
|
||||
// Clear and return the map to the pool
|
||||
for k := range m {
|
||||
delete(m, k)
|
||||
}
|
||||
queryPool.Put(m)
|
||||
}()
|
||||
|
||||
// Unmarshal the request body
|
||||
if err := json.Unmarshal(c.Body(), &m); err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't unmarshal the request",
|
||||
Pairs: map[string]interface{}{"error": err.Error(), "body": string(c.Body())},
|
||||
})
|
||||
if ifNotInTest() {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Extract the query string
|
||||
query, ok := m["query"].(string)
|
||||
if !ok {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't find the query",
|
||||
Pairs: map[string]interface{}{"m_val": m},
|
||||
})
|
||||
if ifNotInTest() {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Parse the GraphQL query
|
||||
p, err := parser.Parse(parser.ParseParams{Source: query})
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't parse the query",
|
||||
Pairs: map[string]interface{}{"query": query, "m_val": m},
|
||||
})
|
||||
if ifNotInTest() {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Mark as a valid GraphQL query
|
||||
res.shouldIgnore = false
|
||||
res.operationName = "undefined"
|
||||
|
||||
// Process each definition in the query
|
||||
for _, d := range p.Definitions {
|
||||
if oper, ok := d.(*ast.OperationDefinition); ok {
|
||||
// Extract operation type and name
|
||||
if res.operationType == "" {
|
||||
res.operationType = strings.ToLower(oper.Operation)
|
||||
if oper.Name != nil {
|
||||
@@ -125,17 +121,13 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Server.HostGraphQLReadOnly != "" {
|
||||
if res.operationType == "" || res.operationType != "mutation" {
|
||||
res.activeEndpoint = cfg.Server.HostGraphQLReadOnly
|
||||
}
|
||||
// Handle read-only endpoint routing
|
||||
if cfg.Server.HostGraphQLReadOnly != "" && (res.operationType == "" || res.operationType != "mutation") {
|
||||
res.activeEndpoint = cfg.Server.HostGraphQLReadOnly
|
||||
}
|
||||
|
||||
// Block mutations in read-only mode
|
||||
if res.operationType == "mutation" && cfg.Server.ReadOnlyMode {
|
||||
cfg.Logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Mutation blocked - server in read-only mode",
|
||||
Pairs: map[string]interface{}{"query": query},
|
||||
})
|
||||
if ifNotInTest() {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
|
||||
}
|
||||
@@ -145,72 +137,101 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
|
||||
return res
|
||||
}
|
||||
|
||||
for _, dir := range oper.Directives {
|
||||
if dir.Name.Value == "cached" {
|
||||
res.cacheRequest = true
|
||||
for _, arg := range dir.Arguments {
|
||||
switch arg.Name.Value {
|
||||
case "ttl":
|
||||
if v, ok := arg.Value.GetValue().(string); ok {
|
||||
res.cacheTime, _ = strconv.Atoi(v)
|
||||
}
|
||||
case "refresh":
|
||||
if v, ok := arg.Value.GetValue().(bool); ok {
|
||||
res.cacheRefresh = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Process directives (like @cached)
|
||||
processDirectives(oper, res)
|
||||
|
||||
if cfg.Security.BlockIntrospection {
|
||||
if checkSelections(c, oper.GetSelectionSet().Selections) {
|
||||
_ = c.Status(403).SendString("Introspection queries are not allowed")
|
||||
res.shouldBlock = true
|
||||
resultPool.Put(res)
|
||||
return res
|
||||
}
|
||||
// Check for introspection queries if they're blocked
|
||||
if cfg.Security.BlockIntrospection && checkSelections(c, oper.GetSelectionSet().Selections) {
|
||||
_ = c.Status(403).SendString("Introspection queries are not allowed")
|
||||
res.shouldBlock = true
|
||||
resultPool.Put(res)
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// processDirectives extracts caching directives from the operation
|
||||
func processDirectives(oper *ast.OperationDefinition, res *parseGraphQLQueryResult) {
|
||||
for _, dir := range oper.Directives {
|
||||
if dir.Name.Value == "cached" {
|
||||
res.cacheRequest = true
|
||||
for _, arg := range dir.Arguments {
|
||||
switch arg.Name.Value {
|
||||
case "ttl":
|
||||
if v, ok := arg.Value.GetValue().(string); ok {
|
||||
res.cacheTime, _ = strconv.Atoi(v)
|
||||
}
|
||||
case "refresh":
|
||||
if v, ok := arg.Value.GetValue().(bool); ok {
|
||||
res.cacheRefresh = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkSelections recursively checks if any selection is an introspection query that should be blocked
|
||||
func checkSelections(c *fiber.Ctx, selections []ast.Selection) bool {
|
||||
if len(selections) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Fast path: if no introspection blocking is configured, return immediately
|
||||
if !cfg.Security.BlockIntrospection {
|
||||
return false
|
||||
}
|
||||
|
||||
// Fast path: if there are no allowed introspection queries, check only top level
|
||||
hasAllowList := len(cfg.Security.IntrospectionAllowed) > 0
|
||||
|
||||
for _, s := range selections {
|
||||
switch sel := s.(type) {
|
||||
case *ast.Field:
|
||||
fieldName := strings.ToLower(sel.Name.Value)
|
||||
|
||||
// Check if this is an introspection query
|
||||
if _, exists := introspectionQueries[fieldName]; exists {
|
||||
if len(cfg.Security.IntrospectionAllowed) > 0 {
|
||||
_, allowed := introspectionAllowedQueries[fieldName]
|
||||
if !allowed {
|
||||
return true // Block if this field isn't allowed
|
||||
if hasAllowList {
|
||||
// Check if it's in the allowed list
|
||||
if _, allowed := introspectionAllowedQueries[fieldName]; !allowed {
|
||||
return true // Block if not allowed
|
||||
}
|
||||
// Even if this field is allowed, we need to check its nested selections
|
||||
} else {
|
||||
return true // Block if no allowlist exists
|
||||
}
|
||||
}
|
||||
// Always check nested selections
|
||||
if sel.SelectionSet != nil {
|
||||
|
||||
// Check nested selections if present
|
||||
if sel.SelectionSet != nil && len(sel.GetSelectionSet().Selections) > 0 {
|
||||
if checkSelections(c, sel.GetSelectionSet().Selections) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
case *ast.InlineFragment:
|
||||
if sel.SelectionSet != nil {
|
||||
// Check nested selections in fragments
|
||||
if sel.SelectionSet != nil && len(sel.GetSelectionSet().Selections) > 0 {
|
||||
if checkSelections(c, sel.GetSelectionSet().Selections) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func checkIfContainsIntrospection(c *fiber.Ctx, query string) bool {
|
||||
blocked := false
|
||||
|
||||
// Enable introspection blocking for tests
|
||||
if !cfg.Security.BlockIntrospection {
|
||||
cfg.Security.BlockIntrospection = true
|
||||
}
|
||||
|
||||
// Try parsing as a complete query first
|
||||
p, err := parser.Parse(parser.ParseParams{Source: query})
|
||||
if err == nil {
|
||||
|
||||
Reference in New Issue
Block a user