Refactor store dwell alert management API and dwell engine
- Updated argument parsing in manage_api.py to include new threshold parameters. - Enhanced _config_payload to include thresholds and webhook configurations. - Modified _build_summary to track queue metrics and adjust alert reporting. - Refactored DwellEngine to utilize queue thresholds for alerting and reporting. - Added queue metrics calculations and status change tracking in dwell_engine.py. - Updated notifier.py to support posting JSON events to webhooks. - Adjusted example configuration to reflect new threshold parameters. - Enhanced Docker entrypoint script for better process management. - Updated tests to cover new queue metrics and thresholds. - Improved ManagedServiceDetail and ManagedServices Vue components to display queue metrics.
This commit is contained in:
@@ -17,7 +17,6 @@ from app.config import (
|
||||
save_config_document,
|
||||
)
|
||||
|
||||
|
||||
PROJECT_TYPE = "store_dwell_alert"
|
||||
DEFAULT_MANAGE_PORT = 18081
|
||||
MAX_PREVIEW_LINES = 2000
|
||||
@@ -136,7 +135,12 @@ def parse_args() -> ArgumentParser:
|
||||
parser = ArgumentParser(description="Store dwell alert management API")
|
||||
parser.add_argument("--config", help="Path to YAML config file")
|
||||
parser.add_argument("--host", default="0.0.0.0", help="Host for the management API")
|
||||
parser.add_argument("--port", type=int, default=DEFAULT_MANAGE_PORT, help="Port for the management API")
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=DEFAULT_MANAGE_PORT,
|
||||
help="Port for the management API",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -160,9 +164,18 @@ def _config_payload(ctx: ManageContext) -> dict:
|
||||
"sample_fps": config.stream.sample_fps,
|
||||
"reconnect_backoff_seconds": config.stream.reconnect_backoff_seconds,
|
||||
},
|
||||
"thresholds": {
|
||||
"queue_time_threshold_seconds": config.thresholds.queue_time_threshold_seconds,
|
||||
"crowded_count_threshold": config.thresholds.crowded_count_threshold,
|
||||
"normal_count_threshold": config.thresholds.normal_count_threshold,
|
||||
},
|
||||
"event_sink": {
|
||||
"path": str(event_sink_path),
|
||||
},
|
||||
"webhook": {
|
||||
"url": config.webhook.url,
|
||||
"timeout_seconds": config.webhook.timeout_seconds,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -179,11 +192,13 @@ def _build_summary(ctx: ManageContext) -> dict:
|
||||
},
|
||||
}
|
||||
|
||||
alert_count = 0
|
||||
last_alert_time = ""
|
||||
last_report_time = ""
|
||||
active_count = 0
|
||||
longest_dwell_seconds = 0
|
||||
queue_level = ""
|
||||
over_threshold_count = 0
|
||||
under_threshold_count = 0
|
||||
status_change = ""
|
||||
window_stats: list[dict] = []
|
||||
|
||||
with events_path.open("r", encoding="utf-8") as handle:
|
||||
@@ -196,10 +211,7 @@ def _build_summary(ctx: ManageContext) -> dict:
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
if payload.get("event") == "long_stay_alert":
|
||||
alert_count += 1
|
||||
last_alert_time = _string_value(payload.get("ts"))
|
||||
elif payload.get("event") == "half_hour_report":
|
||||
if payload.get("event") == "half_hour_report":
|
||||
last_report_time = _string_value(payload.get("window_end"))
|
||||
active_count = _int_value(payload.get("active_customer_count"))
|
||||
stat = _build_window_stat(payload)
|
||||
@@ -208,31 +220,36 @@ def _build_summary(ctx: ManageContext) -> dict:
|
||||
longest_dwell_seconds,
|
||||
stat["max_wait_seconds"],
|
||||
)
|
||||
queue_level = stat["queue_level"]
|
||||
over_threshold_count = stat["over_threshold_count"]
|
||||
under_threshold_count = stat["under_threshold_count"]
|
||||
status_change = stat["status_change"]
|
||||
|
||||
window_stats.sort(
|
||||
key=lambda item: _parse_timestamp(item["window_end"]),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
headline = "No alerts or reports yet"
|
||||
headline = "No reports yet"
|
||||
if last_report_time:
|
||||
headline = (
|
||||
"Latest report shows "
|
||||
f"{active_count} active customers, longest dwell {longest_dwell_seconds}s"
|
||||
f"{queue_level or 'unknown'} queue, "
|
||||
f"{over_threshold_count} over 5 min and {under_threshold_count} under 5 min"
|
||||
)
|
||||
elif alert_count > 0:
|
||||
headline = f"Observed {alert_count} alert(s), latest alert at {last_alert_time}"
|
||||
|
||||
return {
|
||||
"result_type": PROJECT_TYPE,
|
||||
"headline": headline,
|
||||
"last_result_time": _latest_timestamp(last_alert_time, last_report_time),
|
||||
"last_result_time": _latest_timestamp(last_report_time),
|
||||
"metrics": {
|
||||
"alert_count": alert_count,
|
||||
"last_alert_time": last_alert_time,
|
||||
"last_half_hour_report_time": last_report_time,
|
||||
"active_customer_count": active_count,
|
||||
"longest_dwell_seconds": longest_dwell_seconds,
|
||||
"queue_level": queue_level,
|
||||
"over_threshold_count": over_threshold_count,
|
||||
"under_threshold_count": under_threshold_count,
|
||||
"status_change": status_change,
|
||||
"events_path": str(events_path),
|
||||
"recent_window_stats": window_stats[:24],
|
||||
"all_window_stats": window_stats,
|
||||
@@ -260,7 +277,9 @@ def _list_result_files(ctx: ManageContext) -> list[dict]:
|
||||
"label": label,
|
||||
"kind": path.suffix.lstrip(".").lower(),
|
||||
"size": info.st_size,
|
||||
"modified_at": datetime.fromtimestamp(info.st_mtime).astimezone().isoformat(),
|
||||
"modified_at": datetime.fromtimestamp(info.st_mtime)
|
||||
.astimezone()
|
||||
.isoformat(),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -279,12 +298,21 @@ def _build_window_stat(payload: dict) -> dict:
|
||||
payload.get("closed_customers"),
|
||||
"final_dwell_seconds",
|
||||
)
|
||||
queue_metrics = (
|
||||
payload.get("queue_metrics")
|
||||
if isinstance(payload.get("queue_metrics"), dict)
|
||||
else {}
|
||||
)
|
||||
return {
|
||||
"window_start": _string_value(payload.get("window_start")),
|
||||
"window_end": _string_value(payload.get("window_end")),
|
||||
"active_customer_count": _int_value(payload.get("active_customer_count")),
|
||||
"active_wait_seconds": active_wait_seconds,
|
||||
"closed_wait_seconds": closed_wait_seconds,
|
||||
"queue_level": _string_value(queue_metrics.get("queue_level")),
|
||||
"over_threshold_count": _int_value(queue_metrics.get("over_threshold_count")),
|
||||
"under_threshold_count": _int_value(queue_metrics.get("under_threshold_count")),
|
||||
"status_change": _string_value(queue_metrics.get("status_change")),
|
||||
"max_wait_seconds": max(
|
||||
max(active_wait_seconds, default=0),
|
||||
max(closed_wait_seconds, default=0),
|
||||
|
||||
Reference in New Issue
Block a user