diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/src/services/dynamodb.ts b/plugins/node/opentelemetry-instrumentation-aws-sdk/src/services/dynamodb.ts index dd797b5af7..424cab5fa4 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-sdk/src/services/dynamodb.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/src/services/dynamodb.ts @@ -26,6 +26,10 @@ import { } from '../types'; export class DynamodbServiceExtension implements ServiceExtension { + toArray(values: T | T[]): T[] { + return Array.isArray(values) ? values : [values]; + } + requestPreSpanHook(normalizedRequest: NormalizedRequest): RequestMetadata { const spanKind: SpanKind = SpanKind.CLIENT; let spanName: string | undefined; @@ -41,12 +45,144 @@ export class DynamodbServiceExtension implements ServiceExtension { ), }; - if (operation === 'BatchGetItem') { + // normalizedRequest.commandInput.RequestItems) is undefined when no table names are returned + // keys in this object are the table names + if (normalizedRequest.commandInput?.TableName) { + // Necessary for commands with only 1 table name (example: CreateTable). Attribute is TableName not keys of RequestItems + // single table name returned for operations like CreateTable + spanAttributes[SemanticAttributes.AWS_DYNAMODB_TABLE_NAMES] = [ + normalizedRequest.commandInput.TableName, + ]; + } else if (normalizedRequest.commandInput?.RequestItems) { spanAttributes[SemanticAttributes.AWS_DYNAMODB_TABLE_NAMES] = Object.keys( normalizedRequest.commandInput.RequestItems ); } + if (operation === 'CreateTable' || operation === 'UpdateTable') { + // only check for ProvisionedThroughput since ReadCapacityUnits and WriteCapacity units are required attributes + if (normalizedRequest.commandInput?.ProvisionedThroughput) { + spanAttributes[ + SemanticAttributes.AWS_DYNAMODB_PROVISIONED_READ_CAPACITY + ] = + normalizedRequest.commandInput.ProvisionedThroughput.ReadCapacityUnits; + spanAttributes[ + SemanticAttributes.AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY + ] = + normalizedRequest.commandInput.ProvisionedThroughput.WriteCapacityUnits; + } + } + + if ( + operation === 'GetItem' || + operation === 'Scan' || + operation === 'Query' + ) { + if (normalizedRequest.commandInput?.ConsistentRead) { + spanAttributes[SemanticAttributes.AWS_DYNAMODB_CONSISTENT_READ] = + normalizedRequest.commandInput.ConsistentRead; + } + } + + if (operation === 'Query' || operation === 'Scan') { + if (normalizedRequest.commandInput?.ProjectionExpression) { + spanAttributes[SemanticAttributes.AWS_DYNAMODB_PROJECTION] = + normalizedRequest.commandInput.ProjectionExpression; + } + } + + if (operation === 'CreateTable') { + if (normalizedRequest.commandInput?.GlobalSecondaryIndexes) { + spanAttributes[ + SemanticAttributes.AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES + ] = this.toArray( + normalizedRequest.commandInput.GlobalSecondaryIndexes + ).map((x: { [DictionaryKey: string]: any }) => JSON.stringify(x)); + } + + if (normalizedRequest.commandInput?.LocalSecondaryIndexes) { + spanAttributes[ + SemanticAttributes.AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES + ] = this.toArray( + normalizedRequest.commandInput.LocalSecondaryIndexes + ).map((x: { [DictionaryKey: string]: any }) => JSON.stringify(x)); + } + } + + if ( + operation === 'ListTables' || + operation === 'Query' || + operation === 'Scan' + ) { + if (normalizedRequest.commandInput?.Limit) { + spanAttributes[SemanticAttributes.AWS_DYNAMODB_LIMIT] = + normalizedRequest.commandInput.Limit; + } + } + + if (operation === 'ListTables') { + if (normalizedRequest.commandInput?.ExclusiveStartTableName) { + spanAttributes[SemanticAttributes.AWS_DYNAMODB_EXCLUSIVE_START_TABLE] = + normalizedRequest.commandInput.ExclusiveStartTableName; + } + } + + if (operation === 'Query') { + if (normalizedRequest.commandInput?.ScanIndexForward) { + spanAttributes[SemanticAttributes.AWS_DYNAMODB_SCAN_FORWARD] = + normalizedRequest.commandInput.ScanIndexForward; + } + + if (normalizedRequest.commandInput?.IndexName) { + spanAttributes[SemanticAttributes.AWS_DYNAMODB_INDEX_NAME] = + normalizedRequest.commandInput.IndexName; + } + + if (normalizedRequest.commandInput?.Select) { + spanAttributes[SemanticAttributes.AWS_DYNAMODB_SELECT] = + normalizedRequest.commandInput.Select; + } + } + + if (operation === 'Scan') { + if (normalizedRequest.commandInput?.Segment) { + spanAttributes[SemanticAttributes.AWS_DYNAMODB_SEGMENT] = + normalizedRequest.commandInput?.Segment; + } + + if (normalizedRequest.commandInput?.TotalSegments) { + spanAttributes[SemanticAttributes.AWS_DYNAMODB_TOTAL_SEGMENTS] = + normalizedRequest.commandInput?.TotalSegments; + } + + if (normalizedRequest.commandInput?.IndexName) { + spanAttributes[SemanticAttributes.AWS_DYNAMODB_INDEX_NAME] = + normalizedRequest.commandInput.IndexName; + } + + if (normalizedRequest.commandInput?.Select) { + spanAttributes[SemanticAttributes.AWS_DYNAMODB_SELECT] = + normalizedRequest.commandInput.Select; + } + } + + if (operation === 'UpdateTable') { + if (normalizedRequest.commandInput?.AttributeDefinitions) { + spanAttributes[SemanticAttributes.AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS] = + this.toArray(normalizedRequest.commandInput.AttributeDefinitions).map( + (x: { [DictionaryKey: string]: any }) => JSON.stringify(x) + ); + } + + if (normalizedRequest.commandInput?.GlobalSecondaryIndexUpdates) { + spanAttributes[ + SemanticAttributes.AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES + ] = this.toArray( + normalizedRequest.commandInput.GlobalSecondaryIndexUpdates + ).map((x: { [DictionaryKey: string]: any }) => JSON.stringify(x)); + } + } + return { isIncoming, spanAttributes, @@ -73,5 +209,35 @@ export class DynamodbServiceExtension implements ServiceExtension { ); } } + + if (response.data?.ItemCollectionMetrics) { + span.setAttribute( + SemanticAttributes.AWS_DYNAMODB_ITEM_COLLECTION_METRICS, + this.toArray(response.data.ItemCollectionMetrics).map( + (x: { [DictionaryKey: string]: any }) => JSON.stringify(x) + ) + ); + } + + if (response.data?.TableNames) { + span.setAttribute( + SemanticAttributes.AWS_DYNAMODB_TABLE_COUNT, + response.data?.TableNames.length + ); + } + + if (response.data?.Count) { + span.setAttribute( + SemanticAttributes.AWS_DYNAMODB_COUNT, + response.data?.Count + ); + } + + if (response.data?.ScannedCount) { + span.setAttribute( + SemanticAttributes.AWS_DYNAMODB_SCANNED_COUNT, + response.data?.ScannedCount + ); + } } } diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/dynamodb.test.ts b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/dynamodb.test.ts index 5ff0c2b0ab..b8504d4010 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/dynamodb.test.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/dynamodb.test.ts @@ -69,7 +69,14 @@ describe('DynamoDB', () => { ExpressionAttributeValues: { ':v': 'val1', }, + ProjectionExpression: 'id', + ScanIndexForward: true, + ConsistentRead: true, + IndexName: 'name_to_group', + Limit: 10, + Select: 'ALL_ATTRIBUTES', }; + dynamodb.query( params, (err: AWSError, data: AWS.DynamoDB.DocumentClient.QueryOutput) => { @@ -81,6 +88,390 @@ describe('DynamoDB', () => { ); expect(attrs[SemanticAttributes.DB_NAME]).toStrictEqual('test-table'); expect(attrs[SemanticAttributes.DB_OPERATION]).toStrictEqual('Query'); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_SCAN_FORWARD] + ).toStrictEqual(true); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_CONSISTENT_READ] + ).toStrictEqual(true); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_INDEX_NAME] + ).toStrictEqual('name_to_group'); + expect(attrs[SemanticAttributes.AWS_DYNAMODB_SELECT]).toStrictEqual( + 'ALL_ATTRIBUTES' + ); + expect(attrs[SemanticAttributes.AWS_DYNAMODB_LIMIT]).toStrictEqual( + 10 + ); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_TABLE_NAMES] + ).toStrictEqual(['test-table']); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_PROJECTION] + ).toStrictEqual('id'); + expect( + JSON.parse(attrs[SemanticAttributes.DB_STATEMENT] as string) + ).toEqual(params); + expect(err).toBeFalsy(); + done(); + } + ); + }); + }); + + describe('Scan', () => { + beforeEach(() => + mockV2AwsSend(responseMockSuccess, { + ConsumedCapacity: { + TableName: 'test-table', + CapacityUnits: 0.5, + Table: { CapacityUnits: 0.5 }, + }, + Count: 10, + ScannedCount: 50, + } as AWS.DynamoDB.Types.ScanOutput) + ); + + it('should populate specific Scan attributes', done => { + const dynamodb = new AWS.DynamoDB.DocumentClient(); + const params = { + TableName: 'test-table', + Item: { key1: 'val1' }, + ProjectionExpression: 'id', + ConsistentRead: true, + Segment: 10, + TotalSegments: 100, + IndexName: 'index_name', + Limit: 10, + Select: 'ALL_ATTRIBUTES', + }; + + dynamodb.scan( + params, + (err: AWSError, data: AWS.DynamoDB.DocumentClient.ScanOutput) => { + const spans = getTestSpans(); + expect(spans.length).toStrictEqual(1); + const attrs = spans[0].attributes; + expect(attrs[SemanticAttributes.DB_SYSTEM]).toStrictEqual( + DbSystemValues.DYNAMODB + ); + expect(attrs[SemanticAttributes.DB_NAME]).toStrictEqual('test-table'); + expect(attrs[SemanticAttributes.DB_OPERATION]).toStrictEqual('Scan'); + expect(attrs[SemanticAttributes.AWS_DYNAMODB_SEGMENT]).toStrictEqual( + 10 + ); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_TOTAL_SEGMENTS] + ).toStrictEqual(100); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_INDEX_NAME] + ).toStrictEqual('index_name'); + expect(attrs[SemanticAttributes.AWS_DYNAMODB_SELECT]).toStrictEqual( + 'ALL_ATTRIBUTES' + ); + expect(attrs[SemanticAttributes.AWS_DYNAMODB_COUNT]).toStrictEqual( + 10 + ); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_SCANNED_COUNT] + ).toStrictEqual(50); + expect(attrs[SemanticAttributes.AWS_DYNAMODB_LIMIT]).toStrictEqual( + 10 + ); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_TABLE_NAMES] + ).toStrictEqual(['test-table']); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_PROJECTION] + ).toStrictEqual('id'); + expect( + JSON.parse(attrs[SemanticAttributes.DB_STATEMENT] as string) + ).toEqual(params); + expect(err).toBeFalsy(); + done(); + } + ); + }); + }); + + describe('BatchWriteItem', () => { + beforeEach(() => + mockV2AwsSend(responseMockSuccess, { + UnprocessedItems: {}, + ItemCollectionMetrics: { + ItemCollectionKey: [], + SizeEstimateRangeGB: [0], + }, + ConsumedCapacity: undefined, + } as AWS.DynamoDB.Types.BatchWriteItemOutput) + ); + + it('should populate specific BatchWriteItem attributes', done => { + const dynamodb = new AWS.DynamoDB.DocumentClient(); + const params = { + RequestItems: {}, + ReturnConsumedCapacity: 'INDEXES', + ReturnItemCollectionMetrics: 'SIZE', + }; + + dynamodb.batchWrite( + params, + ( + err: AWSError, + data: AWS.DynamoDB.DocumentClient.BatchWriteItemOutput + ) => { + const spans = getTestSpans(); + expect(spans.length).toStrictEqual(1); + const attrs = spans[0].attributes; + expect(attrs[SemanticAttributes.DB_SYSTEM]).toStrictEqual( + DbSystemValues.DYNAMODB + ); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_ITEM_COLLECTION_METRICS] + ).toStrictEqual([ + JSON.stringify({ ItemCollectionKey: [], SizeEstimateRangeGB: [0] }), + ]); + + expect( + JSON.parse(attrs[SemanticAttributes.DB_STATEMENT] as string) + ).toEqual(params); + expect(err).toBeFalsy(); + done(); + } + ); + }); + }); + + describe('CreateTable', () => { + beforeEach(() => + mockV2AwsSend(responseMockSuccess, { + TableName: 'test_table', + ItemCollectionMetrics: { + ItemCollectionKey: [], + SizeEstimateRangeGB: [0], + }, + ConsumedCapacity: undefined, + } as AWS.DynamoDB.Types.CreateTableOutput) + ); + + it('should populate specific CreateTable attributes', done => { + const globalSecondaryIndexMockData = { + IndexName: 'test_index', + KeySchema: [ + { + AttributeName: 'attribute1', + KeyType: 'HASH', + }, + ], + Projection: { + ProjectionType: 'ALL', + NonKeyAttributes: ['non_key_attr'], + }, + ProvisionedThroughput: { + ReadCapacityUnits: 5, + WriteCapacityUnits: 10, + }, + }; + + const localSecondaryIndexMockData = { + IndexName: 'test_index', + KeySchema: [ + { + AttributeName: 'test_attribute', + KeyType: 'HASH', + }, + ], + Projection: { + ProjectionType: 'ALL', + NonKeyAttributes: ['STRING_VALUE'], + }, + }; + + const dynamodb = new AWS.DynamoDB(); + const params = { + AttributeDefinitions: [ + { + AttributeName: 'test_attribute', + AttributeType: 'S', + }, + ], + TableName: 'test_table', + KeySchema: [ + { + AttributeName: 'test_attribute', + KeyType: 'HASH', + }, + ], + LocalSecondaryIndexes: [localSecondaryIndexMockData], + GlobalSecondaryIndexes: [globalSecondaryIndexMockData], + BillingMode: 'PROVISIONED', + ProvisionedThroughput: { + ReadCapacityUnits: 20, + WriteCapacityUnits: 30, + }, + }; + + dynamodb.createTable( + params, + ( + err: AWSError, + data: AWS.DynamoDB.DocumentClient.CreateTableOutput + ) => { + const spans = getTestSpans(); + expect(spans.length).toStrictEqual(1); + const attrs = spans[0].attributes; + expect(attrs[SemanticAttributes.DB_SYSTEM]).toStrictEqual( + DbSystemValues.DYNAMODB + ); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_ITEM_COLLECTION_METRICS] + ).toStrictEqual([ + JSON.stringify({ ItemCollectionKey: [], SizeEstimateRangeGB: [0] }), + ]); + + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES] + ).toStrictEqual([JSON.stringify(globalSecondaryIndexMockData)]); + + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES] + ).toStrictEqual([JSON.stringify(localSecondaryIndexMockData)]); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_PROVISIONED_READ_CAPACITY] + ).toStrictEqual(20); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY] + ).toStrictEqual(30); + expect( + JSON.parse(attrs[SemanticAttributes.DB_STATEMENT] as string) + ).toEqual(params); + expect(err).toBeFalsy(); + done(); + } + ); + }); + }); + + describe('UpdateTable', () => { + beforeEach(() => + mockV2AwsSend(responseMockSuccess, { + TableName: 'test_table', + } as AWS.DynamoDB.Types.UpdateTableOutput) + ); + + it('should populate specific CreateTable attributes', done => { + const dynamodb = new AWS.DynamoDB(); + const params = { + AttributeDefinitions: [ + { + AttributeName: 'test_attr', + AttributeType: 'S', + }, + ], + TableName: 'test_table', + ProvisionedThroughput: { + ReadCapacityUnits: 10, + WriteCapacityUnits: 15, + }, + GlobalSecondaryIndexUpdates: [ + { + Update: { + IndexName: 'test_index', + ProvisionedThroughput: { + ReadCapacityUnits: 1, + WriteCapacityUnits: 5, + }, + }, + }, + ], + }; + + dynamodb.updateTable( + params, + ( + err: AWSError, + data: AWS.DynamoDB.DocumentClient.UpdateTableOutput + ) => { + const spans = getTestSpans(); + expect(spans.length).toStrictEqual(1); + const attrs = spans[0].attributes; + expect(attrs[SemanticAttributes.DB_SYSTEM]).toStrictEqual( + DbSystemValues.DYNAMODB + ); + + expect( + attrs[ + SemanticAttributes.AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES + ] + ).toStrictEqual([ + JSON.stringify({ + Update: { + IndexName: 'test_index', + ProvisionedThroughput: { + ReadCapacityUnits: 1, + WriteCapacityUnits: 5, + }, + }, + }), + ]); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS] + ).toStrictEqual([ + JSON.stringify({ + AttributeName: 'test_attr', + AttributeType: 'S', + }), + ]); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_PROVISIONED_READ_CAPACITY] + ).toStrictEqual(10); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY] + ).toStrictEqual(15); + expect( + JSON.parse(attrs[SemanticAttributes.DB_STATEMENT] as string) + ).toEqual(params); + expect(err).toBeFalsy(); + done(); + } + ); + }); + }); + + describe('ListTables', () => { + beforeEach(() => + mockV2AwsSend(responseMockSuccess, { + TableNames: ['test_table', 'test_table_2', 'start_table'], + } as AWS.DynamoDB.Types.ListTablesOutput) + ); + + it('should populate specific ListTables attributes', done => { + const dynamodb = new AWS.DynamoDB(); + const params = { + ExclusiveStartTableName: 'start_table', + Limit: 10, + }; + + dynamodb.listTables( + params, + (err: AWSError, data: AWS.DynamoDB.DocumentClient.ListTablesOutput) => { + const spans = getTestSpans(); + expect(spans.length).toStrictEqual(1); + const attrs = spans[0].attributes; + expect(attrs[SemanticAttributes.DB_SYSTEM]).toStrictEqual( + DbSystemValues.DYNAMODB + ); + + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_EXCLUSIVE_START_TABLE] + ).toStrictEqual('start_table'); + expect(attrs[SemanticAttributes.AWS_DYNAMODB_LIMIT]).toStrictEqual( + 10 + ); + expect( + attrs[SemanticAttributes.AWS_DYNAMODB_TABLE_COUNT] + ).toStrictEqual(3); + expect( JSON.parse(attrs[SemanticAttributes.DB_STATEMENT] as string) ).toEqual(params);