@sentry/node
Version:
Sentry Node SDK using OpenTelemetry for performance instrumentation
328 lines (325 loc) • 11.4 kB
JavaScript
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