-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(server-utils): Port S3, Kinesis, DynamoDB, SecretsManager and StepFunctions aws-sdk extensions #22164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(server-utils): Port S3, Kinesis, DynamoDB, SecretsManager and StepFunctions aws-sdk extensions #22164
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| import type { Span } from '@sentry/core'; | ||
| import { SPAN_KIND } from '@sentry/core'; | ||
| import { | ||
| AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS as ATTR_AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS, | ||
| AWS_DYNAMODB_CONSISTENT_READ as ATTR_AWS_DYNAMODB_CONSISTENT_READ, | ||
| AWS_DYNAMODB_CONSUMED_CAPACITY as ATTR_AWS_DYNAMODB_CONSUMED_CAPACITY, | ||
| AWS_DYNAMODB_COUNT as ATTR_AWS_DYNAMODB_COUNT, | ||
| AWS_DYNAMODB_EXCLUSIVE_START_TABLE as ATTR_AWS_DYNAMODB_EXCLUSIVE_START_TABLE, | ||
| AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES as ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES, | ||
| AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES as ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES, | ||
| AWS_DYNAMODB_INDEX_NAME as ATTR_AWS_DYNAMODB_INDEX_NAME, | ||
| AWS_DYNAMODB_ITEM_COLLECTION_METRICS as ATTR_AWS_DYNAMODB_ITEM_COLLECTION_METRICS, | ||
| AWS_DYNAMODB_LIMIT as ATTR_AWS_DYNAMODB_LIMIT, | ||
| AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES as ATTR_AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES, | ||
| AWS_DYNAMODB_PROJECTION as ATTR_AWS_DYNAMODB_PROJECTION, | ||
| AWS_DYNAMODB_PROVISIONED_READ_CAPACITY as ATTR_AWS_DYNAMODB_PROVISIONED_READ_CAPACITY, | ||
| AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY as ATTR_AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY, | ||
| AWS_DYNAMODB_SCAN_FORWARD as ATTR_AWS_DYNAMODB_SCAN_FORWARD, | ||
| AWS_DYNAMODB_SCANNED_COUNT as ATTR_AWS_DYNAMODB_SCANNED_COUNT, | ||
| AWS_DYNAMODB_SEGMENT as ATTR_AWS_DYNAMODB_SEGMENT, | ||
| AWS_DYNAMODB_SELECT as ATTR_AWS_DYNAMODB_SELECT, | ||
| AWS_DYNAMODB_TABLE_COUNT as ATTR_AWS_DYNAMODB_TABLE_COUNT, | ||
| AWS_DYNAMODB_TABLE_NAMES as ATTR_AWS_DYNAMODB_TABLE_NAMES, | ||
| AWS_DYNAMODB_TOTAL_SEGMENTS as ATTR_AWS_DYNAMODB_TOTAL_SEGMENTS, | ||
| DB_NAME, | ||
| DB_OPERATION, | ||
| DB_SYSTEM, | ||
| } from '@sentry/conventions/attributes'; | ||
| import { DB_SYSTEM_VALUE_DYNAMODB } from '../constants'; | ||
| import type { NormalizedRequest, NormalizedResponse } from '../types'; | ||
| import type { RequestMetadata, ServiceExtension } from './ServiceExtension'; | ||
|
|
||
| function toArray<T>(values: T | T[]): T[] { | ||
| return Array.isArray(values) ? values : [values]; | ||
| } | ||
|
|
||
| export class DynamodbServiceExtension implements ServiceExtension { | ||
| public requestPreSpanHook(normalizedRequest: NormalizedRequest): RequestMetadata { | ||
| const operation = normalizedRequest.commandName; | ||
| const tableName = normalizedRequest.commandInput?.TableName; | ||
|
|
||
| const spanAttributes: Record<string, unknown> = {}; | ||
|
|
||
| /* oxlint-disable typescript/no-deprecated -- old-semconv db.* attributes, matched to the OTel aws-sdk integration */ | ||
| spanAttributes[DB_SYSTEM] = DB_SYSTEM_VALUE_DYNAMODB; | ||
| spanAttributes[DB_NAME] = tableName; | ||
| spanAttributes[DB_OPERATION] = operation; | ||
| /* oxlint-enable typescript/no-deprecated */ | ||
|
|
||
| // `RequestItems` is undefined when no table names are returned; its keys are the table names. | ||
| if (normalizedRequest.commandInput?.TableName) { | ||
| // Necessary for commands with only 1 table name (e.g. CreateTable). Attribute is `TableName`, not keys of `RequestItems`. | ||
| spanAttributes[ATTR_AWS_DYNAMODB_TABLE_NAMES] = [normalizedRequest.commandInput.TableName]; | ||
| } else if (normalizedRequest.commandInput?.RequestItems) { | ||
| spanAttributes[ATTR_AWS_DYNAMODB_TABLE_NAMES] = Object.keys(normalizedRequest.commandInput.RequestItems); | ||
| } | ||
|
|
||
| if (operation === 'CreateTable' || operation === 'UpdateTable') { | ||
| // only check for ProvisionedThroughput since ReadCapacityUnits and WriteCapacityUnits are required attributes | ||
| if (normalizedRequest.commandInput?.ProvisionedThroughput) { | ||
| spanAttributes[ATTR_AWS_DYNAMODB_PROVISIONED_READ_CAPACITY] = | ||
| normalizedRequest.commandInput.ProvisionedThroughput.ReadCapacityUnits; | ||
| spanAttributes[ATTR_AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY] = | ||
| normalizedRequest.commandInput.ProvisionedThroughput.WriteCapacityUnits; | ||
| } | ||
| } | ||
|
|
||
| if (operation === 'GetItem' || operation === 'Scan' || operation === 'Query') { | ||
| if (normalizedRequest.commandInput?.ConsistentRead) { | ||
| spanAttributes[ATTR_AWS_DYNAMODB_CONSISTENT_READ] = normalizedRequest.commandInput.ConsistentRead; | ||
| } | ||
| } | ||
|
|
||
| if (operation === 'Query' || operation === 'Scan') { | ||
| if (normalizedRequest.commandInput?.ProjectionExpression) { | ||
| spanAttributes[ATTR_AWS_DYNAMODB_PROJECTION] = normalizedRequest.commandInput.ProjectionExpression; | ||
| } | ||
| } | ||
|
|
||
| if (operation === 'CreateTable') { | ||
| if (normalizedRequest.commandInput?.GlobalSecondaryIndexes) { | ||
| spanAttributes[ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES] = toArray( | ||
| normalizedRequest.commandInput.GlobalSecondaryIndexes, | ||
| ).map((x: unknown) => JSON.stringify(x)); | ||
| } | ||
|
|
||
| if (normalizedRequest.commandInput?.LocalSecondaryIndexes) { | ||
| spanAttributes[ATTR_AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES] = toArray( | ||
| normalizedRequest.commandInput.LocalSecondaryIndexes, | ||
| ).map((x: unknown) => JSON.stringify(x)); | ||
| } | ||
| } | ||
|
|
||
| if (operation === 'ListTables' || operation === 'Query' || operation === 'Scan') { | ||
| if (normalizedRequest.commandInput?.Limit) { | ||
| spanAttributes[ATTR_AWS_DYNAMODB_LIMIT] = normalizedRequest.commandInput.Limit; | ||
| } | ||
| } | ||
|
|
||
| if (operation === 'ListTables') { | ||
| if (normalizedRequest.commandInput?.ExclusiveStartTableName) { | ||
| spanAttributes[ATTR_AWS_DYNAMODB_EXCLUSIVE_START_TABLE] = | ||
| normalizedRequest.commandInput.ExclusiveStartTableName; | ||
| } | ||
| } | ||
|
|
||
| if (operation === 'Query') { | ||
| if (normalizedRequest.commandInput?.ScanIndexForward) { | ||
| spanAttributes[ATTR_AWS_DYNAMODB_SCAN_FORWARD] = normalizedRequest.commandInput.ScanIndexForward; | ||
| } | ||
|
|
||
| if (normalizedRequest.commandInput?.IndexName) { | ||
| spanAttributes[ATTR_AWS_DYNAMODB_INDEX_NAME] = normalizedRequest.commandInput.IndexName; | ||
| } | ||
|
|
||
| if (normalizedRequest.commandInput?.Select) { | ||
| spanAttributes[ATTR_AWS_DYNAMODB_SELECT] = normalizedRequest.commandInput.Select; | ||
| } | ||
| } | ||
|
|
||
| if (operation === 'Scan') { | ||
| if (normalizedRequest.commandInput?.Segment) { | ||
| spanAttributes[ATTR_AWS_DYNAMODB_SEGMENT] = normalizedRequest.commandInput?.Segment; | ||
| } | ||
|
andreiborza marked this conversation as resolved.
Comment on lines
+122
to
+124
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The truthy check on Suggested FixModify the conditional check to correctly handle the case where Prompt for AI Agent |
||
|
|
||
| if (normalizedRequest.commandInput?.TotalSegments) { | ||
| spanAttributes[ATTR_AWS_DYNAMODB_TOTAL_SEGMENTS] = normalizedRequest.commandInput?.TotalSegments; | ||
| } | ||
|
|
||
| if (normalizedRequest.commandInput?.IndexName) { | ||
| spanAttributes[ATTR_AWS_DYNAMODB_INDEX_NAME] = normalizedRequest.commandInput.IndexName; | ||
| } | ||
|
|
||
| if (normalizedRequest.commandInput?.Select) { | ||
| spanAttributes[ATTR_AWS_DYNAMODB_SELECT] = normalizedRequest.commandInput.Select; | ||
| } | ||
| } | ||
|
|
||
| if (operation === 'UpdateTable') { | ||
| if (normalizedRequest.commandInput?.AttributeDefinitions) { | ||
| spanAttributes[ATTR_AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS] = toArray( | ||
| normalizedRequest.commandInput.AttributeDefinitions, | ||
| ).map((x: unknown) => JSON.stringify(x)); | ||
| } | ||
|
|
||
| if (normalizedRequest.commandInput?.GlobalSecondaryIndexUpdates) { | ||
| spanAttributes[ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES] = toArray( | ||
| normalizedRequest.commandInput.GlobalSecondaryIndexUpdates, | ||
| ).map((x: unknown) => JSON.stringify(x)); | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| spanAttributes, | ||
| spanKind: SPAN_KIND.CLIENT, | ||
| // Matches what the exporter infers from `db.system` for the OTel DynamoDB spans. | ||
| spanOp: 'db', | ||
| }; | ||
| } | ||
|
|
||
| public responseHook(response: NormalizedResponse, span: Span): void { | ||
| if (response.data?.ConsumedCapacity) { | ||
| span.setAttribute( | ||
| ATTR_AWS_DYNAMODB_CONSUMED_CAPACITY, | ||
| toArray(response.data.ConsumedCapacity).map((x: unknown) => JSON.stringify(x)), | ||
| ); | ||
| } | ||
|
|
||
| if (response.data?.ItemCollectionMetrics) { | ||
| span.setAttribute( | ||
| ATTR_AWS_DYNAMODB_ITEM_COLLECTION_METRICS, | ||
| toArray(response.data.ItemCollectionMetrics).map((x: unknown) => JSON.stringify(x)), | ||
| ); | ||
| } | ||
|
sentry[bot] marked this conversation as resolved.
|
||
|
|
||
| if (response.data?.TableNames) { | ||
| span.setAttribute(ATTR_AWS_DYNAMODB_TABLE_COUNT, response.data?.TableNames.length); | ||
| } | ||
|
|
||
| if (response.data?.Count) { | ||
| span.setAttribute(ATTR_AWS_DYNAMODB_COUNT, response.data?.Count); | ||
| } | ||
|
|
||
| if (response.data?.ScannedCount) { | ||
| span.setAttribute(ATTR_AWS_DYNAMODB_SCANNED_COUNT, response.data?.ScannedCount); | ||
|
andreiborza marked this conversation as resolved.
|
||
| } | ||
|
andreiborza marked this conversation as resolved.
|
||
| } | ||
| } | ||
|
andreiborza marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { SPAN_KIND } from '@sentry/core'; | ||
| import { _AWS_KINESIS_STREAM_NAME as AWS_KINESIS_STREAM_NAME } from '@sentry/conventions/attributes'; | ||
| import type { NormalizedRequest } from '../types'; | ||
| import type { RequestMetadata, ServiceExtension } from './ServiceExtension'; | ||
|
|
||
| export class KinesisServiceExtension implements ServiceExtension { | ||
| public requestPreSpanHook(request: NormalizedRequest): RequestMetadata { | ||
| const streamName = request.commandInput?.StreamName; | ||
| const spanAttributes: Record<string, unknown> = {}; | ||
|
|
||
| if (streamName) { | ||
| // oxlint-disable-next-line typescript/no-deprecated -- old-semconv aws.kinesis.stream.name, matched to the OTel aws-sdk integration | ||
| spanAttributes[AWS_KINESIS_STREAM_NAME] = streamName; | ||
| } | ||
|
|
||
| return { | ||
| spanAttributes, | ||
| spanKind: SPAN_KIND.CLIENT, | ||
| }; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { SPAN_KIND } from '@sentry/core'; | ||
| import { AWS_S3_BUCKET } from '@sentry/conventions/attributes'; | ||
| import type { NormalizedRequest } from '../types'; | ||
| import type { RequestMetadata, ServiceExtension } from './ServiceExtension'; | ||
|
|
||
| export class S3ServiceExtension implements ServiceExtension { | ||
| public requestPreSpanHook(request: NormalizedRequest): RequestMetadata { | ||
| const bucketName = request.commandInput?.Bucket; | ||
| const spanAttributes: Record<string, unknown> = {}; | ||
|
|
||
| if (bucketName) { | ||
| spanAttributes[AWS_S3_BUCKET] = bucketName; | ||
| } | ||
|
|
||
| return { | ||
| spanAttributes, | ||
| spanKind: SPAN_KIND.CLIENT, | ||
| }; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import type { Span } from '@sentry/core'; | ||
| import { SPAN_KIND } from '@sentry/core'; | ||
| import { AWS_SECRETSMANAGER_SECRET_ARN as ATTR_AWS_SECRETSMANAGER_SECRET_ARN } from '@sentry/conventions/attributes'; | ||
| import type { NormalizedRequest, NormalizedResponse } from '../types'; | ||
| import type { RequestMetadata, ServiceExtension } from './ServiceExtension'; | ||
|
|
||
| export class SecretsManagerServiceExtension implements ServiceExtension { | ||
| public requestPreSpanHook(request: NormalizedRequest): RequestMetadata { | ||
| const secretId = request.commandInput?.SecretId; | ||
| const spanAttributes: Record<string, unknown> = {}; | ||
| if (typeof secretId === 'string' && secretId.startsWith('arn:aws:secretsmanager:')) { | ||
| spanAttributes[ATTR_AWS_SECRETSMANAGER_SECRET_ARN] = secretId; | ||
| } | ||
|
|
||
| return { | ||
| spanAttributes, | ||
| spanKind: SPAN_KIND.CLIENT, | ||
| }; | ||
| } | ||
|
|
||
| public responseHook(response: NormalizedResponse, span: Span): void { | ||
| const secretArn = response.data?.ARN; | ||
| if (secretArn) { | ||
| span.setAttribute(ATTR_AWS_SECRETSMANAGER_SECRET_ARN, secretArn); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { SPAN_KIND } from '@sentry/core'; | ||
| import { | ||
| AWS_STEP_FUNCTIONS_ACTIVITY_ARN as ATTR_AWS_STEP_FUNCTIONS_ACTIVITY_ARN, | ||
| AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN as ATTR_AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN, | ||
| } from '@sentry/conventions/attributes'; | ||
| import type { NormalizedRequest } from '../types'; | ||
| import type { RequestMetadata, ServiceExtension } from './ServiceExtension'; | ||
|
|
||
| export class StepFunctionsServiceExtension implements ServiceExtension { | ||
| public requestPreSpanHook(request: NormalizedRequest): RequestMetadata { | ||
| const stateMachineArn = request.commandInput?.stateMachineArn; | ||
| const activityArn = request.commandInput?.activityArn; | ||
| const spanAttributes: Record<string, unknown> = {}; | ||
|
|
||
| if (stateMachineArn) { | ||
| spanAttributes[ATTR_AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN] = stateMachineArn; | ||
| } | ||
|
|
||
| if (activityArn) { | ||
| spanAttributes[ATTR_AWS_STEP_FUNCTIONS_ACTIVITY_ARN] = activityArn; | ||
| } | ||
|
|
||
| return { | ||
| spanAttributes, | ||
| spanKind: SPAN_KIND.CLIENT, | ||
| }; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: The check for
ScanIndexForwarduses a truthy conditional, which incorrectly omits the span attribute when the value is explicitlyfalse, a meaningful state for DynamoDB queries.Severity: MEDIUM
Suggested Fix
Modify the conditional to explicitly handle both
trueandfalsevalues. Instead of a simple truthy check, verify the property is notundefined, for example:if (normalizedRequest.commandInput?.ScanIndexForward !== undefined). This will ensure the attribute is set correctly for bothtrueandfalsevalues.Prompt for AI Agent