UNPKG

@uphold/opentelemetry-instrumentation-connect-node

Version:

OpenTelemetry instrumentation for `@connectrpc/connect-node` RPC client and server

181 lines 8.52 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createServerInterceptor = exports.createClientInterceptor = void 0; const semcov_1 = require("./semcov"); const incubating_1 = require("@opentelemetry/semantic-conventions/incubating"); const semantic_conventions_1 = require("@opentelemetry/semantic-conventions"); 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 (metadata) => { const attributes = {}; if (metadata) { for (const [key, mappedKey] of mappings) { const value = metadata.get(key); if (value != null) { const attributeKey = phase === 'request' ? (0, incubating_1.ATTR_RPC_REQUEST_METADATA)(mappedKey) : (0, incubating_1.ATTR_RPC_RESPONSE_METADATA)(mappedKey); attributes[attributeKey] = 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 rpcSystemName = (0, utils_1.resolveRpcSystemName)(req.header); const span = tracer.startSpan(fullName, { attributes: { [incubating_1.ATTR_RPC_METHOD]: req.method.name, [semcov_1.ATTR_RPC_SERVICE]: req.service.typeName, [semcov_1.ATTR_RPC_SYSTEM]: rpcSystem, [incubating_1.ATTR_RPC_SYSTEM_NAME]: rpcSystemName, [semantic_conventions_1.ATTR_SERVER_ADDRESS]: url.hostname, [semantic_conventions_1.ATTR_SERVER_PORT]: url.port || undefined }, kind: (0, utils_1.rpcKindToSpanKind)(kind) }); span.setAttributes(extractMetadata(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 rpcSystemName = span.attributes?.[incubating_1.ATTR_RPC_SYSTEM_NAME]; span.setStatus({ code: api_1.SpanStatusCode.OK }); if (rpcSystemName === 'grpc') { span.setAttribute(incubating_1.ATTR_RPC_RESPONSE_STATUS_CODE, 0); span.setAttribute(semcov_1.ATTR_RPC_GRPC_STATUS_CODE, 0); } else if (rpcSystemName === 'connectrpc') { span.setAttribute(incubating_1.ATTR_RPC_RESPONSE_STATUS_CODE, 'ok'); span.setAttribute(semcov_1.ATTR_RPC_CONNECT_RPC_ERROR_CODE, 'ok'); } span.setAttributes(extractMetadata(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 rpcSystemName = span.attributes?.[incubating_1.ATTR_RPC_SYSTEM_NAME]; span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error?.message }); if (rpcSystemName === 'grpc') { const statusCode = connectError?.code ?? 2; span.setAttribute(incubating_1.ATTR_RPC_RESPONSE_STATUS_CODE, statusCode); span.setAttribute(semcov_1.ATTR_RPC_GRPC_STATUS_CODE, statusCode); } else if (rpcSystemName === 'connectrpc') { const statusCode = (0, utils_1.errorCodeToString)(connectError?.code); span.setAttribute(incubating_1.ATTR_RPC_RESPONSE_STATUS_CODE, statusCode); span.setAttribute(semcov_1.ATTR_RPC_CONNECT_RPC_ERROR_CODE, statusCode); } span.setAttributes(extractMetadata(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