@nm-logger/logger 1.2.6 → 1.2.7

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 CHANGED
@@ -1,48 +1,317 @@
1
- # @nm-logger/logger v1.2.3
1
+ # @nm-logger/logger
2
2
 
3
- Minimal JSON logger for Express:
3
+ Minimal JSON logger for Express + Axios with optional S3 sync.
4
4
 
5
- - Logs handled requests as **success**
5
+ ## Features
6
+
7
+ - Logs handled Express requests as **success**
6
8
  - Logs Express errors as **error**
7
9
  - Logs Axios calls as **external**
10
+ - Optional **database error** logs as `db_error`
8
11
  - Local daily files under configurable `baseDir`:
9
12
  - `YYYY/MM/DD/daily_logs_success.json`
10
13
  - `YYYY/MM/DD/daily_logs_error.json`
11
14
  - `YYYY/MM/DD/daily_logs_external.json`
12
- - S3: one JSON file per day per category, append (read+merge+put)
15
+ - `YYYY/MM/DD/daily_logs_db_error.json`
16
+ - S3: one JSON file per day per category (append by read + merge + put)
13
17
  - Previous day's local folder is removed when the date changes
14
- - `employee_id` taken from `req.user.employee_id || req.user.emp_code || req.user.id`
15
- - `baseDir` can be absolute (e.g. `/home/logs`)
18
+ - `employee_id` taken from `req.user.employee_id || req.user.emp_code || req.user.id` (used internally only, **not** written to JSON)
19
+ - Each log entry includes:
20
+ - `url`, `method`, `type` (internal/external/db_error)
21
+ - `status_code`
22
+ - `user_agent`
23
+ - `project_version` (from Logger options)
24
+ - `error`
25
+ - optional `response` (for external errors)
26
+ - `count` (when consecutive identical entries are compacted)
27
+ - External API logging:
28
+ - Logs **response body only for error-like calls** (4xx/5xx or when a custom `externalErrorDetector` marks it as error)
29
+ - Long responses are truncated with `maxResponseLength`
30
+
31
+ ## Install
16
32
 
33
+ ```bash
34
+ npm install @nm-logger/logger
35
+ ```
17
36
 
18
- Basic usage:
37
+ ## Basic usage (Express + Axios)
19
38
 
