w-web-perm 1.0.71 → 1.0.72

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.
Files changed (82) hide show
  1. package/dist/css/app.c0e03d41.css +1 -0
  2. package/dist/index.tmp +1 -1
  3. package/dist/js/{app-legacy.a8a679a7.js → app-legacy.9564c1ab.js} +1 -1
  4. package/dist/js/{app.a8a679a7.js → app.9564c1ab.js} +1 -1
  5. package/dist/js/{chunk-vendors.644eda7c.js → chunk-vendors-legacy.778356e3.js} +1 -1
  6. package/dist/js/{chunk-vendors-legacy.644eda7c.js → chunk-vendors.778356e3.js} +1 -1
  7. package/dist/w-web-perm.umd.js +2 -2
  8. package/dist/w-web-perm.umd.js.map +1 -1
  9. package/docs/WWebPerm.html +122 -2
  10. package/docs/WWebPerm.mjs.html +6 -3
  11. package/docs/index.html +1 -1
  12. package/package.json +13 -13
  13. package/server/WWebPerm.mjs +5 -2
  14. package/server/procLang.mjs +0 -12
  15. package/server/procStaInfor.mjs +19 -10
  16. package/server/srLog.mjs +14 -5
  17. package/server/staLogs/staEvent.callWorker.mjs +5 -40
  18. package/server/staLogs/staEvent.mjs +6 -112
  19. package/server/staLogs/staEventTable.callWorker.mjs +11 -0
  20. package/server/staLogs/staEventTable.mjs +6 -96
  21. package/server/staLogs/staLogsCore.callWorker.mjs +89 -0
  22. package/server/staLogs/staLogsCore.mjs +554 -0
  23. package/server/staLogs/staLogsCore.shellWorker.mjs +28 -0
  24. package/settings.json +1 -0
  25. package/src/components/LayoutContentStaInfor.vue +45 -73
  26. package/tableTags.json +1 -1
  27. package/dist/css/app.5a2ba039.css +0 -1
  28. package/logs/2026-06-19T16.log +0 -191
  29. package/logs/2026-06-19T23.log +0 -0
  30. package/logs/2026-06-20T00.log +0 -207
  31. package/logs/2026-06-20T11.log +0 -221
  32. package/logs/2026-06-20T12.log +0 -146
  33. package/logs/2026-06-20T13.log +0 -134
  34. package/logs/2026-06-20T14.log +0 -296
  35. package/logs/2026-06-20T15.log +0 -225
  36. package/logs/2026-06-20T16.log +0 -174
  37. package/logs/2026-06-21T16.log +0 -110
  38. package/logs/2026-06-21T17.log +0 -301
  39. package/logs/2026-06-21T18.log +0 -238
  40. package/logs/2026-06-21T19.log +0 -141
  41. package/logs/2026-06-21T20.log +0 -99
  42. package/logs/2026-06-21T21.log +0 -143
  43. package/logs/2026-06-21T23.log +0 -26
  44. package/logs/2026-06-22T00.log +0 -33
  45. package/logs/2026-06-22T08.log +0 -235
  46. package/logs/2026-06-22T09.log +0 -246
  47. package/logs/2026-06-22T10.log +0 -301
  48. package/logs/2026-06-22T11.log +0 -140
  49. package/logs/2026-06-22T12.log +0 -22
  50. package/logs/2026-06-22T14.log +0 -44
  51. package/logs/2026-06-22T23.log +0 -951
  52. package/logs/2026-06-23T00.log +0 -263
  53. package/logs/2026-06-23T08.log +0 -611
  54. package/logs/2026-06-30T21.log +0 -3609
  55. package/logs/2026-06-30T22.log +0 -3682
  56. package/logs/2026-06-30T23.log +0 -592
  57. package/logs/2026-07-01T12.log +0 -0
  58. package/logs/2026-07-09T22.log +0 -2220
  59. package/logs/2026-07-09T23.log +0 -1140
  60. package/logs/2026-07-10T12.log +0 -6
  61. package/logs/2026-07-10T13.log +0 -351
  62. package/logs/2026-07-10T16.log +0 -1272
  63. package/logs/2026-07-10T19.log +0 -6
  64. package/logs/2026-07-10T20.log +0 -1883
  65. package/logs/2026-07-10T21.log +0 -3505
  66. package/logs/2026-07-10T22.log +0 -3395
  67. package/logs/2026-07-11T00.log +0 -1338
  68. package/logs/2026-07-11T01.log +0 -2492
  69. package/logs/2026-07-11T17.log +0 -19
  70. package/logs/2026-08-25T19.log +0 -4704
  71. package/logs/2026-08-25T20.log +0 -5144
  72. package/logs/2026-08-25T21.log +0 -2116
  73. package/logs/2026-08-26T05.log +0 -1678
  74. package/logs/2026-08-26T06.log +0 -4561
  75. package/logs/2026-08-26T07.log +0 -4916
  76. package/logs/2026-08-26T08.log +0 -260
  77. package/logs/2026-08-26T09.log +0 -1543
  78. package/logs/2026-08-26T13.log +0 -2559
  79. package/logs/2026-08-26T14.log +0 -5526
  80. package/logs/2026-08-26T15.log +0 -2271
  81. package/server/staLogs/staEvent.shellWorker.mjs +0 -19
  82. package/z/344/277/256/346/255/243/350/246/217/345/212/203/346/233/270.md +0 -450
