Skip to content
Open
10 changes: 9 additions & 1 deletion .oxlintrc.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,34 @@ export const AWS_SDK_ORIGIN = 'auto.aws.orchestrion.aws_sdk';
export const ATTR_RPC_SYSTEM = 'rpc.system';
export const AWS_REQUEST_ID = 'aws.request.id';
export const AWS_REQUEST_EXTENDED_ID = 'aws.request.extended_id';
export const AWS_S3_BUCKET = 'aws.s3.bucket';
export const AWS_KINESIS_STREAM_NAME = 'aws.kinesis.stream.name';

// DynamoDB
export const ATTR_AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS = 'aws.dynamodb.attribute_definitions';
export const ATTR_AWS_DYNAMODB_CONSISTENT_READ = 'aws.dynamodb.consistent_read';
export const ATTR_AWS_DYNAMODB_CONSUMED_CAPACITY = 'aws.dynamodb.consumed_capacity';
export const ATTR_AWS_DYNAMODB_COUNT = 'aws.dynamodb.count';
export const ATTR_AWS_DYNAMODB_EXCLUSIVE_START_TABLE = 'aws.dynamodb.exclusive_start_table';
export const ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES = 'aws.dynamodb.global_secondary_indexes';
export const ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES = 'aws.dynamodb.global_secondary_index_updates';
export const ATTR_AWS_DYNAMODB_INDEX_NAME = 'aws.dynamodb.index_name';
export const ATTR_AWS_DYNAMODB_ITEM_COLLECTION_METRICS = 'aws.dynamodb.item_collection_metrics';
export const ATTR_AWS_DYNAMODB_LIMIT = 'aws.dynamodb.limit';
export const ATTR_AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES = 'aws.dynamodb.local_secondary_indexes';
export const ATTR_AWS_DYNAMODB_PROJECTION = 'aws.dynamodb.projection';
export const ATTR_AWS_DYNAMODB_PROVISIONED_READ_CAPACITY = 'aws.dynamodb.provisioned_read_capacity';
export const ATTR_AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY = 'aws.dynamodb.provisioned_write_capacity';
export const ATTR_AWS_DYNAMODB_SCANNED_COUNT = 'aws.dynamodb.scanned_count';
export const ATTR_AWS_DYNAMODB_SCAN_FORWARD = 'aws.dynamodb.scan_forward';
export const ATTR_AWS_DYNAMODB_SEGMENT = 'aws.dynamodb.segment';
export const ATTR_AWS_DYNAMODB_SELECT = 'aws.dynamodb.select';
export const ATTR_AWS_DYNAMODB_TABLE_COUNT = 'aws.dynamodb.table_count';
export const ATTR_AWS_DYNAMODB_TABLE_NAMES = 'aws.dynamodb.table_names';
export const ATTR_AWS_DYNAMODB_TOTAL_SEGMENTS = 'aws.dynamodb.total_segments';
export const DB_SYSTEM_VALUE_DYNAMODB = 'dynamodb';

// SecretsManager / StepFunctions
export const ATTR_AWS_SECRETSMANAGER_SECRET_ARN = 'aws.secretsmanager.secret.arn';
export const ATTR_AWS_STEP_FUNCTIONS_ACTIVITY_ARN = 'aws.step_functions.activity.arn';
export const ATTR_AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN = 'aws.step_functions.state_machine.arn';
Original file line number Diff line number Diff line change
@@ -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<string, ServiceExtension> = new Map();
private _services: Map<string, ServiceExtension> = new Map<string, ServiceExtension>([
['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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import type { Span } from '@sentry/core';
import { SPAN_KIND } from '@sentry/core';
import { DB_NAME, DB_OPERATION, DB_SYSTEM } from '@sentry/conventions/attributes';
import {
ATTR_AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS,
ATTR_AWS_DYNAMODB_CONSISTENT_READ,
ATTR_AWS_DYNAMODB_CONSUMED_CAPACITY,
ATTR_AWS_DYNAMODB_COUNT,
ATTR_AWS_DYNAMODB_EXCLUSIVE_START_TABLE,
ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES,
ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES,
ATTR_AWS_DYNAMODB_INDEX_NAME,
ATTR_AWS_DYNAMODB_ITEM_COLLECTION_METRICS,
ATTR_AWS_DYNAMODB_LIMIT,
ATTR_AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES,
ATTR_AWS_DYNAMODB_PROJECTION,
ATTR_AWS_DYNAMODB_PROVISIONED_READ_CAPACITY,
ATTR_AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY,
ATTR_AWS_DYNAMODB_SCAN_FORWARD,
ATTR_AWS_DYNAMODB_SCANNED_COUNT,
ATTR_AWS_DYNAMODB_SEGMENT,
ATTR_AWS_DYNAMODB_SELECT,
ATTR_AWS_DYNAMODB_TABLE_COUNT,
ATTR_AWS_DYNAMODB_TABLE_NAMES,
ATTR_AWS_DYNAMODB_TOTAL_SEGMENTS,
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;
}
Comment thread
andreiborza marked this conversation as resolved.

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)),
);
}
Comment thread
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);
Comment thread
andreiborza marked this conversation as resolved.
}
Comment thread
andreiborza marked this conversation as resolved.
}
}
Comment thread
andreiborza marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { SPAN_KIND } from '@sentry/core';
import { AWS_KINESIS_STREAM_NAME } from '../constants';
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) {
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 '../constants';
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 { ATTR_AWS_SECRETSMANAGER_SECRET_ARN } from '../constants';
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,25 @@
import { SPAN_KIND } from '@sentry/core';
import { ATTR_AWS_STEP_FUNCTIONS_ACTIVITY_ARN, ATTR_AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN } from '../constants';
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,
};
}
}
Loading