20
39
  ```js
21
- const Logger = require("@nm-logger/logger");
40
+ const express = require("express");
22
41
  const axios = require("axios");
42
+ const Logger = require("@nm-logger/logger");
43
+
44
+ const app = express();
45
+
46
+ const nm_logger = new Logger(
47
+ {
48
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
49
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
50
+ region: "us-east-1",
51
+ bucket: "project",
52
+ },
53
+ {
54
+ baseDir: "/var/log/nm-logger",
55
+ maskFields: ["aadhaar", "pan"],
56
+
57
+ // Optional metadata for logs
58
+ projectVersion: "Project-1.0.0",
59
+
60
+ // Background S3 uploader (set to false if you prefer cron; see below)
61
+ enableDailyWatcher: true,
62
+
63
+ // Interval for background S3 uploads when enableDailyWatcher = true
64
+ uploadIntervalMs: 60 * 1000, // 1 minute
65
+
66
+ // How many characters of external response to keep (after masking)
67
+ maxResponseLength: 4000,
68
+
69
+ // If true (default), include external response body only for error-like calls
70
+ logExternalResponseOnError: true,
71
+
72
+ // Control logging of successful external calls
73
+ // - true (default): log all external calls
74
+ // - false: log only error-like calls (4xx/5xx or externalErrorDetector = true)
75
+ logExternalSuccess: true,
76
+
77
+ // Optional semantic error detector for 2xx/3xx external responses
78
+ // Example below in "External error-only logging"
79
+ externalErrorDetector: null,
80
+ }
81
+ );
82
+
83
+ // Express middlewares
84
+ app.use(express.json());
85
+ app.use(nm_logger.requestLoggerMiddleware());
86
+ app.use("/api", require("./routes"));
87
+ app.use(nm_logger.expressMiddleware());
88
+
89
+ // Attach Axios interceptor
90
+ nm_logger.attachAxiosLogger(axios);
23
91
 
92
+ app.listen(3000, () => {
93
+ console.log("Server listening on 3000");
94
+ });
95
+ ```
96
+
97
+ ### Folder permissions
98
+
99
+ Make sure the base directory exists and is writable by your Node process:
100
+
101
+ ```bash
102
+ mkdir -p /var/log/nm-logger
103
+ chmod -R 777 /var/log/nm-logger
104
+ ```
105
+
106
+ ## External error-only logging (recommended for high load)
107
+
108
+ If your external API returns HTTP 200 but the **body** contains error info, you can:
109
+
110
+ - Skip logging successful calls completely
111
+ - Still log error-like responses with full (masked + truncated) body
112
+
113
+ Example:
114
+
115
+ ```js
24
116
  const nm_logger = new Logger(
25
117
  {
26
118
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
27
119
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
28
120
  region: "us-east-1",
29
- bucket: "bucket-name"
121
+ bucket: "project",
30
122
  },
31
123
  {
32
- baseDir: "/home/logs",
33
- uploadIntervalMs: 60_000,
34
- maskFields: ["aadhaar", "pan"]
124
+ baseDir: "/var/log/nm-logger",
125
+ maskFields: ["aadhaar", "pan"],
126
+
127
+ enableDailyWatcher: false, // use cron for S3 (see next section)
128
+ logExternalSuccess: false, // do NOT log success responses
129
+
130
+ externalErrorDetector: ({ response }) => {
131
+ try {
132
+ // Adjust this according to your external API response format
133
+ if (response && response.success === false) return true;
134
+ if (response && typeof response.status === "string" &&
135
+ response.status.toLowerCase() === "error") return true;
136
+ if (response && (response.error || response.error_code)) return true;
137
+ } catch (e) {
138
+ return false;
139
+ }
140
+ return false;
141
+ },
35
142
  }
36
143
  );
144
+ ```
145
+
146
+ With this config:
147
+
148
+ - Only external calls that look like **errors** are logged (4xx/5xx or `externalErrorDetector` = true).
149
+ - For these, the masked + truncated `response` body is stored in JSON.
150
+
151
+ ## S3 upload modes
37
152
 
38
- Folder permission
153
+ There are two ways to sync daily logs to S3.
39
154
 