@@ -0,0 +1,554 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import readline from 'readline'
4
+ import ot from 'dayjs'
5
+ import get from 'lodash-es/get.js'
6
+ import isfun from 'wsemi/src/isfun.mjs'
7
+ import isestr from 'wsemi/src/isestr.mjs'
8
+ import fsTreeFolder from 'wsemi/src/fsTreeFolder.mjs'
9
+ import filterVpfsByWindow from './filterVpfsByWindow.mjs'
10
+
11
+
12
+ //staLogsCore: staEvent (時序分桶) 與 staEventTable (近 1 日巢狀窗) 之共用核心 (鏡像 w-web-sso server/staLogs/staLogsCore.mjs, ADR-051)
13
+ // 1. 逐行先比 event 再算時間: time 為 epoch ms 數值 (pino) 直接數值比較, 桶鍵以分鐘整數 memo 查表, 不逐行 dayjs; 就地累加不建陣列
14
+ // 2. 檔級彙總快取 (staEvent): w-syslog 依寫入當下時鐘切檔, 整點跨過後舊檔不再被寫入, 每檔彙總以 (name, size, mtimeMs) 為鍵快取;
15
+ // 檔內最早可計入行 minTimeMs > tStartMs 時窗過濾為 no-op 才沿用, 否則 (邊界檔) 重掃; 當前小時檔 size 變動自然重掃; 快取依 fmt 分開
16
+ // 3. single-flight: 同參數併發呼叫共用同一 in-flight promise
17
+ // 4. staEventTable 之各窗計數依「現在 − 行時間」逐小時取整, 結果隨 now 逐秒變動, 不做檔級快取 (只掃近 25 小時之檔, 由 30 秒 wsemi cache 承接)
18
+ // 5. 資料夾項目略過; 單檔讀取錯誤略過該檔 (不入快取、不整體失敗)
19
+ // 輸出形狀與原 staEvent.mjs / staEventTable.mjs 一致 (golden test: test/unit-staEvent-golden.test.mjs 以改造前實作產出之 expected 深比較)
20
+ // 刻意差異 (僅 staEvent): 無 time (或 time 無法解析) 之行不計入任何桶 (原實作 ot(undefined) 視為 now 而計入當前桶); staEventTable 維持原語意
21
+
22
+
23
+ //快取: `${fdLog}|${fmt}` → Map(檔名 → { size, mtimeMs, minTimeMs, scanTStartMs, agg }); 沿用須「彙總完整 (minTimeMs > scanTStartMs)」且「整檔在新窗內 (minTimeMs > tStartMs)」
24
+ let kpCache = new Map()
25
+
26
+ //single-flight: key → promise
27
+ let kpInflight = new Map()
28
+
29
+
30
+ function normTimeInterval(timeInterval) {
31
+ //非 'day' 一律視同 'hr' (原以 ==='day' 決定 fmt、==='hr' 決定 unit, 傳其他值時兩者不一致, 此處收斂為單一判斷)
32
+ return timeInterval === 'day' ? 'day' : 'hr'
33
+ }
34
+
35
+
36
+ function normFdLog(opt) {
37
+ let fdLog = get(opt, 'fdLog')
38
+ if (!isestr(fdLog)) {
39
+ fdLog = './logs'
40
+ }
41
+ return fdLog
42
+ }
43
+
44
+
45
+ function genPlan(timeLength = 7, timeInterval = 'hr', opt = {}) {
46
+
47
+ //fdLog
48
+ let fdLog = normFdLog(opt)
49
+
50
+ //timeInterval, fmt, unit
51
+ let ti = normTimeInterval(timeInterval)
52
+ let fmt = ti === 'day' ? 'YYYY-MM-DD' : 'YYYY-MM-DDTHH'
53
+ let unit = ti === 'day' ? 'day' : 'hour'
54
+
55
+ //now, 測試可由 opt.timeNow (epoch ms) 釘住
56
+ let timeNow = get(opt, 'timeNow')
57
+ let now = (typeof timeNow === 'number') ? ot(timeNow) : ot()
58
+
59
+ //tStart
60
+ let tStart = now.subtract(timeLength, 'day')
61
+ let tStartMs = tStart.valueOf()
62
+
63
+ //kpTime, 產生完整的時間區間
64
+ let tCurr = tStart.startOf(unit)
65
+ let tEnd = now.startOf(unit)
66
+ let kpTime = []
67
+ while (!tCurr.isAfter(tEnd)) {
68
+ kpTime.push(tCurr.format(fmt))
69
+ tCurr = tCurr.add(1, unit)
70
+ }
71
+
72
+ return { fdLog, timeInterval: ti, fmt, unit, tStart, tStartMs, kpTime, timeNow: now.valueOf() }
73
+ }
74
+
75
+
76
+ function listFiles(fdLog, tStart, fmt) {
77
+
78
+ //vpfs
79
+ let vpfs = fsTreeFolder(fdLog)
80
+
81
+ //剔除資料夾項目 (fsTreeFolder 會列出子資料夾, 對其開讀取串流會 throw)
82
+ vpfs = vpfs.filter((vpf) => {
83
+ return !get(vpf, 'isFolder', false)
84
+ })
85
+
86
+ //開檔前剔除窗外檔, 見該模組註解
87
+ vpfs = filterVpfsByWindow(vpfs, tStart, fmt)
88
+
89
+ //stat
90
+ let files = []
91
+ for (let vpf of vpfs) {
92
+ try {
93
+ let st = fs.statSync(vpf.path)
94
+ if (!st.isFile()) {
95
+ continue
96
+ }
97
+ files.push({ path: vpf.path, name: vpf.name, size: st.size, mtimeMs: st.mtimeMs })
98
+ }
99
+ catch (err) {
100
+ //列出後即被移除 (cleanLogs) → 略過
101
+ }
102
+ }
103
+
104
+ return files
105
+ }
106
+
107
+
108
+ function createBucketLabeler(fmt) {
109
+ //以分鐘整數為 memo 鍵 (任何整分鐘偏移之時區皆與逐行 format 等價), 標籤仍由 dayjs 產生
110
+ let kp = new Map()
111
+ return (tms) => {
112
+ let k = Math.floor(tms / 60000)
113
+ let s = kp.get(k)
114
+ if (s === undefined) {
115
+ s = ot(k * 60000).format(fmt)
116
+ kp.set(k, s)
117
+ }
118
+ return s
119
+ }
120
+ }
121
+
122
+
123
+ function toMs(t) {
124
+ if (typeof t === 'number') {
125
+ return t
126
+ }
127
+ if (isestr(t)) {
128
+ return ot(t).valueOf() //ISO 字串 (非 w-syslog 之來源) 走慢路徑; 無法解析回 NaN
129
+ }
130
+ return NaN
131
+ }
132
+
133
+
134
+ function readLines(fp, onLine) {
135
+ return new Promise((resolve, reject) => {
136
+ let errTemp = null
137
+ let stream = fs.createReadStream(fp, { encoding: 'utf8' })
138
+ let rl = readline.createInterface({
139
+ input: stream,
140
+ crlfDelay: Infinity,
141
+ })
142
+ rl.on('line', onLine)
143
+ let onError = (err) => {
144
+ errTemp = err
145
+ rl.close()
146
+ }
147
+ stream.on('error', onError)
148
+ rl.on('error', onError) //readline Interface 會把 input 之 error 再拋一次, 未監聽即 unhandled
149
+ stream.on('close', () => {
150
+ if (errTemp) {
151
+ reject(errTemp)
152
+ }
153
+ else {
154
+ resolve()
155
+ }
156
+ })
157
+ })
158
+ }
159
+
160
+
161
+ function parseLine(line) {
162
+ let v = null
163
+ try {
164
+ v = JSON.parse(line)
165
+ }
166
+ catch (err) {
167
+ return null
168
+ }
169
+ if (v === null || typeof v !== 'object') {
170
+ return null
171
+ }
172
+ return v
173
+ }
174
+
175
+
176
+ //掃一檔 (staEvent): 各桶 { count, kp:{event:n} }, 任何帶 event 字串之行皆計入
177
+ async function scanFile(fp, tStartMs, fmt, labeler = null) {
178
+ if (!isfun(labeler)) {
179
+ labeler = createBucketLabeler(fmt)
180
+ }
181
+
182
+ let agg = { minTimeMs: null, ev: {} }
183
+
184
+ await readLines(fp, (line) => {
185
+
186
+ //v
187
+ let v = parseLine(line)
188
+ if (v === null) {
189
+ return
190
+ }
191
+
192
+ //event, 先比對再算時間
193
+ let e = v.event
194
+ if (!isestr(e)) {
195
+ return
196
+ }
197
+
198
+ //tms
199
+ let tms = toMs(v.time)
200
+ if (Number.isNaN(tms)) {
201
+ return //無 time 或無法解析: 不計入 (刻意差異, 見檔頭)
202
+ }
203
+
204
+ //minTimeMs, 對所有可計入之行取最小值 (不論是否在窗內), 供快取有效性判斷
205
+ if (agg.minTimeMs === null || tms < agg.minTimeMs) {
206
+ agg.minTimeMs = tms
207
+ }
208
+
209
+ //窗判斷, 對齊原 t.isAfter(tStart)
210
+ if (!(tms > tStartMs)) {
211
+ return
212
+ }
213
+
214
+ //accumulate
215
+ let label = labeler(tms)
216
+ let b = agg.ev[label]
217
+ if (b === undefined) {
218
+ b = { count: 0, kp: {} }
219
+ agg.ev[label] = b
220
+ }
221
+ b.count += 1
222
+ b.kp[e] = (b.kp[e] || 0) + 1
223
+
224
+ })
225
+
226
+ return agg
227
+ }
228
+
229
+
230
+ //掃描一批檔案 (staEvent), 回傳 [{ name, ok, agg | err }]; 單檔失敗不影響其他檔 (worker 端亦呼叫此函式)
231
+ async function scanFiles(files, tStartMs, fmt) {
232
+ let labeler = createBucketLabeler(fmt)
233
+ let rs = []
234
+ for (let f of files) {
235
+ try {
236
+ let agg = await scanFile(f.path, tStartMs, fmt, labeler)
237
+ rs.push({ name: f.name, ok: true, agg })
238
+ }
239
+ catch (err) {
240
+ rs.push({ name: f.name, ok: false, err: String(get(err, 'message', err)) })
241
+ }
242
+ }
243
+ return rs
244
+ }
245
+
246
+
247
+ function mergeAgg(target, agg) {
248
+ for (let label in agg.ev) {
249
+ let s = agg.ev[label]
250
+ let b = target[label]
251
+ if (b === undefined) {
252
+ b = { count: 0, kp: {} }
253
+ target[label] = b
254
+ }
255
+ b.count += s.count
256
+ for (let e in s.kp) {
257
+ b.kp[e] = (b.kp[e] || 0) + s.kp[e]
258
+ }
259
+ }
260
+ return target
261
+ }
262
+
263
+
264
+ function buildOutput(merged, kpTime) {
265
+ //對齊原 merge({}, kpTime, gsLog) + Object.keys().sort(): 窗內桶補零, 資料桶 (含未來時間桶) 聯集
266
+ let kp = {}
267
+ for (let label of kpTime) {
268
+ kp[label] = { count: 0 }
269
+ }
270
+ for (let label in merged) {
271
+ let b = merged[label]
272
+ kp[label] = { count: b.count, ...b.kp }
273
+ }
274
+ return Object.keys(kp)
275
+ .sort()
276
+ .map((time) => ({
277
+ time,
278
+ data: kp[time],
279
+ }))
280
+ }
281
+
282
+
283
+ function getCacheKey(fdLog, fmt) {
284
+ return `${path.resolve(fdLog)}|${fmt}`
285
+ }
286
+
287
+
288
+ function getCacheDir(key) {
289
+ let m = kpCache.get(key)
290
+ if (!m) {
291
+ m = new Map()
292
+ kpCache.set(key, m)
293
+ }
294
+ return m
295
+ }
296
+
297
+
298
+ function clearCache() {
299
+ kpCache.clear()
300
+ }
301
+
302
+
303
+ function getCacheSize(fdLog = './logs', timeInterval = 'hr') {
304
+ let fmt = normTimeInterval(timeInterval) === 'day' ? 'YYYY-MM-DD' : 'YYYY-MM-DDTHH'
305
+ let m = kpCache.get(getCacheKey(fdLog, fmt))
306
+ return m ? m.size : 0
307
+ }
308
+
309
+
310
+ async function run(plan, opt = {}) {
311
+
312
+ //useCache
313
+ let useCache = get(opt, 'useCache') !== false
314
+
315
+ //scanFilesFn, callWorker 注入 worker 版
316
+ let scanFilesFn = get(opt, 'scanFiles')
317
+ if (!isfun(scanFilesFn)) {
318
+ scanFilesFn = scanFiles
319
+ }
320
+
321
+ //srLog
322
+ let srLog = get(opt, 'srLog', null)
323
+
324
+ //files
325
+ let files = listFiles(plan.fdLog, plan.tStart, plan.fmt)
326
+
327
+ //cacheDir
328
+ let cacheDir = getCacheDir(getCacheKey(plan.fdLog, plan.fmt))
329
+
330
+ //分流: 快取可沿用 vs 需重掃
331
+ let toScan = []
332
+ let kpAgg = {}
333
+ for (let f of files) {
334
+ let c = useCache ? cacheDir.get(f.name) : undefined
335
+ //沿用條件: 檔未變 + 彙總完整 (掃描時無行被窗過濾: minTimeMs > scanTStartMs) + 整檔仍在新窗內 (minTimeMs > 新 tStartMs)
336
+ // 只檢查後者時, timeLength 由小變大 (tStart 後退) 會沿用以窄窗掃出之不完整彙總 → 靜默漏算
337
+ let b = c !== undefined &&
338
+ c.size === f.size &&
339
+ c.mtimeMs === f.mtimeMs &&
340
+ (c.minTimeMs === null || (c.minTimeMs > c.scanTStartMs && c.minTimeMs > plan.tStartMs))
341
+ if (b) {
342
+ kpAgg[f.name] = c.agg
343
+ }
344
+ else {
345
+ toScan.push(f)
346
+ }
347
+ }
348
+
349
+ //淘汰已不存在之檔 (cleanLogs 刪舊檔)
350
+ if (useCache) {
351
+ let kpName = new Set(files.map((f) => f.name))
352
+ for (let name of cacheDir.keys()) {
353
+ if (!kpName.has(name)) {
354
+ cacheDir.delete(name)
355
+ }
356
+ }
357
+ }
358
+
359
+ //scan
360
+ let errors = []
361
+ if (toScan.length > 0) {
362
+ let rs = await scanFilesFn(toScan.map((f) => ({ path: f.path, name: f.name })), plan.tStartMs, plan.fmt)
363
+ let kpRs = {}
364
+ for (let r of rs) {
365
+ kpRs[r.name] = r
366
+ }
367
+ for (let f of toScan) {
368
+ let r = kpRs[f.name]
369
+ if (r && r.ok) {
370
+ kpAgg[f.name] = r.agg
371
+ if (useCache) {
372
+ cacheDir.set(f.name, { size: f.size, mtimeMs: f.mtimeMs, minTimeMs: r.agg.minTimeMs, scanTStartMs: plan.tStartMs, agg: r.agg })
373
+ }
374
+ }
375
+ else {
376
+ let err = get(r, 'err', 'no result')
377
+ errors.push({ name: f.name, err })
378
+ if (useCache) {
379
+ cacheDir.delete(f.name)
380
+ }
381
+ if (srLog && isfun(srLog.warn)) {
382
+ srLog.warn({ event: 'fun-staLogs', key: 'staLogsFileSkipped', name: f.name, err })
383
+ }
384
+ }
385
+ }
386
+ }
387
+
388
+ //merge
389
+ let merged = {}
390
+ for (let f of files) {
391
+ let agg = kpAgg[f.name]
392
+ if (agg) {
393
+ mergeAgg(merged, agg)
394
+ }
395
+ }
396
+
397
+ //output
398
+ let rs = buildOutput(merged, plan.kpTime)
399
+ return {
400
+ rs,
401
+ stat: {
402
+ nFiles: files.length,
403
+ nScanned: toScan.length,
404
+ nCached: files.length - toScan.length,
405
+ errors,
406
+ },
407
+ }
408
+ }
409
+
410
+
411
+ //staLogs: staEvent 之核心入口, 回傳 { rs, stat }
412
+ function staLogs(timeLength = 7, timeInterval = 'hr', opt = {}) {
413
+ //非 async: 併發呼叫須回傳同一個 promise 物件
414
+
415
+ //plan
416
+ let plan = genPlan(timeLength, timeInterval, opt)
417
+
418
+ //single-flight
419
+ let key = JSON.stringify([path.resolve(plan.fdLog), timeLength, plan.timeInterval, get(opt, 'timeNow', null)])
420
+ if (kpInflight.has(key)) {
421
+ return kpInflight.get(key)
422
+ }
423
+ let pm = run(plan, opt)
424
+ .finally(() => {
425
+ kpInflight.delete(key)
426
+ })
427
+ kpInflight.set(key, pm)
428
+
429
+ return pm
430
+ }
431
+
432
+
433
+ // ---------------------------------------------------------------------------
434
+ // staEventTable: 各 event 近 1日/8hr/4hr/1hr 巢狀窗計數 (語意與原 staEventTable.mjs 逐行一致)
435
+ // ---------------------------------------------------------------------------
436
+
437
+ //absFloor: dayjs diff 之取整 (負數向零取整), 對齊原 now.diff(ot(v.time), 'hour')
438
+ function absFloor(n) {
439
+ return n < 0 ? (Math.ceil(n) || 0) : Math.floor(n)
440
+ }
441
+
442
+
443
+ //toMsForTable: 對齊原 ot(v.time): 無 time 視為 now; 數值直接用; 字串 dayjs 解析 (無法解析回 NaN → 原 diff 為 NaN → 各窗判斷皆 false, 列仍初始化為 0)
444
+ function toMsForTable(t, nowMs) {
445
+ if (t === undefined || t === null) {
446
+ return nowMs
447
+ }
448
+ if (typeof t === 'number') {
449
+ return t
450
+ }
451
+ if (typeof t === 'string') {
452
+ return ot(t).valueOf()
453
+ }
454
+ return ot(t).valueOf()
455
+ }
456
+
457
+
458
+ //掃一批檔案累計各 event 之巢狀窗計數; 單檔失敗略過 (worker 端亦呼叫此函式)
459
+ async function scanTableFiles(files, nowMs) {
460
+ let kp = {}
461
+ let errors = []
462
+ for (let f of files) {
463
+ try {
464
+ await readLines(f.path, (line) => {
465
+ let v = parseLine(line)
466
+ if (v === null) {
467
+ return
468
+ }
469
+ let event = v.event
470
+ if (!isestr(event)) {
471
+ return
472
+ }
473
+
474
+ //dh, 距今幾小時 (對齊 dayjs diff(hour) 之取整)
475
+ let tms = toMsForTable(v.time, nowMs)
476
+ let dh = absFloor((nowMs - tms) / 3600000)
477
+ if (dh < 0 || dh > 24) {
478
+ return //超過 1日窗或未來時間略過 (NaN 時兩者皆 false, 與原語意同)
479
+ }
480
+
481
+ //初始化
482
+ if (!kp[event]) {
483
+ kp[event] = { last1Day: 0, last8Hour: 0, last4Hour: 0, last1Hour: 0 }
484
+ }
485
+
486
+ //巢狀窗各自累計 (1hr⊂4hr⊂8hr⊂24hr)
487
+ if (dh <= 24) {
488
+ kp[event].last1Day += 1
489
+ }
490
+ if (dh <= 8) {
491
+ kp[event].last8Hour += 1
492
+ }
493
+ if (dh <= 4) {
494
+ kp[event].last4Hour += 1
495
+ }
496
+ if (dh <= 1) {
497
+ kp[event].last1Hour += 1
498
+ }
499
+ })
500
+ }
501
+ catch (err) {
502
+ errors.push({ name: f.name, err: String(get(err, 'message', err)) })
503
+ }
504
+ }
505
+ return { kp, errors }
506
+ }
507
+
508
+
509
+ //staTable: staEventTable 之核心入口, 回傳 { rs, stat }
510
+ async function staTable(opt = {}) {
511
+
512
+ //fdLog
513
+ let fdLog = normFdLog(opt)
514
+
515
+ //now
516
+ let timeNow = get(opt, 'timeNow')
517
+ let now = (typeof timeNow === 'number') ? ot(timeNow) : ot()
518
+ let nowMs = now.valueOf()
519
+
520
+ //scanTableFilesFn, callWorker 注入 worker 版
521
+ let scanTableFilesFn = get(opt, 'scanTableFiles')
522
+ if (!isfun(scanTableFilesFn)) {
523
+ scanTableFilesFn = scanTableFiles
524
+ }
525
+
526
+ //srLog
527
+ let srLog = get(opt, 'srLog', null)
528
+
529
+ //files, 開檔前剔除窗外檔; 用 25h (非 24h) 是因 diff('hour') 取整, 最舊可能到 now-25h, 多留一小時確保不漏讀
530
+ let files = listFiles(fdLog, now.subtract(25, 'hour'), 'YYYY-MM-DDTHH')
531
+
532
+ //scan
533
+ let { kp, errors } = await scanTableFilesFn(files.map((f) => ({ path: f.path, name: f.name })), nowMs)
534
+ for (let e of errors) {
535
+ if (srLog && isfun(srLog.warn)) {
536
+ srLog.warn({ event: 'fun-staLogs', key: 'staLogsFileSkipped', name: e.name, err: e.err })
537
+ }
538
+ }
539
+
540
+ //rs
541
+ let rs = []
542
+ for (let event in kp) {
543
+ rs.push({ event, ...kp[event] })
544
+ }
545
+
546
+ //依 last1Day 由大到小排序
547
+ rs.sort((a, b) => b.last1Day - a.last1Day)
548
+
549
+ return { rs, stat: { nFiles: files.length, errors } }
550
+ }
551
+
552
+
553
+ export { staLogs, staTable, genPlan, listFiles, scanFile, scanFiles, scanTableFiles, mergeAgg, buildOutput, clearCache, getCacheSize, normTimeInterval }
554
+ export default staLogs
@@ -0,0 +1,28 @@
1
+ import { parentPort } from 'worker_threads'
2
+ import { scanFiles, scanTableFiles } from './staLogsCore.mjs'
3
+
4
+
5
+ //worker 只負責掃描主執行緒交付之檔案清單, 回傳彙總; 快取本體留在主執行緒 (staLogsCore.mjs)
6
+ // kind='scan' → staEvent 之每檔彙總 [{ name, ok, agg | err }]
7
+ // kind='table' → staEventTable 之 { kp, errors }
8
+ parentPort.on('message', async (param) => {
9
+ try {
10
+ let r = null
11
+ if (param.kind === 'table') {
12
+ r = await scanTableFiles(param.files, param.nowMs)
13
+ }
14
+ else {
15
+ r = await scanFiles(param.files, param.tStartMs, param.fmt)
16
+ }
17
+ parentPort.postMessage({
18
+ mode: 'done',
19
+ payload: r,
20
+ })
21
+ }
22
+ catch (err) {
23
+ parentPort.postMessage({
24
+ mode: 'error',
25
+ payload: String((err && err.message) || err),
26
+ })
27
+ }
28
+ })
package/settings.json CHANGED
@@ -39,5 +39,6 @@
39
39
 
40
40
  logFd: './logs',
41
41
  logInterval: 'hr',
42
+ logNumKeep: 8760, //log 檔保留數上限, 超出者自最舊刪除; hr 模式每小時 1 檔, 8760 約 1 年 (= w-syslog 預設, 未給亦同)
42
43
 
43
44
  }