@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +527 -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/lambda-handler.d.mts +13 -0
  12. package/dist/lambda-handler.d.ts +13 -0
  13. package/dist/lambda-handler.js +2 -0
  14. package/dist/lambda-handler.js.map +1 -0
  15. package/dist/lambda-handler.mjs +2 -0
  16. package/dist/lambda-handler.mjs.map +1 -0
  17. package/dist/register.js +1 -1
  18. package/dist/register.js.map +1 -1
  19. package/dist/register.mjs +1 -1
  20. package/dist/register.mjs.map +1 -1
  21. package/package.json +6 -1
  22. package/src/core/client.ts +57 -0
  23. package/src/core/transport.ts +20 -3
  24. package/src/core/types.ts +5 -1
  25. package/src/index.ts +4 -0
  26. package/src/instrumentation/amqplib.ts +371 -0
  27. package/src/instrumentation/anthropic.ts +245 -0
  28. package/src/instrumentation/aws-sdk.ts +403 -0
  29. package/src/instrumentation/azure-openai.ts +177 -0
  30. package/src/instrumentation/bunyan.ts +93 -0
  31. package/src/instrumentation/cassandra.ts +367 -0
  32. package/src/instrumentation/cohere.ts +227 -0
  33. package/src/instrumentation/connect.ts +200 -0
  34. package/src/instrumentation/dataloader.ts +291 -0
  35. package/src/instrumentation/dns.ts +220 -0
  36. package/src/instrumentation/firebase.ts +445 -0
  37. package/src/instrumentation/fs.ts +260 -0
  38. package/src/instrumentation/generic-pool.ts +317 -0
  39. package/src/instrumentation/google-genai.ts +426 -0
  40. package/src/instrumentation/graphql.ts +434 -0
  41. package/src/instrumentation/grpc.ts +666 -0
  42. package/src/instrumentation/hapi.ts +257 -0
  43. package/src/instrumentation/kafka.ts +360 -0
  44. package/src/instrumentation/knex.ts +249 -0
  45. package/src/instrumentation/lru-memoizer.ts +175 -0
  46. package/src/instrumentation/memcached.ts +190 -0
  47. package/src/instrumentation/mistral.ts +254 -0
  48. package/src/instrumentation/nestjs.ts +243 -0
  49. package/src/instrumentation/net.ts +171 -0
  50. package/src/instrumentation/openai.ts +281 -0
  51. package/src/instrumentation/pino.ts +170 -0
  52. package/src/instrumentation/restify.ts +213 -0
  53. package/src/instrumentation/runtime.ts +352 -0
  54. package/src/instrumentation/socketio.ts +272 -0
  55. package/src/instrumentation/tedious.ts +509 -0
  56. package/src/instrumentation/winston.ts +149 -0
  57. package/src/lambda-handler.ts +262 -0
  58. package/src/register.ts +22 -3
  59. package/src/wrappers/lambda.ts +417 -0
  60. package/tsup.config.ts +4 -4
  61. package/wiki.md +1693 -852