40
- - mkdir -p /home/logs
41
- - chmod -R 777 /home/logs
155
+ ### Mode 1: Background timer (default)
156
+
157
+ If `enableDailyWatcher: true` (or omitted), the logger will:
158
+
159
+ - Start an internal `setInterval` in the same Node process
160
+ - Every `uploadIntervalMs`:
161
+ - Read local daily log JSONs
162
+ - Merge them with S3 files
163
+ - Upload merged logs back to S3
164
+ - Clean up previous day's local folder
165
+
166
+ This is simple but adds some extra work inside your API process.
167
+
168
+ ### Mode 2: Cron-based upload (recommended for production)
169
+
170
+ To keep your API process as light as possible, you can:
171
+
172
+ 1. **Disable the internal watcher in your API**
173
+ 2. **Use a separate cron script** to upload logs to S3
174
+
175
+ #### 1. API server: disable watcher
176
+
177
+ ```js
178
+ const nm_logger = new Logger(
179
+ {
180
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
181
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
182
+ region: "us-east-1",
183
+ bucket: "project",
184
+ },
185
+ {
186
+ baseDir: "/var/log/nm-logger",
187
+ maskFields: ["aadhaar", "pan"],
188
+ enableDailyWatcher: false, // 🔴 no setInterval in API process
189
+ }
190
+ );
42
191
 
43
192
  app.use(nm_logger.requestLoggerMiddleware());
44
193
  app.use("/api", routes);
45
194
  app.use(nm_logger.expressMiddleware());
46
195
  nm_logger.attachAxiosLogger(axios);
196
+ ```
197
+
198
+ This way, the API only writes local JSON files.
199
+
200
+ ### Mode 2b: node-cron inside Node (no system crontab)
201
+
202
+ If you are already using [`node-cron`](https://www.npmjs.com/package/node-cron) in your Node project, you can avoid Linux `crontab` entirely and schedule uploads from your app code.
203
+
204
+ 1. Disable the internal watcher in your API (same as before):
205
+
206
+ ```js
207
+ const nm_logger = new Logger(
208
+ {
209
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
210
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
211
+ region: "us-east-1",
212
+ bucket: "project",
213
+ },
214
+ {
215
+ baseDir: "/var/log/nm-logger",
216
+ maskFields: ["aadhaar", "pan"],
217
+ enableDailyWatcher: false, // no setInterval
218
+ }
219
+ );
220
+ ```
221
+
222
+ 2. Use `node-cron` in your Node app to trigger uploads:
223
+
224
+ ```js
225
+ const cron = require("node-cron");
226
+ const Logger = require("@nm-logger/logger");
227
+
228
+ const nm_logger = new Logger(
229
+ {
230
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
231
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
232
+ region: "us-east-1",
233
+ bucket: "project",
234
+ },
235
+ {
236
+ baseDir: "/var/log/nm-logger",
237
+ maskFields: ["aadhaar", "pan"],
238
+ enableDailyWatcher: false,
239
+ }
240
+ );
241
+
242
+ // Every 5 minutes
243
+ cron.schedule("*/5 * * * *", async () => {
244
+ try {
245
+ await nm_logger.uploadDailyLogsOnce();
246
+ console.log("[nm-logger] ✅ Daily logs uploaded to S3 (node-cron)");
247
+ } catch (err) {
248
+ console.error("[nm-logger] ❌ Upload failed (node-cron):", err);
249
+ }
250
+ });
251
+ ```
252
+
253
+ This way you:
254
+ - Do **not** depend on OS-level `crontab`.
255
+ - Still avoid `setInterval`-based uploads inside the main request flow.
256
+ - Keep all scheduling logic inside Node where you are already comfortable.
257
+
258
+ #### 3. System Crontab entry (optional, Linux)
259
+
260
+ If you prefer OS-level cron instead of `node-cron`, you can still use a separate script as before.
261
+
262
+ Create a separate script (for example in your backend root):
263
+
264
+ ```js
265
+ // upload-logs.js
266
+ require("dotenv").config();
267
+ const Logger = require("@nm-logger/logger");
268
+
269
+ async function main() {
270
+ const logger = new Logger(
271
+ {
272
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
273
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
274
+ region: "us-east-1",
275
+ bucket: "project",
276
+ },
277
+ {
278
+ baseDir: "/var/log/nm-logger",
279
+ maskFields: ["aadhaar", "pan"],
280
+ enableDailyWatcher: false, // no internal timers here either
281
+ }
282
+ );
283
+
284
+ try {
285
+ await logger.uploadDailyLogsOnce();
286
+ console.log("[nm-logger] ✅ Daily logs uploaded to S3");
287
+ } catch (err) {
288
+ console.error("[nm-logger] ❌ Failed to upload daily logs:", err);
289
+ process.exitCode = 1;
290
+ }
291
+ }
292
+
293
+ main();
294
+ ```
295
+
296
+ On your server (Linux):
297
+
298
+ ```bash
299
+ crontab -e
300
+ ```
301
+
302
+ Add a line like:
303
+
304
+ ```cron
305
+ */5 * * * * /usr/bin/node /path/to/upload-logs.js >> /var/log/nm-logger-cron.log 2>&1
306
+ ```
307
+
308
+ - This will **not** be created automatically by the package.
309
+ - You must add it manually using `crontab -e`.
310
+ - Make sure the `node` path (`/usr/bin/node`) and script path (`/path/to/upload-logs.js`) are correct for your server.
311
+
312
+ ## Notes
47
313
 
48
- ```
314
+ - The logger does **not** create or manage cron jobs automatically; you must configure `node-cron` or `crontab` yourself if you choose cron mode.
315
+ - For lower CPU and disk usage on busy servers:
316
+ - Prefer `logExternalSuccess: false` + `externalErrorDetector` to log only problematic external calls.
317
+ - Use cron-based upload (Mode 2 / Mode 2b) so S3 sync runs in a separate short-lived context.
package/package.json CHANGED
@@ -1 +1,12 @@
1
- {"name":"@nm-logger/logger","version":"1.2.6","description":"Express JSON logger with daily success/error/external logs, S3 append uploads, and configurable baseDir.","main":"index.js","types":"index.d.ts","license":"MIT","dependencies":{"@aws-sdk/client-s3":"^3.600.0","fs-extra":"^11.1.1"}}
1
+ {
2
+ "name": "@nm-logger/logger",
3
+ "version": "1.2.7",
4
+ "description": "Express JSON logger with daily success/error/external logs, S3 append uploads, and configurable baseDir.",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "license": "MIT",
8
+ "dependencies": {
9
+ "@aws-sdk/client-s3": "^3.600.0",
10
+ "fs-extra": "^11.1.1"
11
+ }
12
+ }
@@ -1,2 +1,85 @@
1
- const fs=require("fs-extra");const path=require("path");const{getDatePath}=require("./utils");const MAP={success:"daily_logs_success.json",error:"daily_logs_error.json",external:"daily_logs_external.json"};
2
- class DailyWatcher{constructor(baseDir,q,s3,opt={}){this.baseDir=baseDir;this.q=q;this.s3=s3;this.ms=opt.uploadIntervalMs||60000;this.currentDate=getDatePath();this.start();}async cleanup(){const{Y,M,D}=getDatePath();const p=this.currentDate;if(p.Y===Y&&p.M===M&&p.D===D)return;const dir=path.join(this.baseDir,`${p.Y}/${p.M}/${p.D}`);try{if(await fs.pathExists(dir))await fs.remove(dir);}catch(_){ }this.currentDate={Y,M,D};}start(){setInterval(()=>this.tick().catch(()=>{}),this.ms);}async tick(){await this.cleanup();const{Y,M,D}=getDatePath();for(const[cat,fn]of Object.entries(MAP)){const file=path.join(this.baseDir,`${Y}/${M}/${D}`,fn);const key=`${Y}/${M}/${D}/${fn}`;if(!(await fs.pathExists(file)))continue;this.q.add(async()=>{try{const localTxt=await fs.readFile(file,"utf8");let local={logs:[]};if(localTxt.trim())local=JSON.parse(localTxt);let existing={logs:[]};const s3Txt=await this.s3.getObject(key).catch(()=>null);if(s3Txt&&s3Txt.trim())existing=JSON.parse(s3Txt);const merged={logs:[...existing.logs,...local.logs]};await this.s3.putObject(key,JSON.stringify(merged,null,2));}catch(_){}});}}}module.exports=DailyWatcher;
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const { getDatePath } = require("./utils");
4
+
5
+ const MAP = {
6
+ success: "daily_logs_success.json",
7
+ error: "daily_logs_error.json",
8
+ external: "daily_logs_external.json",
9
+ db_error: "daily_logs_db_error.json",
10
+ };
11
+
12
+ class DailyWatcher {
13
+ constructor(baseDir, q, s3, opt = {}) {
14
+ this.baseDir = baseDir;
15
+ this.q = q;
16
+ this.s3 = s3;
17
+ this.uploadIntervalMs = opt.uploadIntervalMs || 60_000;
18
+ this.currentDate = getDatePath();
19
+ this.disableTimer = !!opt.disableTimer;
20
+
21
+ if (!this.disableTimer) {
22
+ this.start();
23
+ }
24
+ }
25
+
26
+ async cleanup() {
27
+ const { Y, M, D } = getDatePath();
28
+ const p = this.currentDate;
29
+
30
+ if (p && (p.Y !== Y || p.M !== M || p.D !== D)) {
31
+ const dir = path.join(this.baseDir, `${p.Y}/${p.M}/${p.D}`);
32
+ try {
33
+ if (await fs.pathExists(dir)) {
34
+ await fs.remove(dir);
35
+ }
36
+ } catch (_) {}
37
+ this.currentDate = { Y, M, D };
38
+ }
39
+ }
40
+
41
+ start() {
42
+ setInterval(() => {
43
+ this.tick().catch(() => {});
44
+ }, this.uploadIntervalMs);
45
+ }
46
+
47
+ async tick() {
48
+ await this.cleanup();
49
+
50
+ const { Y, M, D } = getDatePath();
51
+
52
+ for (const [cat, fileName] of Object.entries(MAP)) {
53
+ const file = path.join(this.baseDir, `${Y}/${M}/${D}`, fileName);
54
+ const key = `${Y}/${M}/${D}/${fileName}`;
55
+
56
+ if (!(await fs.pathExists(file))) continue;
57
+
58
+ this.q.add(async () => {
59
+ try {
60
+ const localTxt = await fs.readFile(file, "utf8");
61
+ let local = { logs: [] };
62
+ if (localTxt && localTxt.trim()) {
63
+ local = JSON.parse(localTxt);
64
+ }
65
+
66
+ let existing = { logs: [] };
67
+ try {
68
+ const s3Txt = await this.s3.getObject(key);
69
+ if (s3Txt && s3Txt.trim()) {
70
+ existing = JSON.parse(s3Txt);
71
+ }
72
+ } catch (_) {}
73
+
74
+ const merged = {
75
+ logs: [...(existing.logs || []), ...(local.logs || [])],
76
+ };
77
+
78
+ await this.s3.putObject(key, JSON.stringify(merged, null, 2));
79
+ } catch (_) {}
80
+ });
81
+ }
82
+ }
83
+ }
84
+
85
+ module.exports = DailyWatcher;
package/src/LogWriter.js CHANGED
@@ -38,17 +38,44 @@ class LogWriter {
38
38
  } catch (_) {}
39
39
  }
40
40
 
41
- wrapper.logs.push({
41
+ const entry = {
42
42
  url: log.url || "",
43
43
  body: log.body || "",
44
44
  params: log.params || "",
45
- response: log.response || "", // 👈 NEW: store external response if present
45
+ response: log.response || "",
46
46
  type: log.type || "",
47
47
  method: log.method || "",
48
48
  error: log.error || "",
49
49
  status_code: log.status_code || "",
50
+ user_agent: log.user_agent || "",
51
+ project_version: log.project_version || "",
50
52
  date: log.date || formatDate(new Date()),
51
- });
53
+ };
54
+
55
+ const logs = wrapper.logs;
56
+
57
+ if (logs.length > 0) {
58
+ const last = logs[logs.length - 1];
59
+
60
+ if (
61
+ last.url === entry.url &&
62
+ last.method === entry.method &&
63
+ last.type === entry.type &&
64
+ String(last.status_code || "") === String(entry.status_code || "") &&
65
+ String(last.user_agent || "") === String(entry.user_agent || "") &&
66
+ String(last.project_version || "") === String(entry.project_version || "")
67
+ ) {
68
+ // Same URL/method/type/status/user_agent/version → compact into one entry
69
+ last.count = (last.count || 1) + 1;
70
+ last.date = entry.date;
71
+ } else {
72
+ entry.count = entry.count || 1;
73
+ logs.push(entry);
74
+ }
75
+ } else {
76
+ entry.count = entry.count || 1;
77
+ logs.push(entry);
78
+ }
52
79
 
53
80
  await fs.writeFile(filePath, JSON.stringify(wrapper, null, 2), "utf8");
54
81
  return filePath;
package/src/Logger.js CHANGED
@@ -40,9 +40,48 @@ class Logger {
40
40
  String(x).toLowerCase()
41
41
  );
42
42
 
43
- new DailyWatcher(this.baseDir, this.queue, this.s3, {
44
- uploadIntervalMs: opt.uploadIntervalMs || 60_000,
45
- });
43
+ // Optional: application/version metadata
44
+ this.projectVersion = opt.projectVersion || opt.version || "";
45
+
46
+ // Limit size of stored external responses to avoid huge log files
47
+ this.maxResponseLength =
48
+ typeof opt.maxResponseLength === "number" ? opt.maxResponseLength : 4000;
49
+
50
+ // Only include external API responses for errors by default
51
+ this.logExternalResponseOnError =
52
+ opt.logExternalResponseOnError !== false; // default: true
53
+
54
+ // Control logging of successful external calls
55
+ this.logExternalSuccess =
56
+ opt.logExternalSuccess !== false; // default: true (for backward compatibility)
57
+
58
+ // Optional semantic error detector for 2xx/3xx responses
59
+ this.externalErrorDetector =
60
+ typeof opt.externalErrorDetector === "function"
61
+ ? opt.externalErrorDetector
62
+ : null;
63
+
64
+ // Control background S3 uploader (DailyWatcher)
65
+ this.enableDailyWatcher = opt.enableDailyWatcher !== false;
66
+
67
+ if (this.enableDailyWatcher) {
68
+ this.dailyWatcher = new DailyWatcher(this.baseDir, this.queue, this.s3, {
69
+ uploadIntervalMs: opt.uploadIntervalMs || 60_000,
70
+ disableTimer: false,
71
+ });
72
+ }
73
+ }
74
+
75
+ async uploadDailyLogsOnce() {
76
+ if (!this._manualDailyWatcher) {
77
+ this._manualDailyWatcher = new DailyWatcher(
78
+ this.baseDir,
79
+ this.queue,
80
+ this.s3,
81
+ { disableTimer: true }
82
+ );
83
+ }
84
+ await this._manualDailyWatcher.tick();
46
85
  }
47
86
 
48
87
  // Mask helper
@@ -62,6 +101,11 @@ class Logger {
62
101
  const maskedBody = this.mask(body);
63
102
  const maskedParams = this.mask(params);
64
103
  const method = (req && req.method ? req.method : "").toUpperCase();
104
+ const userAgent =
105
+ (req &&
106
+ req.headers &&
107
+ (req.headers["user-agent"] || req.headers["User-Agent"])) ||
108
+ "";
65
109
 
66
110
  const emp =
67
111
  employee_id ||
@@ -81,6 +125,8 @@ class Logger {
81
125
  employee_id: emp,
82
126
  status_code:
83
127
  typeof statusCode === "number" ? statusCode : statusCode || "",
128
+ user_agent: userAgent,
129
+ project_version: this.projectVersion || "",
84
130
  };
85
131
  }
86
132
 
@@ -109,22 +155,70 @@ class Logger {
109
155
  data,
110
156
  params,
111
157
  error,
112
- response, // 👈 store external API response
158
+ response,
113
159
  statusCode,
160
+ userAgent,
114
161
  }) {
115
162
  const maskedBody = this.mask(data || {});
116
163
  const maskedParams = this.mask(params || {});
117
- const maskedResponse = this.mask(response || {});
164
+
165
+ const codeStr = statusCode ? String(statusCode) : "";
166
+ const isErrorStatus =
167
+ codeStr.startsWith("4") || codeStr.startsWith("5");
168
+
169
+ let semanticError = false;
170
+ if (this.externalErrorDetector) {
171
+ try {
172
+ semanticError = !!this.externalErrorDetector({
173
+ url,
174
+ method,
175
+ statusCode,
176
+ response,
177
+ error,
178
+ });
179
+ } catch (_) {
180
+ semanticError = false;
181
+ }
182
+ }
183
+
184
+ const hasErrorFlag = !!(error || isErrorStatus || semanticError);
185
+
186
+ // If logExternalSuccess is false, skip non-error calls completely
187
+ if (!this.logExternalSuccess && !hasErrorFlag) {
188
+ return;
189
+ }
190
+
191
+ let respStr = "";
192
+ const includeResponse =
193
+ this.logExternalResponseOnError && hasErrorFlag;
194
+
195
+ if (includeResponse && typeof response !== "undefined") {
196
+ try {
197
+ const maskedResponse = this.mask(response || {});
198
+ respStr = JSON.stringify(maskedResponse || {});
199
+ } catch (_) {
200
+ respStr = "";
201
+ }
202
+
203
+ if (this.maxResponseLength && respStr.length > this.maxResponseLength) {
204
+ respStr =
205
+ respStr.slice(0, this.maxResponseLength) +
206
+ `... [truncated ${respStr.length - this.maxResponseLength} chars]`;
207
+ }
208
+ }
118
209
 
119
210
  const log = {
120
211
  url: url || "",
121
212
  body: JSON.stringify(maskedBody || {}),
122
213
  params: JSON.stringify(maskedParams || {}),
123
- response: JSON.stringify(maskedResponse || {}),
214
+ response: respStr,
124
215
  type: "external_api",
125
216
  method: (method || "").toUpperCase(),
126
217
  error: error || "",
127
- status_code: typeof statusCode === "number" ? statusCode : (statusCode || ""),
218
+ status_code:
219
+ typeof statusCode === "number" ? statusCode : statusCode || "",
220
+ user_agent: userAgent || "",
221
+ project_version: this.projectVersion || "",
128
222
  };
129
223
 
130
224
  await this.logWriter.writeLog(log, "external");
@@ -159,91 +253,7 @@ class Logger {
159
253
  };
160
254
  }
161
255
 
162
-
163
- // ---- Summary helper: filter logs by status_code ----
164
- /**
165
- * Get summary of logs filtered by HTTP status code.
166
- *
167
- * @param {Object} opt
168
- * @param {string} [opt.date] - Date string in YYYY-MM-DD format (defaults to today).
169
- * @param {string} [opt.category] - One of "success", "error", "external", "db_error".
170
- * @param {number|string} [opt.statusCodePrefix] - If provided, matches codes starting with this (e.g. 5 => 5xx).
171
- * @param {Array<number|string>} [opt.statusCodes] - If provided, matches any of these exact codes.
172
- *
173
- * @returns {Promise<{date:string,category:string,total_logs:number,matched_logs:number,by_status:Object,logs:Array}>}
174
- */
175
- async getStatusSummary(opt = {}) {
176
- const category = opt.category || "error";
177
- const dateStr = opt.date;
178
-
179
- // Resolve date parts similar to LogWriter.getFilePath
180
- const d = dateStr ? new Date(dateStr) : new Date();
181
- const Y = String(d.getFullYear());
182
- const M = String(d.getMonth() + 1).padStart(2, "0");
183
- const D = String(d.getDate()).padStart(2, "0");
184
-
185
- const MAP = {
186
- success: "daily_logs_success.json",
187
- error: "daily_logs_error.json",
188
- external: "daily_logs_external.json",
189
- db_error: "daily_logs_db_error.json",
190
- };
191
-
192
- const fileName = MAP[category] || MAP.success;
193
- const filePath = path.join(this.baseDir, `${Y}/${M}/${D}/${fileName}`);
194
-
195
- const result = {
196
- date: `${Y}-${M}-${D}`,
197
- category,
198
- total_logs: 0,
199
- matched_logs: 0,
200
- by_status: {},
201
- logs: [],
202
- };
203
-
204
- if (!(await fs.pathExists(filePath))) {
205
- return result;
206
- }
207
-
208
- try {
209
- const raw = await fs.readFile(filePath, "utf8");
210
- if (!raw.trim()) return result;
211
-
212
- const parsed = JSON.parse(raw);
213
- const logs = Array.isArray(parsed.logs) ? parsed.logs : [];
214
- result.total_logs = logs.length;
215
-
216
- let filtered = logs;
217
-
218
- if (Array.isArray(opt.statusCodes) && opt.statusCodes.length) {
219
- const allowed = opt.statusCodes.map((c) => String(c));
220
- filtered = filtered.filter((log) =>
221
- allowed.includes(String(log.status_code || ""))
222
- );
223
- } else if (opt.statusCodePrefix !== undefined && opt.statusCodePrefix !== null) {
224
- const prefix = String(opt.statusCodePrefix);
225
- filtered = filtered.filter((log) =>
226
- String(log.status_code || "").startsWith(prefix)
227
- );
228
- }
229
-
230
- result.matched_logs = filtered.length;
231
-
232
- const byStatus = {};
233
- for (const log of filtered) {
234
- const code = String(log.status_code || "");
235
- if (!code) continue;
236
- byStatus[code] = (byStatus[code] || 0) + 1;
237
- }
238
- result.by_status = byStatus;
239
- result.logs = filtered;
240
-
241
- return result;
242
- } catch (_) {
243
- return result;
244
- }
245
- }
246
- // ---- Axios interceptor ----
256
+ // ---- Axios interceptor ----
247
257
  attachAxiosLogger(axiosInstance) {
248
258
  if (!axiosInstance || !axiosInstance.interceptors) return;
249
259
 
@@ -252,14 +262,20 @@ class Logger {
252
262
  (response) => {
253
263
  try {
254
264
  const cfg = response.config || {};
265
+ const statusCode = response && response.status;
266
+ const headers = (cfg && cfg.headers) || {};
267
+ const userAgent =
268
+ headers["User-Agent"] || headers["user-agent"] || "";
269
+
255
270
  this.logExternalApi({
256
271
  url: cfg.url,
257
272
  method: cfg.method,
258
273
  data: cfg.data,
259
274
  params: cfg.params,
260
- response: response.data, // 👈 log external API response body
275
+ response: response.data,
261
276
  error: null,
262
- statusCode: response && response.status,
277
+ statusCode,
278
+ userAgent,
263
279
  }).catch(() => {});
264
280
  } catch (_) {}
265
281
 
@@ -271,17 +287,20 @@ class Logger {
271
287
  try {
272
288
  const cfg = (error && error.config) || {};
273
289
  const res = error && error.response;
274
-
275
290
  const statusCode = res && res.status;
291
+ const headers = (cfg && cfg.headers) || {};
292
+ const userAgent =
293
+ headers["User-Agent"] || headers["user-agent"] || "";
276
294
 
277
295
  this.logExternalApi({
278
296
  url: cfg && cfg.url,
279
297
  method: cfg && cfg.method,
280
298
  data: cfg && cfg.data,
281
299
  params: cfg && cfg.params,
282
- response: res && res.data, // 👈 log response even on error
300
+ response: res && res.data,
283
301
  error: error && error.message,
284
302
  statusCode,
303
+ userAgent,
285
304
  }).catch(() => {});
286
305
  } catch (_) {}
287
306
 
@@ -289,25 +308,6 @@ class Logger {
289
308
  }
290
309
  );
291
310
  }
292
-
293
- // ---- Database error logging ----
294
- async logDbError(error, query = "", params = []) {
295
- try {
296
- await this.logWriter.writeLog(
297
- {
298
- url: "",
299
- body: JSON.stringify({ query, params }),
300
- params: JSON.stringify(params || []),
301
- response: "",
302
- type: "db_error",
303
- method: "",
304
- error: error && (error.message || String(error)),
305
- date: new Date().toISOString(),
306
- },
307
- "db_error"
308
- );
309
- } catch (_) {}
310
- }
311
311
  }
312
312
 
313
313
  module.exports = Logger;