@senzops/apm-node 1.2.8 → 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 (54) 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/transport.ts +20 -3
  18. package/src/core/types.ts +5 -1
  19. package/src/index.ts +4 -0
  20. package/src/instrumentation/amqplib.ts +371 -0
  21. package/src/instrumentation/anthropic.ts +245 -0
  22. package/src/instrumentation/aws-sdk.ts +403 -0
  23. package/src/instrumentation/azure-openai.ts +177 -0
  24. package/src/instrumentation/bunyan.ts +93 -0
  25. package/src/instrumentation/cassandra.ts +367 -0
  26. package/src/instrumentation/cohere.ts +227 -0
  27. package/src/instrumentation/connect.ts +200 -0
  28. package/src/instrumentation/dataloader.ts +291 -0
  29. package/src/instrumentation/dns.ts +220 -0
  30. package/src/instrumentation/firebase.ts +445 -0
  31. package/src/instrumentation/fs.ts +260 -0
  32. package/src/instrumentation/generic-pool.ts +317 -0
  33. package/src/instrumentation/google-genai.ts +426 -0
  34. package/src/instrumentation/graphql.ts +434 -0
  35. package/src/instrumentation/grpc.ts +666 -0
  36. package/src/instrumentation/hapi.ts +257 -0
  37. package/src/instrumentation/kafka.ts +360 -0
  38. package/src/instrumentation/knex.ts +249 -0
  39. package/src/instrumentation/lru-memoizer.ts +175 -0
  40. package/src/instrumentation/memcached.ts +190 -0
  41. package/src/instrumentation/mistral.ts +254 -0
  42. package/src/instrumentation/nestjs.ts +243 -0
  43. package/src/instrumentation/net.ts +171 -0
  44. package/src/instrumentation/openai.ts +281 -0
  45. package/src/instrumentation/pino.ts +170 -0
  46. package/src/instrumentation/restify.ts +213 -0
  47. package/src/instrumentation/runtime.ts +352 -0
  48. package/src/instrumentation/socketio.ts +272 -0
  49. package/src/instrumentation/tedious.ts +509 -0
  50. package/src/instrumentation/winston.ts +149 -0
  51. package/src/register.ts +22 -3
  52. package/src/wrappers/lambda.ts +417 -0
  53. package/tsup.config.ts +3 -3
  54. package/wiki.md +1547 -852
