Skip to content

Commit 6549624

Browse files
andreiborzaclaude
andcommitted
feat(server-utils): Port S3, Kinesis, DynamoDB, SecretsManager and StepFunctions aws-sdk extensions
Ports the attribute-only service extensions from the OTel aws-sdk integration to the orchestrion channel integration and registers them in the service registry: - S3: `aws.s3.bucket` - Kinesis: `aws.kinesis.stream.name` - DynamoDB: `db.*` and `aws.dynamodb.*` request/response attributes - SecretsManager: `aws.secretsmanager.secret.arn` (request and response) - StepFunctions: state machine / activity ARNs Straight ports; none of these inject trace propagation or change span lifecycle. Part of #20946 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 47a0d74 commit 6549624

7 files changed

Lines changed: 317 additions & 0 deletions

File tree

packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,34 @@ export const AWS_SDK_ORIGIN = 'auto.aws.orchestrion.aws_sdk';
1515
export const ATTR_RPC_SYSTEM = 'rpc.system';
1616
export const AWS_REQUEST_ID = 'aws.request.id';
1717
export const AWS_REQUEST_EXTENDED_ID = 'aws.request.extended_id';
18+
export const AWS_S3_BUCKET = 'aws.s3.bucket';
19+
export const AWS_KINESIS_STREAM_NAME = 'aws.kinesis.stream.name';
20+
21+
// DynamoDB
22+
export const ATTR_AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS = 'aws.dynamodb.attribute_definitions';
23+
export const ATTR_AWS_DYNAMODB_CONSISTENT_READ = 'aws.dynamodb.consistent_read';
24+
export const ATTR_AWS_DYNAMODB_CONSUMED_CAPACITY = 'aws.dynamodb.consumed_capacity';
25+
export const ATTR_AWS_DYNAMODB_COUNT = 'aws.dynamodb.count';
26+
export const ATTR_AWS_DYNAMODB_EXCLUSIVE_START_TABLE = 'aws.dynamodb.exclusive_start_table';
27+
export const ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES = 'aws.dynamodb.global_secondary_indexes';
28+
export const ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES = 'aws.dynamodb.global_secondary_index_updates';
29+
export const ATTR_AWS_DYNAMODB_INDEX_NAME = 'aws.dynamodb.index_name';
30+
export const ATTR_AWS_DYNAMODB_ITEM_COLLECTION_METRICS = 'aws.dynamodb.item_collection_metrics';
31+
export const ATTR_AWS_DYNAMODB_LIMIT = 'aws.dynamodb.limit';
32+
export const ATTR_AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES = 'aws.dynamodb.local_secondary_indexes';
33+
export const ATTR_AWS_DYNAMODB_PROJECTION = 'aws.dynamodb.projection';
34+
export const ATTR_AWS_DYNAMODB_PROVISIONED_READ_CAPACITY = 'aws.dynamodb.provisioned_read_capacity';
35+
export const ATTR_AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY = 'aws.dynamodb.provisioned_write_capacity';
36+
export const ATTR_AWS_DYNAMODB_SCANNED_COUNT = 'aws.dynamodb.scanned_count';
37+
export const ATTR_AWS_DYNAMODB_SCAN_FORWARD = 'aws.dynamodb.scan_forward';
38+
export const ATTR_AWS_DYNAMODB_SEGMENT = 'aws.dynamodb.segment';
39+
export const ATTR_AWS_DYNAMODB_SELECT = 'aws.dynamodb.select';
40+
export const ATTR_AWS_DYNAMODB_TABLE_COUNT = 'aws.dynamodb.table_count';
41+
export const ATTR_AWS_DYNAMODB_TABLE_NAMES = 'aws.dynamodb.table_names';
42+
export const ATTR_AWS_DYNAMODB_TOTAL_SEGMENTS = 'aws.dynamodb.total_segments';
43+
export const DB_SYSTEM_VALUE_DYNAMODB = 'dynamodb';
44+
45+
// SecretsManager / StepFunctions
46+
export const ATTR_AWS_SECRETSMANAGER_SECRET_ARN = 'aws.secretsmanager.secret.arn';
47+
export const ATTR_AWS_STEP_FUNCTIONS_ACTIVITY_ARN = 'aws.step_functions.activity.arn';
48+
export const ATTR_AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN = 'aws.step_functions.state_machine.arn';

packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServicesExtensions.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type { Span } from '@sentry/core';
22
import type { NormalizedRequest, NormalizedResponse, RequestMetadata } from '../types';
3+
import { DynamodbServiceExtension } from './dynamodb';
4+
import { KinesisServiceExtension } from './kinesis';
5+
import { S3ServiceExtension } from './s3';
6+
import { SecretsManagerServiceExtension } from './secretsmanager';
37
import type { ServiceExtension } from './ServiceExtension';
8+
import { StepFunctionsServiceExtension } from './stepfunctions';
49

510
export class ServicesExtensions implements ServiceExtension {
611
private _services: Map<string, ServiceExtension>;
@@ -9,6 +14,11 @@ export class ServicesExtensions implements ServiceExtension {
914
// Per-service extensions, keyed by the client's `serviceId` (e.g. `'S3'`). Services without a
1015
// registered extension still get the base rpc span from the subscriber.
1116
this._services = new Map();
17+
this._services.set('SecretsManager', new SecretsManagerServiceExtension());
18+
this._services.set('SFN', new StepFunctionsServiceExtension());
19+
this._services.set('DynamoDB', new DynamodbServiceExtension());
20+
this._services.set('S3', new S3ServiceExtension());
21+
this._services.set('Kinesis', new KinesisServiceExtension());
1222
}
1323

1424
public requestPreSpanHook(request: NormalizedRequest): RequestMetadata {
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import type { Span } from '@sentry/core';
2+
import { SPAN_KIND } from '@sentry/core';
3+
import { DB_NAME, DB_OPERATION, DB_SYSTEM } from '@sentry/conventions/attributes';
4+
import {
5+
ATTR_AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS,
6+
ATTR_AWS_DYNAMODB_CONSISTENT_READ,
7+
ATTR_AWS_DYNAMODB_CONSUMED_CAPACITY,
8+
ATTR_AWS_DYNAMODB_COUNT,
9+
ATTR_AWS_DYNAMODB_EXCLUSIVE_START_TABLE,
10+
ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES,
11+
ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES,
12+
ATTR_AWS_DYNAMODB_INDEX_NAME,
13+
ATTR_AWS_DYNAMODB_ITEM_COLLECTION_METRICS,
14+
ATTR_AWS_DYNAMODB_LIMIT,
15+
ATTR_AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES,
16+
ATTR_AWS_DYNAMODB_PROJECTION,
17+
ATTR_AWS_DYNAMODB_PROVISIONED_READ_CAPACITY,
18+
ATTR_AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY,
19+
ATTR_AWS_DYNAMODB_SCAN_FORWARD,
20+
ATTR_AWS_DYNAMODB_SCANNED_COUNT,
21+
ATTR_AWS_DYNAMODB_SEGMENT,
22+
ATTR_AWS_DYNAMODB_SELECT,
23+
ATTR_AWS_DYNAMODB_TABLE_COUNT,
24+
ATTR_AWS_DYNAMODB_TABLE_NAMES,
25+
ATTR_AWS_DYNAMODB_TOTAL_SEGMENTS,
26+
DB_SYSTEM_VALUE_DYNAMODB,
27+
} from '../constants';
28+
import type { NormalizedRequest, NormalizedResponse } from '../types';
29+
import type { RequestMetadata, ServiceExtension } from './ServiceExtension';
30+
31+
function toArray<T>(values: T | T[]): T[] {
32+
return Array.isArray(values) ? values : [values];
33+
}
34+
35+
export class DynamodbServiceExtension implements ServiceExtension {
36+
public requestPreSpanHook(normalizedRequest: NormalizedRequest): RequestMetadata {
37+
const operation = normalizedRequest.commandName;
38+
const tableName = normalizedRequest.commandInput?.TableName;
39+
40+
const spanAttributes: Record<string, unknown> = {};
41+
42+
/* oxlint-disable typescript/no-deprecated -- old-semconv db.* attributes, matched to the OTel aws-sdk integration */
43+
spanAttributes[DB_SYSTEM] = DB_SYSTEM_VALUE_DYNAMODB;
44+
spanAttributes[DB_NAME] = tableName;
45+
spanAttributes[DB_OPERATION] = operation;
46+
/* oxlint-enable typescript/no-deprecated */
47+
48+
// `RequestItems` is undefined when no table names are returned; its keys are the table names.
49+
if (normalizedRequest.commandInput?.TableName) {
50+
// Necessary for commands with only 1 table name (e.g. CreateTable). Attribute is `TableName`, not keys of `RequestItems`.
51+
spanAttributes[ATTR_AWS_DYNAMODB_TABLE_NAMES] = [normalizedRequest.commandInput.TableName];
52+
} else if (normalizedRequest.commandInput?.RequestItems) {
53+
spanAttributes[ATTR_AWS_DYNAMODB_TABLE_NAMES] = Object.keys(normalizedRequest.commandInput.RequestItems);
54+
}
55+
56+
if (operation === 'CreateTable' || operation === 'UpdateTable') {
57+
// only check for ProvisionedThroughput since ReadCapacityUnits and WriteCapacityUnits are required attributes
58+
if (normalizedRequest.commandInput?.ProvisionedThroughput) {
59+
spanAttributes[ATTR_AWS_DYNAMODB_PROVISIONED_READ_CAPACITY] =
60+
normalizedRequest.commandInput.ProvisionedThroughput.ReadCapacityUnits;
61+
spanAttributes[ATTR_AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY] =
62+
normalizedRequest.commandInput.ProvisionedThroughput.WriteCapacityUnits;
63+
}
64+
}
65+
66+
if (operation === 'GetItem' || operation === 'Scan' || operation === 'Query') {
67+
if (normalizedRequest.commandInput?.ConsistentRead) {
68+
spanAttributes[ATTR_AWS_DYNAMODB_CONSISTENT_READ] = normalizedRequest.commandInput.ConsistentRead;
69+
}
70+
}
71+
72+
if (operation === 'Query' || operation === 'Scan') {
73+
if (normalizedRequest.commandInput?.ProjectionExpression) {
74+
spanAttributes[ATTR_AWS_DYNAMODB_PROJECTION] = normalizedRequest.commandInput.ProjectionExpression;
75+
}
76+
}
77+
78+
if (operation === 'CreateTable') {
79+
if (normalizedRequest.commandInput?.GlobalSecondaryIndexes) {
80+
spanAttributes[ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES] = toArray(
81+
normalizedRequest.commandInput.GlobalSecondaryIndexes,
82+
).map((x: Record<string, any>) => JSON.stringify(x));
83+
}
84+
85+
if (normalizedRequest.commandInput?.LocalSecondaryIndexes) {
86+
spanAttributes[ATTR_AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES] = toArray(
87+
normalizedRequest.commandInput.LocalSecondaryIndexes,
88+
).map((x: Record<string, any>) => JSON.stringify(x));
89+
}
90+
}
91+
92+
if (operation === 'ListTables' || operation === 'Query' || operation === 'Scan') {
93+
if (normalizedRequest.commandInput?.Limit) {
94+
spanAttributes[ATTR_AWS_DYNAMODB_LIMIT] = normalizedRequest.commandInput.Limit;
95+
}
96+
}
97+
98+
if (operation === 'ListTables') {
99+
if (normalizedRequest.commandInput?.ExclusiveStartTableName) {
100+
spanAttributes[ATTR_AWS_DYNAMODB_EXCLUSIVE_START_TABLE] =
101+
normalizedRequest.commandInput.ExclusiveStartTableName;
102+
}
103+
}
104+
105+
if (operation === 'Query') {
106+
if (normalizedRequest.commandInput?.ScanIndexForward) {
107+
spanAttributes[ATTR_AWS_DYNAMODB_SCAN_FORWARD] = normalizedRequest.commandInput.ScanIndexForward;
108+
}
109+
110+
if (normalizedRequest.commandInput?.IndexName) {
111+
spanAttributes[ATTR_AWS_DYNAMODB_INDEX_NAME] = normalizedRequest.commandInput.IndexName;
112+
}
113+
114+
if (normalizedRequest.commandInput?.Select) {
115+
spanAttributes[ATTR_AWS_DYNAMODB_SELECT] = normalizedRequest.commandInput.Select;
116+
}
117+
}
118+
119+
if (operation === 'Scan') {
120+
if (normalizedRequest.commandInput?.Segment) {
121+
spanAttributes[ATTR_AWS_DYNAMODB_SEGMENT] = normalizedRequest.commandInput?.Segment;
122+
}
123+
124+
if (normalizedRequest.commandInput?.TotalSegments) {
125+
spanAttributes[ATTR_AWS_DYNAMODB_TOTAL_SEGMENTS] = normalizedRequest.commandInput?.TotalSegments;
126+
}
127+
128+
if (normalizedRequest.commandInput?.IndexName) {
129+
spanAttributes[ATTR_AWS_DYNAMODB_INDEX_NAME] = normalizedRequest.commandInput.IndexName;
130+
}
131+
132+
if (normalizedRequest.commandInput?.Select) {
133+
spanAttributes[ATTR_AWS_DYNAMODB_SELECT] = normalizedRequest.commandInput.Select;
134+
}
135+
}
136+
137+
if (operation === 'UpdateTable') {
138+
if (normalizedRequest.commandInput?.AttributeDefinitions) {
139+
spanAttributes[ATTR_AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS] = toArray(
140+
normalizedRequest.commandInput.AttributeDefinitions,
141+
).map((x: Record<string, any>) => JSON.stringify(x));
142+
}
143+
144+
if (normalizedRequest.commandInput?.GlobalSecondaryIndexUpdates) {
145+
spanAttributes[ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES] = toArray(
146+
normalizedRequest.commandInput.GlobalSecondaryIndexUpdates,
147+
).map((x: Record<string, any>) => JSON.stringify(x));
148+
}
149+
}
150+
151+
return {
152+
spanAttributes,
153+
spanKind: SPAN_KIND.CLIENT,
154+
};
155+
}
156+
157+
public responseHook(response: NormalizedResponse, span: Span): void {
158+
if (response.data?.ConsumedCapacity) {
159+
span.setAttribute(
160+
ATTR_AWS_DYNAMODB_CONSUMED_CAPACITY,
161+
toArray(response.data.ConsumedCapacity).map((x: Record<string, any>) => JSON.stringify(x)),
162+
);
163+
}
164+
165+
if (response.data?.ItemCollectionMetrics) {
166+
span.setAttribute(
167+
ATTR_AWS_DYNAMODB_ITEM_COLLECTION_METRICS,
168+
toArray(response.data.ItemCollectionMetrics).map((x: Record<string, any>) => JSON.stringify(x)),
169+
);
170+
}
171+
172+
if (response.data?.TableNames) {
173+
span.setAttribute(ATTR_AWS_DYNAMODB_TABLE_COUNT, response.data?.TableNames.length);
174+
}
175+
176+
if (response.data?.Count) {
177+
span.setAttribute(ATTR_AWS_DYNAMODB_COUNT, response.data?.Count);
178+
}
179+
180+
if (response.data?.ScannedCount) {
181+
span.setAttribute(ATTR_AWS_DYNAMODB_SCANNED_COUNT, response.data?.ScannedCount);
182+
}
183+
}
184+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { SPAN_KIND } from '@sentry/core';
2+
import { AWS_KINESIS_STREAM_NAME } from '../constants';
3+
import type { NormalizedRequest } from '../types';
4+
import type { RequestMetadata, ServiceExtension } from './ServiceExtension';
5+
6+
export class KinesisServiceExtension implements ServiceExtension {
7+
public requestPreSpanHook(request: NormalizedRequest): RequestMetadata {
8+
const streamName = request.commandInput?.StreamName;
9+
const spanAttributes: Record<string, unknown> = {};
10+
11+
if (streamName) {
12+
spanAttributes[AWS_KINESIS_STREAM_NAME] = streamName;
13+
}
14+
15+
return {
16+
spanAttributes,
17+
spanKind: SPAN_KIND.CLIENT,
18+
};
19+
}
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { SPAN_KIND } from '@sentry/core';
2+
import { AWS_S3_BUCKET } from '../constants';
3+
import type { NormalizedRequest } from '../types';
4+
import type { RequestMetadata, ServiceExtension } from './ServiceExtension';
5+
6+
export class S3ServiceExtension implements ServiceExtension {
7+
public requestPreSpanHook(request: NormalizedRequest): RequestMetadata {
8+
const bucketName = request.commandInput?.Bucket;
9+
const spanAttributes: Record<string, unknown> = {};
10+
11+
if (bucketName) {
12+
spanAttributes[AWS_S3_BUCKET] = bucketName;
13+
}
14+
15+
return {
16+
spanAttributes,
17+
spanKind: SPAN_KIND.CLIENT,
18+
};
19+
}
20+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Span } from '@sentry/core';
2+
import { SPAN_KIND } from '@sentry/core';
3+
import { ATTR_AWS_SECRETSMANAGER_SECRET_ARN } from '../constants';
4+
import type { NormalizedRequest, NormalizedResponse } from '../types';
5+
import type { RequestMetadata, ServiceExtension } from './ServiceExtension';
6+
7+
export class SecretsManagerServiceExtension implements ServiceExtension {
8+
public requestPreSpanHook(request: NormalizedRequest): RequestMetadata {
9+
const secretId = request.commandInput?.SecretId;
10+
const spanAttributes: Record<string, unknown> = {};
11+
if (typeof secretId === 'string' && secretId.startsWith('arn:aws:secretsmanager:')) {
12+
spanAttributes[ATTR_AWS_SECRETSMANAGER_SECRET_ARN] = secretId;
13+
}
14+
15+
return {
16+
spanAttributes,
17+
spanKind: SPAN_KIND.CLIENT,
18+
};
19+
}
20+
21+
public responseHook(response: NormalizedResponse, span: Span): void {
22+
const secretArn = response.data?.ARN;
23+
if (secretArn) {
24+
span.setAttribute(ATTR_AWS_SECRETSMANAGER_SECRET_ARN, secretArn);
25+
}
26+
}
27+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { SPAN_KIND } from '@sentry/core';
2+
import { ATTR_AWS_STEP_FUNCTIONS_ACTIVITY_ARN, ATTR_AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN } from '../constants';
3+
import type { NormalizedRequest } from '../types';
4+
import type { RequestMetadata, ServiceExtension } from './ServiceExtension';
5+
6+
export class StepFunctionsServiceExtension implements ServiceExtension {
7+
public requestPreSpanHook(request: NormalizedRequest): RequestMetadata {
8+
const stateMachineArn = request.commandInput?.stateMachineArn;
9+
const activityArn = request.commandInput?.activityArn;
10+
const spanAttributes: Record<string, unknown> = {};
11+
12+
if (stateMachineArn) {
13+
spanAttributes[ATTR_AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN] = stateMachineArn;
14+
}
15+
16+
if (activityArn) {
17+
spanAttributes[ATTR_AWS_STEP_FUNCTIONS_ACTIVITY_ARN] = activityArn;
18+
}
19+
20+
return {
21+
spanAttributes,
22+
spanKind: SPAN_KIND.CLIENT,
23+
};
24+
}
25+
}

0 commit comments

Comments
 (0)