w-syslog 1.0.7 → 1.0.9

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/docs/WSyslog.html CHANGED
@@ -80,7 +80,7 @@
80
80
 
81
81
  <dt class="tag-source">Source:</dt>
82
82
  <dd class="tag-source"><ul class="dummy"><li>
83
- <a href="WSyslog.mjs.html">WSyslog.mjs</a>, <a href="WSyslog.mjs.html#line48">line 48</a>
83
+ <a href="WSyslog.mjs.html">WSyslog.mjs</a>, <a href="WSyslog.mjs.html#line56">line 56</a>
84
84
  </li></ul></dd>
85
85
 
86
86
 
@@ -127,7 +127,7 @@
127
127
 
128
128
  <h5 class="h5-examples">Example</h5>
129
129
 
130
- <pre class="prettyprint"><code>import fs from 'fs'
130
+ <pre class="prettyprint"><code>import fs from 'fs'
131
131
 
132
132
 
133
133
 
@@ -301,6 +301,84 @@
301
301
  </tr>
302
302
 
303
303
 
304
+
305
+ <tr>
306
+
307
+ <td class="name"><code>timeZone</code></td>
308
+
309
+
310
+ <td class="type">
311
+
312
+
313
+ <span class="param-type">String</span>
314
+
315
+
316
+
317
+
318
+ </td>
319
+
320
+
321
+ <td class="attributes">
322
+
323
+ &lt;optional><br>
324
+
325
+
326
+
327
+
328
+
329
+ </td>
330
+
331
+
332
+
333
+ <td class="default">
334
+
335
+ <code>null</code>
336
+
337
+ </td>
338
+
339
+
340
+ <td class="description last"><p>輸入時區字串(IANA格式如'Asia/Taipei'、'UTC'),用於決定切檔邊界與檔名,預設null代表使用系統時區</p></td>
341
+ </tr>
342
+
343
+
344
+
345
+ <tr>
346
+
347
+ <td class="name"><code>numKeep</code></td>
348
+
349
+
350
+ <td class="type">
351
+
352
+
353
+ <span class="param-type">Number</span>
354
+
355
+
356
+
357
+
358
+ </td>
359
+
360
+
361
+ <td class="attributes">
362
+
363
+ &lt;optional><br>
364
+
365
+
366
+
367
+
368
+
369
+ </td>
370
+
371
+
372
+
373
+ <td class="default">
374
+
375
+ </td>
376
+
377
+
378
+ <td class="description last"><p>輸入保留log檔案數量整數,超出最舊檔將被自動刪除,預設於day模式為365、hr模式為365*24(約等同保留一年)</p></td>
379
+ </tr>
380
+
381
+
304
382
  </tbody>
305
383
  </table>
306
384
 
@@ -330,7 +408,7 @@
330
408
 
331
409
 
332
410
  <div class="param-desc">
333
- <p>回傳log物件,提供info、warn、error紀錄函數</p>
411
+ <p>回傳log物件,提供fatalerror、warn、info、debug、trace紀錄函數,cleanLogs手動清理函數,及clear顯式關閉函數(回傳Promise,將殘餘log全部寫出後終止transport worker並停止清理計時器,附逾時保險預設60000ms可由opt.timeLimit調整,冪等;clear之後不可再呼叫任何log方法)</p>
334
412
  </div>
335
413
 
336
414
 
@@ -389,7 +467,7 @@
389
467
  <br class="clear">
390
468
 
391
469
  <footer>
392
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.4</a> on Mon Aug 25 2025 14:33:18 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
470
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Jul 17 2026 23:54:28 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
393
471
  </footer>
394
472
 
395
473
  <script>prettyPrint();</script>
@@ -45,10 +45,16 @@
45
45
 
46
46
  <section>
47
47
  <article>
48
- <pre class="prettyprint source linenums"><code>import path from 'path'
48
+ <pre class="prettyprint source linenums"><code>import fs from 'fs'
49
+ import path from 'path'
49
50
  import { fileURLToPath } from 'url'
50
51
  import get from 'lodash-es/get.js'
52
+ import evem from 'wsemi/src/evem.mjs'
53
+ import genPm from 'wsemi/src/genPm.mjs'
51
54
  import isestr from 'wsemi/src/isestr.mjs'
55
+ import ispint from 'wsemi/src/ispint.mjs'
56
+ import cint from 'wsemi/src/cint.mjs'
57
+ import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
52
58
  import pino from 'pino'
