@senzops/apm-node 1.2.7 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +479 -398
  3. package/dist/index.d.mts +5 -0
  4. package/dist/index.d.ts +5 -0
  5. package/dist/index.global.js +1 -1
  6. package/dist/index.global.js.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +1 -1
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/register.js +1 -1
  12. package/dist/register.js.map +1 -1
  13. package/dist/register.mjs +1 -1
  14. package/dist/register.mjs.map +1 -1
  15. package/package.json +1 -1
  16. package/src/core/client.ts +57 -0
  17. package/src/core/context.ts +71 -9
  18. package/src/core/transport.ts +20 -3
  19. package/src/core/types.ts +5 -1
  20. package/src/index.ts +4 -0
  21. package/src/instrumentation/amqplib.ts +371 -0
  22. package/src/instrumentation/anthropic.ts +245 -0
  23. package/src/instrumentation/aws-sdk.ts +403 -0
  24. package/src/instrumentation/azure-openai.ts +177 -0
  25. package/src/instrumentation/bunyan.ts +93 -0
  26. package/src/instrumentation/cassandra.ts +367 -0
  27. package/src/instrumentation/cohere.ts +227 -0
  28. package/src/instrumentation/connect.ts +200 -0
  29. package/src/instrumentation/dataloader.ts +291 -0
  30. package/src/instrumentation/dns.ts +220 -0
  31. package/src/instrumentation/firebase.ts +445 -0
  32. package/src/instrumentation/fs.ts +260 -0
  33. package/src/instrumentation/generic-pool.ts +317 -0
  34. package/src/instrumentation/google-genai.ts +426 -0
  35. package/src/instrumentation/graphql.ts +434 -0
  36. package/src/instrumentation/grpc.ts +666 -0
  37. package/src/instrumentation/hapi.ts +257 -0
  38. package/src/instrumentation/kafka.ts +360 -0
  39. package/src/instrumentation/knex.ts +249 -0
  40. package/src/instrumentation/lru-memoizer.ts +175 -0
  41. package/src/instrumentation/memcached.ts +190 -0
  42. package/src/instrumentation/mistral.ts +254 -0
  43. package/src/instrumentation/nestjs.ts +243 -0
  44. package/src/instrumentation/net.ts +171 -0
  45. package/src/instrumentation/openai.ts +281 -0
  46. package/src/instrumentation/pino.ts +170 -0
  47. package/src/instrumentation/restify.ts +213 -0
  48. package/src/instrumentation/runtime.ts +352 -0
  49. package/src/instrumentation/socketio.ts +272 -0
  50. package/src/instrumentation/tedious.ts +509 -0
  51. package/src/instrumentation/winston.ts +149 -0
  52. package/src/register.ts +22 -3
  53. package/src/wrappers/lambda.ts +417 -0
  54. package/tsup.config.ts +3 -3
  55. package/wiki.md +1547 -852
