@instana/core
Version:
Core library for Instana's Node.js packages
578 lines (513 loc) • 20.6 kB
JavaScript
/*
* (c) Copyright IBM Corp. 2026
*/
'use strict';
const {
toUpperCase,
firstDefined,
formatOTLPValue,
combineFields,
extractHost,
extractPort,
getRPCMethod,
getRPCMethodOriginal,
toBoolean,
getRegionFromArn,
getAccountIdFromArn
} = require('./util');
const ctx = require('../../common/context');
const {
INSTRUMENTATION_TYPES,
OTLP_STATUS_CODES,
SPECIAL_SPAN_DATA_TYPES,
LAMBDA_TRIGGER_MAP
} = require('./constants');
const OTLP = /** @type {any} */ (ctx.semConv);
/**
* @typedef {Object} OTLPFormattedValue
* @property {string} [stringValue]
* @property {number} [intValue]
* @property {number} [doubleValue]
* @property {boolean} [boolValue]
*/
/**
* @typedef {Object} SpanAttribute
* @property {string} key
* @property {OTLPFormattedValue} value
*/
/**
* @typedef {(values: any, spanData?: Record<string, any>) => any} TransformFunction
*/
/**
* @typedef {Object} AttributeMapping
* @property {string} otlp
* @property {string | string[]} [instana]
* @property {any} [value]
* @property {TransformFunction} [transform]
*/
/**
* @typedef {(data: Record<string, any>) => string} SpanNameFunction
*/
/**
* @typedef {Object} InstrumentationMapping
* @property {SpanNameFunction} [spanName]
* @property {AttributeMapping[]} [spanAttributes]
*/
/**
* @typedef {Record<string, InstrumentationMapping>} InstrumentationMappings
*/
/** @type {InstrumentationMappings} */
const instrumentationMappings = {
[INSTRUMENTATION_TYPES.HTTP]: {
spanName: data => {
const method = data.operation.toUpperCase();
return `${method} ${data.path_tpl || data.path || '/'}`;
},
spanAttributes: [
{ otlp: OTLP.http.REQUEST_METHOD, instana: 'operation', transform: toUpperCase },
{ otlp: OTLP.http.URL_FULL, instana: 'endpoints' },
{ otlp: OTLP.http.URL_PATH, instana: 'path' },
{ otlp: OTLP.http.URL_QUERY, instana: 'params' },
{ otlp: OTLP.http.RESPONSE_STATUS, instana: 'status' },
// TODO: Instana stores both request and response headers in the same `header` field.
// We need an internal mechanism to distinguish between request and response headers.
{ otlp: OTLP.http.REQUEST_HEADER, instana: 'header' },
{ otlp: OTLP.http.URL_TEMPLATE, instana: 'path_tpl' },
{ otlp: OTLP.http.ROUTE, instana: 'route' },
{ otlp: OTLP.server.ADDRESS, instana: 'connection', transform: extractHost },
{ otlp: OTLP.server.PORT, instana: 'connection', transform: extractPort },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.KAFKA]: {
spanName: data => `${data.operation} ${data.endpoints}`,
spanAttributes: [
{ otlp: OTLP.messaging.SYSTEM, value: INSTRUMENTATION_TYPES.KAFKA },
{ otlp: OTLP.messaging.DESTINATION_NAME, instana: 'endpoints' },
{ otlp: OTLP.messaging.OPERATION_NAME, instana: 'operation' },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.RABBITMQ]: {
spanName: data => `${data.sort} ${data.exchange}`,
spanAttributes: [
{ otlp: OTLP.messaging.SYSTEM, value: INSTRUMENTATION_TYPES.RABBITMQ },
{ otlp: OTLP.messaging.OPERATION_NAME, instana: 'sort' },
{ otlp: OTLP.messaging.DESTINATION_NAME, instana: ['exchange', 'key'] },
{ otlp: OTLP.messaging.rabbitmq.ROUTING_KEY, instana: 'key' },
{ otlp: OTLP.messaging.MESSAGE_BODY_SIZE, instana: 'size' },
{ otlp: OTLP.server.ADDRESS, instana: 'address', transform: extractHost },
{ otlp: OTLP.server.PORT, instana: 'address', transform: extractPort },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.NATS]: {
spanName: data => `${data.sort} ${data.subject}`,
spanAttributes: [
{ otlp: OTLP.messaging.SYSTEM, value: INSTRUMENTATION_TYPES.NATS },
{ otlp: OTLP.messaging.DESTINATION_NAME, instana: 'subject' },
{ otlp: OTLP.messaging.OPERATION_NAME, instana: 'sort' },
{ otlp: OTLP.server.ADDRESS, instana: 'address', transform: extractHost },
{ otlp: OTLP.server.PORT, instana: 'address', transform: extractPort },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.BULL]: {
spanName: data => `${data.sort} ${data.queue}`,
spanAttributes: [
{ otlp: OTLP.messaging.SYSTEM, value: INSTRUMENTATION_TYPES.BULL },
{ otlp: OTLP.messaging.OPERATION_NAME, instana: 'sort' },
{ otlp: OTLP.messaging.DESTINATION_NAME, instana: 'queue' },
{ otlp: OTLP.messaging.MESSAGE_ID, instana: 'messageId' },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.SQS]: {
spanName: data => `${data.sort} ${data.queue}`,
spanAttributes: [
{ otlp: OTLP.messaging.SYSTEM, value: 'aws_sqs' },
{ otlp: OTLP.messaging.OPERATION_NAME, instana: 'type' },
{ otlp: OTLP.messaging.DESTINATION_NAME, instana: 'queue' },
{ otlp: OTLP.messaging.BATCH_MESSAGE_COUNT, instana: 'size' },
{ otlp: OTLP.messaging.MESSAGE_ID, instana: 'messageId' },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.SNS]: {
spanName: data => `publish ${data.topic}`,
spanAttributes: [
{ otlp: OTLP.messaging.SYSTEM, value: 'aws.sns' },
{ otlp: OTLP.messaging.DESTINATION_NAME, instana: 'topic' },
// We don't have the actual operation name. Using "send" based on the RFD.
{ otlp: OTLP.messaging.OPERATION_NAME, value: 'send' },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.GCPS]: {
spanName: data => `${data.op}`,
spanAttributes: [
{ otlp: OTLP.messaging.SYSTEM, value: 'gcp_pubsub' },
{ otlp: OTLP.messaging.OPERATION_NAME, instana: 'op' },
{
otlp: OTLP.messaging.DESTINATION_NAME,
instana: ['top', 'sub'],
transform: firstDefined
},
{ otlp: OTLP.cloud.gcp.PROJECT_ID, instana: 'projid' },
{ otlp: OTLP.messaging.MESSAGE_ID, instana: 'messageId' },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.PG]: {
spanName: data => `pg.${data.stmt?.split(/\s+/)[0]?.toUpperCase()}`,
spanAttributes: [
{ otlp: OTLP.database.SYSTEM_NAME, value: 'postgresql' },
{ otlp: OTLP.database.QUERY_TEXT, instana: 'stmt' },
{ otlp: OTLP.database.USER, instana: 'user' },
{ otlp: OTLP.database.NAMESPACE, instana: 'db' },
{ otlp: OTLP.server.ADDRESS, instana: 'host' },
{ otlp: OTLP.server.PORT, instana: 'port' },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.MYSQL]: {
spanName: data => `mysql.${data.stmt?.split(/\s+/)[0]?.toUpperCase()}`,
spanAttributes: [
{ otlp: OTLP.database.SYSTEM_NAME, value: INSTRUMENTATION_TYPES.MYSQL },
{ otlp: OTLP.database.QUERY_TEXT, instana: 'stmt' },
{ otlp: OTLP.database.USER, instana: 'user' },
{ otlp: OTLP.database.NAMESPACE, instana: 'db' },
{ otlp: OTLP.server.ADDRESS, instana: 'host' },
{ otlp: OTLP.server.PORT, instana: 'port' },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.MSSQL]: {
spanName: data => `mssql.${data.stmt?.split(/\s+/)[0]?.toUpperCase()}`,
spanAttributes: [
{ otlp: OTLP.database.SYSTEM_NAME, value: INSTRUMENTATION_TYPES.MSSQL },
{ otlp: OTLP.database.QUERY_TEXT, instana: 'stmt' },
{ otlp: OTLP.database.USER, instana: 'user' },
{ otlp: OTLP.database.NAMESPACE, instana: 'db' },
{ otlp: OTLP.server.ADDRESS, instana: 'host' },
{ otlp: OTLP.server.PORT, instana: 'port' },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.DB2]: {
spanName: data => `db2.${data.stmt?.split(/\s+/)[0]?.toUpperCase()}`,
spanAttributes: [
{ otlp: OTLP.database.SYSTEM_NAME, value: 'ibm.db2' },
{ otlp: OTLP.database.QUERY_TEXT, instana: 'stmt' },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.MONGO]: {
spanName: data => `mongodb.${data.command}`,
spanAttributes: [
{ otlp: OTLP.database.SYSTEM_NAME, value: 'mongodb' },
{ otlp: OTLP.database.NAMESPACE, instana: 'namespace' },
{ otlp: OTLP.database.mongodb.COLLECTION, instana: 'collection' },
{ otlp: OTLP.database.OPERATION_NAME, instana: 'command', transform: toUpperCase },
{ otlp: OTLP.database.QUERY_TEXT, instana: ['json', 'filter'], transform: firstDefined },
{ otlp: OTLP.server.ADDRESS, instana: 'service', transform: extractHost },
{ otlp: OTLP.server.PORT, instana: 'service', transform: extractPort },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.REDIS]: {
spanName: data => `redis.${data.operation}`,
spanAttributes: [
{ otlp: OTLP.database.SYSTEM_NAME, value: INSTRUMENTATION_TYPES.REDIS },
{ otlp: OTLP.database.OPERATION_NAME, instana: 'operation' },
{ otlp: OTLP.database.CONNECTION_STRING, instana: 'connection' },
{ otlp: OTLP.server.ADDRESS, instana: 'connection', transform: extractHost },
{ otlp: OTLP.server.PORT, instana: 'connection', transform: extractPort },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.COUCHBASE]: {
spanName: data => `couchbase.${data.stmt?.split(/\s+/)[0]?.toUpperCase()}`,
spanAttributes: [
{ otlp: OTLP.database.SYSTEM_NAME, value: [INSTRUMENTATION_TYPES.COUCHBASE] },
{ otlp: OTLP.database.NAMESPACE, instana: 'bucket' },
{ otlp: OTLP.database.QUERY_TEXT, instana: 'sql' },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.ELASTICSEARCH]: {
spanName: data => `elasticsearch.${data.action}`,
spanAttributes: [
{ otlp: OTLP.database.SYSTEM_NAME, value: INSTRUMENTATION_TYPES.ELASTICSEARCH },
{ otlp: OTLP.database.OPERATION_NAME, instana: 'action' },
{ otlp: OTLP.database.NAMESPACE, instana: 'cluster' },
{ otlp: OTLP.database.COLLECTION_NAME, instana: 'index' },
{ otlp: OTLP.database.QUERY_TEXT, instana: 'query' },
{ otlp: OTLP.database.CONNECTION_STRING, instana: 'hostname' },
{ otlp: OTLP.server.ADDRESS, instana: 'hostname', transform: extractHost },
{ otlp: OTLP.server.PORT, instana: 'hostname', transform: extractPort },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.DYNAMODB]: {
spanName: data => `dynamodb.${data.operation}`,
spanAttributes: [
{ otlp: OTLP.database.SYSTEM_NAME, value: 'aws.dynamodb' },
{ otlp: OTLP.rpc.SYSTEM_NAME, value: 'aws-api' },
{ otlp: OTLP.database.OPERATION_NAME, instana: 'operation' },
{ otlp: OTLP.cloud.REGION, instana: 'region' },
{ otlp: OTLP.database.dynamodb.COLLECTION_NAME, instana: 'table' },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.MEMCACHED]: {
spanName: data => `memcached.${data.operation}`,
spanAttributes: [
{ otlp: OTLP.database.SYSTEM_NAME, value: INSTRUMENTATION_TYPES.MEMCACHED },
{ otlp: OTLP.database.CONNECTION_STRING, instana: 'connection' },
{ otlp: OTLP.database.OPERATION_NAME, instana: 'operation' },
{ otlp: OTLP.server.ADDRESS, instana: 'connection', transform: extractHost },
{ otlp: OTLP.server.PORT, instana: 'connection', transform: extractPort },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
// Note: There is no official OpenTelemetry semantic convention for Prisma, and it is not covered in our RFD either.
// We can therefore adopt a generic database convention, using the Prisma provider as the database system identifier.
[INSTRUMENTATION_TYPES.PRISMA]: {
spanName: data => `prisma.${data.action}`,
spanAttributes: [
{ otlp: OTLP.database.SYSTEM_NAME, instana: 'provider' },
{ otlp: OTLP.database.COLLECTION_NAME, instana: 'model' },
{ otlp: OTLP.database.OPERATION_NAME, instana: 'action' },
{ otlp: OTLP.database.CONNECTION_STRING, instana: 'url' },
{ otlp: OTLP.server.ADDRESS, instana: 'url', transform: extractHost },
{ otlp: OTLP.server.PORT, instana: 'url', transform: extractPort },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.RPC]: {
spanName: data => data.call,
spanAttributes: [
{
otlp: OTLP.rpc.METHOD,
instana: 'call',
transform: (value, spanData) => getRPCMethod(value, spanData)
},
{
otlp: OTLP.rpc.METHOD_ORIGINAL,
instana: 'call',
transform: (value, spanData) => getRPCMethodOriginal(value, spanData)
},
{ otlp: OTLP.rpc.SYSTEM_NAME, instana: 'flavor' },
{ otlp: OTLP.server.ADDRESS, instana: 'host' },
{ otlp: OTLP.server.PORT, instana: 'port' },
{ otlp: OTLP.rpc.GRPC_ERROR, instana: 'error' }
]
},
// Note: span attributes follow the OTel GraphQL semantic conventions:
// https://opentelemetry.io/docs/specs/semconv/graphql/graphql-spans/
[INSTRUMENTATION_TYPES.GRAPHQL]: {
spanName: data => data.operationType,
spanAttributes: [
{ otlp: OTLP.graphql.OPERATION_TYPE, instana: 'operationType' },
{ otlp: OTLP.graphql.OPERATION_NAME, instana: 'operationName' },
{ otlp: OTLP.error.TYPE, instana: 'errors' }
]
},
// Note: Google Cloud Storage (GCS) is not included in the official OpenTelemetry semantic conventions
// specification. This mapping uses standard cloud.* and database.* attributes to represent GCS operations.
[INSTRUMENTATION_TYPES.GCS]: {
spanName: data => `gcs.${data.op}`,
spanAttributes: [
{ otlp: OTLP.cloud.PROVIDER, value: 'gcp' },
{ otlp: OTLP.database.OPERATION_NAME, instana: 'op' },
{ otlp: OTLP.cloud.gcp.PROJECT_ID, instana: 'projectId' },
{ otlp: OTLP.database.NAMESPACE, instana: 'bucket' },
{ otlp: OTLP.database.COLLECTION_NAME, instana: 'object' },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.S3]: {
spanName: data => `S3.${data.op}`,
spanAttributes: [
{ otlp: OTLP.rpc.SYSTEM_NAME, value: 'aws-api' },
{ otlp: OTLP.rpc.METHOD, instana: 'op' },
{ otlp: OTLP.cloud.REGION, instana: 'region' },
{ otlp: OTLP.cloud.PROVIDER, value: 'aws' },
{ otlp: OTLP.cloud.aws.S3_BUCKET, instana: 'bucket' },
{ otlp: OTLP.cloud.aws.S3_KEY, instana: 'key' },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.KINESIS]: {
spanName: data => `Kinesis.${data.op}`,
spanAttributes: [
{ otlp: OTLP.messaging.SYSTEM, value: 'aws_kinesis' },
{ otlp: OTLP.messaging.OPERATION_NAME, instana: 'op' },
{ otlp: OTLP.cloud.aws.KINESIS_STREAM, instana: 'stream' },
{ otlp: OTLP.messaging.DESTINATION_NAME, instana: 'stream' },
{ otlp: OTLP.messaging.DESTINATION_PARTITION_ID, instana: 'shard' },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
// Note: Azure Storage is not included in the official OpenTelemetry semantic conventions
// specification. This mapping uses standard cloud.* and database.* attributes to represent azure operations.
[INSTRUMENTATION_TYPES.AZSTORAGE]: {
spanName: data => `azure.storage.${data.op}`,
spanAttributes: [
{ otlp: OTLP.cloud.PROVIDER, value: 'azure' },
{ otlp: OTLP.database.OPERATION_NAME, instana: 'op' },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
// TBD: maybe not required as we already hanlde lambda entry
[INSTRUMENTATION_TYPES.AWS_LAMBDA_INVOKE]: {
spanName: data => data.function,
spanAttributes: [
{ otlp: OTLP.faas.NAME, instana: 'function' },
{ otlp: OTLP.faas.INVOCATION_TYPE, instana: 'type' },
{ otlp: OTLP.cloud.PROVIDER, value: 'aws' },
{ otlp: OTLP.cloud.PLATFORM, value: 'aws_lambda' },
{ otlp: OTLP.error.TYPE, instana: 'error' }
]
},
[INSTRUMENTATION_TYPES.AWS_LAMBDA_ENTRY]: {
spanName: data => data.functionName,
spanAttributes: [
{
otlp: OTLP.faas.TRIGGER,
instana: 'trigger',
transform: trigger => LAMBDA_TRIGGER_MAP[trigger] || 'other'
},
{ otlp: OTLP.faas.NAME, instana: 'functionName' },
{ otlp: OTLP.faas.VERSION, instana: 'functionVersion' },
{ otlp: OTLP.cloud.PROVIDER, value: 'aws' },
{ otlp: OTLP.cloud.PLATFORM, value: 'aws_lambda' },
{
otlp: OTLP.cloud.REGION,
instana: 'arn',
transform: arn => getRegionFromArn(arn)
},
{
otlp: OTLP.cloud.ACCOUNT_ID,
instana: 'arn',
transform: arn => getAccountIdFromArn(arn)
},
{ otlp: OTLP.cloud.RESOURCE_ID, instana: 'arn' },
{ otlp: OTLP.faas.INVOCATION_ID, instana: 'reqId' },
{
otlp: OTLP.faas.COLDSTART,
instana: 'coldStart',
transform: value => toBoolean(value)
},
{ otlp: OTLP.process.RUNTIME_NAME, instana: 'runtime' },
{ otlp: OTLP.exception.MESSAGE, instana: 'error' }
]
},
// PEER is special - only has attributes, no span name
[INSTRUMENTATION_TYPES.PEER]: {
spanAttributes: [
{ otlp: OTLP.network.PEER_NAME, instana: 'hostname' },
{ otlp: OTLP.network.PEER_PORT, instana: 'port' }
]
}
};
/**
* @param {import('../../../core').InstanaBaseSpan} span
* @returns {string|null}
*/
function getSpanType(span) {
if (!span || !span.data) {
return null;
}
const key = Object.keys(span.data).find(
k => k !== INSTRUMENTATION_TYPES.PEER && k !== SPECIAL_SPAN_DATA_TYPES.RESOURCE
);
return key || null;
}
/**
*
* @param {AttributeMapping} mapping
* @param {Record<string, any>} spanData
* @returns {SpanAttribute|null}
*/
function applyMapping(mapping, spanData) {
if (!mapping) return null;
let value;
if (mapping.value !== undefined && !mapping.instana) {
value = mapping.value;
} else if (Array.isArray(mapping.instana)) {
const values = mapping.instana.map(k => spanData?.[k]);
value = mapping.transform ? mapping.transform(values, spanData) : combineFields(spanData, mapping.instana);
} else if (typeof mapping.instana === 'string') {
const rawValue = spanData?.[mapping.instana];
if (rawValue === null || rawValue === undefined) {
return null;
}
value = mapping.transform ? mapping.transform(rawValue, spanData) : rawValue;
} else {
return null;
}
if (value === null || value === undefined) {
return null;
}
return {
key: mapping.otlp,
value: formatOTLPValue(value)
};
}
module.exports = {
/**
* @param {import('../../../core').InstanaBaseSpan} span
*/
spanName(span) {
const type = getSpanType(span);
const handler = instrumentationMappings[type]?.spanName;
const spanData = type ? span.data?.[type] : null;
if (typeof handler === 'function' && spanData) {
return handler(spanData);
}
return span?.n || type || 'unknown';
},
/** @param {import('../../../core').InstanaBaseSpan} span */
spanAttributes(span) {
const attributes = [];
const spanTypes = Object.keys(span.data || {});
for (let i = 0; i < spanTypes.length; i++) {
const spanType = spanTypes[i];
if (spanType === SPECIAL_SPAN_DATA_TYPES.RESOURCE) {
continue;
}
const handler = instrumentationMappings[spanType]?.spanAttributes;
const spanData = span.data[spanType];
if (!Array.isArray(handler) || !spanData) {
continue;
}
for (let j = 0; j < handler.length; j++) {
const attribute = applyMapping(handler[j], spanData);
if (attribute) {
attributes.push(attribute);
}
}
}
return attributes;
},
/**
* @param {import('../../../core').InstanaBaseSpan} span
*/
spanStatus(span) {
const type = getSpanType(span);
const data = type ? span.data?.[type] : null;
// Special case: HTTP client 4xx responses are reported as errors according to OTel semantic conventions, even
// though Instana leaves span.ec unset for these responses. We handled it here.
// Remove this handling once INSTA-98209 is implemented.
const shouldReportHttpClientAsError = span.n === 'node.http.client' && data?.status >= 400 && data?.status < 500;
if (!span?.ec && !shouldReportHttpClientAsError) {
return { code: OTLP_STATUS_CODES.UNSET };
}
return {
code: OTLP_STATUS_CODES.ERROR,
message: String(data?.error || `${type || span.n || 'operation'} failed`)
};
}
};