@opentelemetry/instrumentation-graphql
Version:
OpenTelemetry instrumentation for `graphql` gql query language and runtime for GraphQL
319 lines • 11.9 kB
JavaScript
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.wrapFieldResolver = exports.wrapFields = exports.getSourceFromLocation = exports.getOperation = exports.endSpan = exports.addSpanSource = exports.addInputVariableAttributes = exports.isPromise = void 0;
const api = require("@opentelemetry/api");
const enum_1 = require("./enum");
const AttributeNames_1 = require("./enums/AttributeNames");
const symbols_1 = require("./symbols");
const OPERATION_VALUES = Object.values(enum_1.AllowedOperationTypes);
// https://github.com/graphql/graphql-js/blob/main/src/jsutils/isPromise.ts
const isPromise = (value) => {
return typeof value?.then === 'function';
};
exports.isPromise = isPromise;
// https://github.com/graphql/graphql-js/blob/main/src/jsutils/isObjectLike.ts
const isObjectLike = (value) => {
return typeof value == 'object' && value !== null;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function addInputVariableAttribute(span, key, variable) {
if (Array.isArray(variable)) {
variable.forEach((value, idx) => {
addInputVariableAttribute(span, `${key}.${idx}`, value);
});
}
else if (variable instanceof Object) {
Object.entries(variable).forEach(([nestedKey, value]) => {
addInputVariableAttribute(span, `${key}.${nestedKey}`, value);
});
}
else {
span.setAttribute(`${AttributeNames_1.AttributeNames.VARIABLES}${String(key)}`, variable);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function addInputVariableAttributes(span, variableValues) {
Object.entries(variableValues).forEach(([key, value]) => {
addInputVariableAttribute(span, key, value);
});
}
exports.addInputVariableAttributes = addInputVariableAttributes;
function addSpanSource(span, loc, allowValues, start, end) {
const source = getSourceFromLocation(loc, allowValues, start, end);
span.setAttribute(AttributeNames_1.AttributeNames.SOURCE, source);
}
exports.addSpanSource = addSpanSource;
function createFieldIfNotExists(tracer, getConfig, contextValue, info, path) {
let field = getField(contextValue, path);
let spanAdded = false;
if (!field) {
spanAdded = true;
const parent = getParentField(contextValue, path);
field = {
parent,
span: createResolverSpan(tracer, getConfig, contextValue, info, path, parent.span),
error: null,
};
addField(contextValue, path, field);
}
return { spanAdded, field };
}
function createResolverSpan(tracer, getConfig, contextValue, info, path, parentSpan) {
const attributes = {
[AttributeNames_1.AttributeNames.FIELD_NAME]: info.fieldName,
[AttributeNames_1.AttributeNames.FIELD_PATH]: path.join('.'),
[AttributeNames_1.AttributeNames.FIELD_TYPE]: info.returnType.toString(),
};
const span = tracer.startSpan(`${enum_1.SpanNames.RESOLVE} ${attributes[AttributeNames_1.AttributeNames.FIELD_PATH]}`, {
attributes,
}, parentSpan ? api.trace.setSpan(api.context.active(), parentSpan) : undefined);
const document = contextValue[symbols_1.OTEL_GRAPHQL_DATA_SYMBOL].source;
const fieldNode = info.fieldNodes.find(fieldNode => fieldNode.kind === 'Field');
if (fieldNode) {
addSpanSource(span, document.loc, getConfig().allowValues, fieldNode.loc?.start, fieldNode.loc?.end);
}
return span;
}
function endSpan(span, error) {
if (error) {
span.recordException(error);
}
span.end();
}
exports.endSpan = endSpan;
function getOperation(document, operationName) {
if (!document || !Array.isArray(document.definitions)) {
return undefined;
}
if (operationName) {
return document.definitions
.filter(definition => OPERATION_VALUES.indexOf(definition?.operation) !== -1)
.find(definition => operationName === definition?.name?.value);
}
else {
return document.definitions.find(definition => OPERATION_VALUES.indexOf(definition?.operation) !== -1);
}
}
exports.getOperation = getOperation;
function addField(contextValue, path, field) {
return (contextValue[symbols_1.OTEL_GRAPHQL_DATA_SYMBOL].fields[path.join('.')] =
field);
}
function getField(contextValue, path) {
return contextValue[symbols_1.OTEL_GRAPHQL_DATA_SYMBOL].fields[path.join('.')];
}
function getParentField(contextValue, path) {
for (let i = path.length - 1; i > 0; i--) {
const field = getField(contextValue, path.slice(0, i));
if (field) {
return field;
}
}
return {
span: contextValue[symbols_1.OTEL_GRAPHQL_DATA_SYMBOL].span,
};
}
function pathToArray(mergeItems, path) {
const flattened = [];
let curr = path;
while (curr) {
let key = curr.key;
if (mergeItems && typeof key === 'number') {
key = '*';
}
flattened.push(String(key));
curr = curr.prev;
}
return flattened.reverse();
}
function repeatBreak(i) {
return repeatChar('\n', i);
}
function repeatSpace(i) {
return repeatChar(' ', i);
}
function repeatChar(char, to) {
let text = '';
for (let i = 0; i < to; i++) {
text += char;
}
return text;
}
const KindsToBeRemoved = [
enum_1.TokenKind.FLOAT,
enum_1.TokenKind.STRING,
enum_1.TokenKind.INT,
enum_1.TokenKind.BLOCK_STRING,
];
function getSourceFromLocation(loc, allowValues = false, inputStart, inputEnd) {
let source = '';
if (loc?.startToken) {
const start = typeof inputStart === 'number' ? inputStart : loc.start;
const end = typeof inputEnd === 'number' ? inputEnd : loc.end;
let next = loc.startToken.next;
let previousLine = 1;
while (next) {
if (next.start < start) {
next = next.next;
previousLine = next?.line;
continue;
}
if (next.end > end) {
next = next.next;
previousLine = next?.line;
continue;
}
let value = next.value || next.kind;
let space = '';
if (!allowValues && KindsToBeRemoved.indexOf(next.kind) >= 0) {
// value = repeatChar('*', value.length);
value = '*';
}
if (next.kind === enum_1.TokenKind.STRING) {
value = `"${value}"`;
}
if (next.kind === enum_1.TokenKind.EOF) {
value = '';
}
if (next.line > previousLine) {
source += repeatBreak(next.line - previousLine);
previousLine = next.line;
space = repeatSpace(next.column - 1);
}
else {
if (next.line === next.prev?.line) {
space = repeatSpace(next.start - (next.prev?.end || 0));
}
}
source += space + value;
if (next) {
next = next.next;
}
}
}
return source;
}
exports.getSourceFromLocation = getSourceFromLocation;
function wrapFields(type, tracer, getConfig) {
if (!type ||
typeof type.getFields !== 'function' ||
type[symbols_1.OTEL_PATCHED_SYMBOL]) {
return;
}
const fields = type.getFields();
type[symbols_1.OTEL_PATCHED_SYMBOL] = true;
Object.keys(fields).forEach(key => {
const field = fields[key];
if (!field) {
return;
}
if (field.resolve) {
field.resolve = wrapFieldResolver(tracer, getConfig, field.resolve);
}
if (field.type) {
let unwrappedType = field.type;
while (unwrappedType.ofType) {
unwrappedType = unwrappedType.ofType;
}
wrapFields(unwrappedType, tracer, getConfig);
}
});
}
exports.wrapFields = wrapFields;
const handleResolveSpanError = (resolveSpan, err, shouldEndSpan) => {
if (!shouldEndSpan) {
return;
}
resolveSpan.recordException(err);
resolveSpan.setStatus({
code: api.SpanStatusCode.ERROR,
message: err.message,
});
resolveSpan.end();
};
const handleResolveSpanSuccess = (resolveSpan, shouldEndSpan) => {
if (!shouldEndSpan) {
return;
}
resolveSpan.end();
};
function wrapFieldResolver(tracer, getConfig, fieldResolver, isDefaultResolver = false) {
if (wrappedFieldResolver[symbols_1.OTEL_PATCHED_SYMBOL] ||
typeof fieldResolver !== 'function') {
return fieldResolver;
}
function wrappedFieldResolver(source, args, contextValue, info) {
if (!fieldResolver) {
return undefined;
}
const config = getConfig();
// follows what graphql is doing to decide if this is a trivial resolver
// for which we don't need to create a resolve span
if (config.ignoreTrivialResolveSpans &&
isDefaultResolver &&
(isObjectLike(source) || typeof source === 'function')) {
const property = source[info.fieldName];
// a function execution is not trivial and should be recorder.
// property which is not a function is just a value and we don't want a "resolve" span for it
if (typeof property !== 'function') {
return fieldResolver.call(this, source, args, contextValue, info);
}
}
if (!contextValue[symbols_1.OTEL_GRAPHQL_DATA_SYMBOL]) {
return fieldResolver.call(this, source, args, contextValue, info);
}
const path = pathToArray(config.mergeItems, info && info.path);
const depth = path.filter((item) => typeof item === 'string').length;
let field;
let shouldEndSpan = false;
if (config.depth >= 0 && config.depth < depth) {
field = getParentField(contextValue, path);
}
else {
const newField = createFieldIfNotExists(tracer, getConfig, contextValue, info, path);
field = newField.field;
shouldEndSpan = newField.spanAdded;
}
return api.context.with(api.trace.setSpan(api.context.active(), field.span), () => {
try {
const res = fieldResolver.call(this, source, args, contextValue, info);
if ((0, exports.isPromise)(res)) {
return res.then((r) => {
handleResolveSpanSuccess(field.span, shouldEndSpan);
return r;
}, (err) => {
handleResolveSpanError(field.span, err, shouldEndSpan);
throw err;
});
}
else {
handleResolveSpanSuccess(field.span, shouldEndSpan);
return res;
}
}
catch (err) {
handleResolveSpanError(field.span, err, shouldEndSpan);
throw err;
}
});
}
wrappedFieldResolver[symbols_1.OTEL_PATCHED_SYMBOL] = true;
return wrappedFieldResolver;
}
exports.wrapFieldResolver = wrapFieldResolver;
//# sourceMappingURL=utils.js.map
;