@senzops/apm-node 1.3.0 → 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 +4 -0
- package/README.md +62 -14
- 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/lambda-handler.ts +262 -0
- package/tsup.config.ts +1 -1
- package/wiki.md +229 -83
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@senzops/apm-node",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Universal APM SDK for Senzor",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
"types": "./dist/register.d.ts",
|
|
16
16
|
"require": "./dist/register.js",
|
|
17
17
|
"import": "./dist/register.mjs"
|
|
18
|
+
},
|
|
19
|
+
"./lambda-handler": {
|
|
20
|
+
"types": "./dist/lambda-handler.d.ts",
|
|
21
|
+
"require": "./dist/lambda-handler.js",
|
|
22
|
+
"import": "./dist/lambda-handler.mjs"
|
|
18
23
|
}
|
|
19
24
|
},
|
|
20
25
|
"scripts": {
|
|
@@ -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/tsup.config.ts
CHANGED
package/wiki.md
CHANGED
|
@@ -333,7 +333,228 @@ With preload mode, no wrapper is needed — `http.createServer` is automatically
|
|
|
333
333
|
|
|
334
334
|
## 4. AWS Lambda Integration
|
|
335
335
|
|
|
336
|
-
|
|
336
|
+
Three deployment methods, from zero-code to code-level:
|
|
337
|
+
|
|
338
|
+
| Method | Code Changes | Setup | Coverage |
|
|
339
|
+
|--------|-------------|-------|----------|
|
|
340
|
+
| **Extension Layer** (recommended) | None | Add layer + set env vars | Full: auto-handler wrapping + all auto-instrumentation |
|
|
341
|
+
| **Handler Wrapper** | Modify handler file | `npm install` + code change | Full |
|
|
342
|
+
| **Preload Layer** | None | Add layer + `NODE_OPTIONS` | Partial: outgoing calls/DB only, no handler wrapping |
|
|
343
|
+
|
|
344
|
+
### 4.1 Lambda Extension Layer (Zero Code Changes, Recommended)
|
|
345
|
+
|
|
346
|
+
The Extension Layer approach works identically to New Relic and Datadog Lambda Layers. You package `@senzops/apm-node` as a Lambda Layer, point the function's handler to Senzor's auto-wrapper, and set `SENZOR_LAMBDA_HANDLER` to your original handler. No code changes.
|
|
347
|
+
|
|
348
|
+
**How it works:**
|
|
349
|
+
|
|
350
|
+
1. Lambda invokes `@senzops/apm-node/dist/lambda-handler.handler`
|
|
351
|
+
2. The auto-wrapper reads `SENZOR_LAMBDA_HANDLER` (e.g., `index.handler`)
|
|
352
|
+
3. It dynamically loads your original handler module from `LAMBDA_TASK_ROOT`
|
|
353
|
+
4. It wraps your handler with `wrapLambda()` for full APM coverage
|
|
354
|
+
5. It re-exports the wrapped function for Lambda to invoke
|
|
355
|
+
|
|
356
|
+
**Step 1: Build the Lambda Layer**
|
|
357
|
+
|
|
358
|
+
```sh
|
|
359
|
+
mkdir -p senzor-layer/nodejs
|
|
360
|
+
cd senzor-layer/nodejs
|
|
361
|
+
npm init -y
|
|
362
|
+
npm install @senzops/apm-node
|
|
363
|
+
cd ..
|
|
364
|
+
zip -r senzor-apm-layer.zip nodejs/
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Step 2: Publish the Layer**
|
|
368
|
+
|
|
369
|
+
```sh
|
|
370
|
+
aws lambda publish-layer-version \
|
|
371
|
+
--layer-name senzor-apm-node \
|
|
372
|
+
--zip-file fileb://senzor-apm-layer.zip \
|
|
373
|
+
--compatible-runtimes nodejs18.x nodejs20.x nodejs22.x
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**Step 3: Configure Your Function**
|
|
377
|
+
|
|
378
|
+
```sh
|
|
379
|
+
aws lambda update-function-configuration \
|
|
380
|
+
--function-name my-function \
|
|
381
|
+
--layers <LAYER_ARN> \
|
|
382
|
+
--handler @senzops/apm-node/dist/lambda-handler.handler \
|
|
383
|
+
--environment Variables="{ \
|
|
384
|
+
SENZOR_API_KEY=sz_apm_xxx, \
|
|
385
|
+
SENZOR_LAMBDA_HANDLER=index.handler, \
|
|
386
|
+
NODE_OPTIONS=--require @senzops/apm-node/register \
|
|
387
|
+
}"
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
| Environment Variable | Required | Description |
|
|
391
|
+
|---------------------|----------|-------------|
|
|
392
|
+
| `SENZOR_API_KEY` | Yes | Your Senzor APM API key |
|
|
393
|
+
| `SENZOR_LAMBDA_HANDLER` | Yes | Original handler path (e.g., `index.handler`, `src/app.myHandler`) |
|
|
394
|
+
| `NODE_OPTIONS` | Recommended | `--require @senzops/apm-node/register` for full preload coverage |
|
|
395
|
+
|
|
396
|
+
The handler path supports nested exports: `SENZOR_LAMBDA_HANDLER=src/handlers.api.get` resolves to `require('src/handlers').api.get`.
|
|
397
|
+
|
|
398
|
+
### 4.2 Extension Layer with AWS CDK
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
402
|
+
import * as path from 'path';
|
|
403
|
+
|
|
404
|
+
// Create the Senzor APM Layer
|
|
405
|
+
const senzorLayer = new lambda.LayerVersion(this, 'SenzorApmLayer', {
|
|
406
|
+
code: lambda.Code.fromAsset(path.join(__dirname, 'senzor-layer')),
|
|
407
|
+
compatibleRuntimes: [
|
|
408
|
+
lambda.Runtime.NODEJS_18_X,
|
|
409
|
+
lambda.Runtime.NODEJS_20_X,
|
|
410
|
+
lambda.Runtime.NODEJS_22_X,
|
|
411
|
+
],
|
|
412
|
+
description: 'Senzor APM Node.js Lambda Extension Layer',
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Attach to your Lambda function
|
|
416
|
+
const fn = new lambda.Function(this, 'MyFunction', {
|
|
417
|
+
runtime: lambda.Runtime.NODEJS_20_X,
|
|
418
|
+
// Point handler to Senzor's auto-wrapper
|
|
419
|
+
handler: '@senzops/apm-node/dist/lambda-handler.handler',
|
|
420
|
+
code: lambda.Code.fromAsset('lambda'),
|
|
421
|
+
layers: [senzorLayer],
|
|
422
|
+
environment: {
|
|
423
|
+
SENZOR_API_KEY: senzorApiKey.stringValue,
|
|
424
|
+
// Your original handler path
|
|
425
|
+
SENZOR_LAMBDA_HANDLER: 'index.handler',
|
|
426
|
+
NODE_OPTIONS: '--require @senzops/apm-node/register',
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Build the layer directory first:
|
|
432
|
+
|
|
433
|
+
```sh
|
|
434
|
+
mkdir -p senzor-layer/nodejs && cd senzor-layer/nodejs
|
|
435
|
+
npm init -y && npm install @senzops/apm-node
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### 4.3 Extension Layer with AWS SAM
|
|
439
|
+
|
|
440
|
+
```yaml
|
|
441
|
+
# template.yaml
|
|
442
|
+
AWSTemplateFormatVersion: '2010-09-09'
|
|
443
|
+
Transform: AWS::Serverless-2016-10-31
|
|
444
|
+
|
|
445
|
+
Globals:
|
|
446
|
+
Function:
|
|
447
|
+
Layers:
|
|
448
|
+
- !Ref SenzorApmLayer
|
|
449
|
+
Environment:
|
|
450
|
+
Variables:
|
|
451
|
+
SENZOR_API_KEY: !Ref SenzorApiKey
|
|
452
|
+
NODE_OPTIONS: '--require @senzops/apm-node/register'
|
|
453
|
+
|
|
454
|
+
Resources:
|
|
455
|
+
SenzorApmLayer:
|
|
456
|
+
Type: AWS::Serverless::LayerVersion
|
|
457
|
+
Properties:
|
|
458
|
+
LayerName: senzor-apm-node
|
|
459
|
+
ContentUri: senzor-layer/
|
|
460
|
+
CompatibleRuntimes:
|
|
461
|
+
- nodejs18.x
|
|
462
|
+
- nodejs20.x
|
|
463
|
+
- nodejs22.x
|
|
464
|
+
|
|
465
|
+
MyFunction:
|
|
466
|
+
Type: AWS::Serverless::Function
|
|
467
|
+
Properties:
|
|
468
|
+
Handler: '@senzops/apm-node/dist/lambda-handler.handler'
|
|
469
|
+
Runtime: nodejs20.x
|
|
470
|
+
CodeUri: src/
|
|
471
|
+
Environment:
|
|
472
|
+
Variables:
|
|
473
|
+
SENZOR_LAMBDA_HANDLER: index.handler
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### 4.4 Extension Layer with Serverless Framework
|
|
477
|
+
|
|
478
|
+
```yaml
|
|
479
|
+
# serverless.yml
|
|
480
|
+
service: my-service
|
|
481
|
+
|
|
482
|
+
provider:
|
|
483
|
+
name: aws
|
|
484
|
+
runtime: nodejs20.x
|
|
485
|
+
environment:
|
|
486
|
+
SENZOR_API_KEY: ${ssm:/senzor/api-key}
|
|
487
|
+
NODE_OPTIONS: '--require @senzops/apm-node/register'
|
|
488
|
+
|
|
489
|
+
layers:
|
|
490
|
+
senzorApm:
|
|
491
|
+
path: senzor-layer
|
|
492
|
+
compatibleRuntimes:
|
|
493
|
+
- nodejs18.x
|
|
494
|
+
- nodejs20.x
|
|
495
|
+
- nodejs22.x
|
|
496
|
+
|
|
497
|
+
functions:
|
|
498
|
+
api:
|
|
499
|
+
handler: '@senzops/apm-node/dist/lambda-handler.handler'
|
|
500
|
+
layers:
|
|
501
|
+
- !Ref SenzorApmLambdaLayer
|
|
502
|
+
environment:
|
|
503
|
+
SENZOR_LAMBDA_HANDLER: src/handlers/api.handler
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### 4.5 Extension Layer via AWS Console
|
|
507
|
+
|
|
508
|
+
1. **Create the Layer zip** locally:
|
|
509
|
+
```sh
|
|
510
|
+
mkdir -p senzor-layer/nodejs && cd senzor-layer/nodejs
|
|
511
|
+
npm init -y && npm install @senzops/apm-node
|
|
512
|
+
cd .. && zip -r senzor-apm-layer.zip nodejs/
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
2. **Upload the Layer**: Go to Lambda > Layers > Create layer. Upload `senzor-apm-layer.zip`. Set compatible runtimes to `nodejs18.x`, `nodejs20.x`, `nodejs22.x`.
|
|
516
|
+
|
|
517
|
+
3. **Attach to your function**: Go to your Lambda function > Layers > Add a layer. Choose "Custom layers" and select `senzor-apm-node`.
|
|
518
|
+
|
|
519
|
+
4. **Update function configuration**:
|
|
520
|
+
- **Handler**: `@senzops/apm-node/dist/lambda-handler.handler`
|
|
521
|
+
- **Environment variables**:
|
|
522
|
+
- `SENZOR_API_KEY` = your API key
|
|
523
|
+
- `SENZOR_LAMBDA_HANDLER` = your original handler (e.g., `index.handler`)
|
|
524
|
+
- `NODE_OPTIONS` = `--require @senzops/apm-node/register`
|
|
525
|
+
|
|
526
|
+
### 4.6 Extension Layer with Terraform
|
|
527
|
+
|
|
528
|
+
```hcl
|
|
529
|
+
resource "aws_lambda_layer_version" "senzor_apm" {
|
|
530
|
+
filename = "senzor-apm-layer.zip"
|
|
531
|
+
layer_name = "senzor-apm-node"
|
|
532
|
+
compatible_runtimes = ["nodejs18.x", "nodejs20.x", "nodejs22.x"]
|
|
533
|
+
description = "Senzor APM Node.js Lambda Extension Layer"
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
resource "aws_lambda_function" "api" {
|
|
537
|
+
function_name = "my-function"
|
|
538
|
+
runtime = "nodejs20.x"
|
|
539
|
+
handler = "@senzops/apm-node/dist/lambda-handler.handler"
|
|
540
|
+
filename = "function.zip"
|
|
541
|
+
role = aws_iam_role.lambda.arn
|
|
542
|
+
|
|
543
|
+
layers = [aws_lambda_layer_version.senzor_apm.arn]
|
|
544
|
+
|
|
545
|
+
environment {
|
|
546
|
+
variables = {
|
|
547
|
+
SENZOR_API_KEY = var.senzor_api_key
|
|
548
|
+
SENZOR_LAMBDA_HANDLER = "index.handler"
|
|
549
|
+
NODE_OPTIONS = "--require @senzops/apm-node/register"
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### 4.7 Code-Level Handler Wrapper
|
|
556
|
+
|
|
557
|
+
When you prefer code-level control or cannot use Lambda Layers:
|
|
337
558
|
|
|
338
559
|
```ts
|
|
339
560
|
import Senzor from '@senzops/apm-node';
|
|
@@ -346,7 +567,9 @@ export const handler = Senzor.wrapLambda(async (event, context) => {
|
|
|
346
567
|
});
|
|
347
568
|
```
|
|
348
569
|
|
|
349
|
-
### 4.
|
|
570
|
+
### 4.8 What Gets Captured
|
|
571
|
+
|
|
572
|
+
Both the Extension Layer and code-level wrapper capture the same telemetry:
|
|
350
573
|
|
|
351
574
|
**Cold Start Detection:**
|
|
352
575
|
The first invocation in each container is tagged with `faas.coldstart: true`. Subsequent warm invocations are tagged `false`.
|
|
@@ -369,7 +592,6 @@ The wrapper inspects the event shape and automatically detects:
|
|
|
369
592
|
For HTTP triggers (API Gateway, ALB), the wrapper extracts method, path, headers, client IP, and status code. For messaging triggers, it extracts queue/topic names, batch sizes, and table names.
|
|
370
593
|
|
|
371
594
|
**Lambda Context Extraction:**
|
|
372
|
-
Every invocation captures these attributes:
|
|
373
595
|
|
|
374
596
|
| Attribute | Source |
|
|
375
597
|
|-----------|--------|
|
|
@@ -387,49 +609,14 @@ Every invocation captures these attributes:
|
|
|
387
609
|
| `aws.log.group.names` | `context.logGroupName` |
|
|
388
610
|
|
|
389
611
|
**Forced Flush:**
|
|
390
|
-
After every invocation, the wrapper calls `await Senzor.flush()` before returning to the Lambda runtime.
|
|
612
|
+
After every invocation, the wrapper calls `await Senzor.flush()` before returning to the Lambda runtime. Lambda freezes the process immediately after the handler returns.
|
|
391
613
|
|
|
392
614
|
**Lambda Extensions API:**
|
|
393
|
-
The wrapper registers as an internal Lambda extension to receive `SHUTDOWN` lifecycle events. This provides a safety-net flush when the
|
|
394
|
-
|
|
395
|
-
### 4.3 Lambda Layer Deployment (Zero Code Changes)
|
|
396
|
-
|
|
397
|
-
Package the SDK as a Lambda Layer and use `NODE_OPTIONS` for automatic initialization:
|
|
398
|
-
|
|
399
|
-
**Step 1: Create the Layer**
|
|
400
|
-
|
|
401
|
-
```sh
|
|
402
|
-
mkdir -p layer/nodejs
|
|
403
|
-
cd layer/nodejs
|
|
404
|
-
npm init -y
|
|
405
|
-
npm install @senzops/apm-node
|
|
406
|
-
cd ..
|
|
407
|
-
zip -r senzor-apm-layer.zip nodejs/
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
**Step 2: Publish the Layer**
|
|
411
|
-
|
|
412
|
-
```sh
|
|
413
|
-
aws lambda publish-layer-version \
|
|
414
|
-
--layer-name senzor-apm-node \
|
|
415
|
-
--zip-file fileb://senzor-apm-layer.zip \
|
|
416
|
-
--compatible-runtimes nodejs18.x nodejs20.x nodejs22.x
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
**Step 3: Attach to Your Function**
|
|
615
|
+
The wrapper registers as an internal Lambda extension to receive `SHUTDOWN` lifecycle events. This provides a safety-net flush when the execution environment is being terminated.
|
|
420
616
|
|
|
421
|
-
|
|
422
|
-
aws lambda update-function-configuration \
|
|
423
|
-
--function-name my-function \
|
|
424
|
-
--layers arn:aws:lambda:us-east-1:123456789:layer:senzor-apm-node:1 \
|
|
425
|
-
--environment Variables="{SENZOR_API_KEY=sz_apm_xxx,NODE_OPTIONS=--require @senzops/apm-node/register}"
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
With this approach, the SDK initializes via preload before your handler code runs. For HTTP triggers, you still need `Senzor.wrapLambda` in your handler for proper route detection and response status capture. For non-HTTP triggers, preload alone captures outgoing calls and database operations.
|
|
617
|
+
### 4.9 Lambda Auto-Detection
|
|
429
618
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
When running inside Lambda (detected via `AWS_LAMBDA_FUNCTION_NAME` env var), the register entrypoint automatically applies these optimizations:
|
|
619
|
+
When running inside Lambda (detected via `AWS_LAMBDA_FUNCTION_NAME` env var), the SDK automatically optimizes settings:
|
|
433
620
|
|
|
434
621
|
| Setting | Default | Lambda Override |
|
|
435
622
|
|---------|---------|-----------------|
|
|
@@ -437,47 +624,6 @@ When running inside Lambda (detected via `AWS_LAMBDA_FUNCTION_NAME` env var), th
|
|
|
437
624
|
| `batchSize` | `100` | `10` (short-lived invocations) |
|
|
438
625
|
| `flushInterval` | `10000` | `0` (flush on demand only) |
|
|
439
626
|
|
|
440
|
-
### 4.5 Lambda with SAM / CDK / Serverless Framework
|
|
441
|
-
|
|
442
|
-
**AWS SAM (`template.yaml`):**
|
|
443
|
-
|
|
444
|
-
```yaml
|
|
445
|
-
Globals:
|
|
446
|
-
Function:
|
|
447
|
-
Layers:
|
|
448
|
-
- !Ref SenzorLayer
|
|
449
|
-
Environment:
|
|
450
|
-
Variables:
|
|
451
|
-
SENZOR_API_KEY: !Ref SenzorApiKey
|
|
452
|
-
NODE_OPTIONS: "--require @senzops/apm-node/register"
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
**Serverless Framework (`serverless.yml`):**
|
|
456
|
-
|
|
457
|
-
```yaml
|
|
458
|
-
provider:
|
|
459
|
-
environment:
|
|
460
|
-
SENZOR_API_KEY: ${ssm:/senzor/api-key}
|
|
461
|
-
NODE_OPTIONS: "--require @senzops/apm-node/register"
|
|
462
|
-
layers:
|
|
463
|
-
- arn:aws:lambda:us-east-1:123456789:layer:senzor-apm-node:1
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
**AWS CDK:**
|
|
467
|
-
|
|
468
|
-
```ts
|
|
469
|
-
const fn = new lambda.Function(this, 'Handler', {
|
|
470
|
-
runtime: lambda.Runtime.NODEJS_20_X,
|
|
471
|
-
handler: 'index.handler',
|
|
472
|
-
code: lambda.Code.fromAsset('dist'),
|
|
473
|
-
layers: [senzorLayer],
|
|
474
|
-
environment: {
|
|
475
|
-
SENZOR_API_KEY: senzorApiKey.stringValue,
|
|
476
|
-
NODE_OPTIONS: '--require @senzops/apm-node/register',
|
|
477
|
-
},
|
|
478
|
-
});
|
|
479
|
-
```
|
|
480
|
-
|
|
481
627
|
---
|
|
482
628
|
|
|
483
629
|
## 5. Cloudflare Workers Integration
|