forked from b.peck/BAT
pytest 31/31. In-gateway end-to-end: 559 activations correlated (uuid mode), 3 chatterers, 34 fleeting, 1 flood, health F(22) on the deliberately-bad sim plant. Gate G1 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
397 lines
14 KiB
Python
397 lines
14 KiB
Python
# PrimeControls.alarms - gateway adapter (the ONLY impure module).
|
|
# Journal access via system.alarm.queryJournal/queryStatus exclusively - no SQL,
|
|
# no journal name assumptions, every query bounded by the caller's date range.
|
|
# Jython 2.7. Bare except is used on defensive paths deliberately: java exceptions
|
|
# do not subclass Python Exception and would bypass 'except Exception'.
|
|
from __future__ import division, print_function
|
|
|
|
import sys
|
|
|
|
from PrimeControls import calc, fmt
|
|
|
|
|
|
def _err_text():
|
|
try:
|
|
return "%s" % (sys.exc_info()[1],)
|
|
except:
|
|
return "unknown error"
|
|
|
|
|
|
def _now_ms():
|
|
try:
|
|
return system.date.toMillis(system.date.now())
|
|
except:
|
|
import time
|
|
return int(time.time() * 1000)
|
|
|
|
|
|
def _ms_of(java_date):
|
|
try:
|
|
if java_date is None:
|
|
return None
|
|
return java_date.getTime()
|
|
except:
|
|
return None
|
|
|
|
|
|
def _norm_event(evt):
|
|
try:
|
|
source = str(evt.getSource() or "")
|
|
state = None
|
|
try:
|
|
if evt.getAckData() is not None:
|
|
state = "ack"
|
|
elif evt.getClearedData() is not None:
|
|
state = "clear"
|
|
elif evt.getActiveData() is not None:
|
|
state = "active"
|
|
except:
|
|
state = None
|
|
if state is None:
|
|
s = ""
|
|
try:
|
|
s = str(evt.getState() or "")
|
|
except:
|
|
pass
|
|
if s.startswith("Cleared"):
|
|
state = "clear"
|
|
elif "Acknowledged" in s and not s.startswith("Active,"):
|
|
state = "ack"
|
|
else:
|
|
state = "active"
|
|
ts = None
|
|
try:
|
|
ts = _ms_of(evt.get("eventTime"))
|
|
except:
|
|
ts = None
|
|
if ts is None:
|
|
return None
|
|
prio_raw = None
|
|
try:
|
|
p = evt.getPriority()
|
|
try:
|
|
prio_raw = int(p.ordinal())
|
|
except:
|
|
prio_raw = str(p)
|
|
except:
|
|
prio_raw = None
|
|
lvl, name = calc.normalize_priority(prio_raw)
|
|
display_path = ""
|
|
try:
|
|
display_path = str(evt.getDisplayPath() or "")
|
|
except:
|
|
pass
|
|
is_system = source.startswith("evt:") or ":/alm:" not in source
|
|
try:
|
|
if evt.get("isSystemEvent"):
|
|
is_system = True
|
|
except:
|
|
pass
|
|
ack_user = None
|
|
try:
|
|
u = evt.get("ackUserName")
|
|
if u:
|
|
ack_user = str(u)
|
|
except:
|
|
pass
|
|
event_id = None
|
|
try:
|
|
event_id = str(evt.getId())
|
|
except:
|
|
pass
|
|
return {"event_id": event_id, "source": source, "display_path": display_path,
|
|
"priority": lvl, "priority_name": name, "state": state, "ts": ts,
|
|
"is_system": is_system, "ack_user": ack_user}
|
|
except:
|
|
return None
|
|
|
|
|
|
def _fetch_journal(start_ms, end_ms, max_events, include_data=False):
|
|
"""Returns (rows, dropped, truncated, error)."""
|
|
rows = []
|
|
dropped = 0
|
|
truncated = False
|
|
try:
|
|
kwargs = {"startDate": int(start_ms), "endDate": int(end_ms)}
|
|
result = system.alarm.queryJournal(**kwargs)
|
|
for evt in result:
|
|
r = _norm_event(evt)
|
|
if r is None:
|
|
dropped += 1
|
|
continue
|
|
rows.append(r)
|
|
if len(rows) >= max_events:
|
|
truncated = True
|
|
break
|
|
return rows, dropped, truncated, None
|
|
except:
|
|
return rows, dropped, truncated, _err_text()
|
|
|
|
|
|
def _fetch_status():
|
|
out = []
|
|
try:
|
|
result = system.alarm.queryStatus(state=["ActiveUnacked", "ActiveAcked"])
|
|
for evt in result:
|
|
try:
|
|
source = str(evt.getSource() or "")
|
|
if source.startswith("evt:") or ":/alm:" not in source:
|
|
continue
|
|
active_ms = None
|
|
try:
|
|
data = evt.getActiveData()
|
|
if data is not None:
|
|
active_ms = _ms_of(data.get("eventTime"))
|
|
except:
|
|
active_ms = None
|
|
if active_ms is None:
|
|
try:
|
|
active_ms = _ms_of(evt.get("eventTime"))
|
|
except:
|
|
pass
|
|
prio_raw = None
|
|
try:
|
|
p = evt.getPriority()
|
|
try:
|
|
prio_raw = int(p.ordinal())
|
|
except:
|
|
prio_raw = str(p)
|
|
except:
|
|
pass
|
|
lvl, name = calc.normalize_priority(prio_raw)
|
|
unacked = False
|
|
try:
|
|
unacked = "Unacknowledged" in str(evt.getState() or "")
|
|
except:
|
|
pass
|
|
display_path = ""
|
|
try:
|
|
display_path = str(evt.getDisplayPath() or "")
|
|
except:
|
|
pass
|
|
out.append({"source": source, "display_path": display_path,
|
|
"priority": lvl, "priority_name": name,
|
|
"active_ms": active_ms, "unacked": unacked})
|
|
except:
|
|
continue
|
|
except:
|
|
pass
|
|
return out
|
|
|
|
|
|
def _fetch_shelved():
|
|
try:
|
|
paths = system.alarm.getShelvedPaths()
|
|
return len(list(paths)) if paths is not None else 0
|
|
except:
|
|
return None
|
|
|
|
|
|
def _clean_filters(options):
|
|
f = {"priorities": [], "areas": [], "states": [], "search": ""}
|
|
if not options:
|
|
return f
|
|
try:
|
|
for k in ("priorities", "areas", "states"):
|
|
v = options.get(k)
|
|
if v:
|
|
f[k] = [x for x in list(v)]
|
|
f["priorities"] = [int(x) for x in f["priorities"]]
|
|
f["areas"] = [str(x) for x in f["areas"]]
|
|
f["states"] = [str(x) for x in f["states"]]
|
|
f["search"] = str(options.get("search") or "")
|
|
except:
|
|
pass
|
|
return f
|
|
|
|
|
|
# ---------------- Perspective entry points ----------------
|
|
|
|
|
|
def getDashboardBundle(startMs, endMs, options=None):
|
|
try:
|
|
s = int(startMs)
|
|
e = int(endMs)
|
|
opts = calc.opts_merged(None)
|
|
filters = _clean_filters(options)
|
|
span = max(e - s, 1)
|
|
rows, dropped, truncated, err = _fetch_journal(
|
|
s - span, e, int(opts["max_events"]))
|
|
active_now = _fetch_status()
|
|
shelved = _fetch_shelved()
|
|
bundle = calc.build_bundle(rows, active_now, shelved, s, e, _now_ms(),
|
|
{"filters": filters})
|
|
bundle["meta"]["dropped_rows"] = dropped
|
|
bundle["meta"]["truncated"] = truncated
|
|
if err:
|
|
bundle["meta"]["error"] = err
|
|
bundle["insights"] = fmt.insights(bundle)
|
|
return bundle
|
|
except:
|
|
try:
|
|
bundle = calc.build_bundle([], [], None, int(startMs or 0),
|
|
int(endMs or 0), _now_ms(), None)
|
|
except:
|
|
bundle = calc.build_bundle([], [], None, 0, 0, 0, None)
|
|
bundle["meta"]["error"] = _err_text()
|
|
bundle["insights"] = []
|
|
return bundle
|
|
|
|
|
|
STATE_LABELS = {"active": "Active", "clear": "Cleared", "ack": "Acked"}
|
|
|
|
|
|
def _journal_rows(start_ms, end_ms, filters, max_rows):
|
|
rows, dropped, truncated, err = _fetch_journal(start_ms, end_ms, max_rows)
|
|
out = []
|
|
prios = set(filters.get("priorities") or [])
|
|
areas = set(filters.get("areas") or [])
|
|
states = set(filters.get("states") or [])
|
|
search = (filters.get("search") or "").strip().lower()
|
|
for r in rows:
|
|
if r["is_system"]:
|
|
continue
|
|
parsed = calc.parse_source(r["source"], r["display_path"])
|
|
if prios and r["priority"] not in prios:
|
|
continue
|
|
if areas and parsed["area"] not in areas:
|
|
continue
|
|
if states:
|
|
tag = {"active": "active", "clear": "cleared", "ack": "acked"}[r["state"]]
|
|
if tag not in states and not ("unacked" in states and r["state"] == "active"):
|
|
continue
|
|
if search and search not in (r["source"] + " " + parsed["label"]).lower():
|
|
continue
|
|
out.append({"id": r["event_id"], "time_ms": r["ts"],
|
|
"time_label": fmt.day_clock(r["ts"]),
|
|
"source": r["source"], "label": parsed["label"],
|
|
"area": parsed["area"], "state": r["state"],
|
|
"state_label": STATE_LABELS.get(r["state"], r["state"]),
|
|
"priority": r["priority"], "priority_name": r["priority_name"],
|
|
"ack_user": r["ack_user"] or ""})
|
|
out.sort(key=lambda r: -(r["time_ms"] or 0))
|
|
return out, truncated, err
|
|
|
|
|
|
def getJournalPage(startMs, endMs, filters=None, page=0, pageSize=50):
|
|
try:
|
|
f = _clean_filters(filters)
|
|
rows, truncated, err = _journal_rows(int(startMs), int(endMs), f,
|
|
int(calc.DEFAULTS["max_events"]))
|
|
page = max(0, int(page or 0))
|
|
page_size = max(1, int(pageSize or 50))
|
|
start = page * page_size
|
|
return {"rows": rows[start:start + page_size], "total": len(rows),
|
|
"page": page, "page_size": page_size, "truncated": truncated,
|
|
"error": err}
|
|
except:
|
|
return {"rows": [], "total": 0, "page": 0, "page_size": int(pageSize or 50),
|
|
"truncated": False, "error": _err_text()}
|
|
|
|
|
|
def journalCsv(startMs, endMs, filters=None, maxRows=10000):
|
|
try:
|
|
f = _clean_filters(filters)
|
|
rows, truncated, err = _journal_rows(int(startMs), int(endMs), f, int(maxRows))
|
|
headers = ["time", "state", "priority", "label", "area", "source", "ack_user"]
|
|
data = [[r["time_label"], r["state_label"], r["priority_name"], r["label"],
|
|
r["area"], r["source"], r["ack_user"]] for r in rows]
|
|
ds = system.dataset.toDataSet(headers, data)
|
|
return system.dataset.toCSV(ds)
|
|
except:
|
|
return "error,%s" % _err_text().replace(",", ";")
|
|
|
|
|
|
def getSourceDetail(source, startMs, endMs):
|
|
try:
|
|
s = int(startMs)
|
|
e = int(endMs)
|
|
src = str(source or "")
|
|
rows, dropped, truncated, err = _fetch_journal(s, e, int(calc.DEFAULTS["max_events"]))
|
|
mine = [r for r in rows if r["source"] == src]
|
|
instances, _mode = calc.correlate_instances(mine)
|
|
parsed = calc.parse_source(src, mine[0]["display_path"] if mine else "")
|
|
day_bins = calc.rate_bins(instances, s, e, {"bin_ms": 86400000, "max_bins": 400})
|
|
daily = [{"t0": b["t0"], "count": b["count"]} for b in day_bins["bins"]]
|
|
ttas = [i["tta_ms"] for i in instances if i.get("tta_ms") is not None]
|
|
durs = [i["duration_ms"] for i in instances if i.get("duration_ms") is not None]
|
|
fleet = len([i for i in instances
|
|
if i.get("duration_ms") is not None
|
|
and i["duration_ms"] < calc.DEFAULTS["fleeting_s"] * 1000])
|
|
ack_users = {}
|
|
for r in mine:
|
|
if r["state"] == "ack" and r.get("ack_user"):
|
|
ack_users[r["ack_user"]] = ack_users.get(r["ack_user"], 0) + 1
|
|
top_ack = [{"user": u, "count": c} for u, c in
|
|
sorted(ack_users.items(), key=lambda kv: -kv[1])[:5]]
|
|
events = []
|
|
for r in sorted(mine, key=lambda r: -r["ts"])[:100]:
|
|
events.append({"id": r["event_id"], "time_ms": r["ts"],
|
|
"time_label": fmt.day_clock(r["ts"]),
|
|
"state_label": STATE_LABELS.get(r["state"], r["state"]),
|
|
"priority_name": r["priority_name"],
|
|
"ack_user": r["ack_user"] or ""})
|
|
return {"label": parsed["label"], "area": parsed["area"], "events": events,
|
|
"daily": daily,
|
|
"stats": {"count": len([i for i in instances if i["active_ms"] is not None]),
|
|
"avg_tta_ms": (sum(ttas) / len(ttas)) if ttas else None,
|
|
"avg_active_ms": (sum(durs) / len(durs)) if durs else None,
|
|
"fleeting_count": fleet, "top_ack_users": top_ack},
|
|
"error": err}
|
|
except:
|
|
return {"label": str(source or ""), "area": "", "events": [], "daily": [],
|
|
"stats": {"count": 0, "avg_tta_ms": None, "avg_active_ms": None,
|
|
"fleeting_count": 0, "top_ack_users": []},
|
|
"error": _err_text()}
|
|
|
|
|
|
def getEventData(eventId, aroundMs, source):
|
|
try:
|
|
eid = str(eventId or "")
|
|
mid = int(aroundMs)
|
|
props = []
|
|
seen = {}
|
|
try:
|
|
result = system.alarm.queryJournal(startDate=mid - 3600000,
|
|
endDate=mid + 3600000,
|
|
includeData=True)
|
|
except:
|
|
result = system.alarm.queryJournal(startDate=mid - 3600000,
|
|
endDate=mid + 3600000)
|
|
for evt in result:
|
|
try:
|
|
if str(evt.getId()) != eid:
|
|
continue
|
|
try:
|
|
names = evt.getProperties()
|
|
except:
|
|
names = []
|
|
for p in names or []:
|
|
try:
|
|
n = str(p)
|
|
v = evt.get(p)
|
|
if n not in seen:
|
|
seen[n] = True
|
|
props.append({"name": n, "value": str(v)})
|
|
except:
|
|
continue
|
|
except:
|
|
continue
|
|
props.sort(key=lambda kv: kv["name"])
|
|
return {"props": props, "error": None}
|
|
except:
|
|
return {"props": [], "error": _err_text()}
|
|
|
|
|
|
def getShiftReportText(shiftHours=8, anchorHour=0, options=None):
|
|
try:
|
|
bounds = fmt.shift_bounds(_now_ms(), int(shiftHours), int(anchorHour))
|
|
s, e = bounds["previous"]
|
|
bundle = getDashboardBundle(s, e, options)
|
|
report = fmt.shift_report(bundle, bounds["label"])
|
|
return {"text": report["text"], "start_ms": s, "end_ms": e,
|
|
"label": bounds["label"]}
|
|
except:
|
|
return {"text": "Shift report unavailable: %s" % _err_text(),
|
|
"start_ms": 0, "end_ms": 0, "label": ""}
|