exmtn.lua 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789
  1. --[[
  2. @module exmtn
  3. @summary 运维日志扩展库,负责日志的持久化存储
  4. @version 1.0
  5. @date 2025.12.9
  6. @author zengeshuai
  7. @usage
  8. exmtn.init(1, 0) -- 初始化,1个块,缓存写入
  9. exmtn.log("info", "tag", "message", 123) -- 输出运维日志
  10. ]]
  11. local exmtn = {}
  12. -- 常量定义
  13. local LOG_MTN_CACHE_SIZE = 4096
  14. local LOG_MTN_FILE_COUNT = 4
  15. local LOG_MTN_CONFIG_FILE = "/exmtn.trc"
  16. local LOG_MTN_DEFAULT_BLOCKS_DIVISOR = 40
  17. local LOG_MTN_ADD_WRITE_THRESHOLD = 256
  18. local LOG_MTN_CONFIG_VERSION = 1
  19. -- 写入方式常量
  20. exmtn.CACHE_WRITE = 0
  21. exmtn.ADD_WRITE = 1
  22. -- 内部状态
  23. local ctx = {
  24. inited = false,
  25. enabled = false,
  26. cur_index = 1, -- 1-4
  27. block_size = 4096, -- 默认块大小
  28. blocks_per_file = 1, -- 每文件块数
  29. file_limit = 4096, -- 每文件大小限制
  30. write_way = 0, -- 0=缓存写入, 1=直接追加
  31. cache = "", -- 缓存缓冲区
  32. cache_used = 0, -- 缓存已使用字节数
  33. }
  34. -- 重置缓存
  35. local function reset_cache()
  36. ctx.cache = ""
  37. ctx.cache_used = 0
  38. end
  39. -- 获取当前文件路径
  40. local function get_file_path(index)
  41. return string.format("/hzmtn%d.trc", index or ctx.cur_index)
  42. end
  43. -- 获取当前文件大小
  44. local function get_current_file_size()
  45. local path = get_file_path()
  46. local file = io.open(path, "rb")
  47. if not file then
  48. return 0
  49. end
  50. local size = file:seek("end")
  51. file:close()
  52. -- file:seek("end") 返回文件大小,如果失败返回 nil
  53. if size and size > 0 then
  54. return size
  55. end
  56. return 0
  57. end
  58. -- 检查文件是否存在
  59. local function file_exists(path)
  60. local file = io.open(path, "rb")
  61. if file then
  62. file:close()
  63. return true
  64. end
  65. return false
  66. end
  67. -- 检查所有日志文件是否存在
  68. local function files_exist()
  69. for i = 1, LOG_MTN_FILE_COUNT do
  70. local path = get_file_path(i)
  71. if file_exists(path) then
  72. return true
  73. end
  74. end
  75. return false
  76. end
  77. -- 删除所有日志文件
  78. local function remove_files()
  79. for i = 1, LOG_MTN_FILE_COUNT do
  80. local path = get_file_path(i)
  81. os.remove(path)
  82. end
  83. end
  84. -- 创建空文件
  85. local function create_files()
  86. for i = 1, LOG_MTN_FILE_COUNT do
  87. local path = get_file_path(i)
  88. local file = io.open(path, "wb")
  89. if file then
  90. file:close()
  91. end
  92. end
  93. end
  94. -- 读取配置文件
  95. local function load_config()
  96. local file = io.open(LOG_MTN_CONFIG_FILE, "rb")
  97. if not file then
  98. return nil -- 文件不存在,返回 nil
  99. end
  100. local content = file:read("*a")
  101. file:close()
  102. if not content or #content == 0 then
  103. return nil -- 文件为空
  104. end
  105. -- 解析配置:格式为 "VERSION=1\nINDEX=2\nBLOCKS=10\nWRITE_WAY=0\n"
  106. local config = {}
  107. for line in content:gmatch("[^\r\n]+") do
  108. -- 移除首尾空白字符
  109. line = line:match("^%s*(.-)%s*$") or line
  110. local key, value = line:match("([^=]+)=(.+)")
  111. if key and value then
  112. -- 移除 key 和 value 的首尾空白字符
  113. key = key:match("^%s*(.-)%s*$") or key
  114. value = value:match("^%s*(.-)%s*$") or value
  115. local num_value = tonumber(value)
  116. if num_value then
  117. config[key] = num_value
  118. else
  119. config[key] = value
  120. end
  121. end
  122. end
  123. -- 验证版本号
  124. if config.VERSION ~= LOG_MTN_CONFIG_VERSION then
  125. return nil -- 版本不匹配
  126. end
  127. return config
  128. end
  129. -- 保存配置文件
  130. local function save_config(index, blocks, write_way)
  131. local content = string.format("VERSION=%d\nINDEX=%d\nBLOCKS=%d\nWRITE_WAY=%d\n",
  132. LOG_MTN_CONFIG_VERSION, index, blocks, write_way)
  133. local file = io.open(LOG_MTN_CONFIG_FILE, "wb")
  134. if not file then
  135. log.warn("exmtn", "无法打开配置文件: " .. LOG_MTN_CONFIG_FILE)
  136. return false
  137. end
  138. local ok = file:write(content)
  139. file:close()
  140. if not ok then
  141. log.warn("exmtn", "写入配置文件失败: " .. LOG_MTN_CONFIG_FILE)
  142. return false
  143. end
  144. return true
  145. end
  146. -- 更新索引(同时保存完整配置)
  147. local function update_index(index)
  148. return save_config(index, ctx.blocks_per_file, ctx.write_way)
  149. end
  150. -- 格式化时间戳
  151. -- 返回格式: [2025-11-05 15:06:49.947][00000027.994]
  152. local function format_timestamp()
  153. -- 获取系统运行时间(毫秒)
  154. local ticks_ms = 0
  155. if mcu and mcu.ticks then
  156. local ticks = mcu.ticks()
  157. if ticks then
  158. ticks_ms = ticks
  159. end
  160. end
  161. -- 获取当前日期时间
  162. local date_time_str = ""
  163. local ms = 0
  164. if os and os.date then
  165. -- 获取当前日期时间字符串: 2025-11-05 15:06:49
  166. local dt = os.date("%Y-%m-%d %H:%M:%S")
  167. if dt then
  168. -- 计算毫秒:使用系统运行时间的毫秒部分
  169. -- 如果 RTC 已设置,时间会更准确
  170. ms = ticks_ms % 1000
  171. date_time_str = string.format("%s.%03d", dt, ms)
  172. end
  173. end
  174. -- 如果无法获取日期时间,使用默认格式
  175. if date_time_str == "" then
  176. date_time_str = "1970-01-01 00:00:00.000"
  177. end
  178. -- 计算系统运行时间(秒.毫秒)
  179. local uptime_sec = math.floor(ticks_ms / 1000)
  180. local uptime_ms = ticks_ms % 1000
  181. -- 格式化运行时间部分: 00000027.994(固定宽度,9位整数+3位小数)
  182. local uptime_str = string.format("%09d.%03d", uptime_sec, uptime_ms)
  183. -- 返回完整时间戳
  184. return string.format("[%s][%s]", date_time_str, uptime_str)
  185. end
  186. -- 格式化调试信息
  187. local function format_debug_info(level, include_level)
  188. local info = debug.getinfo(2, "Sl")
  189. if not info or not info.source then
  190. return nil
  191. end
  192. local src = info.source
  193. -- 跳过第一个字符(@ 或 =)
  194. if src:sub(1, 1) == "@" or src:sub(1, 1) == "=" then
  195. src = src:sub(2)
  196. end
  197. local line = info.currentline or 0
  198. if line > 64 * 1024 then
  199. line = 0
  200. end
  201. if include_level and level then
  202. return string.format("%s/%s:%d", level, src, line)
  203. else
  204. return string.format("%s:%d", src, line)
  205. end
  206. end
  207. -- 格式化消息(与 log.info/warn/error 格式一致,但添加时间戳前缀)
  208. local function format_message(level, tag, ...)
  209. local argc = select("#", ...)
  210. -- 获取 log.style 配置
  211. local log_style = 0
  212. if log and log.style then
  213. log_style = log.style() or 0
  214. end
  215. -- 根据级别确定日志标识
  216. local level_char = "I" -- 默认 info
  217. if level == "warn" then
  218. level_char = "W"
  219. elseif level == "error" then
  220. level_char = "E"
  221. end
  222. local msg = ""
  223. local dbg_info_with_level = format_debug_info(level_char, true)
  224. local dbg_info_only = format_debug_info(nil, false)
  225. if log_style == 0 then
  226. -- LOG_STYLE_NORMAL: "I/user.tag arg1 arg2 ...\n"
  227. msg = string.format("%s/user.%s", level_char, tag)
  228. for i = 1, argc do
  229. local arg = select(i, ...)
  230. msg = msg .. " " .. tostring(arg)
  231. end
  232. elseif log_style == 1 then
  233. -- LOG_STYLE_DEBUG_INFO: "I/file.lua:123 tag arg1 arg2 ...\n"
  234. if dbg_info_with_level then
  235. msg = dbg_info_with_level
  236. else
  237. msg = level_char
  238. end
  239. msg = msg .. " " .. tag
  240. for i = 1, argc do
  241. local arg = select(i, ...)
  242. msg = msg .. " " .. tostring(arg)
  243. end
  244. else
  245. -- LOG_STYLE_FULL: "I/user.tag file.lua:123 arg1 arg2 ...\n"
  246. msg = string.format("%s/user.%s", level_char, tag)
  247. if dbg_info_only then
  248. msg = msg .. " " .. dbg_info_only
  249. end
  250. for i = 1, argc do
  251. local arg = select(i, ...)
  252. msg = msg .. " " .. tostring(arg)
  253. end
  254. end
  255. msg = msg .. "\n"
  256. -- 添加时间戳前缀
  257. local timestamp = format_timestamp()
  258. return timestamp .. " " .. msg
  259. end
  260. -- 刷新缓存到文件
  261. local function flush_cache()
  262. if ctx.cache_used == 0 then
  263. return true
  264. end
  265. local path = get_file_path()
  266. local file = io.open(path, "ab")
  267. if not file then
  268. log.warn("exmtn", "无法打开文件: " .. path)
  269. return false
  270. end
  271. -- file:write 返回 true/false 或 nil,不返回字节数
  272. local ok = file:write(ctx.cache)
  273. file:close()
  274. if not ok then
  275. log.warn("exmtn", "写入文件失败: " .. path)
  276. return false
  277. end
  278. reset_cache()
  279. return true
  280. end
  281. -- 直接写入文件(ADD_WRITE 模式)
  282. local function direct_write(data)
  283. local path = get_file_path()
  284. local file = io.open(path, "ab")
  285. if not file then
  286. log.warn("exmtn", "无法打开文件: " .. path)
  287. return false
  288. end
  289. -- file:write 返回 true/false 或 nil,不返回字节数
  290. local ok = file:write(data)
  291. file:close()
  292. if not ok then
  293. log.warn("exmtn", "写入文件失败: " .. path)
  294. return false
  295. end
  296. return true
  297. end
  298. -- 将数据追加到缓存或直接写入
  299. local function buffer_append(data)
  300. if not data or #data == 0 then
  301. return true
  302. end
  303. local len = #data
  304. -- ADD_WRITE 模式:直接写入文件
  305. if ctx.write_way == exmtn.ADD_WRITE then
  306. -- 小数据先缓存,累积到阈值再写入
  307. if len < LOG_MTN_ADD_WRITE_THRESHOLD then
  308. if ctx.cache_used + len > LOG_MTN_CACHE_SIZE then
  309. if not flush_cache() then
  310. return false
  311. end
  312. end
  313. ctx.cache = ctx.cache .. data
  314. ctx.cache_used = ctx.cache_used + len
  315. -- 如果累积到阈值,立即写入
  316. if ctx.cache_used >= LOG_MTN_ADD_WRITE_THRESHOLD then
  317. return flush_cache()
  318. end
  319. return true
  320. end
  321. -- 大数据直接写入
  322. return direct_write(data)
  323. end
  324. -- CACHE_WRITE 模式:原有逻辑
  325. if len > LOG_MTN_CACHE_SIZE then
  326. -- 先刷新缓存
  327. if not flush_cache() then
  328. return false
  329. end
  330. -- 大数据直接写入
  331. return direct_write(data)
  332. end
  333. -- 检查缓存是否足够
  334. if ctx.cache_used + len > LOG_MTN_CACHE_SIZE then
  335. if not flush_cache() then
  336. return false
  337. end
  338. end
  339. ctx.cache = ctx.cache .. data
  340. ctx.cache_used = ctx.cache_used + len
  341. return true
  342. end
  343. -- 写入日志到文件
  344. local function write_to_file(msg)
  345. if not ctx.enabled then
  346. return true -- 未启用时返回成功,不写入
  347. end
  348. local len = #msg
  349. -- CACHE_WRITE 模式
  350. if ctx.write_way == exmtn.CACHE_WRITE then
  351. -- 检查文件大小 + 缓存大小 + 当前数据是否会超过限制
  352. -- 如果会超过,先刷新缓存
  353. if ctx.cache_used > 0 then
  354. local file_sz = get_current_file_size()
  355. if file_sz + ctx.cache_used + len > ctx.file_limit then
  356. -- 先刷新缓存
  357. if not flush_cache() then
  358. return false
  359. end
  360. -- 重新获取文件大小
  361. file_sz = get_current_file_size()
  362. -- 检查文件是否已满
  363. if file_sz >= ctx.file_limit then
  364. -- 文件已满,切换到下一个文件
  365. ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
  366. local path = get_file_path()
  367. local file = io.open(path, "wb")
  368. if file then
  369. file:close()
  370. end
  371. if not update_index(ctx.cur_index) then
  372. log.warn("exmtn", "更新索引失败")
  373. return false
  374. end
  375. reset_cache()
  376. end
  377. end
  378. else
  379. -- 缓存为空,检查文件大小
  380. local file_sz = get_current_file_size()
  381. if file_sz + len > ctx.file_limit then
  382. -- 文件已满,切换到下一个文件
  383. ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
  384. local path = get_file_path()
  385. local file = io.open(path, "wb")
  386. if file then
  387. file:close()
  388. end
  389. if not update_index(ctx.cur_index) then
  390. log.warn("exmtn", "更新索引失败")
  391. return false
  392. end
  393. reset_cache()
  394. end
  395. end
  396. -- 如果加入这条数据后缓存会满,先刷新缓存
  397. if ctx.cache_used + len > LOG_MTN_CACHE_SIZE then
  398. if not flush_cache() then
  399. return false
  400. end
  401. -- 刷新后再次检查文件大小
  402. local file_sz = get_current_file_size()
  403. if file_sz >= ctx.file_limit then
  404. -- 文件已满,切换到下一个文件
  405. ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
  406. local path = get_file_path()
  407. local file = io.open(path, "wb")
  408. if file then
  409. file:close()
  410. end
  411. if not update_index(ctx.cur_index) then
  412. log.warn("exmtn", "更新索引失败")
  413. return false
  414. end
  415. reset_cache()
  416. end
  417. end
  418. -- 加入缓存
  419. return buffer_append(msg)
  420. else
  421. -- ADD_WRITE 模式:先刷新缓存,确保文件大小准确
  422. if ctx.cache_used > 0 then
  423. if not flush_cache() then
  424. return false
  425. end
  426. end
  427. -- 获取当前文件大小
  428. local file_sz = get_current_file_size()
  429. -- 检查当前文件是否已写满
  430. if file_sz >= ctx.file_limit then
  431. -- 文件已满,切换到下一个文件
  432. ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
  433. local path = get_file_path()
  434. local file = io.open(path, "wb")
  435. if file then
  436. file:close()
  437. end
  438. if not update_index(ctx.cur_index) then
  439. log.warn("exmtn", "更新索引失败")
  440. return false
  441. end
  442. reset_cache()
  443. end
  444. -- 检查当前数据是否能放入当前文件
  445. if file_sz + len > ctx.file_limit then
  446. -- 当前数据放不下,切换到下一个文件
  447. ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
  448. local path = get_file_path()
  449. local file = io.open(path, "wb")
  450. if file then
  451. file:close()
  452. end
  453. if not update_index(ctx.cur_index) then
  454. log.warn("exmtn", "更新索引失败")
  455. return false
  456. end
  457. reset_cache()
  458. end
  459. -- 加入缓存或直接写入(buffer_append 会根据大小决定)
  460. return buffer_append(msg)
  461. end
  462. end
  463. --[[
  464. 初始化运维日志
  465. @api exmtn.init(blocks, write_way)
  466. @int blocks 每个文件的块数,0表示禁用,正整数表示块数量
  467. @int write_way 写入方式,可选参数。exmtn.CACHE_WRITE(0)表示缓存写入,exmtn.ADD_WRITE(1)表示直接追加写入,默认为exmtn.CACHE_WRITE
  468. @return boolean 成功返回true,失败返回false
  469. @usage
  470. exmtn.init(1, exmtn.CACHE_WRITE) -- 初始化,1个块,缓存写入
  471. ]]
  472. function exmtn.init(blocks, write_way)
  473. -- 参数校验
  474. if blocks == nil then
  475. blocks = 0
  476. end
  477. blocks = math.floor(blocks)
  478. if blocks < 0 then
  479. log.warn("exmtn", "无效的块数")
  480. return false
  481. end
  482. write_way = write_way or exmtn.CACHE_WRITE
  483. if write_way ~= exmtn.CACHE_WRITE and write_way ~= exmtn.ADD_WRITE then
  484. write_way = exmtn.CACHE_WRITE
  485. end
  486. -- 如果禁用
  487. if blocks == 0 then
  488. reset_cache()
  489. remove_files()
  490. ctx.enabled = false
  491. ctx.cur_index = 1
  492. -- 删除配置文件
  493. os.remove(LOG_MTN_CONFIG_FILE)
  494. ctx.inited = true
  495. return true
  496. end
  497. -- 读取文件系统信息
  498. if not ctx.inited then
  499. ctx.block_size = 4096
  500. ctx.blocks_per_file = 1
  501. -- 尝试获取文件系统信息(需要 fs 模块支持)
  502. -- fs.fsstat 返回: success, total_blocks, used_blocks, block_size, fs_type
  503. if fs and fs.fsstat then
  504. local success, total_blocks, used_blocks, block_size, fs_type = fs.fsstat("/")
  505. if success and block_size and block_size > 0 then
  506. ctx.block_size = block_size
  507. if total_blocks and total_blocks > 0 then
  508. local def_blocks = math.floor(total_blocks / LOG_MTN_DEFAULT_BLOCKS_DIVISOR)
  509. if def_blocks > 0 then
  510. ctx.blocks_per_file = def_blocks
  511. end
  512. end
  513. end
  514. end
  515. end
  516. -- 读取配置文件(仅在首次初始化时读取)
  517. if not ctx.inited then
  518. local config = load_config()
  519. if config then
  520. -- 读取索引
  521. if config.INDEX and config.INDEX >= 1 and config.INDEX <= LOG_MTN_FILE_COUNT then
  522. ctx.cur_index = config.INDEX
  523. end
  524. -- 读取块数配置
  525. if config.BLOCKS and config.BLOCKS > 0 then
  526. ctx.blocks_per_file = config.BLOCKS
  527. end
  528. -- 读取写入方式配置
  529. if config.WRITE_WAY == 0 or config.WRITE_WAY == 1 then
  530. ctx.write_way = config.WRITE_WAY
  531. end
  532. log.info("exmtn", "读取索引", ctx.cur_index)
  533. log.info("exmtn", "读取块数配置", ctx.blocks_per_file)
  534. log.info("exmtn", "读取写入方式配置", ctx.write_way)
  535. end
  536. end
  537. -- 检查配置是否变化
  538. -- 如果已初始化,比较当前配置和新配置;如果未初始化,不需要判断(首次初始化总是"变化"的)
  539. local config_changed = false
  540. if ctx.inited then
  541. -- 已初始化:比较当前配置和新传入的配置
  542. config_changed = (ctx.blocks_per_file ~= blocks) or (ctx.write_way ~= write_way)
  543. end
  544. -- 未初始化:config_changed 保持为 false,因为首次初始化不算"变化"
  545. log.info("exmtn", "配置变化", config_changed)
  546. -- 更新配置
  547. ctx.blocks_per_file = blocks
  548. ctx.write_way = write_way
  549. ctx.file_limit = ctx.block_size * ctx.blocks_per_file
  550. if ctx.file_limit == 0 then
  551. ctx.file_limit = LOG_MTN_CACHE_SIZE
  552. end
  553. -- 处理文件的三种情况
  554. if config_changed then
  555. -- 情况1:配置变化,清空文件
  556. log.info("exmtn", "配置变化,清空文件")
  557. reset_cache()
  558. remove_files()
  559. create_files()
  560. ctx.cur_index = 1
  561. elseif files_exist() then
  562. -- 情况2:配置没有变化,文件存在,根据配置文件中保存的文件指针继续写
  563. log.info("exmtn", "配置未变化,文件存在,继续写入")
  564. -- ctx.cur_index 已经从配置文件读取(如果是首次初始化)或保持当前值(如果已初始化),不需要重置
  565. else
  566. -- 情况3:配置没有变化,文件不存在,创建文件
  567. log.info("exmtn", "配置未变化,文件不存在,创建文件")
  568. create_files()
  569. -- ctx.cur_index 已经从配置文件读取(如果是首次初始化)或保持当前值(如果已初始化),不需要重置
  570. end
  571. -- 保存配置到文件
  572. if not save_config(ctx.cur_index, blocks, write_way) then
  573. log.warn("exmtn", "保存配置失败")
  574. return false
  575. end
  576. ctx.enabled = true
  577. ctx.inited = true
  578. -- 打印初始化信息
  579. if blocks > 0 then
  580. local total_size = ctx.file_limit * LOG_MTN_FILE_COUNT
  581. local file_size_mb = ctx.file_limit / (1024 * 1024)
  582. local total_size_mb = total_size / (1024 * 1024)
  583. local file_size_kb = ctx.file_limit / 1024
  584. local total_size_kb = total_size / 1024
  585. if ctx.file_limit >= 1024 * 1024 then
  586. log.info("exmtn", string.format("初始化成功: 每个文件 %.2f MB (%d 块 × %d 字节), 总空间 %.2f MB (%d 个文件)",
  587. file_size_mb, ctx.blocks_per_file, ctx.block_size, total_size_mb, LOG_MTN_FILE_COUNT))
  588. elseif ctx.file_limit >= 1024 then
  589. log.info("exmtn", string.format("初始化成功: 每个文件 %.2f KB (%d 块 × %d 字节), 总空间 %.2f KB (%d 个文件)",
  590. file_size_kb, ctx.blocks_per_file, ctx.block_size, total_size_kb, LOG_MTN_FILE_COUNT))
  591. else
  592. log.info("exmtn", string.format("初始化成功: 每个文件 %d 字节 (%d 块 × %d 字节), 总空间 %d 字节 (%d 个文件)",
  593. ctx.file_limit, ctx.blocks_per_file, ctx.block_size, total_size, LOG_MTN_FILE_COUNT))
  594. end
  595. end
  596. return true
  597. end
  598. --[[
  599. 输出运维日志并写入文件
  600. @api exmtn.log(level, tag, ...)
  601. @string level 日志级别,必须是 "info", "warn", 或 "error"
  602. @string tag 日志标识,必须是字符串
  603. @... 需打印的参数
  604. @return boolean 成功返回true,失败返回false
  605. @usage
  606. exmtn.log("info", "message", 123)
  607. exmtn.log("warn", "message", 456)
  608. exmtn.log("error", "message", 789)
  609. ]]
  610. function exmtn.log(level, tag, ...)
  611. if not level or type(level) ~= "string" then
  612. log.warn("exmtn", "level 必须是字符串")
  613. return false
  614. end
  615. if not tag or type(tag) ~= "string" then
  616. log.warn("exmtn", "tag 必须是字符串")
  617. return false
  618. end
  619. -- 根据级别调用对应的底层函数(会被日志级别过滤)
  620. if level == "info" then
  621. log.info(tag, ...)
  622. elseif level == "warn" then
  623. log.warn(tag, ...)
  624. elseif level == "error" then
  625. log.error(tag, ...)
  626. else
  627. log.warn("exmtn", "level 必须是 'info', 'warn' 或 'error'")
  628. return false
  629. end
  630. -- 格式化消息(用于文件写入)
  631. local msg = format_message(level, tag, ...)
  632. if not msg then
  633. log.warn("exmtn", "格式化消息失败")
  634. return false
  635. end
  636. -- 写入文件(不受日志级别影响)
  637. return write_to_file(msg)
  638. end
  639. --[[
  640. 获取当前配置
  641. @api exmtn.get_config()
  642. @return table|nil 配置信息,失败返回nil
  643. @usage
  644. local config = exmtn.get_config()
  645. if config then
  646. log.info("exmtn", "blocks:", config.blocks, "write_way:", config.write_way)
  647. end
  648. ]]
  649. function exmtn.get_config()
  650. if not ctx.inited then
  651. return nil
  652. end
  653. return {
  654. enabled = ctx.enabled,
  655. cur_index = ctx.cur_index,
  656. block_size = ctx.block_size,
  657. blocks_per_file = ctx.blocks_per_file,
  658. file_limit = ctx.file_limit,
  659. write_way = ctx.write_way,
  660. }
  661. end
  662. --[[
  663. 清除所有运维日志文件
  664. @api exmtn.clear()
  665. @return boolean 成功返回true,失败返回false
  666. @usage
  667. local ok = exmtn.clear()
  668. if ok then
  669. log.info("exmtn", "日志文件已清除")
  670. end
  671. ]]
  672. function exmtn.clear()
  673. -- 如果已初始化,先刷新缓存(确保数据不丢失)
  674. if ctx.inited and ctx.cache_used > 0 then
  675. if not flush_cache() then
  676. return false
  677. end
  678. end
  679. -- 删除所有日志文件
  680. remove_files()
  681. -- 重新创建空文件
  682. create_files()
  683. -- 重置索引为1
  684. ctx.cur_index = 1
  685. -- 更新配置文件
  686. if not save_config(1, ctx.blocks_per_file, ctx.write_way) then
  687. return false
  688. end
  689. log.info("exmtn", "运维日志文件已清除")
  690. return true
  691. end
  692. return exmtn