#!/usr/bin/lua5.1 -- Created by jiaqiang.ye@lnxall.com -- Simple Syslog-On-Tmpfs Monitor -- 2025/03/20 local cjson = require "cjson" local invoker = require "invoker" local gfmt = string.format local g_MaxSize = 0x2000000 -- 32MB local g_compinter = 8 * 3600 -- 6 hours local g_keepdays = 62 * 86400 -- 62 days local g_maxallsize = 0x60000000 -- 1.5 GB local g_tmplogcfg = "/etc/tmpsyslog.json" local g_comdir = "/var/log/compressed" local g_lastdir = "/var/log/last_syslog" local g_compid = nil -- PID of compressing process local g_comlog = nil -- logfile being compressed local g_logtime = {} local g_logfiles = { ["/tmp/kern.log"] = 0x200000, -- 2MB ["/tmp/crit.log"] = 0x200000, -- 2MB ["/tmp/syslog"] = 0, -- use `g_MaxSize } local function parse_tmplogcfg() local fcfg = io.open(g_tmplogcfg, "rb") if not fcfg then return false end local fdat = fcfg:read("*a") fcfg:close(); fcfg = nil local okay, jdat = pcall(cjson.decode, fdat) fdat = nil -- free memory early if not okay or type(jdat) ~= "table" then return false end local tmpval = jdat['size_syslog'] if type(tmpval) == "number" and tmpval >= 0x100000 and tmpval <= 0x10000000 then g_MaxSize = math.floor(tmpval) end tmpval = jdat['save_interval'] if type(tmpval) == "number" and tmpval >= 3600 and tmpval <= 86400 then g_compinter = math.floor(tmpval) end tmpval = jdat['keepdays'] if type(tmpval) == "number" and tmpval >= 7 and tmpval <= 365 then g_keepdays = math.floor(tmpval) * 86400 end tmpval = jdat['maxallsize'] if type(tmpval) == "number" and tmpval >= 134217728 and tmpval <= 0xFFFFFFFF then g_maxallsize = math.floor(tmpval) end return true end local function reload_rsyslog() local suffixes = { ".1", ".2.gz", ".3.gz", ".4.gz", ".5.gz", ".6.gz", ".7.gz", ".8.gz", ".9.gz" } for logf, _ in pairs(g_logfiles) do for _, suf in ipairs(suffixes) do local flog = logf .. suf if invoker.unlink(flog) == 0 then io.stderr:write(gfmt("Warning, log-file removed: %s\n", flog)) io.stderr:flush() end end end local okay = invoker.invoke(0, "systemctl", "kill", "-s", "HUP", "rsyslog.service") return okay == 0 end local function tmpsyslog_init(arg1, arg2) invoker.setname("TMPSYSLOG") invoker.mkdir(g_comdir) invoker.mkdir(g_lastdir) if type(arg1) == "string" and #arg1 > 0 then local msize = tonumber(arg1) if msize and msize >= 0x8000 and msize < 0x40000000 then g_MaxSize = math.floor(msize) end end if type(arg2) == "string" and #arg2 > 0 then local inval = tonumber(arg2) if inval and inval >= 3600 and inval <= 86400 then g_compinter = math.floor(inval) end end parse_tmplogcfg() io.stdout:write(gfmt("tmpsyslog backup interval: %d, syslog maxsize: %d MB\n", g_compinter, math.floor(g_MaxSize / 1048576))) io.stdout:flush() local upt = invoker.uptime() for logp, _ in pairs(g_logfiles) do if upt >= (g_compinter / 2) then g_logtime[logp] = upt - math.floor(g_compinter / 2) else g_logtime[logp] = 0 end end upt = nil -- delete local variable -- keep only 256 last logs local logs = invoker.glob(g_lastdir .. "/*") if type(logs) == "table" then local sort, num = {}, 0 for _, logf in ipairs(logs) do local _, _, _, _, mtime = invoker.statfile(logf) num = num + 1 sort[num] = { fpath = logf, mtime = mtime } end if num > 256 then table.sort(sort, function (fa, fb) if fa and fb then return fa.mtime < fb.mtime end return false end) --[[ for _, finfo in ipairs(sort) do io.stdout:write(gfmt("last log file %s => %s\n", finfo.fpath, os.date(finfo.mtime))) io.stdout:flush() end --]] num = num - 256 for kn = 1, num do local fname = sort[kn].fpath invoker.unlink(fname) io.stdout:write(gfmt("Remove old last log: %s\n", fname)) io.stdout:flush() end end end io.stdout:write(gfmt("Maximum logfile size: %d, keepdays: %d, maxallsize: %.02f GB\n", g_MaxSize, math.floor(g_keepdays / 86400), g_maxallsize / (1024 * 1024 * 1024))) io.stdout:flush() return true end local function unfinished_comp_files(prefix) local pat = gfmt("%s-*-*", prefix) local files = invoker.glob(pat) if type(files) ~= "table" then return false end for _, file in ipairs(files) do if file ~= g_comlog then invoker.unlink(file) io.stdout:write(gfmt("Remove unfinished compressing file: %s\n", file)) io.stdout:flush() end end end local function check_comp_files() for logf, _ in pairs(g_logfiles) do unfinished_comp_files(logf) end local pat = gfmt("%s/*", g_comdir) local files = invoker.glob(pat) if type(files) ~= "table" or #files == 0 then return false end local nowt = os.time() local old_files, nl, size_all = {}, 0, 0 for _, file in pairs(files) do local typf, sizef, _, _, mtime = invoker.statfile(file) if typf ~= "regular" then invoker.unlink(file) elseif (nowt - mtime) >= g_keepdays then -- older than 14 days invoker.unlink(file) io.stdout:write(gfmt("Deleting old logfile: %s\n", file)) io.stdout:flush() else nl = nl + 1 size_all = size_all + sizef old_files[nl] = { fpath = file, mtime = mtime, } end end -- delete old files, if all sizes large then 1.5GB if size_all > g_maxallsize then if nl > 1 then table.sort(old_files, function (fa, fb) if fa and fb then return fa.mtime < fb.mtime end return false end) end --[[ for _, finfo in ipairs(old_files) do io.stdout:write(gfmt("OLD file found: %s => %s\n", finfo.fpath, os.date(finfo.mtime))) io.stdout:flush() end --]] nl = 0 while true do local finfo = table.remove(old_files, 1) if type(finfo) ~= "table" then break end invoker.unlink(finfo.fpath) io.stdout:write(gfmt("Deleting old file: %s => %f\n", finfo.fpath, finfo.mtime)) io.stdout:flush() nl = nl + 1 if nl >= 0x4 then break end end return true end return false end local function check_comp_pid() if not g_compid then return false end local exited, eval = invoker.waitpid(g_compid, true) if exited or exited == nil then local comfile = g_comlog g_comlog, g_compid = nil, nil if type(eval) ~= "number" then eval = 1 end if type(comfile) ~= "string" then comfile = "[UNKNOWN]" end invoker.unlink(comfile) -- remove the logfile compressed io.stdout:write(gfmt("Compressing logfile finished, %s => %d\n", comfile, eval)) io.stdout:flush() return true end return false end local function check_logfile(abspath, logsize) local logfile = string.match(abspath, "/([^/]+)$") local typf, sizef = invoker.statfile(abspath) if not typf then -- file not found, nothing to check g_logtime[abspath] = invoker.uptime() return false end if typf ~= "regular" then invoker.invoke(0, "rm", "-rf", "-v", abspath) reload_rsyslog() return false end -- delete extra large syslog larger than 256M if sizef >= 0x10000000 then invoker.unlink(abspath) reload_rsyslog() return false end local maxsize, thistime = logsize, invoker.uptime() if maxsize == 0 then maxsize = g_MaxSize end local lastcom = g_logtime[abspath] or 0x1 if sizef >= maxsize or (thistime - lastcom) >= g_compinter then local dt = os.date("*t") g_logtime[abspath] = thistime -- update log compression time dt = gfmt("%d%02d%02d-%02d%02d%02d", dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec) local newpath = gfmt("%s-%s", abspath, dt) local compath = gfmt("%s/%s-%s.gz", g_comdir, logfile, dt) io.stdout:write(gfmt("Renaming '%s' => '%s'\n", abspath, newpath)) io.stdout:flush() invoker.rename(abspath, newpath) reload_rsyslog() if g_compid then io.stdout:write(gfmt("Remove logfile due to existing compressing process: %s\n", newpath)) io.stdout:flush() invoker.unlink(newpath) else -- compress logfile g_compid = invoker.invoke(invoker.NOSTDIO + invoker.NOWAIT + invoker.LOWPRI, "sh", "-c", gfmt("exec gzip -c %s > %s", newpath, compath)) if type(g_compid) == "number" then g_comlog = newpath io.stdout:write(gfmt("Compressing logfile '%s' => '%s'\n", newpath, compath)) io.stdout:flush() else g_compid, g_comlog = nil, nil io.stderr:write("Error, failed to start compressing process.\n") io.stderr:flush() end end end return true end local function mainfunc() local upt = invoker.uptime() if upt < 120 then invoker.waitsec(120 - upt) upt = invoker.uptime() end local itercnt, nextv = 0, 16 local com_pid, just_now = nil, 0 while true do for logf, logs in pairs(g_logfiles) do if g_compid then break end check_logfile(logf, logs) end -- prevent gzip process running for too long: if g_compid then local now = invoker.uptime() if g_compid == com_pid then if (now - just_now) >= 300 then invoker.kill(com_pid, 9) io.stderr:write(gfmt("Killing long running gzip with PID: %d\n", com_pid)) io.stderr:flush() end else com_pid, just_now = g_compid, now end elseif com_pid then com_pid = nil end if (itercnt % nextv) == 0 then nextv = check_comp_files() and 2 or 16 end itercnt = itercnt + 1 upt = upt + 30 local nowt = invoker.uptime() if (nowt + 10) >= upt then upt = nowt + 30 end while nowt < upt do check_comp_pid() invoker.waitsec(3) nowt = invoker.uptime() end end end tmpsyslog_init(arg[1], arg[2]) mainfunc()