blorq-logger 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/.env.example +6 -0
- package/README.md +282 -0
- package/blorq-logger-1.0.0.tgz +0 -0
- package/package.json +21 -0
- package/src/adapters/express.js +74 -0
- package/src/adapters/fastify.js +66 -0
- package/src/adapters/koa.js +55 -0
- package/src/adapters/next.js +142 -0
- package/src/core.js +190 -0
- package/src/index.d.ts +69 -0
- package/src/index.js +141 -0
- package/src/index.mjs +22 -0
- package/tests/basic.test.js +91 -0
package/.env.example
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# blorq-logger
|
|
2
|
+
|
|
3
|
+
Zero-dependency structured logger for Node.js ≥18. Ships logs to [Blorq](https://github.com/your-org/blorq) or any compatible HTTP endpoint. Works with **Express**, **Next.js**, **Fastify**, **Koa**, **NestJS**, and plain **Node.js**.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install blorq-logger
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Quick setup (any framework)
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
const logger = require('blorq-logger');
|
|
15
|
+
|
|
16
|
+
logger.configure({
|
|
17
|
+
appName: 'my-api',
|
|
18
|
+
remoteUrl: 'http://localhost:9900/api/logs', // your Blorq instance
|
|
19
|
+
apiKey: process.env.BLORQ_API_KEY,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Intercept existing console.log/warn/error — zero other changes needed
|
|
23
|
+
logger.install();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
From here, **every existing `console.log()`** in your app ships to Blorq automatically, while still printing to the terminal. No code changes needed in existing files.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Structured logging
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
const logger = require('blorq-logger');
|
|
34
|
+
|
|
35
|
+
// Root logger
|
|
36
|
+
logger.info('Server started', { port: 3000 });
|
|
37
|
+
logger.warn('Rate limit approaching', { userId: 'u123' });
|
|
38
|
+
logger.error('DB timeout', new Error('Connection refused'));
|
|
39
|
+
|
|
40
|
+
// Child loggers — carry context into every line they emit
|
|
41
|
+
const paymentLog = logger.create({ service: 'PaymentService', version: '2' });
|
|
42
|
+
paymentLog.info('Charge processed', { amount: 99, currency: 'USD' });
|
|
43
|
+
|
|
44
|
+
// Chain: inherit + add more context
|
|
45
|
+
const reqLog = paymentLog.child({ requestId: req.requestId });
|
|
46
|
+
reqLog.error('Stripe declined', new Error('card_declined'));
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Each log line is structured JSON:
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"ts": "2024-01-15T10:23:45.000Z",
|
|
53
|
+
"level": "ERROR",
|
|
54
|
+
"appName": "my-api",
|
|
55
|
+
"service": "PaymentService",
|
|
56
|
+
"requestId": "abc-123",
|
|
57
|
+
"message": "Stripe declined",
|
|
58
|
+
"data": [{ "error": "card_declined", "stack": "..." }]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Framework adapters
|
|
65
|
+
|
|
66
|
+
### Express / NestJS / Connect
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
const express = require('express');
|
|
70
|
+
const logger = require('blorq-logger');
|
|
71
|
+
|
|
72
|
+
logger.configure({ appName: 'my-api', remoteUrl: '...', apiKey: '...' });
|
|
73
|
+
|
|
74
|
+
const app = express();
|
|
75
|
+
|
|
76
|
+
// Ships every request as a structured log to {appName}-requests/
|
|
77
|
+
app.use(logger.requestLogger());
|
|
78
|
+
|
|
79
|
+
// req.blorqLogger is a child logger pre-loaded with requestId
|
|
80
|
+
app.get('/users/:id', (req, res) => {
|
|
81
|
+
req.blorqLogger.info('Fetching user', { userId: req.params.id });
|
|
82
|
+
res.json({ ok: true });
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Next.js — Pages Router (API routes)
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
// pages/api/hello.js
|
|
90
|
+
const logger = require('blorq-logger');
|
|
91
|
+
const { withLogger } = require('blorq-logger/next');
|
|
92
|
+
|
|
93
|
+
logger.configure({ appName: 'my-nextapp', remoteUrl: '...', apiKey: '...' });
|
|
94
|
+
|
|
95
|
+
export default withLogger(async (req, res) => {
|
|
96
|
+
logger.info('hello called');
|
|
97
|
+
res.json({ message: 'hello' });
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Next.js — App Router (middleware.js)
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
// middleware.js (runs in Edge Runtime)
|
|
105
|
+
import { nextMiddleware } from 'blorq-logger/next';
|
|
106
|
+
|
|
107
|
+
export default nextMiddleware({
|
|
108
|
+
appName: 'my-nextapp',
|
|
109
|
+
remoteUrl: 'https://blorq.yourdomain.com/api/logs',
|
|
110
|
+
apiKey: process.env.BLORQ_API_KEY,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export const config = { matcher: '/api/:path*' };
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Fastify
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
const fastify = require('fastify')();
|
|
120
|
+
const logger = require('blorq-logger');
|
|
121
|
+
|
|
122
|
+
logger.configure({ appName: 'my-api', remoteUrl: '...', apiKey: '...' });
|
|
123
|
+
|
|
124
|
+
// Register as a Fastify plugin
|
|
125
|
+
await fastify.register(logger.requestLogger({ framework: 'fastify' }));
|
|
126
|
+
|
|
127
|
+
fastify.get('/hello', async (request) => {
|
|
128
|
+
request.log.info('hello route hit'); // per-request structured logger
|
|
129
|
+
return { hello: 'world' };
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Koa
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
const Koa = require('koa');
|
|
137
|
+
const logger = require('blorq-logger');
|
|
138
|
+
|
|
139
|
+
logger.configure({ appName: 'my-api', remoteUrl: '...', apiKey: '...' });
|
|
140
|
+
|
|
141
|
+
const app = new Koa();
|
|
142
|
+
app.use(logger.requestLogger({ framework: 'koa' }));
|
|
143
|
+
|
|
144
|
+
app.use(async ctx => {
|
|
145
|
+
ctx.log.info('request received'); // per-request structured logger
|
|
146
|
+
ctx.body = 'hello';
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### NestJS
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// main.ts
|
|
154
|
+
import logger from 'blorq-logger';
|
|
155
|
+
import { requestMiddleware } from 'blorq-logger/express';
|
|
156
|
+
import { NestFactory } from '@nestjs/core';
|
|
157
|
+
import { AppModule } from './app.module';
|
|
158
|
+
|
|
159
|
+
async function bootstrap() {
|
|
160
|
+
logger.configure({
|
|
161
|
+
appName: 'my-nest-api',
|
|
162
|
+
remoteUrl: process.env.BLORQ_URL,
|
|
163
|
+
apiKey: process.env.BLORQ_API_KEY,
|
|
164
|
+
interceptConsole: true, // capture NestJS's own console output
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const app = await NestFactory.create(AppModule);
|
|
168
|
+
app.use(requestMiddleware()); // from 'blorq-logger/express'
|
|
169
|
+
await app.listen(3000);
|
|
170
|
+
}
|
|
171
|
+
bootstrap();
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Configuration reference
|
|
177
|
+
|
|
178
|
+
All options can be set via `configure()` or environment variables.
|
|
179
|
+
|
|
180
|
+
| Option | Env var | Default | Description |
|
|
181
|
+
|---|---|---|---|
|
|
182
|
+
| `appName` | `BLORQ_APP_NAME` | `'app'` | Name of your service |
|
|
183
|
+
| `remoteUrl` | `BLORQ_URL` | `null` | Blorq ingest URL |
|
|
184
|
+
| `apiKey` | `BLORQ_API_KEY` | `''` | `X-Api-Key` value |
|
|
185
|
+
| `level` | `BLORQ_LEVEL` | `'info'` | Minimum level: `debug\|info\|warn\|error\|fatal\|silent` |
|
|
186
|
+
| `prettyPrint` | — | `true` in dev | Pretty-print JSON to stdout |
|
|
187
|
+
| `stdout` | — | `true` | Write to `process.stdout` (besides remote) |
|
|
188
|
+
| `interceptConsole` | `BLORQ_INTERCEPT` | `false` | Patch `console.*` globally |
|
|
189
|
+
| `bufferSize` | — | `50` | Flush when buffer hits this size |
|
|
190
|
+
| `flushIntervalMs` | — | `200` | Flush timer in ms |
|
|
191
|
+
| `remoteTimeoutMs` | — | `3000` | HTTP timeout for remote sends |
|
|
192
|
+
| `remoteRetries` | — | `2` | Retry attempts on failure |
|
|
193
|
+
| `skipPaths` | `BLORQ_SKIP_PATHS` | `/health,/ping,/favicon,/_next/static` | Paths skipped by `requestLogger()` |
|
|
194
|
+
|
|
195
|
+
### .env example
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
BLORQ_APP_NAME=my-api
|
|
199
|
+
BLORQ_URL=http://blorq:9900/api/logs
|
|
200
|
+
BLORQ_API_KEY=blq_abc123...
|
|
201
|
+
BLORQ_LEVEL=info
|
|
202
|
+
BLORQ_INTERCEPT=true
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Console drop-in
|
|
208
|
+
|
|
209
|
+
If you have a codebase full of `console.log()` and don't want to change anything:
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
// At the very top of your entry file:
|
|
213
|
+
const logger = require('blorq-logger');
|
|
214
|
+
logger.configure({ appName: 'my-api', remoteUrl: '...', apiKey: '...' });
|
|
215
|
+
logger.install(); // patches console.* globally
|
|
216
|
+
|
|
217
|
+
// Everything from here ships to Blorq
|
|
218
|
+
// console.log, console.warn, console.error all still print to terminal too
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Or use it as a direct `console` replacement in a single file:
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
const console = require('blorq-logger').console;
|
|
225
|
+
// Now console.log/warn/error/debug in this file go to Blorq
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Graceful shutdown
|
|
231
|
+
|
|
232
|
+
The logger auto-flushes on `SIGINT`, `SIGTERM`, and `beforeExit`. For manual control:
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
process.on('SIGTERM', async () => {
|
|
236
|
+
await logger.flush();
|
|
237
|
+
process.exit(0);
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## How request logs appear in Blorq
|
|
244
|
+
|
|
245
|
+
Request logs land in `logs/{appName}-requests/{date}.log` and are visible in **Insights → API Analytics**. Each line contains:
|
|
246
|
+
|
|
247
|
+
```json
|
|
248
|
+
{
|
|
249
|
+
"ts": "...",
|
|
250
|
+
"level": "INFO",
|
|
251
|
+
"appName": "my-api-requests",
|
|
252
|
+
"type": "api_request",
|
|
253
|
+
"requestId": "uuid",
|
|
254
|
+
"method": "POST",
|
|
255
|
+
"path": "/api/orders",
|
|
256
|
+
"statusCode": 201,
|
|
257
|
+
"durationMs": 47.2,
|
|
258
|
+
"reqSizeBytes": 128,
|
|
259
|
+
"resSizeBytes": 256
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## TypeScript
|
|
266
|
+
|
|
267
|
+
Full types included via `src/index.d.ts`:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import logger, { Logger, BlorqConfig } from 'blorq-logger';
|
|
271
|
+
|
|
272
|
+
logger.configure({ appName: 'my-api' } satisfies BlorqConfig);
|
|
273
|
+
|
|
274
|
+
const paymentLog: Logger = logger.create({ service: 'PaymentService' });
|
|
275
|
+
paymentLog.info('Charge processed', { amount: 99 });
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## License
|
|
281
|
+
|
|
282
|
+
MIT
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blorq-logger",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero-dependency structured logger for Node.js — ships logs to Blorq or any HTTP endpoint. Works with Express, Next.js, Fastify, Koa, NestJS, and plain Node.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./express": "./src/adapters/express.js",
|
|
9
|
+
"./next": "./src/adapters/next.js",
|
|
10
|
+
"./fastify": "./src/adapters/fastify.js",
|
|
11
|
+
"./koa": "./src/adapters/koa.js"
|
|
12
|
+
},
|
|
13
|
+
"types": "src/index.d.ts",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node tests/basic.test.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["logger","logging","blorq","structured","observability","express","nextjs","fastify","koa"],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {},
|
|
20
|
+
"engines": { "node": ">=18.0.0" }
|
|
21
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express / Connect / NestJS / plain http adapter
|
|
3
|
+
* Works with any Connect-compatible framework.
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const { state, enqueue, buildEntry } = require('../core');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns Express-compatible middleware: (req, res, next) => void
|
|
12
|
+
*
|
|
13
|
+
* Options:
|
|
14
|
+
* appName – override default appName for request logs
|
|
15
|
+
* skipPaths – override default skip paths
|
|
16
|
+
* logBody – include request body (careful: reads stream, default false)
|
|
17
|
+
*/
|
|
18
|
+
function requestMiddleware(opts = {}) {
|
|
19
|
+
const appName = opts.appName || state.cfg.appName;
|
|
20
|
+
const skip = opts.skipPaths || state.cfg.skipPaths;
|
|
21
|
+
|
|
22
|
+
return function blorqRequestLogger(req, res, next) {
|
|
23
|
+
const rawPath = req.path || (req.url || '/').split('?')[0];
|
|
24
|
+
|
|
25
|
+
if (skip.some(p => rawPath.startsWith(p))) return next();
|
|
26
|
+
|
|
27
|
+
const startHr = process.hrtime.bigint();
|
|
28
|
+
const requestId = req.headers['x-request-id'] || crypto.randomUUID();
|
|
29
|
+
const reqSize = parseInt(req.headers['content-length'] || '0', 10) || 0;
|
|
30
|
+
|
|
31
|
+
// Attach to request so route handlers can reference it
|
|
32
|
+
req.requestId = requestId;
|
|
33
|
+
req.blorqLogger = require('..').create({ requestId });
|
|
34
|
+
res.setHeader('X-Request-Id', requestId);
|
|
35
|
+
|
|
36
|
+
let recorded = false;
|
|
37
|
+
function record() {
|
|
38
|
+
if (recorded) return;
|
|
39
|
+
recorded = true;
|
|
40
|
+
|
|
41
|
+
const ms = Math.round(Number(process.hrtime.bigint() - startHr) / 1e4) / 100;
|
|
42
|
+
const status = res.statusCode;
|
|
43
|
+
const routePath = req.route
|
|
44
|
+
? (req.baseUrl || '') + req.route.path
|
|
45
|
+
: rawPath;
|
|
46
|
+
const resSize = parseInt(res.getHeader('content-length') || '0', 10) || 0;
|
|
47
|
+
const level = status >= 500 ? 'error' : status >= 400 ? 'warn' : 'info';
|
|
48
|
+
|
|
49
|
+
const entry = JSON.stringify({
|
|
50
|
+
ts: new Date().toISOString(),
|
|
51
|
+
level: level.toUpperCase(),
|
|
52
|
+
appName: appName + '-requests',
|
|
53
|
+
type: 'api_request',
|
|
54
|
+
requestId,
|
|
55
|
+
method: req.method,
|
|
56
|
+
path: routePath,
|
|
57
|
+
statusCode: status,
|
|
58
|
+
durationMs: ms,
|
|
59
|
+
reqSizeBytes: reqSize,
|
|
60
|
+
resSizeBytes: resSize,
|
|
61
|
+
userAgent: req.headers['user-agent'] || '',
|
|
62
|
+
message: req.method + ' ' + routePath + ' ' + status + ' ' + ms + 'ms',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
enqueue(entry);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
res.once('finish', record);
|
|
69
|
+
res.once('close', record);
|
|
70
|
+
next();
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { requestMiddleware };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fastify adapter — registers as a Fastify plugin
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const fastify = require('fastify')();
|
|
6
|
+
* await fastify.register(require('blorq-logger/fastify'), {
|
|
7
|
+
* appName: 'my-api',
|
|
8
|
+
* remoteUrl: 'http://blorq:9900/api/logs',
|
|
9
|
+
* apiKey: 'blq_...',
|
|
10
|
+
* });
|
|
11
|
+
*/
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
const { state, enqueue } = require('../core');
|
|
16
|
+
|
|
17
|
+
function plugin(opts = {}) {
|
|
18
|
+
const appName = opts.appName || state.cfg.appName;
|
|
19
|
+
const skip = opts.skipPaths || state.cfg.skipPaths;
|
|
20
|
+
|
|
21
|
+
// fastify-plugin style (no encapsulation)
|
|
22
|
+
async function blorqPlugin(fastify, options) {
|
|
23
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
24
|
+
const rawPath = request.routerPath || request.url.split('?')[0];
|
|
25
|
+
if (skip.some(p => rawPath.startsWith(p))) return;
|
|
26
|
+
|
|
27
|
+
request.blorqStart = process.hrtime.bigint();
|
|
28
|
+
request.blorqRequestId = request.headers['x-request-id'] || crypto.randomUUID();
|
|
29
|
+
reply.header('X-Request-Id', request.blorqRequestId);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
fastify.addHook('onResponse', async (request, reply) => {
|
|
33
|
+
if (!request.blorqStart) return;
|
|
34
|
+
|
|
35
|
+
const ms = Math.round(Number(process.hrtime.bigint() - request.blorqStart) / 1e4) / 100;
|
|
36
|
+
const status = reply.statusCode;
|
|
37
|
+
const routePath = request.routerPath || request.url.split('?')[0];
|
|
38
|
+
|
|
39
|
+
enqueue(JSON.stringify({
|
|
40
|
+
ts: new Date().toISOString(),
|
|
41
|
+
level: status >= 500 ? 'ERROR' : status >= 400 ? 'WARN' : 'INFO',
|
|
42
|
+
appName: appName + '-requests',
|
|
43
|
+
type: 'api_request',
|
|
44
|
+
requestId: request.blorqRequestId,
|
|
45
|
+
method: request.method,
|
|
46
|
+
path: routePath,
|
|
47
|
+
statusCode: status,
|
|
48
|
+
durationMs: ms,
|
|
49
|
+
message: request.method + ' ' + routePath + ' ' + status + ' ' + ms + 'ms',
|
|
50
|
+
}));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Attach a structured logger to every request
|
|
54
|
+
fastify.decorateRequest('log', null);
|
|
55
|
+
fastify.addHook('onRequest', async (request) => {
|
|
56
|
+
const Logger = require('../core').Logger;
|
|
57
|
+
request.log = new Logger({ requestId: request.blorqRequestId });
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Mark as fastify-plugin so it doesn't create a new scope
|
|
62
|
+
blorqPlugin[Symbol.for('skip-override')] = true;
|
|
63
|
+
return blorqPlugin;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { plugin };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Koa adapter
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const Koa = require('koa');
|
|
6
|
+
* const { requestMiddleware } = require('blorq-logger/koa');
|
|
7
|
+
* const app = new Koa();
|
|
8
|
+
* app.use(requestMiddleware({ appName: 'my-api' }));
|
|
9
|
+
*/
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
const { state, enqueue } = require('../core');
|
|
14
|
+
|
|
15
|
+
function requestMiddleware(opts = {}) {
|
|
16
|
+
const appName = opts.appName || state.cfg.appName;
|
|
17
|
+
const skip = opts.skipPaths || state.cfg.skipPaths;
|
|
18
|
+
|
|
19
|
+
return async function blorqKoaMiddleware(ctx, next) {
|
|
20
|
+
const rawPath = ctx.path;
|
|
21
|
+
if (skip.some(p => rawPath.startsWith(p))) return next();
|
|
22
|
+
|
|
23
|
+
const startHr = process.hrtime.bigint();
|
|
24
|
+
const requestId = ctx.headers['x-request-id'] || crypto.randomUUID();
|
|
25
|
+
|
|
26
|
+
ctx.state.requestId = requestId;
|
|
27
|
+
ctx.set('X-Request-Id', requestId);
|
|
28
|
+
|
|
29
|
+
// Attach structured logger to context
|
|
30
|
+
const Logger = require('../core').Logger;
|
|
31
|
+
ctx.log = new Logger({ requestId });
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await next();
|
|
35
|
+
} finally {
|
|
36
|
+
const ms = Math.round(Number(process.hrtime.bigint() - startHr) / 1e4) / 100;
|
|
37
|
+
const status = ctx.status;
|
|
38
|
+
|
|
39
|
+
enqueue(JSON.stringify({
|
|
40
|
+
ts: new Date().toISOString(),
|
|
41
|
+
level: status >= 500 ? 'ERROR' : status >= 400 ? 'WARN' : 'INFO',
|
|
42
|
+
appName: appName + '-requests',
|
|
43
|
+
type: 'api_request',
|
|
44
|
+
requestId,
|
|
45
|
+
method: ctx.method,
|
|
46
|
+
path: rawPath,
|
|
47
|
+
statusCode: status,
|
|
48
|
+
durationMs: ms,
|
|
49
|
+
message: ctx.method + ' ' + rawPath + ' ' + status + ' ' + ms + 'ms',
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { requestMiddleware };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js adapter
|
|
3
|
+
*
|
|
4
|
+
* Works for:
|
|
5
|
+
* - App Router (Next 13+) — middleware.js
|
|
6
|
+
* - Pages Router — API routes via withLogger()
|
|
7
|
+
* - Edge Runtime — uses global fetch (already available)
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
const { state, enqueue } = require('../core');
|
|
13
|
+
|
|
14
|
+
// ── Pages Router (API routes) ─────────────────────────────────────────────
|
|
15
|
+
/**
|
|
16
|
+
* Wraps a Next.js API route handler with request logging.
|
|
17
|
+
*
|
|
18
|
+
* Usage (pages/api/hello.js):
|
|
19
|
+
* const { withLogger } = require('blorq-logger/next');
|
|
20
|
+
* export default withLogger(async (req, res) => { res.json({ ok: true }); });
|
|
21
|
+
*
|
|
22
|
+
* Or use the root convenience shorthand:
|
|
23
|
+
* const logger = require('blorq-logger');
|
|
24
|
+
* export default logger.requestLogger({ framework:'next' })(handler);
|
|
25
|
+
*/
|
|
26
|
+
function withLogger(optsOrHandler, maybeOpts = {}) {
|
|
27
|
+
// Support: withLogger(handler) or withLogger(opts)(handler)
|
|
28
|
+
if (typeof optsOrHandler === 'function') {
|
|
29
|
+
return _wrapHandler(optsOrHandler, maybeOpts);
|
|
30
|
+
}
|
|
31
|
+
const opts = optsOrHandler || {};
|
|
32
|
+
return function (handler) { return _wrapHandler(handler, opts); };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function _wrapHandler(handler, opts = {}) {
|
|
36
|
+
const appName = opts.appName || state.cfg.appName;
|
|
37
|
+
const skip = opts.skipPaths || state.cfg.skipPaths;
|
|
38
|
+
|
|
39
|
+
return async function blorqNextHandler(req, res) {
|
|
40
|
+
const rawPath = req.url ? req.url.split('?')[0] : '/';
|
|
41
|
+
if (skip.some(p => rawPath.startsWith(p))) return handler(req, res);
|
|
42
|
+
|
|
43
|
+
const startHr = process.hrtime.bigint();
|
|
44
|
+
const requestId = req.headers['x-request-id'] || crypto.randomUUID();
|
|
45
|
+
req.requestId = requestId;
|
|
46
|
+
if (res.setHeader) res.setHeader('X-Request-Id', requestId);
|
|
47
|
+
|
|
48
|
+
let status = 200;
|
|
49
|
+
const origJson = res.json?.bind(res);
|
|
50
|
+
const origSend = res.send?.bind(res);
|
|
51
|
+
const origStatus = res.status?.bind(res);
|
|
52
|
+
|
|
53
|
+
if (origStatus) res.status = (code) => { status = code; return origStatus(code); };
|
|
54
|
+
|
|
55
|
+
const origEnd = res.end?.bind(res);
|
|
56
|
+
if (origEnd) {
|
|
57
|
+
res.end = function (...args) {
|
|
58
|
+
record();
|
|
59
|
+
return origEnd(...args);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let recorded = false;
|
|
64
|
+
function record() {
|
|
65
|
+
if (recorded) return;
|
|
66
|
+
recorded = true;
|
|
67
|
+
const ms = Math.round(Number(process.hrtime.bigint() - startHr) / 1e4) / 100;
|
|
68
|
+
const sc = res.statusCode || status;
|
|
69
|
+
enqueue(JSON.stringify({
|
|
70
|
+
ts: new Date().toISOString(),
|
|
71
|
+
level: sc >= 500 ? 'ERROR' : sc >= 400 ? 'WARN' : 'INFO',
|
|
72
|
+
appName: appName + '-requests',
|
|
73
|
+
type: 'api_request',
|
|
74
|
+
requestId,
|
|
75
|
+
method: req.method,
|
|
76
|
+
path: rawPath,
|
|
77
|
+
statusCode: sc,
|
|
78
|
+
durationMs: ms,
|
|
79
|
+
message: req.method + ' ' + rawPath + ' ' + sc + ' ' + ms + 'ms',
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try { await handler(req, res); } catch (err) { status = 500; record(); throw err; }
|
|
84
|
+
record();
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── App Router (Next 13+ middleware.js) ───────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* Usage in middleware.js:
|
|
91
|
+
* import { nextMiddleware } from 'blorq-logger/next';
|
|
92
|
+
* export default nextMiddleware();
|
|
93
|
+
* export const config = { matcher: '/api/:path*' };
|
|
94
|
+
*
|
|
95
|
+
* Note: Next middleware runs in the Edge Runtime.
|
|
96
|
+
* This uses fetch (always available in Edge) and does NOT use Node.js built-ins.
|
|
97
|
+
*/
|
|
98
|
+
function nextMiddleware(opts = {}) {
|
|
99
|
+
const appName = opts.appName || state.cfg.appName || 'app';
|
|
100
|
+
const apiKey = opts.apiKey || state.cfg.apiKey || '';
|
|
101
|
+
const url = opts.remoteUrl || state.cfg.remoteUrl;
|
|
102
|
+
const skip = opts.skipPaths || state.cfg.skipPaths || [];
|
|
103
|
+
|
|
104
|
+
return async function blorqEdgeMiddleware(request, event) {
|
|
105
|
+
const { NextResponse } = await import('next/server');
|
|
106
|
+
const pathname = new URL(request.url).pathname;
|
|
107
|
+
|
|
108
|
+
if (skip.some(p => pathname.startsWith(p))) return NextResponse.next();
|
|
109
|
+
|
|
110
|
+
const start = Date.now();
|
|
111
|
+
const requestId = request.headers.get('x-request-id') || crypto.randomUUID();
|
|
112
|
+
|
|
113
|
+
const response = NextResponse.next();
|
|
114
|
+
response.headers.set('X-Request-Id', requestId);
|
|
115
|
+
|
|
116
|
+
const ms = Date.now() - start;
|
|
117
|
+
const entry = JSON.stringify({
|
|
118
|
+
ts: new Date().toISOString(),
|
|
119
|
+
level: 'INFO',
|
|
120
|
+
appName: appName + '-requests',
|
|
121
|
+
type: 'api_request',
|
|
122
|
+
requestId,
|
|
123
|
+
method: request.method,
|
|
124
|
+
path: pathname,
|
|
125
|
+
durationMs: ms,
|
|
126
|
+
message: request.method + ' ' + pathname + ' ' + ms + 'ms',
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (url) {
|
|
130
|
+
// Fire-and-forget in Edge (no await — keeps it non-blocking)
|
|
131
|
+
fetch(url, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { 'Content-Type': 'application/json', ...(apiKey ? { 'x-api-key': apiKey } : {}) },
|
|
134
|
+
body: JSON.stringify({ appName, logs: [entry] }),
|
|
135
|
+
}).catch(() => {});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return response;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { withLogger, nextMiddleware };
|
package/src/core.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* blorq-logger — core engine
|
|
3
|
+
* Zero external dependencies. Works in any Node.js >=18 environment.
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
// ── Default config ────────────────────────────────────────────────────────
|
|
11
|
+
const DEFAULT_CFG = {
|
|
12
|
+
appName: process.env.BLORQ_APP_NAME || process.env.APP_NAME || 'app',
|
|
13
|
+
remoteUrl: process.env.BLORQ_URL || process.env.LOG_REMOTE_URL || null,
|
|
14
|
+
apiKey: process.env.BLORQ_API_KEY || process.env.LOG_API_KEY || '',
|
|
15
|
+
level: process.env.BLORQ_LEVEL || process.env.LOG_LEVEL || 'info',
|
|
16
|
+
prettyPrint: true, // pretty-print JSON (with newlines + indentation)
|
|
17
|
+
stdout: false, // also write to stdout (besides sending remote)
|
|
18
|
+
bufferSize: 50, // flush when buffer reaches this
|
|
19
|
+
flushIntervalMs: 200, // flush every N ms
|
|
20
|
+
remoteTimeoutMs: 3000,
|
|
21
|
+
remoteRetries: 2,
|
|
22
|
+
retryDelayMs: 300,
|
|
23
|
+
// Console interception (set via configure({ interceptConsole:true }))
|
|
24
|
+
interceptConsole: process.env.BLORQ_INTERCEPT === 'true',
|
|
25
|
+
// Paths skipped by requestLogger()
|
|
26
|
+
skipPaths: (process.env.BLORQ_SKIP_PATHS || '/health,/ping,/favicon,/_next/static')
|
|
27
|
+
.split(',').map(s => s.trim()).filter(Boolean),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ── Level ordering ────────────────────────────────────────────────────────
|
|
31
|
+
const LEVELS = { silent:0, debug:10, info:20, warn:30, error:40, fatal:50 };
|
|
32
|
+
|
|
33
|
+
// ── Sensitive keys masked in JSON output ───────────────────────────────────
|
|
34
|
+
const MASK_KEYS = new Set(['authorization','token','password','secret','apikey','key','auth','passwd','credential','cookie','x-api-key']);
|
|
35
|
+
|
|
36
|
+
// ── Shared mutable state (module singleton) ────────────────────────────────
|
|
37
|
+
const state = {
|
|
38
|
+
cfg: { ...DEFAULT_CFG },
|
|
39
|
+
buffer: [],
|
|
40
|
+
flushTimer: null,
|
|
41
|
+
flushPromise: null,
|
|
42
|
+
consoleInstalled: false,
|
|
43
|
+
origConsole: null,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
function levelNum(l) { return LEVELS[String(l).toLowerCase()] ?? LEVELS.info; }
|
|
49
|
+
function shouldLog(l) { return levelNum(l) >= levelNum(state.cfg.level); }
|
|
50
|
+
|
|
51
|
+
function safeStringify(obj) {
|
|
52
|
+
const seen = new WeakSet();
|
|
53
|
+
return JSON.stringify(obj, (key, val) => {
|
|
54
|
+
if (typeof val === 'object' && val !== null) {
|
|
55
|
+
if (seen.has(val)) return '[Circular]';
|
|
56
|
+
seen.add(val);
|
|
57
|
+
}
|
|
58
|
+
if (key && MASK_KEYS.has(key.toLowerCase())) return '***';
|
|
59
|
+
return val;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function serializeExtras(extras) {
|
|
64
|
+
return (extras || []).map(x => {
|
|
65
|
+
if (x instanceof Error) return { error: x.message, stack: x.stack };
|
|
66
|
+
return x;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildEntry(level, appName, context, message, extras) {
|
|
71
|
+
const payload = {
|
|
72
|
+
ts: new Date().toISOString(),
|
|
73
|
+
level: level.toUpperCase(),
|
|
74
|
+
appName: appName || state.cfg.appName,
|
|
75
|
+
host: os.hostname(),
|
|
76
|
+
pid: process.pid,
|
|
77
|
+
...context,
|
|
78
|
+
message: String(message == null ? '' : message),
|
|
79
|
+
};
|
|
80
|
+
const ex = serializeExtras(extras);
|
|
81
|
+
if (ex.length) payload.data = ex;
|
|
82
|
+
return state.cfg.prettyPrint ? JSON.stringify(payload, null, 2) : safeStringify(payload);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Buffer + flush ────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
function enqueue(line) {
|
|
88
|
+
state.buffer.push(line);
|
|
89
|
+
if (state.buffer.length >= state.cfg.bufferSize) { flush(); return; }
|
|
90
|
+
if (!state.flushTimer) {
|
|
91
|
+
state.flushTimer = setTimeout(() => { state.flushTimer = null; flush(); }, state.cfg.flushIntervalMs);
|
|
92
|
+
if (state.flushTimer.unref) state.flushTimer.unref();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function flush() {
|
|
97
|
+
if (state.flushPromise) {
|
|
98
|
+
state.flushPromise = state.flushPromise.then(() => drain());
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
state.flushPromise = drain().finally(() => { state.flushPromise = null; });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function drain() {
|
|
105
|
+
return new Promise(resolve => setImmediate(async () => {
|
|
106
|
+
const logs = state.buffer.splice(0);
|
|
107
|
+
if (!logs.length) { resolve(); return; }
|
|
108
|
+
|
|
109
|
+
// Local stdout
|
|
110
|
+
if (state.cfg.stdout) {
|
|
111
|
+
for (const l of logs) try { process.stdout.write(l + '\n'); } catch {}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Remote ship
|
|
115
|
+
if (state.cfg.remoteUrl) await sendBatch(logs).catch(() => {});
|
|
116
|
+
resolve();
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function sendBatch(logs) {
|
|
121
|
+
const { remoteUrl, apiKey, appName, remoteTimeoutMs, remoteRetries, retryDelayMs } = state.cfg;
|
|
122
|
+
let attempt = 0;
|
|
123
|
+
while (attempt < remoteRetries + 1) {
|
|
124
|
+
try {
|
|
125
|
+
const res = await fetch(remoteUrl, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: {
|
|
128
|
+
'Content-Type': 'application/json',
|
|
129
|
+
...(apiKey ? { 'x-api-key': apiKey } : {}),
|
|
130
|
+
},
|
|
131
|
+
body: JSON.stringify({ appName, logs }),
|
|
132
|
+
signal: AbortSignal.timeout(remoteTimeoutMs),
|
|
133
|
+
});
|
|
134
|
+
if (res.ok) return;
|
|
135
|
+
throw new Error('HTTP ' + res.status);
|
|
136
|
+
} catch {
|
|
137
|
+
attempt++;
|
|
138
|
+
if (attempt > remoteRetries) return;
|
|
139
|
+
await new Promise(r => setTimeout(r, retryDelayMs * Math.pow(2, attempt - 1)));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Logger class ──────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
class Logger {
|
|
147
|
+
/**
|
|
148
|
+
* @param {object} context – Extra fields merged into every log entry
|
|
149
|
+
*/
|
|
150
|
+
constructor(context = {}) {
|
|
151
|
+
this._ctx = context;
|
|
152
|
+
this._appName = context.appName || null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Create a child logger that inherits context + merges extra fields */
|
|
156
|
+
child(extra = {}) {
|
|
157
|
+
return new Logger({ ...this._ctx, ...extra });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Attach extra context to this logger (mutates) */
|
|
161
|
+
with(extra = {}) {
|
|
162
|
+
Object.assign(this._ctx, extra);
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_log(level, message, params) {
|
|
167
|
+
if (!shouldLog(level)) return;
|
|
168
|
+
enqueue(buildEntry(level, this._appName, this._ctx, message, params));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
debug(msg, ...p) { this._log('debug', msg, p.length ? p : undefined); }
|
|
172
|
+
info (msg, ...p) { this._log('info', msg, p.length ? p : undefined); }
|
|
173
|
+
warn (msg, ...p) { this._log('warn', msg, p.length ? p : undefined); }
|
|
174
|
+
error(msg, ...p) { this._log('error', msg, p.length ? p : undefined); }
|
|
175
|
+
|
|
176
|
+
/** Fatal: bypasses buffer, ships immediately (for crash handlers) */
|
|
177
|
+
fatal(msg, ...p) {
|
|
178
|
+
if (!shouldLog('fatal')) return;
|
|
179
|
+
const line = buildEntry('fatal', this._appName, this._ctx, msg, p.length ? p : undefined);
|
|
180
|
+
try { process.stderr.write(line + '\n'); } catch {}
|
|
181
|
+
if (state.cfg.remoteUrl) sendBatch([line]).catch(() => {});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Flush any buffered logs right now (useful before process.exit) */
|
|
185
|
+
flush() { return drain(); }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Exports ───────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
module.exports = { Logger, state, LEVELS, enqueue, buildEntry, flush, drain, shouldLog, safeStringify };
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* blorq-logger — TypeScript definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface BlorqConfig {
|
|
6
|
+
/** Application name (default: APP_NAME env or 'app') */
|
|
7
|
+
appName?: string;
|
|
8
|
+
/** Blorq ingest URL e.g. http://localhost:9900/api/logs */
|
|
9
|
+
remoteUrl?: string | null;
|
|
10
|
+
/** API key for Blorq (X-Api-Key header) */
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
/** Minimum log level: 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent' */
|
|
13
|
+
level?: "debug" | "info" | "warn" | "error" | "fatal" | "silent";
|
|
14
|
+
/** Pretty-print JSON to stdout (default: true in development) */
|
|
15
|
+
prettyPrint?: boolean;
|
|
16
|
+
/** Also write to process.stdout (default: true) */
|
|
17
|
+
stdout?: boolean;
|
|
18
|
+
/** Intercept console.log/warn/error/debug and ship them too */
|
|
19
|
+
interceptConsole?: boolean;
|
|
20
|
+
/** Flush when buffer reaches this size (default: 50) */
|
|
21
|
+
bufferSize?: number;
|
|
22
|
+
/** Flush interval in ms (default: 200) */
|
|
23
|
+
flushIntervalMs?: number;
|
|
24
|
+
/** HTTP timeout for remote sends in ms (default: 3000) */
|
|
25
|
+
remoteTimeoutMs?: number;
|
|
26
|
+
/** Retry attempts for failed sends (default: 2) */
|
|
27
|
+
remoteRetries?: number;
|
|
28
|
+
/** Paths skipped by requestLogger (default: /health, /ping, /favicon) */
|
|
29
|
+
skipPaths?: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RequestLoggerOptions {
|
|
33
|
+
framework?: "express" | "next" | "fastify" | "koa";
|
|
34
|
+
appName?: string;
|
|
35
|
+
skipPaths?: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export declare class Logger {
|
|
39
|
+
constructor(context?: Record<string, unknown>);
|
|
40
|
+
child(extra?: Record<string, unknown>): Logger;
|
|
41
|
+
with(extra?: Record<string, unknown>): this;
|
|
42
|
+
debug(message: string, ...args: unknown[]): void;
|
|
43
|
+
info(message: string, ...args: unknown[]): void;
|
|
44
|
+
warn(message: string, ...args: unknown[]): void;
|
|
45
|
+
error(message: string, ...args: unknown[]): void;
|
|
46
|
+
fatal(message: string, ...args: unknown[]): void;
|
|
47
|
+
flush(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
declare const logger: Logger & {
|
|
51
|
+
configure(opts: BlorqConfig): void;
|
|
52
|
+
create(context?: Record<string, unknown>): Logger;
|
|
53
|
+
install(): void;
|
|
54
|
+
uninstall(): void;
|
|
55
|
+
requestLogger(
|
|
56
|
+
opts?: RequestLoggerOptions,
|
|
57
|
+
): (req: unknown, res: unknown, next?: unknown) => void;
|
|
58
|
+
requestId(): string;
|
|
59
|
+
console: {
|
|
60
|
+
log: (...args: unknown[]) => void;
|
|
61
|
+
info: (...args: unknown[]) => void;
|
|
62
|
+
warn: (...args: unknown[]) => void;
|
|
63
|
+
error: (...args: unknown[]) => void;
|
|
64
|
+
debug: (...args: unknown[]) => void;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export default logger;
|
|
69
|
+
// module.exports = logger;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* blorq-logger — main entry point
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic. For framework-specific helpers use the adapters:
|
|
5
|
+
* require('blorq-logger/express')
|
|
6
|
+
* require('blorq-logger/next')
|
|
7
|
+
* require('blorq-logger/fastify')
|
|
8
|
+
* require('blorq-logger/koa')
|
|
9
|
+
*/
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const { Logger, state, flush, drain, shouldLog, safeStringify } = require('./core');
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
|
|
15
|
+
// ── Root singleton logger ─────────────────────────────────────────────────
|
|
16
|
+
const root = new Logger();
|
|
17
|
+
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
19
|
+
// configure({ appName, remoteUrl, apiKey, level, ... })
|
|
20
|
+
// Call once at startup before any logging.
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
22
|
+
root.configure = function configure(opts = {}) {
|
|
23
|
+
Object.assign(state.cfg, opts);
|
|
24
|
+
if (opts.interceptConsole) _installConsole();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
28
|
+
// create(context) — factory for child loggers
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
30
|
+
root.create = function create(context = {}) {
|
|
31
|
+
return new Logger(context);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
35
|
+
// install() — Patch global console.* to also ship to Blorq
|
|
36
|
+
// All existing console.log/warn/error calls continue printing to terminal.
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
38
|
+
root.install = function install() { _installConsole(); };
|
|
39
|
+
root.uninstall = function uninstall() { _uninstallConsole(); };
|
|
40
|
+
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
42
|
+
// console — a console-compatible object you can use as a drop-in replacement
|
|
43
|
+
// const console = require('blorq-logger').console;
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
45
|
+
root.console = {
|
|
46
|
+
log: (...a) => root.info(...a),
|
|
47
|
+
info: (...a) => root.info(...a),
|
|
48
|
+
warn: (...a) => root.warn(...a),
|
|
49
|
+
error: (...a) => root.error(...a),
|
|
50
|
+
debug: (...a) => root.debug(...a),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
54
|
+
// requestLogger() — generic request logger middleware factory
|
|
55
|
+
// Returns the right middleware for the detected / specified framework.
|
|
56
|
+
//
|
|
57
|
+
// app.use(logger.requestLogger()) // auto-detect
|
|
58
|
+
// app.use(logger.requestLogger({ framework:'express' }))
|
|
59
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
60
|
+
root.requestLogger = function requestLogger(opts = {}) {
|
|
61
|
+
const framework = opts.framework || _detectFramework();
|
|
62
|
+
|
|
63
|
+
switch (framework) {
|
|
64
|
+
case 'koa':
|
|
65
|
+
return require('./adapters/koa').requestMiddleware(opts);
|
|
66
|
+
case 'fastify':
|
|
67
|
+
// Fastify uses plugins — this returns a plugin function
|
|
68
|
+
return require('./adapters/fastify').plugin(opts);
|
|
69
|
+
case 'next':
|
|
70
|
+
// For Next.js API routes, returns a wrapper function, not middleware
|
|
71
|
+
return require('./adapters/next').withLogger(opts);
|
|
72
|
+
default:
|
|
73
|
+
// Express / NestJS / Connect / plain http — returns (req, res, next)
|
|
74
|
+
return require('./adapters/express').requestMiddleware(opts);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Convenience: generate a request ID
|
|
79
|
+
root.requestId = function () { return crypto.randomUUID(); };
|
|
80
|
+
|
|
81
|
+
// ── Auto-flush & shutdown ─────────────────────────────────────────────────
|
|
82
|
+
if (process.env.BLORQ_NO_AUTOSHUTDOWN !== 'true') {
|
|
83
|
+
process.on('beforeExit', () => drain());
|
|
84
|
+
process.on('exit', () => { /* sync flush not possible */ });
|
|
85
|
+
process.on('SIGINT', async () => { await drain(); process.exit(0); });
|
|
86
|
+
process.on('SIGTERM', async () => { await drain(); process.exit(0); });
|
|
87
|
+
process.on('uncaughtException', async (err) => {
|
|
88
|
+
root.fatal('Uncaught exception', err);
|
|
89
|
+
await drain();
|
|
90
|
+
process.exit(1);
|
|
91
|
+
});
|
|
92
|
+
process.on('unhandledRejection', async (r) => {
|
|
93
|
+
root.error('Unhandled rejection', r);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Auto-install if env var set
|
|
98
|
+
if (state.cfg.interceptConsole) _installConsole();
|
|
99
|
+
|
|
100
|
+
// ── Console interception (private) ────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
function _installConsole() {
|
|
103
|
+
if (state.consoleInstalled) return;
|
|
104
|
+
state.consoleInstalled = true;
|
|
105
|
+
state.origConsole = {
|
|
106
|
+
log: console.log.bind(console),
|
|
107
|
+
info: console.info.bind(console),
|
|
108
|
+
warn: console.warn.bind(console),
|
|
109
|
+
error: console.error.bind(console),
|
|
110
|
+
debug: console.debug.bind(console),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const make = (level, orig) => function (...args) {
|
|
114
|
+
orig(...args); // always preserve terminal output
|
|
115
|
+
if (!shouldLog(level)) return;
|
|
116
|
+
const msg = args.map(a => typeof a === 'string' ? a : safeStringify(a)).join(' ');
|
|
117
|
+
const { enqueue: eq, buildEntry: be } = require('./core');
|
|
118
|
+
eq(be(level, state.cfg.appName, {}, msg, []));
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
console.log = make('info', state.origConsole.log);
|
|
122
|
+
console.info = make('info', state.origConsole.info);
|
|
123
|
+
console.warn = make('warn', state.origConsole.warn);
|
|
124
|
+
console.error = make('error', state.origConsole.error);
|
|
125
|
+
console.debug = make('debug', state.origConsole.debug);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function _uninstallConsole() {
|
|
129
|
+
if (!state.consoleInstalled || !state.origConsole) return;
|
|
130
|
+
Object.assign(console, state.origConsole);
|
|
131
|
+
state.consoleInstalled = false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function _detectFramework() {
|
|
135
|
+
// Check if we're in a Next.js environment
|
|
136
|
+
if (process.env.NEXT_RUNTIME || process.env.__NEXT_PRIVATE_RENDER_WORKER) return 'next';
|
|
137
|
+
// Otherwise assume Express/Connect
|
|
138
|
+
return 'express';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = root;
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// ESM re-export wrapper — allows `import logger from 'blorq-logger'`
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const logger = require('./index.js');
|
|
5
|
+
|
|
6
|
+
export default logger;
|
|
7
|
+
export const configure = logger.configure.bind(logger);
|
|
8
|
+
export const create = logger.create.bind(logger);
|
|
9
|
+
export const install = logger.install.bind(logger);
|
|
10
|
+
export const uninstall = logger.uninstall.bind(logger);
|
|
11
|
+
export const express = (...a) => logger.express(...a);
|
|
12
|
+
export const nextjs = (...a) => logger.nextjs(...a);
|
|
13
|
+
export const fastify = logger.fastify;
|
|
14
|
+
export const node = (...a) => logger.node(...a);
|
|
15
|
+
export const nestjs = (...a) => logger.nestjs(...a);
|
|
16
|
+
|
|
17
|
+
export const debug = (...a) => logger.debug(...a);
|
|
18
|
+
export const info = (...a) => logger.info(...a);
|
|
19
|
+
export const warn = (...a) => logger.warn(...a);
|
|
20
|
+
export const error = (...a) => logger.error(...a);
|
|
21
|
+
export const fatal = (...a) => logger.fatal(...a);
|
|
22
|
+
export const flush = () => logger.flush();
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
let p = 0,
|
|
3
|
+
f = 0;
|
|
4
|
+
function ok(d, fn) {
|
|
5
|
+
try {
|
|
6
|
+
fn();
|
|
7
|
+
console.log(" \u2705 " + d);
|
|
8
|
+
p++;
|
|
9
|
+
} catch (e) {
|
|
10
|
+
console.error(" \u274c " + d + "\n " + e.message);
|
|
11
|
+
f++;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
console.log("\nblorq-logger tests\n");
|
|
15
|
+
const {
|
|
16
|
+
Logger,
|
|
17
|
+
state,
|
|
18
|
+
buildEntry,
|
|
19
|
+
shouldLog,
|
|
20
|
+
safeStringify,
|
|
21
|
+
} = require("../src/core");
|
|
22
|
+
ok("buildEntry returns valid JSON", () => {
|
|
23
|
+
const obj = JSON.parse(
|
|
24
|
+
buildEntry("info", "test-app", { req: "abc" }, "hello", []),
|
|
25
|
+
);
|
|
26
|
+
if (obj.level !== "INFO") throw new Error("level=" + obj.level);
|
|
27
|
+
if (obj.message !== "hello") throw new Error("msg");
|
|
28
|
+
});
|
|
29
|
+
ok("shouldLog filters by level", () => {
|
|
30
|
+
state.cfg.level = "warn";
|
|
31
|
+
if (shouldLog("info")) throw new Error("info should be filtered");
|
|
32
|
+
if (!shouldLog("error")) throw new Error("error should pass");
|
|
33
|
+
state.cfg.level = "info";
|
|
34
|
+
});
|
|
35
|
+
ok("child logger inherits context", () => {
|
|
36
|
+
const c = new Logger({ svc: "pay" }).child({ uid: "123" });
|
|
37
|
+
if (c._ctx.svc !== "pay") throw new Error("parent ctx lost");
|
|
38
|
+
if (c._ctx.uid !== "123") throw new Error("child ctx not set");
|
|
39
|
+
});
|
|
40
|
+
ok("sensitive keys masked", () => {
|
|
41
|
+
const r = JSON.parse(
|
|
42
|
+
safeStringify({ user: "alice", password: "s3cr3t", token: "tok" }),
|
|
43
|
+
);
|
|
44
|
+
if (r.password !== "***") throw new Error("password not masked");
|
|
45
|
+
if (r.token !== "***") throw new Error("token not masked");
|
|
46
|
+
if (r.user !== "alice") throw new Error("user should not be masked");
|
|
47
|
+
});
|
|
48
|
+
const blorq = require("../src/index");
|
|
49
|
+
ok("root logger has full API", () => {
|
|
50
|
+
[
|
|
51
|
+
"configure",
|
|
52
|
+
"create",
|
|
53
|
+
"install",
|
|
54
|
+
"uninstall",
|
|
55
|
+
"requestLogger",
|
|
56
|
+
"debug",
|
|
57
|
+
"info",
|
|
58
|
+
"warn",
|
|
59
|
+
"error",
|
|
60
|
+
"fatal",
|
|
61
|
+
"flush",
|
|
62
|
+
].forEach((m) => {
|
|
63
|
+
if (typeof blorq[m] !== "function") throw new Error("missing: " + m);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
ok("express middleware is (req,res,next)", () => {
|
|
67
|
+
const mw = require("../src/adapters/express").requestMiddleware();
|
|
68
|
+
if (typeof mw !== "function" || mw.length !== 3)
|
|
69
|
+
throw new Error("wrong signature");
|
|
70
|
+
});
|
|
71
|
+
ok("express middleware skips /health", () => {
|
|
72
|
+
const mw = require("../src/adapters/express").requestMiddleware({
|
|
73
|
+
skipPaths: ["/health"],
|
|
74
|
+
});
|
|
75
|
+
let called = false;
|
|
76
|
+
mw(
|
|
77
|
+
{ path: "/health", url: "/health", headers: {}, method: "GET" },
|
|
78
|
+
{
|
|
79
|
+
statusCode: 200,
|
|
80
|
+
getHeader: () => "0",
|
|
81
|
+
setHeader: () => {},
|
|
82
|
+
once: () => {},
|
|
83
|
+
},
|
|
84
|
+
() => {
|
|
85
|
+
called = true;
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
if (!called) throw new Error("should have called next");
|
|
89
|
+
});
|
|
90
|
+
console.log("\n " + (p + f) + " tests: " + p + " passed, " + f + " failed\n");
|
|
91
|
+
if (f > 0) process.exit(1);
|