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 2 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,7 @@ public interface AnalyticsDataProvider {
default Map<String, Object> getProperties() {
return Collections.EMPTY_MAP;
}

HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
Map<String, Object> getOperationProperties();
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class AnalyticsDataPublisher {

private List<CounterMetric> successMetricReporters;
private List<CounterMetric> faultyMetricReporters;
private List<CounterMetric> operationInfoMetricReporters;
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved

private AnalyticsDataPublisher() {

Expand Down Expand Up @@ -142,6 +143,9 @@ public void initialize(AnalyticsCommonConfiguration commonConfig) {
MetricSchema.ERROR);
}

this.operationInfoMetricReporters = getSuccessOrFaultyCounterMetrics(metricReporters,
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
"graphql:operationInfo", MetricSchema.RESPONSE);
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved

// not necessary to handle IllegalArgumentException here
// since we are handling it in getSuccessOrFaultyCounterMetrics method
} catch (MetricCreationException e) {
Expand All @@ -164,4 +168,12 @@ public List<CounterMetric> getFaultyMetricReporters() throws MetricCreationExcep
}
return faultyMetricReporters;
}

public List<CounterMetric> getOperationInfoMetricReporters() throws MetricCreationException {

if (this.operationInfoMetricReporters.isEmpty()) {
throw new MetricCreationException("None of AnalyticsDataPublishers are initialized.");
}
return operationInfoMetricReporters;
}
}

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,9 @@
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.Optional;

