ca-apm-probe
Version:
CA APM Node.js Agent monitors real-time health and performance of Node.js applications
268 lines (233 loc) • 10.3 kB
JavaScript
/**
* Copyright (c) 2015 CA. All rights reserved.
*
* This software and all information contained therein is confidential and proprietary and
* shall not be duplicated, used, disclosed or disseminated in any way except as authorized
* by the applicable license agreement, without the express written permission of CA. All
* authorized reproductions must be marked with this language.
*
* EXCEPT AS SET FORTH IN THE APPLICABLE LICENSE AGREEMENT, TO THE EXTENT
* PERMITTED BY APPLICABLE LAW, CA PROVIDES THIS SOFTWARE WITHOUT WARRANTY
* OF ANY KIND, INCLUDING WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT WILL CA BE
* LIABLE TO THE END USER OR ANY THIRD PARTY FOR ANY LOSS OR DAMAGE, DIRECT OR
* INDIRECT, FROM THE USE OF THIS SOFTWARE, INCLUDING WITHOUT LIMITATION, LOST
* PROFITS, BUSINESS INTERRUPTION, GOODWILL, OR LOST DATA, EVEN IF CA IS
* EXPRESSLY ADVISED OF SUCH LOSS OR DAMAGE.
*/
var metrics = require('../metrics');
var metricsReporter = metrics.getReporter();
var logger = require("../logger.js");
var config = require('../configdata');
const IGNORED_PATH_FIELDS = ['id', '__typename']
const OBFUSCATION_STR = '***';
const COLON = ':';
const COMMA = ',';
const METRIC_DELIMITER = '|';
const METRIC_NAME_PREFIX = 'ApolloServer' + METRIC_DELIMITER + 'GraphQL' + METRIC_DELIMITER;
const OPERATION_COUNT = 'operationCount';
const OPERATION_ERROR_COUNT = 'operationErrorCount';
const OPERATION_RESPONSE_TIME = 'operationResponseTime (ms)';
const OPERATION_RESPONSE_TIME_MICRO = 'operationResponseTime (us)';
const RESOLVERS = 'resolvers';
const RESOLVERS_PREFIX = METRIC_NAME_PREFIX + RESOLVERS + COLON;
function probePlugin() {
logger.info('Apollo Server plugin added.');
var metricCache = new Set();
let prevFieldToShow;
return {
requestDidStart(requestContext) {
var metricInfo = {reportAvgDurationMetric : {}, reportIntervalCounterMetric : {}, reportStringMetric : {}};
var apolloConfigData = config.getConfigData().graphql.apolloServer;
var queryTime = process.hrtime();
var clampCount = apolloConfigData.resolvers.clamp;
var fieldsToShow = apolloConfigData.resolvers.fieldsToShow || "*";
if(prevFieldToShow != fieldsToShow){
prevFieldToShow = fieldsToShow;
metricCache.clear();
}
let duration;
let deepestQueryPath;
let rootOperationMetricName;
return {
didResolveOperation(resolveContext) {
const operationDetails = getOperationDetails(resolveContext);
deepestQueryPath = operationDetails.deepestUniquePath;
rootOperationMetricName = METRIC_NAME_PREFIX + operationDetails.operationType + METRIC_DELIMITER + deepestQueryPath.join(COMMA);
var count = metricInfo.reportIntervalCounterMetric[rootOperationMetricName + COLON + OPERATION_COUNT] | 0;
metricInfo.reportIntervalCounterMetric[rootOperationMetricName + COLON + OPERATION_COUNT] = count + 1;
},
didEncounterErrors(errorsRequestContext) {
var count = metricInfo.reportIntervalCounterMetric[rootOperationMetricName + COLON + OPERATION_ERROR_COUNT] | 0;
metricInfo.reportIntervalCounterMetric[rootOperationMetricName + COLON + OPERATION_ERROR_COUNT] = count + 1;
},
executionDidStart() {
return {
executionDidEnd: () => {
duration = process.hrtime(queryTime);
},
willResolveField({source, args, context, info}) {
var fieldResponseTime = process.hrtime();
const pathArray = flattenToArray(info.path);
const formattedPath = pathArray.reverse().join('.');
if(!deepestQueryPath.includes(formattedPath)){
if(!apolloConfigData.resolvers.enabled){
return;
}
var resolversFieldsToShow = fieldsToShow.split(',');
if(resolversFieldsToShow[0] != '*'){
const startsWithFieldToShow = resolversFieldsToShow.filter((fieldToShow) => formattedPath.startsWith(fieldToShow.trim()));
if(startsWithFieldToShow.length == 0){
return;
}
}
}
var fieldMetricName = RESOLVERS_PREFIX + formattedPath + '.';
if(deepestQueryPath.includes(formattedPath)){
fieldMetricName = rootOperationMetricName + METRIC_DELIMITER + RESOLVERS + METRIC_DELIMITER + formattedPath + COLON;
} else if(metricCache.size == clampCount){
if(!metricCache.has(fieldMetricName)){
fieldMetricName = RESOLVERS_PREFIX + 'default' + '.';
}
} else{
metricCache.add(fieldMetricName);
}
var count = metricInfo.reportIntervalCounterMetric[fieldMetricName + OPERATION_COUNT] | 0;
metricInfo.reportIntervalCounterMetric[fieldMetricName + OPERATION_COUNT] = count + 1;
return (error) => {
if (error) {
var errCount = metricInfo.reportIntervalCounterMetric[fieldMetricName + OPERATION_ERROR_COUNT] | 0;
metricInfo.reportIntervalCounterMetric[fieldMetricName + OPERATION_ERROR_COUNT] = errCount + 1;
}
var responseTime = metricInfo.reportAvgDurationMetric[fieldMetricName + OPERATION_RESPONSE_TIME_MICRO] | 0;
metricInfo.reportAvgDurationMetric[fieldMetricName + OPERATION_RESPONSE_TIME_MICRO] = responseTime + (durationHrTimeToNanos(process.hrtime(fieldResponseTime)) / 1000);
}
}
}
},
willSendResponse(responseContext) {
var responseTime = metricInfo.reportAvgDurationMetric[rootOperationMetricName + COLON + OPERATION_RESPONSE_TIME] | 0;
metricInfo.reportAvgDurationMetric[rootOperationMetricName + COLON + OPERATION_RESPONSE_TIME] = responseTime + durationHrTimeToNanos(duration)/1000000;
for (const metricName in metricInfo.reportAvgDurationMetric) {
metricsReporter.reportAvgDurationMetric( metricName, metricInfo.reportAvgDurationMetric[metricName]);
}
for (const metricName in metricInfo.reportIntervalCounterMetric) {
metricsReporter.reportIntervalCounterMetric( metricName, metricInfo.reportIntervalCounterMetric[metricName]);
}
}
}
}
}
}
function durationHrTimeToNanos(hrtime) {
return hrtime[0] * 1e9 + hrtime[1];
}
function flattenToArray(fieldPath) {
const pathArray = []
let thisPath = fieldPath
while (thisPath) {
if (typeof thisPath.key !== 'number') {
pathArray.push(thisPath.key)
}
thisPath = thisPath.prev
}
return pathArray
}
function getOperationDetails(responseContext) {
if (!responseContext.document) {
return null;
}
const definitions = responseContext.document.definitions;
const operation = definitions.find((definition) => definition.kind === 'OperationDefinition');
const pathAndArgs = getDeepestPathAndQueryArguments(operation);
let query = cleanQuery(responseContext.source, pathAndArgs.argLocations);
const deepestUniquePath = pathAndArgs.deepestPath;
const definitionName = getOperationName(operation);
const definitionType = getOperationType(operation);
return {
operationType: definitionType,
operationName: definitionName,
deepestUniquePath: deepestUniquePath,
cleanedQuery: query
}
}
const cleanQuery = (query, argLocations) => {
let cleanedQuery = query;
let offset = 0;
argLocations.forEach((loc) => {
cleanedQuery =
cleanedQuery.slice(0, loc.start - offset) +
OBFUSCATION_STR +
cleanedQuery.slice(loc.end - offset);
offset = loc.end - loc.start - OBFUSCATION_STR.length;
})
return cleanedQuery;
}
function getOperationType(definition) {
return definition.operation;
}
function getOperationName(definition) {
return (definition.name && definition.name.value) || 'Not Named';
}
function getDeepestPathAndQueryArguments(definition) {
let deepestPath = [];
let foundDeepestPath = false;
let argLocations = [];
definition.selectionSet.selections.forEach((selection) => {
foundDeepestPath = false;
searchSelection(selection, deepestPath);
})
return {
deepestPath,
argLocations
};
function searchSelection(selection, currentParts) {
const parts = currentParts ? [...currentParts] : [];
// capture the arguments for a selection
if (selection.arguments && selection.arguments.length > 0) {
selection.arguments.forEach((arg) => {
argLocations.push(arg.loc);
})
}
if (!foundDeepestPath) {
if (isNamedType(selection)) {
const lastItemIdx = parts.length - 1;
parts[lastItemIdx] = `${parts[lastItemIdx]}<${selection.typeCondition.name.value}>`;
} else {
selection.name &&
IGNORED_PATH_FIELDS.indexOf(selection.name.value) < 0 &&
parts.push(selection.name.value);
}
}
if (selection.selectionSet) {
const filtered = filterSelectionsForDeepestPath(selection.selectionSet.selections)
if (filtered.length === 0 || filtered.length > 1) {
foundDeepestPath = true;
deepestPath = parts;
}
filtered.forEach((innerSelection) => {
searchSelection(innerSelection, parts);
})
} else if (!deepestPath.length || parts.length > deepestPath.length) {
deepestPath = parts;
}
}
function filterSelectionsForDeepestPath(selections) {
return selections.filter((currentSelection) => {
if (currentSelection.kind === 'InlineFragment') {
return true;
}
return IGNORED_PATH_FIELDS.indexOf(currentSelection.name.value) < 0;
})
}
function isNamedType(selection) {
return (
selection.kind === 'InlineFragment' &&
selection.typeCondition &&
selection.typeCondition.kind === 'NamedType' &&
selection.typeCondition.name
);
}
}
module.exports = probePlugin;