trickle-observe 0.2.60 → 0.2.61
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/dist/lambda.d.ts +61 -0
- package/dist/lambda.js +177 -0
- package/dist/trace-var.js +4 -1
- package/dist/transport.js +4 -1
- package/package.json +4 -3
- package/src/lambda.test.ts +147 -0
- package/src/lambda.ts +160 -0
- package/src/trace-var.ts +4 -1
- package/src/transport.ts +4 -1
package/dist/lambda.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trickle/lambda — Zero-config observability for AWS Lambda functions.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a Lambda handler to:
|
|
5
|
+
* 1. Auto-instrument all functions with type observation
|
|
6
|
+
* 2. Write observations to /tmp/.trickle/ (Lambda's writable filesystem)
|
|
7
|
+
* OR send to TRICKLE_BACKEND_URL if set
|
|
8
|
+
* 3. Flush all pending observations synchronously before Lambda freezes
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* import { wrapLambda } from 'trickle-observe/lambda';
|
|
13
|
+
*
|
|
14
|
+
* export const handler = wrapLambda(async (event, context) => {
|
|
15
|
+
* const result = await processOrder(event.orderId);
|
|
16
|
+
* return { statusCode: 200, body: JSON.stringify(result) };
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* OR zero-code via NODE_OPTIONS:
|
|
20
|
+
*
|
|
21
|
+
* NODE_OPTIONS="--require trickle-observe/auto-env" TRICKLE_AUTO=1
|
|
22
|
+
*
|
|
23
|
+
* Environment variables:
|
|
24
|
+
* TRICKLE_BACKEND_URL — Send observations to HTTP backend (optional)
|
|
25
|
+
* TRICKLE_LOCAL_DIR — Override local dir (default: /tmp/.trickle)
|
|
26
|
+
* TRICKLE_DEBUG — Set to "1" for debug logging
|
|
27
|
+
* TRICKLE_OBSERVE_INCLUDE — Comma-separated substrings to observe
|
|
28
|
+
* TRICKLE_OBSERVE_EXCLUDE — Comma-separated substrings to skip
|
|
29
|
+
*/
|
|
30
|
+
import './observe-register';
|
|
31
|
+
export type LambdaEvent = Record<string, unknown>;
|
|
32
|
+
export type LambdaContext = {
|
|
33
|
+
functionName: string;
|
|
34
|
+
functionVersion: string;
|
|
35
|
+
invokedFunctionArn: string;
|
|
36
|
+
awsRequestId: string;
|
|
37
|
+
remainingTimeInMillis?: () => number;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
};
|
|
40
|
+
export type LambdaHandler<E = LambdaEvent, R = unknown> = (event: E, context: LambdaContext) => Promise<R>;
|
|
41
|
+
/**
|
|
42
|
+
* Wrap a Lambda handler with trickle observability.
|
|
43
|
+
*
|
|
44
|
+
* Instruments all called functions automatically, writes type observations
|
|
45
|
+
* to /tmp/.trickle/, and flushes before Lambda freezes the process.
|
|
46
|
+
*/
|
|
47
|
+
export declare function wrapLambda<E = LambdaEvent, R = unknown>(handler: LambdaHandler<E, R>): LambdaHandler<E, R>;
|
|
48
|
+
/**
|
|
49
|
+
* Print the contents of the local trickle observations directory to stdout
|
|
50
|
+
* as newline-delimited JSON. Useful for inspecting Lambda observations in
|
|
51
|
+
* CloudWatch Logs when TRICKLE_BACKEND_URL is not set.
|
|
52
|
+
*
|
|
53
|
+
* Call at the end of your handler to stream observations to CloudWatch:
|
|
54
|
+
*
|
|
55
|
+
* export const handler = wrapLambda(async (event) => {
|
|
56
|
+
* const result = await processOrder(event.orderId);
|
|
57
|
+
* printObservations(); // → streamed to CloudWatch Logs
|
|
58
|
+
* return result;
|
|
59
|
+
* });
|
|
60
|
+
*/
|
|
61
|
+
export declare function printObservations(): void;
|
package/dist/lambda.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* trickle/lambda — Zero-config observability for AWS Lambda functions.
|
|
4
|
+
*
|
|
5
|
+
* Wraps a Lambda handler to:
|
|
6
|
+
* 1. Auto-instrument all functions with type observation
|
|
7
|
+
* 2. Write observations to /tmp/.trickle/ (Lambda's writable filesystem)
|
|
8
|
+
* OR send to TRICKLE_BACKEND_URL if set
|
|
9
|
+
* 3. Flush all pending observations synchronously before Lambda freezes
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
*
|
|
13
|
+
* import { wrapLambda } from 'trickle-observe/lambda';
|
|
14
|
+
*
|
|
15
|
+
* export const handler = wrapLambda(async (event, context) => {
|
|
16
|
+
* const result = await processOrder(event.orderId);
|
|
17
|
+
* return { statusCode: 200, body: JSON.stringify(result) };
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* OR zero-code via NODE_OPTIONS:
|
|
21
|
+
*
|
|
22
|
+
* NODE_OPTIONS="--require trickle-observe/auto-env" TRICKLE_AUTO=1
|
|
23
|
+
*
|
|
24
|
+
* Environment variables:
|
|
25
|
+
* TRICKLE_BACKEND_URL — Send observations to HTTP backend (optional)
|
|
26
|
+
* TRICKLE_LOCAL_DIR — Override local dir (default: /tmp/.trickle)
|
|
27
|
+
* TRICKLE_DEBUG — Set to "1" for debug logging
|
|
28
|
+
* TRICKLE_OBSERVE_INCLUDE — Comma-separated substrings to observe
|
|
29
|
+
* TRICKLE_OBSERVE_EXCLUDE — Comma-separated substrings to skip
|
|
30
|
+
*/
|
|
31
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
32
|
+
if (k2 === undefined) k2 = k;
|
|
33
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
34
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
35
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
36
|
+
}
|
|
37
|
+
Object.defineProperty(o, k2, desc);
|
|
38
|
+
}) : (function(o, m, k, k2) {
|
|
39
|
+
if (k2 === undefined) k2 = k;
|
|
40
|
+
o[k2] = m[k];
|
|
41
|
+
}));
|
|
42
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
43
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
44
|
+
}) : function(o, v) {
|
|
45
|
+
o["default"] = v;
|
|
46
|
+
});
|
|
47
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
48
|
+
var ownKeys = function(o) {
|
|
49
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
50
|
+
var ar = [];
|
|
51
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
52
|
+
return ar;
|
|
53
|
+
};
|
|
54
|
+
return ownKeys(o);
|
|
55
|
+
};
|
|
56
|
+
return function (mod) {
|
|
57
|
+
if (mod && mod.__esModule) return mod;
|
|
58
|
+
var result = {};
|
|
59
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
60
|
+
__setModuleDefault(result, mod);
|
|
61
|
+
return result;
|
|
62
|
+
};
|
|
63
|
+
})();
|
|
64
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
65
|
+
exports.wrapLambda = wrapLambda;
|
|
66
|
+
exports.printObservations = printObservations;
|
|
67
|
+
const path = __importStar(require("path"));
|
|
68
|
+
const fs = __importStar(require("fs"));
|
|
69
|
+
const transport_1 = require("./transport");
|
|
70
|
+
const trace_var_1 = require("./trace-var");
|
|
71
|
+
// Install Module._compile and Module._load hooks for auto-instrumentation
|
|
72
|
+
require("./observe-register");
|
|
73
|
+
let initialized = false;
|
|
74
|
+
function initOnce() {
|
|
75
|
+
if (initialized)
|
|
76
|
+
return;
|
|
77
|
+
initialized = true;
|
|
78
|
+
const debug = process.env.TRICKLE_DEBUG === '1';
|
|
79
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || '/tmp/.trickle';
|
|
80
|
+
// Ensure /tmp/.trickle exists (writable in Lambda, unlike /var/task)
|
|
81
|
+
try {
|
|
82
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
catch { }
|
|
85
|
+
if (process.env.TRICKLE_BACKEND_URL) {
|
|
86
|
+
// HTTP mode: send to developer's backend (e.g. via ngrok)
|
|
87
|
+
(0, transport_1.configure)({
|
|
88
|
+
backendUrl: process.env.TRICKLE_BACKEND_URL,
|
|
89
|
+
batchIntervalMs: 100, // aggressive batching for Lambda
|
|
90
|
+
enabled: true,
|
|
91
|
+
debug,
|
|
92
|
+
environment: 'node',
|
|
93
|
+
});
|
|
94
|
+
if (debug)
|
|
95
|
+
console.log(`[trickle/lambda] HTTP mode → ${process.env.TRICKLE_BACKEND_URL}`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// Local file mode: write to /tmp/.trickle/observations.jsonl
|
|
99
|
+
process.env.TRICKLE_LOCAL = '1';
|
|
100
|
+
process.env.TRICKLE_LOCAL_DIR = dir;
|
|
101
|
+
(0, transport_1.configure)({ backendUrl: 'http://localhost:4888', batchIntervalMs: 2000, enabled: true, debug, environment: 'node' });
|
|
102
|
+
if (debug)
|
|
103
|
+
console.log(`[trickle/lambda] Local mode → ${dir}/observations.jsonl`);
|
|
104
|
+
}
|
|
105
|
+
// Initialize variable tracer (writes to variables.jsonl)
|
|
106
|
+
(0, trace_var_1.initVarTracer)({ debug });
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Wrap a Lambda handler with trickle observability.
|
|
110
|
+
*
|
|
111
|
+
* Instruments all called functions automatically, writes type observations
|
|
112
|
+
* to /tmp/.trickle/, and flushes before Lambda freezes the process.
|
|
113
|
+
*/
|
|
114
|
+
function wrapLambda(handler) {
|
|
115
|
+
return async (event, context) => {
|
|
116
|
+
initOnce();
|
|
117
|
+
const debug = process.env.TRICKLE_DEBUG === '1';
|
|
118
|
+
const startMs = Date.now();
|
|
119
|
+
try {
|
|
120
|
+
const result = await handler(event, context);
|
|
121
|
+
// Flush pending HTTP observations before Lambda freezes the process.
|
|
122
|
+
// File-mode writes are already synchronous, so no flush needed there.
|
|
123
|
+
if (process.env.TRICKLE_BACKEND_URL) {
|
|
124
|
+
await (0, transport_1.flush)();
|
|
125
|
+
}
|
|
126
|
+
if (debug) {
|
|
127
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || '/tmp/.trickle';
|
|
128
|
+
const elapsed = Date.now() - startMs;
|
|
129
|
+
const varsFile = path.join(dir, 'variables.jsonl');
|
|
130
|
+
try {
|
|
131
|
+
const size = fs.statSync(varsFile).size;
|
|
132
|
+
console.log(`[trickle/lambda] Flushed in ${elapsed}ms, observations: ${varsFile} (${size}b)`);
|
|
133
|
+
}
|
|
134
|
+
catch { }
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
// Flush even on errors so partial observations are captured
|
|
140
|
+
if (process.env.TRICKLE_BACKEND_URL) {
|
|
141
|
+
await (0, transport_1.flush)().catch(() => { });
|
|
142
|
+
}
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Print the contents of the local trickle observations directory to stdout
|
|
149
|
+
* as newline-delimited JSON. Useful for inspecting Lambda observations in
|
|
150
|
+
* CloudWatch Logs when TRICKLE_BACKEND_URL is not set.
|
|
151
|
+
*
|
|
152
|
+
* Call at the end of your handler to stream observations to CloudWatch:
|
|
153
|
+
*
|
|
154
|
+
* export const handler = wrapLambda(async (event) => {
|
|
155
|
+
* const result = await processOrder(event.orderId);
|
|
156
|
+
* printObservations(); // → streamed to CloudWatch Logs
|
|
157
|
+
* return result;
|
|
158
|
+
* });
|
|
159
|
+
*/
|
|
160
|
+
function printObservations() {
|
|
161
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || '/tmp/.trickle';
|
|
162
|
+
for (const file of ['variables.jsonl', 'observations.jsonl']) {
|
|
163
|
+
const filePath = path.join(dir, file);
|
|
164
|
+
try {
|
|
165
|
+
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
166
|
+
if (content) {
|
|
167
|
+
for (const line of content.split('\n')) {
|
|
168
|
+
if (line.trim()) {
|
|
169
|
+
// Prefix so trickle CLI can grep from CloudWatch Logs
|
|
170
|
+
console.log(`[trickle] ${line}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch { /* file may not exist yet */ }
|
|
176
|
+
}
|
|
177
|
+
}
|
package/dist/trace-var.js
CHANGED
|
@@ -70,7 +70,10 @@ const MAX_BUFFER_SIZE = 100;
|
|
|
70
70
|
*/
|
|
71
71
|
function initVarTracer(opts = {}) {
|
|
72
72
|
debugMode = opts.debug === true;
|
|
73
|
-
|
|
73
|
+
// Auto-detect Lambda: use /tmp/.trickle (writable) instead of cwd (read-only in Lambda)
|
|
74
|
+
const isLambda = !!process.env.AWS_LAMBDA_FUNCTION_NAME;
|
|
75
|
+
const defaultDir = isLambda ? '/tmp/.trickle' : path.join(process.cwd(), '.trickle');
|
|
76
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || defaultDir;
|
|
74
77
|
try {
|
|
75
78
|
fs.mkdirSync(dir, { recursive: true });
|
|
76
79
|
}
|
package/dist/transport.js
CHANGED
|
@@ -66,7 +66,10 @@ function configure(opts) {
|
|
|
66
66
|
// Check for local/file-based mode
|
|
67
67
|
if (process.env.TRICKLE_LOCAL === '1') {
|
|
68
68
|
localMode = true;
|
|
69
|
-
|
|
69
|
+
// Auto-detect Lambda: use /tmp/.trickle (writable) instead of cwd (read-only in Lambda)
|
|
70
|
+
const isLambda = !!process.env.AWS_LAMBDA_FUNCTION_NAME;
|
|
71
|
+
const defaultDir = isLambda ? '/tmp/.trickle' : pathMod.join(process.cwd(), '.trickle');
|
|
72
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || defaultDir;
|
|
70
73
|
try {
|
|
71
74
|
fs.mkdirSync(dir, { recursive: true });
|
|
72
75
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trickle-observe",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.61",
|
|
4
4
|
"description": "Runtime type observability for JavaScript applications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
"import": "./dist-esm/vite-plugin.js",
|
|
17
17
|
"require": "./dist/vite-plugin.js"
|
|
18
18
|
},
|
|
19
|
-
"./trace-var": "./dist/trace-var.js"
|
|
19
|
+
"./trace-var": "./dist/trace-var.js",
|
|
20
|
+
"./lambda": "./dist/lambda.js"
|
|
20
21
|
},
|
|
21
22
|
"scripts": {
|
|
22
23
|
"build": "tsc && tsc -p tsconfig.esm.json",
|
|
23
|
-
"test": "npm run build && node --experimental-strip-types --test src/vite-plugin.test.ts",
|
|
24
|
+
"test": "npm run build && node --experimental-strip-types --test src/vite-plugin.test.ts src/lambda.test.ts",
|
|
24
25
|
"prepublishOnly": "npm run build"
|
|
25
26
|
},
|
|
26
27
|
"optionalDependencies": {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for wrapLambda and printObservations.
|
|
3
|
+
*
|
|
4
|
+
* Run with: node --experimental-strip-types --test src/lambda.test.ts
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, before, after } from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as os from 'os';
|
|
11
|
+
|
|
12
|
+
// ── wrapLambda ────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
describe('wrapLambda', () => {
|
|
15
|
+
const makeContext = () => ({
|
|
16
|
+
functionName: 'test-fn',
|
|
17
|
+
functionVersion: '$LATEST',
|
|
18
|
+
invokedFunctionArn: 'arn:aws:lambda:us-east-1:123:function:test-fn',
|
|
19
|
+
awsRequestId: 'req-1',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
let tmpDir: string;
|
|
23
|
+
|
|
24
|
+
before(() => {
|
|
25
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trickle-lambda-test-'));
|
|
26
|
+
process.env.TRICKLE_LOCAL_DIR = tmpDir;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
after(() => {
|
|
30
|
+
delete process.env.TRICKLE_LOCAL_DIR;
|
|
31
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns the handler result unchanged', async () => {
|
|
35
|
+
// Import fresh to avoid module cache issues with env vars
|
|
36
|
+
const { wrapLambda } = await import('../dist/lambda.js');
|
|
37
|
+
const handler = async (_event: unknown, _ctx: unknown) => ({ statusCode: 200, body: 'ok' });
|
|
38
|
+
const wrapped = wrapLambda(handler);
|
|
39
|
+
const result = await wrapped({ orderId: '123' }, makeContext());
|
|
40
|
+
assert.deepEqual(result, { statusCode: 200, body: 'ok' });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('passes event and context to the handler', async () => {
|
|
44
|
+
const { wrapLambda } = await import('../dist/lambda.js');
|
|
45
|
+
let receivedEvent: unknown;
|
|
46
|
+
let receivedCtx: unknown;
|
|
47
|
+
const handler = async (event: unknown, ctx: unknown) => {
|
|
48
|
+
receivedEvent = event;
|
|
49
|
+
receivedCtx = ctx;
|
|
50
|
+
return 'done';
|
|
51
|
+
};
|
|
52
|
+
const wrapped = wrapLambda(handler);
|
|
53
|
+
const ctx = makeContext();
|
|
54
|
+
await wrapped({ hello: 'world' }, ctx);
|
|
55
|
+
assert.deepEqual(receivedEvent, { hello: 'world' });
|
|
56
|
+
assert.equal((receivedCtx as typeof ctx).awsRequestId, 'req-1');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('re-throws errors from the handler', async () => {
|
|
60
|
+
const { wrapLambda } = await import('../dist/lambda.js');
|
|
61
|
+
const handler = async () => { throw new Error('Lambda error'); };
|
|
62
|
+
const wrapped = wrapLambda(handler);
|
|
63
|
+
await assert.rejects(() => wrapped({}, makeContext()), /Lambda error/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('does not throw if TRICKLE_BACKEND_URL is not set', async () => {
|
|
67
|
+
delete process.env.TRICKLE_BACKEND_URL;
|
|
68
|
+
const { wrapLambda } = await import('../dist/lambda.js');
|
|
69
|
+
const handler = async () => 42;
|
|
70
|
+
const wrapped = wrapLambda(handler);
|
|
71
|
+
const result = await wrapped({}, makeContext());
|
|
72
|
+
assert.equal(result, 42);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ── printObservations ─────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
describe('printObservations', () => {
|
|
79
|
+
let tmpDir: string;
|
|
80
|
+
let originalDir: string | undefined;
|
|
81
|
+
let logged: string[];
|
|
82
|
+
let origLog: typeof console.log;
|
|
83
|
+
|
|
84
|
+
before(() => {
|
|
85
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trickle-print-test-'));
|
|
86
|
+
originalDir = process.env.TRICKLE_LOCAL_DIR;
|
|
87
|
+
process.env.TRICKLE_LOCAL_DIR = tmpDir;
|
|
88
|
+
// Capture console.log output
|
|
89
|
+
origLog = console.log;
|
|
90
|
+
logged = [];
|
|
91
|
+
console.log = (...args: unknown[]) => { logged.push(args.join(' ')); };
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
after(() => {
|
|
95
|
+
console.log = origLog;
|
|
96
|
+
if (originalDir !== undefined) {
|
|
97
|
+
process.env.TRICKLE_LOCAL_DIR = originalDir;
|
|
98
|
+
} else {
|
|
99
|
+
delete process.env.TRICKLE_LOCAL_DIR;
|
|
100
|
+
}
|
|
101
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('prints [trickle] prefixed JSON lines from variables.jsonl', async () => {
|
|
105
|
+
const record = JSON.stringify({ kind: 'var', name: 'x', value: 1 });
|
|
106
|
+
fs.writeFileSync(path.join(tmpDir, 'variables.jsonl'), record + '\n');
|
|
107
|
+
|
|
108
|
+
const { printObservations } = await import('../dist/lambda.js');
|
|
109
|
+
logged = [];
|
|
110
|
+
printObservations();
|
|
111
|
+
|
|
112
|
+
assert.ok(logged.some(l => l.startsWith('[trickle]')), 'should prefix with [trickle]');
|
|
113
|
+
assert.ok(logged.some(l => l.includes('"kind"')), 'should include JSON content');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('does not crash when no files exist', async () => {
|
|
117
|
+
const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trickle-empty-'));
|
|
118
|
+
const prevDir = process.env.TRICKLE_LOCAL_DIR;
|
|
119
|
+
process.env.TRICKLE_LOCAL_DIR = emptyDir;
|
|
120
|
+
try {
|
|
121
|
+
const { printObservations } = await import('../dist/lambda.js');
|
|
122
|
+
assert.doesNotThrow(() => printObservations());
|
|
123
|
+
} finally {
|
|
124
|
+
process.env.TRICKLE_LOCAL_DIR = prevDir;
|
|
125
|
+
fs.rmSync(emptyDir, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('prints observations from both variables.jsonl and observations.jsonl', async () => {
|
|
130
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'trickle-both-'));
|
|
131
|
+
const prevDir = process.env.TRICKLE_LOCAL_DIR;
|
|
132
|
+
process.env.TRICKLE_LOCAL_DIR = dir;
|
|
133
|
+
try {
|
|
134
|
+
fs.writeFileSync(path.join(dir, 'variables.jsonl'), JSON.stringify({ kind: 'var' }) + '\n');
|
|
135
|
+
fs.writeFileSync(path.join(dir, 'observations.jsonl'), JSON.stringify({ kind: 'obs' }) + '\n');
|
|
136
|
+
|
|
137
|
+
const { printObservations } = await import('../dist/lambda.js');
|
|
138
|
+
logged = [];
|
|
139
|
+
printObservations();
|
|
140
|
+
const count = logged.filter(l => l.startsWith('[trickle]')).length;
|
|
141
|
+
assert.ok(count >= 2, `should print at least 2 [trickle] lines, got ${count}`);
|
|
142
|
+
} finally {
|
|
143
|
+
process.env.TRICKLE_LOCAL_DIR = prevDir;
|
|
144
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
package/src/lambda.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trickle/lambda — Zero-config observability for AWS Lambda functions.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a Lambda handler to:
|
|
5
|
+
* 1. Auto-instrument all functions with type observation
|
|
6
|
+
* 2. Write observations to /tmp/.trickle/ (Lambda's writable filesystem)
|
|
7
|
+
* OR send to TRICKLE_BACKEND_URL if set
|
|
8
|
+
* 3. Flush all pending observations synchronously before Lambda freezes
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* import { wrapLambda } from 'trickle-observe/lambda';
|
|
13
|
+
*
|
|
14
|
+
* export const handler = wrapLambda(async (event, context) => {
|
|
15
|
+
* const result = await processOrder(event.orderId);
|
|
16
|
+
* return { statusCode: 200, body: JSON.stringify(result) };
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* OR zero-code via NODE_OPTIONS:
|
|
20
|
+
*
|
|
21
|
+
* NODE_OPTIONS="--require trickle-observe/auto-env" TRICKLE_AUTO=1
|
|
22
|
+
*
|
|
23
|
+
* Environment variables:
|
|
24
|
+
* TRICKLE_BACKEND_URL — Send observations to HTTP backend (optional)
|
|
25
|
+
* TRICKLE_LOCAL_DIR — Override local dir (default: /tmp/.trickle)
|
|
26
|
+
* TRICKLE_DEBUG — Set to "1" for debug logging
|
|
27
|
+
* TRICKLE_OBSERVE_INCLUDE — Comma-separated substrings to observe
|
|
28
|
+
* TRICKLE_OBSERVE_EXCLUDE — Comma-separated substrings to skip
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import * as path from 'path';
|
|
32
|
+
import * as fs from 'fs';
|
|
33
|
+
import { configure, flush } from './transport';
|
|
34
|
+
import { initVarTracer } from './trace-var';
|
|
35
|
+
// Install Module._compile and Module._load hooks for auto-instrumentation
|
|
36
|
+
import './observe-register';
|
|
37
|
+
|
|
38
|
+
export type LambdaEvent = Record<string, unknown>;
|
|
39
|
+
export type LambdaContext = {
|
|
40
|
+
functionName: string;
|
|
41
|
+
functionVersion: string;
|
|
42
|
+
invokedFunctionArn: string;
|
|
43
|
+
awsRequestId: string;
|
|
44
|
+
remainingTimeInMillis?: () => number;
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
};
|
|
47
|
+
export type LambdaHandler<E = LambdaEvent, R = unknown> = (
|
|
48
|
+
event: E,
|
|
49
|
+
context: LambdaContext,
|
|
50
|
+
) => Promise<R>;
|
|
51
|
+
|
|
52
|
+
let initialized = false;
|
|
53
|
+
|
|
54
|
+
function initOnce() {
|
|
55
|
+
if (initialized) return;
|
|
56
|
+
initialized = true;
|
|
57
|
+
|
|
58
|
+
const debug = process.env.TRICKLE_DEBUG === '1';
|
|
59
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || '/tmp/.trickle';
|
|
60
|
+
|
|
61
|
+
// Ensure /tmp/.trickle exists (writable in Lambda, unlike /var/task)
|
|
62
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
63
|
+
|
|
64
|
+
if (process.env.TRICKLE_BACKEND_URL) {
|
|
65
|
+
// HTTP mode: send to developer's backend (e.g. via ngrok)
|
|
66
|
+
configure({
|
|
67
|
+
backendUrl: process.env.TRICKLE_BACKEND_URL,
|
|
68
|
+
batchIntervalMs: 100, // aggressive batching for Lambda
|
|
69
|
+
enabled: true,
|
|
70
|
+
debug,
|
|
71
|
+
environment: 'node',
|
|
72
|
+
});
|
|
73
|
+
if (debug) console.log(`[trickle/lambda] HTTP mode → ${process.env.TRICKLE_BACKEND_URL}`);
|
|
74
|
+
} else {
|
|
75
|
+
// Local file mode: write to /tmp/.trickle/observations.jsonl
|
|
76
|
+
process.env.TRICKLE_LOCAL = '1';
|
|
77
|
+
process.env.TRICKLE_LOCAL_DIR = dir;
|
|
78
|
+
configure({ backendUrl: 'http://localhost:4888', batchIntervalMs: 2000, enabled: true, debug, environment: 'node' });
|
|
79
|
+
if (debug) console.log(`[trickle/lambda] Local mode → ${dir}/observations.jsonl`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Initialize variable tracer (writes to variables.jsonl)
|
|
83
|
+
initVarTracer({ debug });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Wrap a Lambda handler with trickle observability.
|
|
88
|
+
*
|
|
89
|
+
* Instruments all called functions automatically, writes type observations
|
|
90
|
+
* to /tmp/.trickle/, and flushes before Lambda freezes the process.
|
|
91
|
+
*/
|
|
92
|
+
export function wrapLambda<E = LambdaEvent, R = unknown>(
|
|
93
|
+
handler: LambdaHandler<E, R>,
|
|
94
|
+
): LambdaHandler<E, R> {
|
|
95
|
+
return async (event: E, context: LambdaContext): Promise<R> => {
|
|
96
|
+
initOnce();
|
|
97
|
+
|
|
98
|
+
const debug = process.env.TRICKLE_DEBUG === '1';
|
|
99
|
+
const startMs = Date.now();
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const result = await handler(event, context);
|
|
103
|
+
|
|
104
|
+
// Flush pending HTTP observations before Lambda freezes the process.
|
|
105
|
+
// File-mode writes are already synchronous, so no flush needed there.
|
|
106
|
+
if (process.env.TRICKLE_BACKEND_URL) {
|
|
107
|
+
await flush();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (debug) {
|
|
111
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || '/tmp/.trickle';
|
|
112
|
+
const elapsed = Date.now() - startMs;
|
|
113
|
+
const varsFile = path.join(dir, 'variables.jsonl');
|
|
114
|
+
try {
|
|
115
|
+
const size = fs.statSync(varsFile).size;
|
|
116
|
+
console.log(`[trickle/lambda] Flushed in ${elapsed}ms, observations: ${varsFile} (${size}b)`);
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
// Flush even on errors so partial observations are captured
|
|
123
|
+
if (process.env.TRICKLE_BACKEND_URL) {
|
|
124
|
+
await flush().catch(() => {});
|
|
125
|
+
}
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Print the contents of the local trickle observations directory to stdout
|
|
133
|
+
* as newline-delimited JSON. Useful for inspecting Lambda observations in
|
|
134
|
+
* CloudWatch Logs when TRICKLE_BACKEND_URL is not set.
|
|
135
|
+
*
|
|
136
|
+
* Call at the end of your handler to stream observations to CloudWatch:
|
|
137
|
+
*
|
|
138
|
+
* export const handler = wrapLambda(async (event) => {
|
|
139
|
+
* const result = await processOrder(event.orderId);
|
|
140
|
+
* printObservations(); // → streamed to CloudWatch Logs
|
|
141
|
+
* return result;
|
|
142
|
+
* });
|
|
143
|
+
*/
|
|
144
|
+
export function printObservations(): void {
|
|
145
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || '/tmp/.trickle';
|
|
146
|
+
for (const file of ['variables.jsonl', 'observations.jsonl']) {
|
|
147
|
+
const filePath = path.join(dir, file);
|
|
148
|
+
try {
|
|
149
|
+
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
150
|
+
if (content) {
|
|
151
|
+
for (const line of content.split('\n')) {
|
|
152
|
+
if (line.trim()) {
|
|
153
|
+
// Prefix so trickle CLI can grep from CloudWatch Logs
|
|
154
|
+
console.log(`[trickle] ${line}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch { /* file may not exist yet */ }
|
|
159
|
+
}
|
|
160
|
+
}
|
package/src/trace-var.ts
CHANGED
|
@@ -50,7 +50,10 @@ export interface VariableObservation {
|
|
|
50
50
|
*/
|
|
51
51
|
export function initVarTracer(opts: { debug?: boolean } = {}): void {
|
|
52
52
|
debugMode = opts.debug === true;
|
|
53
|
-
|
|
53
|
+
// Auto-detect Lambda: use /tmp/.trickle (writable) instead of cwd (read-only in Lambda)
|
|
54
|
+
const isLambda = !!process.env.AWS_LAMBDA_FUNCTION_NAME;
|
|
55
|
+
const defaultDir = isLambda ? '/tmp/.trickle' : path.join(process.cwd(), '.trickle');
|
|
56
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || defaultDir;
|
|
54
57
|
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
55
58
|
varsFilePath = path.join(dir, 'variables.jsonl');
|
|
56
59
|
|
package/src/transport.ts
CHANGED
|
@@ -32,7 +32,10 @@ export function configure(opts: GlobalOpts): void {
|
|
|
32
32
|
// Check for local/file-based mode
|
|
33
33
|
if (process.env.TRICKLE_LOCAL === '1') {
|
|
34
34
|
localMode = true;
|
|
35
|
-
|
|
35
|
+
// Auto-detect Lambda: use /tmp/.trickle (writable) instead of cwd (read-only in Lambda)
|
|
36
|
+
const isLambda = !!process.env.AWS_LAMBDA_FUNCTION_NAME;
|
|
37
|
+
const defaultDir = isLambda ? '/tmp/.trickle' : pathMod.join(process.cwd(), '.trickle');
|
|
38
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || defaultDir;
|
|
36
39
|
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
37
40
|
localFilePath = pathMod.join(dir, 'observations.jsonl');
|
|
38
41
|
if (debug) {
|