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 +15 -0
- package/README.md +149 -0
- package/package.json +46 -0
- package/src/index.js +43 -0
- package/src/logger.js +60 -0
- package/src/middleware.js +112 -0
- package/src/sqlite.js +139 -0
- package/src/storage.js +41 -0
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 = {};
|