feat: add searchable event log history
This commit is contained in:
@@ -91,18 +91,39 @@ def update_ini(updates):
|
||||
temporary.write_text(text); temporary.chmod(0o644); temporary.replace(CFG)
|
||||
return str(backup)
|
||||
|
||||
def journal(limit):
|
||||
result=subprocess.run(["journalctl","-u","pigway-pi-control.service","-n",str(limit),
|
||||
"--no-pager","-o","json"],capture_output=True,text=True,timeout=5)
|
||||
def journal_query(since=None,until=None,severity="",event="",search="",sort="timestamp",order="desc",limit=100,offset=0):
|
||||
command=["journalctl","-u","pigway-pi-control.service","--no-pager","-o","json"]
|
||||
if since is not None: command.extend(["--since",f"@{since}"])
|
||||
if until is not None: command.extend(["--until",f"@{until}"])
|
||||
result=subprocess.run(command,capture_output=True,text=True,timeout=15)
|
||||
if result.returncode: raise RuntimeError(result.stderr.strip() or "journalctl failed")
|
||||
rows=[]
|
||||
for line in result.stdout.splitlines():
|
||||
try:
|
||||
item=json.loads(line)
|
||||
rows.append({"timestamp":item.get("__REALTIME_TIMESTAMP"),"priority":item.get("PRIORITY"),
|
||||
"message":item.get("MESSAGE","")})
|
||||
message=item.get("MESSAGE","")
|
||||
level_match=re.search(r"(?:^|\s)level=([A-Z]+)(?:\s|$)",message)
|
||||
event_match=re.search(r"(?:^|\s)event=([^ ]+)",message)
|
||||
priority=str(item.get("PRIORITY","6"))
|
||||
level=(level_match.group(1) if level_match else
|
||||
{"0":"EMERGENCY","1":"ALERT","2":"CRITICAL","3":"ERROR","4":"WARN","5":"NOTICE","6":"INFO","7":"DEBUG"}.get(priority,"INFO"))
|
||||
rows.append({"timestamp":int(item.get("__REALTIME_TIMESTAMP",0)),"priority":priority,
|
||||
"severity":level,"event":event_match.group(1) if event_match else "SYSTEM",
|
||||
"message":message})
|
||||
except json.JSONDecodeError: pass
|
||||
return rows
|
||||
severities={x.strip().upper() for x in severity.split(",") if x.strip()}
|
||||
if severities: rows=[x for x in rows if x["severity"] in severities]
|
||||
event_types=sorted({x["event"] for x in rows})
|
||||
if event: rows=[x for x in rows if x["event"]==event]
|
||||
if search:
|
||||
needle=search.casefold(); rows=[x for x in rows if needle in x["message"].casefold()]
|
||||
severity_rank={"EMERGENCY":0,"ALERT":1,"CRITICAL":2,"ERROR":3,"WARN":4,"NOTICE":5,"INFO":6,"DEBUG":7}
|
||||
keys={"timestamp":lambda x:x["timestamp"],"severity":lambda x:severity_rank.get(x["severity"],99),
|
||||
"event":lambda x:x["event"],"message":lambda x:x["message"]}
|
||||
rows.sort(key=keys[sort],reverse=order=="desc")
|
||||
total=len(rows)
|
||||
return {"logs":rows[offset:offset+limit],"total":total,"offset":offset,"limit":limit,
|
||||
"event_types":event_types,"retention":"systemd-journal"}
|
||||
|
||||
def service_inventory():
|
||||
cfg=load_cfg()
|
||||
@@ -214,13 +235,25 @@ class Handler(BaseHTTPRequestHandler):
|
||||
if not STATUS.exists(): self.send_json(503,{"error":"agent status unavailable"}); return
|
||||
self.send_json(200,json.loads(STATUS.read_text())); return
|
||||
if parsed.path=="/api/v1/logs":
|
||||
limit=min(max(int(parse_qs(parsed.query).get("limit",[api_setting("log_limit",200,int)])[0]),1),1000)
|
||||
self.send_json(200,{"logs":journal(limit)}); return
|
||||
query=parse_qs(parsed.query)
|
||||
value=lambda key,default="":query.get(key,[default])[0]
|
||||
limit=min(max(int(value("limit",api_setting("log_limit",200,int))),1),500)
|
||||
offset=max(int(value("offset",0)),0)
|
||||
since=int(value("since")) if value("since") else None
|
||||
until=int(value("until")) if value("until") else None
|
||||
if since is not None and until is not None and since>until: raise ValueError("since must not be after until")
|
||||
sort=value("sort","timestamp"); order=value("order","desc")
|
||||
if sort not in {"timestamp","severity","event","message"}: raise ValueError("invalid log sort")
|
||||
if order not in {"asc","desc"}: raise ValueError("invalid log order")
|
||||
severity=value("severity").upper(); event=value("event"); search=value("search")
|
||||
if len(event)>80 or len(search)>120: raise ValueError("log filter is too long")
|
||||
self.send_json(200,journal_query(since,until,severity,event,search,sort,order,limit,offset)); return
|
||||
if parsed.path=="/api/v1/config":
|
||||
self.send_json(200,{"config":config_json(),"write_requires_token":True}); return
|
||||
if parsed.path=="/api/v1/services":
|
||||
self.send_json(200,{"services":service_inventory()}); return
|
||||
self.send_json(404,{"error":"not found"})
|
||||
except ValueError as e: self.send_json(400,{"error":str(e)})
|
||||
except Exception as e: self.send_json(500,{"error":str(e)})
|
||||
|
||||
def do_PUT(self):
|
||||
|
||||
Reference in New Issue
Block a user