feat: add configurable smart alert rules (#183) - #206
Conversation
|
Warning Review limit reached
Next review available in: 28 minutes Limit details: You’ve used all 1 included review currently available under your plan. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (21)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary
Adds a rule engine so operators can decide which suspicious activity actually raises an alert, instead of every zone-entry-plus-dwell event reaching the reasoning layer. Rules live in
config/alert_rules.yaml, are validated on load, and are re-read when the file changes — no restart, no code edit.The engine narrows the existing trigger; it never widens it. Activity must still clear the zone, dwell, and suspicious-action gates, so a permissive rule cannot manufacture an alert. With no rule enabled the engine abstains rather than matching nothing, which is what makes this safe to merge: an absent or fully disabled config leaves the pipeline behaving exactly as it does today.
Closes #183
Deliverables from the issue
object_types: [person], matched against the tracked object's classmin_confidence, checked against the highest confidence in the sequencezones, using the names already defined inconfig/zones.yamltime_windowswithHH:MMranges that may cross midnight, plus optionaldaysenabled: falseswitches a rule off in place, applied liveobject_types: [vehicle]parses, validates, and matches correctly in the engine (there are tests for it), but it cannot fire in the live pipeline yet:services/tracking/tracker.pydrops every non-person detection before DeepSORT and hardcodeslabel = "person". Making vehicle alerts real means multi-class tracking — ReID, action hints, and dwell semantics all currently assume people — which is a substantial feature of its own and would change what gets tracked for every existing deployment. Rather than add vehicle classes to the detector and ship a rule that silently never matches, the taxonomy is in place and the limitation is documented in the README and the example config. Happy to open a follow-up issue for multi-class tracking if you'd like.How a rule looks
Rules are off until the file exists:
Every dimension is optional and an omitted one means "any". Populated dimensions are ANDed, rules are tried top to bottom, and the first match wins — so specific rules go first.
object_typesaccepts the group aliasesperson,vehicle,bag,deviceor any raw COCO class.API
GET /rulesreports what the pipeline is currently enforcing, in evaluation order. It's read-only on purpose: the YAML file is the source of truth, and it hot-reloads, so there's no second write path to keep consistent.GET /rulesGET /rules?enabled_only=trueGET /rules/{rule_id}404if unknownPicked up automatically by the existing router auto-discovery — no registration edit.
Design
services/rules/engine.pyis pure: the rules, the activity, and the current time all arrive as arguments, so a decision is reproducible in a test with no config file and no frozen clock. YAML parsing and reload live inlibs/config/rule_loader.py, andservices/rules/provider.pyholds the process-wide instance, keeping filesystem concerns out of the matcher.Three behaviours worth reviewing:
statat most everyRULES_RELOAD_SECONDS. This deviates fromzone_loader.py's thread; a shared base class would have forced one strategy onto both.RULES_TIMEZONEsets the site's timezone; an unknown value falls back to UTC with a warning.Prerequisite: the object class was never persisted
Rules match on object type, but
TrackEventhad no field for it — so this PR addsTrackEvent.label, populated inservices/memory/pipeline.pyfrom the tracked object's class and inPOST /ingestfrom the request body. It defaults toNone, so events already in Redis stay readable; they simply never match a rule that namesobject_types, which is pinned by a test.Why the config ships as
.exampleShipping a populated
config/alert_rules.yamlwould activate the engine for everyone on upgrade, suppressing alerts that fire today. It ships asconfig/alert_rules.example.yamlfor operators to copy, following the.env.exampleconvention, andconfig/alert_rules.yamlis gitignored so local rules can't be committed by accident.Testing
78 new tests, all passing:
tests/test_alert_rules.py(39) — group expansion, midnight-crossing and weekday windows, per-dimension matching, AND semantics, first-match-wins, abstention with no or all-disabled rules, and the inclusive confidence boundary.tests/test_rule_loader.py(18) — valid load and ordering, missing/empty/null-key files, malformed YAML, duplicate ids, theALERT_RULES_PATHoverride, hot reload, keeping the last good rules through a broken edit, and a check that the shipped example config actually loads.tests/integration/test_rules.py(7) — the endpoints over HTTP, including404and the no-config-file case.tests/test_trigger.py(+10) — the gate itself: default behaviour preserved with no rules, suppression on non-match, that a permissive rule cannot bypass the dwell threshold, per-rule cooldown, and event-timestamp window evaluation.tests/test_memory.py(+4) — the object class reaching storage, including a non-person class and the legacyNonedefault.341 passedlocally, excluding eight modules needing heavy ML dependencies (ultralytics,deep_sort_realtime,matplotlib, Kafka) andtests/integration/test_backend.py.ruff checkis clean on every touched path. Also verified end to end against the real config path: creating, editing, disabling, and breaking the rules file while running, with the trigger andGET /rulesboth tracking the file live.Notes for reviewers
services/memory/pipeline.pyimportedconfluent_kafkaunconditionally, though it is undeclared in every requirements file and only used whenUSE_KAFKAis set — that made the module unimportable in CI, so it is now imported only when Kafka is enabled. Separately,services/rules/__init__.pyexports only the pure engine, not the YAML-reading provider, so PyYAML doesn't become a hard import for the memory package thatphase3-tests.ymldoesn't install.tests/integration/test_backend.pyalready fails onmain(11 errors — it patches abackend._redisattribute that doesn't exist). Unrelated to this PR, and no workflow runs that file.GET /rulesdirectly; I left it out to keep this reviewable and because writes would need a second source of truth alongside the YAML file..env.example:ALERT_RULES_PATH,RULES_TIMEZONE,RULES_RELOAD_SECONDS.