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

Add GraphQL operation metrics #12441

Closed
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,5 @@ public interface AnalyticsDataProvider {
default Map<String, Object> getProperties() {
return Collections.EMPTY_MAP;
}

HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
package org.wso2.carbon.apimgt.common.gateway.graphql;

import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.GraphQL;
import graphql.GraphQLError;
Expand All @@ -29,7 +30,10 @@
import org.apache.commons.logging.LogFactory;
import org.json.simple.parser.ParseException;
import org.wso2.carbon.apimgt.common.gateway.dto.QueryAnalyzerResponseDTO;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
* This class contains methods using for Graphql query depth and complexity analysis.
Expand All @@ -52,6 +56,18 @@ public QueryAnalyzer(GraphQLSchema schema) {
*/
public QueryAnalyzerResponseDTO analyseQueryDepth(int maxQueryDepth, String payload) {

return analyseQueryDepth(maxQueryDepth, payload, new HashMap<>());
}

/**
* @param maxQueryDepth maximum query depth
* @param payload payload of the request
* @param variableMap external variable map
* @return true, if the query depth does not exceed the maximum value or false, if query depth exceeds the maximum
*/
public QueryAnalyzerResponseDTO analyseQueryDepth(int maxQueryDepth, String payload,
Map<String, Object> variableMap) {

if (log.isDebugEnabled()) {
log.debug("Analyzing query depth for " + payload + " and max query depth:" + maxQueryDepth);
}
Expand All @@ -63,7 +79,13 @@ public QueryAnalyzerResponseDTO analyseQueryDepth(int maxQueryDepth, String payl
new MaxQueryDepthInstrumentation(maxQueryDepth);
GraphQL runtime = GraphQL.newGraphQL(schema).instrumentation(maxQueryDepthInstrumentation).build();

ExecutionResult executionResult = runtime.execute(payload);
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
.query(payload)
.context(Optional.empty())
.variables(variableMap)
.build();

ExecutionResult executionResult = runtime.execute(executionInput);
List<GraphQLError> errors = executionResult.getErrors();
if (errors.size() > 0) {
for (GraphQLError error : errors) {
Expand Down Expand Up @@ -98,6 +120,22 @@ public QueryAnalyzerResponseDTO analyseQueryDepth(int maxQueryDepth, String payl
*/
public QueryAnalyzerResponseDTO analyseQueryComplexity(int maxQueryComplexity, String payload,
FieldComplexityCalculator fieldComplexityCalculator) {
return analyseQueryComplexity(maxQueryComplexity, payload, fieldComplexityCalculator, new HashMap<>());
}

/**
* This method analyses the query complexity.
*
* @param fieldComplexityCalculator Field Complexity Calculator
* @param maxQueryComplexity Maximum query complexity value
* @param payload payload of the request
* @param variableMap external variable map
* @return true, if query complexity does not exceed the maximum or false, if query complexity exceeds the maximum
*/

public QueryAnalyzerResponseDTO analyseQueryComplexity(int maxQueryComplexity, String payload,
FieldComplexityCalculator fieldComplexityCalculator,
Map<String, Object> variableMap) {

if (log.isDebugEnabled()) {
log.debug("Analyzing query complexity for " + payload + " and max complexity: " + maxQueryComplexity);
Expand All @@ -111,7 +149,13 @@ public QueryAnalyzerResponseDTO analyseQueryComplexity(int maxQueryComplexity, S
new MaxQueryComplexityInstrumentation(maxQueryComplexity, fieldComplexityCalculator);
GraphQL runtime = GraphQL.newGraphQL(schema).instrumentation(maxQueryComplexityInstrumentation).build();

ExecutionResult executionResult = runtime.execute(payload);
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
.query(payload)
.context(Optional.empty())
.variables(variableMap)
.build();

ExecutionResult executionResult = runtime.execute(executionInput);
List<GraphQLError> errors = executionResult.getErrors();
if (errors.size() > 0) {
for (GraphQLError error : errors) {
Expand Down Expand Up @@ -152,6 +196,24 @@ public QueryAnalyzerResponseDTO analyseQueryMutationComplexity(String payload, i
return analyseQueryComplexity(maxQueryComplexity, payload, fieldComplexityCalculator);
}

/**
* This method analyses the query complexity
*
* @param payload payload of the request
* @param complexityInfoJson gql complexity info in json string format
* @param variableMap
* @return true, if query complexity does not exceed the maximum or false, if query complexity exceeds the maximum
*/

public QueryAnalyzerResponseDTO analyseQueryMutationComplexity(String payload, int maxQueryComplexity,
String complexityInfoJson,
Map<String, Object> variableMap)
throws ParseException {
FieldComplexityCalculatorImpl fieldComplexityCalculator = new FieldComplexityCalculatorImpl();
fieldComplexityCalculator.parseAccessControlPolicy(complexityInfoJson);
return analyseQueryComplexity(maxQueryComplexity, payload, fieldComplexityCalculator, variableMap);
}

public GraphQLSchema getSchema() {
return schema;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
System.currentTimeMillis());
if (msg instanceof TextWebSocketFrame) {
WebSocketUtils.setApiPropertyToChannel(ctx, Constants.RESPONSE_SIZE,
((TextWebSocketFrame) msg).text().length());
((TextWebSocketFrame) msg).text().length());
}
}
// publish analytics events if analytics is enabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,17 @@
import org.wso2.carbon.apimgt.common.analytics.collectors.impl.GenericRequestDataCollector;
import org.wso2.carbon.apimgt.common.analytics.exceptions.AnalyticsException;
import org.wso2.carbon.apimgt.gateway.handlers.DataPublisherUtil;
import org.wso2.carbon.apimgt.gateway.handlers.graphQL.analytics.GraphQLOperationInfoAnalyzer;
import org.wso2.carbon.apimgt.gateway.handlers.streaming.AsyncAnalyticsDataProvider;
import org.wso2.carbon.apimgt.gateway.internal.ServiceReferenceHolder;
import org.wso2.carbon.apimgt.gateway.utils.GatewayUtils;
import org.wso2.carbon.apimgt.impl.APIConstants;
import org.wso2.carbon.apimgt.impl.utils.APIUtil;
import org.wso2.carbon.inbound.endpoint.protocol.websocket.InboundWebsocketConstants;

import java.util.Map;

import static org.wso2.carbon.apimgt.gateway.APIMgtGatewayConstants.TRANSPORT_HEADERS;
/**
* Global synapse handler to publish analytics data to analytics cloud.
*/
Expand Down Expand Up @@ -82,19 +85,28 @@ public boolean handleResponseOutFlow(MessageContext messageContext) {
if (messageContext.getPropertyKeySet().contains(InboundWebsocketConstants.WEBSOCKET_SUBSCRIBER_PATH)) {
return true;
}
messageContext.setProperty(TRANSPORT_HEADERS, ((Axis2MessageContext) messageContext).getAxis2MessageContext().
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
getProperty(TRANSPORT_HEADERS));

AnalyticsDataProvider provider;
Object skipPublishMetrics = messageContext.getProperty(Constants.SKIP_DEFAULT_METRICS_PUBLISHING);
if (skipPublishMetrics != null && (Boolean) skipPublishMetrics) {
provider = new AsyncAnalyticsDataProvider(messageContext);
if (messageContext.getProperty(APIConstants.API_TYPE) == "GRAPHQL"){
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
GraphQLOperationInfoAnalyzer operationInfoAnalyzer = new GraphQLOperationInfoAnalyzer();
operationInfoAnalyzer.analyzePayload(messageContext);
} else {
provider = new SynapseAnalyticsDataProvider(messageContext,
ServiceReferenceHolder.getInstance().getAnalyticsCustomDataProvider());
}
GenericRequestDataCollector dataCollector = new GenericRequestDataCollector(provider);
try {
dataCollector.collectData();
} catch (Exception e) {
log.error("Error Occurred when collecting data", e);
if (skipPublishMetrics != null && (Boolean) skipPublishMetrics) {
provider = new AsyncAnalyticsDataProvider(messageContext);
} else {
provider = new SynapseAnalyticsDataProvider(messageContext,
ServiceReferenceHolder.getInstance().getAnalyticsCustomDataProvider());
}

GenericRequestDataCollector dataCollector = new GenericRequestDataCollector(provider);
try {
dataCollector.collectData();
} catch (Exception e) {
log.error("Error Occurred when collecting data", e);
}
}
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ public class Constants {
public static final String API_USER_NAME_KEY = "userName";
public static final String API_CONTEXT_KEY = "apiContext";
public static final String RESPONSE_SIZE = "responseSize";
public static final String OPERATION_NAME = "operationName";

public static final String OPERATION_INFO = "operationInfo";
public static final String RESPONSE_CONTENT_TYPE = "responseContentType";
public static final String API_ANALYTICS_CUSTOM_DATA_PROVIDER_CLASS = "publisher.custom.data.provider.class";

public static final String ACCESSED_FIELDS = "accessedFields";
public static final String MUTATED_FIELDS = "mutatedFields";
public static final String REGION_ID_PROP = "apim.gw.region";
public static final String DEFAULT_REGION_ID = "default";
public static final String SUCCESS_EVENT_TYPE = "response";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,9 +405,29 @@ public Map<String, Object> getProperties() {
customProperties.put(Constants.API_CONTEXT_KEY, getApiContext());
customProperties.put(Constants.RESPONSE_SIZE, getResponseSize());
customProperties.put(Constants.RESPONSE_CONTENT_TYPE, getResponseContentType());
if (messageContext.getProperty(APIConstants.API_TYPE) == "GRAPHQL"){
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
customProperties.put(Constants.OPERATION_INFO, getOperationInfo());
customProperties.put(Constants.ACCESSED_FIELDS, getAccessedFields());
customProperties.put(Constants.MUTATED_FIELDS, getMutatedFields());
}
return customProperties;
}

private Map<String, Object> getOperationInfo() {
Object fieldUsage = messageContext.getProperty(APIConstants.OPERATION_INFO);
return (Map<String, Object>) fieldUsage;
}

private Object getAccessedFields() {
Object accessedFields = messageContext.getProperty(APIConstants.ACCESSED_FIELDS);
return accessedFields;
}

private Object getMutatedFields(){
Object mutatedFields = messageContext.getProperty(APIConstants.MUTATED_FIELDS);
return mutatedFields;
}

private String getApiContext() {

if (messageContext.getPropertyKeySet().contains(JWTConstants.REST_API_CONTEXT)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import graphql.schema.GraphQLType;
import graphql.validation.Validator;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.impl.llom.OMElementImpl;
import org.apache.axiom.om.impl.llom.OMTextImpl;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpStatus;
Expand All @@ -51,7 +53,9 @@
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.Iterator;

import static org.apache.axis2.Constants.Configuration.HTTP_METHOD;

Expand All @@ -62,6 +66,7 @@ public class GraphQLAPIHandler extends AbstractHandler {
private static final String REST_SUB_REQUEST_PATH = "REST_SUB_REQUEST_PATH";
private static final String GRAPHQL_API = "GRAPHQL";
private static final String HTTP_VERB = "HTTP_VERB";
private static final String HTTP_NAME = "HTTP_NAME";
private static final String UNICODE_TRANSFORMATION_FORMAT = "UTF-8";
private static final Log log = LogFactory.getLog(GraphQLAPIHandler.class);
private GraphQLSchemaDTO graphQLSchemaDTO;
Expand Down Expand Up @@ -92,6 +97,8 @@ public boolean handleRequest(MessageContext messageContext) {
return true;
}
String payload;
OMElement variables;
Map<String, Object> variablesMap = null;
Parser parser = new Parser();
org.apache.axis2.context.MessageContext axis2MC = ((Axis2MessageContext) messageContext).
getAxis2MessageContext();
Expand All @@ -106,6 +113,12 @@ public boolean handleRequest(MessageContext messageContext) {
OMElement body = axis2MC.getEnvelope().getBody().getFirstElement();
if (body != null && body.getFirstChildWithName(QName.valueOf(QUERY_PAYLOAD_STRING)) != null){
payload = body.getFirstChildWithName(QName.valueOf(QUERY_PAYLOAD_STRING)).getText();
if (body.getFirstChildWithName(QName.valueOf("variables")) != null) {
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
variables = body.getFirstChildWithName(QName.valueOf("variables"));
ArrayList<OMElement> variablesArray = convertXMLToArray(variables);
variablesMap = convertArrayToMap(variablesArray);
messageContext.setProperty(APIConstants.VARIABLE_MAP, variablesMap);
}
} else {
if (log.isDebugEnabled()) {
log.debug("Invalid query parameter " + queryParams[0]);
Expand Down Expand Up @@ -136,6 +149,7 @@ public boolean handleRequest(MessageContext messageContext) {
messageContext.setProperty(HTTP_VERB, httpVerb);
((Axis2MessageContext) messageContext).getAxis2MessageContext().setProperty(HTTP_METHOD,
operation.getOperation().toString());
messageContext.setProperty(HTTP_NAME, operation.getName());
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
String operationList = GraphQLProcessorUtil.getOperationListAsString(operation,
graphQLSchemaDTO.getTypeDefinitionRegistry());
messageContext.setProperty(APIConstants.API_ELECTED_RESOURCE, operationList);
Expand All @@ -159,6 +173,34 @@ public boolean handleRequest(MessageContext messageContext) {
return false;
}

private Map<String, Object> convertArrayToMap(ArrayList<OMElement> variablesArray) {
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
Map<String, Object> externalVariables = new HashMap<>();
for(OMElement omElement: variablesArray){
externalVariables = getVariables(omElement, externalVariables);
}
return externalVariables;
}

private ArrayList<OMElement> convertXMLToArray(OMElement variables) {
ArrayList<OMElement> variablesArray = new ArrayList<>();
Iterator it = variables.getChildElements();
while (it.hasNext()) {
variablesArray.add((OMElement) it.next());
}
return variablesArray;
}

private Map<String, Object> getVariables(OMElement omElement, Map<String, Object> externalVariables) {
String variableKey = omElement.getLocalName();
Object variable = null;
if (omElement.getFirstOMChild() instanceof OMTextImpl){
variable = ((OMTextImpl) omElement.getFirstOMChild()).getText();
} else if (omElement.getFirstOMChild() instanceof OMElementImpl) {
variable = convertArrayToMap(convertXMLToArray(omElement));
}
externalVariables.put(variableKey, variable);
return externalVariables;
}
/**
* Support GraphQL APIs for basic,JWT authentication, this method extract the scopes and operations from
* local Entry and set them to properties. If the operations have scopes, scopes operation mapping and scope
Expand Down Expand Up @@ -221,6 +263,7 @@ private void supportForBasicAndAuthentication(MessageContext messageContext) {
messageContext.setProperty(APIConstants.GRAPHQL_ACCESS_CONTROL_POLICY, graphQLAccessControlPolicy);
messageContext.setProperty(APIConstants.API_TYPE, GRAPHQL_API);
messageContext.setProperty(APIConstants.GRAPHQL_SCHEMA, graphQLSchemaDTO.getGraphQLSchema());
messageContext.setProperty(APIConstants.TYPE_DEFINITION, graphQLSchemaDTO.getTypeDefinitionRegistry());
}

private void setMappingList(String additionalTypeName, String base64DecodedTypeValue,
Expand Down
Loading