package middleware import ( "fmt" "strings" "time" "github.com/99designs/gqlgen/graphql" "github.com/gin-gonic/gin" "github.com/penglongli/gin-metrics/ginmetrics" "github.com/vektah/gqlparser/v2/ast" ) const ( metricPath = "/metrics" maxLatencySeconds = 1 graphQLRequests = "graphql_request_total" graphQlOperations = "graphql_query_total" graphQLSlowReQuests = "graphql_slow_request_total" graphQLResponseTime = "graphql_query_duration" ) func UseMetrics(r gin.IRoutes) { // get global Monitor object m := ginmetrics.GetMonitor() m.SetMetricPath(metricPath) m.SetSlowTime(maxLatencySeconds) _ = m.AddMetric(&ginmetrics.Metric{ Type: ginmetrics.Counter, Name: graphQLRequests, Description: "count all graphql requests.", Labels: nil, }) _ = m.AddMetric(&ginmetrics.Metric{ Type: ginmetrics.Counter, Name: graphQlOperations, Description: "count all graphql requests by name, operation and status.", Labels: []string{"name", "operation", "status"}, }) _ = m.AddMetric(&ginmetrics.Metric{ Type: ginmetrics.Counter, Name: graphQLSlowReQuests, Description: fmt.Sprintf("count all graphql slow requests by name, operation and status. max_lentency=%d sec.", maxLatencySeconds), Labels: []string{"name", "operation", "status"}, }) m.SetDuration([]float64{0.1, 0.3, 1.2, 5, 10}) _ = m.AddMetric(&ginmetrics.Metric{ Type: ginmetrics.Histogram, Name: graphQLResponseTime, Description: "the time graphql took handle the request by name.", Labels: []string{"name"}, // +required set request duration, default {0.1, 0.3, 1.2, 5, 10} // used to p95, p99 Buckets: []float64{0.1, 0.3, 1.2, 5, 10}, }) m.Use(r) } func HandleGraphQlMetrics(ctx *graphql.OperationContext, resp *graphql.Response, start time.Time) { latency := time.Since(start) // metrics for any graphql query _ = ginmetrics.GetMonitor().GetMetric(graphQLRequests).Inc(nil) name := "no_selection" selections := GetSelectionNames(ctx) if len(selections) > 0 { name = strings.Join(selections, ",") } operation := ctx.OperationName if operation == "" && ctx != nil && ctx.Operation != nil { operation = string(ctx.Operation.Operation) } if operation == "" { operation = "unknown" } status := "success" if resp != nil && len(resp.Errors) > 0 { status = "error" } labels := []string{name, operation, status} // metrics per name, operation and status _ = ginmetrics.GetMonitor().GetMetric(graphQlOperations).Inc(labels) // slow metrics per name, operation and status if int32(latency.Seconds()) > maxLatencySeconds { _ = ginmetrics.GetMonitor().GetMetric(graphQLSlowReQuests).Inc(labels) } // metrics for response time _ = ginmetrics.GetMonitor().GetMetric(graphQLResponseTime).Observe([]string{name}, latency.Seconds()) } // GetSelectionNames return selection names and ignore introspection queries func GetSelectionNames(rc *graphql.OperationContext) []string { var selections []string if rc.Operation != nil { // can be null if query/mutation is invalid for _, selection := range rc.Operation.SelectionSet { selectionName := selection.(*ast.Field).Name // queries that starts with underscore are used for introspection (used by graphql playground) if !strings.HasPrefix(selectionName, "_") { selections = append(selections, selectionName) } } } return selections }