53
59
 
54
60
 
@@ -64,7 +70,9 @@ let __dirname = path.dirname(__filename)
64
70
  * @param {Object} [opt={}] 輸入設定物件,預設{}
65
71
  * @param {String} [opt.fdLog='./_logs'] 輸入儲存log資料夾字串,預設'./_logs'
66
72
  * @param {String} [opt.interval='day'] 輸入儲存log時分檔模式字串,可選'day'、'hr',分別代表每日或每時分檔,預設'day'
67
- * @returns {Object} 回傳log物件,提供infowarn、error紀錄函數
73
+ * @param {String} [opt.timeZone=null] 輸入時區字串(IANA格式如'Asia/Taipei''UTC'),用於決定切檔邊界與檔名,預設null代表使用系統時區
74
+ * @param {Number} [opt.numKeep] 輸入保留log檔案數量整數,超出最舊檔將被自動刪除,預設於day模式為365、hr模式為365*24(約等同保留一年)
75
+ * @returns {Object} 回傳log物件,提供fatal、error、warn、info、debug、trace紀錄函數,cleanLogs手動清理函數,及clear顯式關閉函數(回傳Promise,將殘餘log全部寫出後終止transport worker並停止清理計時器,附逾時保險預設60000ms可由opt.timeLimit調整,冪等;clear之後不可再呼叫任何log方法)
68
76
  * @example
69
77
  * import fs from 'fs'
70
78
  * import _ from 'lodash-es'
@@ -78,7 +86,7 @@ let __dirname = path.dirname(__filename)
78
86
  * log.warn({ event: 'monitor-memory', msg: 'usage-high', ratio: 85.4 })
79
87
  * log.error({ event: 'crash', msg: 'db connection', code: 500 })
80
88
  *
81
- * await w.delay(2000) //等待2秒讓pino能flush數據
89
+ * await log.clear() //顯式關閉, flush殘餘log並終止transport worker, 之後不可再呼叫log方法
82
90
  *
83
91
  * let vpfs = w.fsTreeFolder('./_logs')
84
92
  * // console.log('vpfs', vpfs)
@@ -94,6 +102,12 @@ let __dirname = path.dirname(__filename)
94
102
  */
