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.
- package/dist/css/app.c0e03d41.css +1 -0
- package/dist/index.tmp +1 -1
- package/dist/js/{app-legacy.a8a679a7.js → app-legacy.9564c1ab.js} +1 -1
- package/dist/js/{app.a8a679a7.js → app.9564c1ab.js} +1 -1
- package/dist/js/{chunk-vendors.644eda7c.js → chunk-vendors-legacy.778356e3.js} +1 -1
- package/dist/js/{chunk-vendors-legacy.644eda7c.js → chunk-vendors.778356e3.js} +1 -1
- package/dist/w-web-perm.umd.js +2 -2
- package/dist/w-web-perm.umd.js.map +1 -1
- package/docs/WWebPerm.html +122 -2
- package/docs/WWebPerm.mjs.html +6 -3
- package/docs/index.html +1 -1
- package/package.json +13 -13
- package/server/WWebPerm.mjs +5 -2
- package/server/procLang.mjs +0 -12
- package/server/procStaInfor.mjs +19 -10
- package/server/srLog.mjs +14 -5
- package/server/staLogs/staEvent.callWorker.mjs +5 -40
- package/server/staLogs/staEvent.mjs +6 -112
- package/server/staLogs/staEventTable.callWorker.mjs +11 -0
- package/server/staLogs/staEventTable.mjs +6 -96
- package/server/staLogs/staLogsCore.callWorker.mjs +89 -0
- package/server/staLogs/staLogsCore.mjs +554 -0
- package/server/staLogs/staLogsCore.shellWorker.mjs +28 -0
- package/settings.json +1 -0
- package/src/components/LayoutContentStaInfor.vue +45 -73
- package/tableTags.json +1 -1
- package/dist/css/app.5a2ba039.css +0 -1
- package/logs/2026-06-19T16.log +0 -191
- package/logs/2026-06-19T23.log +0 -0
- package/logs/2026-06-20T00.log +0 -207
- package/logs/2026-06-20T11.log +0 -221
- package/logs/2026-06-20T12.log +0 -146
- package/logs/2026-06-20T13.log +0 -134
- package/logs/2026-06-20T14.log +0 -296
- package/logs/2026-06-20T15.log +0 -225
- package/logs/2026-06-20T16.log +0 -174
- package/logs/2026-06-21T16.log +0 -110
- package/logs/2026-06-21T17.log +0 -301
- package/logs/2026-06-21T18.log +0 -238
- package/logs/2026-06-21T19.log +0 -141
- package/logs/2026-06-21T20.log +0 -99
- package/logs/2026-06-21T21.log +0 -143
- package/logs/2026-06-21T23.log +0 -26
- package/logs/2026-06-22T00.log +0 -33
- package/logs/2026-06-22T08.log +0 -235
- package/logs/2026-06-22T09.log +0 -246
- package/logs/2026-06-22T10.log +0 -301
- package/logs/2026-06-22T11.log +0 -140
- package/logs/2026-06-22T12.log +0 -22
- package/logs/2026-06-22T14.log +0 -44
- package/logs/2026-06-22T23.log +0 -951
- package/logs/2026-06-23T00.log +0 -263
- package/logs/2026-06-23T08.log +0 -611
- package/logs/2026-06-30T21.log +0 -3609
- package/logs/2026-06-30T22.log +0 -3682
- package/logs/2026-06-30T23.log +0 -592
- package/logs/2026-07-01T12.log +0 -0
- package/logs/2026-07-09T22.log +0 -2220
- package/logs/2026-07-09T23.log +0 -1140
- package/logs/2026-07-10T12.log +0 -6
- package/logs/2026-07-10T13.log +0 -351
- package/logs/2026-07-10T16.log +0 -1272
- package/logs/2026-07-10T19.log +0 -6
- package/logs/2026-07-10T20.log +0 -1883
- package/logs/2026-07-10T21.log +0 -3505
- package/logs/2026-07-10T22.log +0 -3395
- package/logs/2026-07-11T00.log +0 -1338
- package/logs/2026-07-11T01.log +0 -2492
- package/logs/2026-07-11T17.log +0 -19
- package/logs/2026-08-25T19.log +0 -4704
- package/logs/2026-08-25T20.log +0 -5144
- package/logs/2026-08-25T21.log +0 -2116
- package/logs/2026-08-26T05.log +0 -1678
- package/logs/2026-08-26T06.log +0 -4561
- package/logs/2026-08-26T07.log +0 -4916
- package/logs/2026-08-26T08.log +0 -260
- package/logs/2026-08-26T09.log +0 -1543
- package/logs/2026-08-26T13.log +0 -2559
- package/logs/2026-08-26T14.log +0 -5526
- package/logs/2026-08-26T15.log +0 -2271
- package/server/staLogs/staEvent.shellWorker.mjs +0 -19
- 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
|
+
})
|