@@ -0,0 +1,245 @@
1
+ import { SenzorOptions } from '../core/types';
2
+ import { hookRequire } from './hook';
3
+ import { patchMethod } from './patch';
4
+ import { runWithCapturedSpan, startCapturedSpan } from './span';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Anthropic SDK Instrumentation
8
+ //
9
+ // Instruments the official `@anthropic-ai/sdk` package (v0.20+).
10
+ //
11
+ // The Anthropic SDK is architecturally identical to OpenAI v4 — both are
12
+ // built on the same Stainless-generated base client (`APIClient`) with
13
+ // HTTP dispatch via `.post()`, `.get()`, etc.
14
+ //
15
+ // Patches:
16
+ // - Anthropic.prototype.post/get/put/patch/delete — HTTP dispatch methods
17
+ // - Anthropic.prototype._request — internal request dispatcher
18
+ //
19
+ // This covers ALL API calls:
20
+ // - messages.create() (streaming & non-streaming)
21
+ // - messages.batches.create()
22
+ // - completions.create() (legacy)
23
+ //
24
+ // Captured attributes (OTel GenAI semantic conventions):
25
+ // - gen_ai.system: 'anthropic'
26
+ // - gen_ai.request.model: claude-sonnet-4-20250514, etc.
27
+ // - gen_ai.operation.name: messages, completions, batches
28
+ // - gen_ai.response.model: actual model from response
29
+ // - gen_ai.usage.input_tokens: prompt token count
30
+ // - gen_ai.usage.output_tokens: completion token count
31
+ // - gen_ai.request.max_tokens: max_tokens parameter
32
+ // - gen_ai.request.temperature: temperature parameter
33
+ // - gen_ai.response.stop_reason: end_turn, max_tokens, etc.
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /** Map API path segments to operation names. */
37
+ const OPERATION_MAP: Record<string, string> = {
38
+ messages: 'messages',
39
+ 'messages/batches': 'messages.batches',
40
+ completions: 'completions',
41
+ };
42
+
43
+ const getOperationName = (path: string): string => {
44
+ if (!path) return 'unknown';
45
+ const normalized = path.replace(/^\/?(v1\/)?/, '');
46
+
47
+ for (const [pattern, name] of Object.entries(OPERATION_MAP)) {
48
+ if (normalized === pattern || normalized.startsWith(pattern + '/')) {
49
+ return name;
50
+ }
51
+ }
52
+
53
+ return normalized.split('/')[0] || 'api';
54
+ };
55
+
56
+ const getRequestModel = (body: any): string | undefined => {
57
+ if (!body || typeof body !== 'object') return undefined;
58
+ return body.model || undefined;
59
+ };
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Core client patching
63
+ // ---------------------------------------------------------------------------
64
+
65
+ const patchAnthropicClient = (anthropicModule: any, options?: SenzorOptions) => {
66
+ const Anthropic = anthropicModule?.Anthropic
67
+ || anthropicModule?.default
68
+ || anthropicModule;
69
+
70
+ if (!Anthropic || typeof Anthropic !== 'function') return;
71
+
72
+ const proto = Anthropic.prototype;
73
+ if (!proto) return;
74
+
75
+ // Patch HTTP dispatch methods
76
+ const httpMethods = ['post', 'get', 'put', 'patch', 'delete'] as const;
77
+
78
+ for (const method of httpMethods) {
79
+ if (typeof proto[method] !== 'function') continue;
80
+
81
+ patchMethod(
82
+ proto,
83
+ method,
84
+ `senzor.anthropic.client.${method}`,
85
+ (original) =>
86
+ function patchedMethod(this: any, path: string, opts?: any) {
87
+ const operationName = getOperationName(path);
88
+ const model = getRequestModel(opts?.body);
89
+ const httpMethod = method.toUpperCase();
90
+
91
+ const spanName = model
92
+ ? `Anthropic ${operationName} ${model}`
93
+ : `Anthropic ${operationName}`;
94
+
95
+ const span = startCapturedSpan(
96
+ spanName,
97
+ 'http',
98
+ {
99
+ 'gen_ai.system': 'anthropic',
100
+ 'gen_ai.operation.name': operationName,
101
+ 'gen_ai.request.model': model,
102
+ 'gen_ai.request.max_tokens': opts?.body?.max_tokens,
103
+ 'gen_ai.request.temperature': opts?.body?.temperature,
104
+ 'gen_ai.request.top_p': opts?.body?.top_p,
105
+ 'http.request.method': httpMethod,
106
+ 'url.path': path,
107
+ library: 'anthropic',
108
+ },
109
+ options
110
+ );
111
+
112
+ if (!span) return original.call(this, path, opts);
113
+
114
+ return runWithCapturedSpan(span, () => {
115
+ try {
116
+ const result = original.call(this, path, opts);
117
+
118
+ if (result && typeof result.then === 'function') {
119
+ return result.then(
120
+ (response: any) => {
121
+ const endMeta: Record<string, any> = {};
122
+
123
+ // Anthropic response format
124
+ if (response?.usage) {
125
+ endMeta['gen_ai.usage.input_tokens'] = response.usage.input_tokens;
126
+ endMeta['gen_ai.usage.output_tokens'] = response.usage.output_tokens;
127
+ }
128
+ if (response?.model) {
129
+ endMeta['gen_ai.response.model'] = response.model;
130
+ }
131
+ if (response?.stop_reason) {
132
+ endMeta['gen_ai.response.stop_reason'] = response.stop_reason;
133
+ }
134
+
135
+ span.end(0, endMeta);
136
+ return response;
137
+ },
138
+ (error: any) => {
139
+ const statusCode = error?.status || error?.statusCode || 500;
140
+ span.end(statusCode, {
141
+ 'error.message': error?.message,
142
+ 'error.type': error?.name || error?.type || 'AnthropicError',
143
+ 'http.response.status_code': statusCode,
144
+ 'gen_ai.error.code': error?.error?.type,
145
+ });
146
+ throw error;
147
+ }
148
+ );
149
+ }
150
+
151
+ span.end(0);
152
+ return result;
153
+ } catch (error: any) {
154
+ span.end(error?.status || 500, {
155
+ 'error.message': error?.message,
156
+ 'error.type': error?.name || 'Error',
157
+ });
158
+ throw error;
159
+ }
160
+ });
161
+ }
162
+ );
163
+ }
164
+
165
+ // Also patch _request as fallback
166
+ if (typeof proto._request === 'function') {
167
+ patchMethod(
168
+ proto,
169
+ '_request',
170
+ 'senzor.anthropic.client._request',
171
+ (original) =>
172
+ function patchedRequest(this: any, requestOptions: any, ...args: any[]) {
173
+ const path = requestOptions?.path || '';
174
+ const method = requestOptions?.method || 'POST';
175
+ const operationName = getOperationName(path);
176
+ const model = getRequestModel(requestOptions?.body);
177
+
178
+ const spanName = model
179
+ ? `Anthropic ${operationName} ${model}`
180
+ : `Anthropic ${operationName}`;
181
+
182
+ const span = startCapturedSpan(
183
+ spanName,
184
+ 'http',
185
+ {
186
+ 'gen_ai.system': 'anthropic',
187
+ 'gen_ai.operation.name': operationName,
188
+ 'gen_ai.request.model': model,
189
+ 'http.request.method': method,
190
+ 'url.path': path,
191
+ library: 'anthropic',
192
+ },
193
+ options
194
+ );
195
+
196
+ if (!span) return original.call(this, requestOptions, ...args);
197
+
198
+ return runWithCapturedSpan(span, () => {
199
+ try {
200
+ const result = original.call(this, requestOptions, ...args);
201
+
202
+ if (result && typeof result.then === 'function') {
203
+ return result.then(
204
+ (response: any) => {
205
+ const endMeta: Record<string, any> = {};
206
+ if (response?.usage) {
207
+ endMeta['gen_ai.usage.input_tokens'] = response.usage.input_tokens;
208
+ endMeta['gen_ai.usage.output_tokens'] = response.usage.output_tokens;
209
+ }
210
+ if (response?.model) {
211
+ endMeta['gen_ai.response.model'] = response.model;
212
+ }
213
+ span.end(0, endMeta);
214
+ return response;
215
+ },
216
+ (error: any) => {
217
+ span.end(error?.status || 500, {
218
+ 'error.message': error?.message,
219
+ });
220
+ throw error;
221
+ }
222
+ );
223
+ }
224
+
225
+ span.end(0);
226
+ return result;
227
+ } catch (error: any) {
228
+ span.end(500, { 'error.message': error?.message });
229
+ throw error;
230
+ }
231
+ });
232
+ }
233
+ );
234
+ }
235
+ };
236
+
237
+ // ---------------------------------------------------------------------------
238
+ // Public API
239
+ // ---------------------------------------------------------------------------
240
+
241
+ export const instrumentAnthropic = (options?: SenzorOptions) => {
242
+ hookRequire('@anthropic-ai/sdk', (exports: any) => {
243
+ patchAnthropicClient(exports, options);
244
+ });
245
+ };
@@ -0,0 +1,403 @@
1
+ import { SenzorOptions } from '../core/types';
2
+ import { hookRequire } from './hook';
3
+ import { patchMethod } from './patch';
4
+ import { runWithCapturedSpan, startCapturedSpan } from './span';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // AWS SDK v3 Instrumentation
8
+ //
9
+ // Instruments @aws-sdk/smithy-client (the core of AWS SDK v3) by patching
10
+ // the Client.prototype.send() method — the single dispatch point for ALL
11
+ // AWS service calls (S3, DynamoDB, SQS, SNS, Lambda, etc.).
12
+ //
13
+ // Also hooks individual service packages as a fallback, intercepting their
14
+ // Client classes directly.
15
+ //
16
+ // Captured span attributes follow OTel semantic conventions for cloud/AWS:
17
+ // - rpc.system: 'aws-api'
18
+ // - rpc.service: e.g. 'S3', 'DynamoDB'
19
+ // - rpc.method: e.g. 'PutObject', 'GetItem'
20
+ // - aws.region: configured region
21
+ // - aws.request_id: from response metadata
22
+ // - http.response.status_code: HTTP status of the API call
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /** Known AWS service name mappings from client constructor names. */
26
+ const SERVICE_NAME_MAP: Record<string, string> = {
27
+ S3Client: 'S3',
28
+ DynamoDBClient: 'DynamoDB',
29
+ SQSClient: 'SQS',
30
+ SNSClient: 'SNS',
31
+ LambdaClient: 'Lambda',
32
+ SESClient: 'SES',
33
+ SESv2Client: 'SESv2',
34
+ CloudWatchClient: 'CloudWatch',
35
+ CloudWatchLogsClient: 'CloudWatchLogs',
36
+ KinesisClient: 'Kinesis',
37
+ EventBridgeClient: 'EventBridge',
38
+ SecretsManagerClient: 'SecretsManager',
39
+ SSMClient: 'SSM',
40
+ STSClient: 'STS',
41
+ IAMClient: 'IAM',
42
+ EC2Client: 'EC2',
43
+ ECSClient: 'ECS',
44
+ EKSClient: 'EKS',
45
+ RDSClient: 'RDS',
46
+ ElastiCacheClient: 'ElastiCache',
47
+ RedshiftClient: 'Redshift',
48
+ CognitoIdentityProviderClient: 'CognitoIdentityProvider',
49
+ Route53Client: 'Route53',
50
+ CloudFrontClient: 'CloudFront',
51
+ APIGatewayClient: 'APIGateway',
52
+ StepFunctionsClient: 'StepFunctions',
53
+ CodeBuildClient: 'CodeBuild',
54
+ CodePipelineClient: 'CodePipeline',
55
+ ACMClient: 'ACM',
56
+ KMSClient: 'KMS',
57
+ BedrockRuntimeClient: 'BedrockRuntime',
58
+ };
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Bedrock Runtime — GenAI attribute extraction
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /** Bedrock model ID patterns for gen_ai.system mapping. */
65
+ const BEDROCK_SYSTEM_MAP: Record<string, string> = {
66
+ anthropic: 'anthropic',
67
+ amazon: 'aws_bedrock',
68
+ meta: 'meta',
69
+ cohere: 'cohere',
70
+ mistral: 'mistral',
71
+ ai21: 'ai21',
72
+ stability: 'stability',
73
+ };
74
+
75
+ const getBedrockSystem = (modelId: string | undefined): string => {
76
+ if (!modelId) return 'aws_bedrock';
77
+ const provider = modelId.split('.')[0]?.toLowerCase();
78
+ return BEDROCK_SYSTEM_MAP[provider] || 'aws_bedrock';
79
+ };
80
+
81
+ /** Extract Bedrock-specific attributes from InvokeModel / InvokeModelWithResponseStream commands. */
82
+ const extractBedrockAttributes = (command: any, response: any): Record<string, any> => {
83
+ const meta: Record<string, any> = {};
84
+ const input = command?.input;
85
+ if (!input) return meta;
86
+
87
+ const modelId = input.modelId;
88
+ if (modelId) {
89
+ meta['gen_ai.system'] = getBedrockSystem(modelId);
90
+ meta['gen_ai.request.model'] = modelId;
91
+ }
92
+
93
+ // Try to parse the response body for token usage
94
+ // Bedrock responses are in the 'body' field as a Uint8Array or string
95
+ try {
96
+ let bodyStr: string | undefined;
97
+ if (response?.body) {
98
+ if (typeof response.body === 'string') {
99
+ bodyStr = response.body;
100
+ } else if (response.body instanceof Uint8Array) {
101
+ bodyStr = new TextDecoder().decode(response.body);
102
+ } else if (Buffer.isBuffer(response.body)) {
103
+ bodyStr = response.body.toString('utf-8');
104
+ }
105
+ }
106
+
107
+ if (bodyStr) {
108
+ const parsed = JSON.parse(bodyStr);
109
+
110
+ // Anthropic Messages API format
111
+ if (parsed.usage) {
112
+ if (parsed.usage.input_tokens !== undefined) {
113
+ meta['gen_ai.usage.input_tokens'] = parsed.usage.input_tokens;
114
+ }
115
+ if (parsed.usage.output_tokens !== undefined) {
116
+ meta['gen_ai.usage.output_tokens'] = parsed.usage.output_tokens;
117
+ }
118
+ }
119
+
120
+ // Amazon Titan format
121
+ if (parsed.inputTextTokenCount !== undefined) {
122
+ meta['gen_ai.usage.input_tokens'] = meta['gen_ai.usage.input_tokens'] || parsed.inputTextTokenCount;
123
+ }
124
+ if (parsed.results?.[0]?.tokenCount !== undefined) {
125
+ meta['gen_ai.usage.output_tokens'] = meta['gen_ai.usage.output_tokens'] || parsed.results[0].tokenCount;
126
+ }
127
+
128
+ // Cohere format
129
+ if (parsed.meta?.billed_units) {
130
+ meta['gen_ai.usage.input_tokens'] = meta['gen_ai.usage.input_tokens'] || parsed.meta.billed_units.input_tokens;
131
+ meta['gen_ai.usage.output_tokens'] = meta['gen_ai.usage.output_tokens'] || parsed.meta.billed_units.output_tokens;
132
+ }
133
+
134
+ // Stop reason / finish reason
135
+ if (parsed.stop_reason) meta['gen_ai.response.finish_reason'] = parsed.stop_reason;
136
+ if (parsed.completionReason) meta['gen_ai.response.finish_reason'] = parsed.completionReason;
137
+ }
138
+ } catch {
139
+ // Body parsing is best-effort — the span still captures the Bedrock call
140
+ }
141
+
142
+ return meta;
143
+ };
144
+
145
+ /** Check if a command is a Bedrock model invocation. */
146
+ const isBedrockInvocation = (operationName: string): boolean =>
147
+ operationName === 'InvokeModel' ||
148
+ operationName === 'InvokeModelWithResponseStream' ||
149
+ operationName === 'Converse' ||
150
+ operationName === 'ConverseStream';
151
+
152
+ /** Extract Bedrock Converse API attributes. */
153
+ const extractBedrockConverseAttributes = (command: any, response: any): Record<string, any> => {
154
+ const meta: Record<string, any> = {};
155
+ const input = command?.input;
156
+ if (!input) return meta;
157
+
158
+ const modelId = input.modelId;
159
+ if (modelId) {
160
+ meta['gen_ai.system'] = getBedrockSystem(modelId);
161
+ meta['gen_ai.request.model'] = modelId;
162
+ }
163
+
164
+ // Converse API returns usage directly in the response object
165
+ if (response?.usage) {
166
+ if (response.usage.inputTokens !== undefined) {
167
+ meta['gen_ai.usage.input_tokens'] = response.usage.inputTokens;
168
+ }
169
+ if (response.usage.outputTokens !== undefined) {
170
+ meta['gen_ai.usage.output_tokens'] = response.usage.outputTokens;
171
+ }
172
+ if (response.usage.totalTokens !== undefined) {
173
+ meta['gen_ai.usage.total_tokens'] = response.usage.totalTokens;
174
+ }
175
+ }
176
+
177
+ if (response?.stopReason) {
178
+ meta['gen_ai.response.finish_reason'] = response.stopReason;
179
+ }
180
+
181
+ return meta;
182
+ };
183
+
184
+ /** Extract service name from client instance or command. */
185
+ const getServiceName = (client: any): string => {
186
+ // Try constructor name mapping
187
+ const ctorName = client?.constructor?.name;
188
+ if (ctorName && SERVICE_NAME_MAP[ctorName]) {
189
+ return SERVICE_NAME_MAP[ctorName];
190
+ }
191
+
192
+ // Try config.serviceId (AWS SDK v3 standard)
193
+ if (client?.config?.serviceId) {
194
+ return client.config.serviceId;
195
+ }
196
+
197
+ // Try middleware stack metadata
198
+ if (client?.middlewareStack?.identify) {
199
+ try {
200
+ const id = client.middlewareStack.identify();
201
+ if (typeof id === 'string' && id.length > 0) {
202
+ return id.split(' ')[0];
203
+ }
204
+ } catch { }
205
+ }
206
+
207
+ // Fallback: strip 'Client' suffix from constructor name
208
+ if (ctorName && ctorName.endsWith('Client')) {
209
+ return ctorName.slice(0, -6) || 'AWS';
210
+ }
211
+
212
+ return 'AWS';
213
+ };
214
+
215
+ /** Extract operation name from a command object. */
216
+ const getOperationName = (command: any): string => {
217
+ const name = command?.constructor?.name;
218
+ if (!name) return 'UnknownCommand';
219
+ // Strip 'Command' suffix: PutObjectCommand → PutObject
220
+ return name.endsWith('Command') ? name.slice(0, -7) : name;
221
+ };
222
+
223
+ /** Extract region from client config. */
224
+ const getRegion = async (client: any): Promise<string | undefined> => {
225
+ try {
226
+ const region = client?.config?.region;
227
+ if (typeof region === 'function') {
228
+ return await region();
229
+ }
230
+ return region || undefined;
231
+ } catch {
232
+ return undefined;
233
+ }
234
+ };
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // Core send() patching
238
+ // ---------------------------------------------------------------------------
239
+
240
+ const patchClientSend = (clientProto: any, options?: SenzorOptions) => {
241
+ if (!clientProto) return;
242
+
243
+ patchMethod(
244
+ clientProto,
245
+ 'send',
246
+ 'senzor.aws-sdk.client.send',
247
+ (original) =>
248
+ function patchedSend(this: any, command: any, ...args: any[]) {
249
+ const serviceName = getServiceName(this);
250
+ const operationName = getOperationName(command);
251
+ const spanName = `AWS ${serviceName} ${operationName}`;
252
+
253
+ const span = startCapturedSpan(
254
+ spanName,
255
+ 'http',
256
+ {
257
+ 'rpc.system': 'aws-api',
258
+ 'rpc.service': serviceName,
259
+ 'rpc.method': operationName,
260
+ 'cloud.provider': 'aws',
261
+ 'aws.service': serviceName,
262
+ 'aws.operation': operationName,
263
+ },
264
+ options
265
+ );
266
+
267
+ if (!span) return original.call(this, command, ...args);
268
+
269
+ // Resolve region asynchronously but don't block
270
+ getRegion(this).then((region) => {
271
+ if (region && span) {
272
+ // Region is added on span end via meta
273
+ (span as any).__awsRegion = region;
274
+ }
275
+ }).catch(() => { });
276
+
277
+ return runWithCapturedSpan(span, () => {
278
+ try {
279
+ const result = original.call(this, command, ...args);
280
+
281
+ if (result && typeof result.then === 'function') {
282
+ return result.then(
283
+ (response: any) => {
284
+ const statusCode = response?.$metadata?.httpStatusCode;
285
+ const requestId = response?.$metadata?.requestId;
286
+
287
+ const endMeta: Record<string, any> = {
288
+ 'aws.request_id': requestId,
289
+ 'aws.region': (span as any).__awsRegion,
290
+ 'http.response.status_code': statusCode,
291
+ };
292
+
293
+ // Bedrock GenAI attribute extraction
294
+ if (isBedrockInvocation(operationName)) {
295
+ const op = operationName;
296
+ const bedrockMeta = (op === 'Converse' || op === 'ConverseStream')
297
+ ? extractBedrockConverseAttributes(command, response)
298
+ : extractBedrockAttributes(command, response);
299
+ Object.assign(endMeta, bedrockMeta);
300
+ }
301
+
302
+ span.end(
303
+ statusCode && statusCode >= 400 ? statusCode : 0,
304
+ endMeta
305
+ );
306
+
307
+ return response;
308
+ },
309
+ (error: any) => {
310
+ const statusCode = error?.$metadata?.httpStatusCode || 500;
311
+ const requestId = error?.$metadata?.requestId;
312
+
313
+ span.end(statusCode, {
314
+ 'error.message': error?.message,
315
+ 'error.type': error?.name || error?.code || 'AwsError',
316
+ 'aws.request_id': requestId,
317
+ 'aws.region': (span as any).__awsRegion,
318
+ 'http.response.status_code': statusCode,
319
+ });
320
+
321
+ throw error;
322
+ }
323
+ );
324
+ }
325
+
326
+ span.end(0);
327
+ return result;
328
+ } catch (error: any) {
329
+ span.end(error?.$metadata?.httpStatusCode || 500, {
330
+ 'error.message': error?.message,
331
+ 'error.type': error?.name || 'Error',
332
+ });
333
+ throw error;
334
+ }
335
+ });
336
+ }
337
+ );
338
+ };
339
+
340
+ // ---------------------------------------------------------------------------
341
+ // Public API
342
+ // ---------------------------------------------------------------------------
343
+
344
+ /** Common AWS SDK v3 service packages to instrument. */
345
+ const AWS_SERVICE_PACKAGES = [
346
+ '@aws-sdk/client-s3',
347
+ '@aws-sdk/client-dynamodb',
348
+ '@aws-sdk/client-sqs',
349
+ '@aws-sdk/client-sns',
350
+ '@aws-sdk/client-lambda',
351
+ '@aws-sdk/client-ses',
352
+ '@aws-sdk/client-sesv2',
353
+ '@aws-sdk/client-cloudwatch',
354
+ '@aws-sdk/client-cloudwatch-logs',
355
+ '@aws-sdk/client-kinesis',
356
+ '@aws-sdk/client-eventbridge',
357
+ '@aws-sdk/client-secrets-manager',
358
+ '@aws-sdk/client-ssm',
359
+ '@aws-sdk/client-sts',
360
+ '@aws-sdk/client-iam',
361
+ '@aws-sdk/client-ec2',
362
+ '@aws-sdk/client-ecs',
363
+ '@aws-sdk/client-rds',
364
+ '@aws-sdk/client-cognito-identity-provider',
365
+ '@aws-sdk/client-route-53',
366
+ '@aws-sdk/client-cloudfront',
367
+ '@aws-sdk/client-api-gateway',
368
+ '@aws-sdk/client-sfn',
369
+ '@aws-sdk/client-codebuild',
370
+ '@aws-sdk/client-kms',
371
+ '@aws-sdk/client-bedrock-runtime',
372
+ ];
373
+
374
+ export const instrumentAwsSdk = (options?: SenzorOptions) => {
375
+ // Primary: patch the smithy Client base class — covers ALL services
376
+ hookRequire('@smithy/smithy-client', (exports: any) => {
377
+ const Client = exports?.Client;
378
+ if (Client?.prototype) {
379
+ patchClientSend(Client.prototype, options);
380
+ }
381
+ });
382
+
383
+ // Also try the older @aws-sdk/smithy-client path
384
+ hookRequire('@aws-sdk/smithy-client', (exports: any) => {
385
+ const Client = exports?.Client;
386
+ if (Client?.prototype) {
387
+ patchClientSend(Client.prototype, options);
388
+ }
389
+ });
390
+
391
+ // Fallback: hook individual service packages for resilience
392
+ // Only hook a subset of the most common ones to avoid over-registration
393
+ for (const pkg of AWS_SERVICE_PACKAGES) {
394
+ hookRequire(pkg, (exports: any) => {
395
+ // Each package exports a *Client class (e.g., S3Client, DynamoDBClient)
396
+ for (const key of Object.keys(exports)) {
397
+ if (key.endsWith('Client') && exports[key]?.prototype?.send) {
398
+ patchClientSend(exports[key].prototype, options);
399
+ }
400
+ }
401
+ });
402
+ }
403
+ };