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/.github/workflows/ci-test.yml +3 -3
- package/README.md +3 -1
- package/dist/formatter.umd.js +2 -2
- package/dist/formatter.umd.js.map +1 -1
- package/dist/w-syslog.umd.js +2 -2
- package/dist/w-syslog.umd.js.map +1 -1
- package/docs/WSyslog.html +82 -4
- package/docs/WSyslog.mjs.html +152 -5
- package/docs/index.html +1 -1
- package/g.mjs +1 -1
- package/package.json +5 -5
- package/src/WSyslog.mjs +150 -3
- package/src/formatter.mjs +24 -6
- package/test/{WSyslog.test.mjs → basic.test.mjs} +55 -50
- package/test/cleanLogs.test.mjs +213 -0
- package/test/clear.test.mjs +138 -0
- package/test/fixtures/clear-exit.mjs +25 -0
- package/test/fixtures/log-after-clear.mjs +18 -0
- package/test/timeZone.test.mjs +84 -0
- package/_logs/2025-08-25T14.log +0 -3
|
@@ -1,50 +1,55 @@
|
|
|
1
|
-
import fs from 'fs'
|
|
2
|
-
import assert from 'assert'
|
|
3
|
-
import _ from 'lodash-es'
|
|
4
|
-
import w from 'wsemi'
|
|
5
|
-
import WSyslog from '../src/WSyslog.mjs'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
describe('WSyslog', function() {
|
|
9
|
-
|
|
10
|
-
it('test', async function() {
|
|
11
|
-
|
|
12
|
-
w.fsCleanFolder('./_logs')
|
|
13
|
-
|
|
14
|
-
let log = WSyslog({ interval: 'hr' })
|
|
15
|
-
log.info({ event: 'runner', msg: 'start' })
|
|
16
|
-
log.warn({ event: 'monitor-memory', msg: 'usage-high', ratio: 85.4 })
|
|
17
|
-
log.error({ event: 'crash', msg: 'db connection', code: 500 })
|
|
18
|
-
|
|
19
|
-
await
|
|
20
|
-
|
|
21
|
-
let vpfs = w.fsTreeFolder('./_logs')
|
|
22
|
-
// console.log('vpfs', vpfs)
|
|
23
|
-
|
|
24
|
-
let fp = _.get(vpfs, `0.path`, '')
|
|
25
|
-
|
|
26
|
-
let jj = fs.readFileSync(fp, 'utf8')
|
|
27
|
-
|
|
28
|
-
let ss = w.sep(jj, '\n')
|
|
29
|
-
|
|
30
|
-
let r = _.map(ss, (j) => {
|
|
31
|
-
let v = JSON.parse(j)
|
|
32
|
-
let event = _.get(v, 'event')
|
|
33
|
-
let msg = _.get(v, 'msg')
|
|
34
|
-
return {
|
|
35
|
-
event,
|
|
36
|
-
msg,
|
|
37
|
-
}
|
|
38
|
-
})
|
|
39
|
-
// console.log('r', r)
|
|
40
|
-
|
|
41
|
-
let rr = [
|
|
42
|
-
{ event: 'runner', msg: 'start' },
|
|
43
|
-
{ event: 'monitor-memory', msg: 'usage-high' },
|
|
44
|
-
{ event: 'crash', msg: 'db connection' },
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
assert.strict.deepEqual(r, rr)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import assert from 'assert'
|
|
3
|
+
import _ from 'lodash-es'
|
|
4
|
+
import w from 'wsemi'
|
|
5
|
+
import WSyslog from '../src/WSyslog.mjs'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
describe('WSyslog', function() {
|
|
9
|
+
|
|
10
|
+
it('test', async function() {
|
|
11
|
+
|
|
12
|
+
w.fsCleanFolder('./_logs')
|
|
13
|
+
|
|
14
|
+
let log = WSyslog({ interval: 'hr' })
|
|
15
|
+
log.info({ event: 'runner', msg: 'start' })
|
|
16
|
+
log.warn({ event: 'monitor-memory', msg: 'usage-high', ratio: 85.4 })
|
|
17
|
+
log.error({ event: 'crash', msg: 'db connection', code: 500 })
|
|
18
|
+
|
|
19
|
+
await log.clear() //顯式關閉, 保證殘餘log已全數寫出
|
|
20
|
+
|
|
21
|
+
let vpfs = w.fsTreeFolder('./_logs')
|
|
22
|
+
// console.log('vpfs', vpfs)
|
|
23
|
+
|
|
24
|
+
let fp = _.get(vpfs, `0.path`, '')
|
|
25
|
+
|
|
26
|
+
let jj = fs.readFileSync(fp, 'utf8')
|
|
27
|
+
|
|
28
|
+
let ss = w.sep(jj, '\n')
|
|
29
|
+
|
|
30
|
+
let r = _.map(ss, (j) => {
|
|
31
|
+
let v = JSON.parse(j)
|
|
32
|
+
let event = _.get(v, 'event')
|
|
33
|
+
let msg = _.get(v, 'msg')
|
|
34
|
+
return {
|
|
35
|
+
event,
|
|
36
|
+
msg,
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
// console.log('r', r)
|
|
40
|
+
|
|
41
|
+
let rr = [
|
|
42
|
+
{ event: 'runner', msg: 'start' },
|
|
43
|
+
{ event: 'monitor-memory', msg: 'usage-high' },
|
|
44
|
+
{ event: 'crash', msg: 'db connection' },
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
assert.strict.deepEqual(r, rr)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
after(async function() {
|
|
51
|
+
//clear已保證worker收攤與fd釋放, 直接移除測試資料夾, 不留空殼
|
|
52
|
+
w.fsDeleteFolder('./_logs')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
})
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import assert from 'assert'
|
|
4
|
+
import w from 'wsemi'
|
|
5
|
+
import WSyslog from '../src/WSyslog.mjs'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
//獨立測試資料夾, 避免與basic.test.mjs在parallel模式下競用'./_logs'
|
|
9
|
+
let fdLog = './_logs_clean_test'
|
|
10
|
+
let fdNoExist = './_logs_clean_test_nonexistent'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
function seedFiles(folder, names) {
|
|
14
|
+
//已存在則視為成功, 不存在則自動建立
|
|
15
|
+
w.fsCreateFolder(folder)
|
|
16
|
+
for (let fn of names) {
|
|
17
|
+
fs.writeFileSync(path.join(folder, fn), '')
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
function listSeed(folder, prefix) {
|
|
23
|
+
if (!w.fsIsFolder(folder)) {
|
|
24
|
+
return []
|
|
25
|
+
}
|
|
26
|
+
return fs.readdirSync(folder).filter((fn) => fn.startsWith(prefix)).sort()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
describe('WSyslog cleanLogs', function() {
|
|
31
|
+
|
|
32
|
+
beforeEach(function() {
|
|
33
|
+
//每個test開始前完全移除舊資料夾, 從乾淨狀態開始
|
|
34
|
+
w.fsDeleteFolder(fdLog)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
afterEach(function() {
|
|
38
|
+
//clear已保證worker收攤與fd釋放, 直接移除測試資料夾, 不留空殼
|
|
39
|
+
w.fsDeleteFolder(fdLog)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
after(function() {
|
|
43
|
+
//最終把'資料夾不存在時不崩'測試可能由pino formatter建立的資料夾也清掉
|
|
44
|
+
w.fsDeleteFolder(fdNoExist)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('day模式: 保留最新numKeep個檔, 最舊的被刪', async function() {
|
|
48
|
+
seedFiles(fdLog, [
|
|
49
|
+
'2020-01-01.log',
|
|
50
|
+
'2020-01-02.log',
|
|
51
|
+
'2020-01-03.log',
|
|
52
|
+
'2020-01-04.log',
|
|
53
|
+
'2020-01-05.log',
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
//init同步階段會跑一次cleanLogs(), 看到5個2020檔, numKeep=2, 刪3個最舊
|
|
57
|
+
let log = WSyslog({ fdLog, numKeep: 2, interval: 'day' })
|
|
58
|
+
|
|
59
|
+
let kept = listSeed(fdLog, '2020-')
|
|
60
|
+
assert.strict.deepEqual(kept, [
|
|
61
|
+
'2020-01-04.log',
|
|
62
|
+
'2020-01-05.log',
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
await log.clear()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('當檔數小於等於numKeep時不刪任何檔', async function() {
|
|
69
|
+
seedFiles(fdLog, [
|
|
70
|
+
'2020-01-01.log',
|
|
71
|
+
'2020-01-02.log',
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
let log = WSyslog({ fdLog, numKeep: 5, interval: 'day' })
|
|
75
|
+
log.cleanLogs()
|
|
76
|
+
|
|
77
|
+
let kept = listSeed(fdLog, '2020-')
|
|
78
|
+
assert.strict.deepEqual(kept, [
|
|
79
|
+
'2020-01-01.log',
|
|
80
|
+
'2020-01-02.log',
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
await log.clear()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('hr模式: 只匹配hr命名, 不誤刪day命名', async function() {
|
|
87
|
+
seedFiles(fdLog, [
|
|
88
|
+
//day命名(不該被hr模式cleanLogs動到)
|
|
89
|
+
'2020-01-01.log',
|
|
90
|
+
'2020-01-02.log',
|
|
91
|
+
//hr命名
|
|
92
|
+
'2020-01-01T01.log',
|
|
93
|
+
'2020-01-01T02.log',
|
|
94
|
+
'2020-01-01T03.log',
|
|
95
|
+
'2020-01-01T04.log',
|
|
96
|
+
'2020-01-01T05.log',
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
//numKeep=2, hr模式應只看hr檔, 5刪3, 留04, 05
|
|
100
|
+
let log = WSyslog({ fdLog, numKeep: 2, interval: 'hr' })
|
|
101
|
+
log.cleanLogs()
|
|
102
|
+
|
|
103
|
+
let dayFiles = listSeed(fdLog, '2020-').filter((fn) => /^\d{4}-\d{2}-\d{2}\.log$/.test(fn))
|
|
104
|
+
let hrFiles = listSeed(fdLog, '2020-').filter((fn) => /^\d{4}-\d{2}-\d{2}T\d{2}\.log$/.test(fn))
|
|
105
|
+
|
|
106
|
+
assert.strict.deepEqual(dayFiles, [
|
|
107
|
+
'2020-01-01.log',
|
|
108
|
+
'2020-01-02.log',
|
|
109
|
+
])
|
|
110
|
+
assert.strict.deepEqual(hrFiles, [
|
|
111
|
+
'2020-01-01T04.log',
|
|
112
|
+
'2020-01-01T05.log',
|
|
113
|
+
])
|
|
114
|
+
|
|
115
|
+
await log.clear()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('刪除時觸發delete事件, payload含fp與fn', async function() {
|
|
119
|
+
//先啟動空資料夾的logger, 此時init的cleanLogs不會刪任何檔
|
|
120
|
+
let log = WSyslog({ fdLog, numKeep: 5, interval: 'hr' })
|
|
121
|
+
|
|
122
|
+
//listener在init之後掛上, 不會錯過後續手動觸發的事件
|
|
123
|
+
let deleted = []
|
|
124
|
+
log.on('delete', ({ fp, fn }) => {
|
|
125
|
+
deleted.push({ fp, fn })
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
//seed 8個未來日期的hr檔, 確保任何今日檔(若pino async建立)都會被視為最舊優先刪除
|
|
129
|
+
//如此過濾'2099-'前綴後, 必然看到T01-T03被刪, 不受今日檔存在與否影響
|
|
130
|
+
seedFiles(fdLog, [
|
|
131
|
+
'2099-01-01T01.log',
|
|
132
|
+
'2099-01-01T02.log',
|
|
133
|
+
'2099-01-01T03.log',
|
|
134
|
+
'2099-01-01T04.log',
|
|
135
|
+
'2099-01-01T05.log',
|
|
136
|
+
'2099-01-01T06.log',
|
|
137
|
+
'2099-01-01T07.log',
|
|
138
|
+
'2099-01-01T08.log',
|
|
139
|
+
])
|
|
140
|
+
|
|
141
|
+
log.cleanLogs()
|
|
142
|
+
|
|
143
|
+
//過濾seed檔, 必為被刪的最舊3個
|
|
144
|
+
let names2099 = deleted
|
|
145
|
+
.map((d) => d.fn)
|
|
146
|
+
.filter((fn) => fn.startsWith('2099-'))
|
|
147
|
+
.sort()
|
|
148
|
+
assert.strict.deepEqual(names2099, [
|
|
149
|
+
'2099-01-01T01.log',
|
|
150
|
+
'2099-01-01T02.log',
|
|
151
|
+
'2099-01-01T03.log',
|
|
152
|
+
])
|
|
153
|
+
|
|
154
|
+
//每筆事件的fp必為path.join(fdLog, fn)
|
|
155
|
+
for (let d of deleted) {
|
|
156
|
+
assert.strict.equal(d.fp, path.join(fdLog, d.fn))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await log.clear()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('資料夾不存在時不崩', async function() {
|
|
163
|
+
//確保起始狀態為不存在
|
|
164
|
+
w.fsDeleteFolder(fdNoExist)
|
|
165
|
+
|
|
166
|
+
let log = null
|
|
167
|
+
assert.doesNotThrow(() => {
|
|
168
|
+
log = WSyslog({ fdLog: fdNoExist, numKeep: 5, interval: 'day' })
|
|
169
|
+
log.cleanLogs()
|
|
170
|
+
})
|
|
171
|
+
await log.clear()
|
|
172
|
+
//pino formatter可能async建立資料夾, 由describe區塊的after hook統一清理
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('忽略不符regex命名的檔', async function() {
|
|
176
|
+
seedFiles(fdLog, [
|
|
177
|
+
'2020-01-01.log',
|
|
178
|
+
'2020-01-02.log',
|
|
179
|
+
'2020-01-03.log',
|
|
180
|
+
//不符regex(注意cleanLogs regex僅檢查格式不檢查日期合法性, 故'2020-13-99.log'這類仍會匹配)
|
|
181
|
+
'readme.txt',
|
|
182
|
+
'app.config',
|
|
183
|
+
'log.json',
|
|
184
|
+
'2020-1-1.log', //月份/日期僅1碼, 不符\d{2}
|
|
185
|
+
'YYYY-MM-DD.log', //非數字
|
|
186
|
+
])
|
|
187
|
+
|
|
188
|
+
let log = WSyslog({ fdLog, numKeep: 1, interval: 'day' })
|
|
189
|
+
log.cleanLogs()
|
|
190
|
+
|
|
191
|
+
//不符的檔全保留
|
|
192
|
+
let nonMatching = fs.readdirSync(fdLog).filter((fn) =>
|
|
193
|
+
['readme.txt', 'app.config', 'log.json', '2020-1-1.log', 'YYYY-MM-DD.log'].includes(fn)
|
|
194
|
+
).sort()
|
|
195
|
+
assert.strict.deepEqual(nonMatching, [
|
|
196
|
+
'2020-1-1.log',
|
|
197
|
+
'YYYY-MM-DD.log',
|
|
198
|
+
'app.config',
|
|
199
|
+
'log.json',
|
|
200
|
+
'readme.txt',
|
|
201
|
+
])
|
|
202
|
+
|
|
203
|
+
//符合regex的: 3個2020檔, numKeep=1, 留03
|
|
204
|
+
let matching = fs.readdirSync(fdLog)
|
|
205
|
+
.filter((fn) => /^\d{4}-\d{2}-\d{2}\.log$/.test(fn))
|
|
206
|
+
.filter((fn) => fn.startsWith('2020-'))
|
|
207
|
+
.sort()
|
|
208
|
+
assert.strict.deepEqual(matching, ['2020-01-03.log'])
|
|
209
|
+
|
|
210
|
+
await log.clear()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import assert from 'assert'
|
|
4
|
+
import { execFile } from 'child_process'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import _ from 'lodash-es'
|
|
7
|
+
import w from 'wsemi'
|
|
8
|
+
import WSyslog from '../src/WSyslog.mjs'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
//獨立測試資料夾, 避免與其他測試檔在parallel模式下競用
|
|
12
|
+
let fdLog = './_logs_clear_test'
|
|
13
|
+
let fdLogChild = './_logs_clear_child' //子程序fixture用, 實際為`${fdLogChild}_${i}`
|
|
14
|
+
let fdLogStress = './_logs_clear_stress'
|
|
15
|
+
let fdLogAfter = './_logs_clear_after'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
//fixture路徑錨定於本測試檔位置, 不受cwd影響
|
|
19
|
+
let fpFxExit = fileURLToPath(new URL('./fixtures/clear-exit.mjs', import.meta.url))
|
|
20
|
+
let fpFxAfter = fileURLToPath(new URL('./fixtures/log-after-clear.mjs', import.meta.url))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
function runNode(args) {
|
|
24
|
+
//以子程序執行fixture, 回傳{code, stderr, ms}
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
let ts = Date.now()
|
|
27
|
+
execFile(process.execPath, args, { cwd: process.cwd() }, (err, stdout, stderr) => {
|
|
28
|
+
let code = err ? err.code : 0
|
|
29
|
+
resolve({ code, stderr, ms: Date.now() - ts })
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
function countLogLines(folder) {
|
|
36
|
+
//統計資料夾內所有log檔的非空行數
|
|
37
|
+
if (!w.fsIsFolder(folder)) {
|
|
38
|
+
return 0
|
|
39
|
+
}
|
|
40
|
+
let n = 0
|
|
41
|
+
for (let fn of fs.readdirSync(folder)) {
|
|
42
|
+
let jj = fs.readFileSync(path.join(folder, fn), 'utf8')
|
|
43
|
+
n += w.sep(jj, '\n').length
|
|
44
|
+
}
|
|
45
|
+
return n
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
function cleanAll() {
|
|
50
|
+
w.fsDeleteFolder(fdLog)
|
|
51
|
+
w.fsDeleteFolder(fdLogAfter)
|
|
52
|
+
for (let i = 0; i < 10; i++) {
|
|
53
|
+
w.fsDeleteFolder(`${fdLogChild}_${i}`)
|
|
54
|
+
w.fsDeleteFolder(`${fdLogStress}_${i}`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
describe('WSyslog clear', function() {
|
|
60
|
+
|
|
61
|
+
before(function() {
|
|
62
|
+
cleanAll()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
after(async function() {
|
|
66
|
+
await w.delay(100)
|
|
67
|
+
cleanAll()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('clear後檔案內容立即完整, 不需delay等flush', async function() {
|
|
71
|
+
|
|
72
|
+
let log = WSyslog({ fdLog, interval: 'day' })
|
|
73
|
+
log.info({ event: 'runner', msg: 'start' })
|
|
74
|
+
log.warn({ event: 'monitor-memory', msg: 'usage-high', ratio: 85.4 })
|
|
75
|
+
log.error({ event: 'crash', msg: 'db connection', code: 500 })
|
|
76
|
+
|
|
77
|
+
await log.clear()
|
|
78
|
+
|
|
79
|
+
//clear已保證殘餘log全數寫出, 立即讀檔驗證
|
|
80
|
+
let vpfs = w.fsTreeFolder(fdLog)
|
|
81
|
+
let fp = _.get(vpfs, `0.path`, '')
|
|
82
|
+
let jj = fs.readFileSync(fp, 'utf8')
|
|
83
|
+
let ss = w.sep(jj, '\n')
|
|
84
|
+
let r = _.map(ss, (j) => {
|
|
85
|
+
let v = JSON.parse(j)
|
|
86
|
+
return { event: _.get(v, 'event'), msg: _.get(v, 'msg') }
|
|
87
|
+
})
|
|
88
|
+
assert.strict.deepEqual(r, [
|
|
89
|
+
{ event: 'runner', msg: 'start' },
|
|
90
|
+
{ event: 'monitor-memory', msg: 'usage-high' },
|
|
91
|
+
{ event: 'crash', msg: 'db connection' },
|
|
92
|
+
])
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('重複clear回傳同一Promise(冪等)', async function() {
|
|
96
|
+
|
|
97
|
+
let log = WSyslog({ fdLog, interval: 'day' })
|
|
98
|
+
log.info({ event: 'idem', msg: 'x' })
|
|
99
|
+
|
|
100
|
+
let p1 = log.clear()
|
|
101
|
+
let p2 = log.clear()
|
|
102
|
+
assert.strict.equal(p1, p2)
|
|
103
|
+
|
|
104
|
+
await p1
|
|
105
|
+
|
|
106
|
+
//clear之後再呼叫依然回傳同一Promise
|
|
107
|
+
let p3 = log.clear()
|
|
108
|
+
assert.strict.equal(p1, p3)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('子程序寫300筆log後clear, 行程於數秒內自然退出且無漏筆', async function() {
|
|
112
|
+
|
|
113
|
+
let { code, stderr, ms } = await runNode([fpFxExit, fdLogChild, '1', '300'])
|
|
114
|
+
|
|
115
|
+
assert.strict.equal(code, 0, `子程序應exit 0, 實得code=${code}, stderr=${stderr}`)
|
|
116
|
+
assert.ok(ms < 20000, `子程序應於數秒內退出, 實測${ms}ms`)
|
|
117
|
+
assert.strict.equal(countLogLines(`${fdLogChild}_0`), 300)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('壓測: 10實例各寫300筆後逐一clear, 行程自然退出且無漏筆', async function() {
|
|
121
|
+
|
|
122
|
+
let { code, stderr } = await runNode([fpFxExit, fdLogStress, '10', '300'])
|
|
123
|
+
|
|
124
|
+
assert.strict.equal(code, 0, `子程序應exit 0, 實得code=${code}, stderr=${stderr}`)
|
|
125
|
+
for (let i = 0; i < 10; i++) {
|
|
126
|
+
assert.strict.equal(countLogLines(`${fdLogStress}_${i}`), 300, `實例${i}應有300筆log`)
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('clear後再寫log, 非同步丟the worker has exited(子程序exit非0)', async function() {
|
|
131
|
+
|
|
132
|
+
let { code, stderr } = await runNode([fpFxAfter, fdLogAfter])
|
|
133
|
+
|
|
134
|
+
assert.notStrictEqual(code, 0, 'clear後再寫log的子程序應以非0退出')
|
|
135
|
+
assert.ok(stderr.includes('the worker has exited'), `stderr應含'the worker has exited', 實得: ${stderr}`)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import WSyslog from '../../src/WSyslog.mjs'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
//子程序fixture: 建nInst個實例各寫nLogs筆log後逐一clear, 用於驗證行程能自然退出(不依賴exit hook)
|
|
5
|
+
//argv: [fdLogBase, nInst, nLogs], 各實例寫入`${fdLogBase}_${i}`資料夾
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
let fdLogBase = process.argv[2]
|
|
9
|
+
let nInst = parseInt(process.argv[3], 10)
|
|
10
|
+
let nLogs = parseInt(process.argv[4], 10)
|
|
11
|
+
|
|
12
|
+
let logs = []
|
|
13
|
+
for (let i = 0; i < nInst; i++) {
|
|
14
|
+
logs.push(WSyslog({ fdLog: `${fdLogBase}_${i}`, interval: 'day' }))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (let log of logs) {
|
|
18
|
+
for (let k = 0; k < nLogs; k++) {
|
|
19
|
+
log.info({ event: 'stress', msg: `m${k}` })
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (let log of logs) {
|
|
24
|
+
await log.clear()
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import WSyslog from '../../src/WSyslog.mjs'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
//子程序fixture: clear後再寫log, 預期非同步丟uncaughtException(the worker has exited)致exit code非0
|
|
5
|
+
//argv: [fdLog]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
let fdLog = process.argv[2]
|
|
9
|
+
|
|
10
|
+
let log = WSyslog({ fdLog, interval: 'day' })
|
|
11
|
+
log.info({ event: 'probe', msg: 'before-clear' })
|
|
12
|
+
|
|
13
|
+
await log.clear()
|
|
14
|
+
|
|
15
|
+
log.info({ event: 'probe', msg: 'after-clear' })
|
|
16
|
+
|
|
17
|
+
//保持存活讓非同步錯誤有時間浮現; 若未丟錯則計時器到期以exit 0結束, 由測試端斷言非0
|
|
18
|
+
setTimeout(() => {}, 3000)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import assert from 'assert'
|
|
3
|
+
import w from 'wsemi'
|
|
4
|
+
import WSyslog from '../src/WSyslog.mjs'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
//每個test各自獨立資料夾, 避免parallel/重跑干擾
|
|
8
|
+
let fdLogUtc = './_logs_tz_utc'
|
|
9
|
+
let fdLogTpe = './_logs_tz_tpe'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
function readHrKey(folder) {
|
|
13
|
+
if (!w.fsIsFolder(folder)) {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
let files = fs.readdirSync(folder).filter((fn) => /^\d{4}-\d{2}-\d{2}T\d{2}\.log$/.test(fn))
|
|
17
|
+
if (files.length !== 1) {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
return files[0].replace('.log', '') //例如'2025-12-08T15'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
describe('WSyslog timeZone', function() {
|
|
25
|
+
|
|
26
|
+
beforeEach(function() {
|
|
27
|
+
w.fsDeleteFolder(fdLogUtc)
|
|
28
|
+
w.fsDeleteFolder(fdLogTpe)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
afterEach(function() {
|
|
32
|
+
//clear已保證worker收攤與fd釋放, 直接移除測試資料夾
|
|
33
|
+
w.fsDeleteFolder(fdLogUtc)
|
|
34
|
+
w.fsDeleteFolder(fdLogTpe)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('timeZone="UTC" 寫log應使用UTC時區的小時作檔名 (模擬GitHub Actions UTC+0)', async function() {
|
|
38
|
+
|
|
39
|
+
let log = WSyslog({ fdLog: fdLogUtc, interval: 'hr', timeZone: 'UTC' })
|
|
40
|
+
log.info({ event: 'tz-utc', msg: 'check' })
|
|
41
|
+
|
|
42
|
+
await log.clear() //顯式關閉, 保證殘餘log已全數寫出
|
|
43
|
+
|
|
44
|
+
let actualKey = readHrKey(fdLogUtc)
|
|
45
|
+
assert.ok(actualKey !== null, `應有1個hr log檔, 取得actualKey=${actualKey}`)
|
|
46
|
+
|
|
47
|
+
//內建Date.toISOString()永遠輸出UTC時區之ISO字串, 取前13碼即'YYYY-MM-DDTHH'
|
|
48
|
+
let utcKey = new Date().toISOString().slice(0, 13)
|
|
49
|
+
let utcKeyPrev = new Date(Date.now() - 60 * 60 * 1000).toISOString().slice(0, 13)
|
|
50
|
+
|
|
51
|
+
//容忍跨小時boundary: 寫入與assert之間若橫跨整點, 接受前一小時
|
|
52
|
+
assert.ok(
|
|
53
|
+
actualKey === utcKey || actualKey === utcKeyPrev,
|
|
54
|
+
`actualKey="${actualKey}" 應為UTC當前小時"${utcKey}"或前一小時"${utcKeyPrev}"`
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('timeZone="UTC" 與 "Asia/Taipei" (UTC+8) 寫log應產生不同小時檔名', async function() {
|
|
59
|
+
|
|
60
|
+
let logUtc = WSyslog({ fdLog: fdLogUtc, interval: 'hr', timeZone: 'UTC' })
|
|
61
|
+
let logTpe = WSyslog({ fdLog: fdLogTpe, interval: 'hr', timeZone: 'Asia/Taipei' })
|
|
62
|
+
|
|
63
|
+
//同步呼叫兩者, 縮短時間差以避免跨小時boundary影響
|
|
64
|
+
logUtc.info({ msg: 'x' })
|
|
65
|
+
logTpe.info({ msg: 'x' })
|
|
66
|
+
|
|
67
|
+
await logUtc.clear()
|
|
68
|
+
await logTpe.clear()
|
|
69
|
+
|
|
70
|
+
let keyUtc = readHrKey(fdLogUtc)
|
|
71
|
+
let keyTpe = readHrKey(fdLogTpe)
|
|
72
|
+
|
|
73
|
+
assert.ok(keyUtc !== null, `UTC側應有1個hr log檔, 取得keyUtc=${keyUtc}`)
|
|
74
|
+
assert.ok(keyTpe !== null, `Taipei側應有1個hr log檔, 取得keyTpe=${keyTpe}`)
|
|
75
|
+
|
|
76
|
+
//UTC+0與UTC+8必差8小時, 兩個小時檔名永不相等(此assert不論系統時區為UTC+8或UTC+0皆成立)
|
|
77
|
+
assert.notStrictEqual(
|
|
78
|
+
keyUtc,
|
|
79
|
+
keyTpe,
|
|
80
|
+
`UTC與Asia/Taipei檔名應不同: keyUtc="${keyUtc}", keyTpe="${keyTpe}"`
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
})
|
package/_logs/2025-08-25T14.log
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
{"level":30,"time":1756103533307,"pid":13392,"hostname":"DESKTOP-6R7USAO","event":"runner","msg":"start"}
|
|
2
|
-
{"level":40,"time":1756103533308,"pid":13392,"hostname":"DESKTOP-6R7USAO","event":"monitor-memory","msg":"usage-high","ratio":85.4}
|
|
3
|
-
{"level":50,"time":1756103533308,"pid":13392,"hostname":"DESKTOP-6R7USAO","event":"crash","msg":"db connection","code":500}
|