From 47f32c5f51073a011ffc2a2ee7db69242b94daca Mon Sep 17 00:00:00 2001 From: jjllee Date: Wed, 31 Jul 2024 09:40:44 -0700 Subject: [PATCH 1/8] AppSignals Functionality - add AWS Metric Attribute Generator --- .../src/aws-metric-attribute-generator.ts | 563 ++++++++++++++++++ .../src/metric-attribute-generator.ts | 31 + 2 files changed, 594 insertions(+) create mode 100644 aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts create mode 100644 aws-distro-opentelemetry-node-autoinstrumentation/src/metric-attribute-generator.ts diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts new file mode 100644 index 0000000..b7a3caa --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts @@ -0,0 +1,563 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Attributes, AttributeValue, diag, SpanKind } from '@opentelemetry/api'; +import { Resource } from '@opentelemetry/resources'; +import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; +import { + SEMATTRS_DB_CONNECTION_STRING, + SEMATTRS_DB_NAME, + SEMATTRS_DB_OPERATION, + SEMATTRS_DB_STATEMENT, + SEMATTRS_DB_SYSTEM, + SEMATTRS_DB_USER, + SEMATTRS_FAAS_INVOKED_NAME, + SEMATTRS_FAAS_TRIGGER, + SEMATTRS_HTTP_METHOD, + SEMATTRS_HTTP_URL, + SEMATTRS_MESSAGING_OPERATION, + SEMATTRS_MESSAGING_SYSTEM, + SEMATTRS_NET_PEER_NAME, + SEMATTRS_NET_PEER_PORT, + SEMATTRS_PEER_SERVICE, + SEMATTRS_RPC_METHOD, + SEMATTRS_RPC_SERVICE, + SEMRESATTRS_SERVICE_NAME, +} from '@opentelemetry/semantic-conventions'; +import { AWS_ATTRIBUTE_KEYS } from './aws-attribute-keys'; +import { AwsSpanProcessingUtil } from './aws-span-processing-util'; +import { + AttributeMap, + DEPENDENCY_METRIC, + MetricAttributeGenerator, + SERVICE_METRIC, +} from './metric-attribute-generator'; +import { SqsUrlParser } from './sqs-url-parser'; + +// Does not exist in @opentelemetry/semantic-conventions +const _SERVER_SOCKET_ADDRESS: string = 'server.socket.address'; +const _SERVER_SOCKET_PORT: string = 'server.socket.port'; +const _NET_SOCK_PEER_ADDR: string = 'net.sock.peer.addr'; +const _NET_SOCK_PEER_PORT: string = 'net.sock.peer.port'; +// Alternatively, `import { SemanticAttributes } from '@opentelemetry/instrumentation-undici/build/src/enums/SemanticAttributes';` +// SemanticAttributes.SERVER_ADDRESS +// SemanticAttributes.SERVER_PORT +const _SERVER_ADDRESS: string = 'server.address'; +const _SERVER_PORT: string = 'server.port'; +// Alternatively, `import { AttributeNames } from '@opentelemetry/instrumentation-graphql/build/src/enums/AttributeNames';` +// AttributeNames.OPERATION_TYPE +const _GRAPHQL_OPERATION_TYPE: string = 'graphql.operation.type'; +// Special DEPENDENCY attribute value if GRAPHQL_OPERATION_TYPE attribute key is present. +const GRAPHQL: string = 'graphql'; + +// Normalized remote service names for supported AWS services +const NORMALIZED_DYNAMO_DB_SERVICE_NAME: string = 'AWS::DynamoDB'; +const NORMALIZED_KINESIS_SERVICE_NAME: string = 'AWS::Kinesis'; +const NORMALIZED_S3_SERVICE_NAME: string = 'AWS::S3'; +const NORMALIZED_SQS_SERVICE_NAME: string = 'AWS::SQS'; + +const DB_CONNECTION_RESOURCE_TYPE: string = 'DB::Connection'; +// As per https://opentelemetry.io/docs/specs/semconv/resource/#service, if service name is not specified, SDK defaults +// the service name to unknown_service: or just unknown_service. +const OTEL_UNKNOWN_SERVICE: string = 'unknown_service:node'; + +const JDBC_PROTOCOL_PREFIX: string = 'jdbc:'; + +/** + * AwsMetricAttributeGenerator generates very specific metric attributes based on low-cardinality + * span and resource attributes. If such attributes are not present, we fallback to default values. + * + *

The goal of these particular metric attributes is to get metrics for incoming and outgoing + * traffic for a service. Namely, {@link SpanKind.SERVER} and {@link SpanKind.CONSUMER} spans + * represent "incoming" traffic, {@link SpanKind.CLIENT} and {@link SpanKind.PRODUCER} spans + * represent "outgoing" traffic, and {@link SpanKind.INTERNAL} spans are ignored. + */ +export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { + // This method is used by the AwsSpanMetricsProcessor to generate service and dependency metrics + public generateMetricAttributeMapFromSpan(span: ReadableSpan, resource: Resource): AttributeMap { + const attributesMap: AttributeMap = {}; + + if (AwsSpanProcessingUtil.shouldGenerateServiceMetricAttributes(span)) { + attributesMap[SERVICE_METRIC] = this.generateServiceMetricAttributes(span, resource); + } + if (AwsSpanProcessingUtil.shouldGenerateDependencyMetricAttributes(span)) { + attributesMap[DEPENDENCY_METRIC] = this.generateDependencyMetricAttributes(span, resource); + } + + return attributesMap; + } + + private generateServiceMetricAttributes(span: ReadableSpan, resource: Resource): Attributes { + const attributes: Attributes = {}; + + AwsMetricAttributeGenerator.setService(resource, span, attributes); + AwsMetricAttributeGenerator.setIngressOperation(span, attributes); + AwsMetricAttributeGenerator.setSpanKindForService(span, attributes); + + return attributes; + } + + private generateDependencyMetricAttributes(span: ReadableSpan, resource: Resource): Attributes { + const attributes: Attributes = {}; + AwsMetricAttributeGenerator.setService(resource, span, attributes); + AwsMetricAttributeGenerator.setEgressOperation(span, attributes); + AwsMetricAttributeGenerator.setRemoteServiceAndOperation(span, attributes); + AwsMetricAttributeGenerator.setRemoteResourceTypeAndIdentifier(span, attributes); + AwsMetricAttributeGenerator.setSpanKindForDependency(span, attributes); + AwsMetricAttributeGenerator.setRemoteDbUser(span, attributes); + + return attributes; + } + + /** Service is always derived from {@link SEMRESATTRS_SERVICE_NAME} */ + private static setService(resource: Resource, span: ReadableSpan, builder: Attributes): void { + let service: AttributeValue | undefined = resource.attributes[SEMRESATTRS_SERVICE_NAME]; + + // In practice the service name is never undefined, but we can be defensive here. + if (service === undefined || service === OTEL_UNKNOWN_SERVICE) { + AwsMetricAttributeGenerator.logUnknownAttribute(AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE, span); + service = AwsSpanProcessingUtil.UNKNOWN_SERVICE; + } + builder[AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE] = service; + } + + /** + * 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. + */ + private static setIngressOperation(span: ReadableSpan, builder: Attributes): void { + const operation: string = AwsSpanProcessingUtil.getIngressOperation(span); + if (operation === AwsSpanProcessingUtil.UNKNOWN_OPERATION) { + AwsMetricAttributeGenerator.logUnknownAttribute(AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION, span); + } + builder[AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION] = operation; + } + + /** + * Egress operation (i.e. operation for Client and Producer spans) is always derived from a + * special span attribute, {@link AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION}. This attribute is + * generated with a separate SpanProcessor, {@link AttributePropagatingSpanProcessor} + */ + private static setEgressOperation(span: ReadableSpan, builder: Attributes): void { + let operation: AttributeValue | undefined = AwsSpanProcessingUtil.getEgressOperation(span); + if (operation === undefined) { + AwsMetricAttributeGenerator.logUnknownAttribute(AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION, span); + operation = AwsSpanProcessingUtil.UNKNOWN_OPERATION; + } + builder[AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION] = operation; + } + + /** + * Remote attributes (only for Client and Producer spans) are generated based on low-cardinality + * span attributes, in priority order. + * + *

The first priority is the AWS Remote attributes, which are generated from manually + * instrumented span attributes, and are clear indications of customer intent. If AWS Remote + * attributes are not present, the next highest priority span attribute is Peer Service, which is + * also a reliable indicator of customer intent. If this is set, it will override + * AWS_REMOTE_SERVICE identified from any other span attribute, other than AWS Remote attributes. + * + *

After this, we look for the following low-cardinality span attributes that can be used to + * determine the remote metric attributes: + * + *

+ * + *

In each case, these span attributes were selected from the OpenTelemetry trace semantic + * convention specifications as they adhere to the three following criteria: + * + *

+ * + * if the selected attributes are still producing the UnknownRemoteService or + * UnknownRemoteOperation, `net.peer.name`, `net.peer.port`, `net.peer.sock.addr`, + * `net.peer.sock.port` and `http.url` will be used to derive the RemoteService. And `http.method` + * and `http.url` will be used to derive the RemoteOperation. + */ + private static setRemoteServiceAndOperation(span: ReadableSpan, builder: Attributes): void { + let remoteService: string = AwsSpanProcessingUtil.UNKNOWN_REMOTE_SERVICE; + let remoteOperation: string = AwsSpanProcessingUtil.UNKNOWN_REMOTE_OPERATION; + + if ( + AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE) || + AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION) + ) { + remoteService = AwsMetricAttributeGenerator.getRemoteService(span, AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE); + remoteOperation = AwsMetricAttributeGenerator.getRemoteOperation(span, AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION); + } else if ( + AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_RPC_SERVICE) || + AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_RPC_METHOD) + ) { + remoteService = AwsMetricAttributeGenerator.normalizeRemoteServiceName( + span, + AwsMetricAttributeGenerator.getRemoteService(span, SEMATTRS_RPC_SERVICE) + ); + remoteOperation = AwsMetricAttributeGenerator.getRemoteOperation(span, SEMATTRS_RPC_METHOD); + } else if (AwsSpanProcessingUtil.isDBSpan(span)) { + remoteService = AwsMetricAttributeGenerator.getRemoteService(span, SEMATTRS_DB_SYSTEM); + if (AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_DB_OPERATION)) { + remoteOperation = AwsMetricAttributeGenerator.getRemoteOperation(span, SEMATTRS_DB_OPERATION); + } else { + remoteOperation = AwsMetricAttributeGenerator.getDBStatementRemoteOperation(span, SEMATTRS_DB_STATEMENT); + } + } else if ( + AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_FAAS_INVOKED_NAME) || + AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_FAAS_TRIGGER) + ) { + remoteService = AwsMetricAttributeGenerator.getRemoteService(span, SEMATTRS_FAAS_INVOKED_NAME); + remoteOperation = AwsMetricAttributeGenerator.getRemoteOperation(span, SEMATTRS_FAAS_TRIGGER); + } else if ( + AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_MESSAGING_SYSTEM) || + AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_MESSAGING_OPERATION) + ) { + remoteService = AwsMetricAttributeGenerator.getRemoteService(span, SEMATTRS_MESSAGING_SYSTEM); + remoteOperation = AwsMetricAttributeGenerator.getRemoteOperation(span, SEMATTRS_MESSAGING_OPERATION); + } else if (AwsSpanProcessingUtil.isKeyPresent(span, _GRAPHQL_OPERATION_TYPE)) { + remoteService = GRAPHQL; + remoteOperation = AwsMetricAttributeGenerator.getRemoteOperation(span, _GRAPHQL_OPERATION_TYPE); + } + + // Peer service takes priority as RemoteService over everything but AWS Remote. + if ( + AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_PEER_SERVICE) && + !AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE) + ) { + remoteService = AwsMetricAttributeGenerator.getRemoteService(span, SEMATTRS_PEER_SERVICE); + } + + // try to derive RemoteService and RemoteOperation from the other related attributes + if (remoteService === AwsSpanProcessingUtil.UNKNOWN_REMOTE_SERVICE) { + remoteService = AwsMetricAttributeGenerator.generateRemoteService(span); + } + if (remoteOperation === AwsSpanProcessingUtil.UNKNOWN_REMOTE_OPERATION) { + remoteOperation = AwsMetricAttributeGenerator.generateRemoteOperation(span); + } + + builder[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE] = remoteService; + builder[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION] = remoteOperation; + } + + /** + * When the remote call operation is undetermined for http use cases, will try to extract the + * remote operation name from http url string + */ + private static generateRemoteOperation(span: ReadableSpan): string { + let remoteOperation: string = AwsSpanProcessingUtil.UNKNOWN_REMOTE_OPERATION; + if (AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_HTTP_URL)) { + const httpUrl: AttributeValue | undefined = span.attributes[SEMATTRS_HTTP_URL]; + try { + let url: URL; + if (httpUrl !== undefined) { + url = new URL(httpUrl as string); + remoteOperation = AwsSpanProcessingUtil.extractAPIPathValue(url.pathname); + } + } catch (e: unknown) { + diag.verbose(`invalid http.url attribute: ${httpUrl}`); + } + } + if (AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_HTTP_METHOD)) { + const httpMethod: AttributeValue | undefined = span.attributes[SEMATTRS_HTTP_METHOD]; + remoteOperation = (httpMethod as string) + ' ' + remoteOperation; + } + if (remoteOperation === AwsSpanProcessingUtil.UNKNOWN_REMOTE_OPERATION) { + AwsMetricAttributeGenerator.logUnknownAttribute(AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION, span); + } + return remoteOperation; + } + + private static generateRemoteService(span: ReadableSpan): string { + let remoteService: string = AwsSpanProcessingUtil.UNKNOWN_REMOTE_SERVICE; + + if (AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_NET_PEER_NAME)) { + remoteService = AwsMetricAttributeGenerator.getRemoteService(span, SEMATTRS_NET_PEER_NAME); + if (AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_NET_PEER_PORT)) { + const port: AttributeValue | undefined = span.attributes[SEMATTRS_NET_PEER_PORT]; + remoteService += ':' + (port as string); + } + } else if (AwsSpanProcessingUtil.isKeyPresent(span, _NET_SOCK_PEER_ADDR)) { + remoteService = AwsMetricAttributeGenerator.getRemoteService(span, _NET_SOCK_PEER_ADDR); + if (AwsSpanProcessingUtil.isKeyPresent(span, _NET_SOCK_PEER_PORT)) { + const port: AttributeValue | undefined = span.attributes[_NET_SOCK_PEER_PORT]; + remoteService += ':' + (port as string); + } + } else if (AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_HTTP_URL)) { + const httpUrl: string = span.attributes[SEMATTRS_HTTP_URL] as string; + try { + const url: URL = new URL(httpUrl); + if (url.hostname !== '') { + remoteService = url.hostname; + if (url.port !== '') { + remoteService += ':' + url.port; + } + } + } catch (e: unknown) { + diag.verbose(`invalid http.url attribute: ${httpUrl}`); + } + } else { + AwsMetricAttributeGenerator.logUnknownAttribute(AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE, span); + } + return remoteService; + } + + /** + * If the span is an AWS SDK span, normalize the name to align with AWS + * Cloud Control resource format as much as possible, with special attention to services we + * can detect remote resource information for. Long term, we would like to normalize service name + * in the upstream. + */ + private static normalizeRemoteServiceName(span: ReadableSpan, serviceName: string): string { + if (AwsSpanProcessingUtil.isAwsSDKSpan(span)) { + return 'AWS::' + serviceName; + } + return serviceName; + } + + /** + * Remote resource attributes {@link AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE} and + * {@link AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER} are used to store information about the + * resource associated with the remote invocation, such as S3 bucket name, etc. We should only + * ever set both type and identifier or neither. If any identifier value contains | or ^ , they + * will be replaced with ^| or ^^. + * + *

