lazylog-trace 1.0.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.
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # Lazylog
2
+
3
+ Request-scoped logging for Express. All logs produced during a request are tied to that request and stored (SQLite by default). Retrieve logs by request id, and optionally attach context (e.g. user id) to every log for that request.
4
+
5
+ No UI, no heavy abstractions—just middleware, a request-scoped logger, and persistence.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install lazylog
11
+ ```
12
+
13
+ Peer dependency: **Express** (v4 or v5). You must have `express` installed in your app.
14
+
15
+ ## Build and test (in this repo)
16
+
17
+ There is no build step—the package is plain JavaScript and `main` points at `src/index.js`.
18
+
19
+ **Run tests in this repo:**
20
+
21
+ ```bash
22
+ npm install
23
+ npm test
24
+ ```
25
+
26
+ This runs the integration test (Express app, one request, asserts logs and request are stored). You need Node.js build tools for the `better-sqlite3` native addon; on Windows you may need [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools) or Visual Studio Build Tools.
27
+
28
+ **Test lazylog in another repo (local development):**
29
+
30
+ 1. In **lazylog** (this repo):
31
+ `npm link`
32
+
33
+ 2. In your **app repo**:
34
+ `npm link lazylog`
35
+ Then use it as usual: `const { requestLogger, getRequestLogger } = require('lazylog');`
36
+
37
+ **Or install from a tarball (no link):**
38
+
39
+ 1. In **lazylog**:
40
+ `npm pack`
41
+ This creates `lazylog-1.0.0.tgz`.
42
+
43
+ 2. In your **app repo**:
44
+ `npm install /path/to/lazylog/lazylog-1.0.0.tgz`
45
+
46
+ ## Usage
47
+
48
+ ### 1. Middleware
49
+
50
+ Mount the middleware to enable request-scoped logging and persistence:
51
+
52
+ ```js
53
+ const { requestLogger } = require('lazylog');
54
+
55
+ app.use(express.json()); // if you need req.body
56
+ app.use(requestLogger({
57
+ ignorePaths: ['/health'],
58
+ logBody: true,
59
+ logResponse: true,
60
+ databasePath: './logs.sqlite', // optional; default is ./logs.sqlite
61
+ // or databaseUrl: 'file:./logs.sqlite',
62
+ }));
63
+ ```
64
+
65
+ **Options:**
66
+
67
+ | Option | Default | Description |
68
+ |-----------------|----------------|-------------|
69
+ | `ignorePaths` | `[]` | Paths (strings or RegExp) to skip logging entirely. |
70
+ | `logBody` | `true` | Persist `req.body` on the request row. |
71
+ | `logResponse` | `true` | Capture response body (via `res.send`/`res.json`) and persist on the request row. |
72
+ | `databasePath` | `'./logs.sqlite'` | Path to the SQLite file. |
73
+ | `databaseUrl` | — | Alternative: e.g. `'file:./logs.sqlite'` (parsed to a path). |
74
+
75
+ ### 2. Request-scoped logger
76
+
77
+ Inside any route or downstream middleware, get the logger for the current request:
78
+
79
+ ```js
80
+ const { getRequestLogger } = require('lazylog');
81
+
82
+ app.post('/login', (req, res) => {
83
+ const log = getRequestLogger();
84
+ log.info('Login attempt');
85
+ log.error('Invalid password');
86
+ // All of these are stored with the same request_id
87
+ res.json({ ok: false });
88
+ });
89
+ ```
90
+
91
+ Logger methods: **`.log()`**, **`.info()`**, **`.warn()`**, **`.error()`**. Each accepts a message string and an optional metadata object:
92
+
93
+ ```js
94
+ log.info('User action', { actionId: 42 });
95
+ ```
96
+
97
+ If called **outside** a request (e.g. in a cron job), `getRequestLogger()` returns a logger that only writes to the console (no DB).
98
+
99
+ ### 3. Add context
100
+
101
+ Attach extra fields to every subsequent log for that request:
102
+
103
+ ```js
104
+ const log = getRequestLogger();
105
+ log.addContext({ userId: 123, email: 'u@example.com' });
106
+ log.info('User action'); // stored with userId and email in context
107
+ ```
108
+
109
+ ### 4. Retrieve logs by request
110
+
111
+ Use the stored data in your app or a simple admin route:
112
+
113
+ ```js
114
+ const { getLogsByRequestId, getRequest } = require('lazylog');
115
+
116
+ // All log entries for a request
117
+ const logs = getLogsByRequestId(requestId);
118
+
119
+ // Request row plus its logs
120
+ const data = getRequest(requestId);
121
+ // data.request: { id, method, path, body, response, response_status, started_at, finished_at }
122
+ // data.logs: array of { id, request_id, level, message, context, created_at }
123
+ ```
124
+
125
+ Storage is the one from the last-configured `requestLogger()` middleware. If the middleware has never been mounted, `getLogsByRequestId` returns `[]` and `getRequest` returns `null`.
126
+
127
+ ## Storage schema (SQLite)
128
+
129
+ - **requests**: `id` (UUID), `method`, `path`, `body` (JSON/text), `response` (text), `response_status` (int), `started_at`, `finished_at`.
130
+ - **logs**: `id`, `request_id` (FK to requests), `level`, `message`, `context` (JSON), `created_at`.
131
+
132
+ Index on `logs.request_id` for fast lookups.
133
+
134
+ ## Best practices
135
+
136
+ - Use `getRequestLogger()` for request-scoped logs instead of `console.log`, so everything is tied to the request and stored.
137
+ - Use `ignorePaths` for health checks or noisy endpoints you don’t need to log.
138
+ - Use `addContext()` after auth so every log for that request includes user id or email.
139
+
140
+ ## Publishing to npm
141
+
142
+ Before publishing:
143
+
144
+ 1. **Set repo URLs** in `package.json`: replace `YOUR_USERNAME` in `repository`, `bugs`, and `homepage` with your GitHub username (or org).
145
+ 2. **Set author** in `package.json`, e.g. `"author": "Your Name <you@example.com>"` or `"author": "https://github.com/YOUR_USERNAME"`.
146
+ 3. **Check the package name**: `lazylog` may be taken on npm. Use `npm search lazylog` or pick a scoped name, e.g. `@yourusername/lazylog`.
147
+ 4. **Log in**: `npm login`
148
+ 5. **Dry run**: `npm publish --dry-run` to see what will be published (only `src/`, `README.md`, and `LICENSE` are included via the `files` field).
149
+ 6. **Publish**: `npm publish` (or `npm publish --access public` if using a scoped package like `@yourusername/lazylog`).
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "lazylog-trace",
3
+ "version": "1.0.0",
4
+ "description": "Request-scoped logging for Express with SQLite persistence. Tie logs to each request and retrieve them by request id.",
5
+ "main": "src/index.js",
6
+ "engines": {
7
+ "node": ">=14.0.0"
8
+ },
9
+ "scripts": {
10
+ "test": "node test/integration.js",
11
+ "prepublishOnly": "npm test"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "keywords": [
19
+ "express",
20
+ "logging",
21
+ "request-scoped",
22
+ "sqlite",
23
+ "middleware",
24
+ "request-id",
25
+ "persistence"
26
+ ],
27
+ "author": "Chomas_dev ",
28
+ "license": "ISC",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/ChomasDev/lazylog.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/ChomasDev/lazylog/issues"
35
+ },
36
+ "homepage": "https://github.com/ChomasDev/lazylog#readme",
37
+ "dependencies": {
38
+ "better-sqlite3": "^11.0.0"
39
+ },
40
+ "peerDependencies": {
41
+ "express": "^4.0.0 || ^5.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "express": "^4.21.0"
45
+ }
46
+ }
package/src/index.js ADDED
@@ -0,0 +1,43 @@
1
+ const middleware = require('./middleware.js');
2
+ const { noopLogger } = require('./logger.js');
3
+
4
+ /**
5
+ * Returns the request-scoped logger for the current request.
6
+ * If called outside a request (no middleware context), returns a no-op logger that only writes to console.
7
+ * @returns {{ log: function, info: function, warn: function, error: function, addContext: function }}
8
+ */
9
+ function getRequestLogger() {
10
+ const store = middleware.getStore();
11
+ if (!store || !store.logger) return noopLogger;
12
+ return store.logger;
13
+ }
14
+
15
+ /**
16
+ * Returns all log entries for the given request id.
17
+ * Uses the storage from the last-configured requestLogger middleware.
18
+ * @param {string} requestId
19
+ * @returns {Array<{ id: number, request_id: string, level: string, message: string, context: object, created_at: string }>}
20
+ */
21
+ function getLogsByRequestId(requestId) {
22
+ const storage = middleware.defaultStorage;
23
+ if (!storage) return [];
24
+ return storage.getLogsByRequestId(requestId);
25
+ }
26
+
27
+ /**
28
+ * Returns the request row and its logs, or null if not found.
29
+ * @param {string} requestId
30
+ * @returns {{ request: object, logs: array } | null}
31
+ */
32
+ function getRequest(requestId) {
33
+ const storage = middleware.defaultStorage;
34
+ if (!storage) return null;
35
+ return storage.getRequest(requestId);
36
+ }
37
+
38
+ module.exports = {
39
+ requestLogger: middleware.requestLogger,
40
+ getRequestLogger,
41
+ getLogsByRequestId,
42
+ getRequest,
43
+ };
package/src/logger.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Request-scoped logger. Writes to storage with request_id and optional context.
3
+ * addContext() merges into a shared context object; every log includes that context.
4
+ */
5
+
6
+ function createRequestLogger(requestId, storage, contextRef) {
7
+ function write(level, message, meta = {}) {
8
+ const context = { ...contextRef };
9
+ const payload = { ...context, ...meta };
10
+ storage.saveLog(requestId, level, message, payload);
11
+ const out = payload && Object.keys(payload).length ? ` ${JSON.stringify(payload)}` : '';
12
+ console[level === 'log' ? 'log' : level](`[${requestId}] ${level}: ${message}${out}`);
13
+ }
14
+
15
+ return {
16
+ addContext(obj) {
17
+ if (obj && typeof obj === 'object') {
18
+ Object.assign(contextRef, obj);
19
+ }
20
+ },
21
+
22
+ log(message, meta) {
23
+ write('log', message, meta);
24
+ },
25
+
26
+ info(message, meta) {
27
+ write('info', message, meta);
28
+ },
29
+
30
+ warn(message, meta) {
31
+ write('warn', message, meta);
32
+ },
33
+
34
+ error(message, meta) {
35
+ write('error', message, meta);
36
+ },
37
+ };
38
+ }
39
+
40
+ /**
41
+ * No-op logger for use outside a request (e.g. getRequestLogger() when not in middleware).
42
+ * Logs to console only, no DB.
43
+ */
44
+ const noopLogger = {
45
+ addContext() {},
46
+ log(msg, meta) {
47
+ console.log(msg, meta != null ? meta : '');
48
+ },
49
+ info(msg, meta) {
50
+ console.info(msg, meta != null ? meta : '');
51
+ },
52
+ warn(msg, meta) {
53
+ console.warn(msg, meta != null ? meta : '');
54
+ },
55
+ error(msg, meta) {
56
+ console.error(msg, meta != null ? meta : '');
57
+ },
58
+ };
59
+
60
+ module.exports = { createRequestLogger, noopLogger };
@@ -0,0 +1,112 @@
1
+ const { AsyncLocalStorage } = require('async_hooks');
2
+ const crypto = require('crypto');
3
+ const { createRequestLogger } = require('./logger.js');
4
+ const { createSqliteStorage } = require('./sqlite.js');
5
+
6
+ const asyncLocalStorage = new AsyncLocalStorage();
7
+
8
+ /** Default storage instance; set when requestLogger is first used. */
9
+ let defaultStorage = null;
10
+
11
+ function generateRequestId() {
12
+ if (crypto.randomUUID) {
13
+ return crypto.randomUUID();
14
+ }
15
+ return crypto.randomBytes(16).toString('hex');
16
+ }
17
+
18
+ function matchesIgnore(path, ignorePaths) {
19
+ if (!ignorePaths || !ignorePaths.length) return false;
20
+ for (const p of ignorePaths) {
21
+ if (typeof p === 'string' && path === p) return true;
22
+ if (p instanceof RegExp && p.test(path)) return true;
23
+ }
24
+ return false;
25
+ }
26
+
27
+ /**
28
+ * @param {object} options
29
+ * @param {string[]|RegExp[]} [options.ignorePaths]
30
+ * @param {boolean} [options.logBody=true]
31
+ * @param {boolean} [options.logResponse=true]
32
+ * @param {string} [options.databasePath]
33
+ * @param {string} [options.databaseUrl]
34
+ */
35
+ function requestLogger(options = {}) {
36
+ const {
37
+ ignorePaths = [],
38
+ logBody = true,
39
+ logResponse = true,
40
+ databasePath,
41
+ databaseUrl,
42
+ } = options;
43
+
44
+ const storage = createSqliteStorage({ databasePath, databaseUrl });
45
+ defaultStorage = storage;
46
+
47
+ return function lazylogMiddleware(req, res, next) {
48
+ const path = req.path || req.url?.split('?')[0] || '';
49
+ if (matchesIgnore(path, ignorePaths)) {
50
+ return next();
51
+ }
52
+
53
+ const requestId = generateRequestId();
54
+ const startedAt = new Date().toISOString();
55
+ const context = {};
56
+ const logger = createRequestLogger(requestId, storage, context);
57
+
58
+ storage.saveRequest(requestId, {
59
+ method: req.method,
60
+ path,
61
+ body: logBody ? req.body : null,
62
+ response: null,
63
+ response_status: null,
64
+ started_at: startedAt,
65
+ finished_at: null,
66
+ });
67
+
68
+ let responseBody = null;
69
+ let responseStatus = null;
70
+
71
+ if (logResponse) {
72
+ const originalSend = res.send.bind(res);
73
+ const originalJson = res.json.bind(res);
74
+
75
+ res.send = function (body) {
76
+ responseBody = typeof body === 'string' ? body : JSON.stringify(body);
77
+ return originalSend(body);
78
+ };
79
+ res.json = function (body) {
80
+ responseBody = JSON.stringify(body);
81
+ return originalJson(body);
82
+ };
83
+
84
+ res.on('finish', () => {
85
+ responseStatus = res.statusCode;
86
+ storage.updateRequest(requestId, {
87
+ response: responseBody,
88
+ response_status: responseStatus,
89
+ finished_at: new Date().toISOString(),
90
+ });
91
+ });
92
+ } else {
93
+ res.on('finish', () => {
94
+ storage.updateRequest(requestId, {
95
+ response: null,
96
+ response_status: res.statusCode,
97
+ finished_at: new Date().toISOString(),
98
+ });
99
+ });
100
+ }
101
+
102
+ res.setHeader('X-Request-Id', requestId);
103
+ const store = { requestId, logger, context };
104
+ asyncLocalStorage.run(store, () => next());
105
+ };
106
+ }
107
+
108
+ function getStore() {
109
+ return asyncLocalStorage.getStore();
110
+ }
111
+
112
+ module.exports = { requestLogger, getStore, get defaultStorage() { return defaultStorage; } };
package/src/sqlite.js ADDED
@@ -0,0 +1,139 @@
1
+ const Database = require('better-sqlite3');
2
+ const path = require('path');
3
+
4
+ const DEFAULT_DB_PATH = './logs.sqlite';
5
+
6
+ /**
7
+ * Resolve database path from options (databasePath or databaseUrl).
8
+ * databaseUrl like 'file:./logs.sqlite' is parsed to a path.
9
+ * @param {{ databasePath?: string, databaseUrl?: string }} options
10
+ * @returns {string}
11
+ */
12
+ function resolveDbPath(options = {}) {
13
+ if (options.databasePath) {
14
+ return path.resolve(process.cwd(), options.databasePath);
15
+ }
16
+ if (options.databaseUrl) {
17
+ const url = options.databaseUrl;
18
+ if (url.startsWith('file:')) {
19
+ return path.resolve(process.cwd(), url.slice(5).replace(/^\/+/, ''));
20
+ }
21
+ return path.resolve(process.cwd(), url);
22
+ }
23
+ return path.resolve(process.cwd(), DEFAULT_DB_PATH);
24
+ }
25
+
26
+ /**
27
+ * Create SQLite storage and ensure tables exist.
28
+ * @param {{ databasePath?: string, databaseUrl?: string }} options
29
+ * @returns {import('./storage.js')}
30
+ */
31
+ function createSqliteStorage(options = {}) {
32
+ const dbPath = resolveDbPath(options);
33
+ const db = new Database(dbPath);
34
+
35
+ db.exec(`
36
+ CREATE TABLE IF NOT EXISTS requests (
37
+ id TEXT PRIMARY KEY,
38
+ method TEXT NOT NULL,
39
+ path TEXT NOT NULL,
40
+ body TEXT,
41
+ response TEXT,
42
+ response_status INTEGER,
43
+ started_at TEXT NOT NULL,
44
+ finished_at TEXT
45
+ );
46
+
47
+ CREATE TABLE IF NOT EXISTS logs (
48
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
49
+ request_id TEXT NOT NULL,
50
+ level TEXT NOT NULL,
51
+ message TEXT NOT NULL,
52
+ context TEXT,
53
+ created_at TEXT NOT NULL,
54
+ FOREIGN KEY (request_id) REFERENCES requests(id)
55
+ );
56
+
57
+ CREATE INDEX IF NOT EXISTS idx_logs_request_id ON logs(request_id);
58
+ `);
59
+
60
+ return {
61
+ saveRequest(requestId, data) {
62
+ const stmt = db.prepare(`
63
+ INSERT INTO requests (id, method, path, body, response, response_status, started_at, finished_at)
64
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
65
+ `);
66
+ const bodyStr = data.body != null ? JSON.stringify(data.body) : null;
67
+ stmt.run(
68
+ requestId,
69
+ data.method,
70
+ data.path,
71
+ bodyStr,
72
+ data.response ?? null,
73
+ data.response_status ?? null,
74
+ data.started_at,
75
+ data.finished_at ?? null
76
+ );
77
+ },
78
+
79
+ saveLog(requestId, level, message, context) {
80
+ const stmt = db.prepare(`
81
+ INSERT INTO logs (request_id, level, message, context, created_at)
82
+ VALUES (?, ?, ?, ?, ?)
83
+ `);
84
+ const contextStr = context && Object.keys(context).length > 0 ? JSON.stringify(context) : null;
85
+ stmt.run(requestId, level, message, contextStr, new Date().toISOString());
86
+ },
87
+
88
+ getLogsByRequestId(requestId) {
89
+ const stmt = db.prepare(`
90
+ SELECT id, request_id, level, message, context, created_at
91
+ FROM logs WHERE request_id = ? ORDER BY created_at ASC
92
+ `);
93
+ const rows = stmt.all(requestId);
94
+ return rows.map((row) => ({
95
+ id: row.id,
96
+ request_id: row.request_id,
97
+ level: row.level,
98
+ message: row.message,
99
+ context: row.context ? JSON.parse(row.context) : {},
100
+ created_at: row.created_at,
101
+ }));
102
+ },
103
+
104
+ updateRequest(requestId, data) {
105
+ const stmt = db.prepare(`
106
+ UPDATE requests SET response = ?, response_status = ?, finished_at = ?
107
+ WHERE id = ?
108
+ `);
109
+ stmt.run(
110
+ data.response ?? null,
111
+ data.response_status ?? null,
112
+ data.finished_at ?? null,
113
+ requestId
114
+ );
115
+ },
116
+
117
+ getRequest(requestId) {
118
+ const reqStmt = db.prepare('SELECT * FROM requests WHERE id = ?');
119
+ const requestRow = reqStmt.get(requestId);
120
+ if (!requestRow) return null;
121
+
122
+ const request = {
123
+ id: requestRow.id,
124
+ method: requestRow.method,
125
+ path: requestRow.path,
126
+ body: requestRow.body ? JSON.parse(requestRow.body) : null,
127
+ response: requestRow.response,
128
+ response_status: requestRow.response_status,
129
+ started_at: requestRow.started_at,
130
+ finished_at: requestRow.finished_at,
131
+ };
132
+
133
+ const logs = this.getLogsByRequestId(requestId);
134
+ return { request, logs };
135
+ },
136
+ };
137
+ }
138
+
139
+ module.exports = { createSqliteStorage, resolveDbPath };
package/src/storage.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Abstract storage interface for request and log persistence.
3
+ * Implementations (e.g. sqlite.js) provide: saveRequest, saveLog, getLogsByRequestId, getRequest.
4
+ */
5
+
6
+ /**
7
+ * @typedef {Object} RequestRow
8
+ * @property {string} id
9
+ * @property {string} method
10
+ * @property {string} path
11
+ * @property {string|object|null} body
12
+ * @property {string|null} response
13
+ * @property {number|null} response_status
14
+ * @property {string} started_at
15
+ * @property {string|null} finished_at
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} LogEntry
20
+ * @property {number} id
21
+ * @property {string} request_id
22
+ * @property {string} level
23
+ * @property {string} message
24
+ * @property {object} context
25
+ * @property {string} created_at
26
+ */
27
+
28
+ /**
29
+ * @typedef {Object} RequestWithLogs
30
+ * @property {RequestRow} request
31
+ * @property {LogEntry[]} logs
32
+ */
33
+
34
+ /**
35
+ * Storage interface. Implementations must provide:
36
+ * - saveRequest(requestId, data)
37
+ * - saveLog(requestId, level, message, context)
38
+ * - getLogsByRequestId(requestId) -> LogEntry[]
39
+ * - getRequest(requestId) -> RequestWithLogs | null
40
+ */
41
+ module.exports = {};