UNPKG

@sentry/node

Version:

Sentry Node SDK using OpenTelemetry for performance instrumentation

328 lines (325 loc) 11.4 kB
import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped, safeExecuteInTheMiddle } from '@opentelemetry/instrumentation'; import { InstrumentationNodeModuleFile } from '../../InstrumentationNodeModuleFile.js'; import { SpanNames } from './enum.js'; import { AttributeNames } from './enums/AttributeNames.js'; import { OTEL_GRAPHQL_DATA_SYMBOL } from './symbols.js'; import { OPERATION_NOT_SUPPORTED } from './internal-types.js'; import { getOperation, endSpan, isPromise, addSpanSource, wrapFieldResolver, wrapFields } from './utils.js'; import { SDK_VERSION, withActiveSpan, spanToJSON, SPAN_STATUS_ERROR, getRootSpan, startInactiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION } from '@sentry/opentelemetry'; const PACKAGE_NAME = "@sentry/instrumentation-graphql"; const ORIGIN = "auto.graphql.otel.graphql"; const DEFAULT_CONFIG = { ignoreResolveSpans: false }; const supportedVersions = [">=14.0.0 <17"]; class GraphQLInstrumentation extends InstrumentationBase { constructor(config = {}) { super(PACKAGE_NAME, SDK_VERSION, { ...DEFAULT_CONFIG, ...config }); } setConfig(config = {}) { super.setConfig({ ...DEFAULT_CONFIG, ...config }); } init() { const module = new InstrumentationNodeModuleDefinition("graphql", supportedVersions); module.files.push(this._addPatchingExecute()); module.files.push(this._addPatchingParser()); module.files.push(this._addPatchingValidate()); return module; } _addPatchingExecute() { return new InstrumentationNodeModuleFile( "graphql/execution/execute.js", supportedVersions, // cannot make it work with appropriate type as execute function has 2 //types and/cannot import function but only types (moduleExports) => { if (isWrapped(moduleExports.execute)) { this._unwrap(moduleExports, "execute"); } this._wrap(moduleExports, "execute", this._patchExecute(moduleExports.defaultFieldResolver)); return moduleExports; }, (moduleExports) => { if (moduleExports) { this._unwrap(moduleExports, "execute"); } } ); } _addPatchingParser() { return new InstrumentationNodeModuleFile( "graphql/language/parser.js", supportedVersions, (moduleExports) => { if (isWrapped(moduleExports.parse)) { this._unwrap(moduleExports, "parse"); } this._wrap(moduleExports, "parse", this._patchParse()); return moduleExports; }, (moduleExports) => { if (moduleExports) { this._unwrap(moduleExports, "parse"); } } ); } _addPatchingValidate() { return new InstrumentationNodeModuleFile( "graphql/validation/validate.js", supportedVersions, (moduleExports) => { if (isWrapped(moduleExports.validate)) { this._unwrap(moduleExports, "validate"); } this._wrap(moduleExports, "validate", this._patchValidate()); return moduleExports; }, (moduleExports) => { if (moduleExports) { this._unwrap(moduleExports, "validate"); } } ); } _patchExecute(defaultFieldResolved) { const instrumentation = this; return function execute(original) { return function patchExecute() { let processedArgs; if (arguments.length >= 2) { const args = arguments; processedArgs = instrumentation._wrapExecuteArgs( args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], defaultFieldResolved ); } else { const args = arguments[0]; processedArgs = instrumentation._wrapExecuteArgs( args.schema, args.document, args.rootValue, args.contextValue, args.variableValues, args.operationName, args.fieldResolver, args.typeResolver, defaultFieldResolved ); } const operation = getOperation(processedArgs.document, processedArgs.operationName); const span = instrumentation._createExecuteSpan(operation, processedArgs); processedArgs.contextValue[OTEL_GRAPHQL_DATA_SYMBOL] = { source: processedArgs.document ? processedArgs.document || processedArgs.document[OTEL_GRAPHQL_DATA_SYMBOL] : void 0, span, fields: {} }; return withActiveSpan(span, () => { return safeExecuteInTheMiddle( () => { return original.apply(this, [processedArgs]); }, (err, result) => { instrumentation._handleExecutionResult(span, err, result); } ); }); }; }; } _handleExecutionResult(span, err, result) { if (result === void 0 || err) { endSpan(span, err); return; } if (isPromise(result)) { result.then( (resultData) => { this._updateSpanFromResult(span, resultData); endSpan(span); }, (error) => { endSpan(span, error); } ); } else { this._updateSpanFromResult(span, result); endSpan(span); } } /** * Applies Sentry-specific span mutations based on the GraphQL execution result: * - Marks the execute span as errored if the result contains errors (and no status was set yet) * - Optionally renames the containing root span to include the GraphQL operation name(s) */ _updateSpanFromResult(span, result) { if (result.errors?.length && !spanToJSON(span).status) { span.setStatus({ code: SPAN_STATUS_ERROR }); } if (!this.getConfig().useOperationNameForRootSpan) { return; } const attributes = spanToJSON(span).data; const operationType = attributes[AttributeNames.OPERATION_TYPE]; const operationName = attributes[AttributeNames.OPERATION_NAME]; if (!operationType) { return; } const rootSpan = getRootSpan(span); const rootSpanAttributes = spanToJSON(rootSpan).data; const existingOperations = rootSpanAttributes[SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION] || []; const newOperation = operationName ? `${operationType} ${operationName}` : `${operationType}`; if (Array.isArray(existingOperations)) { existingOperations.push(newOperation); rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION, existingOperations); } else if (typeof existingOperations === "string") { rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION, [existingOperations, newOperation]); } else { rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION, newOperation); } if (!spanToJSON(rootSpan).data["original-description"]) { rootSpan.setAttribute("original-description", spanToJSON(rootSpan).description); } rootSpan.updateName( `${spanToJSON(rootSpan).data["original-description"]} (${getGraphqlOperationNamesFromAttribute( existingOperations )})` ); } _patchParse() { const instrumentation = this; return function parse(original) { return function patchParse(source, options) { return instrumentation._parse(this, original, source, options); }; }; } _patchValidate() { const instrumentation = this; return function validate(original) { return function patchValidate(schema, documentAST, rules, options, typeInfo) { return instrumentation._validate(this, original, schema, documentAST, rules, typeInfo, options); }; }; } _parse(obj, original, source, options) { const span = startInactiveSpan({ name: SpanNames.PARSE }); return withActiveSpan(span, () => { return safeExecuteInTheMiddle( () => { return original.call(obj, source, options); }, (err, result) => { if (result) { const operation = getOperation(result); if (!operation) { span.updateName(SpanNames.SCHEMA_PARSE); } else if (result.loc) { addSpanSource(span, result.loc); } } endSpan(span, err); } ); }); } _validate(obj, original, schema, documentAST, rules, typeInfo, options) { const span = startInactiveSpan({ name: SpanNames.VALIDATE }); return withActiveSpan(span, () => { return safeExecuteInTheMiddle( () => { return original.call(obj, schema, documentAST, rules, options, typeInfo); }, (err, _errors) => { if (!documentAST.loc) { span.updateName(SpanNames.SCHEMA_VALIDATE); } endSpan(span, err); } ); }); } _createExecuteSpan(operation, processedArgs) { const span = startInactiveSpan({ name: SpanNames.EXECUTE, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN } }); if (operation) { const { operation: operationType, name: nameNode } = operation; span.setAttribute(AttributeNames.OPERATION_TYPE, operationType); const operationName = nameNode?.value; if (operationName) { span.setAttribute(AttributeNames.OPERATION_NAME, operationName); span.updateName(`${operationType} ${operationName}`); } else { span.updateName(operationType); } } else { let operationName = " "; if (processedArgs.operationName) { operationName = ` "${processedArgs.operationName}" `; } operationName = OPERATION_NOT_SUPPORTED.replace("$operationName$", operationName); span.setAttribute(AttributeNames.OPERATION_NAME, operationName); } if (processedArgs.document?.loc) { addSpanSource(span, processedArgs.document.loc); } return span; } _wrapExecuteArgs(schema, document, rootValue, contextValue, variableValues, operationName, fieldResolver, typeResolver, defaultFieldResolved) { if (!contextValue) { contextValue = {}; } if (contextValue[OTEL_GRAPHQL_DATA_SYMBOL] || this.getConfig().ignoreResolveSpans) { return { schema, document, rootValue, contextValue, variableValues, operationName, fieldResolver, typeResolver }; } const isUsingDefaultResolver = fieldResolver == null; const fieldResolverForExecute = fieldResolver ?? defaultFieldResolved; fieldResolver = wrapFieldResolver(() => this.getConfig(), fieldResolverForExecute, isUsingDefaultResolver); if (schema) { wrapFields(schema.getQueryType(), () => this.getConfig()); wrapFields(schema.getMutationType(), () => this.getConfig()); } return { schema, document, rootValue, contextValue, variableValues, operationName, fieldResolver, typeResolver }; } } function getGraphqlOperationNamesFromAttribute(attr) { if (Array.isArray(attr)) { const sorted = attr.slice().sort(); if (sorted.length <= 5) { return sorted.join(", "); } else { return `${sorted.slice(0, 5).join(", ")}, +${sorted.length - 5}`; } } return `${attr}`; } export { GraphQLInstrumentation }; //# sourceMappingURL=instrumentation.js.map