reqsight 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/README.md ADDED
@@ -0,0 +1,558 @@
1
+ # ReqSight
2
+
3
+ Production-grade observability middleware for Express. Structured request/response logging, correlation ID propagation, outbound HTTP tracing via Axios interceptors, and a consistent error-handling pipeline — wired up in minutes, configurable without touching package internals.
4
+
5
+ Built on [Pino](https://getpino.io) for high-throughput, low-overhead JSON logging.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ - [Features](#features)
12
+ - [Installation](#installation)
13
+ - [Quick Start](#quick-start)
14
+ - [Configuration](#configuration)
15
+ - [logLevel](#loglevel)
16
+ - [prettyLogs](#prettylogs)
17
+ - [stackTrace](#stacktrace)
18
+ - [sanitize.request](#sanitizerequest)
19
+ - [sanitize.response](#sanitizeresponse)
20
+ - [sanitize.axiosErrors](#sanitizeaxioserrors)
21
+ - [API Reference](#api-reference)
22
+ - [requestLogger](#requestlogger)
23
+ - [errorLogger](#errorlogger)
24
+ - [notFoundLogger](#notfoundlogger)
25
+ - [throwError](#throwerror)
26
+ - [axiosInterceptors](#axiosinterceptors)
27
+ - [asyncHandler](#asynchandler)
28
+ - [Correlation IDs](#correlation-ids)
29
+ - [Request-Scoped Logging](#request-scoped-logging)
30
+ - [Axios Outbound Tracing](#axios-outbound-tracing)
31
+ - [Error Handling](#error-handling)
32
+ - [TypeScript](#typescript)
33
+ - [Log Output Examples](#log-output-examples)
34
+ - [Requirements](#requirements)
35
+ - [License](#license)
36
+
37
+ ---
38
+
39
+ ## Features
40
+
41
+ - **Structured logging** — every request and response logged as JSON, ready for Datadog, CloudWatch, Loki, or any log aggregator
42
+ - **Correlation ID propagation** — reads `x-correlation-id` from inbound headers or generates a UUID v4; attaches it to every log line and echoes it back in the response header
43
+ - **Request-scoped child logger** — `req.logger` is a Pino child bound to the correlation ID, method, and path, so your route handlers log in context without any extra setup
44
+ - **Outbound HTTP tracing** — Axios interceptors log every outbound request with correlation ID forwarding, and capture errors with configurable sanitization
45
+ - **Consistent error pipeline** — distinguishes operational errors (safe to expose) from unexpected crashes, with structured JSON responses and optional stack traces
46
+ - **Zero-config startup** — `postinstall` copies a fully annotated `reqsight.config.js` into your project root; edit it, or leave the defaults
47
+ - **Config evaluated once** — serializer functions are resolved at startup via a factory pattern, never per-request; no runtime branching on hot paths
48
+ - **Pretty logs in dev, raw JSON in prod** — controlled by a single `prettyLogs` flag; `pino-pretty` is optional so it never bloats production images
49
+ - **Full TypeScript support** — ships `.d.ts` declarations with Express module augmentation for `req.correlationId`, `req.startTime`, and `req.logger`
50
+
51
+ ---
52
+
53
+ ## Installation
54
+
55
+ ```bash
56
+ npm install reqsight
57
+ ```
58
+
59
+ A `reqsight.config.js` file will be created in your project root automatically. If one already exists it is left untouched.
60
+
61
+ If you want pretty-printed logs in development:
62
+
63
+ ```bash
64
+ npm install --save-dev pino-pretty
65
+ ```
66
+
67
+ If you want outbound Axios request tracing:
68
+
69
+ ```bash
70
+ npm install axios
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Quick Start
76
+
77
+ ```js
78
+ const express = require("express");
79
+ const {
80
+ requestLogger,
81
+ errorLogger,
82
+ notFoundLogger,
83
+ axiosInterceptors,
84
+ } = require("reqsight");
85
+
86
+ const app = express();
87
+
88
+ // Register Axios interceptors once at startup (optional)
89
+ axiosInterceptors();
90
+
91
+ app.use(express.json());
92
+
93
+ // Must be first — attaches req.correlationId, req.logger, req.startTime
94
+ app.use(requestLogger);
95
+
96
+ // Your routes
97
+ app.get("/", (req, res) => {
98
+ res.json({ message: "ok" });
99
+ });
100
+
101
+ // After all routes
102
+ app.use(notFoundLogger);
103
+ app.use(errorLogger);
104
+
105
+ app.listen(3000);
106
+ ```
107
+
108
+ That's it. Every request is now logged with a correlation ID, timing, and a structured response payload.
109
+
110
+ ---
111
+
112
+ ## Configuration
113
+
114
+ After `npm install`, open `reqsight.config.js` in your project root. It is a plain CommonJS module — edit it directly. ReqSight reads it once at startup.
115
+
116
+ ```
117
+ your-project/
118
+ ├── reqsight.config.js ← generated on install, yours to edit
119
+ ├── src/
120
+ └── ...
121
+ ```
122
+
123
+ > The file is a verbatim copy of ReqSight's internal defaults. Every option is annotated with its possible values and examples.
124
+
125
+ ---
126
+
127
+ ### `logLevel`
128
+
129
+ Controls the minimum level emitted by ReqSight's logger.
130
+
131
+ ```js
132
+ logLevel: process.env.LOG_LEVEL || "info"
133
+ ```
134
+
135
+ | Value | Emits |
136
+ |-------|-------|
137
+ | `"fatal"` | fatal only |
138
+ | `"error"` | error, fatal |
139
+ | `"warn"` | warn and above |
140
+ | `"info"` | info and above (default) |
141
+ | `"debug"` | debug and above |
142
+ | `"trace"` | everything |
143
+
144
+ ---
145
+
146
+ ### `prettyLogs`
147
+
148
+ Enables colorized, human-readable output via `pino-pretty`. Requires `pino-pretty` to be installed.
149
+
150
+ ```js
151
+ prettyLogs: process.env.NODE_ENV !== "production" || false
152
+ ```
153
+
154
+ Disable in production — raw JSON is faster and integrates directly with log aggregators.
155
+
156
+ ---
157
+
158
+ ### `stackTrace`
159
+
160
+ Whether to include `stack` in error responses sent to the client.
161
+
162
+ ```js
163
+ stackTrace: process.env.NODE_ENV !== "production" || false
164
+ ```
165
+
166
+ Never expose stack traces in production — they reveal internal paths and module structure.
167
+
168
+ ---
169
+
170
+ ### `sanitize.request`
171
+
172
+ Controls what gets logged from incoming Express `req` objects on every request.
173
+
174
+ **Three modes:**
175
+
176
+ ```js
177
+ // Mode 1 — function (default)
178
+ // Called per-request. Return exactly what you want logged.
179
+ request: (req) => ({
180
+ ip: req?.ip,
181
+ method: req?.method,
182
+ originalUrl: req?.originalUrl,
183
+ params: req?.params,
184
+ query: req?.query,
185
+ body: req?.body,
186
+ "user-agent": req?.headers?.["user-agent"],
187
+ })
188
+
189
+ // Mode 2 — plain object
190
+ // Snapshotted once at startup. No dynamic values from req.
191
+ // Use for fixed metadata like service name.
192
+ request: { service: "payments-api", env: process.env.NODE_ENV }
193
+
194
+ // Mode 3 — anything else (e.g. null, true)
195
+ // Spreads the full raw req object. May expose sensitive data.
196
+ // Only appropriate in controlled environments.
197
+ request: null
198
+ ```
199
+
200
+ ---
201
+
202
+ ### `sanitize.response`
203
+
204
+ Controls what gets logged from outgoing responses. Compatible with both Express `res` (uses `getHeader()`) and Axios response shapes (uses `headers` object).
205
+
206
+ `duration` and `responseSize` are always calculated directly in the middleware and merged into the log — they are not part of this serializer.
207
+
208
+ ```js
209
+ // Mode 1 — function (default)
210
+ response: (res) => ({
211
+ statusCode: res?.statusCode || res?.status,
212
+ "content-type": res?.getHeader?.("content-type") || res?.headers?.["content-type"],
213
+ "x-correlation-id": res?.getHeader?.("x-correlation-id") || res?.headers?.["x-correlation-id"],
214
+ })
215
+
216
+ // Mode 2 — plain object (static snapshot)
217
+ response: { service: "payments-api" }
218
+
219
+ // Mode 3 — anything else
220
+ // Spreads the raw response object.
221
+ response: null
222
+ ```
223
+
224
+ ---
225
+
226
+ ### `sanitize.axiosErrors`
227
+
228
+ Controls what gets logged when an outbound Axios request fails.
229
+
230
+ Sensitive headers (`Authorization`, `Cookie`, `x-api-key`) are excluded from the default. The `data` payload from the downstream error response is included — useful for surfacing upstream validation messages.
231
+
232
+ ```js
233
+ // Mode 1 — function (default)
234
+ axiosErrors: (err) => ({
235
+ code: err?.code,
236
+ message: err?.message,
237
+ statusCode: err?.status,
238
+ isRequestMade: !!err?.request,
239
+ config: {
240
+ url: err?.config?.url,
241
+ method: err?.config?.method,
242
+ },
243
+ response: {
244
+ status: err?.response?.status,
245
+ data: err?.response?.data,
246
+ },
247
+ stack: err?.stack,
248
+ })
249
+
250
+ // Mode 2 — plain object (static snapshot)
251
+ axiosErrors: { service: "payments-api" }
252
+
253
+ // Mode 3 — anything else
254
+ // Spreads the full Axios error including raw config with all headers.
255
+ // Only use if you fully trust and control your log destination.
256
+ axiosErrors: null
257
+ ```
258
+
259
+ ---
260
+
261
+ ## API Reference
262
+
263
+ ### `requestLogger`
264
+
265
+ Express middleware. Must be registered **before** your routes.
266
+
267
+ - Reads `x-correlation-id` from the inbound request header, or generates a UUID v4
268
+ - Sets `x-correlation-id` on the response header
269
+ - Attaches `req.correlationId`, `req.startTime`, and `req.logger` to the request
270
+ - Logs `REQUEST RECEIVED` on every inbound request
271
+ - Patches `res.send` and `res.end` to log `REQUEST COMPLETED` with duration and response size when the response is sent
272
+
273
+ ```js
274
+ app.use(requestLogger);
275
+ ```
276
+
277
+ ---
278
+
279
+ ### `errorLogger`
280
+
281
+ Express error-handling middleware (4-argument signature). Must be registered **after** all routes.
282
+
283
+ Handles three error categories:
284
+
285
+ | Error type | Behaviour |
286
+ |---|---|
287
+ | Axios error with response | Forwards downstream status and data to the client |
288
+ | Operational error (`throwError`) | Sends structured JSON with the message and any extra properties |
289
+ | Unexpected crash | Sends a generic 500 with the correlation ID as a reference |
290
+
291
+ ```js
292
+ app.use(errorLogger);
293
+ ```
294
+
295
+ Stack traces are included in responses only when `config.stackTrace` is `true`.
296
+
297
+ ---
298
+
299
+ ### `notFoundLogger`
300
+
301
+ Catch-all middleware for unmatched routes. Register it after all routes and before `errorLogger`.
302
+
303
+ Responds with:
304
+
305
+ ```json
306
+ {
307
+ "success": false,
308
+ "message": "Path not found",
309
+ "code": "PATH_NOT_FOUND",
310
+ "method": "GET",
311
+ "path": "/unknown",
312
+ "statusCode": 404
313
+ }
314
+ ```
315
+
316
+ ```js
317
+ app.use(notFoundLogger);
318
+ app.use(errorLogger);
319
+ ```
320
+
321
+ ---
322
+
323
+ ### `throwError`
324
+
325
+ Throws an operational error that `errorLogger` will handle gracefully — the message is safe to expose to the client.
326
+
327
+ ```js
328
+ const { throwError } = require("reqsight");
329
+
330
+ // Basic
331
+ throwError("User not found", 404);
332
+
333
+ // With extra fields merged into the response
334
+ throwError("Validation failed", 422, {
335
+ field: "email",
336
+ code: "INVALID_FORMAT",
337
+ });
338
+ ```
339
+
340
+ ```json
341
+ {
342
+ "success": false,
343
+ "message": "Validation failed",
344
+ "field": "email",
345
+ "code": "INVALID_FORMAT"
346
+ }
347
+ ```
348
+
349
+ Non-operational errors (unhandled throws, programmer errors) receive a generic 500 response — internal details are never leaked.
350
+
351
+ ---
352
+
353
+ ### `axiosInterceptors`
354
+
355
+ Registers request and response interceptors on the global Axios instance. Call it **once** at startup. Safe to call multiple times — guards against double-registration internally.
356
+
357
+ ```js
358
+ const { axiosInterceptors } = require("reqsight");
359
+ axiosInterceptors();
360
+ ```
361
+
362
+ What it does:
363
+
364
+ - **Request interceptor** — logs every outbound request; warns if `x-correlation-id` is not set on the request config
365
+ - **Response interceptor (error)** — logs Axios errors using your `sanitize.axiosErrors` config
366
+
367
+ Requires Axios to be installed in your project. If Axios is not found, a warning is printed and the call is a no-op.
368
+
369
+ **Forwarding correlation IDs to downstream services:**
370
+
371
+ ```js
372
+ const axios = require("axios");
373
+
374
+ const callDownstream = async (req) => {
375
+ return axios.get("https://api.example.com/data", {
376
+ headers: {
377
+ "x-correlation-id": req.correlationId,
378
+ },
379
+ });
380
+ };
381
+ ```
382
+
383
+ ---
384
+
385
+ ### `asyncHandler`
386
+
387
+ Wraps an async route handler and forwards any rejected promise to Express's `next(err)`, eliminating try/catch boilerplate in every route.
388
+
389
+ ```js
390
+ const { asyncHandler, throwError } = require("reqsight");
391
+
392
+ app.get(
393
+ "/users/:id",
394
+ asyncHandler(async (req, res) => {
395
+ const user = await db.findUser(req.params.id);
396
+ if (!user) throwError("User not found", 404);
397
+ res.json(user);
398
+ })
399
+ );
400
+ ```
401
+
402
+ ---
403
+
404
+ ## Correlation IDs
405
+
406
+ Every request handled by `requestLogger` gets a correlation ID:
407
+
408
+ 1. If the client sends `x-correlation-id` in the request header, that value is used as-is
409
+ 2. Otherwise, a UUID v4 is generated
410
+
411
+ The ID is:
412
+ - Attached to `req.correlationId`
413
+ - Set on the response header `x-correlation-id`
414
+ - Bound into `req.logger` (every log from that logger carries it automatically)
415
+ - Included in the `REQUEST RECEIVED` and `REQUEST COMPLETED` log messages
416
+
417
+ To propagate the ID through your entire call chain, pass it when calling downstream services:
418
+
419
+ ```js
420
+ axios.get(url, {
421
+ headers: { "x-correlation-id": req.correlationId },
422
+ });
423
+ ```
424
+
425
+ ---
426
+
427
+ ## Request-Scoped Logging
428
+
429
+ `requestLogger` attaches a Pino child logger to `req.logger`. It is pre-bound with `correlationId`, `method`, and `path` — every message your route handler logs carries that context without any extra work.
430
+
431
+ ```js
432
+ app.get("/orders", asyncHandler(async (req, res) => {
433
+ req.logger.info("fetching orders from database");
434
+
435
+ const orders = await db.getOrders();
436
+
437
+ req.logger.info({ count: orders.length }, "orders fetched");
438
+
439
+ res.json(orders);
440
+ }));
441
+ ```
442
+
443
+ Log output:
444
+
445
+ ```json
446
+ { "level": "info", "correlationId": "a1b2c3...", "method": "GET", "path": "/orders", "msg": "fetching orders from database" }
447
+ { "level": "info", "correlationId": "a1b2c3...", "method": "GET", "path": "/orders", "count": 42, "msg": "orders fetched" }
448
+ ```
449
+
450
+ ---
451
+
452
+ ## Axios Outbound Tracing
453
+
454
+ Once `axiosInterceptors()` is called, every Axios request is logged automatically:
455
+
456
+ ```
457
+ [a1b2c3] - OUTBOUND REQUEST { method: "POST", url: "https://api.payments.io/charge", ... }
458
+ [a1b2c3] - OUTBOUND ERROR { code: "ECONNREFUSED", statusCode: 503, ... }
459
+ ```
460
+
461
+ If `x-correlation-id` is not set on the outbound request config, a warning is emitted so you can trace which call is missing ID propagation.
462
+
463
+ ---
464
+
465
+ ## Error Handling
466
+
467
+ ReqSight's error pipeline is built around the distinction between **operational errors** and **unexpected errors**.
468
+
469
+ **Operational errors** — raised intentionally via `throwError`. Safe to expose to clients.
470
+
471
+ ```js
472
+ // In a route handler
473
+ throwError("Insufficient balance", 402, { required: 100, available: 40 });
474
+
475
+ // Response the client receives:
476
+ // { "success": false, "message": "Insufficient balance", "required": 100, "available": 40 }
477
+ ```
478
+
479
+ **Unexpected errors** — unhandled exceptions, programmer errors. The client receives a generic 500 referencing the correlation ID so you can look it up in your logs.
480
+
481
+ ```json
482
+ {
483
+ "success": false,
484
+ "message": "Even the best systems have bad days. Ours is having one right now. Reference: a1b2c3..."
485
+ }
486
+ ```
487
+
488
+ **Axios errors** — when a downstream call fails and the error reaches `errorLogger`, ReqSight forwards the downstream status code and response body to the client:
489
+
490
+ ```json
491
+ { "success": false, "message": "Payment declined", "code": "CARD_DECLINED" }
492
+ ```
493
+
494
+ ---
495
+
496
+ ## TypeScript
497
+
498
+ ReqSight ships `.d.ts` declarations. No `@types` package needed.
499
+
500
+ The Express `Request` interface is augmented automatically:
501
+
502
+ ```ts
503
+ import express, { Request, Response } from "express";
504
+ import { requestLogger, throwError, asyncHandler } from "reqsight";
505
+
506
+ const app = express();
507
+ app.use(requestLogger);
508
+
509
+ app.get("/", asyncHandler(async (req: Request, res: Response) => {
510
+ req.correlationId; // string | undefined
511
+ req.startTime; // number | undefined
512
+ req.logger; // pino.Logger | undefined
513
+
514
+ throwError("Not allowed", 403);
515
+ }));
516
+ ```
517
+
518
+ ---
519
+
520
+ ## Log Output Examples
521
+
522
+ **Development (`prettyLogs: true`):**
523
+
524
+ ```
525
+ [2025-06-21 14:32:01.123] INFO [a1b2c3d4] - REQUEST RECEIVED
526
+ ip: "127.0.0.1"
527
+ method: "POST"
528
+ originalUrl: "/api/users"
529
+
530
+ [2025-06-21 14:32:01.187] INFO [a1b2c3d4] - REQUEST COMPLETED
531
+ statusCode: 201
532
+ duration: "64ms"
533
+ responseSize: "312 bytes"
534
+ ```
535
+
536
+ **Production (`prettyLogs: false`):**
537
+
538
+ ```json
539
+ {"level":"info","time":"2025-06-21T14:32:01.123Z","correlationId":"a1b2c3d4","method":"POST","path":"/api/users","ip":"127.0.0.1","originalUrl":"/api/users","msg":"[a1b2c3d4] - REQUEST RECEIVED"}
540
+ {"level":"info","time":"2025-06-21T14:32:01.187Z","correlationId":"a1b2c3d4","method":"POST","path":"/api/users","statusCode":201,"duration":"64ms","responseSize":"312 bytes","msg":"[a1b2c3d4] - REQUEST COMPLETED"}
541
+ ```
542
+
543
+ ---
544
+
545
+ ## Requirements
546
+
547
+ | Dependency | Version | Notes |
548
+ |---|---|---|
549
+ | Node.js | ≥ 18 | |
550
+ | Express | ≥ 4.0.0 | peer dependency |
551
+ | Axios | ≥ 1.0.0 | optional peer dependency — only needed for `axiosInterceptors` |
552
+ | pino-pretty | any | optional — only needed when `prettyLogs: true` |
553
+
554
+ ---
555
+
556
+ ## License
557
+
558
+ MIT
package/index.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ import { Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from "express";
2
+ import { Logger } from "pino";
3
+
4
+ declare module "express" {
5
+ interface Request {
6
+ correlationId?: string;
7
+ startTime?: number;
8
+ logger?: Logger;
9
+ }
10
+ }
11
+
12
+ /**
13
+ * Middleware that logs incoming requests and outgoing responses.
14
+ * Attaches `req.correlationId`, `req.logger`, and `req.startTime` to every request.
15
+ * Reads `x-correlation-id` from incoming headers or generates a new UUID.
16
+ */
17
+ export declare const requestLogger: RequestHandler;
18
+
19
+ /**
20
+ * Error-handling middleware that logs errors and sends a structured JSON response.
21
+ * Distinguishes between operational errors (safe to expose) and unexpected crashes.
22
+ * Must be registered after all routes.
23
+ */
24
+ export declare const errorLogger: ErrorRequestHandler;
25
+
26
+ /**
27
+ * Catch-all middleware that handles unmatched routes.
28
+ * Logs a 404 warning and responds with a structured JSON error.
29
+ * Must be registered after all routes and before `errorLogger`.
30
+ */
31
+ export declare const notFoundLogger: RequestHandler;
32
+
33
+ /**
34
+ * Throws an operational error with a status code and optional extra properties.
35
+ * Errors thrown this way are treated as safe to expose to the client by `errorLogger`.
36
+ *
37
+ * @param message - Error message returned to the client
38
+ * @param statusCode - HTTP status code (defaults to 500)
39
+ * @param extra - Additional properties merged into the error response
40
+ */
41
+ export declare function throwError(
42
+ message: string,
43
+ statusCode?: number,
44
+ extra?: Record<string, unknown>
45
+ ): never;
46
+
47
+ /**
48
+ * Registers request and response interceptors on the global axios instance.
49
+ * Logs all outbound HTTP requests with correlation ID propagation.
50
+ * Safe to call multiple times — interceptors are only registered once.
51
+ * Requires `axios` to be installed separately.
52
+ */
53
+ export declare function axiosInterceptors(): void;
package/index.js ADDED
@@ -0,0 +1,15 @@
1
+ const requestLogger = require("./src/middleware/requestLogger");
2
+ const errorLogger = require("./src/middleware/errorLogger");
3
+ const notFoundLogger = require("./src/middleware/notFoundLogger");
4
+ const throwError = require("./src/utils/throwError");
5
+ const axiosInterceptors = require("./src/interceptors/axiosInterceptors");
6
+ const asyncHandler = require("./src/utils/asyncHandler");
7
+
8
+ module.exports = {
9
+ requestLogger,
10
+ errorLogger,
11
+ notFoundLogger,
12
+ throwError,
13
+ axiosInterceptors,
14
+ asyncHandler,
15
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "reqsight",
3
+ "version": "1.0.0",
4
+ "description": "Observability and error handling middleware for Express",
5
+ "keywords": [
6
+ "express",
7
+ "middleware",
8
+ "observability",
9
+ "logging",
10
+ "monitoring",
11
+ "tracing",
12
+ "correlation-id"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "Suvid",
16
+ "type": "commonjs",
17
+ "main": "index.js",
18
+ "types": "index.d.ts",
19
+ "files": [
20
+ "index.js",
21
+ "index.d.ts",
22
+ "src/"
23
+ ],
24
+ "scripts": {
25
+ "start": "node server.js",
26
+ "test": "echo \"Error: no test specified\" && exit 1",
27
+ "postinstall": "node src/scripts/postinstall.js"
28
+ },
29
+ "dependencies": {
30
+ "pino": "^10.3.1",
31
+ "uuid": "^14.0.1"
32
+ },
33
+ "optionalDependencies": {
34
+ "pino-pretty": "^13.1.3"
35
+ },
36
+ "peerDependencies": {
37
+ "express": ">=4.0.0",
38
+ "axios": ">=1.0.0"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "axios": {
42
+ "optional": true
43
+ }
44
+ }
45
+ }
package/src/config.js ADDED
@@ -0,0 +1,145 @@
1
+ const config = {
2
+ /*
3
+ * logLevel
4
+ * Controls the minimum log level emitted by reqsight.
5
+ *
6
+ * Possible values: "fatal" | "error" | "warn" | "info" | "debug" | "trace"
7
+ *
8
+ * Examples:
9
+ * logLevel: "info" // logs info, warn, error, fatal (default)
10
+ * logLevel: "warn" // only logs warn and above — less noise in production
11
+ * logLevel: "debug" // verbose output for local development
12
+ */
13
+ logLevel: process.env.LOG_LEVEL || "info",
14
+
15
+ sanitize: {
16
+ /*
17
+ * sanitize.request
18
+ * Controls what gets logged from incoming request objects.
19
+ *
20
+ * Possible values:
21
+ * function — called on every request with (req). Return any object.
22
+ * Edit the default below to add, remove, or rename fields.
23
+ * object — a static shape with no dynamic values. Snapshotted once at startup.
24
+ * Use for fixed metadata like service name or environment.
25
+ * Example: { service: "my-api", env: process.env.NODE_ENV }
26
+ * any other — spreads the full raw req object. May expose sensitive data.
27
+ */
28
+ request: (req) => ({
29
+ ip: req?.ip,
30
+ method: req?.method,
31
+ originalUrl: req?.originalUrl,
32
+ params: req?.params,
33
+ query: req?.query,
34
+ body: req?.body,
35
+ startTime: req?.startTime,
36
+ accept: req?.headers?.["accept"],
37
+ "accept-language": req?.headers?.["accept-language"],
38
+ "user-agent": req?.headers?.["user-agent"],
39
+ origin: req?.headers?.["origin"],
40
+ referer: req?.headers?.["referer"],
41
+ }),
42
+
43
+ /*
44
+ * sanitize.response
45
+ * Controls what gets logged from outgoing response objects.
46
+ * Compatible with both Express res and axios response shapes.
47
+ *
48
+ * Possible values:
49
+ * function — called on every response with (res). Return any object.
50
+ * Duration and responseSize are calculated separately in the middleware.
51
+ * Edit the default below to add, remove, or rename fields.
52
+ * object — a static shape with no dynamic values. Snapshotted once at startup.
53
+ * Example: { service: "my-api", env: process.env.NODE_ENV }
54
+ * any other — logs statusCode, duration, responseSize, and full headers object.
55
+ */
56
+ response: (res) => ({
57
+ statusCode: res?.statusCode || res?.status,
58
+ "content-type":
59
+ res?.getHeader?.("content-type") || res?.headers?.["content-type"],
60
+ "content-length":
61
+ res?.getHeader?.("content-length") || res?.headers?.["content-length"],
62
+ "x-correlation-id":
63
+ res?.getHeader?.("x-correlation-id") ||
64
+ res?.headers?.["x-correlation-id"],
65
+ "access-control-allow-origin":
66
+ res?.getHeader?.("access-control-allow-origin") ||
67
+ res?.headers?.["access-control-allow-origin"],
68
+ }),
69
+
70
+ /*
71
+ * sanitize.axiosErrors
72
+ * Controls what gets logged when an outbound axios request fails.
73
+ *
74
+ * Possible values:
75
+ * function — called on every axios error with (err). Return any object.
76
+ * Edit the default below to add, remove, or rename fields.
77
+ * Sensitive headers like Authorization and Cookie are excluded by default.
78
+ * object — a static shape with no dynamic values. Snapshotted once at startup.
79
+ * Example: { service: "my-api", env: process.env.NODE_ENV }
80
+ * any other — spreads the full axios error. Includes raw config with all headers.
81
+ * Only use if you fully trust and control your log destination.
82
+ */
83
+ axiosErrors: (err) => ({
84
+ code: err?.code,
85
+ message: err?.message,
86
+ name: err?.name,
87
+ statusCode: err?.status,
88
+ isRequestMade: !!err?.request,
89
+ config: {
90
+ url: err?.config?.url,
91
+ method: err?.config?.method,
92
+ accept: err?.config?.headers?.["accept"],
93
+ "accept-language": err?.config?.headers?.["accept-language"],
94
+ "user-agent": err?.config?.headers?.["user-agent"],
95
+ origin: err?.config?.headers?.["origin"],
96
+ referer: err?.config?.headers?.["referer"],
97
+ },
98
+ response: {
99
+ status: err?.response?.status,
100
+ statusText: err?.response?.statusText,
101
+ headers: {
102
+ "content-type": err?.response?.headers?.["content-type"],
103
+ "content-length": err?.response?.headers?.["content-length"],
104
+ "x-correlation-id": err?.response?.headers?.["x-correlation-id"],
105
+ "access-control-allow-origin":
106
+ err?.response?.headers?.["access-control-allow-origin"],
107
+ },
108
+ data: err?.response?.data,
109
+ },
110
+ stack: err?.stack,
111
+ }),
112
+ },
113
+
114
+ /*
115
+ * stackTrace
116
+ * Whether to include the error stack trace in error responses sent to the client.
117
+ * Never expose stack traces in production — they reveal internal paths and logic.
118
+ *
119
+ * Possible values: true | false
120
+ *
121
+ * Examples:
122
+ * stackTrace: true // always include (dev only)
123
+ * stackTrace: false // never include
124
+ * stackTrace: process.env.NODE_ENV !== "production" // only outside prod (default)
125
+ */
126
+ stackTrace: process.env.NODE_ENV !== "production" || false,
127
+
128
+ /*
129
+ * prettyLogs
130
+ * Whether to use pino-pretty for colorized, human-readable console output.
131
+ * Requires pino-pretty to be installed (optional dependency).
132
+ * Disable in production — raw JSON is faster and works with log aggregators
133
+ * (Datadog, CloudWatch, Loki, etc.).
134
+ *
135
+ * Possible values: true | false
136
+ *
137
+ * Examples:
138
+ * prettyLogs: true // colorized output (dev only)
139
+ * prettyLogs: false // raw JSON
140
+ * prettyLogs: process.env.NODE_ENV !== "production" // only outside prod (default)
141
+ */
142
+ prettyLogs: process.env.NODE_ENV !== "production" || false,
143
+ };
144
+
145
+ module.exports = config;
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+
3
+ const logger = require("../logger");
4
+
5
+ let registered = false;
6
+
7
+ const axiosInterceptors = () => {
8
+ if (registered) return;
9
+
10
+ let axios;
11
+
12
+ try {
13
+ axios = require("axios");
14
+ } catch {
15
+ console.warn(
16
+ "[reqsight] axios not found. " +
17
+ "Install axios to enable outbound HTTP interception: " +
18
+ "npm install axios",
19
+ );
20
+ return;
21
+ }
22
+
23
+ registered = true;
24
+
25
+ axios.interceptors.request.use(
26
+ (config) => {
27
+ if (!config?.headers?.["x-correlation-id"])
28
+ logger.warn(
29
+ { method: config?.method, url: config?.url, type: "OUTBOUND" },
30
+ "x-correlation-id not set for outbound request",
31
+ );
32
+
33
+ config["startTime"] = Date.now();
34
+
35
+ logger.info(
36
+ {
37
+ correlationId: config?.headers?.["x-correlation-id"],
38
+ method: config?.method,
39
+ url: config?.url,
40
+ params: config?.params,
41
+ data: config?.data,
42
+ startTime: config["startTime"],
43
+ type: "OUTBOUND",
44
+ },
45
+ config.headers["x-correlation-id"]
46
+ ? `[${config.headers["x-correlation-id"]}] - OUTBOUND REQUEST`
47
+ : "OUTBOUND REQUEST",
48
+ );
49
+ return config;
50
+ },
51
+ (err) => Promise.reject(err),
52
+ );
53
+
54
+ axios.interceptors.response.use(
55
+ (response) => {
56
+ // logger.info(
57
+ // {
58
+ // correlationId: response?.config?.headers?.["x-correlation-id"],
59
+ // method: response?.config?.method,
60
+ // path: response?.config?.url,
61
+ // ...responseSerializer(
62
+ // response,
63
+ // response?.data,
64
+ // Date.now() - response?.config?.startTime,
65
+ // ),
66
+ // type: "OUTBOUND",
67
+ // },
68
+ // response?.config?.headers?.["x-correlation-id"]
69
+ // ? `[${response?.config?.headers["x-correlation-id"]}] - OUTBOUND RESPONSE`
70
+ // : "OUTBOUND RESPONSE",
71
+ // );
72
+
73
+ return response;
74
+ },
75
+
76
+ (err) => {
77
+ logger.error(
78
+ {
79
+ correlationId: err?.config?.headers?.["x-correlation-id"],
80
+ method: err?.config?.method,
81
+ url: err?.config?.url,
82
+ params: err?.config?.params,
83
+ body: err?.config?.data,
84
+ contentType: err?.config?.headers?.["content-type"],
85
+ type: "OUTBOUND",
86
+ ...(err?.isAxiosError && { isAxiosError: true }),
87
+ error: err?.isAxiosError ? err?.response?.data : err,
88
+ },
89
+ err?.config?.headers?.["x-correlation-id"]
90
+ ? `[${err?.config?.headers["x-correlation-id"]}] - OUTBOUND ERROR`
91
+ : "OUTBOUND ERROR",
92
+ );
93
+
94
+ return Promise.reject(err);
95
+ },
96
+ );
97
+ };
98
+
99
+ module.exports = axiosInterceptors;
@@ -0,0 +1,45 @@
1
+ const pino = require("pino");
2
+ const config = require("../config");
3
+
4
+ const redact = {
5
+ paths: [],
6
+ censor: "[REDACTED]",
7
+ };
8
+
9
+ const serializers = {
10
+ err: pino.stdSerializers.err,
11
+ error: pino.stdSerializers.err,
12
+ };
13
+
14
+ const loggerOptions = {
15
+ level: config.logLevel,
16
+ timestamp: pino.stdTimeFunctions.isoTime,
17
+ serializers,
18
+ redact,
19
+ base: undefined,
20
+ };
21
+
22
+ // Logger configuration
23
+ const log = config.prettyLogs
24
+ ? pino(
25
+ loggerOptions,
26
+ pino.transport({
27
+ target: "pino-pretty",
28
+ options: {
29
+ colorize: true,
30
+ translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l",
31
+ ignore: "pid,hostname",
32
+ },
33
+ }),
34
+ )
35
+ : pino(loggerOptions);
36
+
37
+ log.withRequest = (correlationId, method, path) => {
38
+ return log.child({
39
+ correlationId,
40
+ method,
41
+ path,
42
+ });
43
+ };
44
+
45
+ module.exports = log;
@@ -0,0 +1,55 @@
1
+ const config = require("../config");
2
+ const logger = require("../logger");
3
+ const sanitizeAxiosError = require("../utils/sanitizeAxiosError");
4
+
5
+ const errorLogger = (err, req, res, _next) => {
6
+ (req.logger || logger).error(
7
+ {
8
+ ...(err?.isAxiosError && { isAxiosError: true }),
9
+ error: err?.isAxiosError ? sanitizeAxiosError(err) : err,
10
+ },
11
+ req.correlationId
12
+ ? `[${req.correlationId}] - REQUEST ERROR`
13
+ : "REQUEST ERROR",
14
+ );
15
+
16
+ if (err?.isAxiosError && err?.response) {
17
+ const { status, data } = err.response;
18
+ return res.status(status || 500).json({
19
+ success: false,
20
+ message: data?.message || "Downstream service error",
21
+ ...(typeof data === "object" ? data : {}),
22
+ });
23
+ }
24
+
25
+ if (!err?.isOperational)
26
+ return res.status(500).json({
27
+ success: false,
28
+ message: `Even the best systems have bad days. Ours is having one right now.${req?.correlationId ? ` Reference: ${req.correlationId}` : ""}`,
29
+ });
30
+
31
+ const errorResponse = { success: false, message: err?.message };
32
+ Object.keys(err).forEach((key) => {
33
+ if (key === "stack" || key === "isOperational") return;
34
+
35
+ const value = err[key];
36
+
37
+ if (Array.isArray(value)) {
38
+ errorResponse[key] = value;
39
+ return;
40
+ }
41
+
42
+ if (value !== null && typeof value !== "object") {
43
+ errorResponse[key] = value;
44
+ }
45
+ });
46
+
47
+ // Include stack trace only in development
48
+ if (config.stackTrace) {
49
+ errorResponse.stack = err?.stack;
50
+ }
51
+
52
+ res.status(err.statusCode || 500).json(errorResponse);
53
+ };
54
+
55
+ module.exports = errorLogger;
@@ -0,0 +1,24 @@
1
+ const logger = require("../logger");
2
+
3
+ const notFoundLogger = (req, res, next) => {
4
+ if (res?.headersSent) return next();
5
+
6
+ (req.logger || logger).warn(
7
+ {
8
+ statusCode: 404,
9
+ code: "PATH_NOT_FOUND",
10
+ },
11
+ "PATH NOT FOUND",
12
+ );
13
+
14
+ res.status(404).json({
15
+ success: false,
16
+ message: "Path not found",
17
+ code: "PATH_NOT_FOUND",
18
+ method: req?.method,
19
+ path: req?.path,
20
+ statusCode: res?.statusCode,
21
+ });
22
+ };
23
+
24
+ module.exports = notFoundLogger;
@@ -0,0 +1,65 @@
1
+ const { generateCorrelationId } = require("../utils/correlationId");
2
+ const logger = require("../logger");
3
+ const {
4
+ requestSerializer,
5
+ responseSerializer,
6
+ } = require("../utils/serializer");
7
+
8
+ const requestLogger = (req, res, next) => {
9
+ req.correlationId =
10
+ req.headers["x-correlation-id"] || generateCorrelationId();
11
+
12
+ res.setHeader("x-correlation-id", req.correlationId);
13
+
14
+ req.startTime = Date.now();
15
+
16
+ req.logger = logger.withRequest(req.correlationId, req.method, req.path);
17
+
18
+ req.logger.info(
19
+ { ...requestSerializer(req) },
20
+ `[${req.correlationId}] - REQUEST RECEIVED`,
21
+ );
22
+
23
+ let responseSent = false;
24
+
25
+ const logResponse = (data) => {
26
+ if (responseSent || !data) return;
27
+
28
+ responseSent = true;
29
+
30
+ const endTime = Date.now();
31
+
32
+ req.logger.info(
33
+ {
34
+ ...requestSerializer(req),
35
+ ...responseSerializer(res),
36
+ duration: `${endTime - req.startTime}ms`,
37
+ endTime,
38
+ responseSize: data
39
+ ? `${Buffer.byteLength(
40
+ typeof data === "string" ? data : JSON.stringify(data),
41
+ "utf8",
42
+ )} bytes`
43
+ : "0 bytes",
44
+ },
45
+ `[${req.correlationId}] - REQUEST COMPLETED`,
46
+ );
47
+ };
48
+
49
+ const originalEnd = res.end;
50
+ const originalSend = res.send;
51
+
52
+ res.end = function (data, encoding) {
53
+ logResponse(data);
54
+ return originalEnd.call(this, data, encoding);
55
+ };
56
+
57
+ res.send = function (data) {
58
+ logResponse(data);
59
+ return originalSend.call(this, data);
60
+ };
61
+
62
+ next();
63
+ };
64
+
65
+ module.exports = requestLogger;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ // Skip when running inside the package's own directory (e.g. during development)
7
+ const packageRoot = path.resolve(__dirname, "../..");
8
+ if (path.resolve(process.cwd()) === packageRoot) process.exit(0);
9
+
10
+ const configPath = path.join(process.cwd(), "reqsight.config.js");
11
+
12
+ if (fs.existsSync(configPath)) {
13
+ console.log("[reqsight] Config file already exists — skipping creation.");
14
+ process.exit(0);
15
+ }
16
+
17
+ const sourcePath = path.join(__dirname, "../config.js");
18
+ const defaultConfig = fs.readFileSync(sourcePath, "utf8");
19
+
20
+ try {
21
+ fs.writeFileSync(configPath, defaultConfig, "utf8");
22
+ console.log("[reqsight] Config file created → reqsight.config.js");
23
+ console.log("[reqsight] Customize it to control what gets logged.");
24
+ } catch (err) {
25
+ console.warn("[reqsight] Could not create config file:", err.message);
26
+ }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+
3
+ const asyncHandler = (fn) => {
4
+ if (typeof fn !== "function") {
5
+ throw new Error("[reqsight] asyncHandler expects a function");
6
+ }
7
+
8
+ return (req, res, next) => {
9
+ Promise.resolve(fn(req, res, next)).catch(next);
10
+ };
11
+ };
12
+
13
+ module.exports = asyncHandler;
@@ -0,0 +1,9 @@
1
+ const { v4: uuidv4 } = require("uuid");
2
+
3
+ function generateCorrelationId() {
4
+ return uuidv4();
5
+ }
6
+
7
+ module.exports = {
8
+ generateCorrelationId,
9
+ };
@@ -0,0 +1,16 @@
1
+ const config = require("../config");
2
+
3
+ const buildSanitizeAxiosError = (sanitize) => {
4
+ if (typeof sanitize === "function") return sanitize;
5
+
6
+ if (sanitize !== null && typeof sanitize === "object") {
7
+ const snapshot = { ...sanitize };
8
+ return () => snapshot;
9
+ }
10
+
11
+ return (err) => ({ ...err });
12
+ };
13
+
14
+ const sanitizeAxiosError = buildSanitizeAxiosError(config.sanitize.axiosErrors);
15
+
16
+ module.exports = sanitizeAxiosError;
@@ -0,0 +1,20 @@
1
+ const config = require("../config");
2
+
3
+ const buildSerializer = (sanitize) => {
4
+ if (typeof sanitize === "function") return sanitize;
5
+
6
+ if (sanitize !== null && typeof sanitize === "object") {
7
+ const snapshot = { ...sanitize };
8
+ return () => snapshot;
9
+ }
10
+
11
+ return (target) => ({ ...target });
12
+ };
13
+
14
+ const requestSerializer = buildSerializer(config.sanitize.request);
15
+ const responseSerializer = buildSerializer(config.sanitize.response);
16
+
17
+ module.exports = {
18
+ requestSerializer,
19
+ responseSerializer,
20
+ };
@@ -0,0 +1,15 @@
1
+ const throwError = (msg, statusCode, arg = {}) => {
2
+ const errorObject = {
3
+ statusCode: typeof statusCode === "number" ? statusCode : 500,
4
+ isOperational: true,
5
+ };
6
+
7
+ if (arg && typeof arg === "object") Object.assign(errorObject, arg);
8
+
9
+ throw Object.assign(
10
+ new Error(typeof msg === "string" ? msg : "Something went wrong"),
11
+ errorObject,
12
+ );
13
+ };
14
+
15
+ module.exports = throwError;