lib/instrumentation/modules/graphql.js (215 lines of code) (raw):
/*
* Copyright Elasticsearch B.V. and other contributors where applicable.
* Licensed under the BSD 2-Clause License; you may not use this file except in
* compliance with the BSD 2-Clause License.
*/
'use strict';
var semver = require('semver');
var clone = require('shallow-clone-shim');
var getPathFromRequest = require('../express-utils').getPathFromRequest;
module.exports = function (graphql, agent, { version, enabled }) {
if (!enabled) return graphql;
if (
!semver.satisfies(version, '>=0.7.0 <17') ||
!graphql.Kind ||
typeof graphql.Source !== 'function' ||
typeof graphql.parse !== 'function' ||
typeof graphql.validate !== 'function' ||
typeof graphql.execute !== 'function'
) {
agent.logger.debug(
'graphql version %s not supported - aborting...',
version,
);
return graphql;
}
// Over the many versions the `graphql()` and `execute()` functions have
// changed in what arguments they accept:
// - Initially they only supported positional arguments:
// function graphql(schema, requestString, rootValue, contextValue, variableValues, operationName)
// - Starting in v0.10.0 (in #356), they started accepting a second form where
// all fields were passed in an object as the first argument:
// function graphql(argsOrSchema, source, rootValue, contextValue, variableValues, operationName, fieldResolver)
// and `arguments.length === 1` was used as the test to determine which.
// - Starting in v16 (in #2904), they dropped positional arguments:
// function graphql(args)
const onlySupportsPositionalArgs = semver.lt(version, '0.10.0');
const onlySupportsSingleArg = semver.gte(version, '16.0.0');
const ins = agent._instrumentation;
return clone({}, graphql, {
graphql(descriptor) {
const getter = descriptor.get;
if (getter) {
descriptor.get = function get() {
return wrapGraphql(getter());
};
}
return descriptor;
},
execute(descriptor) {
const getter = descriptor.get;
if (getter) {
descriptor.get = function get() {
return wrapExecute(getter());
};
}
return descriptor;
},
});
function wrapGraphql(orig) {
return function wrappedGraphql(args) {
agent.logger.debug('intercepted call to graphql.graphql');
const span = ins.createSpan(
'GraphQL: Unknown Query',
'db',
'graphql',
'execute',
);
if (!span) {
return orig.apply(this, arguments);
}
let schema;
let source;
let operationName;
const singleArgForm =
onlySupportsSingleArg ||
(!onlySupportsPositionalArgs && arguments.length === 1);
if (singleArgForm) {
schema = args.schema;
source = args.source;
operationName = args.operationName;
} else {
schema = arguments[0];
source = arguments[1];
operationName = arguments[5];
}
const sourceObj =
typeof source === 'string'
? new graphql.Source(source || '', 'GraphQL request')
: source;
if (sourceObj) {
var documentAST;
try {
documentAST = graphql.parse(sourceObj);
} catch (syntaxError) {
agent.logger.debug(
'graphql.parse(source) failed - skipping graphql query extraction',
);
}
if (documentAST) {
var validationErrors = graphql.validate(schema, documentAST);
if (validationErrors && validationErrors.length === 0) {
var queries = extractDetails(documentAST, operationName).queries;
if (queries.length > 0)
span.name = 'GraphQL: ' + queries.join(', ');
}
}
} else {
agent.logger.debug(
'graphql.Source(query) failed - skipping graphql query extraction',
);
}
const spanRunContext = ins.currRunContext().enterSpan(span);
const p = ins.withRunContext(spanRunContext, orig, this, ...arguments);
p.then(function () {
span.end();
});
return p;
};
}
function wrapExecute(orig) {
return function wrappedExecute(args) {
agent.logger.debug('intercepted call to graphql.execute');
const span = ins.createSpan(
'GraphQL: Unknown Query',
'db',
'graphql',
'execute',
);
if (!span) {
agent.logger.debug(
'no active transaction found - skipping graphql tracing',
);
return orig.apply(this, arguments);
}
let document;
let operationName;
const singleArgForm =
onlySupportsSingleArg ||
(!onlySupportsPositionalArgs && arguments.length === 1);
if (singleArgForm) {
document = args.document;
operationName = args.operationName;
} else {
document = arguments[1];
operationName = arguments[5];
}
var details = extractDetails(document, operationName);
var queries = details.queries;
operationName =
operationName ||
(details.operation &&
details.operation.name &&
details.operation.name.value);
if (queries.length > 0) {
span.name =
'GraphQL: ' +
(operationName ? operationName + ' ' : '') +
queries.join(', ');
}
// `_graphqlRoute` is a boolean, set in instrumentations of other modules
// that specify 'graphql' in peerDependencies (e.g. '@apollo/server') to
// indicate that this transaction is for a GraphQL request.
const trans = span.transaction;
if (trans._graphqlRoute) {
var name =
queries.length > 0 ? queries.join(', ') : 'Unknown GraphQL query';
if (trans.req) var path = getPathFromRequest(trans.req, true);
var defaultName = name;
defaultName = path ? defaultName + ' (' + path + ')' : defaultName;
defaultName = operationName
? operationName + ' ' + defaultName
: defaultName;
trans.setDefaultName(defaultName);
trans.type = 'graphql';
}
const spanRunContext = ins.currRunContext().enterSpan(span);
const p = ins.withRunContext(spanRunContext, orig, this, ...arguments);
if (typeof p.then === 'function') {
p.then(function () {
span.end();
});
} else {
span.end();
}
return p;
};
}
function extractDetails(document, operationName) {
var queries = [];
var operation;
if (document && Array.isArray(document.definitions)) {
document.definitions.some(function (definition) {
if (
!definition ||
definition.kind !== graphql.Kind.OPERATION_DEFINITION
)
return false;
if (!operationName && operation) return false;
if (
!operationName ||
(definition.name && definition.name.value === operationName)
) {
operation = definition;
return true;
}
return false;
});
var selections =
operation &&
operation.selectionSet &&
operation.selectionSet.selections;
if (selections && Array.isArray(selections)) {
for (const selection of selections) {
const kind = selection.name && selection.name.kind;
if (kind === graphql.Kind.NAME) {
const queryName = selection.name.value;
if (queryName) queries.push(queryName);
}
}
queries = queries.sort(function (a, b) {
if (a > b) return 1;
else if (a < b) return -1;
return 0;
});
}
} else {
agent.logger.debug(
'unexpected document format - skipping graphql query extraction',
);
}
return { queries, operation };
}
};