/**
* This class contains methods using for Graphql query depth and complexity analysis.
Expand All @@ -39,6 +42,9 @@ public class QueryAnalyzer {
private static final Log log = LogFactory.getLog(QueryAnalyzer.class);
private final GraphQLSchema schema;

/**
* @param schema
*/
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
public QueryAnalyzer(GraphQLSchema schema) {
this.schema = schema;
}
Expand All @@ -52,6 +58,18 @@ public QueryAnalyzer(GraphQLSchema schema) {
*/
public QueryAnalyzerResponseDTO analyseQueryDepth(int maxQueryDepth, String payload) {

return analyseQueryDepth(maxQueryDepth, payload, null);
}

/**
* @param maxQueryDepth
* @param payload
* @param variableMap
* @return
*/
public QueryAnalyzerResponseDTO analyseQueryDepth(int maxQueryDepth, String payload,
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
HashMap<String, Object> variableMap) {

if (log.isDebugEnabled()) {
log.debug("Analyzing query depth for " + payload + " and max query depth:" + maxQueryDepth);
}
Expand All @@ -62,8 +80,18 @@ public QueryAnalyzerResponseDTO analyseQueryDepth(int maxQueryDepth, String payl
MaxQueryDepthInstrumentation maxQueryDepthInstrumentation =
new MaxQueryDepthInstrumentation(maxQueryDepth);
GraphQL runtime = GraphQL.newGraphQL(schema).instrumentation(maxQueryDepthInstrumentation).build();
if (variableMap == null) {
variableMap = new HashMap<>();
}
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved

ExecutionResult executionResult = runtime.execute(payload);
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
.query(payload)
.context(Optional.ofNullable(null))
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
.root(null)
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
.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 +126,22 @@ public QueryAnalyzerResponseDTO analyseQueryDepth(int maxQueryDepth, String payl
*/
public QueryAnalyzerResponseDTO analyseQueryComplexity(int maxQueryComplexity, String payload,
FieldComplexityCalculator fieldComplexityCalculator) {
return analyseQueryComplexity(maxQueryComplexity, payload, fieldComplexityCalculator, null);
}

/**
* 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
* @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,
HashMap<String, Object> variableMap) {

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

ExecutionResult executionResult = runtime.execute(payload);
if (variableMap == null) {
variableMap = new HashMap<>();
}

HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
.query(payload)
.context(Optional.ofNullable(null))
.root(null)
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
.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 +207,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,
HashMap<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 @@ -130,8 +130,8 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
WebSocketUtils.setApiPropertyToChannel(ctx, Constants.BACKEND_END_TIME_PROPERTY,
System.currentTimeMillis());
if (msg instanceof TextWebSocketFrame) {
WebSocketUtils.setApiPropertyToChannel(ctx, Constants.RESPONSE_SIZE,
((TextWebSocketFrame) msg).text().length());
// WebSocketUtils.setApiPropertyToChannel(ctx, Constants.RESPONSE_SIZE,
// ((TextWebSocketFrame) msg).text().length());
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
}
}
// publish analytics events if analytics is enabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,22 @@
import org.apache.synapse.SynapseConstants;
import org.apache.synapse.core.axis2.Axis2MessageContext;
import org.wso2.carbon.apimgt.common.analytics.collectors.AnalyticsDataProvider;
import org.wso2.carbon.apimgt.common.analytics.collectors.RequestDataCollector;
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.GraphQLAnalyticsDataProvider;
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,13 +87,27 @@ public boolean handleResponseOutFlow(MessageContext messageContext) {
if (messageContext.getPropertyKeySet().contains(InboundWebsocketConstants.WEBSOCKET_SUBSCRIBER_PATH)) {
return true;
}
Object transportHeaders = ((Axis2MessageContext) messageContext).getAxis2MessageContext().
getProperty(TRANSPORT_HEADERS);
messageContext.setProperty(TRANSPORT_HEADERS, transportHeaders);
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved

AnalyticsDataProvider provider;
Object skipPublishMetrics = messageContext.getProperty(Constants.SKIP_DEFAULT_METRICS_PUBLISHING);
if (skipPublishMetrics != null && (Boolean) skipPublishMetrics) {
provider = new AsyncAnalyticsDataProvider(messageContext);
} else {
provider = new SynapseAnalyticsDataProvider(messageContext,
if (messageContext.getProperty(APIConstants.API_TYPE) == "GRAPHQL") {
if (APIUtil.isAnalyticsEnabled()) {
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
GraphQLOperationInfoAnalyzer operationInfoAnalyzer = new GraphQLOperationInfoAnalyzer();
operationInfoAnalyzer.analyzePayload(messageContext);
}
provider = new GraphQLAnalyticsDataProvider(messageContext,
ServiceReferenceHolder.getInstance().getAnalyticsCustomDataProvider());
} else {
if (skipPublishMetrics != null && (Boolean) skipPublishMetrics) {
provider = new AsyncAnalyticsDataProvider(messageContext);

} else {
provider = new SynapseAnalyticsDataProvider(messageContext,
ServiceReferenceHolder.getInstance().getAnalyticsCustomDataProvider());
}
}
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
GenericRequestDataCollector dataCollector = new GenericRequestDataCollector(provider);
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ 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_DEPTH = "operationDepth";
public static final String OPERATION_COMPLEXITY = "operationComplexity";
public static final String FIELD_USAGE = "fieldUsage";
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";

Expand All @@ -44,6 +49,7 @@ public class Constants {
public static final String UNKNOWN_VALUE = "UNKNOWN";
public static final int UNKNOWN_INT_VALUE = -1;
public static final String ANONYMOUS_VALUE = "anonymous";
public static final String TYPE_USAGE = "typeUsage";

public static final class ERROR_CODE_RANGES {
public static final int AUTH_FAILURE_START = 900900;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,11 @@ public Map<String, Object> getProperties() {
return customProperties;
}

@Override
public Map<String, Object> getOperationProperties() {
return null;
}

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 @@ -52,6 +54,7 @@
import java.util.Base64;
import java.util.HashMap;
import java.util.Set;
import java.util.Iterator;

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

Expand All @@ -62,6 +65,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 +96,8 @@ public boolean handleRequest(MessageContext messageContext) {
return true;
}
String payload;
OMElement variables;
HashMap<String, Object> variablesMap = null;
Parser parser = new Parser();
org.apache.axis2.context.MessageContext axis2MC = ((Axis2MessageContext) messageContext).
getAxis2MessageContext();
Expand All @@ -106,6 +112,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 +148,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 +172,34 @@ public boolean handleRequest(MessageContext messageContext) {
return false;
}

private HashMap<String, Object> convertArrayToMap(ArrayList<OMElement> variablesArray) {
HimanshiDeSilva marked this conversation as resolved.
Show resolved Hide resolved
HashMap<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 HashMap<String, Object> getVariables(OMElement omElement, HashMap<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 +262,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