工程提交

This commit is contained in:
2026-03-31 15:46:04 +08:00
parent 75f512a5b4
commit da4e944bca
2841 changed files with 4822938 additions and 1 deletions

View File

@@ -0,0 +1,789 @@
--[[
@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