Skip to content

Commit

Permalink
Adding Support for GraphQL Observability (#1852) (#1860)
Browse files Browse the repository at this point in the history
* Adding Support for GraphQL Observability Metrics (#1852)

* [Automated] Update the native jar versions

* Add observability metric tags

* Update the metrics tags to use underscores

* Update the spec with observability section

* [Automated] Update the native jar versions

* Update the GraphQL spec with observability functionalities

* Apply suggestions from code review

Co-authored-by: DimuthuMadushan <35717653+DimuthuMadushan@users.noreply.github.com>

---------

Co-authored-by: DimuthuMadushan <35717653+DimuthuMadushan@users.noreply.github.com>

* Check Metrics Enabled Before Adding Tags (#1857)

* Include Observability support for graphql (#1859)

* Initial tracing implementation

* Set observer-context to strand

* Fix starting observer-context

* Update tracing functionalities

---------

Co-authored-by: Thisaru Guruge <thisaru.guruge@gmail.com>

* Add tracing details on the spec

* Handle response errors with observer context (#1863)

* Initial tracing implementation

* Set observer-context to strand

* Fix starting observer-context

* Update tracing functionalities

* Handles error responses with observer context

* [Automated] Update the native jar versions

* Address review suggestions

* Update version

* [Automated] Update the native jar versions

* Check observability enabled

* [Automated] Update the native jar versions

* [Automated] Update the native jar versions

* Update the ballerina lang version

* remove redundant check

* Address review suggestions

---------

Co-authored-by: Thisaru Guruge <thisaru.guruge@gmail.com>
Co-authored-by: Thisaru Guruge <thisaru@wso2.com>

* Update the Observability Metrics Field Name Retrieval (#1866)

* Update book_reviews_mutation test results on Fri May  3 22:28:00 UTC 2024

* Update book_reviews_subscription test results on Fri May  3 22:28:00 UTC 2024

* Update snowtooth test results on Fri May  3 22:28:00 UTC 2024

* Update book_reviews_mutation test results on Sat May  4 22:28:19 UTC 2024

* Update book_reviews_subscription test results on Sat May  4 22:28:20 UTC 2024

* Update snowtooth test results on Sat May  4 22:28:20 UTC 2024

* [Automated] Sync master after 1.12.0 release (#1861)

* [Automated] Update the native jar versions

* Move dependencies to stable versions

* [Automated] Update the native jar versions

* [Automated] Update the native jar versions

* [Gradle Release Plugin] - pre tag commit:  'v1.12.0'.

* [Gradle Release Plugin] - new version commit:  'v1.12.1-SNAPSHOT'.

* Update the changelog after 1.12.0 release

---------

Co-authored-by: ballerina-bot <ballerina-bot@ballerina.org>
Co-authored-by: Thisaru Guruge <thisaru@wso2.com>

* Update book_reviews_mutation test results on Sun May  5 22:28:14 UTC 2024

* Update book_reviews_subscription test results on Sun May  5 22:28:14 UTC 2024

* Update snowtooth test results on Sun May  5 22:28:14 UTC 2024

* Update the metrics tag retrieval for field name

---------

Co-authored-by: ballerina-bot <ballerina-bot@ballerina.org>
Co-authored-by: ballerina-bot <ballerinalang@wso2.com>

* Update the field name for observability metrics

---------

Co-authored-by: DimuthuMadushan <35717653+DimuthuMadushan@users.noreply.github.com>
Co-authored-by: Nipuna Madhushan <51471998+NipunaMadhushan@users.noreply.github.com>
Co-authored-by: ballerina-bot <ballerina-bot@ballerina.org>
Co-authored-by: ballerina-bot <ballerinalang@wso2.com>
  • Loading branch information
5 people authored May 6, 2024
1 parent 41fc970 commit f631d46
Show file tree
Hide file tree
Showing 25 changed files with 685 additions and 154 deletions.
4 changes: 3 additions & 1 deletion ballerina-tests/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
[package]
org = "ballerina"
name = "graphql_tests"
version = "1.12.0"
version = "1.13.0"

[build-options]
observabilityIncluded = true
20 changes: 17 additions & 3 deletions ballerina-tests/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ modules = [
[[package]]
org = "ballerina"
name = "graphql"
version = "1.12.0"
version = "1.13.0"
dependencies = [
{org = "ballerina", name = "auth"},
{org = "ballerina", name = "cache"},
Expand All @@ -83,6 +83,7 @@ dependencies = [
{org = "ballerina", name = "log"},
{org = "ballerina", name = "mime"},
{org = "ballerina", name = "oauth2"},
{org = "ballerina", name = "observe"},
{org = "ballerina", name = "task"},
{org = "ballerina", name = "time"},
{org = "ballerina", name = "uuid"},
Expand All @@ -98,7 +99,7 @@ modules = [
[[package]]
org = "ballerina"
name = "graphql_tests"
version = "1.12.0"
version = "1.13.0"
dependencies = [
{org = "ballerina", name = "constraint"},
{org = "ballerina", name = "file"},
Expand All @@ -112,7 +113,8 @@ dependencies = [
{org = "ballerina", name = "test"},
{org = "ballerina", name = "url"},
{org = "ballerina", name = "uuid"},
{org = "ballerina", name = "websocket"}
{org = "ballerina", name = "websocket"},
{org = "ballerinai", name = "observe"}
]
modules = [
{org = "ballerina", packageName = "graphql_tests", moduleName = "graphql_tests"}
Expand Down Expand Up @@ -406,3 +408,15 @@ modules = [
{org = "ballerina", packageName = "websocket", moduleName = "websocket"}
]

[[package]]
org = "ballerinai"
name = "observe"
version = "0.0.0"
dependencies = [
{org = "ballerina", name = "jballerina.java"},
{org = "ballerina", name = "observe"}
]
modules = [
{org = "ballerinai", packageName = "observe", moduleName = "observe"}
]

10 changes: 5 additions & 5 deletions ballerina/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
org = "ballerina"
name = "graphql"
version = "1.12.0"
version = "1.13.0"
authors = ["Ballerina"]
export=["graphql", "graphql.subgraph", "graphql.dataloader"]
keywords = ["gql", "network", "query", "service"]
Expand All @@ -16,11 +16,11 @@ graalvmCompatible = true
[[platform.java17.dependency]]
groupId = "io.ballerina.stdlib"
artifactId = "graphql-native"
version = "1.12.0"
path = "../native/build/libs/graphql-native-1.12.0.jar"
version = "1.13.0"
path = "../native/build/libs/graphql-native-1.13.0-SNAPSHOT.jar"

[[platform.java17.dependency]]
groupId = "io.ballerina.stdlib"
artifactId = "graphql-commons"
version = "1.12.0"
path = "../commons/build/libs/graphql-commons-1.12.0.jar"
version = "1.13.0"
path = "../commons/build/libs/graphql-commons-1.13.0-SNAPSHOT.jar"
4 changes: 2 additions & 2 deletions ballerina/CompilerPlugin.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ id = "graphql-compiler-plugin"
class = "io.ballerina.stdlib.graphql.compiler.GraphqlCompilerPlugin"

[[dependency]]
path = "../compiler-plugin/build/libs/graphql-compiler-plugin-1.12.0.jar"
path = "../compiler-plugin/build/libs/graphql-compiler-plugin-1.13.0-SNAPSHOT.jar"

[[dependency]]
path = "../commons/build/libs/graphql-commons-1.12.0.jar"
path = "../commons/build/libs/graphql-commons-1.13.0-SNAPSHOT.jar"
7 changes: 6 additions & 1 deletion ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ modules = [
[[package]]
org = "ballerina"
name = "graphql"
version = "1.12.0"
version = "1.13.0"
dependencies = [
{org = "ballerina", name = "auth"},
{org = "ballerina", name = "cache"},
Expand All @@ -89,6 +89,7 @@ dependencies = [
{org = "ballerina", name = "log"},
{org = "ballerina", name = "mime"},
{org = "ballerina", name = "oauth2"},
{org = "ballerina", name = "observe"},
{org = "ballerina", name = "task"},
{org = "ballerina", name = "test"},
{org = "ballerina", name = "time"},
Expand Down Expand Up @@ -313,6 +314,10 @@ version = "1.2.3"
dependencies = [
{org = "ballerina", name = "jballerina.java"}
]
modules = [
{org = "ballerina", packageName = "observe", moduleName = "observe"},
{org = "ballerina", packageName = "observe", moduleName = "observe.mockextension"}
]

[[package]]
org = "ballerina"
Expand Down
15 changes: 15 additions & 0 deletions ballerina/constants.bal
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,18 @@ const DEFAULT_PREFETCH_METHOD_NAME_PREFIX = "pre";

// Localhost
const LOCALHOST = "localhost";

// Observability Metrics
const GRAPHQL_OPERATION_NAME = "graphql_service_operation_name";
const GRAPHQL_OPERATION_TYPE = "graphql_service_operation_type";
const GRAPHQL_ERRORS = "graphql_service_errors";
const GRAPHQL_FIELD_NAME = "graphql_service_field_name";

const GRAPHQL_PARSING_ERROR = "graphql_service_parsing_error";
const GRAPHQL_VALIDATION_ERROR = "graphql_service_validation_error";
const GRAPHQL_EXECUTION_ERROR = "graphql_service_execution_error";
const GRPAHQL_ANONYMOUS_OPERATION = "anonymous";

// Observability Tracing
const OPERATION_VALIDATION = "validation";
const OPERATION_EXECUTION = "execution";
85 changes: 61 additions & 24 deletions ballerina/engine.bal
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ isolated class Engine {
private final readonly & ServerCacheConfig? cacheConfig;

isolated function init(string schemaString, int? maxQueryDepth, Service s,
readonly & (readonly & Interceptor)[] interceptors, boolean introspection,
boolean validation, ServerCacheConfig? cacheConfig = (),
ServerCacheConfig? fieldCacheConfig = ())
readonly & (readonly & Interceptor)[] interceptors, boolean introspection,
boolean validation, ServerCacheConfig? cacheConfig = (),
ServerCacheConfig? fieldCacheConfig = ())
returns Error? {
if maxQueryDepth is int && maxQueryDepth < 1 {
return error Error("Max query depth value must be a positive integer");
Expand Down Expand Up @@ -82,18 +82,23 @@ isolated class Engine {
isolated function validate(string documentString, string? operationName, map<json>? variables)
returns parser:OperationNode|OutputObject {

addObservabilityMetricsTags(GRAPHQL_OPERATION_NAME, operationName ?: GRPAHQL_ANONYMOUS_OPERATION);
ParseResult|OutputObject result = self.parse(documentString);
if result is OutputObject {
addObservabilityMetricsTags(GRAPHQL_ERRORS, GRAPHQL_PARSING_ERROR);
return result;
}

OutputObject|parser:DocumentNode validationResult = self.validateDocument(result, operationName, variables);
if validationResult is OutputObject {
addObservabilityMetricsTags(GRAPHQL_ERRORS, GRAPHQL_VALIDATION_ERROR);
return validationResult;
}
// Since unused operation nodes are removed from the Document node, it includes only the operation node
// related to the currently executing operation. Hence directly access that node from here.
return validationResult.getOperations()[0];
parser:OperationNode operationNode = validationResult.getOperations()[0];
addObservabilityMetricsTags(GRAPHQL_OPERATION_TYPE, operationNode.getKind());
return operationNode;
}

isolated function getResult(parser:OperationNode operationNode, Context context, any|error result = ())
Expand Down Expand Up @@ -206,7 +211,7 @@ isolated class Engine {

worker fieldAndVariableValidatorWorker returns ErrorDetail[] {
ErrorDetail[] errors = [];
ValidatorVisitor[] validatorVisitors = [
ValidatorVisitor[] validatorVisitors = [
new VariableValidatorVisitor(schema, vars, nodeModifierContext),
new FieldValidatorVisitor(schema, nodeModifierContext)
];
Expand Down Expand Up @@ -271,30 +276,48 @@ isolated class Engine {

isolated function resolve(Context context, Field 'field, boolean executePrefetchMethod = true) returns anydata {
parser:FieldNode fieldNode = 'field.getInternalNode();

if executePrefetchMethod {
service object {}? serviceObject = 'field.getServiceObject();
if serviceObject is service object {} {
string prefetchMethodName = getPrefetchMethodName(serviceObject, 'field)
?: getDefaultPrefetchMethodName(fieldNode.getName());
if self.hasPrefetchMethod(serviceObject, prefetchMethodName) {
return self.getResultFromPrefetchMethodExecution(context, 'field, serviceObject, prefetchMethodName);
addTracingInfomation({
context,
serviceName: prefetchMethodName,
operationType: 'field.getOperationType()
});
anydata result = self.getResultFromPrefetchMethodExecution(context, 'field, serviceObject, prefetchMethodName);
stopTracing(context);
return result;
}
}
}

(readonly & Interceptor)? interceptor = context.getNextInterceptor('field);
__Type fieldType = 'field.getFieldType();
ResponseGenerator responseGenerator = new (self, context, fieldType, 'field.getPath().clone(),
'field.getCacheConfig(), 'field.getParentArgHashes());
'field.getCacheConfig(), 'field.getParentArgHashes()
);
do {
if interceptor is readonly & Interceptor {
any|error result = self.executeInterceptor(interceptor, 'field, context);
string interceptorName = self.getInterceptorName(interceptor);
return check validateInterceptorReturnValue(fieldType, result, interceptorName);
addTracingInfomation({
context,
serviceName: interceptorName,
operationType: 'field.getOperationType()
});
any|error result = self.executeInterceptor(interceptor, 'field, context);
anydata response = check validateInterceptorReturnValue(fieldType, result, interceptorName);
stopTracing(context);
return response;
}
any fieldValue;
if 'field.getOperationType() == parser:OPERATION_QUERY && 'field.isCacheEnabled() {
parser:RootOperationType operationType = 'field.getOperationType();
if operationType == parser:OPERATION_QUERY && 'field.isCacheEnabled() {
string cacheName = string `${'field.getName()}.cache`;
addTracingInfomation({context, serviceName: cacheName, operationType});
addFieldMetric('field);
string cacheKey = 'field.getCacheKey();
any|error cachedValue = self.getFromCache(cacheKey);
if cachedValue is any {
Expand All @@ -307,16 +330,26 @@ isolated class Engine {
}
}
} else {
addTracingInfomation({
context,
serviceName: 'field.getName(),
operationType
});
addFieldMetric('field);
fieldValue = check self.getFieldValue(context, 'field, responseGenerator);
}
return responseGenerator.getResult(fieldValue, fieldNode);
anydata response = responseGenerator.getResult(fieldValue, fieldNode);
stopTracing(context);
return response;
} on fail error errorValue {
return responseGenerator.getResult(errorValue, fieldNode);
anydata result = responseGenerator.getResult(errorValue, fieldNode);
stopTracing(context);
return result;
}
}

private isolated function getResultFromPrefetchMethodExecution(Context context, Field 'field,
service object {} serviceObject, string prefetchMethodName) returns PlaceholderNode? {
service object {} serviceObject, string prefetchMethodName) returns PlaceholderNode? {
handle? prefetchMethodHandle = self.getMethod(serviceObject, prefetchMethodName);
if prefetchMethodHandle is () {
return ();
Expand All @@ -343,7 +376,7 @@ isolated class Engine {
isolated function resolveRemoteMethod(Context context, Field 'field, ResponseGenerator responseGenerator) returns any|error {
service object {}? serviceObject = 'field.getServiceObject();
if serviceObject is service object {} {
return self.executeMutationMethod(context, serviceObject, 'field, responseGenerator, self.validation);
return self.executeMutationMethod(context, serviceObject, 'field, responseGenerator, self.validation);
}
return 'field.getFieldValue();
}
Expand All @@ -364,7 +397,7 @@ isolated class Engine {
}

isolated function resolveHierarchicalResourceFromFragment(Context context, Field 'field,
parser:FragmentNode fragmentNode, map<anydata> result) {
parser:FragmentNode fragmentNode, map<anydata> result) {
foreach parser:SelectionNode selection in fragmentNode.getSelections() {
if selection is parser:FieldNode {
self.getHierarchicalResult(context, 'field, selection, result);
Expand All @@ -374,12 +407,16 @@ isolated class Engine {
}
}

isolated function getHierarchicalResult(Context context, Field 'field, parser:FieldNode fieldNode, map<anydata> result) {
isolated function getHierarchicalResult(Context context, Field 'field, parser:FieldNode fieldNode,
map<anydata> result) {
string[] resourcePath = 'field.getResourcePath();
(string|int)[] path = 'field.getPath().clone();
path.push(fieldNode.getName());
__Type fieldType = getFieldTypeFromParentType('field.getFieldType(), self.schema.types, fieldNode);
Field selectionField = new (fieldNode, fieldType, 'field.getServiceObject(), path = path, resourcePath = resourcePath);
__Type parentType = 'field.getFieldType();
__Type fieldType = getFieldTypeFromParentType(parentType, self.schema.types, fieldNode);
Field selectionField = new (fieldNode, fieldType, parentType, 'field.getServiceObject(), path = path,
resourcePath = resourcePath
);
context.resetInterceptorCount();
anydata fieldValue = self.resolve(context, selectionField);
result[fieldNode.getAlias()] = fieldValue is ErrorDetail ? () : fieldValue;
Expand All @@ -398,7 +435,7 @@ isolated class Engine {
isolated function invalidate(string path) returns error? {
cache:Cache? cache = self.cache;
if cache is cache:Cache {
string[] keys = cache.keys().filter(isolated function (string key) returns boolean {
string[] keys = cache.keys().filter(isolated function(string key) returns boolean {
return key.startsWith(string `${path}.`);
});
foreach string key in keys {
Expand Down Expand Up @@ -435,18 +472,18 @@ isolated class Engine {
} external;

isolated function executeQueryResource(Context context, service object {} serviceObject, handle resourceMethod,
Field 'field, ResponseGenerator responseGenerator, boolean validation)
Field 'field, ResponseGenerator responseGenerator, boolean validation)
returns any|error = @java:Method {
'class: "io.ballerina.stdlib.graphql.runtime.engine.Engine"
} external;

isolated function executeMutationMethod(Context context, service object {} serviceObject,
Field 'field, ResponseGenerator responseGenerator, boolean validation) returns any|error = @java:Method {
Field 'field, ResponseGenerator responseGenerator, boolean validation) returns any|error = @java:Method {
'class: "io.ballerina.stdlib.graphql.runtime.engine.Engine"
} external;

isolated function executeSubscriptionResource(Context context, service object {} serviceObject,
Field 'field, ResponseGenerator responseGenerator, boolean validation) returns any|error = @java:Method {
Field 'field, ResponseGenerator responseGenerator, boolean validation) returns any|error = @java:Method {
'class: "io.ballerina.stdlib.graphql.runtime.engine.Engine"
} external;

Expand All @@ -465,7 +502,7 @@ isolated class Engine {
} external;

isolated function executePrefetchMethod(Context context, service object {} serviceObject,
handle prefetchMethodHandle, Field 'field) = @java:Method {
handle prefetchMethodHandle, Field 'field) = @java:Method {
'class: "io.ballerina.stdlib.graphql.runtime.engine.Engine"
} external;
}
14 changes: 8 additions & 6 deletions ballerina/engine_utils.bal
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,15 @@ isolated function isValidReturnType(__Type 'type, anydata value) returns boolean
}

isolated function getFieldObject(parser:FieldNode fieldNode, parser:RootOperationType operationType, __Schema schema,
Engine engine, any|error fieldValue = ()) returns Field {
Engine engine, any|error fieldValue = ()) returns Field {
(string|int)[] path = [fieldNode.getAlias()];
string operationTypeName = getOperationTypeNameFromOperationType(operationType);
__Type parentType = <__Type>getTypeFromTypeArray(schema.types, operationTypeName);
__Type fieldType = getFieldTypeFromParentType(parentType, schema.types, fieldNode);
string parentArgHashes = generateArgHash(fieldNode.getArguments());
return new (fieldNode, fieldType, engine.getService(), path, operationType, fieldValue = fieldValue,
cacheConfig = engine.getCacheConfig(), parentArgHashes = [parentArgHashes]);
return new (fieldNode, fieldType, parentType, engine.getService(), path, operationType, fieldValue = fieldValue,
cacheConfig = engine.getCacheConfig(), parentArgHashes = [parentArgHashes]
);
}

isolated function createSchema(string schemaString) returns readonly & __Schema|Error = @java:Method {
Expand Down Expand Up @@ -121,11 +122,12 @@ isolated function getDefaultPrefetchMethodName(string fieldName) returns string
});
}

isolated function initCacheTable(ServerCacheConfig? operationCacheConfig, ServerCacheConfig? fieldCacheConfig) returns cache:Cache? {
isolated function initCacheTable(ServerCacheConfig? operationCacheConfig, ServerCacheConfig? fieldCacheConfig)
returns cache:Cache? {
if operationCacheConfig is ServerCacheConfig && operationCacheConfig.enabled {
return new ({capacity:operationCacheConfig.maxSize, evictionFactor:0.2, defaultMaxAge:operationCacheConfig.maxAge});
return new ({capacity: operationCacheConfig.maxSize, evictionFactor: 0.2, defaultMaxAge: operationCacheConfig.maxAge});
} else if fieldCacheConfig is ServerCacheConfig && fieldCacheConfig.enabled {
return new({capacity:fieldCacheConfig.maxSize, evictionFactor:0.2, defaultMaxAge:fieldCacheConfig.maxAge});
return new ({capacity: fieldCacheConfig.maxSize, evictionFactor: 0.2, defaultMaxAge: fieldCacheConfig.maxAge});
}
return;
}
Expand Down
Loading

0 comments on commit f631d46

Please sign in to comment.