UNPKG

@aws/aws-distro-opentelemetry-node-autoinstrumentation

Version:

This package provides Amazon Web Services distribution of the OpenTelemetry Node Instrumentation, which allows for auto-instrumentation of NodeJS applications.

383 lines 20.6 kB
"use strict"; // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 Object.defineProperty(exports, "__esModule", { value: true }); exports.AwsSpanProcessingUtil = void 0; const api_1 = require("@opentelemetry/api"); const semantic_conventions_1 = require("@opentelemetry/semantic-conventions"); const aws_attribute_keys_1 = require("./aws-attribute-keys"); const aws_opentelemetry_configurator_1 = require("./aws-opentelemetry-configurator"); const SQL_DIALECT_KEYWORDS_JSON = require("./configuration/sql_dialect_keywords.json"); /** Utility class designed to support shared logic across AWS Span Processors. */ class AwsSpanProcessingUtil { /** * Parse the OTEL_AWS_HTTP_OPERATION_PATHS env var into a sorted list of path templates * (longest first by segment count). Returns an empty array if the env var is not set. */ static getOperationPaths() { if (AwsSpanProcessingUtil.operationPaths === undefined) { const config = process.env[AwsSpanProcessingUtil.OTEL_AWS_HTTP_OPERATION_PATHS_CONFIG]; if (config === undefined || config.trim() === '') { AwsSpanProcessingUtil.operationPaths = []; } else { const paths = config .split(',') .map(p => p.trim()) .filter(p => p.length > 0); // Sort longest first (by segment count) for longest-prefix-match. // For patterns with the same number of segments, original config order is preserved (stable sort). paths.sort((a, b) => b.split('/').length - a.split('/').length); AwsSpanProcessingUtil.operationPaths = paths; } } return AwsSpanProcessingUtil.operationPaths; } /** Reset cached operation paths (for testing). */ static resetOperationPaths() { AwsSpanProcessingUtil.operationPaths = undefined; } /** * If OTEL_AWS_HTTP_OPERATION_PATHS is configured and a pattern matches the span's URL path, * mutates the span name to "METHOD /path/template". Returns the span unchanged if no config * is set or no pattern matches. */ static applyOperationPathSpanName(span) { const paths = AwsSpanProcessingUtil.getOperationPaths(); if (paths.length === 0) { return span; } let urlPath = AwsSpanProcessingUtil.getUrlPath(span); if (urlPath === undefined || urlPath === '') { return span; } // Strip query string and fragment (relevant for http.target) for (const sep of ['?', '#']) { const idx = urlPath.indexOf(sep); if (idx >= 0) { urlPath = urlPath.substring(0, idx); } } // Normalize trailing slashes while (urlPath.endsWith('/') && urlPath.length > 1) { urlPath = urlPath.substring(0, urlPath.length - 1); } const urlSegments = urlPath.split('/'); for (const pattern of paths) { let normalizedPattern = pattern; while (normalizedPattern.endsWith('/') && normalizedPattern.length > 1) { normalizedPattern = normalizedPattern.substring(0, normalizedPattern.length - 1); } if (AwsSpanProcessingUtil.segmentsMatch(urlSegments, normalizedPattern.split('/'))) { const httpMethod = AwsSpanProcessingUtil.getHttpMethod(span); const newName = httpMethod !== undefined ? httpMethod + ' ' + pattern : pattern; // Mutate the span name in place const mutableSpan = span; mutableSpan.name = newName; return span; } } return span; } /** Return the URL path from server span attributes, preferring url.path over http.target. */ static getUrlPath(span) { var _a; const urlPath = (_a = span.attributes[semantic_conventions_1.ATTR_URL_PATH]) !== null && _a !== void 0 ? _a : span.attributes[semantic_conventions_1.SEMATTRS_HTTP_TARGET]; return typeof urlPath === 'string' ? urlPath : undefined; } /** Get the HTTP method from the span, checking new and deprecated semconv attributes. */ static getHttpMethod(span) { var _a; const method = (_a = span.attributes[semantic_conventions_1.ATTR_HTTP_REQUEST_METHOD]) !== null && _a !== void 0 ? _a : span.attributes[semantic_conventions_1.SEMATTRS_HTTP_METHOD]; return typeof method === 'string' ? method : undefined; } /** * Check if URL segments match a pattern's segments. Only pattern segments can be wildcards * ({param}, :param, or *) — URL segments are always treated as literals. The pattern acts * as a prefix — extra URL segments after the pattern are allowed. */ static segmentsMatch(urlSegments, patternSegments) { for (let i = 0; i < patternSegments.length; i++) { if (i >= urlSegments.length) { return false; } const ps = patternSegments[i]; const us = urlSegments[i]; if (AwsSpanProcessingUtil.isWildcardSegment(ps)) { if (us === '') { return false; } continue; } if (ps !== us) { return false; } } return true; } /** A segment is a wildcard if it uses {param}, :param, or * format. */ static isWildcardSegment(segment) { return (segment.startsWith('{') && segment.endsWith('}')) || segment.startsWith(':') || segment === '*'; } static getDialectKeywords() { return SQL_DIALECT_KEYWORDS_JSON.keywords; } /** * Ingress operation (i.e. operation for Server and Consumer spans) will be generated from * "http.method + http.target/with the first API path parameter" if the default span name equals * null, UnknownOperation or http.method value. */ static getIngressOperation(span) { let operation = span.name; if (AwsSpanProcessingUtil.shouldUseInternalOperation(span)) { operation = AwsSpanProcessingUtil.INTERNAL_OPERATION; } if ((0, aws_opentelemetry_configurator_1.isLambdaEnvironment)()) { operation = process.env[aws_opentelemetry_configurator_1.AWS_LAMBDA_FUNCTION_NAME_CONFIG] + '/FunctionHandler'; } else if (!AwsSpanProcessingUtil.isValidOperation(span, operation)) { operation = AwsSpanProcessingUtil.generateIngressOperation(span); } return operation; } static getEgressOperation(span) { if (AwsSpanProcessingUtil.shouldUseInternalOperation(span)) { return AwsSpanProcessingUtil.INTERNAL_OPERATION; } else { const awsLocalOperation = span.attributes[aws_attribute_keys_1.AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]; return awsLocalOperation === undefined ? undefined : awsLocalOperation.toString(); } } /** * Extract the first part from API http target if it exists * * @param httpTarget http request target string value. Eg, /payment/1234 * @return the first part from the http target. Eg, /payment */ static extractAPIPathValue(httpTarget) { // In TypeScript, `httpTarget == null` checks both null and undefined if (httpTarget == null || httpTarget === '') { return '/'; } // Divergence from Java/Python // https://github.com/open-telemetry/semantic-conventions/blob/4e7c42ee8e4c3a39a899c4c85c64df28cd543f78/docs/attributes-registry/http.md#deprecated-http-attributes // According to OTel Spec, httpTarget may include query and fragment: // - `/search?q=OpenTelemetry#SemConv` // We do NOT want the `?` or `#` parts, so let us strip it out, // because HTTP (ingress) instrumentation was observed to include the query (`?`) part // - https://github.com/open-telemetry/opentelemetry-js/blob/b418d36609c371d1fcae46898e9ede6278aca917/experimental/packages/opentelemetry-instrumentation-http/src/utils.ts#L502-L504 // According to RFC Specification, "The path is terminated by the first question mark ("?") or number sign ("#") character, or by the end of the URI." // - https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 // // This is a fix that can be applied here since this is the central location for generating API Path Value // TODO: Possibly contribute fix to upstream for this diff between langauges. However, the current attribute value in JS is according to spec. // // Interestingly, according to Spec, Java/Python should be affected, but they are not. const paths = httpTarget.split(/[/?#]/); if (paths.length > 1) { return '/' + paths[1]; } return '/'; } static isKeyPresent(span, key) { return span.attributes[key] !== undefined; } static isAwsSDKSpan(span) { const rpcSystem = span.attributes[semantic_conventions_1.SEMATTRS_RPC_SYSTEM]; if (rpcSystem === undefined) { return false; } // https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/instrumentation/aws-sdk/#common-attributes return 'aws-api' === rpcSystem; } static shouldGenerateServiceMetricAttributes(span) { return ((AwsSpanProcessingUtil.isLocalRoot(span) && !AwsSpanProcessingUtil.isSqsReceiveMessageConsumerSpan(span)) || api_1.SpanKind.SERVER === span.kind); } static shouldGenerateDependencyMetricAttributes(span) { return (api_1.SpanKind.CLIENT === span.kind || api_1.SpanKind.PRODUCER === span.kind || (AwsSpanProcessingUtil.isDependencyConsumerSpan(span) && !AwsSpanProcessingUtil.isSqsReceiveMessageConsumerSpan(span))); } static isConsumerProcessSpan(spanData) { const messagingOperation = spanData.attributes[semantic_conventions_1.SEMATTRS_MESSAGING_OPERATION]; if (messagingOperation === undefined) { return false; } return api_1.SpanKind.CONSUMER === spanData.kind && semantic_conventions_1.MessagingOperationValues.PROCESS === messagingOperation; } // Any spans that are Local Roots and also not SERVER should have aws.local.operation renamed to // InternalOperation. static shouldUseInternalOperation(span) { return AwsSpanProcessingUtil.isLocalRoot(span) && api_1.SpanKind.SERVER !== span.kind; } // A span is a local root if it has no parent or if the parent is remote. This function checks the // parent context and returns true if it is a local root. static isLocalRoot(spanData) { // Workaround implemented for this function as parent span context is not obtainable. // This isLocalRoot value is precalculated in AttributePropagatingSpanProcessor, which // is started before the other processors (e.g. AwsSpanMetricsProcessor) // Thus this function is implemented differently than in Java/Python const isLocalRoot = spanData.attributes[aws_attribute_keys_1.AWS_ATTRIBUTE_KEYS.AWS_IS_LOCAL_ROOT]; if (typeof isLocalRoot !== 'boolean') { // isLocalRoot should be a precalculated boolean, this code block should not be entered api_1.diag.debug('isLocalRoot for span has not been precalculated. Assuming span is Local Root Span.'); return true; } return isLocalRoot; } // To identify the SQS consumer spans produced by AWS SDK instrumentation static isSqsReceiveMessageConsumerSpan(spanData) { const spanName = spanData.name; const spanKind = spanData.kind; const messagingOperation = spanData.attributes[semantic_conventions_1.SEMATTRS_MESSAGING_OPERATION]; const instrumentationScope = spanData.instrumentationScope; return (AwsSpanProcessingUtil.SQS_RECEIVE_MESSAGE_SPAN_NAME.toLowerCase() === spanName.toLowerCase() && api_1.SpanKind.CONSUMER === spanKind && instrumentationScope != null && instrumentationScope.name.startsWith(AwsSpanProcessingUtil.AWS_SDK_INSTRUMENTATION_SCOPE_PREFIX) && (messagingOperation === undefined || messagingOperation === semantic_conventions_1.MessagingOperationValues.PROCESS)); } static isDependencyConsumerSpan(span) { if (api_1.SpanKind.CONSUMER !== span.kind) { return false; } else if (AwsSpanProcessingUtil.isConsumerProcessSpan(span)) { if (AwsSpanProcessingUtil.isLocalRoot(span)) { return true; } const parentSpanKind = span.attributes[aws_attribute_keys_1.AWS_ATTRIBUTE_KEYS.AWS_CONSUMER_PARENT_SPAN_KIND]; return api_1.SpanKind[api_1.SpanKind.CONSUMER] !== parentSpanKind; } return true; } /** * When Span name is null, UnknownOperation or HttpMethod value, it will be treated as invalid * local operation value that needs to be further processed */ static isValidOperation(span, operation) { if (operation == null || operation === AwsSpanProcessingUtil.UNKNOWN_OPERATION) { return false; } const httpMethod = AwsSpanProcessingUtil.getHttpMethod(span); if (httpMethod !== undefined) { return operation !== httpMethod; } return true; } /** * When span name is not meaningful(null, unknown or http_method value) as operation name for http * use cases. Will try to extract the operation name from http target string */ static generateIngressOperation(span) { var _a; let operation = AwsSpanProcessingUtil.UNKNOWN_OPERATION; let httpPath = undefined; if (AwsSpanProcessingUtil.isKeyPresent(span, semantic_conventions_1.SEMATTRS_HTTP_TARGET)) { httpPath = span.attributes[semantic_conventions_1.SEMATTRS_HTTP_TARGET]; } else if (AwsSpanProcessingUtil.isKeyPresent(span, semantic_conventions_1.SEMATTRS_HTTP_URL)) { const httpUrl = span.attributes[semantic_conventions_1.SEMATTRS_HTTP_URL]; try { let url; if (typeof httpUrl === 'string') { url = new URL(httpUrl); httpPath = url.pathname; } } catch (e) { // In Python, if `httpUrl == ''`, there is no error from URL parsing, and `url.pathname = ''` // In TypeScript, this catch block will be invoked. Here `httpPath = ''` is set as default to match Python. api_1.diag.verbose(`invalid URL attribute: ${httpUrl}, setting httpPath as empty string`); httpPath = ''; } } else if (AwsSpanProcessingUtil.isKeyPresent(span, semantic_conventions_1.ATTR_URL_PATH)) { httpPath = span.attributes[semantic_conventions_1.ATTR_URL_PATH]; } else if (AwsSpanProcessingUtil.isKeyPresent(span, semantic_conventions_1.ATTR_URL_FULL)) { const httpUrl = span.attributes[semantic_conventions_1.ATTR_URL_FULL]; try { let url; if (typeof httpUrl === 'string') { url = new URL(httpUrl); httpPath = url.pathname; } } catch (e) { // In Python, if `httpUrl == ''`, there is no error from URL parsing, and `url.pathname = ''` // In TypeScript, this catch block will be invoked. Here `httpPath = ''` is set as default to match Python. api_1.diag.verbose(`invalid URL attribute: ${httpUrl}, setting httpPath as empty string`); httpPath = ''; } } if (typeof httpPath === 'string') { operation = this.extractAPIPathValue(httpPath); if (AwsSpanProcessingUtil.isKeyPresent(span, semantic_conventions_1.SEMATTRS_HTTP_METHOD) || AwsSpanProcessingUtil.isKeyPresent(span, semantic_conventions_1.ATTR_HTTP_REQUEST_METHOD)) { const httpMethod = (_a = span.attributes[semantic_conventions_1.SEMATTRS_HTTP_METHOD]) !== null && _a !== void 0 ? _a : span.attributes[semantic_conventions_1.ATTR_HTTP_REQUEST_METHOD]; if (httpMethod !== undefined) { operation = httpMethod + ' ' + operation; } } } return operation; } // Check if the current Span adheres to database semantic conventions static isDBSpan(span) { return (AwsSpanProcessingUtil.isKeyPresent(span, semantic_conventions_1.SEMATTRS_DB_SYSTEM) || AwsSpanProcessingUtil.isKeyPresent(span, semantic_conventions_1.SEMATTRS_DB_OPERATION) || AwsSpanProcessingUtil.isKeyPresent(span, semantic_conventions_1.SEMATTRS_DB_STATEMENT)); } // Divergence from Java/Python static setIsLocalRootInformation(span, parentContext) { var _a; const parentSpanContext = api_1.trace.getSpanContext(parentContext); const isParentSpanContextValid = parentSpanContext !== undefined && (0, api_1.isSpanContextValid)(parentSpanContext); const isParentSpanRemote = parentSpanContext !== undefined && parentSpanContext.isRemote === true; const isLocalRoot = ((_a = span.parentSpanContext) === null || _a === void 0 ? void 0 : _a.spanId) === undefined || !isParentSpanContextValid || isParentSpanRemote; span.setAttribute(aws_attribute_keys_1.AWS_ATTRIBUTE_KEYS.AWS_IS_LOCAL_ROOT, isLocalRoot); } static getResourceId(span) { let resourceId = undefined; if (AwsSpanProcessingUtil.isKeyPresent(span, AwsSpanProcessingUtil.CLOUD_RESOURCE_ID)) { resourceId = span.attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID]; } else if (AwsSpanProcessingUtil.isKeyPresent(span, semantic_conventions_1.SEMRESATTRS_FAAS_ID)) { resourceId = span.attributes[semantic_conventions_1.SEMRESATTRS_FAAS_ID]; } return typeof resourceId === 'string' ? resourceId : undefined; } } // Default attribute values if no valid span attribute value is identified AwsSpanProcessingUtil.UNKNOWN_SERVICE = 'UnknownService'; AwsSpanProcessingUtil.UNKNOWN_OPERATION = 'UnknownOperation'; AwsSpanProcessingUtil.UNKNOWN_REMOTE_SERVICE = 'UnknownRemoteService'; AwsSpanProcessingUtil.UNKNOWN_REMOTE_OPERATION = 'UnknownRemoteOperation'; AwsSpanProcessingUtil.INTERNAL_OPERATION = 'InternalOperation'; AwsSpanProcessingUtil.LOCAL_ROOT = 'LOCAL_ROOT'; AwsSpanProcessingUtil.SQS_RECEIVE_MESSAGE_SPAN_NAME = 'Sqs.ReceiveMessage'; AwsSpanProcessingUtil.AWS_SDK_INSTRUMENTATION_SCOPE_PREFIX = '@opentelemetry/instrumentation-aws-sdk'; // "cloud.resource_id" is defined in semconv which has not yet picked up by OTel JS // https://opentelemetry.io/docs/specs/semconv/attributes-registry/cloud/ AwsSpanProcessingUtil.CLOUD_RESOURCE_ID = 'cloud.resource_id'; // Max keyword length supported by parsing into remote_operation from DB_STATEMENT. // The current longest command word is DATETIME_INTERVAL_PRECISION at 27 characters. // If we add a longer keyword to the sql dialect keyword list, need to update the constant below. AwsSpanProcessingUtil.MAX_KEYWORD_LENGTH = 27; AwsSpanProcessingUtil.SQL_DIALECT_PATTERN = '^(?:' + AwsSpanProcessingUtil.getDialectKeywords().join('|') + ')\\b'; // TODO: Use Semantic Conventions once upgraded AwsSpanProcessingUtil.GEN_AI_REQUEST_MODEL = 'gen_ai.request.model'; // Environment variable for configurable operation name paths AwsSpanProcessingUtil.OTEL_AWS_HTTP_OPERATION_PATHS_CONFIG = 'OTEL_AWS_HTTP_OPERATION_PATHS'; AwsSpanProcessingUtil.GEN_AI_SYSTEM = 'gen_ai.system'; AwsSpanProcessingUtil.GEN_AI_REQUEST_MAX_TOKENS = 'gen_ai.request.max_tokens'; AwsSpanProcessingUtil.GEN_AI_REQUEST_TEMPERATURE = 'gen_ai.request.temperature'; AwsSpanProcessingUtil.GEN_AI_REQUEST_TOP_P = 'gen_ai.request.top_p'; AwsSpanProcessingUtil.GEN_AI_RESPONSE_FINISH_REASONS = 'gen_ai.response.finish_reasons'; AwsSpanProcessingUtil.GEN_AI_USAGE_INPUT_TOKENS = 'gen_ai.usage.input_tokens'; AwsSpanProcessingUtil.GEN_AI_USAGE_OUTPUT_TOKENS = 'gen_ai.usage.output_tokens'; exports.AwsSpanProcessingUtil = AwsSpanProcessingUtil; //# sourceMappingURL=aws-span-processing-util.js.map