diff --git a/.oxlintrc.base.json b/.oxlintrc.base.json index 5e1a40beb267..57e39bcc20dd 100644 --- a/.oxlintrc.base.json +++ b/.oxlintrc.base.json @@ -150,7 +150,15 @@ { "files": ["**/integrations/tracing-channel/aws-sdk/**/*.ts"], "rules": { - "typescript/no-explicit-any": "off" + "typescript/no-explicit-any": "off", + "typescript/no-unsafe-member-access": "off" + } + }, + { + "files": ["**/integrations/tracing-channel/aws-sdk/services/**/*.ts"], + "rules": { + "max-lines": "off", + "complexity": "off" } }, { diff --git a/.size-limit.js b/.size-limit.js index 53bd15c11bf6..fc6ddeb16c53 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -400,7 +400,7 @@ module.exports = [ import: createImport('init', 'experimentalUseDiagnosticsChannelInjection'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '142 KB', + limit: '144 KB', disablePlugins: ['@size-limit/esbuild'], }, { diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts index 4d82a9c676c6..320c24540f0a 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts @@ -6,3 +6,6 @@ /** The span origin every aws-sdk channel span carries, mirroring the uniform OTel `auto.otel.aws`. */ export const AWS_SDK_ORIGIN = 'auto.aws.orchestrion.aws_sdk'; + +/** DynamoDB `db.system` value (an attribute value, not a key, so not covered by conventions). */ +export const DB_SYSTEM_VALUE_DYNAMODB = 'dynamodb'; diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServicesExtensions.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServicesExtensions.ts index 83d0573b184e..b98d402c9078 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServicesExtensions.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServicesExtensions.ts @@ -1,11 +1,22 @@ import type { Span } from '@sentry/core'; import type { NormalizedRequest, NormalizedResponse, RequestMetadata } from '../types'; +import { DynamodbServiceExtension } from './dynamodb'; +import { KinesisServiceExtension } from './kinesis'; +import { S3ServiceExtension } from './s3'; +import { SecretsManagerServiceExtension } from './secretsmanager'; import type { ServiceExtension } from './ServiceExtension'; +import { StepFunctionsServiceExtension } from './stepfunctions'; export class ServicesExtensions implements ServiceExtension { // Per-service extensions, keyed by the client's `serviceId` (e.g. `'S3'`). Services without a // registered extension still get the base rpc span from the subscriber. - private _services: Map = new Map(); + private _services: Map = new Map([ + ['SecretsManager', new SecretsManagerServiceExtension()], + ['SFN', new StepFunctionsServiceExtension()], + ['DynamoDB', new DynamodbServiceExtension()], + ['S3', new S3ServiceExtension()], + ['Kinesis', new KinesisServiceExtension()], + ]); public requestPreSpanHook(request: NormalizedRequest): RequestMetadata { const serviceExtension = this._services.get(request.serviceName); diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/dynamodb.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/dynamodb.ts new file mode 100644 index 000000000000..f9276fc20d96 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/dynamodb.ts @@ -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(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 = {}; + + /* 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; + } + + 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)), + ); + } + + 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); + } + } +} diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/kinesis.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/kinesis.ts new file mode 100644 index 000000000000..22599882d443 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/kinesis.ts @@ -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 = {}; + + 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, + }; + } +} diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/s3.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/s3.ts new file mode 100644 index 000000000000..24062efe3cd8 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/s3.ts @@ -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 = {}; + + if (bucketName) { + spanAttributes[AWS_S3_BUCKET] = bucketName; + } + + return { + spanAttributes, + spanKind: SPAN_KIND.CLIENT, + }; + } +} diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/secretsmanager.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/secretsmanager.ts new file mode 100644 index 000000000000..b73318ef23b8 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/secretsmanager.ts @@ -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 = {}; + 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); + } + } +} diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/stepfunctions.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/stepfunctions.ts new file mode 100644 index 000000000000..05afa34d6a39 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/stepfunctions.ts @@ -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 = {}; + + 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, + }; + } +}