95
103
  function WSyslog(opt = {}) {
96
104
 
105
+ //ev
106
+ let ev = evem()
107
+
108
+ //timeZone, 'Asia/Taipei'
109
+ let timeZone = get(opt, 'timeZone', null)
110
+
97
111
  //fdLog
98
112
  let fdLog = get(opt, 'fdLog')
99
113
  if (!isestr(fdLog)) {
@@ -106,6 +120,18 @@ function WSyslog(opt = {}) {
106
120
  interval = 'day'
107
121
  }
108
122
 
123
+ //numKeep
124
+ let numKeep = get(opt, 'numKeep')
125
+ if (!ispint(numKeep)) {
126
+ if (interval === 'day') {
127
+ numKeep = 365
128
+ }
129
+ else {
130
+ numKeep = 365 * 24
131
+ }
132
+ }
133
+ numKeep = cint(numKeep)
134
+
109
135
  // 建立 logger,使用自訂 transport
110
136
  let transport = pino.transport({
111
137
  targets: [
@@ -124,6 +150,7 @@ function WSyslog(opt = {}) {
124
150
  options: {
125
151
  interval,
126
152
  fdLog,
153
+ timeZone,
127
154
  },
128
155
 
129
156
  },
@@ -134,7 +161,127 @@ function WSyslog(opt = {}) {
134
161
  let log = pino(transport)
135
162
  // console.log('log', log)
136
163
 
137
- return log
164
+ //cleanLogs, 依numKeep保留最新N個檔, 超出的最舊檔刪除
165
+ let cleanLogs = () => {
166
+ try {
167
+ if (!fsIsFolder(fdLog)) {
168
+ return
169
+ }
170
+ //pattern, day模式為'YYYY-MM-DD.log', hr模式為'YYYY-MM-DDTHH.log'
171
+ let pattern = (interval === 'hr')
172
+ ? /^\d{4}-\d{2}-\d{2}T\d{2}\.log$/
173
+ : /^\d{4}-\d{2}-\d{2}\.log$/
174
+ let files = fs.readdirSync(fdLog).filter((fn) => pattern.test(fn))
175
+ //ISO日期格式字典序等同時間序, 排序後最舊在前
176
+ files.sort()
177
+ let nDel = files.length - numKeep
178
+ if (nDel &lt;= 0) {
179
+ return
180
+ }
181
+ let toDelete = files.slice(0, nDel)
182
+ for (let fn of toDelete) {
183
+ let fp = path.join(fdLog, fn)
184
+ try {
185
+ fs.unlinkSync(fp)
186
+ ev.emit('delete', { fp, fn })
187
+ }
188
+ catch (err) {
189
+ //容忍其他process同時刪除或檔案被佔用之情況
190
+ }
191
+ }
192
+ }
193
+ catch (err) {
194
+ console.error('WSyslog cleanLogs error:', err)
195
+ }
196
+ }
197
+
198
+ //啟動時立刻清理一次, 確保短命腳本也能執行清理
199
+ cleanLogs()
200
+
201
+ //之後每小時清理一次, 足以跟上day/hr切檔節奏
202
+ let t = setInterval(cleanLogs, 60 * 60 * 1000)
203
+ //unref不阻塞process結束
204
+ t.unref()
205
+
206
+ //clear, 顯式關閉: flush殘餘log -> 終止transport worker -> 停止cleanLogs計時器, 附逾時保險保證必定resolve
207
+ //clear之後不可再呼叫任何log方法(pino會非同步丟the worker has exited)
208
+ let pm = null
209
+ let clear = (optClear = {}) => {
210
+ if (pm !== null) {
211
+ return pm //冪等
212
+ }
213
+ let timeLimit = get(optClear, 'timeLimit')
214
+ if (!ispint(timeLimit)) {
215
+ timeLimit = 60 * 1000
216
+ }
217
+ pm = genPm()
218
+ let done = false
219
+ let fin = () => {
220
+ if (!done) {
221
+ done = true
222
+ clearInterval(t) //併停cleanLogs計時器
223
+ pm.resolve()
224
+ }
225
+ }
226
+ let tt = setTimeout(() => {
227
+ try {
228
+ transport.worker.terminate() //保險: 逾時直接終結worker
229
+ }
230
+ catch (err) {}
231
+ fin()
232
+ }, timeLimit)
233
+ try {
234
+ log.flush((err) => {
235
+ //worker已亡或transport已關閉時'close'事件可能已發過, 直接收尾避免空等逾時
236
+ if (err || transport.closed) {
237
+ clearTimeout(tt)
238
+ fin()
239
+ return
240
+ }
241
+ //'close'事件於worker真正退出(MessagePort釋放)後才發出, 等'finish'不足以保證
242
+ transport.once('close', () => {
243
+ clearTimeout(tt)
244
+ fin()
245
+ })
246
+ transport.end()
247
+ })
248
+ }
249
+ catch (err) {
250
+ clearTimeout(tt)
251
+ fin()
252
+ }
253
+ return pm
254
+ }
255
+
256
+ //wrap, 包裝pino各等級method, 於寫log前先觸發'log'事件, 使外部可監聽進行console輸出或轉送其他記錄器
257
+ // fatal: 60
258
+ // error: 50
259
+ // warn: 40
260
+ // info: 30
261
+ // debug: 20
262
+ // trace: 10
263
+ let wrap = (level) => {
264
+ let fn = log[level].bind(log)
265
+ return (...args) => {
266
+ try {
267
+ ev.emit(level, { args })
268
+ }
269
+ catch (err) {
270
+ //容忍listener內部錯誤, 避免影響pino寫log
271
+ }
272
+ fn(...args)
273
+ }
274
+ }
275
+ ev.fatal = wrap('fatal')
276
+ ev.error = wrap('error')
277
+ ev.warn = wrap('warn')
278
+ ev.info = wrap('info')
279
+ ev.debug = wrap('debug')
280
+ ev.trace = wrap('trace')
281
+ ev.cleanLogs = cleanLogs
282
+ ev.clear = clear
283
+
284
+ return ev
138
285
  }
139
286
 
140
287
 
@@ -153,7 +300,7 @@ export default WSyslog
153
300
  <br class="clear">
154
301
 
155
302
  <footer>
156
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.4</a> on Mon Aug 25 2025 14:33:18 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
303
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Jul 17 2026 23:54:28 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
157
304
  </footer>
158
305
 
159
306
  <script>prettyPrint();</script>
package/docs/index.html CHANGED
@@ -71,7 +71,7 @@
71
71
  <br class="clear">
72
72
 
73
73
  <footer>
74
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.4</a> on Mon Aug 25 2025 14:33:18 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
74
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Jul 17 2026 23:54:28 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
75
75
  </footer>
76
76
 
77
77
  <script>prettyPrint();</script>
package/g.mjs CHANGED
@@ -15,7 +15,7 @@ log.info({ event: 'runner', msg: 'start' })
15
15
  log.warn({ event: 'monitor-memory', msg: 'usage-high', ratio: 85.4 })
16
16
  log.error({ event: 'crash', msg: 'db connection', code: 500 })
17
17
 
18
- await w.delay(2000) //等待2秒讓pino能flush數據
18
+ await log.clear() //顯式關閉, flush殘餘log並終止transport worker, 之後不可再呼叫log方法
19
19
 
20
20
  let vpfs = w.fsTreeFolder(fdLog)
21
21
  // console.log('vpfs', vpfs)
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "w-syslog",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "main": "dist/w-syslog.umd.js",
5
5
  "dependencies": {
6
- "lodash-es": "^4.17.21",
7
- "pino": "^9.9.0",
8
- "wsemi": "^1.8.19"
6
+ "lodash-es": "^4.18.1",
7
+ "pino": "^10.3.1",
8
+ "wsemi": "^1.8.65"
9
9
  },
10
10
  "devDependencies": {
11
- "w-package-tools": "^1.1.2"
11
+ "w-package-tools": "^1.1.10"
12
12
  },
13
13
  "scripts": {
14
14
  "test": "mocha --parallel --timeout 60000",
package/src/WSyslog.mjs CHANGED
@@ -1,7 +1,13 @@
1
+ import fs from 'fs'
1
2
  import path from 'path'
2
3
  import { fileURLToPath } from 'url'
3
4
  import get from 'lodash-es/get.js'
5
+ import evem from 'wsemi/src/evem.mjs'
6
+ import genPm from 'wsemi/src/genPm.mjs'
4
7
  import isestr from 'wsemi/src/isestr.mjs'
8
+ import ispint from 'wsemi/src/ispint.mjs'
9
+ import cint from 'wsemi/src/cint.mjs'
10
+ import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
5
11
  import pino from 'pino'
6
12
 
7
13
 
@@ -17,7 +23,9 @@ let __dirname = path.dirname(__filename)
17
23
  * @param {Object} [opt={}] 輸入設定物件,預設{}
18
24
  * @param {String} [opt.fdLog='./_logs'] 輸入儲存log資料夾字串,預設'./_logs'
19
25
  * @param {String} [opt.interval='day'] 輸入儲存log時分檔模式字串,可選'day'、'hr',分別代表每日或每時分檔,預設'day'
20
- * @returns {Object} 回傳log物件,提供infowarn、error紀錄函數
26
+ * @param {String} [opt.timeZone=null] 輸入時區字串(IANA格式如'Asia/Taipei''UTC'),用於決定切檔邊界與檔名,預設null代表使用系統時區
27
+ * @param {Number} [opt.numKeep] 輸入保留log檔案數量整數,超出最舊檔將被自動刪除,預設於day模式為365、hr模式為365*24(約等同保留一年)
28
+ * @returns {Object} 回傳log物件,提供fatal、error、warn、info、debug、trace紀錄函數,cleanLogs手動清理函數,及clear顯式關閉函數(回傳Promise,將殘餘log全部寫出後終止transport worker並停止清理計時器,附逾時保險預設60000ms可由opt.timeLimit調整,冪等;clear之後不可再呼叫任何log方法)
21
29
  * @example
22
30
  * import fs from 'fs'
23
31
  * import _ from 'lodash-es'
@@ -31,7 +39,7 @@ let __dirname = path.dirname(__filename)
31
39
  * log.warn({ event: 'monitor-memory', msg: 'usage-high', ratio: 85.4 })
32
40
  * log.error({ event: 'crash', msg: 'db connection', code: 500 })
33
41
  *
34
- * await w.delay(2000) //等待2秒讓pino能flush數據
42
+ * await log.clear() //顯式關閉, flush殘餘log並終止transport worker, 之後不可再呼叫log方法
35
43
  *
36
44
  * let vpfs = w.fsTreeFolder('./_logs')
37
45
  * // console.log('vpfs', vpfs)
@@ -47,6 +55,12 @@ let __dirname = path.dirname(__filename)
47
55
  */
48
56
  function WSyslog(opt = {}) {
49
57
 
58
+ //ev
59
+ let ev = evem()
60
+
61
+ //timeZone, 'Asia/Taipei'
62
+ let timeZone = get(opt, 'timeZone', null)
63
+
50
64
  //fdLog
51
65
  let fdLog = get(opt, 'fdLog')
52
66
  if (!isestr(fdLog)) {
@@ -59,6 +73,18 @@ function WSyslog(opt = {}) {
59
73
  interval = 'day'
60
74
  }
61
75
 
76
+ //numKeep
77
+ let numKeep = get(opt, 'numKeep')
78
+ if (!ispint(numKeep)) {
79
+ if (interval === 'day') {
80
+ numKeep = 365
81
+ }
82
+ else {
83
+ numKeep = 365 * 24
84
+ }
85
+ }
86
+ numKeep = cint(numKeep)
87
+
62
88
  // 建立 logger,使用自訂 transport
63
89
  let transport = pino.transport({
64
90
  targets: [
@@ -77,6 +103,7 @@ function WSyslog(opt = {}) {
77
103
  options: {
78
104
  interval,
79
105
  fdLog,
106
+ timeZone,
80
107
  },
81
108
 
82
109
  },
@@ -87,7 +114,127 @@ function WSyslog(opt = {}) {
87
114
  let log = pino(transport)
88
115
  // console.log('log', log)
89
116
 
90
- return log
117
+ //cleanLogs, 依numKeep保留最新N個檔, 超出的最舊檔刪除
118
+ let cleanLogs = () => {
119
+ try {
120
+ if (!fsIsFolder(fdLog)) {
121
+ return
122
+ }
123
+ //pattern, day模式為'YYYY-MM-DD.log', hr模式為'YYYY-MM-DDTHH.log'
124
+ let pattern = (interval === 'hr')
125
+ ? /^\d{4}-\d{2}-\d{2}T\d{2}\.log$/
126
+ : /^\d{4}-\d{2}-\d{2}\.log$/
127
+ let files = fs.readdirSync(fdLog).filter((fn) => pattern.test(fn))
128
+ //ISO日期格式字典序等同時間序, 排序後最舊在前
129
+ files.sort()
130
+ let nDel = files.length - numKeep
131
+ if (nDel <= 0) {
132
+ return
133
+ }
134
+ let toDelete = files.slice(0, nDel)
135
+ for (let fn of toDelete) {
136
+ let fp = path.join(fdLog, fn)
137
+ try {
138
+ fs.unlinkSync(fp)
139
+ ev.emit('delete', { fp, fn })
140
+ }
141
+ catch (err) {
142
+ //容忍其他process同時刪除或檔案被佔用之情況
143
+ }
144
+ }
145
+ }
146
+ catch (err) {
147
+ console.error('WSyslog cleanLogs error:', err)
148
+ }
149
+ }
150
+
151
+ //啟動時立刻清理一次, 確保短命腳本也能執行清理
152
+ cleanLogs()
153
+
154
+ //之後每小時清理一次, 足以跟上day/hr切檔節奏
155
+ let t = setInterval(cleanLogs, 60 * 60 * 1000)
156
+ //unref不阻塞process結束
157
+ t.unref()
158
+
159
+ //clear, 顯式關閉: flush殘餘log -> 終止transport worker -> 停止cleanLogs計時器, 附逾時保險保證必定resolve
160
+ //clear之後不可再呼叫任何log方法(pino會非同步丟the worker has exited)
161
+ let pm = null
162
+ let clear = (optClear = {}) => {
163
+ if (pm !== null) {
164
+ return pm //冪等
165
+ }
166
+ let timeLimit = get(optClear, 'timeLimit')
167
+ if (!ispint(timeLimit)) {
168
+ timeLimit = 60 * 1000
169
+ }
170
+ pm = genPm()
171
+ let done = false
172
+ let fin = () => {
173
+ if (!done) {
174
+ done = true
175
+ clearInterval(t) //併停cleanLogs計時器
176
+ pm.resolve()
177
+ }
178
+ }
179
+ let tt = setTimeout(() => {
180
+ try {
181
+ transport.worker.terminate() //保險: 逾時直接終結worker
182
+ }
183
+ catch (err) {}
184
+ fin()
185
+ }, timeLimit)
186
+ try {
187
+ log.flush((err) => {
188
+ //worker已亡或transport已關閉時'close'事件可能已發過, 直接收尾避免空等逾時
189
+ if (err || transport.closed) {
190
+ clearTimeout(tt)
191
+ fin()
192
+ return
193
+ }
194
+ //'close'事件於worker真正退出(MessagePort釋放)後才發出, 等'finish'不足以保證
195
+ transport.once('close', () => {
196
+ clearTimeout(tt)
197
+ fin()
198
+ })
199
+ transport.end()
200
+ })
201
+ }
202
+ catch (err) {
203
+ clearTimeout(tt)
204
+ fin()
205
+ }
206
+ return pm
207
+ }
208
+
209
+ //wrap, 包裝pino各等級method, 於寫log前先觸發'log'事件, 使外部可監聽進行console輸出或轉送其他記錄器
210
+ // fatal: 60
211
+ // error: 50
212
+ // warn: 40
213
+ // info: 30
214
+ // debug: 20
215
+ // trace: 10
216
+ let wrap = (level) => {
217
+ let fn = log[level].bind(log)
218
+ return (...args) => {
219
+ try {
220
+ ev.emit(level, { args })
221
+ }
222
+ catch (err) {
223
+ //容忍listener內部錯誤, 避免影響pino寫log
224
+ }
225
+ fn(...args)
226
+ }
227
+ }
228
+ ev.fatal = wrap('fatal')
229
+ ev.error = wrap('error')
230
+ ev.warn = wrap('warn')
231
+ ev.info = wrap('info')
232
+ ev.debug = wrap('debug')
233
+ ev.trace = wrap('trace')
234
+ ev.cleanLogs = cleanLogs
235
+ ev.clear = clear
236
+
237
+ return ev
91
238
  }
92
239
 
93
240
 
package/src/formatter.mjs CHANGED
@@ -2,9 +2,16 @@ import fs from 'fs'
2
2
  import path from 'path'
3
3
  import { Writable } from 'stream'
4
4
  import ot from 'dayjs'
5
+ import utc from 'dayjs/plugin/utc.js'
6
+ import timezone from 'dayjs/plugin/timezone.js'
5
7
  import get from 'lodash-es/get.js'
6
8
  import isestr from 'wsemi/src/isestr.mjs'
7
9
  import strleft from 'wsemi/src/strleft.mjs'
10
+ import fsCreateFolder from 'wsemi/src/fsCreateFolder.mjs'
11
+
12
+
13
+ ot.extend(utc)
14
+ ot.extend(timezone)
8
15
 
9
16
 
10
17
  async function formatter(opt) {
@@ -22,17 +29,19 @@ async function formatter(opt) {
22
29
  interval = 'day'
23
30
  }
24
31
 
25
- // 建立 logs 資料夾
26
- if (!fs.existsSync(fdLog)) {
27
- fs.mkdirSync(fdLog, { recursive: true })
28
- }
32
+ //timeZone, IANA時區字串如'Asia/Taipei', 為null時使用系統時區
33
+ let timeZone = get(opt, 'timeZone', null)
34
+
35
+ //建立logs資料夾, 已存在則視為成功, 不存在則自動建立
36
+ fsCreateFolder(fdLog)
29
37
 
30
38
  let streamCurr = null
31
39
  let keyCurr = ''
32
40
 
33
41
  let getTimeKey = () => {
34
- // let d = new Date()
35
- let t = ot().format('YYYY-MM-DDTHH:mm:ss.SSSZ')
42
+ //依timeZone決定時區基準, 確保跨機器切檔邊界與檔名一致
43
+ let d = isestr(timeZone) ? ot().tz(timeZone) : ot()
44
+ let t = d.format('YYYY-MM-DDTHH:mm:ss.SSSZ')
36
45
  if (interval === 'hr') {
37
46
  // return d.toISOString().slice(0, 13).replace(/:/g, '-') //例如2025-07-06T14
38
47
  t = strleft(t, 13)
@@ -78,6 +87,15 @@ async function formatter(opt) {
78
87
  catch (err) {
79
88
  callback(err)
80
89
  }
90
+ },
91
+ final(callback) {
92
+ //顯式關閉時將當前fs write stream flush並關閉, 確保worker退出前資料落盤且fd釋放
93
+ if (streamCurr) {
94
+ streamCurr.end(callback)
95
+ }
96
+ else {
97
+ callback()
98
+ }
81
99
  }
82
100
  })
83
101