Files
BR_YKC/4G/tools/resource/soc_script/v2025.12.31.22/lib/exmtn.lua
2026-03-31 15:46:04 +08:00

790 lines
24 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
--[[
@module exmtn
@summary 运维日志扩展库,负责日志的持久化存储
@version 1.0
@date 2025.12.9
@author zengeshuai
@usage
exmtn.init(1, 0) -- 初始化1个块缓存写入
exmtn.log("info", "tag", "message", 123) -- 输出运维日志
]]
local exmtn = {}
-- 常量定义
local LOG_MTN_CACHE_SIZE = 4096
local LOG_MTN_FILE_COUNT = 4
local LOG_MTN_CONFIG_FILE = "/exmtn.trc"
local LOG_MTN_DEFAULT_BLOCKS_DIVISOR = 40
local LOG_MTN_ADD_WRITE_THRESHOLD = 256
local LOG_MTN_CONFIG_VERSION = 1
-- 写入方式常量
exmtn.CACHE_WRITE = 0
exmtn.ADD_WRITE = 1
-- 内部状态
local ctx = {
inited = false,
enabled = false,
cur_index = 1, -- 1-4
block_size = 4096, -- 默认块大小
blocks_per_file = 1, -- 每文件块数
file_limit = 4096, -- 每文件大小限制
write_way = 0, -- 0=缓存写入, 1=直接追加
cache = "", -- 缓存缓冲区
cache_used = 0, -- 缓存已使用字节数
}
-- 重置缓存
local function reset_cache()
ctx.cache = ""
ctx.cache_used = 0
end
-- 获取当前文件路径
local function get_file_path(index)
return string.format("/hzmtn%d.trc", index or ctx.cur_index)
end
-- 获取当前文件大小
local function get_current_file_size()
local path = get_file_path()
local file = io.open(path, "rb")
if not file then
return 0
end
local size = file:seek("end")
file:close()
-- file:seek("end") 返回文件大小,如果失败返回 nil
if size and size > 0 then
return size
end
return 0
end
-- 检查文件是否存在
local function file_exists(path)
local file = io.open(path, "rb")
if file then
file:close()
return true
end
return false
end
-- 检查所有日志文件是否存在
local function files_exist()
for i = 1, LOG_MTN_FILE_COUNT do
local path = get_file_path(i)
if file_exists(path) then
return true
end
end
return false
end
-- 删除所有日志文件
local function remove_files()
for i = 1, LOG_MTN_FILE_COUNT do
local path = get_file_path(i)
os.remove(path)
end
end
-- 创建空文件
local function create_files()
for i = 1, LOG_MTN_FILE_COUNT do
local path = get_file_path(i)
local file = io.open(path, "wb")
if file then
file:close()
end
end
end
-- 读取配置文件
local function load_config()
local file = io.open(LOG_MTN_CONFIG_FILE, "rb")
if not file then
return nil -- 文件不存在,返回 nil
end
local content = file:read("*a")
file:close()
if not content or #content == 0 then
return nil -- 文件为空
end
-- 解析配置:格式为 "VERSION=1\nINDEX=2\nBLOCKS=10\nWRITE_WAY=0\n"
local config = {}
for line in content:gmatch("[^\r\n]+") do
-- 移除首尾空白字符
line = line:match("^%s*(.-)%s*$") or line
local key, value = line:match("([^=]+)=(.+)")
if key and value then
-- 移除 key 和 value 的首尾空白字符
key = key:match("^%s*(.-)%s*$") or key
value = value:match("^%s*(.-)%s*$") or value
local num_value = tonumber(value)
if num_value then
config[key] = num_value
else
config[key] = value
end
end
end
-- 验证版本号
if config.VERSION ~= LOG_MTN_CONFIG_VERSION then
return nil -- 版本不匹配
end
return config
end
-- 保存配置文件
local function save_config(index, blocks, write_way)
local content = string.format("VERSION=%d\nINDEX=%d\nBLOCKS=%d\nWRITE_WAY=%d\n",
LOG_MTN_CONFIG_VERSION, index, blocks, write_way)
local file = io.open(LOG_MTN_CONFIG_FILE, "wb")
if not file then
log.warn("exmtn", "无法打开配置文件: " .. LOG_MTN_CONFIG_FILE)
return false
end
local ok = file:write(content)
file:close()
if not ok then
log.warn("exmtn", "写入配置文件失败: " .. LOG_MTN_CONFIG_FILE)
return false
end
return true
end
-- 更新索引(同时保存完整配置)
local function update_index(index)
return save_config(index, ctx.blocks_per_file, ctx.write_way)
end
-- 格式化时间戳
-- 返回格式: [2025-11-05 15:06:49.947][00000027.994]
local function format_timestamp()
-- 获取系统运行时间(毫秒)
local ticks_ms = 0
if mcu and mcu.ticks then
local ticks = mcu.ticks()
if ticks then
ticks_ms = ticks
end
end
-- 获取当前日期时间
local date_time_str = ""
local ms = 0
if os and os.date then
-- 获取当前日期时间字符串: 2025-11-05 15:06:49
local dt = os.date("%Y-%m-%d %H:%M:%S")
if dt then
-- 计算毫秒:使用系统运行时间的毫秒部分
-- 如果 RTC 已设置,时间会更准确
ms = ticks_ms % 1000
date_time_str = string.format("%s.%03d", dt, ms)
end
end
-- 如果无法获取日期时间,使用默认格式
if date_time_str == "" then
date_time_str = "1970-01-01 00:00:00.000"
end
-- 计算系统运行时间(秒.毫秒)
local uptime_sec = math.floor(ticks_ms / 1000)
local uptime_ms = ticks_ms % 1000
-- 格式化运行时间部分: 00000027.994固定宽度9位整数+3位小数
local uptime_str = string.format("%09d.%03d", uptime_sec, uptime_ms)
-- 返回完整时间戳
return string.format("[%s][%s]", date_time_str, uptime_str)
end
-- 格式化调试信息
local function format_debug_info(level, include_level)
local info = debug.getinfo(2, "Sl")
if not info or not info.source then
return nil
end
local src = info.source
-- 跳过第一个字符(@ 或 =
if src:sub(1, 1) == "@" or src:sub(1, 1) == "=" then
src = src:sub(2)
end
local line = info.currentline or 0
if line > 64 * 1024 then
line = 0
end
if include_level and level then
return string.format("%s/%s:%d", level, src, line)
else
return string.format("%s:%d", src, line)
end
end
-- 格式化消息(与 log.info/warn/error 格式一致,但添加时间戳前缀)
local function format_message(level, tag, ...)
local argc = select("#", ...)
-- 获取 log.style 配置
local log_style = 0
if log and log.style then
log_style = log.style() or 0
end
-- 根据级别确定日志标识
local level_char = "I" -- 默认 info
if level == "warn" then
level_char = "W"
elseif level == "error" then
level_char = "E"
end
local msg = ""
local dbg_info_with_level = format_debug_info(level_char, true)
local dbg_info_only = format_debug_info(nil, false)
if log_style == 0 then
-- LOG_STYLE_NORMAL: "I/user.tag arg1 arg2 ...\n"
msg = string.format("%s/user.%s", level_char, tag)
for i = 1, argc do
local arg = select(i, ...)
msg = msg .. " " .. tostring(arg)
end
elseif log_style == 1 then
-- LOG_STYLE_DEBUG_INFO: "I/file.lua:123 tag arg1 arg2 ...\n"
if dbg_info_with_level then
msg = dbg_info_with_level
else
msg = level_char
end
msg = msg .. " " .. tag
for i = 1, argc do
local arg = select(i, ...)
msg = msg .. " " .. tostring(arg)
end
else
-- LOG_STYLE_FULL: "I/user.tag file.lua:123 arg1 arg2 ...\n"
msg = string.format("%s/user.%s", level_char, tag)
if dbg_info_only then
msg = msg .. " " .. dbg_info_only
end
for i = 1, argc do
local arg = select(i, ...)
msg = msg .. " " .. tostring(arg)
end
end
msg = msg .. "\n"
-- 添加时间戳前缀
local timestamp = format_timestamp()
return timestamp .. " " .. msg
end
-- 刷新缓存到文件
local function flush_cache()
if ctx.cache_used == 0 then
return true
end
local path = get_file_path()
local file = io.open(path, "ab")
if not file then
log.warn("exmtn", "无法打开文件: " .. path)
return false
end
-- file:write 返回 true/false 或 nil不返回字节数
local ok = file:write(ctx.cache)
file:close()
if not ok then
log.warn("exmtn", "写入文件失败: " .. path)
return false
end
reset_cache()
return true
end
-- 直接写入文件ADD_WRITE 模式)
local function direct_write(data)
local path = get_file_path()
local file = io.open(path, "ab")
if not file then
log.warn("exmtn", "无法打开文件: " .. path)
return false
end
-- file:write 返回 true/false 或 nil不返回字节数
local ok = file:write(data)
file:close()
if not ok then
log.warn("exmtn", "写入文件失败: " .. path)
return false
end
return true
end
-- 将数据追加到缓存或直接写入
local function buffer_append(data)
if not data or #data == 0 then
return true
end
local len = #data
-- ADD_WRITE 模式:直接写入文件
if ctx.write_way == exmtn.ADD_WRITE then
-- 小数据先缓存,累积到阈值再写入
if len < LOG_MTN_ADD_WRITE_THRESHOLD then
if ctx.cache_used + len > LOG_MTN_CACHE_SIZE then
if not flush_cache() then
return false
end
end
ctx.cache = ctx.cache .. data
ctx.cache_used = ctx.cache_used + len
-- 如果累积到阈值,立即写入
if ctx.cache_used >= LOG_MTN_ADD_WRITE_THRESHOLD then
return flush_cache()
end
return true
end
-- 大数据直接写入
return direct_write(data)
end
-- CACHE_WRITE 模式:原有逻辑
if len > LOG_MTN_CACHE_SIZE then
-- 先刷新缓存
if not flush_cache() then
return false
end
-- 大数据直接写入
return direct_write(data)
end
-- 检查缓存是否足够
if ctx.cache_used + len > LOG_MTN_CACHE_SIZE then
if not flush_cache() then
return false
end
end
ctx.cache = ctx.cache .. data
ctx.cache_used = ctx.cache_used + len
return true
end
-- 写入日志到文件
local function write_to_file(msg)
if not ctx.enabled then
return true -- 未启用时返回成功,不写入
end
local len = #msg
-- CACHE_WRITE 模式
if ctx.write_way == exmtn.CACHE_WRITE then
-- 检查文件大小 + 缓存大小 + 当前数据是否会超过限制
-- 如果会超过,先刷新缓存
if ctx.cache_used > 0 then
local file_sz = get_current_file_size()
if file_sz + ctx.cache_used + len > ctx.file_limit then
-- 先刷新缓存
if not flush_cache() then
return false
end
-- 重新获取文件大小
file_sz = get_current_file_size()
-- 检查文件是否已满
if file_sz >= ctx.file_limit then
-- 文件已满,切换到下一个文件
ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
local path = get_file_path()
local file = io.open(path, "wb")
if file then
file:close()
end
if not update_index(ctx.cur_index) then
log.warn("exmtn", "更新索引失败")
return false
end
reset_cache()
end
end
else
-- 缓存为空,检查文件大小
local file_sz = get_current_file_size()
if file_sz + len > ctx.file_limit then
-- 文件已满,切换到下一个文件
ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
local path = get_file_path()
local file = io.open(path, "wb")
if file then
file:close()
end
if not update_index(ctx.cur_index) then
log.warn("exmtn", "更新索引失败")
return false
end
reset_cache()
end
end
-- 如果加入这条数据后缓存会满,先刷新缓存
if ctx.cache_used + len > LOG_MTN_CACHE_SIZE then
if not flush_cache() then
return false
end
-- 刷新后再次检查文件大小
local file_sz = get_current_file_size()
if file_sz >= ctx.file_limit then
-- 文件已满,切换到下一个文件
ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
local path = get_file_path()
local file = io.open(path, "wb")
if file then
file:close()
end
if not update_index(ctx.cur_index) then
log.warn("exmtn", "更新索引失败")
return false
end
reset_cache()
end
end
-- 加入缓存
return buffer_append(msg)
else
-- ADD_WRITE 模式:先刷新缓存,确保文件大小准确
if ctx.cache_used > 0 then
if not flush_cache() then
return false
end
end
-- 获取当前文件大小
local file_sz = get_current_file_size()
-- 检查当前文件是否已写满
if file_sz >= ctx.file_limit then
-- 文件已满,切换到下一个文件
ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
local path = get_file_path()
local file = io.open(path, "wb")
if file then
file:close()
end
if not update_index(ctx.cur_index) then
log.warn("exmtn", "更新索引失败")
return false
end
reset_cache()
end
-- 检查当前数据是否能放入当前文件
if file_sz + len > ctx.file_limit then
-- 当前数据放不下,切换到下一个文件
ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
local path = get_file_path()
local file = io.open(path, "wb")
if file then
file:close()
end
if not update_index(ctx.cur_index) then
log.warn("exmtn", "更新索引失败")
return false
end
reset_cache()
end
-- 加入缓存或直接写入buffer_append 会根据大小决定)
return buffer_append(msg)
end
end
--[[
初始化运维日志
@api exmtn.init(blocks, write_way)
@int blocks 每个文件的块数0表示禁用正整数表示块数量
@int write_way 写入方式可选参数。exmtn.CACHE_WRITE(0)表示缓存写入exmtn.ADD_WRITE(1)表示直接追加写入默认为exmtn.CACHE_WRITE
@return boolean 成功返回true失败返回false
@usage
exmtn.init(1, exmtn.CACHE_WRITE) -- 初始化1个块缓存写入
]]
function exmtn.init(blocks, write_way)
-- 参数校验
if blocks == nil then
blocks = 0
end
blocks = math.floor(blocks)
if blocks < 0 then
log.warn("exmtn", "无效的块数")
return false
end
write_way = write_way or exmtn.CACHE_WRITE
if write_way ~= exmtn.CACHE_WRITE and write_way ~= exmtn.ADD_WRITE then
write_way = exmtn.CACHE_WRITE
end
-- 如果禁用
if blocks == 0 then
reset_cache()
remove_files()
ctx.enabled = false
ctx.cur_index = 1
-- 删除配置文件
os.remove(LOG_MTN_CONFIG_FILE)
ctx.inited = true
return true
end
-- 读取文件系统信息
if not ctx.inited then
ctx.block_size = 4096
ctx.blocks_per_file = 1
-- 尝试获取文件系统信息(需要 fs 模块支持)
-- fs.fsstat 返回: success, total_blocks, used_blocks, block_size, fs_type
if fs and fs.fsstat then
local success, total_blocks, used_blocks, block_size, fs_type = fs.fsstat("/")
if success and block_size and block_size > 0 then
ctx.block_size = block_size
if total_blocks and total_blocks > 0 then
local def_blocks = math.floor(total_blocks / LOG_MTN_DEFAULT_BLOCKS_DIVISOR)
if def_blocks > 0 then
ctx.blocks_per_file = def_blocks
end
end
end
end
end
-- 读取配置文件(仅在首次初始化时读取)
if not ctx.inited then
local config = load_config()
if config then
-- 读取索引
if config.INDEX and config.INDEX >= 1 and config.INDEX <= LOG_MTN_FILE_COUNT then
ctx.cur_index = config.INDEX
end
-- 读取块数配置
if config.BLOCKS and config.BLOCKS > 0 then
ctx.blocks_per_file = config.BLOCKS
end
-- 读取写入方式配置
if config.WRITE_WAY == 0 or config.WRITE_WAY == 1 then
ctx.write_way = config.WRITE_WAY
end
log.info("exmtn", "读取索引", ctx.cur_index)
log.info("exmtn", "读取块数配置", ctx.blocks_per_file)
log.info("exmtn", "读取写入方式配置", ctx.write_way)
end
end
-- 检查配置是否变化
-- 如果已初始化,比较当前配置和新配置;如果未初始化,不需要判断(首次初始化总是"变化"的)
local config_changed = false
if ctx.inited then
-- 已初始化:比较当前配置和新传入的配置
config_changed = (ctx.blocks_per_file ~= blocks) or (ctx.write_way ~= write_way)
end
-- 未初始化config_changed 保持为 false因为首次初始化不算"变化"
log.info("exmtn", "配置变化", config_changed)
-- 更新配置
ctx.blocks_per_file = blocks
ctx.write_way = write_way
ctx.file_limit = ctx.block_size * ctx.blocks_per_file
if ctx.file_limit == 0 then
ctx.file_limit = LOG_MTN_CACHE_SIZE
end
-- 处理文件的三种情况
if config_changed then
-- 情况1配置变化清空文件
log.info("exmtn", "配置变化,清空文件")
reset_cache()
remove_files()
create_files()
ctx.cur_index = 1
elseif files_exist() then
-- 情况2配置没有变化文件存在根据配置文件中保存的文件指针继续写
log.info("exmtn", "配置未变化,文件存在,继续写入")
-- ctx.cur_index 已经从配置文件读取(如果是首次初始化)或保持当前值(如果已初始化),不需要重置
else
-- 情况3配置没有变化文件不存在创建文件
log.info("exmtn", "配置未变化,文件不存在,创建文件")
create_files()
-- ctx.cur_index 已经从配置文件读取(如果是首次初始化)或保持当前值(如果已初始化),不需要重置
end
-- 保存配置到文件
if not save_config(ctx.cur_index, blocks, write_way) then
log.warn("exmtn", "保存配置失败")
return false
end
ctx.enabled = true
ctx.inited = true
-- 打印初始化信息
if blocks > 0 then
local total_size = ctx.file_limit * LOG_MTN_FILE_COUNT
local file_size_mb = ctx.file_limit / (1024 * 1024)
local total_size_mb = total_size / (1024 * 1024)
local file_size_kb = ctx.file_limit / 1024
local total_size_kb = total_size / 1024
if ctx.file_limit >= 1024 * 1024 then
log.info("exmtn", string.format("初始化成功: 每个文件 %.2f MB (%d 块 × %d 字节), 总空间 %.2f MB (%d 个文件)",
file_size_mb, ctx.blocks_per_file, ctx.block_size, total_size_mb, LOG_MTN_FILE_COUNT))
elseif ctx.file_limit >= 1024 then
log.info("exmtn", string.format("初始化成功: 每个文件 %.2f KB (%d 块 × %d 字节), 总空间 %.2f KB (%d 个文件)",
file_size_kb, ctx.blocks_per_file, ctx.block_size, total_size_kb, LOG_MTN_FILE_COUNT))
else
log.info("exmtn", string.format("初始化成功: 每个文件 %d 字节 (%d 块 × %d 字节), 总空间 %d 字节 (%d 个文件)",
ctx.file_limit, ctx.blocks_per_file, ctx.block_size, total_size, LOG_MTN_FILE_COUNT))
end
end
return true
end
--[[
输出运维日志并写入文件
@api exmtn.log(level, tag, ...)
@string level 日志级别,必须是 "info", "warn", 或 "error"
@string tag 日志标识,必须是字符串
@... 需打印的参数
@return boolean 成功返回true失败返回false
@usage
exmtn.log("info", "message", 123)
exmtn.log("warn", "message", 456)
exmtn.log("error", "message", 789)
]]
function exmtn.log(level, tag, ...)
if not level or type(level) ~= "string" then
log.warn("exmtn", "level 必须是字符串")
return false
end
if not tag or type(tag) ~= "string" then
log.warn("exmtn", "tag 必须是字符串")
return false
end
-- 根据级别调用对应的底层函数(会被日志级别过滤)
if level == "info" then
log.info(tag, ...)
elseif level == "warn" then
log.warn(tag, ...)
elseif level == "error" then
log.error(tag, ...)
else
log.warn("exmtn", "level 必须是 'info', 'warn' 或 'error'")
return false
end
-- 格式化消息(用于文件写入)
local msg = format_message(level, tag, ...)
if not msg then
log.warn("exmtn", "格式化消息失败")
return false
end
-- 写入文件(不受日志级别影响)
return write_to_file(msg)
end
--[[
获取当前配置
@api exmtn.get_config()
@return table|nil 配置信息失败返回nil
@usage
local config = exmtn.get_config()
if config then
log.info("exmtn", "blocks:", config.blocks, "write_way:", config.write_way)
end
]]
function exmtn.get_config()
if not ctx.inited then
return nil
end
return {
enabled = ctx.enabled,
cur_index = ctx.cur_index,
block_size = ctx.block_size,
blocks_per_file = ctx.blocks_per_file,
file_limit = ctx.file_limit,
write_way = ctx.write_way,
}
end
--[[
清除所有运维日志文件
@api exmtn.clear()
@return boolean 成功返回true失败返回false
@usage
local ok = exmtn.clear()
if ok then
log.info("exmtn", "日志文件已清除")
end
]]
function exmtn.clear()
-- 如果已初始化,先刷新缓存(确保数据不丢失)
if ctx.inited and ctx.cache_used > 0 then
if not flush_cache() then
return false
end
end
-- 删除所有日志文件
remove_files()
-- 重新创建空文件
create_files()
-- 重置索引为1
ctx.cur_index = 1
-- 更新配置文件
if not save_config(1, ctx.blocks_per_file, ctx.write_way) then
return false
end
log.info("exmtn", "运维日志文件已清除")
return true
end
return exmtn