@uphold/opentelemetry-instrumentation-connect-node
Version:
OpenTelemetry instrumentation for `@connectrpc/connect-node` RPC client and server
166 lines • 7.58 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createServerInterceptor = exports.createClientInterceptor = void 0;
const incubating_1 = require("@opentelemetry/semantic-conventions/incubating");
const api_1 = require("@opentelemetry/api");
const utils_1 = require("./utils");
const lodash_1 = require("lodash");
const instrumentation_1 = require("@opentelemetry/instrumentation");
const createMetadataAttributesExtractor = (config, kind, phase) => {
const metadataToSpanAttributes = config.metadataToSpanAttributes?.[kind]?.[phase] ?? [];
const mappings = new Map(metadataToSpanAttributes.map(value => [value.toLowerCase(), value.toLowerCase().replace(/-/g, '_')]));
if (mappings.size === 0) {
return () => ({});
}
// See: https://opentelemetry.io/docs/specs/semconv/rpc/
return (span, metadata) => {
const rpcSystem = span.attributes[incubating_1.ATTR_RPC_SYSTEM];
const attributes = {};
if (rpcSystem && metadata) {
for (const [key, mappedKey] of mappings) {
const value = metadata.get(key);
if (value != null) {
attributes[`grpc.${rpcSystem}.${phase}.metadata.${mappedKey}`] = value;
}
}
}
return attributes;
};
};
const createStartSpan = (config, tracer, kind) => {
const extractMetadata = createMetadataAttributesExtractor(config, kind, 'request');
const parseUrl = (0, lodash_1.memoize)((url) => new URL(url));
// See: https://opentelemetry.io/docs/specs/semconv/rpc/
return (req) => {
const fullName = `${req.service.typeName}/${req.method.name}`;
const url = parseUrl(req.url);
const rpcSystem = (0, utils_1.resolveRpcSystem)(req.header);
const span = tracer.startSpan(fullName, {
attributes: {
[incubating_1.ATTR_RPC_METHOD]: req.method.name,
[incubating_1.ATTR_RPC_SERVICE]: req.service.typeName,
[incubating_1.ATTR_RPC_SYSTEM]: rpcSystem,
[incubating_1.ATTR_SERVER_ADDRESS]: url.hostname,
[incubating_1.ATTR_SERVER_PORT]: url.port || undefined
},
kind: (0, utils_1.rpcKindToSpanKind)(kind)
});
span.setAttributes(extractMetadata(span, req.header));
return span;
};
};
const createEndSpanWithSuccess = (config, kind) => {
const extractMetadata = createMetadataAttributesExtractor(config, kind, 'response');
// See: https://opentelemetry.io/docs/specs/semconv/rpc/
return (span, res) => {
if (span.ended) {
return;
}
const rpcSystem = span.attributes[incubating_1.ATTR_RPC_SYSTEM];
span.setStatus({ code: api_1.SpanStatusCode.OK });
if (rpcSystem === 'grpc') {
span.setAttribute(incubating_1.ATTR_RPC_GRPC_STATUS_CODE, 0);
}
span.setAttributes(extractMetadata(span, res.header));
span.end();
};
};
const createEndSpanWithError = (config, kind) => {
const extractMetadata = createMetadataAttributesExtractor(config, kind, 'response');
// See: https://opentelemetry.io/docs/specs/semconv/rpc/
return (span, err) => {
if (span.ended) {
return;
}
const error = err instanceof Error ? err : undefined;
const connectError = (0, utils_1.isConnectError)(error) ? error : undefined;
const rpcSystem = span.attributes[incubating_1.ATTR_RPC_SYSTEM];
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error?.message });
if (rpcSystem === 'grpc') {
span.setAttribute(incubating_1.ATTR_RPC_GRPC_STATUS_CODE, connectError?.code ?? 2);
}
else if (rpcSystem === 'connect_rpc') {
span.setAttribute(incubating_1.ATTR_RPC_CONNECT_RPC_ERROR_CODE, (0, utils_1.errorCodeToString)(connectError?.code));
}
span.setAttributes(extractMetadata(span, connectError?.metadata));
if (error) {
span.recordException(error);
}
span.end();
};
};
const carrierSetterAndGetter = {
get: (carrier, key) => carrier.get(key) ?? undefined,
keys: (carrier) => Array.from(carrier.keys()),
set: (carrier, key, value) => carrier.set(key, value)
};
const createClientInterceptor = (config, diag, tracer) => {
const startSpan = createStartSpan(config, tracer, 'client');
const endSpanWithSuccess = createEndSpanWithSuccess(config, 'client');
const endSpanWithError = createEndSpanWithError(config, 'client');
return (next) => async (req) => {
// Only unary requests are supported due to a bug in Node.js async context propagation on generator functions.
// See https://github.com/open-telemetry/opentelemetry-js/issues/2951 and https://github.com/nodejs/node/issues/42237
if (req.method.methodKind !== 'unary') {
return await next(req);
}
const shouldIgnoreRequest = (0, instrumentation_1.safeExecuteInTheMiddle)(() => config.ignoreRequest?.(req) === true, (err) => {
if (err != null) {
diag.error('caught ignoreRequest error: ', err);
}
}, true);
if (shouldIgnoreRequest) {
return await next(req);
}
const span = startSpan(req);
try {
return await api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), async () => {
api_1.propagation.inject(api_1.context.active(), req.header, carrierSetterAndGetter);
const res = await next(req);
endSpanWithSuccess(span, res);
return res;
});
}
catch (err) {
endSpanWithError(span, err);
throw err;
}
};
};
exports.createClientInterceptor = createClientInterceptor;
const createServerInterceptor = (config, diag, tracer) => {
const startSpan = createStartSpan(config, tracer, 'server');
const endSpanWithSuccess = createEndSpanWithSuccess(config, 'server');
const endSpanWithError = createEndSpanWithError(config, 'server');
return (next) => async (req) => {
// Only unary requests are supported due to a bug in Node.js async context propagation on generator functions.
// See https://github.com/open-telemetry/opentelemetry-js/issues/2951 and https://github.com/nodejs/node/issues/42237
if (req.method.methodKind !== 'unary') {
return await next(req);
}
const shouldIgnoreRequest = (0, instrumentation_1.safeExecuteInTheMiddle)(() => config.ignoreRequest?.(req) === true, (err) => {
if (err != null) {
diag.error('caught ignoreRequest error: ', err);
}
}, true);
if (shouldIgnoreRequest) {
return await next(req);
}
return await api_1.context.with(api_1.propagation.extract(api_1.context.active(), req.header, carrierSetterAndGetter), async () => {
const span = startSpan(req);
try {
return await api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), async () => {
const res = await next(req);
endSpanWithSuccess(span, res);
return res;
});
}
catch (err) {
endSpanWithError(span, err);
throw err;
}
});
};
};
exports.createServerInterceptor = createServerInterceptor;
//# sourceMappingURL=interceptor.js.map