Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gql graphql query complexity analysis poc #20

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
102 changes: 98 additions & 4 deletions graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"context"
"encoding/json"
"fmt"
"math"
"reflect"
"time"

"github.com/tokopedia/graphql-go/errors"
"github.com/tokopedia/graphql-go/internal/common"
Expand All @@ -19,6 +21,19 @@ import (
"github.com/tokopedia/graphql-go/trace"
)

//parameters for generating score for query complexity analysis
const (
LatencyMaxThreshold = float64(4000) //upper bound for latency in ms
LatencyMinThreshold = float64(0) // lower bound for latency in ms
ResponseSizeMaxThreshold = float64(10000) // upper bound for response size in bytes
ResponseSizeMinThreshold = float64(1) //lower bound for response size in bytes
NestingDepthMaxThreshold = float64(10) // upper bound for nesting depth
NestingDepthMinThreshold = float64(2) //lower bound for nesting depth
ResolverMaxThreshold = float64(200) // upper bound for number of resolvers
ResolverMinThreshold = float64(1) // lower bound for number of resolvers

)

// ParseSchema parses a GraphQL schema and attaches the given root resolver. It returns an error if
// the Go type signature of the resolvers does not match the schema. If nil is passed as the
// resolver, then the schema can not be executed, but it may be inspected (e.g. with ToJSON).
Expand Down Expand Up @@ -143,13 +158,13 @@ type Response struct {

// Validate validates the given query with the schema.
func (s *Schema) Validate(queryString string, variables map[string]interface{}) ([]string, bool, []*errors.QueryError) {
var queries []string
var queries []string
doc, qErr := query.Parse(queryString)
if qErr != nil {
return queries, true, []*errors.QueryError{qErr}
}
for _, op := range doc.Operations{
for _, sel := range op.Selections{
for _, op := range doc.Operations {
for _, sel := range op.Selections {
query, ok := sel.(*query.Field)
if ok {
queries = append(queries, query.Name.Name)
Expand Down Expand Up @@ -177,6 +192,14 @@ func (s *Schema) exec(ctx context.Context, queryString string, operationName str
return &Response{Errors: []*errors.QueryError{qErr}}
}

//number of resolvers needed to resolve a single field is 1 hence field type will have resolverComplexity as 1
resolverComplexity := 0
for _, op := range doc.Operations {
var queue = make([][]query.Selection, 0)
resolverComplexity += CalculateResolverComplexity(queue, op.Selections)
}

QueryNestingDepth := CalculateNestingDepth(queryString)
validationFinish := s.validationTracer.TraceValidation()
errs := validation.Validate(s.schema, doc, variables, s.maxDepth)
validationFinish(errs)
Expand Down Expand Up @@ -232,13 +255,54 @@ func (s *Schema) exec(ctx context.Context, queryString string, operationName str
varTypes[v.Name.Name] = introspection.WrapType(t)
}
traceCtx, finish := s.tracer.TraceQuery(ctx, queryString, operationName, variables, varTypes)
st := time.Now()
data, errs := r.Execute(traceCtx, res, op)
en := time.Now()
latency := en.Sub(st) //time taken to execute the query and get back the response
finish(errs)

/*Calculating the score on the basis of latency, response size, nesting depth and number of resolvers.
Each parameter can contribute an individual score ranging from 0 to 1. Hence, for a query with parameters
below or equal to the threshold values defined, the cumulative score won't go above 4. If a query has a cumulative
score of above 4, then this means that the threshold values have not been obeyed and the query is complex or not
lightweight
*/
Score := float64(latency.Milliseconds())/(LatencyMaxThreshold-LatencyMinThreshold) + float64(len(data)-int(ResolverMinThreshold))/(ResponseSizeMaxThreshold-ResponseSizeMinThreshold) + float64(QueryNestingDepth-int(NestingDepthMinThreshold))/(NestingDepthMaxThreshold-NestingDepthMinThreshold) + float64(resolverComplexity-int(ResolverMinThreshold))/(ResolverMaxThreshold-ResolverMinThreshold)
return &Response{
Data: data,
Errors: errs,
Extensions: map[string]interface{}{
"Resolver Complexity": resolverComplexity,
"Nesting Depth": QueryNestingDepth,
"latency": latency.Milliseconds(),
"Response Size": len(data),
"Score": Score,
},
}
}

func CalculateResolverComplexity(queue [][]query.Selection, Selections []query.Selection) int {
resolvercomplexity := 0
queue = enqueue(queue, Selections)
for len(queue) > 0 {
var selections []query.Selection
selections, queue = dequeue(queue)
if selections != nil {
for _, sel := range selections {
Query, ok := sel.(*query.Field)
if ok {
queue = enqueue(queue, Query.Selections)
}

}

} else {
resolvercomplexity++
}

}

return resolvercomplexity

}

func getOperation(document *query.Document, operationName string) (*query.Operation, error) {
Expand All @@ -261,3 +325,33 @@ func getOperation(document *query.Document, operationName string) (*query.Operat
}
return op, nil
}

func CalculateNestingDepth(queryString string) int {
depth := 0
max := math.MinInt32
for _, ch := range queryString {
if ch == '{' {
depth++
} else if ch == '}' {
depth--
}
max = int(math.Max(float64(max), float64(depth)))
}
return max
}

func enqueue(queue [][]query.Selection, element []query.Selection) [][]query.Selection {
queue = append(queue, element)
return queue
}

func dequeue(queue [][]query.Selection) ([]query.Selection, [][]query.Selection) {
element := queue[0]
if len(queue) == 1 {
var tmp [][]query.Selection
return element, tmp

}

return element, queue[1:]
}