import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import get from 'lodash-es/get.js'
import evem from 'wsemi/src/evem.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import ispint from 'wsemi/src/ispint.mjs'
import cint from 'wsemi/src/cint.mjs'
import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
import pino from 'pino'
//__dirname
let __filename = fileURLToPath(import.meta.url)
let __dirname = path.dirname(__filename)
/**
* 輸出log
*
* @class
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {String} [opt.fdLog='./_logs'] 輸入儲存log資料夾字串,預設'./_logs'
* @param {String} [opt.interval='day'] 輸入儲存log時分檔模式字串,可選'day'、'hr',分別代表每日或每時分檔,預設'day'
* @param {String} [opt.timeZone=null] 輸入時區字串(IANA格式如'Asia/Taipei'、'UTC'),用於決定切檔邊界與檔名,預設null代表使用系統時區
* @param {Number} [opt.numKeep] 輸入保留log檔案數量整數,超出最舊檔將被自動刪除,預設於day模式為365、hr模式為365*24(約等同保留一年)
* @returns {Object} 回傳log物件,提供fatal、error、warn、info、debug、trace紀錄函數,cleanLogs手動清理函數,及clear清理計時器函數
* @example
* import fs from 'fs'
* import _ from 'lodash-es'
* import w from 'wsemi'
* import WSyslog from './src/WSyslog.mjs'
*
* w.fsCleanFolder('./_logs')
*
* let log = WSyslog()
* log.info({ event: 'runner', msg: 'start' })
* log.warn({ event: 'monitor-memory', msg: 'usage-high', ratio: 85.4 })
* log.error({ event: 'crash', msg: 'db connection', code: 500 })
*
* await w.delay(2000) //等待2秒讓pino能flush數據
*
* let vpfs = w.fsTreeFolder('./_logs')
* // console.log('vpfs', vpfs)
*
* let fp = _.get(vpfs, `0.path`, '')
*
* let jj = fs.readFileSync(fp, 'utf8')
* console.log(jj)
* // {"level":30,"time":1751780174415,"pid":24144,"hostname":"DESKTOP-6R7USAO","event":"runner","msg":"start"}
* // {"level":40,"time":1751780174415,"pid":24144,"hostname":"DESKTOP-6R7USAO","event":"monitor-memory","msg":"usage-high"}
* // {"level":50,"time":1751780174415,"pid":24144,"hostname":"DESKTOP-6R7USAO","event":"crash","msg":"db connection","code":500}
*
*/
function WSyslog(opt = {}) {
//ev
let ev = evem()
//timeZone, 'Asia/Taipei'
let timeZone = get(opt, 'timeZone', null)
//fdLog
let fdLog = get(opt, 'fdLog')
if (!isestr(fdLog)) {
fdLog = './_logs'
}
//interval
let interval = get(opt, 'interval')
if (interval !== 'day' && interval !== 'hr') {
interval = 'day'
}
//numKeep
let numKeep = get(opt, 'numKeep')
if (!ispint(numKeep)) {
if (interval === 'day') {
numKeep = 365
}
else {
numKeep = 365 * 24
}
}
numKeep = cint(numKeep)
// 建立 logger,使用自訂 transport
let transport = pino.transport({
targets: [
{
// fatal: 60
// error: 50
// warn: 40
// info: 30
// debug: 20
// trace: 10
level: 'info', //僅紀錄info(30)以上
target: path.resolve(__dirname, './formatter.mjs'),
options: {
interval,
fdLog,
timeZone,
},
},
]
})
//log
let log = pino(transport)
// console.log('log', log)
//cleanLogs, 依numKeep保留最新N個檔, 超出的最舊檔刪除
let cleanLogs = () => {
try {
if (!fsIsFolder(fdLog)) {
return
}
//pattern, day模式為'YYYY-MM-DD.log', hr模式為'YYYY-MM-DDTHH.log'
let pattern = (interval === 'hr')
? /^\d{4}-\d{2}-\d{2}T\d{2}\.log$/
: /^\d{4}-\d{2}-\d{2}\.log$/
let files = fs.readdirSync(fdLog).filter((fn) => pattern.test(fn))
//ISO日期格式字典序等同時間序, 排序後最舊在前
files.sort()
let nDel = files.length - numKeep
if (nDel <= 0) {
return
}
let toDelete = files.slice(0, nDel)
for (let fn of toDelete) {
let fp = path.join(fdLog, fn)
try {
fs.unlinkSync(fp)
ev.emit('delete', { fp, fn })
}
catch (err) {
//容忍其他process同時刪除或檔案被佔用之情況
}
}
}
catch (err) {
console.error('WSyslog cleanLogs error:', err)
}
}
//啟動時立刻清理一次, 確保短命腳本也能執行清理
cleanLogs()
//之後每小時清理一次, 足以跟上day/hr切檔節奏
let t = setInterval(cleanLogs, 60 * 60 * 1000)
//unref不阻塞process結束
t.unref()
//clear
let clear = () => {
clearInterval(t)
}
//wrap, 包裝pino各等級method, 於寫log前先觸發'log'事件, 使外部可監聽進行console輸出或轉送其他記錄器
// fatal: 60
// error: 50
// warn: 40
// info: 30
// debug: 20
// trace: 10
let wrap = (level) => {
let fn = log[level].bind(log)
return (...args) => {
try {
ev.emit(level, { args })
}
catch (err) {
//容忍listener內部錯誤, 避免影響pino寫log
}
fn(...args)
}
}
ev.fatal = wrap('fatal')
ev.error = wrap('error')
ev.warn = wrap('warn')
ev.info = wrap('info')
ev.debug = wrap('debug')
ev.trace = wrap('trace')
ev.cleanLogs = cleanLogs
ev.clear = clear
return ev
}
export default WSyslog