216 lines
6.3 KiB
Python
216 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Ops-vyer för Scrape Console (Octoparse-paritet): runs-historik, scheman,
|
|
proxy-pool, pipeline-steg och alerts. Endast stdlib. Endpoint: /ops."""
|
|
|
|
import json, os, re, shutil, subprocess, time
|
|
from collections import Counter
|
|
|
|
HOME = os.path.expanduser("~")
|
|
SF = os.path.join(HOME, "Documents/SF_Extractor")
|
|
CI = os.path.join(HOME, ".local/share/carinfo")
|
|
CACHE = {"ts": 0, "data": None}
|
|
|
|
UNITS = [
|
|
"sf-multi-scrape.service",
|
|
"carinfo-crawl.service",
|
|
"siax-enrich.service",
|
|
"siax-laptop-scanner.service",
|
|
]
|
|
|
|
|
|
def _runs(unit, days=3):
|
|
try:
|
|
out = subprocess.run(
|
|
[
|
|
"journalctl",
|
|
"--user",
|
|
"-u",
|
|
unit,
|
|
"--since",
|
|
f"{days} days ago",
|
|
"-o",
|
|
"short-iso",
|
|
"--no-pager",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
).stdout.splitlines()
|
|
except Exception:
|
|
return []
|
|
runs, cur = [], None
|
|
for line in out:
|
|
m = re.match(
|
|
r"(\S+) \S+ systemd\[\d+\]: (Started|Stopped|.*Failed with result '([^']+)')",
|
|
line,
|
|
)
|
|
if not m:
|
|
continue
|
|
ts, ev = m.group(1), m.group(2)
|
|
if ev == "Started":
|
|
cur = {"start": ts[11:16], "day": ts[:10]}
|
|
elif cur is not None and (ev == "Stopped" or "Failed" in ev):
|
|
cur["end"] = ts[11:16]
|
|
cur["result"] = m.group(3) if "Failed" in ev else "ok"
|
|
runs.append(cur)
|
|
cur = None
|
|
if cur is not None:
|
|
cur["end"] = "pågår"
|
|
cur["result"] = "running"
|
|
runs.append(cur)
|
|
return runs[-12:]
|
|
|
|
|
|
def _schedules():
|
|
out = []
|
|
try:
|
|
txt = subprocess.run(
|
|
["systemctl", "--user", "list-timers", "--no-pager"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=15,
|
|
).stdout
|
|
for line in txt.splitlines():
|
|
for key in ("p0lytics", "sf-scrape", "proton", "opencode-db", "sloso"):
|
|
if key in line:
|
|
parts = line.split()
|
|
out.append(
|
|
{
|
|
"next": parts[0] + " " + parts[1]
|
|
if len(parts) > 2
|
|
else parts[0],
|
|
"unit": next((p for p in parts if ".timer" in p), "?"),
|
|
}
|
|
)
|
|
break
|
|
except Exception:
|
|
pass
|
|
try:
|
|
cron = subprocess.run(
|
|
["crontab", "-l"], capture_output=True, text=True, timeout=10
|
|
).stdout
|
|
for line in cron.splitlines():
|
|
if (
|
|
line.strip()
|
|
and not line.startswith("#")
|
|
and any(k in line for k in ("p0lytics", "plane-sync", "valkompass"))
|
|
):
|
|
out.append({"next": "cron", "unit": line[:110]})
|
|
except Exception:
|
|
pass
|
|
return out
|
|
|
|
|
|
def _proxy():
|
|
try:
|
|
d = json.load(open(os.path.join(SF, "proxies/pool.json")))
|
|
recs = d.get("records", [])
|
|
now = time.time()
|
|
fresh = sum(
|
|
1
|
|
for r in recs
|
|
if r.get("lastChecked") and now - _ts(r["lastChecked"]) < 3600
|
|
)
|
|
healthy = sum(1 for r in recs if (r.get("consecutiveFailures") or 0) == 0)
|
|
lat = [r["latencyMsEwma"] for r in recs if r.get("latencyMsEwma")]
|
|
lat.sort()
|
|
countries = Counter(r.get("country", "?") for r in recs).most_common(5)
|
|
return {
|
|
"total": len(recs),
|
|
"fresh_1h": fresh,
|
|
"healthy": healthy,
|
|
"lat_p50": round(lat[len(lat) // 2]) if lat else -1,
|
|
"top_countries": countries,
|
|
}
|
|
except Exception as e:
|
|
return {"error": str(e)[:100]}
|
|
|
|
|
|
def _ts(iso):
|
|
try:
|
|
return (
|
|
time.mktime(time.strptime(iso[:19], "%Y-%m-%dT%H:%M:%S"))
|
|
- time.altzone * 0
|
|
+ (7200 if iso.endswith("Z") else 0)
|
|
)
|
|
except Exception:
|
|
return 0
|
|
|
|
|
|
def _wc(path):
|
|
try:
|
|
with open(path, "rb") as f:
|
|
return sum(1 for _ in f)
|
|
except Exception:
|
|
return -1
|
|
|
|
|
|
def _pipeline():
|
|
stages = {}
|
|
b = os.path.join(SF, "full_data/biluppgifter")
|
|
stages["biluppgifter frontier (väntar)"] = _wc(os.path.join(b, "frontier.txt"))
|
|
stages["biluppgifter crawled"] = _wc(os.path.join(b, "crawled.txt"))
|
|
stages["biluppgifter prio-kö"] = _wc(os.path.join(b, "pending_priority.txt"))
|
|
m = os.path.join(SF, "full_data/multi")
|
|
try:
|
|
for src in sorted(os.listdir(m)):
|
|
cp = os.path.join(m, src, "crawled.txt")
|
|
if os.path.exists(cp):
|
|
stages[f"multi/{src} crawled"] = _wc(cp)
|
|
except Exception:
|
|
pass
|
|
return stages
|
|
|
|
|
|
def ops():
|
|
if time.time() - CACHE["ts"] < 300:
|
|
return CACHE["data"]
|
|
runs = {u: _runs(u) for u in UNITS}
|
|
data = {
|
|
"runs": runs,
|
|
"schedules": _schedules(),
|
|
"proxy": _proxy(),
|
|
"pipeline": _pipeline(),
|
|
"alerts": [],
|
|
"disk_free_gb": 0,
|
|
}
|
|
try:
|
|
data["disk_free_gb"] = round(shutil.disk_usage("/").free / 1e9, 1)
|
|
except Exception:
|
|
pass
|
|
A = data["alerts"]
|
|
if data["disk_free_gb"] and data["disk_free_gb"] < 6:
|
|
A.append(
|
|
{
|
|
"level": "crit",
|
|
"text": f"Disken nästan full: {data['disk_free_gb']} GB fritt",
|
|
}
|
|
)
|
|
try:
|
|
clog = (
|
|
open(os.path.join(CI, "crawl.log"), errors="replace")
|
|
.read()
|
|
.splitlines()[-3:]
|
|
)
|
|
if clog and all("cooldown" in l for l in clog):
|
|
A.append(
|
|
{
|
|
"level": "warn",
|
|
"text": "car.info stampar i cooldown-loop (tunna modeller)",
|
|
}
|
|
)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
if "not found" in open("/tmp/p0lytics-health.log", errors="replace").read():
|
|
A.append(
|
|
{
|
|
"level": "err",
|
|
"text": "p0lytics-cron trasig (.venv saknas) — ingen health-data",
|
|
}
|
|
)
|
|
except Exception:
|
|
pass
|
|
CACHE.update(ts=time.time(), data=data)
|
|
return data
|