@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,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) {
|