@senzops/apm-node 1.2.8 → 1.3.1
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.
- package/CHANGELOG.md +13 -0
- package/README.md +527 -398
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/lambda-handler.d.mts +13 -0
- package/dist/lambda-handler.d.ts +13 -0
- package/dist/lambda-handler.js +2 -0
- package/dist/lambda-handler.js.map +1 -0
- package/dist/lambda-handler.mjs +2 -0
- package/dist/lambda-handler.mjs.map +1 -0
- package/dist/register.js +1 -1
- package/dist/register.js.map +1 -1
- package/dist/register.mjs +1 -1
- package/dist/register.mjs.map +1 -1
- package/package.json +6 -1
- package/src/core/client.ts +57 -0
- package/src/core/transport.ts +20 -3
- package/src/core/types.ts +5 -1
- package/src/index.ts +4 -0
- package/src/instrumentation/amqplib.ts +371 -0
- package/src/instrumentation/anthropic.ts +245 -0
- package/src/instrumentation/aws-sdk.ts +403 -0
- package/src/instrumentation/azure-openai.ts +177 -0
- package/src/instrumentation/bunyan.ts +93 -0
- package/src/instrumentation/cassandra.ts +367 -0
- package/src/instrumentation/cohere.ts +227 -0
- package/src/instrumentation/connect.ts +200 -0
- package/src/instrumentation/dataloader.ts +291 -0
- package/src/instrumentation/dns.ts +220 -0
- package/src/instrumentation/firebase.ts +445 -0
- package/src/instrumentation/fs.ts +260 -0
- package/src/instrumentation/generic-pool.ts +317 -0
- package/src/instrumentation/google-genai.ts +426 -0
- package/src/instrumentation/graphql.ts +434 -0
- package/src/instrumentation/grpc.ts +666 -0
- package/src/instrumentation/hapi.ts +257 -0
- package/src/instrumentation/kafka.ts +360 -0
- package/src/instrumentation/knex.ts +249 -0
- package/src/instrumentation/lru-memoizer.ts +175 -0
- package/src/instrumentation/memcached.ts +190 -0
- package/src/instrumentation/mistral.ts +254 -0
- package/src/instrumentation/nestjs.ts +243 -0
- package/src/instrumentation/net.ts +171 -0
- package/src/instrumentation/openai.ts +281 -0
- package/src/instrumentation/pino.ts +170 -0
- package/src/instrumentation/restify.ts +213 -0
- package/src/instrumentation/runtime.ts +352 -0
- package/src/instrumentation/socketio.ts +272 -0
- package/src/instrumentation/tedious.ts +509 -0
- package/src/instrumentation/winston.ts +149 -0
- package/src/lambda-handler.ts +262 -0
- package/src/register.ts +22 -3
- package/src/wrappers/lambda.ts +417 -0
- package/tsup.config.ts +4 -4
- package/wiki.md +1693 -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
|
+
};
|