@@ -0,0 +1,417 @@
1
+ import { client } from '../core/client';
2
+ import { normalizePath } from '../core/normalizer';
3
+ import { getClientIp } from '../utils/getClientIp';
4
+ import { generateTraceId } from '../utils/ids';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // AWS Lambda Handler Wrapper
8
+ //
9
+ // Provides automatic APM instrumentation for AWS Lambda functions.
10
+ //
11
+ // Features:
12
+ // 1. Cold start detection — tags first invocation per container
13
+ // 2. Lambda context extraction — faas.*, cloud.*, aws.* attributes
14
+ // 3. Trigger-type detection — API Gateway v1/v2, ALB, SQS, SNS,
15
+ // DynamoDB Streams, EventBridge, S3, Scheduled, generic
16
+ // 4. Forced flush before response — ensures telemetry delivery
17
+ // since Lambda freezes the process between invocations
18
+ // 5. Lambda Extensions API registration — registers as internal
19
+ // extension for SHUTDOWN lifecycle event as a safety-net flush
20
+ //
21
+ // Usage:
22
+ // import Senzor from '@anthropic/senzor-node';
23
+ // export const handler = Senzor.wrapLambda(async (event, context) => {
24
+ // return { statusCode: 200, body: 'OK' };
25
+ // });
26
+ // ---------------------------------------------------------------------------
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Cold start detection
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /** Module-level flag — true only for the very first invocation in this container. */
33
+ let isColdStart = true;
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Trigger type detection
37
+ // ---------------------------------------------------------------------------
38
+
39
+ type LambdaTrigger =
40
+ | 'api-gateway-v1'
41
+ | 'api-gateway-v2'
42
+ | 'alb'
43
+ | 'sqs'
44
+ | 'sns'
45
+ | 'dynamodb-streams'
46
+ | 'eventbridge'
47
+ | 's3'
48
+ | 'scheduled'
49
+ | 'generic';
50
+
51
+ const detectTrigger = (event: any): LambdaTrigger => {
52
+ if (!event || typeof event !== 'object') return 'generic';
53
+
54
+ // API Gateway v2 (HTTP API)
55
+ if (event.requestContext?.http?.method) return 'api-gateway-v2';
56
+
57
+ // API Gateway v1 (REST API)
58
+ if (event.requestContext?.httpMethod || event.httpMethod) return 'api-gateway-v1';
59
+
60
+ // ALB (Application Load Balancer)
61
+ if (event.requestContext?.elb) return 'alb';
62
+
63
+ // SQS
64
+ if (Array.isArray(event.Records) && event.Records[0]?.eventSource === 'aws:sqs') return 'sqs';
65
+
66
+ // SNS
67
+ if (Array.isArray(event.Records) && event.Records[0]?.EventSource === 'aws:sns') return 'sns';
68
+
69
+ // DynamoDB Streams
70
+ if (Array.isArray(event.Records) && event.Records[0]?.eventSource === 'aws:dynamodb') return 'dynamodb-streams';
71
+
72
+ // S3
73
+ if (Array.isArray(event.Records) && event.Records[0]?.eventSource === 'aws:s3') return 's3';
74
+
75
+ // EventBridge / CloudWatch Events
76
+ if (event.source && event['detail-type'] && event.detail) return 'eventbridge';
77
+
78
+ // CloudWatch Scheduled Event
79
+ if (event.source === 'aws.events' || event['detail-type'] === 'Scheduled Event') return 'scheduled';
80
+
81
+ return 'generic';
82
+ };
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // HTTP event extraction (API Gateway v1/v2, ALB)
86
+ // ---------------------------------------------------------------------------
87
+
88
+ interface HttpEventInfo {
89
+ method: string;
90
+ path: string;
91
+ headers: Record<string, string>;
92
+ statusCode?: number;
93
+ }
94
+
95
+ const extractHttpEvent = (event: any, trigger: LambdaTrigger): HttpEventInfo | null => {
96
+ if (trigger === 'api-gateway-v2') {
97
+ const http = event.requestContext?.http;
98
+ return {
99
+ method: http?.method || 'GET',
100
+ path: event.rawPath || event.requestContext?.http?.path || '/',
101
+ headers: normalizeHeaders(event.headers),
102
+ };
103
+ }
104
+
105
+ if (trigger === 'api-gateway-v1') {
106
+ return {
107
+ method: event.httpMethod || event.requestContext?.httpMethod || 'GET',
108
+ path: event.path || event.resource || '/',
109
+ headers: normalizeHeaders(event.headers),
110
+ };
111
+ }
112
+
113
+ if (trigger === 'alb') {
114
+ return {
115
+ method: event.httpMethod || 'GET',
116
+ path: event.path || '/',
117
+ headers: normalizeHeaders(event.headers),
118
+ };
119
+ }
120
+
121
+ return null;
122
+ };
123
+
124
+ const normalizeHeaders = (headers: any): Record<string, string> => {
125
+ if (!headers || typeof headers !== 'object') return {};
126
+ const normalized: Record<string, string> = {};
127
+ for (const [key, value] of Object.entries(headers)) {
128
+ normalized[key.toLowerCase()] = String(value);
129
+ }
130
+ return normalized;
131
+ };
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Lambda context attribute extraction
135
+ // ---------------------------------------------------------------------------
136
+
137
+ const extractLambdaAttributes = (
138
+ event: any,
139
+ context: any,
140
+ trigger: LambdaTrigger,
141
+ coldStart: boolean
142
+ ): Record<string, any> => {
143
+ const attrs: Record<string, any> = {
144
+ 'faas.trigger': mapTriggerToFaasTrigger(trigger),
145
+ 'faas.coldstart': coldStart,
146
+ 'cloud.provider': 'aws',
147
+ 'cloud.platform': 'aws_lambda',
148
+ 'firebase.service': undefined, // clear any inherited
149
+ library: 'aws-lambda',
150
+ };
151
+
152
+ // Lambda context fields
153
+ if (context) {
154
+ if (context.functionName) attrs['faas.name'] = context.functionName;
155
+ if (context.functionVersion) attrs['faas.version'] = context.functionVersion;
156
+ if (context.awsRequestId) attrs['faas.execution'] = context.awsRequestId;
157
+ if (context.invokedFunctionArn) attrs['cloud.resource_id'] = context.invokedFunctionArn;
158
+ if (context.memoryLimitInMB) attrs['faas.max_memory'] = Number(context.memoryLimitInMB);
159
+ if (context.logGroupName) attrs['aws.log.group.names'] = context.logGroupName;
160
+ if (context.logStreamName) attrs['aws.log.stream.names'] = context.logStreamName;
161
+ }
162
+
163
+ // Region from environment
164
+ const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION;
165
+ if (region) attrs['cloud.region'] = region;
166
+
167
+ // Account ID from ARN
168
+ if (context?.invokedFunctionArn) {
169
+ const arnParts = context.invokedFunctionArn.split(':');
170
+ if (arnParts.length >= 5) {
171
+ attrs['cloud.account.id'] = arnParts[4];
172
+ }
173
+ }
174
+
175
+ // Trigger-specific attributes
176
+ switch (trigger) {
177
+ case 'sqs':
178
+ if (Array.isArray(event.Records)) {
179
+ attrs['messaging.system'] = 'aws_sqs';
180
+ attrs['messaging.batch.message_count'] = event.Records.length;
181
+ const arnSrc = event.Records[0]?.eventSourceARN;
182
+ if (arnSrc) attrs['messaging.source.name'] = arnSrc.split(':').pop();
183
+ }
184
+ break;
185
+
186
+ case 'sns':
187
+ if (Array.isArray(event.Records)) {
188
+ attrs['messaging.system'] = 'aws_sns';
189
+ const topicArn = event.Records[0]?.Sns?.TopicArn;
190
+ if (topicArn) attrs['messaging.source.name'] = topicArn.split(':').pop();
191
+ }
192
+ break;
193
+
194
+ case 'dynamodb-streams':
195
+ if (Array.isArray(event.Records)) {
196
+ attrs['aws.dynamodb.table_names'] = [
197
+ ...new Set(
198
+ event.Records
199
+ .map((r: any) => r.eventSourceARN?.split('/')[1])
200
+ .filter(Boolean)
201
+ ),
202
+ ];
203
+ attrs['messaging.batch.message_count'] = event.Records.length;
204
+ }
205
+ break;
206
+
207
+ case 's3':
208
+ if (Array.isArray(event.Records) && event.Records[0]?.s3) {
209
+ attrs['aws.s3.bucket'] = event.Records[0].s3.bucket?.name;
210
+ attrs['aws.s3.key'] = event.Records[0].s3.object?.key;
211
+ }
212
+ break;
213
+
214
+ case 'eventbridge':
215
+ if (event.source) attrs['aws.eventbridge.source'] = event.source;
216
+ if (event['detail-type']) attrs['aws.eventbridge.detail_type'] = event['detail-type'];
217
+ break;
218
+ }
219
+
220
+ return attrs;
221
+ };
222
+
223
+ const mapTriggerToFaasTrigger = (trigger: LambdaTrigger): string => {
224
+ switch (trigger) {
225
+ case 'api-gateway-v1':
226
+ case 'api-gateway-v2':
227
+ case 'alb':
228
+ return 'http';
229
+ case 'sqs':
230
+ case 'sns':
231
+ return 'pubsub';
232
+ case 'dynamodb-streams':
233
+ return 'datasource';
234
+ case 's3':
235
+ return 'datasource';
236
+ case 'eventbridge':
237
+ case 'scheduled':
238
+ return 'timer';
239
+ default:
240
+ return 'other';
241
+ }
242
+ };
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // Response status extraction
246
+ // ---------------------------------------------------------------------------
247
+
248
+ const extractStatusFromResponse = (result: any, trigger: LambdaTrigger): number => {
249
+ // HTTP triggers: response has statusCode
250
+ if (
251
+ (trigger === 'api-gateway-v1' || trigger === 'api-gateway-v2' || trigger === 'alb') &&
252
+ result &&
253
+ typeof result === 'object'
254
+ ) {
255
+ return typeof result.statusCode === 'number' ? result.statusCode : 200;
256
+ }
257
+
258
+ // Non-HTTP: success = 200
259
+ return 200;
260
+ };
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // Lambda Extensions API — internal extension registration
264
+ // ---------------------------------------------------------------------------
265
+
266
+ /**
267
+ * Register as a Lambda internal extension to receive SHUTDOWN events.
268
+ * This is a safety-net: if the process is about to be terminated,
269
+ * we get a last chance to flush telemetry.
270
+ *
271
+ * Only runs when the Lambda Extensions API is available (runtime >= 2020-01-01).
272
+ * Failures are silently ignored — the wrapper's own forced flush is the primary mechanism.
273
+ */
274
+ const registerExtension = (() => {
275
+ let registered = false;
276
+
277
+ return (onShutdown: () => Promise<void>) => {
278
+ if (registered) return;
279
+ registered = true;
280
+
281
+ const runtimeApi = process.env.AWS_LAMBDA_RUNTIME_API;
282
+ if (!runtimeApi) return;
283
+
284
+ const extensionName = 'senzor-apm';
285
+ const registerUrl = `http://${runtimeApi}/2020-01-01/extension/register`;
286
+
287
+ // Fire-and-forget registration
288
+ (async () => {
289
+ try {
290
+ const registerResponse = await fetch(registerUrl, {
291
+ method: 'POST',
292
+ headers: {
293
+ 'Content-Type': 'application/json',
294
+ 'Lambda-Extension-Name': extensionName,
295
+ },
296
+ body: JSON.stringify({ events: ['SHUTDOWN'] }),
297
+ });
298
+
299
+ if (!registerResponse.ok) return;
300
+
301
+ const extensionId = registerResponse.headers.get('Lambda-Extension-Identifier');
302
+ if (!extensionId) return;
303
+
304
+ // Event loop: wait for SHUTDOWN
305
+ const nextUrl = `http://${runtimeApi}/2020-01-01/extension/event/next`;
306
+
307
+ // This blocks until Lambda sends SHUTDOWN — runs on a background "thread"
308
+ // via the microtask queue; does not block the handler.
309
+ const waitForShutdown = async () => {
310
+ try {
311
+ const eventResponse = await fetch(nextUrl, {
312
+ method: 'GET',
313
+ headers: { 'Lambda-Extension-Identifier': extensionId },
314
+ });
315
+
316
+ if (eventResponse.ok) {
317
+ const event = await eventResponse.json() as any;
318
+ if (event.eventType === 'SHUTDOWN') {
319
+ await onShutdown();
320
+ }
321
+ }
322
+ } catch {
323
+ // Extension event loop failure — primary flush in handler covers this
324
+ }
325
+ };
326
+
327
+ // Start the event loop (non-blocking)
328
+ waitForShutdown();
329
+ } catch {
330
+ // Registration failure is expected outside Lambda or in older runtimes
331
+ }
332
+ })();
333
+ };
334
+ })();
335
+
336
+ // ---------------------------------------------------------------------------
337
+ // Public API: wrapLambda
338
+ // ---------------------------------------------------------------------------
339
+
340
+ type LambdaHandler<TEvent = any, TResult = any> = (
341
+ event: TEvent,
342
+ context: any,
343
+ ) => Promise<TResult>;
344
+
345
+ /**
346
+ * Wraps an AWS Lambda handler function with Senzor APM instrumentation.
347
+ *
348
+ * @example
349
+ * ```typescript
350
+ * import Senzor from '@senzor/apm-node';
351
+ *
352
+ * Senzor.init({ apiKey: process.env.SENZOR_API_KEY });
353
+ *
354
+ * export const handler = Senzor.wrapLambda(async (event, context) => {
355
+ * // Your Lambda logic here
356
+ * return { statusCode: 200, body: JSON.stringify({ ok: true }) };
357
+ * });
358
+ * ```
359
+ */
360
+ export const wrapLambda = <TEvent = any, TResult = any>(
361
+ handler: LambdaHandler<TEvent, TResult>,
362
+ ): LambdaHandler<TEvent, TResult> => {
363
+ // Register extension on first wrap (idempotent)
364
+ registerExtension(() => client.flush());
365
+
366
+ return async (event: TEvent, context: any): Promise<TResult> => {
367
+ const coldStart = isColdStart;
368
+ if (isColdStart) isColdStart = false;
369
+
370
+ const trigger = detectTrigger(event);
371
+ const lambdaAttrs = extractLambdaAttributes(event, context, trigger, coldStart);
372
+ const httpInfo = extractHttpEvent(event, trigger);
373
+
374
+ // For HTTP triggers, use proper HTTP trace semantics
375
+ const method = httpInfo?.method || trigger.toUpperCase();
376
+ const path = httpInfo?.path || `/${context?.functionName || 'lambda'}`;
377
+ const route = httpInfo ? normalizePath(httpInfo.path) : context?.functionName || 'lambda';
378
+ const headers = httpInfo?.headers || {};
379
+
380
+ return client.startTrace(
381
+ {
382
+ method,
383
+ path,
384
+ route,
385
+ ip: httpInfo ? getClientIp({ headers }) : undefined,
386
+ userAgent: headers['user-agent'],
387
+ headers,
388
+ ...lambdaAttrs,
389
+ },
390
+ async () => {
391
+ let status = 500;
392
+ try {
393
+ const result = await handler(event, context);
394
+ status = extractStatusFromResponse(result, trigger);
395
+ return result;
396
+ } catch (err: any) {
397
+ client.captureError(err, {
398
+ 'faas.name': context?.functionName,
399
+ 'faas.execution': context?.awsRequestId,
400
+ trigger,
401
+ });
402
+ throw err;
403
+ } finally {
404
+ client.endTrace(status, {
405
+ route,
406
+ ...lambdaAttrs,
407
+ });
408
+
409
+ // Force flush before Lambda freezes the execution environment.
410
+ // This is critical: Lambda may freeze or terminate the process
411
+ // immediately after the handler returns.
412
+ await client.flush();
413
+ }
414
+ },
415
+ );
416
+ };
417
+ };
package/tsup.config.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { defineConfig } from 'tsup';
2
2
 
3
3
  const NODE_BUILTINS = [
4
- 'http', 'https', 'url', 'net', 'module', 'crypto', 'async_hooks',
5
- 'node:http', 'node:https', 'node:url', 'node:net', 'node:module',
6
- 'node:crypto', 'node:async_hooks'
4
+ 'http', 'https', 'url', 'net', 'dns', 'module', 'crypto', 'async_hooks', 'perf_hooks',
5
+ 'node:http', 'node:https', 'node:url', 'node:net', 'node:dns', 'node:module',
6
+ 'node:crypto', 'node:async_hooks', 'node:perf_hooks'
7
7
  ];
8
8
 
9
9
  export default defineConfig([