@nm-logger/logger 1.1.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 +21 -0
- package/README.md +236 -0
- package/index.d.ts +117 -0
- package/index.js +4 -0
- package/package.json +37 -0
- package/src/DailyWatcher.js +53 -0
- package/src/LogWriter.js +29 -0
- package/src/Logger.js +297 -0
- package/src/Queue.js +32 -0
- package/src/S3Uploader.js +28 -0
- package/src/utils.js +111 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Virtual Employee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# @ve/logger
|
|
2
|
+
|
|
3
|
+
Daily JSON logger for Node.js / Express APIs with:
|
|
4
|
+
|
|
5
|
+
- S3 upload + queue + daily rotation
|
|
6
|
+
- Correlation IDs (with `X-Correlation-ID` header)
|
|
7
|
+
- External API logging (Axios)
|
|
8
|
+
- Sensitive field masking (password, token, otp, etc.)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
After you publish the package to npm:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @ve/logger
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Log Format
|
|
23
|
+
|
|
24
|
+
Each log line in `daily_logs.json` is a JSON object:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"url": "/api/v1/attendance/get",
|
|
29
|
+
"body": "{\"month\":\"2025-12\"}",
|
|
30
|
+
"params": "{\"params\":{},\"query\":{}}",
|
|
31
|
+
"type": "get",
|
|
32
|
+
"error": "",
|
|
33
|
+
"date": "2025-12-05 12:24:00",
|
|
34
|
+
"employee_id": "TAKK122",
|
|
35
|
+
"correlation_id": "cid-abcd1234-17f5d3c9a"
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Fields:
|
|
40
|
+
|
|
41
|
+
- `url` – `req.originalUrl`
|
|
42
|
+
- `body` – stringified (and masked) `req.body`
|
|
43
|
+
- `params` – stringified (and masked) object `{ params: req.params, query: req.query }`
|
|
44
|
+
- `type` – last segment of the URL (e.g. `/api/v1/attendance/get` → `"get"`)
|
|
45
|
+
- `error` – error message if any
|
|
46
|
+
- `date` – `YYYY-MM-DD HH:mm:ss`
|
|
47
|
+
- `employee_id` – from argument or `req.user.employee_id / emp_code / id`
|
|
48
|
+
- `correlation_id` – unique per request chain (also added as `X-Correlation-ID` header)
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Basic Usage
|
|
53
|
+
|
|
54
|
+
### 1. Create the logger
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
const Logger = require("@ve/logger");
|
|
58
|
+
|
|
59
|
+
const logger = new Logger(
|
|
60
|
+
{
|
|
61
|
+
accessKeyId: process.env.AWS_KEY,
|
|
62
|
+
secretAccessKey: process.env.AWS_SECRET,
|
|
63
|
+
region: "ap-south-1",
|
|
64
|
+
bucket: "your-log-bucket-name"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
baseDir: "logs", // optional, default "logs"
|
|
68
|
+
watchIntervalMs: 60000, // optional, default 60s
|
|
69
|
+
maskFields: ["aadhaar", "panNumber"] // extra fields to mask
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 2. Log every request + set correlation ID header
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
app.use(logger.requestLoggerMiddleware());
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 3. Log errors via Express error middleware
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
// your routes above...
|
|
84
|
+
|
|
85
|
+
app.use(logger.expressMiddleware()); // or logger.expressErrorMiddleware()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 4. Manual logging in routes
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
app.post("/api/v1/attendance/get", async (req, res) => {
|
|
92
|
+
try {
|
|
93
|
+
// ... your logic, external APIs etc ...
|
|
94
|
+
|
|
95
|
+
await logger.logRequest(req, req.user?.employee_id);
|
|
96
|
+
res.json({ success: true });
|
|
97
|
+
} catch (err) {
|
|
98
|
+
await logger.logError(err, req, req.user?.employee_id);
|
|
99
|
+
res.status(500).json({ error: err.message });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## External API logging with Axios
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
const axios = require("axios");
|
|
110
|
+
|
|
111
|
+
// Attach once at startup
|
|
112
|
+
logger.attachAxiosLogger(axios);
|
|
113
|
+
|
|
114
|
+
app.get("/api/v1/some-data", async (req, res) => {
|
|
115
|
+
try {
|
|
116
|
+
const response = await axios.get("https://api.example.com/data", {
|
|
117
|
+
headers: {
|
|
118
|
+
"X-Correlation-ID": req.correlationId, // propagated
|
|
119
|
+
"X-Employee-ID": req.user?.employee_id || "" // optional
|
|
120
|
+
},
|
|
121
|
+
params: {
|
|
122
|
+
id: 123
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
res.json(response.data);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
await logger.logError(err, req, req.user?.employee_id);
|
|
129
|
+
res.status(500).json({ error: err.message });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
This will produce external log lines like:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"url": "https://api.example.com/data",
|
|
139
|
+
"body": "{}",
|
|
140
|
+
"params": "{\"id\":123}",
|
|
141
|
+
"type": "external_api",
|
|
142
|
+
"error": "",
|
|
143
|
+
"date": "2025-12-05 12:24:00",
|
|
144
|
+
"employee_id": "TAKK122",
|
|
145
|
+
"correlation_id": "cid-abcd1234-17f5d3c9a"
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Sensitive Data Masking
|
|
152
|
+
|
|
153
|
+
Built-in masked keys (case-insensitive, partial match):
|
|
154
|
+
|
|
155
|
+
- password, pass
|
|
156
|
+
- token, secret
|
|
157
|
+
- otp
|
|
158
|
+
- auth, authorization
|
|
159
|
+
- apiKey, api_key
|
|
160
|
+
- session
|
|
161
|
+
- ssn
|
|
162
|
+
|
|
163
|
+
Plus anything you pass in `maskFields` option.
|
|
164
|
+
|
|
165
|
+
Any object like:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"password": "MyPass123",
|
|
170
|
+
"otp": "111222",
|
|
171
|
+
"aadhaar": "9999-8888-7777",
|
|
172
|
+
"email": "user@example.com"
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
will be logged as:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"password": "*****",
|
|
181
|
+
"otp": "*****",
|
|
182
|
+
"aadhaar": "*****",
|
|
183
|
+
"email": "user@example.com"
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## S3 Upload Behavior
|
|
190
|
+
|
|
191
|
+
- Logs are stored locally under:
|
|
192
|
+
- `logs/YYYY/MM/DD/daily_logs.json`
|
|
193
|
+
- A watcher runs every `watchIntervalMs` (default 60 seconds)
|
|
194
|
+
- When the date changes (e.g., from `2025-12-05` to `2025-12-06`),
|
|
195
|
+
- The logger uploads the **previous day's** log file to S3:
|
|
196
|
+
|
|
197
|
+
Example S3 key:
|
|
198
|
+
|
|
199
|
+
```txt
|
|
200
|
+
2025/12/05/daily_logs.json
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
So final S3 path:
|
|
204
|
+
|
|
205
|
+
```txt
|
|
206
|
+
s3://<bucket>/<year>/<month>/<day>/daily_logs.json
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## TypeScript Usage
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import Logger, { S3Config, LoggerOptions } from "@ve/logger";
|
|
215
|
+
|
|
216
|
+
const s3config: S3Config = {
|
|
217
|
+
accessKeyId: process.env.AWS_KEY!,
|
|
218
|
+
secretAccessKey: process.env.AWS_SECRET!,
|
|
219
|
+
region: "ap-south-1",
|
|
220
|
+
bucket: "your-log-bucket"
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const options: LoggerOptions = {
|
|
224
|
+
baseDir: "logs",
|
|
225
|
+
watchIntervalMs: 60000,
|
|
226
|
+
maskFields: ["aadhaar", "pan"]
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const logger = new Logger(s3config, options);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
MIT
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as express from "express";
|
|
2
|
+
|
|
3
|
+
export interface S3Config {
|
|
4
|
+
accessKeyId: string;
|
|
5
|
+
secretAccessKey: string;
|
|
6
|
+
region: string;
|
|
7
|
+
bucket: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface LoggerOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Local base directory where logs are stored.
|
|
13
|
+
* Default: "logs"
|
|
14
|
+
*/
|
|
15
|
+
baseDir?: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Interval in milliseconds for the daily watcher to check for date change.
|
|
19
|
+
* Default: 60000 (1 minute)
|
|
20
|
+
*/
|
|
21
|
+
watchIntervalMs?: number;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extra field names to mask in logs (in addition to built-in ones like password, token, otp, etc.).
|
|
25
|
+
* Matching is case-insensitive and uses "includes".
|
|
26
|
+
*/
|
|
27
|
+
maskFields?: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface LogEntryShape {
|
|
31
|
+
url: string;
|
|
32
|
+
body: string;
|
|
33
|
+
params: string;
|
|
34
|
+
type: string;
|
|
35
|
+
error: string;
|
|
36
|
+
date: string;
|
|
37
|
+
employee_id: string;
|
|
38
|
+
correlation_id: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ExternalApiLogOptions {
|
|
42
|
+
url?: string;
|
|
43
|
+
method?: string;
|
|
44
|
+
data?: any;
|
|
45
|
+
params?: any;
|
|
46
|
+
error?: string | null;
|
|
47
|
+
correlationId?: string;
|
|
48
|
+
employeeId?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
declare class Logger {
|
|
52
|
+
constructor(s3config?: S3Config, options?: LoggerOptions);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Log an error for a given request.
|
|
56
|
+
* The log will be appended to the daily JSON file.
|
|
57
|
+
*/
|
|
58
|
+
logError(
|
|
59
|
+
err: any,
|
|
60
|
+
req?: express.Request,
|
|
61
|
+
employee_id?: string
|
|
62
|
+
): Promise<void>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Log a normal API request (no error).
|
|
66
|
+
*/
|
|
67
|
+
logRequest(req: express.Request, employee_id?: string): Promise<void>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Log an external API call (axios, etc.).
|
|
71
|
+
* Type will be "external_api".
|
|
72
|
+
*/
|
|
73
|
+
logExternalApi(options: ExternalApiLogOptions): Promise<void>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Express error-handling middleware.
|
|
77
|
+
* Use: app.use(logger.expressMiddleware());
|
|
78
|
+
* Also ensures X-Correlation-ID header is present.
|
|
79
|
+
*/
|
|
80
|
+
expressMiddleware(): (
|
|
81
|
+
err: any,
|
|
82
|
+
req: express.Request,
|
|
83
|
+
res: express.Response,
|
|
84
|
+
next: express.NextFunction
|
|
85
|
+
) => void;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Alias for expressMiddleware (for clarity).
|
|
89
|
+
*/
|
|
90
|
+
expressErrorMiddleware(): (
|
|
91
|
+
err: any,
|
|
92
|
+
req: express.Request,
|
|
93
|
+
res: express.Response,
|
|
94
|
+
next: express.NextFunction
|
|
95
|
+
) => void;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Express request-logging middleware (non-error).
|
|
99
|
+
* Also generates/propagates correlation ID and sets X-Correlation-ID header.
|
|
100
|
+
* Use near the top of your middleware chain.
|
|
101
|
+
*/
|
|
102
|
+
requestLoggerMiddleware(): (
|
|
103
|
+
req: express.Request,
|
|
104
|
+
res: express.Response,
|
|
105
|
+
next: express.NextFunction
|
|
106
|
+
) => void;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Attach axios interceptors to log external API calls.
|
|
110
|
+
* Usage:
|
|
111
|
+
* const axios = require("axios");
|
|
112
|
+
* logger.attachAxiosLogger(axios);
|
|
113
|
+
*/
|
|
114
|
+
attachAxiosLogger(axiosInstance: any): void;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export = Logger;
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nm-logger/logger",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Daily JSON logger with S3 upload queue, correlation IDs, external API logging, and sensitive field masking for Express-based APIs.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node -e \"console.log('no tests yet')\""
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"logger",
|
|
12
|
+
"logging",
|
|
13
|
+
"express",
|
|
14
|
+
"s3",
|
|
15
|
+
"aws",
|
|
16
|
+
"json-logger",
|
|
17
|
+
"daily-logs",
|
|
18
|
+
"correlation-id",
|
|
19
|
+
"axios",
|
|
20
|
+
"masking"
|
|
21
|
+
],
|
|
22
|
+
"author": "Virtual Employee (https://virtualemployee.com)",
|
|
23
|
+
"homepage": "https://virtualemployee.com",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"aws-sdk": "^2.1554.0",
|
|
27
|
+
"fs-extra": "^11.1.1"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"express": ">=4.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/express": "^4.17.21",
|
|
34
|
+
"@types/node": "^22.0.0",
|
|
35
|
+
"typescript": "^5.6.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const { getDatePath } = require("./utils");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fs = require("fs-extra");
|
|
4
|
+
|
|
5
|
+
class DailyWatcher {
|
|
6
|
+
constructor(baseDir, queue, s3Uploader, options = {}) {
|
|
7
|
+
this.baseDir = baseDir;
|
|
8
|
+
this.queue = queue;
|
|
9
|
+
this.s3Uploader = s3Uploader;
|
|
10
|
+
this.intervalMs = options.watchIntervalMs || 60_000; // default: 1 minute
|
|
11
|
+
|
|
12
|
+
this.lastDay = null;
|
|
13
|
+
this.startWatching();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
startWatching() {
|
|
17
|
+
setInterval(() => {
|
|
18
|
+
const { Y, M, D } = getDatePath();
|
|
19
|
+
const currentDay = `${Y}-${M}-${D}`;
|
|
20
|
+
|
|
21
|
+
if (!this.lastDay) {
|
|
22
|
+
// First run, just set lastDay
|
|
23
|
+
this.lastDay = currentDay;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (this.lastDay !== currentDay) {
|
|
28
|
+
// Day changed → upload previous day's log
|
|
29
|
+
const lastDayPath = this.lastDay.replace(/-/g, "/");
|
|
30
|
+
|
|
31
|
+
const logFile = path.join(
|
|
32
|
+
this.baseDir,
|
|
33
|
+
lastDayPath,
|
|
34
|
+
"daily_logs.json"
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (fs.existsSync(logFile)) {
|
|
38
|
+
const s3Key = `${lastDayPath}/daily_logs.json`; // e.g. 2025/12/05/daily_logs.json
|
|
39
|
+
|
|
40
|
+
this.queue.add(async () => {
|
|
41
|
+
console.log("📤 Uploading previous day logs →", s3Key);
|
|
42
|
+
await this.s3Uploader.upload(logFile, s3Key);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// update lastDay to current
|
|
47
|
+
this.lastDay = currentDay;
|
|
48
|
+
}
|
|
49
|
+
}, this.intervalMs);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = DailyWatcher;
|
package/src/LogWriter.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const fs = require("fs-extra");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { getDatePath, formatDate } = require("./utils");
|
|
4
|
+
|
|
5
|
+
class LogWriter {
|
|
6
|
+
constructor(baseDir = "logs") {
|
|
7
|
+
this.baseDir = baseDir;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Writes a single log entry in the required JSON format
|
|
12
|
+
* and appends it as one line to daily_logs.json
|
|
13
|
+
*/
|
|
14
|
+
async writeLog(log) {
|
|
15
|
+
const { Y, M, D } = getDatePath();
|
|
16
|
+
const logDir = path.join(this.baseDir, `${Y}/${M}/${D}`);
|
|
17
|
+
const logFile = path.join(logDir, "daily_logs.json");
|
|
18
|
+
|
|
19
|
+
await fs.ensureDir(logDir);
|
|
20
|
+
|
|
21
|
+
// Ensure date field is always set in required format
|
|
22
|
+
log.date = formatDate(new Date());
|
|
23
|
+
|
|
24
|
+
await fs.appendFile(logFile, JSON.stringify(log) + "\n");
|
|
25
|
+
return logFile;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = LogWriter;
|
package/src/Logger.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
const LogWriter = require("./LogWriter");
|
|
2
|
+
const Queue = require("./Queue");
|
|
3
|
+
const S3Uploader = require("./S3Uploader");
|
|
4
|
+
const DailyWatcher = require("./DailyWatcher");
|
|
5
|
+
const {
|
|
6
|
+
getApiType,
|
|
7
|
+
maskSensitive,
|
|
8
|
+
generateCorrelationId
|
|
9
|
+
} = require("./utils");
|
|
10
|
+
|
|
11
|
+
class Logger {
|
|
12
|
+
constructor(s3config = {}, options = {}) {
|
|
13
|
+
this.baseDir = options.baseDir || "logs";
|
|
14
|
+
|
|
15
|
+
this.logWriter = new LogWriter(this.baseDir);
|
|
16
|
+
this.queue = new Queue();
|
|
17
|
+
this.s3Uploader = new S3Uploader(s3config);
|
|
18
|
+
|
|
19
|
+
// Extra fields user wants masked (e.g. ["aadhaar", "pan"])
|
|
20
|
+
this.extraMaskFields = (options.maskFields || []).map((f) =>
|
|
21
|
+
String(f).toLowerCase()
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Start daily watcher for S3 uploads
|
|
25
|
+
new DailyWatcher(this.baseDir, this.queue, this.s3Uploader, {
|
|
26
|
+
watchIntervalMs: options.watchIntervalMs
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Mask any sensitive data using default + custom fields.
|
|
32
|
+
*/
|
|
33
|
+
mask(value) {
|
|
34
|
+
return maskSensitive(value, this.extraMaskFields);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build the base log entry with required shape.
|
|
39
|
+
* {
|
|
40
|
+
* url, body, params, type, error, date (added by LogWriter), employee_id, correlation_id
|
|
41
|
+
* }
|
|
42
|
+
*/
|
|
43
|
+
buildBaseLog(req, employee_id = "") {
|
|
44
|
+
const url = req?.originalUrl || "";
|
|
45
|
+
const bodySrc = req?.body || {};
|
|
46
|
+
const paramsObj = {
|
|
47
|
+
params: req?.params || {},
|
|
48
|
+
query: req?.query || {}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const maskedBody = this.mask(bodySrc);
|
|
52
|
+
const maskedParams = this.mask(paramsObj);
|
|
53
|
+
|
|
54
|
+
// derive correlation ID from request or headers
|
|
55
|
+
let correlationId =
|
|
56
|
+
req?.correlationId ||
|
|
57
|
+
(req?.headers &&
|
|
58
|
+
(req.headers["x-correlation-id"] || req.headers["X-Correlation-ID"])) ||
|
|
59
|
+
"";
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
url,
|
|
63
|
+
body: JSON.stringify(maskedBody || {}),
|
|
64
|
+
params: JSON.stringify(maskedParams || {}),
|
|
65
|
+
type: getApiType(url),
|
|
66
|
+
error: "",
|
|
67
|
+
employee_id:
|
|
68
|
+
employee_id ||
|
|
69
|
+
(req &&
|
|
70
|
+
req.user &&
|
|
71
|
+
(req.user.employee_id || req.user.emp_code || req.user.id)) ||
|
|
72
|
+
"",
|
|
73
|
+
correlation_id: correlationId
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Log an error (incoming API).
|
|
79
|
+
*/
|
|
80
|
+
async logError(err, req, employee_id = "") {
|
|
81
|
+
const base = this.buildBaseLog(req || {}, employee_id);
|
|
82
|
+
|
|
83
|
+
const logData = {
|
|
84
|
+
...base,
|
|
85
|
+
error: err && err.message ? err.message : String(err || "")
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
await this.logWriter.writeLog(logData);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Log a normal request (incoming API).
|
|
93
|
+
*/
|
|
94
|
+
async logRequest(req, employee_id = "") {
|
|
95
|
+
const logData = this.buildBaseLog(req, employee_id);
|
|
96
|
+
await this.logWriter.writeLog(logData);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Log an external API call (axios, etc.).
|
|
101
|
+
* For external APIs, `type` is always "external_api".
|
|
102
|
+
*/
|
|
103
|
+
async logExternalApi({
|
|
104
|
+
url,
|
|
105
|
+
method,
|
|
106
|
+
data,
|
|
107
|
+
params,
|
|
108
|
+
error,
|
|
109
|
+
correlationId,
|
|
110
|
+
employeeId
|
|
111
|
+
}) {
|
|
112
|
+
const maskedBody = this.mask(data || {});
|
|
113
|
+
const maskedParams = this.mask(params || {});
|
|
114
|
+
|
|
115
|
+
const logData = {
|
|
116
|
+
url: url || "",
|
|
117
|
+
body: JSON.stringify(maskedBody || {}),
|
|
118
|
+
params: JSON.stringify(maskedParams || {}),
|
|
119
|
+
type: "external_api",
|
|
120
|
+
error: error || "",
|
|
121
|
+
date: undefined, // set by LogWriter
|
|
122
|
+
employee_id: employeeId || "",
|
|
123
|
+
correlation_id: correlationId || ""
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
await this.logWriter.writeLog(logData);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Express error-handling middleware.
|
|
131
|
+
* Use: app.use(logger.expressMiddleware());
|
|
132
|
+
* Also ensures correlation ID is present and added to response header.
|
|
133
|
+
*/
|
|
134
|
+
expressMiddleware() {
|
|
135
|
+
return (err, req, res, next) => {
|
|
136
|
+
try {
|
|
137
|
+
let correlationId =
|
|
138
|
+
req.correlationId ||
|
|
139
|
+
(req.headers &&
|
|
140
|
+
(req.headers["x-correlation-id"] ||
|
|
141
|
+
req.headers["X-Correlation-ID"])) ||
|
|
142
|
+
"";
|
|
143
|
+
|
|
144
|
+
if (!correlationId) {
|
|
145
|
+
correlationId = generateCorrelationId();
|
|
146
|
+
req.correlationId = correlationId;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (res && !res.headersSent) {
|
|
150
|
+
res.setHeader("X-Correlation-ID", correlationId);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.logError(err, req).catch((e) =>
|
|
154
|
+
console.error("Logger expressMiddleware error:", e)
|
|
155
|
+
);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
console.error("Logger expressMiddleware outer error:", e);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
next(err);
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Alias for expressMiddleware (for clarity).
|
|
166
|
+
*/
|
|
167
|
+
expressErrorMiddleware() {
|
|
168
|
+
return this.expressMiddleware();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Express request-logging middleware.
|
|
173
|
+
* - Generates or reuses correlation ID
|
|
174
|
+
* - Sets X-Correlation-ID header on response
|
|
175
|
+
* - Logs each incoming request
|
|
176
|
+
*
|
|
177
|
+
* Use near the top of middleware stack:
|
|
178
|
+
* app.use(logger.requestLoggerMiddleware());
|
|
179
|
+
*/
|
|
180
|
+
requestLoggerMiddleware() {
|
|
181
|
+
return (req, res, next) => {
|
|
182
|
+
try {
|
|
183
|
+
let correlationId =
|
|
184
|
+
req.headers["x-correlation-id"] ||
|
|
185
|
+
req.headers["X-Correlation-ID"] ||
|
|
186
|
+
req.correlationId;
|
|
187
|
+
|
|
188
|
+
if (!correlationId) {
|
|
189
|
+
correlationId = generateCorrelationId();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
req.correlationId = correlationId;
|
|
193
|
+
res.setHeader("X-Correlation-ID", correlationId);
|
|
194
|
+
|
|
195
|
+
this.logRequest(req).catch((e) =>
|
|
196
|
+
console.error("Logger requestLoggerMiddleware error:", e)
|
|
197
|
+
);
|
|
198
|
+
} catch (e) {
|
|
199
|
+
console.error("Logger requestLoggerMiddleware outer error:", e);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
next();
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Attach axios interceptors to log EXTERNAL API calls.
|
|
208
|
+
*
|
|
209
|
+
* Usage:
|
|
210
|
+
* const axios = require("axios");
|
|
211
|
+
* logger.attachAxiosLogger(axios);
|
|
212
|
+
*
|
|
213
|
+
* In your route:
|
|
214
|
+
* await axios.get("https://api.example.com", {
|
|
215
|
+
* headers: {
|
|
216
|
+
* "X-Correlation-ID": req.correlationId,
|
|
217
|
+
* "X-Employee-ID": req.user?.employee_id
|
|
218
|
+
* }
|
|
219
|
+
* });
|
|
220
|
+
*/
|
|
221
|
+
attachAxiosLogger(axiosInstance) {
|
|
222
|
+
if (!axiosInstance || !axiosInstance.interceptors) {
|
|
223
|
+
console.warn(
|
|
224
|
+
"[@ve/logger] attachAxiosLogger: provided axios instance is invalid"
|
|
225
|
+
);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
axiosInstance.interceptors.response.use(
|
|
230
|
+
(response) => {
|
|
231
|
+
try {
|
|
232
|
+
const cfg = response.config || {};
|
|
233
|
+
const headers = cfg.headers || {};
|
|
234
|
+
|
|
235
|
+
const correlationId =
|
|
236
|
+
headers["X-Correlation-ID"] ||
|
|
237
|
+
headers["x-correlation-id"] ||
|
|
238
|
+
"";
|
|
239
|
+
|
|
240
|
+
const employeeId =
|
|
241
|
+
headers["X-Employee-ID"] ||
|
|
242
|
+
headers["x-employee-id"] ||
|
|
243
|
+
"";
|
|
244
|
+
|
|
245
|
+
this.logExternalApi({
|
|
246
|
+
url: cfg.url,
|
|
247
|
+
method: cfg.method,
|
|
248
|
+
data: cfg.data,
|
|
249
|
+
params: cfg.params,
|
|
250
|
+
error: null,
|
|
251
|
+
correlationId,
|
|
252
|
+
employeeId
|
|
253
|
+
}).catch((e) =>
|
|
254
|
+
console.error("Logger axios success logExternalApi error:", e)
|
|
255
|
+
);
|
|
256
|
+
} catch (e) {
|
|
257
|
+
console.error("Logger axios response interceptor error:", e);
|
|
258
|
+
}
|
|
259
|
+
return response;
|
|
260
|
+
},
|
|
261
|
+
(error) => {
|
|
262
|
+
try {
|
|
263
|
+
const cfg = error.config || {};
|
|
264
|
+
const headers = cfg ? cfg.headers || {} : {};
|
|
265
|
+
|
|
266
|
+
const correlationId =
|
|
267
|
+
headers["X-Correlation-ID"] ||
|
|
268
|
+
headers["x-correlation-id"] ||
|
|
269
|
+
"";
|
|
270
|
+
|
|
271
|
+
const employeeId =
|
|
272
|
+
headers["X-Employee-ID"] ||
|
|
273
|
+
headers["x-employee-id"] ||
|
|
274
|
+
"";
|
|
275
|
+
|
|
276
|
+
this.logExternalApi({
|
|
277
|
+
url: cfg && cfg.url,
|
|
278
|
+
method: cfg && cfg.method,
|
|
279
|
+
data: cfg && cfg.data,
|
|
280
|
+
params: cfg && cfg.params,
|
|
281
|
+
error: error && error.message,
|
|
282
|
+
correlationId,
|
|
283
|
+
employeeId
|
|
284
|
+
}).catch((e) =>
|
|
285
|
+
console.error("Logger axios error logExternalApi error:", e)
|
|
286
|
+
);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
console.error("Logger axios error interceptor outer error:", e);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return Promise.reject(error);
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = Logger;
|
package/src/Queue.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class Queue {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.jobs = [];
|
|
4
|
+
this.processing = false;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
add(job) {
|
|
8
|
+
this.jobs.push(job);
|
|
9
|
+
this.run();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async run() {
|
|
13
|
+
if (this.processing) return;
|
|
14
|
+
|
|
15
|
+
this.processing = true;
|
|
16
|
+
|
|
17
|
+
while (this.jobs.length) {
|
|
18
|
+
const job = this.jobs.shift();
|
|
19
|
+
try {
|
|
20
|
+
await job();
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.error("Queue job failed → re-added to queue:", err);
|
|
23
|
+
// Re-add the job for retry (very simple retry mechanism)
|
|
24
|
+
this.jobs.push(job);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this.processing = false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = Queue;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const AWS = require("aws-sdk");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
|
|
4
|
+
class S3Uploader {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.s3 = new AWS.S3({
|
|
7
|
+
accessKeyId: config.accessKeyId,
|
|
8
|
+
secretAccessKey: config.secretAccessKey,
|
|
9
|
+
region: config.region
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
this.bucket = config.bucket;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async upload(filePath, s3Key) {
|
|
16
|
+
const fileData = fs.readFileSync(filePath);
|
|
17
|
+
|
|
18
|
+
return this.s3
|
|
19
|
+
.putObject({
|
|
20
|
+
Bucket: this.bucket,
|
|
21
|
+
Key: s3Key,
|
|
22
|
+
Body: fileData
|
|
23
|
+
})
|
|
24
|
+
.promise();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = S3Uploader;
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Build path segments for logs: YYYY/MM/DD
|
|
2
|
+
exports.getDatePath = () => {
|
|
3
|
+
const now = new Date();
|
|
4
|
+
const Y = now.getFullYear();
|
|
5
|
+
const M = String(now.getMonth() + 1).padStart(2, "0");
|
|
6
|
+
const D = String(now.getDate()).padStart(2, "0");
|
|
7
|
+
return { Y, M, D };
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Format date as: 2025-12-05 12:24:00
|
|
11
|
+
exports.formatDate = (d) => {
|
|
12
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
d.getFullYear() +
|
|
16
|
+
"-" +
|
|
17
|
+
pad(d.getMonth() + 1) +
|
|
18
|
+
"-" +
|
|
19
|
+
pad(d.getDate()) +
|
|
20
|
+
" " +
|
|
21
|
+
pad(d.getHours()) +
|
|
22
|
+
":" +
|
|
23
|
+
pad(d.getMinutes()) +
|
|
24
|
+
":" +
|
|
25
|
+
pad(d.getSeconds())
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Extract last segment from URL, used as "type"
|
|
30
|
+
// e.g. "/api/v1/attendance/get" → "get"
|
|
31
|
+
exports.getApiType = (url) => {
|
|
32
|
+
if (!url) return "";
|
|
33
|
+
const segments = url.split("/").filter(Boolean);
|
|
34
|
+
return segments[segments.length - 1] || "";
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Default keys to mask (case-insensitive, "includes" match)
|
|
38
|
+
const DEFAULT_MASK_KEYS = [
|
|
39
|
+
"password",
|
|
40
|
+
"pass",
|
|
41
|
+
"token",
|
|
42
|
+
"secret",
|
|
43
|
+
"otp",
|
|
44
|
+
"auth",
|
|
45
|
+
"authorization",
|
|
46
|
+
"apikey",
|
|
47
|
+
"api_key",
|
|
48
|
+
"session",
|
|
49
|
+
"ssn"
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Deep mask of sensitive fields in any object/array.
|
|
54
|
+
* - Keys containing any of the mask keys are replaced with "*****"
|
|
55
|
+
* - Works recursively
|
|
56
|
+
* - If input is string, tries JSON.parse then masks
|
|
57
|
+
*/
|
|
58
|
+
exports.maskSensitive = (value, extraFields = []) => {
|
|
59
|
+
const allKeys = [
|
|
60
|
+
...DEFAULT_MASK_KEYS,
|
|
61
|
+
...(extraFields || [])
|
|
62
|
+
].map((k) => String(k).toLowerCase());
|
|
63
|
+
|
|
64
|
+
const shouldMaskKey = (key) => {
|
|
65
|
+
const lower = String(key).toLowerCase();
|
|
66
|
+
return allKeys.some((mk) => lower.includes(mk));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const maskAny = (val) => {
|
|
70
|
+
if (val && typeof val === "object") {
|
|
71
|
+
if (Array.isArray(val)) {
|
|
72
|
+
return val.map(maskAny);
|
|
73
|
+
}
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const [k, v] of Object.entries(val)) {
|
|
76
|
+
if (shouldMaskKey(k)) {
|
|
77
|
+
if (v === null || v === undefined) {
|
|
78
|
+
out[k] = v;
|
|
79
|
+
} else if (typeof v === "string" && v.length) {
|
|
80
|
+
out[k] = "*****";
|
|
81
|
+
} else {
|
|
82
|
+
out[k] = "*****";
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
out[k] = maskAny(v);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
return val;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// If it's a string, try JSON.parse and mask
|
|
94
|
+
if (typeof value === "string") {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(value);
|
|
97
|
+
return maskAny(parsed);
|
|
98
|
+
} catch (_) {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return maskAny(value);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Simple correlation ID generator
|
|
107
|
+
exports.generateCorrelationId = () => {
|
|
108
|
+
const rand = Math.random().toString(16).slice(2, 10);
|
|
109
|
+
const ts = Date.now().toString(16);
|
|
110
|
+
return `cid-${rand}-${ts}`;
|
|
111
|
+
};
|