UNPKG

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
/** * 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. */ 'use strict' 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;