AWS resources type and identifier adhere to AWS + * Cloud Control resource format. + */ + private static setRemoteResourceTypeAndIdentifier(span: ReadableSpan, builder: Attributes): void { + let remoteResourceType: AttributeValue | undefined; + let remoteResourceIdentifier: AttributeValue | undefined; + + if (AwsSpanProcessingUtil.isAwsSDKSpan(span)) { + if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAME)) { + remoteResourceType = NORMALIZED_DYNAMO_DB_SERVICE_NAME + '::Table'; + remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters( + span.attributes[AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAME] + ); + } else if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_STREAM_NAME)) { + remoteResourceType = NORMALIZED_KINESIS_SERVICE_NAME + '::Stream'; + remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters( + span.attributes[AWS_ATTRIBUTE_KEYS.AWS_STREAM_NAME] + ); + } else if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_BUCKET_NAME)) { + remoteResourceType = NORMALIZED_S3_SERVICE_NAME + '::Bucket'; + remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters( + span.attributes[AWS_ATTRIBUTE_KEYS.AWS_BUCKET_NAME] + ); + } else if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_QUEUE_NAME)) { + remoteResourceType = NORMALIZED_SQS_SERVICE_NAME + '::Queue'; + remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters( + span.attributes[AWS_ATTRIBUTE_KEYS.AWS_QUEUE_NAME] + ); + } else if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_QUEUE_URL)) { + remoteResourceType = NORMALIZED_SQS_SERVICE_NAME + '::Queue'; + remoteResourceIdentifier = SqsUrlParser.getQueueName( + AwsMetricAttributeGenerator.escapeDelimiters(span.attributes[AWS_ATTRIBUTE_KEYS.AWS_QUEUE_URL]) + ); + } + } else if (AwsSpanProcessingUtil.isDBSpan(span)) { + remoteResourceType = DB_CONNECTION_RESOURCE_TYPE; + remoteResourceIdentifier = AwsMetricAttributeGenerator.getDbConnection(span); + } + + if (remoteResourceType !== undefined && remoteResourceIdentifier !== undefined) { + builder[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE] = remoteResourceType; + builder[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER] = remoteResourceIdentifier; + } + } + + /** + * RemoteResourceIdentifier is populated with rule + * ^[{db.name}|]?{address}[|{port}]? + * + * + *

+   * {address} attribute is retrieved in priority order:
+   * - {@link _SERVER_ADDRESS},
+   * - {@link SEMATTRS_NET_PEER_NAME},
+   * - {@link _SERVER_SOCKET_ADDRESS}
+   * - {@link SEMATTRS_DB_CONNECTION_STRING}-Hostname
+   * 
+ * + *
+   * {port} attribute is retrieved in priority order:
+   * - {@link _SERVER_PORT},
+   * - {@link SEMATTRS_NET_PEER_PORT},
+   * - {@link _SERVER_SOCKET_PORT}
+   * - {@link SEMATTRS_DB_CONNECTION_STRING}-Port
+   * 
+ * + * If address is not present, neither RemoteResourceType nor RemoteResourceIdentifier will be + * provided. + */ + private static getDbConnection(span: ReadableSpan): string | undefined { + const dbName: AttributeValue | undefined = span.attributes[SEMATTRS_DB_NAME]; + let dbConnection: string | undefined; + + if (AwsSpanProcessingUtil.isKeyPresent(span, _SERVER_ADDRESS)) { + const serverAddress: AttributeValue | undefined = span.attributes[_SERVER_ADDRESS]; + const serverPort: AttributeValue | undefined = span.attributes[_SERVER_PORT]; + dbConnection = AwsMetricAttributeGenerator.buildDbConnection(serverAddress, serverPort); + } else if (AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_NET_PEER_NAME)) { + const networkPeerAddress: AttributeValue | undefined = span.attributes[SEMATTRS_NET_PEER_NAME]; + const networkPeerPort: AttributeValue | undefined = span.attributes[SEMATTRS_NET_PEER_PORT]; + dbConnection = AwsMetricAttributeGenerator.buildDbConnection(networkPeerAddress, networkPeerPort); + } else if (AwsSpanProcessingUtil.isKeyPresent(span, _SERVER_SOCKET_ADDRESS)) { + const serverSocketAddress: AttributeValue | undefined = span.attributes[_SERVER_SOCKET_ADDRESS]; + const serverSocketPort: AttributeValue | undefined = span.attributes[_SERVER_SOCKET_PORT]; + dbConnection = AwsMetricAttributeGenerator.buildDbConnection(serverSocketAddress, serverSocketPort); + } else if (AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_DB_CONNECTION_STRING)) { + const connectionString: AttributeValue | undefined = span.attributes[SEMATTRS_DB_CONNECTION_STRING]; + dbConnection = AwsMetricAttributeGenerator.buildDbConnectionString(connectionString); + } + + // return empty resource identifier if db server is not found + if (dbConnection !== undefined && dbName !== undefined) { + return AwsMetricAttributeGenerator.escapeDelimiters(dbName) + '|' + dbConnection; + } + + return dbConnection; + } + + private static buildDbConnection( + address: AttributeValue | undefined, + port: AttributeValue | undefined + ): string | undefined { + if (address === undefined || port === undefined) { + return undefined; + } + + return AwsMetricAttributeGenerator.escapeDelimiters(address as string) + (port !== undefined ? '|' + port : ''); + } + + private static buildDbConnectionString(connectionString: AttributeValue | undefined): string | undefined { + if (connectionString === undefined) { + return undefined; + } + + let uri: URL; + let address: string; + let port: string; + try { + uri = new URL(connectionString as string); + + // Divergence from Java/Python + // `jdbc:://` isn't handled well with `new URL()` + // uri.host and uri.port will be empty strings + // examples: + // - jdbc:postgresql://host:port/database?properties + // - jdbc:mysql://localhost:3306 + // Try again with the substring after `jdbc:`) + if ((connectionString as string).startsWith(JDBC_PROTOCOL_PREFIX)) { + uri = new URL((connectionString as string).substring(JDBC_PROTOCOL_PREFIX.length)); + } + + address = uri.hostname; + port = uri.port; + } catch (error: unknown) { + diag.verbose(`invalid DB ConnectionString: ${connectionString}`); + return undefined; + } + + if (address === '') { + return undefined; + } + + return AwsMetricAttributeGenerator.escapeDelimiters(address) + (port !== '' ? '|' + port : ''); + } + + private static escapeDelimiters(input: string | AttributeValue | undefined): string | undefined { + if (input === undefined) { + return undefined; + } + + // Divergence from Java/Python + // `replaceAll(a,b)` is not available, and `replace(a,b)` only replaces the first occurrence + // `split(a).join(b)` is not equivalent for all (a,b), but works with `a = '^'` or a = '|'`. + // Implementing some regex is also possible + // e.g. let re = new RegExp(String.raw`\s${variable}\s`, "g"); + return (input as string).split('^').join('^^').split('|').join('^|'); + } + + /** Span kind is needed for differentiating metrics in the EMF exporter */ + private static setSpanKindForService(span: ReadableSpan, builder: Attributes): void { + let spanKind: string = SpanKind[span.kind]; + if (AwsSpanProcessingUtil.isLocalRoot(span)) { + spanKind = AwsSpanProcessingUtil.LOCAL_ROOT; + } + builder[AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND] = spanKind; + } + + private static setSpanKindForDependency(span: ReadableSpan, builder: Attributes): void { + const spanKind: string = SpanKind[span.kind]; + builder[AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND] = spanKind; + } + + private static setRemoteDbUser(span: ReadableSpan, builder: Attributes): void { + if (AwsSpanProcessingUtil.isDBSpan(span) && AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_DB_USER)) { + builder[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_DB_USER] = span.attributes[SEMATTRS_DB_USER]; + } + } + + private static getRemoteService(span: ReadableSpan, remoteServiceKey: string): string { + let remoteService: AttributeValue | undefined = span.attributes[remoteServiceKey]; + if (remoteService === undefined) { + remoteService = AwsSpanProcessingUtil.UNKNOWN_REMOTE_SERVICE; + } + return remoteService as string; + } + + private static getRemoteOperation(span: ReadableSpan, remoteOperationKey: string): string { + let remoteOperation: AttributeValue | undefined = span.attributes[remoteOperationKey]; + if (remoteOperation === undefined) { + remoteOperation = AwsSpanProcessingUtil.UNKNOWN_REMOTE_OPERATION; + } + return remoteOperation as string; + } + + /** + * If no db.operation attribute provided in the span, we use db.statement to compute a valid + * remote operation in a best-effort manner. To do this, we take the first substring of the + * statement and compare to a regex list of known SQL keywords. The substring length is determined + * by the longest known SQL keywords. + */ + private static getDBStatementRemoteOperation(span: ReadableSpan, remoteOperationKey: string): string { + let remoteOperation: AttributeValue | undefined = span.attributes[remoteOperationKey]; + if (remoteOperation === undefined) { + remoteOperation = AwsSpanProcessingUtil.UNKNOWN_REMOTE_OPERATION; + } + + // Remove all whitespace and newline characters from the beginning of remote_operation + // and retrieve the first MAX_KEYWORD_LENGTH characters + remoteOperation = (remoteOperation as string).trimStart(); + if (remoteOperation.length > AwsSpanProcessingUtil.MAX_KEYWORD_LENGTH) { + remoteOperation = remoteOperation.substring(0, AwsSpanProcessingUtil.MAX_KEYWORD_LENGTH); + } + + const matcher: RegExpMatchArray | null = remoteOperation + .toUpperCase() + .match(AwsSpanProcessingUtil.SQL_DIALECT_PATTERN); + if (matcher == null || matcher.length === 0) { + remoteOperation = AwsSpanProcessingUtil.UNKNOWN_REMOTE_OPERATION; + } else { + remoteOperation = matcher[0]; + } + + return remoteOperation; + } + + private static logUnknownAttribute(attributeKey: string, span: ReadableSpan): void { + diag.verbose(`No valid ${attributeKey} value found for ${SpanKind[span.kind]} span ${span.spanContext().spanId}`); + } +} diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/metric-attribute-generator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/metric-attribute-generator.ts new file mode 100644 index 0000000..7a9c469 --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/metric-attribute-generator.ts @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Attributes } from '@opentelemetry/api'; +import { Resource } from '@opentelemetry/resources'; +import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; + +export const SERVICE_METRIC: string = 'Service'; +export const DEPENDENCY_METRIC: string = 'Dependency'; + +export interface AttributeMap { + [attributeKey: string]: Attributes; +} + +/** + * Metric attribute generator defines an interface for classes that can generate specific attributes + * to be used by an {@link AwsSpanMetricsProcessor} to produce metrics and by + * {@link AwsMetricAttributesSpanExporter} to wrap the original span. + */ +export interface MetricAttributeGenerator { + /** + * Given a span and associated resource, produce meaningful metric attributes for metrics produced + * from the span. If no metrics should be generated from this span, return an empty Attributes={}. + * + * @param span - SpanData to be used to generate metric attributes. + * @param resource - Resource associated with Span to be used to generate metric attributes. + * @return A map of Attributes objects0 with values assigned to key "Service" or "Dependency". It + * will contain either 0, 1, or 2 items. + */ + generateMetricAttributeMapFromSpan(span: ReadableSpan, resource: Resource): AttributeMap; +} From d9d37825cefd9eeb0d6df2e614e0971dcecde6ab Mon Sep 17 00:00:00 2001 From: jjllee Date: Thu, 1 Aug 2024 18:32:43 -0700 Subject: [PATCH 2/8] add unit tests for AwsMetricAttributeGenerator --- .../src/aws-metric-attribute-generator.ts | 2 +- .../aws-metric-attribute-generator.test.ts | 1190 +++++++++++++++++ 2 files changed, 1191 insertions(+), 1 deletion(-) create mode 100644 aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts index b7a3caa..88002e5 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts @@ -434,7 +434,7 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { address: AttributeValue | undefined, port: AttributeValue | undefined ): string | undefined { - if (address === undefined || port === undefined) { + if (address === undefined) { return undefined; } diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts new file mode 100644 index 0000000..88d7f28 --- /dev/null +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts @@ -0,0 +1,1190 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AttributeValue, Attributes, SpanContext, SpanKind } from '@opentelemetry/api'; +import { InstrumentationLibrary } from '@opentelemetry/core'; +import { Resource } from '@opentelemetry/resources'; +import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; +import { + MESSAGINGOPERATIONVALUES_PROCESS, + SEMATTRS_DB_CONNECTION_STRING, + SEMATTRS_DB_NAME, + SEMATTRS_DB_OPERATION, + SEMATTRS_DB_STATEMENT, + SEMATTRS_DB_SYSTEM, + SEMATTRS_DB_USER, + SEMATTRS_FAAS_INVOKED_NAME, + SEMATTRS_FAAS_INVOKED_PROVIDER, + SEMATTRS_FAAS_TRIGGER, + SEMATTRS_HTTP_METHOD, + SEMATTRS_HTTP_TARGET, + SEMATTRS_HTTP_URL, + SEMATTRS_MESSAGING_OPERATION, + SEMATTRS_MESSAGING_SYSTEM, + SEMATTRS_NET_PEER_NAME, + SEMATTRS_NET_PEER_PORT, + SEMATTRS_PEER_SERVICE, + SEMATTRS_RPC_METHOD, + SEMATTRS_RPC_SERVICE, + SEMATTRS_RPC_SYSTEM, + SEMRESATTRS_SERVICE_NAME, +} from '@opentelemetry/semantic-conventions'; +import { expect } from 'expect'; +import { AWS_ATTRIBUTE_KEYS } from '../src/aws-attribute-keys'; +import { AwsMetricAttributeGenerator } from '../src/aws-metric-attribute-generator'; +import { AttributeMap, DEPENDENCY_METRIC, SERVICE_METRIC } from '../src/metric-attribute-generator'; + +// Does not exist in @opentelemetry/semantic-conventions +const _SERVER_SOCKET_ADDRESS: string = 'server.socket.address'; +const _SERVER_SOCKET_PORT: string = 'server.socket.port'; +const _NET_SOCK_PEER_ADDR: string = 'net.sock.peer.addr'; +const _NET_SOCK_PEER_PORT: string = 'net.sock.peer.port'; +// Alternatively, `import { SemanticAttributes } from '@opentelemetry/instrumentation-undici/build/src/enums/SemanticAttributes';` +// SemanticAttributes._SERVER_ADDRESS +// SemanticAttributes._SERVER_PORT +const _SERVER_ADDRESS: string = 'server.address'; +const _SERVER_PORT: string = 'server.port'; +// Alternatively, `import { AttributeNames } from '@opentelemetry/instrumentation-graphql/build/src/enums/AttributeNames';` +// AttributeNames.OPERATION_TYPE +const _GRAPHQL_OPERATION_TYPE: string = 'graphql.operation.type'; + +// String constants that are used many times in these tests. +const AWS_LOCAL_OPERATION_VALUE: string = 'AWS local operation'; +const AWS_REMOTE_SERVICE_VALUE: string = 'AWS remote service'; +const AWS_REMOTE_OPERATION_VALUE: string = 'AWS remote operation'; +const SERVICE_NAME_VALUE: string = 'Service name'; +const SPAN_NAME_VALUE: string = 'Span name'; +const UNKNOWN_SERVICE: string = 'UnknownService'; +const UNKNOWN_OPERATION: string = 'UnknownOperation'; +const UNKNOWN_REMOTE_SERVICE: string = 'UnknownRemoteService'; +const UNKNOWN_REMOTE_OPERATION: string = 'UnknownRemoteOperation'; +const INTERNAL_OPERATION: string = 'InternalOperation'; +const LOCAL_ROOT: string = 'LOCAL_ROOT'; + +const GENERATOR: AwsMetricAttributeGenerator = new AwsMetricAttributeGenerator(); + +let attributesMock: Attributes; +let spanDataMock: ReadableSpan; +let instrumentationScopeInfoMock: InstrumentationLibrary; +let resource: Resource; +let parentSpanContextMock: SpanContext; + +/** Unit tests for {@link AwsMetricAttributeGenerator}. */ +describe('AwsMetricAttributeGeneratorTest', () => { + // static class ThrowableWithMethodGetStatusCode extends Throwable { + // private final int httpStatusCode; + + // ThrowableWithMethodGetStatusCode(int httpStatusCode) { + // this.httpStatusCode = httpStatusCode; + // } + + // public int getStatusCode() { + // return this.httpStatusCode; + // } + // } + + // static class ThrowableWithMethodStatusCode extends Throwable { + // private final int httpStatusCode; + + // ThrowableWithMethodStatusCode(int httpStatusCode) { + // this.httpStatusCode = httpStatusCode; + // } + + // public int statusCode() { + // return this.httpStatusCode; + // } + // } + + // static class ThrowableWithoutStatusCode extends Throwable {} + + // setUpMocks + beforeEach(() => { + attributesMock = {}; + instrumentationScopeInfoMock = { + name: 'Scope name', + }; + spanDataMock = { + name: 'spanDataMockName', + kind: SpanKind.SERVER, + spanContext: () => { + const spanContext: SpanContext = { + traceId: 'traceId', + spanId: 'spanId', + traceFlags: 0, + }; + return spanContext; + }, + startTime: [0, 0], + endTime: [0, 1], + status: { code: 0 }, + attributes: attributesMock, + links: [], + events: [], + duration: [0, 1], + ended: true, + resource: Resource.default(), + instrumentationLibrary: instrumentationScopeInfoMock, + droppedAttributesCount: 0, + droppedEventsCount: 0, + droppedLinksCount: 0, + }; + // (spanDataMock as any).getAttributes() = attributesMock; + // (spanDataMock as any).getInstrumentationScopeInfo() = instrumentationScopeInfoMock; + //[][] (spanDataMock as any).getSpanContext() = mock(SpanContext.class); + parentSpanContextMock = { + traceId: 'traceId', + spanId: 'spanId', + traceFlags: 0, + }; + parentSpanContextMock.isRemote = false; + // Divergence from Java/Python - set AWS_IS_LOCAL_ROOT as false because parentSpanContext is valid and not remote in this test + attributesMock[AWS_ATTRIBUTE_KEYS.AWS_IS_LOCAL_ROOT] = false; + + // OTel strongly recommends to start out with the default instead of Resource.empty() + // In OTel JS, default Resource's default Service Name is `unknown_service:${process.argv0}` + // - https://github.com/open-telemetry/opentelemetry-js/blob/b2778e1b2ff7b038cebf371f1eb9f808fd98107f/packages/opentelemetry-resources/src/platform/node/default-service-name.ts#L16 + resource = Resource.default(); + }); + + it('testSpanAttributesForEmptyResource', () => { + resource = Resource.empty(); + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.SERVER], + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: UNKNOWN_SERVICE, + // This is tested to be UNKNOWN_OPERATION in Java/Python + // This is because in other langauges, span name could be null, but + // this is not possibly in OTel JS. + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: 'spanDataMockName', + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.SERVER); + }); + + it('testConsumerSpanWithoutAttributes', () => { + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.CONSUMER], + // This is tested to be UnknownService in Java/Python + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: `unknown_service:${process.argv0}`, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: UNKNOWN_OPERATION, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]: UNKNOWN_REMOTE_SERVICE, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION]: UNKNOWN_REMOTE_OPERATION, + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.CONSUMER); + }); + + it('testServerSpanWithoutAttributes', () => { + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.SERVER], + // This is tested to be UnknownService in Java/Python + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: `unknown_service:${process.argv0}`, + // This is tested to be UNKNOWN_OPERATION in Java/Python + // This is because in other langauges, span name could be null, but + // this is not possibly in OTel JS. + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: 'spanDataMockName', + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.SERVER); + }); + + it('testProducerSpanWithoutAttributes', () => { + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.PRODUCER], + // This is tested to be UnknownService in Java/Python + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: `unknown_service:${process.argv0}`, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: UNKNOWN_OPERATION, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]: UNKNOWN_REMOTE_SERVICE, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION]: UNKNOWN_REMOTE_OPERATION, + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.PRODUCER); + }); + + it('testClientSpanWithoutAttributes', () => { + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.CLIENT], + // This is tested to be UnknownService in Java/Python + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: `unknown_service:${process.argv0}`, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: UNKNOWN_OPERATION, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]: UNKNOWN_REMOTE_SERVICE, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION]: UNKNOWN_REMOTE_OPERATION, + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.CLIENT); + }); + + it('testInternalSpan', () => { + // Spans with internal span kind should not produce any attributes. + validateAttributesProducedForNonLocalRootSpanOfKind({}, SpanKind.INTERNAL); + }); + + it('testLocalRootServerSpan', () => { + updateResourceWithServiceName(); + // Divergence from Java/Python - set AWS_IS_LOCAL_ROOT as true because parentSpanContext is not valid in this test + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_IS_LOCAL_ROOT, true); + (spanDataMock as any).name = SPAN_NAME_VALUE; + + const expectedAttributesMap: AttributeMap = {}; + expectedAttributesMap[SERVICE_METRIC] = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: LOCAL_ROOT, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: SPAN_NAME_VALUE, + }; + + (spanDataMock as any).kind = SpanKind.SERVER; + const actualAttributesMap: AttributeMap = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource); + expect(actualAttributesMap).toEqual(expectedAttributesMap); + }); + + it('testLocalRootInternalSpan', () => { + updateResourceWithServiceName(); + // Divergence from Java/Python - set AWS_IS_LOCAL_ROOT as true because parentSpanContext is not valid in this test + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_IS_LOCAL_ROOT, true); + + (spanDataMock as any).name = SPAN_NAME_VALUE; + + const expectedAttributesMap: AttributeMap = {}; + expectedAttributesMap[SERVICE_METRIC] = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: LOCAL_ROOT, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: INTERNAL_OPERATION, + }; + + (spanDataMock as any).kind = SpanKind.INTERNAL; + const actualAttributesMap: AttributeMap = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource); + expect(actualAttributesMap).toEqual(expectedAttributesMap); + }); + + it('testLocalRootClientSpan', () => { + updateResourceWithServiceName(); + // Divergence from Java/Python - set AWS_IS_LOCAL_ROOT as true because parentSpanContext is not valid in this test + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_IS_LOCAL_ROOT, true); + (spanDataMock as any).name = SPAN_NAME_VALUE; + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE, AWS_REMOTE_SERVICE_VALUE); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION, AWS_REMOTE_OPERATION_VALUE); + + const expectedAttributesMap: AttributeMap = {}; + + expectedAttributesMap[SERVICE_METRIC] = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: LOCAL_ROOT, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: INTERNAL_OPERATION, + }; + expectedAttributesMap[DEPENDENCY_METRIC] = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.CLIENT], + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: INTERNAL_OPERATION, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]: AWS_REMOTE_SERVICE_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION]: AWS_REMOTE_OPERATION_VALUE, + }; + + (spanDataMock as any).kind = SpanKind.CLIENT; + const actualAttributesMap: AttributeMap = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource); + expect(actualAttributesMap).toEqual(expectedAttributesMap); + }); + + it('testLocalRootConsumerSpan', () => { + updateResourceWithServiceName(); + // Divergence from Java/Python - set AWS_IS_LOCAL_ROOT as true because parentSpanContext is not valid in this test + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_IS_LOCAL_ROOT, true); + (spanDataMock as any).name = SPAN_NAME_VALUE; + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE, AWS_REMOTE_SERVICE_VALUE); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION, AWS_REMOTE_OPERATION_VALUE); + + const expectedAttributesMap: AttributeMap = {}; + + expectedAttributesMap[SERVICE_METRIC] = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: LOCAL_ROOT, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: INTERNAL_OPERATION, + }; + + expectedAttributesMap[DEPENDENCY_METRIC] = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.CONSUMER], + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: INTERNAL_OPERATION, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]: AWS_REMOTE_SERVICE_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION]: AWS_REMOTE_OPERATION_VALUE, + }; + + (spanDataMock as any).kind = SpanKind.CONSUMER; + const actualAttributesMap: AttributeMap = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource); + expect(actualAttributesMap).toEqual(expectedAttributesMap); + }); + + it('testLocalRootProducerSpan', () => { + updateResourceWithServiceName(); + // Divergence from Java/Python - set AWS_IS_LOCAL_ROOT as true because parentSpanContext is not valid in this test + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_IS_LOCAL_ROOT, true); + (spanDataMock as any).name = SPAN_NAME_VALUE; + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE, AWS_REMOTE_SERVICE_VALUE); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION, AWS_REMOTE_OPERATION_VALUE); + + const expectedAttributesMap: AttributeMap = {}; + + expectedAttributesMap[SERVICE_METRIC] = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: LOCAL_ROOT, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: INTERNAL_OPERATION, + }; + + expectedAttributesMap[DEPENDENCY_METRIC] = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.PRODUCER], + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: INTERNAL_OPERATION, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]: AWS_REMOTE_SERVICE_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION]: AWS_REMOTE_OPERATION_VALUE, + }; + + (spanDataMock as any).kind = SpanKind.PRODUCER; + const actualAttributesMap: AttributeMap = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource); + expect(actualAttributesMap).toEqual(expectedAttributesMap); + }); + + it('testConsumerSpanWithAttributes', () => { + updateResourceWithServiceName(); + (spanDataMock as any).name = SPAN_NAME_VALUE; + + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.CONSUMER], + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: UNKNOWN_OPERATION, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]: UNKNOWN_REMOTE_SERVICE, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION]: UNKNOWN_REMOTE_OPERATION, + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.CONSUMER); + }); + + it('testServerSpanWithAttributes', () => { + updateResourceWithServiceName(); + (spanDataMock as any).name = SPAN_NAME_VALUE; + + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.SERVER], + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: SPAN_NAME_VALUE, + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.SERVER); + }); + + it('testServerSpanWithNullSpanName', () => { + updateResourceWithServiceName(); + (spanDataMock as any).name = null; + + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.SERVER], + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: UNKNOWN_OPERATION, + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.SERVER); + }); + + it('testServerSpanWithSpanNameAsHttpMethod', () => { + updateResourceWithServiceName(); + (spanDataMock as any).name = 'GET'; + mockAttribute(SEMATTRS_HTTP_METHOD, 'GET'); + + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.SERVER], + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: UNKNOWN_OPERATION, + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.SERVER); + mockAttribute(SEMATTRS_HTTP_METHOD, undefined); + }); + + it('testServerSpanWithSpanNameWithHttpTarget', () => { + updateResourceWithServiceName(); + (spanDataMock as any).name = 'POST'; + mockAttribute(SEMATTRS_HTTP_METHOD, 'POST'); + mockAttribute(SEMATTRS_HTTP_TARGET, '/payment/123'); + + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.SERVER], + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: 'POST /payment', + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.SERVER); + mockAttribute(SEMATTRS_HTTP_METHOD, undefined); + mockAttribute(SEMATTRS_HTTP_TARGET, undefined); + }); + + // when http.target & http.url are present, the local operation should be derived from the http.target + it('testServerSpanWithSpanNameWithTargetAndUrl', () => { + updateResourceWithServiceName(); + (spanDataMock as any).name = 'POST'; + mockAttribute(SEMATTRS_HTTP_METHOD, 'POST'); + mockAttribute(SEMATTRS_HTTP_TARGET, '/my-target/09876'); + mockAttribute(SEMATTRS_HTTP_URL, 'http://127.0.0.1:8000/payment/123'); + + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.SERVER], + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: 'POST /my-target', + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.SERVER); + mockAttribute(SEMATTRS_HTTP_METHOD, undefined); + mockAttribute(SEMATTRS_HTTP_TARGET, undefined); + mockAttribute(SEMATTRS_HTTP_URL, undefined); + }); + + it('testServerSpanWithSpanNameWithHttpUrl', () => { + updateResourceWithServiceName(); + (spanDataMock as any).name = 'POST'; + mockAttribute(SEMATTRS_HTTP_METHOD, 'POST'); + mockAttribute(SEMATTRS_HTTP_URL, 'http://127.0.0.1:8000/payment/123'); + + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.SERVER], + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: 'POST /payment', + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.SERVER); + mockAttribute(SEMATTRS_HTTP_METHOD, undefined); + mockAttribute(SEMATTRS_HTTP_URL, undefined); + }); + + // http.url with no path should result in local operation to be "POST /" + it('testServerSpanWithHttpUrlWithNoPath', () => { + updateResourceWithServiceName(); + (spanDataMock as any).name = 'POST'; + mockAttribute(SEMATTRS_HTTP_METHOD, 'POST'); + mockAttribute(SEMATTRS_HTTP_URL, 'http://www.example.com'); + + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.SERVER], + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: 'POST /', + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.SERVER); + mockAttribute(SEMATTRS_HTTP_METHOD, undefined); + mockAttribute(SEMATTRS_HTTP_URL, undefined); + }); + + // if http.url is none, local operation should default to UnknownOperation + it('testServerSpanWithHttpUrlAsNone', () => { + updateResourceWithServiceName(); + (spanDataMock as any).name = 'POST'; + mockAttribute(SEMATTRS_HTTP_METHOD, 'POST'); + mockAttribute(SEMATTRS_HTTP_URL, undefined); + + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.SERVER], + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: UNKNOWN_OPERATION, + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.SERVER); + mockAttribute(SEMATTRS_HTTP_METHOD, undefined); + mockAttribute(SEMATTRS_HTTP_URL, undefined); + }); + + // if http.url is empty, local operation should default to "POST /" + it('testServerSpanWithHttpUrlAsEmpty', () => { + updateResourceWithServiceName(); + (spanDataMock as any).name = 'POST'; + mockAttribute(SEMATTRS_HTTP_METHOD, 'POST'); + mockAttribute(SEMATTRS_HTTP_URL, ''); + + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.SERVER], + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: 'POST /', + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.SERVER); + mockAttribute(SEMATTRS_HTTP_METHOD, undefined); + mockAttribute(SEMATTRS_HTTP_URL, undefined); + }); + + // if http.url is invalid, local operation should default to "POST /" + it('testServerSpanWithHttpUrlAsInvalid', () => { + updateResourceWithServiceName(); + (spanDataMock as any).name = 'POST'; + mockAttribute(SEMATTRS_HTTP_METHOD, 'POST'); + mockAttribute(SEMATTRS_HTTP_URL, 'invalid_url'); + + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.SERVER], + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: 'POST /', + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.SERVER); + mockAttribute(SEMATTRS_HTTP_METHOD, undefined); + mockAttribute(SEMATTRS_HTTP_URL, undefined); + }); + + it('testProducerSpanWithAttributes', () => { + updateResourceWithServiceName(); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION, AWS_LOCAL_OPERATION_VALUE); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE, AWS_REMOTE_SERVICE_VALUE); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION, AWS_REMOTE_OPERATION_VALUE); + + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.PRODUCER], + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: AWS_LOCAL_OPERATION_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]: AWS_REMOTE_SERVICE_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION]: AWS_REMOTE_OPERATION_VALUE, + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.PRODUCER); + }); + + it('testClientSpanWithAttributes', () => { + updateResourceWithServiceName(); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION, AWS_LOCAL_OPERATION_VALUE); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE, AWS_REMOTE_SERVICE_VALUE); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION, AWS_REMOTE_OPERATION_VALUE); + + const expectedAttributes: Attributes = { + [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.CLIENT], + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: SERVICE_NAME_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: AWS_LOCAL_OPERATION_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]: AWS_REMOTE_SERVICE_VALUE, + [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION]: AWS_REMOTE_OPERATION_VALUE, + }; + validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes, SpanKind.CLIENT); + }); + + it('testRemoteAttributesCombinations', () => { + // Set all expected fields to a test string, we will overwrite them in descending order to test + // the priority-order logic in AwsMetricAttributeGenerator remote attribute methods. + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE, 'TestString'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION, 'TestString'); + mockAttribute(SEMATTRS_RPC_SERVICE, 'TestString'); + mockAttribute(SEMATTRS_RPC_METHOD, 'TestString'); + mockAttribute(SEMATTRS_DB_SYSTEM, 'TestString'); + mockAttribute(SEMATTRS_DB_OPERATION, 'TestString'); + mockAttribute(SEMATTRS_DB_STATEMENT, 'TestString'); + mockAttribute(SEMATTRS_FAAS_INVOKED_PROVIDER, 'TestString'); + mockAttribute(SEMATTRS_FAAS_INVOKED_NAME, 'TestString'); + mockAttribute(SEMATTRS_MESSAGING_SYSTEM, 'TestString'); + mockAttribute(SEMATTRS_MESSAGING_OPERATION, 'TestString'); + mockAttribute(_GRAPHQL_OPERATION_TYPE, 'TestString'); + // Do not set dummy value for SEMATTRS_PEER_SERVICE, since it has special behaviour. + + // Two unused attributes to show that we will not make use of unrecognized attributes + mockAttribute('unknown.service.key', 'TestString'); + mockAttribute('unknown.operation.key', 'TestString'); + + // Validate behaviour of various combinations of AWS remote attributes, then remove them. + validateAndRemoveRemoteAttributes( + AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE, + AWS_REMOTE_SERVICE_VALUE, + AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION, + AWS_REMOTE_OPERATION_VALUE + ); + + // Validate behaviour of various combinations of RPC attributes, then remove them. + validateAndRemoveRemoteAttributes(SEMATTRS_RPC_SERVICE, 'RPC service', SEMATTRS_RPC_METHOD, 'RPC method'); + + // Validate behaviour of various combinations of DB attributes, then remove them. + validateAndRemoveRemoteAttributes(SEMATTRS_DB_SYSTEM, 'DB system', SEMATTRS_DB_OPERATION, 'DB operation'); + + // Validate db.operation not exist, but db.statement exist, where SpanAttributes.SEMATTRS_DB_STATEMENT is + // invalid + mockAttribute(SEMATTRS_DB_SYSTEM, 'DB system'); + mockAttribute(SEMATTRS_DB_STATEMENT, 'invalid DB statement'); + mockAttribute(SEMATTRS_DB_OPERATION, undefined); + validateAndRemoveRemoteAttributes(SEMATTRS_DB_SYSTEM, 'DB system', SEMATTRS_DB_OPERATION, UNKNOWN_REMOTE_OPERATION); + + // Validate both db.operation and db.statement not exist. + mockAttribute(SEMATTRS_DB_SYSTEM, 'DB system'); + mockAttribute(SEMATTRS_DB_OPERATION, undefined); + mockAttribute(SEMATTRS_DB_STATEMENT, undefined); + validateAndRemoveRemoteAttributes(SEMATTRS_DB_SYSTEM, 'DB system', SEMATTRS_DB_OPERATION, UNKNOWN_REMOTE_OPERATION); + + // Validate behaviour of various combinations of FAAS attributes, then remove them. + validateAndRemoveRemoteAttributes( + SEMATTRS_FAAS_INVOKED_NAME, + 'FAAS invoked name', + SEMATTRS_FAAS_TRIGGER, + 'FAAS trigger name' + ); + + // Validate behaviour of various combinations of Messaging attributes, then remove them. + validateAndRemoveRemoteAttributes( + SEMATTRS_MESSAGING_SYSTEM, + 'Messaging system', + SEMATTRS_MESSAGING_OPERATION, + 'Messaging operation' + ); + + // Validate behaviour of GraphQL operation type attribute, then remove it. + mockAttribute(_GRAPHQL_OPERATION_TYPE, 'GraphQL operation type'); + validateExpectedRemoteAttributes('graphql', 'GraphQL operation type'); + mockAttribute(_GRAPHQL_OPERATION_TYPE, undefined); + + // Validate behaviour of extracting Remote Service from net.peer.name + mockAttribute(SEMATTRS_NET_PEER_NAME, 'www.example.com'); + validateExpectedRemoteAttributes('www.example.com', UNKNOWN_REMOTE_OPERATION); + mockAttribute(SEMATTRS_NET_PEER_NAME, undefined); + + // Validate behaviour of extracting Remote Service from net.peer.name and net.peer.port + mockAttribute(SEMATTRS_NET_PEER_NAME, '192.168.0.0'); + mockAttribute(SEMATTRS_NET_PEER_PORT, 8081); + validateExpectedRemoteAttributes('192.168.0.0:8081', UNKNOWN_REMOTE_OPERATION); + mockAttribute(SEMATTRS_NET_PEER_NAME, undefined); + mockAttribute(SEMATTRS_NET_PEER_PORT, undefined); + + // Validate behaviour of extracting Remote Service from net.peer.socket.addr + mockAttribute(_NET_SOCK_PEER_ADDR, 'www.example.com'); + validateExpectedRemoteAttributes('www.example.com', UNKNOWN_REMOTE_OPERATION); + mockAttribute(_NET_SOCK_PEER_ADDR, undefined); + + // Validate behaviour of extracting Remote Service from net.peer.socket.addr and + // net.sock.peer.port + mockAttribute(_NET_SOCK_PEER_ADDR, '192.168.0.0'); + mockAttribute(_NET_SOCK_PEER_PORT, 8081); + validateExpectedRemoteAttributes('192.168.0.0:8081', UNKNOWN_REMOTE_OPERATION); + mockAttribute(_NET_SOCK_PEER_ADDR, undefined); + mockAttribute(_NET_SOCK_PEER_PORT, undefined); + + // Validate behavior of Remote Operation from HttpTarget - with 1st api part. Also validates + // that RemoteService is extracted from HttpUrl. + mockAttribute(SEMATTRS_HTTP_URL, 'http://www.example.com/payment/123'); + validateExpectedRemoteAttributes('www.example.com', '/payment'); + mockAttribute(SEMATTRS_HTTP_URL, undefined); + + // Validate behavior of Remote Operation from HttpTarget - with 1st api part. Also validates + // that RemoteService is extracted from HttpUrl. + mockAttribute(SEMATTRS_HTTP_URL, 'http://www.example.com'); + validateExpectedRemoteAttributes('www.example.com', '/'); + mockAttribute(SEMATTRS_HTTP_URL, undefined); + + // Validate behavior of Remote Service from HttpUrl + mockAttribute(SEMATTRS_HTTP_URL, 'http://192.168.1.1:8000'); + validateExpectedRemoteAttributes('192.168.1.1:8000', '/'); + mockAttribute(SEMATTRS_HTTP_URL, undefined); + + // Validate behavior of Remote Service from HttpUrl + mockAttribute(SEMATTRS_HTTP_URL, 'http://192.168.1.1'); + validateExpectedRemoteAttributes('192.168.1.1', '/'); + mockAttribute(SEMATTRS_HTTP_URL, undefined); + + // Validate behavior of Remote Service from HttpUrl + mockAttribute(SEMATTRS_HTTP_URL, ''); + validateExpectedRemoteAttributes(UNKNOWN_REMOTE_SERVICE, UNKNOWN_REMOTE_OPERATION); + mockAttribute(SEMATTRS_HTTP_URL, undefined); + + // Validate behavior of Remote Service from HttpUrl + mockAttribute(SEMATTRS_HTTP_URL, undefined); + validateExpectedRemoteAttributes(UNKNOWN_REMOTE_SERVICE, UNKNOWN_REMOTE_OPERATION); + mockAttribute(SEMATTRS_HTTP_URL, undefined); + + // Validate behavior of Remote Operation from HttpTarget - invalid url, then remove it + mockAttribute(SEMATTRS_HTTP_URL, 'abc'); + validateExpectedRemoteAttributes(UNKNOWN_REMOTE_SERVICE, UNKNOWN_REMOTE_OPERATION); + mockAttribute(SEMATTRS_HTTP_URL, undefined); + + // Validate behaviour of Peer service attribute, then remove it. + mockAttribute(SEMATTRS_PEER_SERVICE, 'Peer service'); + validateExpectedRemoteAttributes('Peer service', UNKNOWN_REMOTE_OPERATION); + mockAttribute(SEMATTRS_PEER_SERVICE, undefined); + + // Once we have removed all usable metrics, we only have "unknown" attributes, which are unused. + validateExpectedRemoteAttributes(UNKNOWN_REMOTE_SERVICE, UNKNOWN_REMOTE_OPERATION); + }); + + // Validate behaviour of various combinations of DB attributes). + it('testGetDBStatementRemoteOperation', () => { + // Set all expected fields to a test string, we will overwrite them in descending order to test + mockAttribute(SEMATTRS_DB_SYSTEM, 'TestString'); + mockAttribute(SEMATTRS_DB_OPERATION, 'TestString'); + mockAttribute(SEMATTRS_DB_STATEMENT, 'TestString'); + + // Validate SpanAttributes.SEMATTRS_DB_OPERATION not exist, but SpanAttributes.SEMATTRS_DB_STATEMENT exist, + // where SpanAttributes.SEMATTRS_DB_STATEMENT is valid + // Case 1: Only 1 valid keywords match + mockAttribute(SEMATTRS_DB_SYSTEM, 'DB system'); + mockAttribute(SEMATTRS_DB_STATEMENT, 'SELECT DB statement'); + mockAttribute(SEMATTRS_DB_OPERATION, undefined); + validateExpectedRemoteAttributes('DB system', 'SELECT'); + + // Case 2: More than 1 valid keywords match, we want to pick the longest match + mockAttribute(SEMATTRS_DB_SYSTEM, 'DB system'); + mockAttribute(SEMATTRS_DB_STATEMENT, 'DROP VIEW DB statement'); + mockAttribute(SEMATTRS_DB_OPERATION, undefined); + validateExpectedRemoteAttributes('DB system', 'DROP VIEW'); + + // Case 3: More than 1 valid keywords match, but the other keywords is not + // at the start of the SpanAttributes.SEMATTRS_DB_STATEMENT. We want to only pick start match + mockAttribute(SEMATTRS_DB_SYSTEM, 'DB system'); + mockAttribute(SEMATTRS_DB_STATEMENT, 'SELECT data FROM domains'); + mockAttribute(SEMATTRS_DB_OPERATION, undefined); + validateExpectedRemoteAttributes('DB system', 'SELECT'); + + // Case 4: Have valid keywords,but it is not at the start of SpanAttributes.SEMATTRS_DB_STATEMENT + mockAttribute(SEMATTRS_DB_SYSTEM, 'DB system'); + mockAttribute(SEMATTRS_DB_STATEMENT, 'invalid SELECT DB statement'); + mockAttribute(SEMATTRS_DB_OPERATION, undefined); + validateExpectedRemoteAttributes('DB system', UNKNOWN_REMOTE_OPERATION); + + // Case 5: Have valid keywords, match the longest word + mockAttribute(SEMATTRS_DB_SYSTEM, 'DB system'); + mockAttribute(SEMATTRS_DB_STATEMENT, 'UUID'); + mockAttribute(SEMATTRS_DB_OPERATION, undefined); + validateExpectedRemoteAttributes('DB system', 'UUID'); + + // Case 6: Have valid keywords, match with first word + mockAttribute(SEMATTRS_DB_SYSTEM, 'DB system'); + mockAttribute(SEMATTRS_DB_STATEMENT, 'FROM SELECT *'); + mockAttribute(SEMATTRS_DB_OPERATION, undefined); + validateExpectedRemoteAttributes('DB system', 'FROM'); + + // Case 7: Have valid keyword, match with first word + mockAttribute(SEMATTRS_DB_SYSTEM, 'DB system'); + mockAttribute(SEMATTRS_DB_STATEMENT, 'SELECT FROM *'); + mockAttribute(SEMATTRS_DB_OPERATION, undefined); + validateExpectedRemoteAttributes('DB system', 'SELECT'); + + // Case 8: Have valid keywords, match with upper case + mockAttribute(SEMATTRS_DB_SYSTEM, 'DB system'); + mockAttribute(SEMATTRS_DB_STATEMENT, 'seLeCt *'); + mockAttribute(SEMATTRS_DB_OPERATION, undefined); + validateExpectedRemoteAttributes('DB system', 'SELECT'); + + // Case 9: Both SEMATTRS_DB_OPERATION and SEMATTRS_DB_STATEMENT are set but the former takes precedence + mockAttribute(SEMATTRS_DB_SYSTEM, 'DB system'); + mockAttribute(SEMATTRS_DB_STATEMENT, 'SELECT FROM *'); + mockAttribute(SEMATTRS_DB_OPERATION, 'DB operation'); + validateExpectedRemoteAttributes('DB system', 'DB operation'); + }); + + it('testPeerServiceDoesOverrideOtherRemoteServices', () => { + validatePeerServiceDoesOverride(SEMATTRS_RPC_SERVICE); + validatePeerServiceDoesOverride(SEMATTRS_DB_SYSTEM); + validatePeerServiceDoesOverride(SEMATTRS_FAAS_INVOKED_PROVIDER); + validatePeerServiceDoesOverride(SEMATTRS_MESSAGING_SYSTEM); + validatePeerServiceDoesOverride(_GRAPHQL_OPERATION_TYPE); + validatePeerServiceDoesOverride(SEMATTRS_NET_PEER_NAME); + validatePeerServiceDoesOverride(_NET_SOCK_PEER_ADDR); + // Actually testing that peer service overrides "UnknownRemoteService". + validatePeerServiceDoesOverride('unknown.service.key'); + }); + + it('testPeerServiceDoesNotOverrideAwsRemoteService', () => { + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE, 'TestString'); + mockAttribute(SEMATTRS_PEER_SERVICE, 'PeerService'); + + (spanDataMock as any).kind = SpanKind.CLIENT; + const actualAttributes: Attributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[ + DEPENDENCY_METRIC + ]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]).toEqual('TestString'); + }); + + it('testSdkClientSpanWithRemoteResourceAttributes', () => { + mockAttribute(SEMATTRS_RPC_SYSTEM, 'aws-api'); + // Validate behaviour of aws bucket name attribute, then remove it. + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BUCKET_NAME, 'aws_s3_bucket_name'); + validateRemoteResourceAttributes('AWS::S3::Bucket', 'aws_s3_bucket_name'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_BUCKET_NAME, undefined); + + // Validate behaviour of AWS_QUEUE_NAME attribute, then remove it. + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_QUEUE_NAME, 'aws_queue_name'); + validateRemoteResourceAttributes('AWS::SQS::Queue', 'aws_queue_name'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_QUEUE_NAME, undefined); + + // Validate behaviour of having both AWS_QUEUE_NAME and AWS_QUEUE_URL attribute, then remove + // them. Queue name is more reliable than queue URL, so we prefer to use name over URL. + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_QUEUE_URL, 'https://sqs.us-east-2.amazonaws.com/123456789012/Queue'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_QUEUE_NAME, 'aws_queue_name'); + validateRemoteResourceAttributes('AWS::SQS::Queue', 'aws_queue_name'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_QUEUE_URL, undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_QUEUE_NAME, undefined); + + // Valid queue name with invalid queue URL, we should default to using the queue name. + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_QUEUE_URL, 'invalidUrl'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_QUEUE_NAME, 'aws_queue_name'); + validateRemoteResourceAttributes('AWS::SQS::Queue', 'aws_queue_name'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_QUEUE_URL, undefined); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_QUEUE_NAME, undefined); + + // Validate behaviour of AWS_STREAM_NAME attribute, then remove it. + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_STREAM_NAME, 'aws_stream_name'); + validateRemoteResourceAttributes('AWS::Kinesis::Stream', 'aws_stream_name'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_STREAM_NAME, undefined); + + // Validate behaviour of AWS_TABLE_NAME attribute, then remove it. + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAME, 'aws_table_name'); + validateRemoteResourceAttributes('AWS::DynamoDB::Table', 'aws_table_name'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAME, undefined); + + // Validate behaviour of AWS_TABLE_NAME attribute with special chars(|), then remove it. + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAME, 'aws_table|name'); + validateRemoteResourceAttributes('AWS::DynamoDB::Table', 'aws_table^|name'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAME, undefined); + + // Validate behaviour of AWS_TABLE_NAME attribute with special chars(^), then remove it. + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAME, 'aws_table^name'); + validateRemoteResourceAttributes('AWS::DynamoDB::Table', 'aws_table^^name'); + mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_TABLE_NAME, undefined); + }); + + it('testDBClientSpanWithRemoteResourceAttributes', () => { + mockAttribute(SEMATTRS_DB_SYSTEM, 'mysql'); + // Validate behaviour of SEMATTRS_DB_NAME, _SERVER_ADDRESS and _SERVER_PORT exist, then remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name'); + mockAttribute(_SERVER_ADDRESS, 'abc.com'); + mockAttribute(_SERVER_PORT, 3306); + validateRemoteResourceAttributes('DB::Connection', 'db_name|abc.com|3306'); + mockAttribute(SEMATTRS_DB_NAME, undefined); + mockAttribute(_SERVER_ADDRESS, undefined); + mockAttribute(_SERVER_PORT, undefined); + + // Validate behaviour of SEMATTRS_DB_NAME with '|' char, _SERVER_ADDRESS and _SERVER_PORT exist, then + // remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name|special'); + mockAttribute(_SERVER_ADDRESS, 'abc.com'); + mockAttribute(_SERVER_PORT, 3306); + validateRemoteResourceAttributes('DB::Connection', 'db_name^|special|abc.com|3306'); + mockAttribute(SEMATTRS_DB_NAME, undefined); + mockAttribute(_SERVER_ADDRESS, undefined); + mockAttribute(_SERVER_PORT, undefined); + + // Validate behaviour of SEMATTRS_DB_NAME with '^' char, _SERVER_ADDRESS and _SERVER_PORT exist, then + // remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name^special'); + mockAttribute(_SERVER_ADDRESS, 'abc.com'); + mockAttribute(_SERVER_PORT, 3306); + validateRemoteResourceAttributes('DB::Connection', 'db_name^^special|abc.com|3306'); + mockAttribute(SEMATTRS_DB_NAME, undefined); + mockAttribute(_SERVER_ADDRESS, undefined); + mockAttribute(_SERVER_PORT, undefined); + + // Validate behaviour of SEMATTRS_DB_NAME, _SERVER_ADDRESS exist, then remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name'); + mockAttribute(_SERVER_ADDRESS, 'abc.com'); + validateRemoteResourceAttributes('DB::Connection', 'db_name|abc.com'); + mockAttribute(SEMATTRS_DB_NAME, undefined); + mockAttribute(_SERVER_ADDRESS, undefined); + + // Validate behaviour of _SERVER_ADDRESS exist, then remove it. + mockAttribute(_SERVER_ADDRESS, 'abc.com'); + validateRemoteResourceAttributes('DB::Connection', 'abc.com'); + mockAttribute(_SERVER_ADDRESS, undefined); + + // Validate behaviour of _SERVER_PORT exist, then remove it. + mockAttribute(_SERVER_PORT, 3306); + (spanDataMock as any).kind = SpanKind.CLIENT; + let actualAttributes: Attributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[ + DEPENDENCY_METRIC + ]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE]).toBeUndefined(); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER]).toBeUndefined(); + mockAttribute(_SERVER_PORT, undefined); + + // Validate behaviour of SEMATTRS_DB_NAME, SEMATTRS_NET_PEER_NAME and SEMATTRS_NET_PEER_PORT exist, then remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name'); + mockAttribute(SEMATTRS_NET_PEER_NAME, 'abc.com'); + mockAttribute(SEMATTRS_NET_PEER_PORT, 3306); + validateRemoteResourceAttributes('DB::Connection', 'db_name|abc.com|3306'); + mockAttribute(SEMATTRS_DB_NAME, undefined); + mockAttribute(SEMATTRS_NET_PEER_NAME, undefined); + mockAttribute(SEMATTRS_NET_PEER_PORT, undefined); + + // Validate behaviour of SEMATTRS_DB_NAME, SEMATTRS_NET_PEER_NAME exist, then remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name'); + mockAttribute(SEMATTRS_NET_PEER_NAME, 'abc.com'); + validateRemoteResourceAttributes('DB::Connection', 'db_name|abc.com'); + mockAttribute(SEMATTRS_DB_NAME, undefined); + mockAttribute(SEMATTRS_NET_PEER_NAME, undefined); + + // Validate behaviour of SEMATTRS_NET_PEER_NAME exist, then remove it. + mockAttribute(SEMATTRS_NET_PEER_NAME, 'abc.com'); + validateRemoteResourceAttributes('DB::Connection', 'abc.com'); + mockAttribute(SEMATTRS_NET_PEER_NAME, undefined); + + // Validate behaviour of SEMATTRS_NET_PEER_PORT exist, then remove it. + mockAttribute(SEMATTRS_NET_PEER_PORT, 3306); + (spanDataMock as any).kind = SpanKind.CLIENT; + actualAttributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[DEPENDENCY_METRIC]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE]).toBeUndefined(); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER]).toBeUndefined(); + mockAttribute(SEMATTRS_NET_PEER_PORT, undefined); + + // Validate behaviour of SEMATTRS_DB_NAME, _SERVER_SOCKET_ADDRESS and _SERVER_SOCKET_PORT exist, then + // remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name'); + mockAttribute(_SERVER_SOCKET_ADDRESS, 'abc.com'); + mockAttribute(_SERVER_SOCKET_PORT, 3306); + validateRemoteResourceAttributes('DB::Connection', 'db_name|abc.com|3306'); + mockAttribute(SEMATTRS_DB_NAME, undefined); + mockAttribute(_SERVER_SOCKET_ADDRESS, undefined); + mockAttribute(_SERVER_SOCKET_PORT, undefined); + + // Validate behaviour of SEMATTRS_DB_NAME, _SERVER_SOCKET_ADDRESS exist, then remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name'); + mockAttribute(_SERVER_SOCKET_ADDRESS, 'abc.com'); + validateRemoteResourceAttributes('DB::Connection', 'db_name|abc.com'); + mockAttribute(SEMATTRS_DB_NAME, undefined); + mockAttribute(_SERVER_SOCKET_ADDRESS, undefined); + + // Validate behaviour of _SERVER_SOCKET_PORT exist, then remove it. + mockAttribute(_SERVER_SOCKET_PORT, 3306); + (spanDataMock as any).kind = SpanKind.CLIENT; + actualAttributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[DEPENDENCY_METRIC]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE]).toBeUndefined(); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER]).toBeUndefined(); + mockAttribute(_SERVER_SOCKET_PORT, undefined); + + // Validate behaviour of only SEMATTRS_DB_NAME exist, then remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name'); + (spanDataMock as any).kind = SpanKind.CLIENT; + actualAttributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[DEPENDENCY_METRIC]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE]).toBeUndefined(); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER]).toBeUndefined(); + mockAttribute(SEMATTRS_DB_NAME, undefined); + + // Validate behaviour of SEMATTRS_DB_NAME and SEMATTRS_DB_CONNECTION_STRING exist, then remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name'); + mockAttribute( + SEMATTRS_DB_CONNECTION_STRING, + 'mysql://test-apm.cluster-cnrw3s3ddo7n.us-east-1.rds.amazonaws.com:3306/petclinic' + ); + validateRemoteResourceAttributes( + 'DB::Connection', + 'db_name|test-apm.cluster-cnrw3s3ddo7n.us-east-1.rds.amazonaws.com|3306' + ); + mockAttribute(SEMATTRS_DB_NAME, undefined); + mockAttribute(SEMATTRS_DB_CONNECTION_STRING, undefined); + + // Validate behaviour of SEMATTRS_DB_CONNECTION_STRING exist, then remove it. + mockAttribute( + SEMATTRS_DB_CONNECTION_STRING, + 'mysql://test-apm.cluster-cnrw3s3ddo7n.us-east-1.rds.amazonaws.com:3306/petclinic' + ); + validateRemoteResourceAttributes( + 'DB::Connection', + 'test-apm.cluster-cnrw3s3ddo7n.us-east-1.rds.amazonaws.com|3306' + ); + mockAttribute(SEMATTRS_DB_CONNECTION_STRING, undefined); + + // Validate behaviour of SEMATTRS_DB_CONNECTION_STRING exist without port, then remove it. + mockAttribute(SEMATTRS_DB_CONNECTION_STRING, 'http://dbserver'); + validateRemoteResourceAttributes('DB::Connection', 'dbserver'); + mockAttribute(SEMATTRS_DB_CONNECTION_STRING, undefined); + + // Validate behaviour of SEMATTRS_DB_NAME and invalid SEMATTRS_DB_CONNECTION_STRING exist, then remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name'); + mockAttribute(SEMATTRS_DB_CONNECTION_STRING, 'hsqldb:mem:'); + (spanDataMock as any).kind = SpanKind.CLIENT; + actualAttributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[DEPENDENCY_METRIC]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE]).toBeUndefined(); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER]).toBeUndefined(); + mockAttribute(SEMATTRS_DB_NAME, undefined); + mockAttribute(SEMATTRS_DB_CONNECTION_STRING, undefined); + + mockAttribute(SEMATTRS_DB_SYSTEM, undefined); + }); + + function mockAttribute(key: string, value: AttributeValue | undefined): void { + attributesMock[key] = value; + } + + function validateAttributesProducedForNonLocalRootSpanOfKind(expectedAttributes: Attributes, kind: SpanKind): void { + (spanDataMock as any).kind = kind; + const attributeMap: AttributeMap = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource); + const serviceAttributes: Attributes = attributeMap[SERVICE_METRIC]; + const dependencyAttributes: Attributes = attributeMap[DEPENDENCY_METRIC]; + if (Object.keys(attributeMap).length !== 0) { + if (SpanKind.PRODUCER === kind || SpanKind.CLIENT === kind || SpanKind.CONSUMER === kind) { + expect(serviceAttributes).toBeUndefined(); + expect(dependencyAttributes).toEqual(expectedAttributes); + expect(Object.keys(dependencyAttributes).length).toEqual(Object.keys(expectedAttributes).length); + } else { + expect(serviceAttributes).toEqual(expectedAttributes); + expect(Object.keys(serviceAttributes).length).toEqual(Object.keys(expectedAttributes).length); + expect(dependencyAttributes).toBeUndefined(); + } + } + } + + function updateResourceWithServiceName(): void { + resource.attributes[SEMRESATTRS_SERVICE_NAME] = SERVICE_NAME_VALUE; + } + + function validateExpectedRemoteAttributes(expectedRemoteService: string, expectedRemoteOperation: string): void { + (spanDataMock as any).kind = SpanKind.CLIENT; + let actualAttributes: Attributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[ + DEPENDENCY_METRIC + ]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]).toEqual(expectedRemoteService); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION]).toEqual(expectedRemoteOperation); + + (spanDataMock as any).kind = SpanKind.PRODUCER; + actualAttributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[DEPENDENCY_METRIC]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]).toEqual(expectedRemoteService); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION]).toEqual(expectedRemoteOperation); + } + + function validateAndRemoveRemoteAttributes( + remoteServiceKey: string, + remoteServiceValue: string, + remoteOperationKey: string, + remoteOperationValue: string + ): void { + mockAttribute(remoteServiceKey, remoteServiceValue); + mockAttribute(remoteOperationKey, remoteOperationValue); + validateExpectedRemoteAttributes(remoteServiceValue, remoteOperationValue); + + mockAttribute(remoteServiceKey, undefined); + mockAttribute(remoteOperationKey, remoteOperationValue); + validateExpectedRemoteAttributes(UNKNOWN_REMOTE_SERVICE, remoteOperationValue); + + mockAttribute(remoteServiceKey, remoteServiceValue); + mockAttribute(remoteOperationKey, undefined); + validateExpectedRemoteAttributes(remoteServiceValue, UNKNOWN_REMOTE_OPERATION); + + mockAttribute(remoteServiceKey, undefined); + mockAttribute(remoteOperationKey, undefined); + } + + function validatePeerServiceDoesOverride(remoteServiceKey: string): void { + mockAttribute(remoteServiceKey, 'TestString'); + mockAttribute(SEMATTRS_PEER_SERVICE, 'PeerService'); + + // Validate that peer service value takes precedence over whatever remoteServiceKey was set + (spanDataMock as any).kind = SpanKind.CLIENT; + const actualAttributes: Attributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[ + DEPENDENCY_METRIC + ]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]).toEqual('PeerService'); + + mockAttribute(remoteServiceKey, undefined); + mockAttribute(SEMATTRS_PEER_SERVICE, undefined); + } + + function validateRemoteResourceAttributes(type: string, identifier: string): void { + // Client, Producer and Consumer spans should generate the expected remote resource attributes + (spanDataMock as any).kind = SpanKind.CLIENT; + let actualAttributes: Attributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[ + DEPENDENCY_METRIC + ]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE]).toEqual(type); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER]).toEqual(identifier); + + (spanDataMock as any).kind = SpanKind.PRODUCER; + actualAttributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[DEPENDENCY_METRIC]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE]).toEqual(type); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER]).toEqual(identifier); + + (spanDataMock as any).kind = SpanKind.CONSUMER; + actualAttributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[DEPENDENCY_METRIC]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE]).toEqual(type); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER]).toEqual(identifier); + + // Server span should not generate remote resource attributes + (spanDataMock as any).kind = SpanKind.SERVER; + actualAttributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[SERVICE_METRIC]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE]).toEqual(undefined); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER]).toEqual(undefined); + } + + it('testDBUserAttribute', () => { + mockAttribute(SEMATTRS_DB_OPERATION, 'db_operation'); + mockAttribute(SEMATTRS_DB_USER, 'db_user'); + (spanDataMock as any).kind = SpanKind.CLIENT; + + const actualAttributes: Attributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[ + DEPENDENCY_METRIC + ]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION]).toEqual('db_operation'); + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_DB_USER]).toEqual('db_user'); + }); + + it('testDBUserAttributeAbsent', () => { + mockAttribute(SEMATTRS_DB_SYSTEM, 'db_system'); + (spanDataMock as any).kind = SpanKind.CLIENT; + + const actualAttributes: Attributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[ + DEPENDENCY_METRIC + ]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_DB_USER]).toBeUndefined(); + }); + + it('testDBUserAttributeWithDifferentValues', () => { + mockAttribute(SEMATTRS_DB_OPERATION, 'db_operation'); + mockAttribute(SEMATTRS_DB_USER, 'non_db_user'); + (spanDataMock as any).kind = SpanKind.CLIENT; + + const actualAttributes: Attributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[ + DEPENDENCY_METRIC + ]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_DB_USER]).toEqual('non_db_user'); + }); + + it('testDBUserAttributeNotPresentInServiceMetricForServerSpan', () => { + mockAttribute(SEMATTRS_DB_USER, 'db_user'); + mockAttribute(SEMATTRS_DB_SYSTEM, 'db_system'); + (spanDataMock as any).kind = SpanKind.SERVER; + + const actualAttributes: Attributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[ + SERVICE_METRIC + ]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_DB_USER]).toBeUndefined(); + }); + + it('testDbUserPresentAndIsDbSpanFalse', () => { + mockAttribute(SEMATTRS_DB_USER, 'DB user'); + (spanDataMock as any).kind = SpanKind.CLIENT; + + const actualAttributes: Attributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[ + DEPENDENCY_METRIC + ]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_DB_USER]).toBeUndefined(); + }); + + it('testNormalizeRemoteServiceName_NoNormalization', () => { + const serviceName: string = 'non aws service'; + mockAttribute(SEMATTRS_RPC_SERVICE, serviceName); + (spanDataMock as any).kind = SpanKind.CLIENT; + + const actualAttributes: Attributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[ + DEPENDENCY_METRIC + ]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]).toEqual(serviceName); + }); + + it('testNormalizeRemoteServiceName_AwsSdk', () => { + testAwsSdkServiceNormalization('DynamoDB', 'AWS::DynamoDB'); + testAwsSdkServiceNormalization('Kinesis', 'AWS::Kinesis'); + testAwsSdkServiceNormalization('S3', 'AWS::S3'); + testAwsSdkServiceNormalization('SQS', 'AWS::SQS'); + }); + + function testAwsSdkServiceNormalization(serviceName: string, expectedRemoteService: string): void { + mockAttribute(SEMATTRS_RPC_SYSTEM, 'aws-api'); + mockAttribute(SEMATTRS_RPC_SERVICE, serviceName); + (spanDataMock as any).kind = SpanKind.CLIENT; + + const actualAttributes: Attributes = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource)[ + DEPENDENCY_METRIC + ]; + expect(actualAttributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]).toEqual(expectedRemoteService); + } + + it('testNoMetricWhenConsumerProcessWithConsumerParent', () => { + attributesMock[AWS_ATTRIBUTE_KEYS.AWS_CONSUMER_PARENT_SPAN_KIND] = SpanKind[SpanKind.CONSUMER]; + attributesMock[SEMATTRS_MESSAGING_OPERATION] = MESSAGINGOPERATIONVALUES_PROCESS; + (spanDataMock as any).kind = SpanKind.CONSUMER; + + const attributeMap: AttributeMap = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource); + + const serviceAttributes: Attributes = attributeMap[SERVICE_METRIC]; + const dependencyAttributes: Attributes = attributeMap[DEPENDENCY_METRIC]; + + expect(serviceAttributes).toBeUndefined(); + expect(dependencyAttributes).toBeUndefined(); + }); + + it('testBothMetricsWhenLocalRootConsumerProcess', () => { + attributesMock[AWS_ATTRIBUTE_KEYS.AWS_CONSUMER_PARENT_SPAN_KIND] = SpanKind[SpanKind.CONSUMER]; + attributesMock[SEMATTRS_MESSAGING_OPERATION] = MESSAGINGOPERATIONVALUES_PROCESS; + (spanDataMock as any).kind = SpanKind.CONSUMER; + // Divergence from Java/Python - set AWS_IS_LOCAL_ROOT as true because parentSpanContext is not valid in this test + attributesMock[AWS_ATTRIBUTE_KEYS.AWS_IS_LOCAL_ROOT] = true; + + const attributeMap: AttributeMap = GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource); + + const serviceAttributes: Attributes = attributeMap[SERVICE_METRIC]; + const dependencyAttributes: Attributes = attributeMap[DEPENDENCY_METRIC]; + + expect(attributeMap[SERVICE_METRIC]).toEqual(serviceAttributes); + expect(attributeMap[DEPENDENCY_METRIC]).toEqual(dependencyAttributes); + }); +}); From 6d6ebc90c8ea9be3f68370182c34bfee4e232155 Mon Sep 17 00:00:00 2001 From: jjllee Date: Fri, 2 Aug 2024 11:23:59 -0700 Subject: [PATCH 3/8] update generateIngressOperation to be more similar to Python instead of Java --- .../src/aws-span-processing-util.ts | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-span-processing-util.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-span-processing-util.ts index dd8d451..ffd9bbd 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-span-processing-util.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-span-processing-util.ts @@ -12,6 +12,7 @@ import { SEMATTRS_DB_SYSTEM, SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_TARGET, + SEMATTRS_HTTP_URL, SEMATTRS_MESSAGING_OPERATION, SEMATTRS_RPC_SYSTEM, } from '@opentelemetry/semantic-conventions'; @@ -206,21 +207,36 @@ export class AwsSpanProcessingUtil { */ private static generateIngressOperation(span: ReadableSpan): string { let operation: string = AwsSpanProcessingUtil.UNKNOWN_OPERATION; + let httpPath: AttributeValue | undefined = undefined; + if (AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_HTTP_TARGET)) { - const httpTarget: AttributeValue | undefined = span.attributes[SEMATTRS_HTTP_TARGET]; - // get the first part from API path string as operation value - // the more levels/parts we get from API path the higher chance for getting high cardinality - // data - if (httpTarget != null) { - operation = AwsSpanProcessingUtil.extractAPIPathValue(httpTarget.toString()); - if (AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_HTTP_METHOD)) { - const httpMethod: AttributeValue | undefined = span.attributes[SEMATTRS_HTTP_METHOD]; - if (httpMethod != null) { - operation = httpMethod.toString() + ' ' + operation; - } + httpPath = span.attributes[SEMATTRS_HTTP_TARGET]; + } else if (AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_HTTP_URL)) { + const httpUrl: AttributeValue | undefined = span.attributes[SEMATTRS_HTTP_URL]; + try { + let url: URL; + if (httpUrl !== undefined) { + url = new URL(httpUrl as string); + httpPath = url.pathname; + } + } catch (e: unknown) { + // 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. + diag.verbose(`invalid http.url attribute: ${httpUrl}, setting httpPath as empty string`); + httpPath = ''; + } + } + + if (httpPath !== undefined) { + operation = this.extractAPIPathValue(httpPath as string); + if (this.isKeyPresent(span, SEMATTRS_HTTP_METHOD)) { + const httpMethod: AttributeValue | undefined = span.attributes[SEMATTRS_HTTP_METHOD]; + if (httpMethod !== undefined) { + operation = httpMethod + ' ' + operation; } } } + return operation; } From b3a02ab1672946fe0c7e7a94657973fd7e3d9cdd Mon Sep 17 00:00:00 2001 From: jjllee Date: Fri, 2 Aug 2024 11:26:14 -0700 Subject: [PATCH 4/8] update test comment --- .../test/aws-metric-attribute-generator.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts index 88d7f28..b9482e6 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts @@ -128,9 +128,6 @@ describe('AwsMetricAttributeGeneratorTest', () => { droppedEventsCount: 0, droppedLinksCount: 0, }; - // (spanDataMock as any).getAttributes() = attributesMock; - // (spanDataMock as any).getInstrumentationScopeInfo() = instrumentationScopeInfoMock; - //[][] (spanDataMock as any).getSpanContext() = mock(SpanContext.class); parentSpanContextMock = { traceId: 'traceId', spanId: 'spanId', From 92d6899cd25ce43df2a812e2ebe4ed1ecf51fcd3 Mon Sep 17 00:00:00 2001 From: jjllee Date: Fri, 2 Aug 2024 13:49:00 -0700 Subject: [PATCH 5/8] add tests testJdbcDbConnectionString --- .../src/aws-metric-attribute-generator.ts | 14 ++-- .../aws-metric-attribute-generator.test.ts | 64 +++++++++++++++++++ 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts index 88002e5..31b0fbe 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts @@ -61,8 +61,6 @@ const DB_CONNECTION_RESOURCE_TYPE: string = 'DB::Connection'; // the service name to unknown_service: or just unknown_service. const OTEL_UNKNOWN_SERVICE: string = 'unknown_service:node'; -const JDBC_PROTOCOL_PREFIX: string = 'jdbc:'; - /** * AwsMetricAttributeGenerator generates very specific metric attributes based on low-cardinality * span and resource attributes. If such attributes are not present, we fallback to default values. @@ -450,17 +448,19 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { let address: string; let port: string; try { - uri = new URL(connectionString as string); - // Divergence from Java/Python // `jdbc:://` isn't handled well with `new URL()` // uri.host and uri.port will be empty strings // examples: // - jdbc:postgresql://host:port/database?properties // - jdbc:mysql://localhost:3306 - // Try again with the substring after `jdbc:`) - if ((connectionString as string).startsWith(JDBC_PROTOCOL_PREFIX)) { - uri = new URL((connectionString as string).substring(JDBC_PROTOCOL_PREFIX.length)); + // - abc:def:ghi://host:3306 + // Try with a dummy schema without `:`, since we do not care about the schema + const schemeEndIndex: number = (connectionString as string).indexOf('://'); + if (schemeEndIndex === -1) { + uri = new URL(connectionString as string); + } else { + uri = new URL('dummyschema' + (connectionString as string).substring(schemeEndIndex)); } address = uri.hostname; diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts index b9482e6..e55d1a7 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts @@ -1184,4 +1184,68 @@ describe('AwsMetricAttributeGeneratorTest', () => { expect(attributeMap[SERVICE_METRIC]).toEqual(serviceAttributes); expect(attributeMap[DEPENDENCY_METRIC]).toEqual(dependencyAttributes); }); + + it('testJdbcDbConnectionString', () => { + mockAttribute(SEMATTRS_DB_SYSTEM, 'mysql'); + + // Validate behaviour of SEMATTRS_DB_NAME and SEMATTRS_DB_CONNECTION_STRING exist, then remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name'); + mockAttribute( + SEMATTRS_DB_CONNECTION_STRING, + 'jdbc:mysql://mysql.db.server:3306/my_database?useSSL=false&serverTimezone=UTC' + ); + validateRemoteResourceAttributes('DB::Connection', 'db_name|mysql.db.server|3306'); + mockAttribute(SEMATTRS_DB_NAME, undefined); + mockAttribute(SEMATTRS_DB_CONNECTION_STRING, undefined); + + // Validate behaviour of SEMATTRS_DB_NAME and SEMATTRS_DB_CONNECTION_STRING exist, then remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name'); + mockAttribute(SEMATTRS_DB_CONNECTION_STRING, 'jdbc:mysql://myhostname:3306/db_name?prop1=value1&prop2=value2'); + validateRemoteResourceAttributes('DB::Connection', 'db_name|myhostname|3306'); + mockAttribute(SEMATTRS_DB_NAME, undefined); + mockAttribute(SEMATTRS_DB_CONNECTION_STRING, undefined); + + // Validate behaviour of SEMATTRS_DB_NAME and SEMATTRS_DB_CONNECTION_STRING exist, then remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name'); + mockAttribute(SEMATTRS_DB_CONNECTION_STRING, 'jdbc:mysql://root:mypassword@myhostname:3306/db_name'); + validateRemoteResourceAttributes('DB::Connection', 'db_name|myhostname|3306'); + mockAttribute(SEMATTRS_DB_NAME, undefined); + mockAttribute(SEMATTRS_DB_CONNECTION_STRING, undefined); + + // Validate behaviour of SEMATTRS_DB_NAME and SEMATTRS_DB_CONNECTION_STRING exist, then remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name'); + mockAttribute(SEMATTRS_DB_CONNECTION_STRING, 'jdbc:postgresql://host:3306/database?properties'); + validateRemoteResourceAttributes('DB::Connection', 'db_name|host|3306'); + mockAttribute(SEMATTRS_DB_NAME, undefined); + mockAttribute(SEMATTRS_DB_CONNECTION_STRING, undefined); + + // Validate behaviour of SEMATTRS_DB_NAME and SEMATTRS_DB_CONNECTION_STRING exist, then remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name'); + mockAttribute( + SEMATTRS_DB_CONNECTION_STRING, + 'jdbc:postgresql://postgresql.db.server:3306/mydatabase?ssl=true&loglevel=1' + ); + validateRemoteResourceAttributes('DB::Connection', 'db_name|postgresql.db.server|3306'); + mockAttribute(SEMATTRS_DB_NAME, undefined); + mockAttribute(SEMATTRS_DB_CONNECTION_STRING, undefined); + + // Validate behaviour of SEMATTRS_DB_NAME and SEMATTRS_DB_CONNECTION_STRING exist, then remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name'); + mockAttribute(SEMATTRS_DB_CONNECTION_STRING, 'jdbc://myhostname:3306/db_name?user=root&password=mypassword'); + validateRemoteResourceAttributes('DB::Connection', 'db_name|myhostname|3306'); + mockAttribute(SEMATTRS_DB_NAME, undefined); + mockAttribute(SEMATTRS_DB_CONNECTION_STRING, undefined); + + // Validate behaviour of SEMATTRS_DB_NAME and SEMATTRS_DB_CONNECTION_STRING exist, then remove it. + mockAttribute(SEMATTRS_DB_NAME, 'db_name'); + mockAttribute( + SEMATTRS_DB_CONNECTION_STRING, + 'jdbc:mysql:loadbalance://myhostname:3306/db_name?user=root&password=mypassword' + ); + validateRemoteResourceAttributes('DB::Connection', 'db_name|myhostname|3306'); + mockAttribute(SEMATTRS_DB_NAME, undefined); + mockAttribute(SEMATTRS_DB_CONNECTION_STRING, undefined); + + mockAttribute(SEMATTRS_DB_SYSTEM, undefined); + }); }); From 07accf68644c92d3be934a69fceda6eb8084f149 Mon Sep 17 00:00:00 2001 From: jjllee Date: Fri, 2 Aug 2024 19:20:42 -0700 Subject: [PATCH 6/8] remove unneeded comments --- .../aws-metric-attribute-generator.test.ts | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts index e55d1a7..3b6089f 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts @@ -71,32 +71,6 @@ let parentSpanContextMock: SpanContext; /** Unit tests for {@link AwsMetricAttributeGenerator}. */ describe('AwsMetricAttributeGeneratorTest', () => { - // static class ThrowableWithMethodGetStatusCode extends Throwable { - // private final int httpStatusCode; - - // ThrowableWithMethodGetStatusCode(int httpStatusCode) { - // this.httpStatusCode = httpStatusCode; - // } - - // public int getStatusCode() { - // return this.httpStatusCode; - // } - // } - - // static class ThrowableWithMethodStatusCode extends Throwable { - // private final int httpStatusCode; - - // ThrowableWithMethodStatusCode(int httpStatusCode) { - // this.httpStatusCode = httpStatusCode; - // } - - // public int statusCode() { - // return this.httpStatusCode; - // } - // } - - // static class ThrowableWithoutStatusCode extends Throwable {} - // setUpMocks beforeEach(() => { attributesMock = {}; From e09e3bd3180e204f6776e2f00b690568e6046eaf Mon Sep 17 00:00:00 2001 From: jjllee Date: Tue, 6 Aug 2024 11:00:38 -0700 Subject: [PATCH 7/8] address comments --- .../src/aws-metric-attribute-generator.ts | 42 ++++++++++--------- .../src/metric-attribute-generator.ts | 4 +- .../aws-metric-attribute-generator.test.ts | 12 ++---- 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts index 31b0fbe..9282245 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Attributes, AttributeValue, diag, SpanKind } from '@opentelemetry/api'; -import { Resource } from '@opentelemetry/resources'; +import { Resource, defaultServiceName } from '@opentelemetry/resources'; import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import { SEMATTRS_DB_CONNECTION_STRING, @@ -59,7 +59,9 @@ const NORMALIZED_SQS_SERVICE_NAME: string = 'AWS::SQS'; const DB_CONNECTION_RESOURCE_TYPE: string = 'DB::Connection'; // As per https://opentelemetry.io/docs/specs/semconv/resource/#service, if service name is not specified, SDK defaults // the service name to unknown_service: or just unknown_service. -const OTEL_UNKNOWN_SERVICE: string = 'unknown_service:node'; +// - https://github.com/open-telemetry/opentelemetry-js/blob/b2778e1b2ff7b038cebf371f1eb9f808fd98107f/packages/opentelemetry-resources/src/platform/node/default-service-name.ts#L16. +// - `defaultServiceName()` returns `unknown_service:${process.argv0}` +const OTEL_UNKNOWN_SERVICE: string = defaultServiceName(); /** * AwsMetricAttributeGenerator generates very specific metric attributes based on low-cardinality @@ -108,7 +110,7 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { } /** Service is always derived from {@link SEMRESATTRS_SERVICE_NAME} */ - private static setService(resource: Resource, span: ReadableSpan, builder: Attributes): void { + private static setService(resource: Resource, span: ReadableSpan, attributes: Attributes): void { let service: AttributeValue | undefined = resource.attributes[SEMRESATTRS_SERVICE_NAME]; // In practice the service name is never undefined, but we can be defensive here. @@ -116,7 +118,7 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { AwsMetricAttributeGenerator.logUnknownAttribute(AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE, span); service = AwsSpanProcessingUtil.UNKNOWN_SERVICE; } - builder[AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE] = service; + attributes[AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE] = service; } /** @@ -124,12 +126,12 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { * "http.method + http.target/with the first API path parameter" if the default span name equals * null, UnknownOperation or http.method value. */ - private static setIngressOperation(span: ReadableSpan, builder: Attributes): void { + private static setIngressOperation(span: ReadableSpan, attributes: Attributes): void { const operation: string = AwsSpanProcessingUtil.getIngressOperation(span); if (operation === AwsSpanProcessingUtil.UNKNOWN_OPERATION) { AwsMetricAttributeGenerator.logUnknownAttribute(AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION, span); } - builder[AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION] = operation; + attributes[AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION] = operation; } /** @@ -137,13 +139,13 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { * special span attribute, {@link AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION}. This attribute is * generated with a separate SpanProcessor, {@link AttributePropagatingSpanProcessor} */ - private static setEgressOperation(span: ReadableSpan, builder: Attributes): void { + private static setEgressOperation(span: ReadableSpan, attributes: Attributes): void { let operation: AttributeValue | undefined = AwsSpanProcessingUtil.getEgressOperation(span); if (operation === undefined) { AwsMetricAttributeGenerator.logUnknownAttribute(AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION, span); operation = AwsSpanProcessingUtil.UNKNOWN_OPERATION; } - builder[AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION] = operation; + attributes[AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION] = operation; } /** @@ -183,7 +185,7 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { * `net.peer.sock.port` and `http.url` will be used to derive the RemoteService. And `http.method` * and `http.url` will be used to derive the RemoteOperation. */ - private static setRemoteServiceAndOperation(span: ReadableSpan, builder: Attributes): void { + private static setRemoteServiceAndOperation(span: ReadableSpan, attributes: Attributes): void { let remoteService: string = AwsSpanProcessingUtil.UNKNOWN_REMOTE_SERVICE; let remoteOperation: string = AwsSpanProcessingUtil.UNKNOWN_REMOTE_OPERATION; @@ -242,8 +244,8 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { remoteOperation = AwsMetricAttributeGenerator.generateRemoteOperation(span); } - builder[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE] = remoteService; - builder[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION] = remoteOperation; + attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE] = remoteService; + attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION] = remoteOperation; } /** @@ -333,7 +335,7 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { * href="https://docs.aws.amazon.com/cloudcontrolapi/latest/userguide/supported-resources.html">AWS * Cloud Control resource format. */ - private static setRemoteResourceTypeAndIdentifier(span: ReadableSpan, builder: Attributes): void { + private static setRemoteResourceTypeAndIdentifier(span: ReadableSpan, attributes: Attributes): void { let remoteResourceType: AttributeValue | undefined; let remoteResourceIdentifier: AttributeValue | undefined; @@ -370,8 +372,8 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { } if (remoteResourceType !== undefined && remoteResourceIdentifier !== undefined) { - builder[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE] = remoteResourceType; - builder[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER] = remoteResourceIdentifier; + attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE] = remoteResourceType; + attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER] = remoteResourceIdentifier; } } @@ -491,22 +493,22 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator { } /** Span kind is needed for differentiating metrics in the EMF exporter */ - private static setSpanKindForService(span: ReadableSpan, builder: Attributes): void { + private static setSpanKindForService(span: ReadableSpan, attributes: Attributes): void { let spanKind: string = SpanKind[span.kind]; if (AwsSpanProcessingUtil.isLocalRoot(span)) { spanKind = AwsSpanProcessingUtil.LOCAL_ROOT; } - builder[AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND] = spanKind; + attributes[AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND] = spanKind; } - private static setSpanKindForDependency(span: ReadableSpan, builder: Attributes): void { + private static setSpanKindForDependency(span: ReadableSpan, attributes: Attributes): void { const spanKind: string = SpanKind[span.kind]; - builder[AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND] = spanKind; + attributes[AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND] = spanKind; } - private static setRemoteDbUser(span: ReadableSpan, builder: Attributes): void { + private static setRemoteDbUser(span: ReadableSpan, attributes: Attributes): void { if (AwsSpanProcessingUtil.isDBSpan(span) && AwsSpanProcessingUtil.isKeyPresent(span, SEMATTRS_DB_USER)) { - builder[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_DB_USER] = span.attributes[SEMATTRS_DB_USER]; + attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_DB_USER] = span.attributes[SEMATTRS_DB_USER]; } } diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/metric-attribute-generator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/metric-attribute-generator.ts index 7a9c469..e68feed 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/metric-attribute-generator.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/metric-attribute-generator.ts @@ -22,9 +22,9 @@ export interface MetricAttributeGenerator { * Given a span and associated resource, produce meaningful metric attributes for metrics produced * from the span. If no metrics should be generated from this span, return an empty Attributes={}. * - * @param span - SpanData to be used to generate metric attributes. + * @param span - ReadableSpan to be used to generate metric attributes. * @param resource - Resource associated with Span to be used to generate metric attributes. - * @return A map of Attributes objects0 with values assigned to key "Service" or "Dependency". It + * @return A map of Attributes objects with values assigned to key "Service" or "Dependency". It * will contain either 0, 1, or 2 items. */ generateMetricAttributeMapFromSpan(span: ReadableSpan, resource: Resource): AttributeMap; diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts index 3b6089f..4a96f40 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts @@ -133,8 +133,7 @@ describe('AwsMetricAttributeGeneratorTest', () => { it('testConsumerSpanWithoutAttributes', () => { const expectedAttributes: Attributes = { [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.CONSUMER], - // This is tested to be UnknownService in Java/Python - [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: `unknown_service:${process.argv0}`, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: UNKNOWN_SERVICE, [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: UNKNOWN_OPERATION, [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]: UNKNOWN_REMOTE_SERVICE, [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION]: UNKNOWN_REMOTE_OPERATION, @@ -145,8 +144,7 @@ describe('AwsMetricAttributeGeneratorTest', () => { it('testServerSpanWithoutAttributes', () => { const expectedAttributes: Attributes = { [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.SERVER], - // This is tested to be UnknownService in Java/Python - [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: `unknown_service:${process.argv0}`, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: UNKNOWN_SERVICE, // This is tested to be UNKNOWN_OPERATION in Java/Python // This is because in other langauges, span name could be null, but // this is not possibly in OTel JS. @@ -158,8 +156,7 @@ describe('AwsMetricAttributeGeneratorTest', () => { it('testProducerSpanWithoutAttributes', () => { const expectedAttributes: Attributes = { [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.PRODUCER], - // This is tested to be UnknownService in Java/Python - [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: `unknown_service:${process.argv0}`, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: UNKNOWN_SERVICE, [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: UNKNOWN_OPERATION, [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]: UNKNOWN_REMOTE_SERVICE, [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION]: UNKNOWN_REMOTE_OPERATION, @@ -170,8 +167,7 @@ describe('AwsMetricAttributeGeneratorTest', () => { it('testClientSpanWithoutAttributes', () => { const expectedAttributes: Attributes = { [AWS_ATTRIBUTE_KEYS.AWS_SPAN_KIND]: SpanKind[SpanKind.CLIENT], - // This is tested to be UnknownService in Java/Python - [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: `unknown_service:${process.argv0}`, + [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_SERVICE]: UNKNOWN_SERVICE, [AWS_ATTRIBUTE_KEYS.AWS_LOCAL_OPERATION]: UNKNOWN_OPERATION, [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE]: UNKNOWN_REMOTE_SERVICE, [AWS_ATTRIBUTE_KEYS.AWS_REMOTE_OPERATION]: UNKNOWN_REMOTE_OPERATION, From a538ec6a92684988ab09af089714e3343ba34759 Mon Sep 17 00:00:00 2001 From: jjllee Date: Tue, 6 Aug 2024 12:32:49 -0700 Subject: [PATCH 8/8] make traceIds and spanIds valid in tests --- .../src/aws-opentelemetry-configurator.ts | 2 +- .../test/aws-metric-attribute-generator.test.ts | 12 +++--------- .../test/aws-span-processing-util.test.ts | 12 ++++++------ 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts index ee0e6d0..7a3e461 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts @@ -58,7 +58,7 @@ export class AwsOpentelemetryConfigurator { if (process.env.OTEL_NODE_RESOURCE_DETECTORS != null) { defaultDetectors = getResourceDetectorsFromEnv(); // Add Env/AWS Resource Detectors if not present - const resourceDetectorsFromEnv = process.env.OTEL_NODE_RESOURCE_DETECTORS.split(','); + const resourceDetectorsFromEnv: string[] = process.env.OTEL_NODE_RESOURCE_DETECTORS.split(','); if (!resourceDetectorsFromEnv.includes('env')) { defaultDetectors.push(envDetectorSync); } diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts index 4a96f40..d220545 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts @@ -67,7 +67,6 @@ let attributesMock: Attributes; let spanDataMock: ReadableSpan; let instrumentationScopeInfoMock: InstrumentationLibrary; let resource: Resource; -let parentSpanContextMock: SpanContext; /** Unit tests for {@link AwsMetricAttributeGenerator}. */ describe('AwsMetricAttributeGeneratorTest', () => { @@ -82,12 +81,13 @@ describe('AwsMetricAttributeGeneratorTest', () => { kind: SpanKind.SERVER, spanContext: () => { const spanContext: SpanContext = { - traceId: 'traceId', - spanId: 'spanId', + traceId: '00000000000000000000000000000008', + spanId: '0000000000000009', traceFlags: 0, }; return spanContext; }, + parentSpanId: '0000000000000007', startTime: [0, 0], endTime: [0, 1], status: { code: 0 }, @@ -102,12 +102,6 @@ describe('AwsMetricAttributeGeneratorTest', () => { droppedEventsCount: 0, droppedLinksCount: 0, }; - parentSpanContextMock = { - traceId: 'traceId', - spanId: 'spanId', - traceFlags: 0, - }; - parentSpanContextMock.isRemote = false; // Divergence from Java/Python - set AWS_IS_LOCAL_ROOT as false because parentSpanContext is valid and not remote in this test attributesMock[AWS_ATTRIBUTE_KEYS.AWS_IS_LOCAL_ROOT] = false; diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-span-processing-util.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-span-processing-util.test.ts index aaeba2d..f002006 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-span-processing-util.test.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-span-processing-util.test.ts @@ -32,8 +32,8 @@ describe('AwsSpanProcessingUtilTest', () => { kind: SpanKind.SERVER, spanContext: () => { const spanContext: SpanContext = { - traceId: 'traceId', - spanId: 'spanId', + traceId: '00000000000000000000000000000008', + spanId: '0000000000000009', traceFlags: 0, }; return spanContext; @@ -205,8 +205,8 @@ describe('AwsSpanProcessingUtilTest', () => { it('testShouldGenerateServiceMetricAttributes', () => { const parentSpanContext: SpanContext = { - traceId: 'traceId', - spanId: 'spanId', + traceId: '00000000000000000000000000000008', + spanId: '0000000000000009', traceFlags: 0, }; (parentSpanContext as any).isRemote = false; @@ -354,8 +354,8 @@ describe('AwsSpanProcessingUtilTest', () => { function createMockSpanContext(): SpanContext { return { - traceId: 'traceId', - spanId: 'spanId', + traceId: '00000000000000000000000000000008', + spanId: '0000000000000009', traceFlags: 0, }; }