@@ -0,0 +1,262 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Senzor Lambda Auto-Handler Wrapper
3
+ //
4
+ // This module is the entry point for zero-code-change Lambda Extension Layer
5
+ // deployments. It is referenced as the Lambda function's handler:
6
+ //
7
+ // Handler: @senzops/apm-node/dist/lambda-handler.handler
8
+ //
9
+ // It reads the user's original handler from an environment variable,
10
+ // dynamically loads it, wraps it with Senzor instrumentation, and
11
+ // re-exports the wrapped function.
12
+ //
13
+ // Required environment variables:
14
+ // SENZOR_API_KEY — Senzor API key
15
+ // SENZOR_LAMBDA_HANDLER — Original handler in module.function format
16
+ // (e.g., "index.handler", "src/app.myHandler")
17
+ //
18
+ // Optional environment variables:
19
+ // All standard SENZOR_* env vars (see register.ts)
20
+ //
21
+ // How it works:
22
+ // 1. Initializes Senzor SDK via the same logic as register.ts
23
+ // 2. Parses SENZOR_LAMBDA_HANDLER into module path + export name
24
+ // 3. Resolves the module from LAMBDA_TASK_ROOT (the function's code dir)
25
+ // 4. Extracts the named export (supports nested paths like "a.b.c")
26
+ // 5. Wraps with wrapLambda() for full APM coverage
27
+ // 6. Exports as "handler" for Lambda to invoke
28
+ //
29
+ // This gives users the same experience as New Relic / Datadog Lambda Layers:
30
+ // just add the layer, set 2 env vars, zero code changes.
31
+ // ---------------------------------------------------------------------------
32
+
33
+ import { client } from './core/client';
34
+ import { getEnv } from './core/runtime';
35
+ import { wrapLambda } from './wrappers/lambda';
36
+ import * as path from 'path';
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // 1. Initialize Senzor SDK (same logic as register.ts)
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const truthy = (value: string | undefined): boolean =>
43
+ value === '1' || value === 'true' || value === 'yes';
44
+
45
+ const numberFromEnv = (value: string | undefined): number | undefined => {
46
+ if (!value) return undefined;
47
+ const parsed = Number(value);
48
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
49
+ };
50
+
51
+ const apiKey =
52
+ getEnv('SENZOR_API_KEY') ||
53
+ getEnv('SENZOR_APM_API_KEY') ||
54
+ getEnv('SENZOR_SERVICE_API_KEY');
55
+
56
+ const endpoint =
57
+ getEnv('SENZOR_ENDPOINT') ||
58
+ getEnv('SENZOR_APM_ENDPOINT');
59
+
60
+ const isLambda = !!getEnv('AWS_LAMBDA_FUNCTION_NAME');
61
+
62
+ const options = {
63
+ apiKey: apiKey || '',
64
+ endpoint,
65
+ debug: truthy(getEnv('SENZOR_DEBUG')),
66
+ autoLogs: getEnv('SENZOR_AUTO_LOGS') === 'false' ? false : undefined,
67
+ batchSize: numberFromEnv(getEnv('SENZOR_BATCH_SIZE')) ?? (isLambda ? 10 : undefined),
68
+ flushInterval: numberFromEnv(getEnv('SENZOR_FLUSH_INTERVAL')) ?? (isLambda ? 0 : undefined),
69
+ flushTimeoutMs: numberFromEnv(getEnv('SENZOR_FLUSH_TIMEOUT_MS')),
70
+ maxQueueSize: numberFromEnv(getEnv('SENZOR_MAX_QUEUE_SIZE')),
71
+ maxSpansPerTrace: numberFromEnv(getEnv('SENZOR_MAX_SPANS_PER_TRACE')),
72
+ captureHeaders: truthy(getEnv('SENZOR_CAPTURE_HEADERS')),
73
+ captureDbStatement:
74
+ getEnv('SENZOR_CAPTURE_DB_STATEMENT') === 'false' ? false : undefined,
75
+ frameworkSpans:
76
+ getEnv('SENZOR_FRAMEWORK_SPANS') === 'false' ? false : undefined,
77
+ captureMiddlewareSpans:
78
+ getEnv('SENZOR_CAPTURE_MIDDLEWARE_SPANS') === 'false' ? false : undefined,
79
+ captureRouterSpans:
80
+ getEnv('SENZOR_CAPTURE_ROUTER_SPANS') === 'false' ? false : undefined,
81
+ captureLifecycleHookSpans:
82
+ getEnv('SENZOR_CAPTURE_LIFECYCLE_HOOK_SPANS') === 'false' ? false : undefined,
83
+ runtimeMetrics:
84
+ getEnv('SENZOR_RUNTIME_METRICS') === 'false' || isLambda ? false : undefined,
85
+ runtimeMetricsInterval: numberFromEnv(getEnv('SENZOR_RUNTIME_METRICS_INTERVAL')),
86
+ };
87
+
88
+ if (apiKey) {
89
+ client.init(options);
90
+ } else {
91
+ client.preload(options);
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // 2. Resolve and wrap the user's original handler
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /**
99
+ * Parse a Lambda handler string into module path and function path.
100
+ *
101
+ * Examples:
102
+ * "index.handler" → { modulePath: "index", fnPath: ["handler"] }
103
+ * "src/app.myHandler" → { modulePath: "src/app", fnPath: ["myHandler"] }
104
+ * "dist/handlers.api.get" → { modulePath: "dist/handlers", fnPath: ["api", "get"] }
105
+ *
106
+ * Lambda convention: everything before the LAST dot that isn't part of a
107
+ * directory path is the module, everything after is the function path.
108
+ * Since module paths can contain dots in directory names, we split on the
109
+ * last dot after the last path separator.
110
+ */
111
+ const parseHandlerString = (handlerStr: string): { modulePath: string; fnPath: string[] } => {
112
+ const lastSlash = Math.max(handlerStr.lastIndexOf('/'), handlerStr.lastIndexOf('\\'));
113
+ const afterSlash = lastSlash >= 0 ? handlerStr.substring(lastSlash + 1) : handlerStr;
114
+ const beforeSlash = lastSlash >= 0 ? handlerStr.substring(0, lastSlash + 1) : '';
115
+
116
+ const firstDot = afterSlash.indexOf('.');
117
+ if (firstDot < 0) {
118
+ // No dot — treat the whole thing as the module, export "handler"
119
+ return { modulePath: handlerStr, fnPath: ['handler'] };
120
+ }
121
+
122
+ const moduleName = afterSlash.substring(0, firstDot);
123
+ const fnParts = afterSlash.substring(firstDot + 1).split('.');
124
+
125
+ return {
126
+ modulePath: beforeSlash + moduleName,
127
+ fnPath: fnParts,
128
+ };
129
+ };
130
+
131
+ /**
132
+ * Resolve a handler export from a module given a function path.
133
+ * Supports nested exports: ["api", "get"] resolves module.api.get
134
+ */
135
+ const resolveExport = (moduleExports: any, fnPath: string[]): Function | null => {
136
+ let current = moduleExports;
137
+
138
+ for (const part of fnPath) {
139
+ if (current == null || typeof current !== 'object') return null;
140
+ current = current[part];
141
+ }
142
+
143
+ // Also check .default for ESM interop
144
+ if (current == null && moduleExports?.default) {
145
+ current = moduleExports.default;
146
+ for (const part of fnPath) {
147
+ if (current == null || typeof current !== 'object') return null;
148
+ current = current[part];
149
+ }
150
+ }
151
+
152
+ return typeof current === 'function' ? current : null;
153
+ };
154
+
155
+ /**
156
+ * Load the user's original handler module. Tries multiple resolution strategies:
157
+ * 1. Absolute path from LAMBDA_TASK_ROOT
158
+ * 2. require() with the module path as-is (for node_modules)
159
+ * 3. With common extensions (.js, .mjs, .cjs)
160
+ */
161
+ const loadHandlerModule = (modulePath: string): any => {
162
+ const taskRoot = process.env.LAMBDA_TASK_ROOT || process.cwd();
163
+ const absolutePath = path.resolve(taskRoot, modulePath);
164
+
165
+ // Strategy 1: Direct absolute path
166
+ try {
167
+ return require(absolutePath);
168
+ } catch { }
169
+
170
+ // Strategy 2: With extensions
171
+ const extensions = ['.js', '.cjs', '.mjs'];
172
+ for (const ext of extensions) {
173
+ try {
174
+ return require(absolutePath + ext);
175
+ } catch { }
176
+ }
177
+
178
+ // Strategy 3: Module as-is (may be in node_modules or an absolute path)
179
+ try {
180
+ return require(modulePath);
181
+ } catch { }
182
+
183
+ return null;
184
+ };
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // 3. Build and export the wrapped handler
188
+ // ---------------------------------------------------------------------------
189
+
190
+ const handlerEnv = getEnv('SENZOR_LAMBDA_HANDLER');
191
+
192
+ let wrappedHandler: Function;
193
+
194
+ if (!handlerEnv) {
195
+ // No handler configured — export a diagnostic handler that returns an error
196
+ wrappedHandler = async () => {
197
+ const message =
198
+ 'Senzor Lambda Layer: SENZOR_LAMBDA_HANDLER environment variable is not set. ' +
199
+ 'Set it to your original handler (e.g., "index.handler") and set the Lambda ' +
200
+ 'function handler to "@senzops/apm-node/dist/lambda-handler.handler".';
201
+
202
+ console.error(`[Senzor] ${message}`);
203
+
204
+ return {
205
+ statusCode: 500,
206
+ body: JSON.stringify({ error: message }),
207
+ };
208
+ };
209
+ } else {
210
+ const { modulePath, fnPath } = parseHandlerString(handlerEnv);
211
+ const handlerModule = loadHandlerModule(modulePath);
212
+
213
+ if (!handlerModule) {
214
+ const errorMsg = `Senzor Lambda Layer: Could not load handler module "${modulePath}" ` +
215
+ `(from SENZOR_LAMBDA_HANDLER="${handlerEnv}"). Verify the module path exists ` +
216
+ `relative to your Lambda function code.`;
217
+
218
+ console.error(`[Senzor] ${errorMsg}`);
219
+
220
+ wrappedHandler = async () => ({
221
+ statusCode: 500,
222
+ body: JSON.stringify({ error: errorMsg }),
223
+ });
224
+ } else {
225
+ const originalHandler = resolveExport(handlerModule, fnPath);
226
+
227
+ if (!originalHandler) {
228
+ const errorMsg = `Senzor Lambda Layer: Module "${modulePath}" loaded successfully ` +
229
+ `but export "${fnPath.join('.')}" is not a function. ` +
230
+ `Available exports: ${Object.keys(handlerModule).join(', ')}`;
231
+
232
+ console.error(`[Senzor] ${errorMsg}`);
233
+
234
+ wrappedHandler = async () => ({
235
+ statusCode: 500,
236
+ body: JSON.stringify({ error: errorMsg }),
237
+ });
238
+ } else {
239
+ // Success — wrap the handler with full Senzor APM instrumentation
240
+ wrappedHandler = wrapLambda(originalHandler as any);
241
+
242
+ if (truthy(getEnv('SENZOR_DEBUG'))) {
243
+ console.log(
244
+ `[Senzor] Lambda handler wrapped: ${handlerEnv} → ` +
245
+ `module="${modulePath}", export="${fnPath.join('.')}"`,
246
+ );
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ /**
253
+ * The wrapped Lambda handler. Configure your Lambda function to use:
254
+ *
255
+ * Handler: @senzops/apm-node/dist/lambda-handler.handler
256
+ *
257
+ * And set environment variables:
258
+ *
259
+ * SENZOR_API_KEY=sz_apm_xxx
260
+ * SENZOR_LAMBDA_HANDLER=index.handler
261
+ */
262
+ export const handler = wrappedHandler;
package/src/register.ts CHANGED
@@ -19,13 +19,27 @@ const endpoint =
19
19
  getEnv('SENZOR_ENDPOINT') ||
20
20
  getEnv('SENZOR_APM_ENDPOINT');
21
21
 
22
+ // ---------------------------------------------------------------------------
23
+ // Lambda environment auto-detection
24
+ //
25
+ // When running inside AWS Lambda, the execution model differs fundamentally
26
+ // from long-running servers:
27
+ // - Runtime metrics (event loop, GC, heap) are meaningless per-invocation
28
+ // - Interval-based flushing wastes resources between frozen invocations
29
+ // - Batch size should be small since each invocation is short-lived
30
+ //
31
+ // We detect Lambda via the AWS_LAMBDA_FUNCTION_NAME env var (always set by
32
+ // the Lambda runtime) and auto-configure optimal defaults.
33
+ // ---------------------------------------------------------------------------
34
+ const isLambda = !!getEnv('AWS_LAMBDA_FUNCTION_NAME');
35
+
22
36
  const options = {
23
37
  apiKey: apiKey || '',
24
38
  endpoint,
25
39
  debug: truthy(getEnv('SENZOR_DEBUG')),
26
40
  autoLogs: getEnv('SENZOR_AUTO_LOGS') === 'false' ? false : undefined,
27
- batchSize: numberFromEnv(getEnv('SENZOR_BATCH_SIZE')),
28
- flushInterval: numberFromEnv(getEnv('SENZOR_FLUSH_INTERVAL')),
41
+ batchSize: numberFromEnv(getEnv('SENZOR_BATCH_SIZE')) ?? (isLambda ? 10 : undefined),
42
+ flushInterval: numberFromEnv(getEnv('SENZOR_FLUSH_INTERVAL')) ?? (isLambda ? 0 : undefined),
29
43
  flushTimeoutMs: numberFromEnv(getEnv('SENZOR_FLUSH_TIMEOUT_MS')),
30
44
  maxQueueSize: numberFromEnv(getEnv('SENZOR_MAX_QUEUE_SIZE')),
31
45
  maxSpansPerTrace: numberFromEnv(getEnv('SENZOR_MAX_SPANS_PER_TRACE')),
@@ -49,7 +63,12 @@ const options = {
49
63
  captureLifecycleHookSpans:
50
64
  getEnv('SENZOR_CAPTURE_LIFECYCLE_HOOK_SPANS') === 'false'
51
65
  ? false
52
- : undefined
66
+ : undefined,
67
+ runtimeMetrics:
68
+ getEnv('SENZOR_RUNTIME_METRICS') === 'false' || isLambda
69
+ ? false
70
+ : undefined,
71
+ runtimeMetricsInterval: numberFromEnv(getEnv('SENZOR_RUNTIME_METRICS_INTERVAL')),
53
72
  };
54
73
 
55
74
  if (apiKey) {