Skip to content

Commit 017bfae

Browse files
committed
feat(server-utils): Port S3, Kinesis, DynamoDB, SecretsManager and StepFunctions aws-sdk extensions
1 parent 54789f2 commit 017bfae

9 files changed

Lines changed: 343 additions & 6 deletions

File tree

.oxlintrc.base.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,15 @@
150150
{
151151
"files": ["**/integrations/tracing-channel/aws-sdk/**/*.ts"],
152152
"rules": {
153-
"typescript/no-explicit-any": "off"
153+
"typescript/no-explicit-any": "off",
154+
"typescript/no-unsafe-member-access": "off"
155+
}
156+
},
157+
{
158+
"files": ["**/integrations/tracing-channel/aws-sdk/services/**/*.ts"],
159+
"rules": {
160+
"max-lines": "off",
161+
"complexity": "off"
154162
}
155163
},
156164
{

.size-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ module.exports = [
400400
import: createImport('init', 'experimentalUseDiagnosticsChannelInjection'),
401401
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
402402
gzip: true,
403-
limit: '142 KB',
403+
limit: '144 KB',
404404
disablePlugins: ['@size-limit/esbuild'],
405405
},
406406
{
Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,48 @@
11
/**
2-
* AWS-specific span constants used by the aws-sdk channel integration that are NOT covered by
3-
* `@sentry/conventions/attributes` (attribute names that exist there are imported from there
4-
* directly). Per-service files append their own such constants below.
2+
* AWS-specific span attribute names used by the aws-sdk channel integration.
3+
*
4+
* These mirror the constants the OTel `@opentelemetry/instrumentation-aws-sdk` emits (some are
5+
* unstable/obsolete OTel semantic conventions with no `ATTR_*` export in
6+
* `@opentelemetry/semantic-conventions`), inlined here so the integration stays free of OTel deps.
7+
* Attributes that exist in `@sentry/conventions/attributes` are imported from there instead;
8+
* TODO(aws-sdk): the active attributes below are being added to sentry-conventions and should move
9+
* to `@sentry/conventions/attributes` imports once a release containing them ships.
510
*/
611

712
/** The span origin every aws-sdk channel span carries, mirroring the uniform OTel `auto.otel.aws`. */
813
export const AWS_SDK_ORIGIN = 'auto.aws.orchestrion.aws_sdk';
14+
15+
export const ATTR_RPC_SYSTEM = 'rpc.system';
16+
export const AWS_REQUEST_ID = 'aws.request.id';
17+
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: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
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
// Per-service extensions, keyed by the client's `serviceId` (e.g. `'S3'`). Services without a
712
// registered extension still get the base rpc span from the subscriber.
8-
private _services: Map<string, ServiceExtension> = new Map();
13+
private _services: Map<string, ServiceExtension> = new Map<string, ServiceExtension>([
14+
['SecretsManager', new SecretsManagerServiceExtension()],
15+
['SFN', new StepFunctionsServiceExtension()],
16+
['DynamoDB', new DynamodbServiceExtension()],
17+
['S3', new S3ServiceExtension()],
18+
['Kinesis', new KinesisServiceExtension()],
19+
]);
920

1021
public requestPreSpanHook(request: NormalizedRequest): RequestMetadata {
1122
const serviceExtension = this._services.get(request.serviceName);
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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: unknown) => 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: unknown) => 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: unknown) => 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: unknown) => JSON.stringify(x));
148+
}
149+
}
150+
151+
return {
152+
spanAttributes,
153+
spanKind: SPAN_KIND.CLIENT,
154+
// Matches what the exporter infers from `db.system` for the OTel DynamoDB spans.
155+
spanOp: 'db',
156+
};
157+
}
158+
159+
public responseHook(response: NormalizedResponse, span: Span): void {
160+
if (response.data?.ConsumedCapacity) {
161+
span.setAttribute(
162+
ATTR_AWS_DYNAMODB_CONSUMED_CAPACITY,
163+
toArray(response.data.ConsumedCapacity).map((x: unknown) => JSON.stringify(x)),
164+
);
165+
}
166+
167+
if (response.data?.ItemCollectionMetrics) {
168+
span.setAttribute(
169+
ATTR_AWS_DYNAMODB_ITEM_COLLECTION_METRICS,
170+
toArray(response.data.ItemCollectionMetrics).map((x: unknown) => JSON.stringify(x)),
171+
);
172+
}
173+
174+
if (response.data?.TableNames) {
175+
span.setAttribute(ATTR_AWS_DYNAMODB_TABLE_COUNT, response.data?.TableNames.length);
176+
}
177+
178+
if (response.data?.Count) {
179+
span.setAttribute(ATTR_AWS_DYNAMODB_COUNT, response.data?.Count);
180+
}
181+
182+
if (response.data?.ScannedCount) {
183+
span.setAttribute(ATTR_AWS_DYNAMODB_SCANNED_COUNT, response.data?.ScannedCount);
184+
}
185+
}
186+
}
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)