-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/save persistency #144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
40 commits
Select commit
Hold shift + click to select a range
70305e6
change name to be agent agnostic
viktorbeck98 9bdb4e2
feat: add PersistencyLoadError and dump/load protocol to EventDataStr…
viktorbeck98 2327e4c
test: strengthen abstract contract verification for EventDataStructure
viktorbeck98 7bb165d
feat: add to_state/from_state serialization to SingleStabilityTracker
viktorbeck98 18990b4
feat: implement dump/load on EventTracker via MessagePack
viktorbeck98 a72b03a
docs: document converter_function and event_id limitations in EventTr…
viktorbeck98 6aa82f5
feat: implement dump/load on EventDataFrame via Parquet
viktorbeck98 b0d9c7e
docs: document event_id/template limitations in EventDataFrame load
viktorbeck98 357e7a8
feat: implement dump/load on ChunkedEventDataFrame via Parquet + msgp…
viktorbeck98 bec28ee
feat: add _dirty_count counter and reset_dirty_count() to EventPersis…
viktorbeck98 b842599
feat: add PersistencySaverConfig and _SaveTimer
viktorbeck98 5b227b1
feat: implement PersistencySaver save() and load() with fsspec
viktorbeck98 36962a2
fix: collapse _tick dead branch, coerce event ID types on load, impro…
viktorbeck98 997acdc
test: add trigger tests for PersistencySaver timer, dirty threshold, …
viktorbeck98 c869b68
fix: join timer thread in stop(), rename misleading dirty-threshold test
viktorbeck98 dc7a2f6
feat: add context manager protocol to Component for saver cleanup
viktorbeck98 0e7f2ac
fix: remove redundant hasattr, add _Stoppable Protocol type for saver
viktorbeck98 e07853f
test: add integration tests for full save/load cycle across backends
viktorbeck98 3d36643
test: add cell-value assertion to DataFrame integration test
viktorbeck98 9d3d5bb
chore: add fsspec, msgpack, pyarrow deps with optional s3/gcs/azure e…
viktorbeck98 2ef2b47
adapt gitignore
viktorbeck98 03e8162
minor changes
viktorbeck98 5c704fd
chore: never commit design docs
viktorbeck98 204bd1f
feat: change dirty_threshold default to None in PersistencySaverConfig
viktorbeck98 ac2f1fc
feat: add PersistConfig model and persist field to CoreDetectorConfig
viktorbeck98 5cd8d39
feat: add _register_persistency() helper to CoreDetector
viktorbeck98 7b1a58a
style: fix import ordering in detector.py
viktorbeck98 515ee3b
feat: handle persist block in config serialization and suppress spuri…
viktorbeck98 7f9cd7e
style: update MissingParamsWarning message to include global and persist
viktorbeck98 8bbc6df
feat: wire _register_persistency() into NewValueDetector, NewValueCom…
viktorbeck98 9b47ffd
fix: preserve persist config across set_configuration() rebuild
viktorbeck98 b893e98
fix: filter non-serializable event_data_kwargs and make stop() idempo…
viktorbeck98 1049ab0
refactor: rename dirty counter to events_since_save / events_until_save
viktorbeck98 bceb931
docs: document persist block, PersistencySaver API, and storage optio…
viktorbeck98 a58d016
docs: add persist block and detector wiring guidance to AGENTS.md
viktorbeck98 f80041e
feat: implement events_until_save trigger in PersistencySaver
viktorbeck98 57830a2
chore: merge development into feat/save_persistency
viktorbeck98 353b282
refactor: drop explicit pyarrow API in EventDataFrame
viktorbeck98 8534b8c
docs: explain msgpack config header layout in ChunkedEventDataFrame.dump
viktorbeck98 aa19b10
refactor: treat persistency as a sub-library and slim CoreDetector
viktorbeck98 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -202,3 +202,6 @@ test.py | |
|
|
||
| # claude code | ||
| CLAUDE.md | ||
| docs/superpowers/ | ||
| docs/design/ | ||
| .claude/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,127 +1,197 @@ | ||
| # Persistency | ||
|
|
||
| The persistency module provides event-based state management for detectors. It allows detectors to accumulate, store, and query data across their lifecycle — during training, detection, and auto-configuration. | ||
| The persistency module gives a detector a place to remember things about the | ||
| events it sees. State is keyed by `EventID` and survives across training, the | ||
| detection loop, and (optionally) restarts on disk. | ||
|
|
||
| ## EventPersistency | ||
| This page is structured to read top-to-bottom: first the mental model, then a | ||
| quick start, then the API surface. | ||
|
|
||
| `EventPersistency` is the main entry point. It manages one storage backend instance per event ID, so each event type maintains its own isolated state. | ||
| ## Mental model | ||
|
|
||
| ### Creating an instance | ||
| Persistency has three moving parts. Understanding what each one does makes the | ||
| rest of the page much easier to follow. | ||
|
|
||
| ```python | ||
| from detectmatelibrary.common.persistency import EventPersistency | ||
| ### 1. Events | ||
|
|
||
| persistency = EventPersistency( | ||
| event_data_class=MyBackend, # storage backend class (see below) | ||
| variable_blacklist=["Content"], # variable names to exclude (optional) | ||
| event_data_kwargs={"max_rows": 1000} # extra kwargs forwarded to the backend (optional) | ||
| ) | ||
| ``` | ||
| Logs are grouped by `EventID`. Two events with the same ID share a template | ||
| but have their own variable values. Persistency stores **one independent state | ||
| object per event ID**, so an `EventStabilityTracker` for `EventID=4733` does | ||
| not interfere with one for `EventID=4624`. | ||
|
|
||
| | Parameter | Description | | ||
| |---|---| | ||
| | `event_data_class` | An `EventDataStructure` subclass that defines how data is stored and queried. | | ||
| | `variable_blacklist` | Variable names to exclude from storage. Defaults to `["Content"]`. | | ||
| | `event_data_kwargs` | A dictionary of keyword arguments forwarded to the backend constructor. | | ||
| ### 2. Backends (`EventDataStructure`) | ||
|
|
||
| A backend is the thing that actually stores the per-event state. Persistency | ||
| owns the dict `{event_id: backend}`; the backend itself decides *how* data is | ||
| kept. | ||
|
|
||
| Two families ship today: | ||
|
|
||
| - **DataFrame backends** (`EventDataFrame`, `ChunkedEventDataFrame`) keep the | ||
| raw rows. Use these when a detector needs to scan history. | ||
| - **Tracker backends** (`EventStabilityTracker`) keep only derived features | ||
| (e.g. "this variable has been constant for the last 10k events"). Use these | ||
| when you only need a summary, not the raw history — they cost a fraction of | ||
| the memory. | ||
|
|
||
| All backends implement the same four-method contract: `add_data`, `get_data`, | ||
| `dump`, `load`. That contract is what `EventPersistency` and | ||
| `PersistencySaver` rely on — anything you add later only has to follow it. | ||
|
|
||
| ### 3. Saver lifecycle (`PersistencySaver`) | ||
|
|
||
| ### Storing data | ||
| `EventPersistency` itself is in-memory. To survive a process restart, the | ||
| state has to be written somewhere. `PersistencySaver` wraps an | ||
| `EventPersistency` and: | ||
|
|
||
| - writes to disk (or any `fsspec` URI) on two triggers — a wall-clock interval | ||
| and an event-count threshold; | ||
| - optionally `auto_load`s previously saved state during construction; | ||
| - exposes `start()` / `stop()` so the background timer can be torn down | ||
| cleanly. `stop()` is idempotent and is called automatically when a | ||
| `Component` is used as a context manager. | ||
|
|
||
| In practice a detector never instantiates `PersistencySaver` directly: it sets | ||
| a `persist:` block in its config and `CoreDetector` wires the saver up via | ||
| [`init_persistency`](../../src/detectmatelibrary/common/persist.py). | ||
|
|
||
| --- | ||
|
|
||
| ## Quick start | ||
|
|
||
| ```python | ||
| persistency.ingest_event( | ||
| event_id=event_id, | ||
| event_template=template, | ||
| variables=positional_vars, # optional positional variables | ||
| named_variables=named_vars # optional named variables | ||
| from detectmatelibrary.utils import persistency | ||
|
|
||
| ep = persistency.EventPersistency( | ||
| event_data_class=persistency.EventStabilityTracker, | ||
| ) | ||
|
|
||
| ep.ingest_event( | ||
| event_id="4624", | ||
| event_template="An account was successfully logged on.", | ||
| named_variables={"AccountName": "alice", "LogonType": "3"}, | ||
| ) | ||
|
|
||
| tracker = ep.get_event_data("4624") # or ep["4624"] | ||
| ``` | ||
|
|
||
| Each call appends data to the backend associated with the given `event_id`. If no backend exists for that ID yet, one is created automatically. | ||
| That snippet covers the whole in-memory API: pick a backend class, ingest | ||
| events, query state. | ||
|
|
||
| ### Retrieving data | ||
| --- | ||
|
|
||
| ```python | ||
| # Single event | ||
| data = persistency.get_event_data(event_id) | ||
| ## API reference | ||
|
|
||
| # All events | ||
| all_data = persistency.get_events_data() # dict[event_id -> backend] | ||
| ### `EventPersistency` | ||
|
|
||
| # Templates | ||
| template = persistency.get_event_template(event_id) | ||
| all_templates = persistency.get_event_templates() | ||
| | Parameter | Description | | ||
| |---|---| | ||
| | `event_data_class` | An `EventDataStructure` subclass; one instance is created per event ID. | | ||
| | `variable_blacklist` | Variable names to skip when ingesting. Defaults to `["Content"]`. | | ||
| | `event_data_kwargs` | Extra kwargs forwarded to each backend instance. | | ||
|
|
||
| # Bracket access | ||
| backend = persistency[event_id] | ||
| ``` | ||
| Common methods: | ||
|
|
||
| ## Storage backends | ||
| ```python | ||
| ep.ingest_event(event_id, event_template, variables=..., named_variables=...) | ||
|
|
||
| ep.get_event_data(event_id) # backend for a single event | ||
| ep.get_events_data() # dict[event_id -> backend] | ||
| ep.get_event_template(event_id) | ||
| ep.get_event_templates() | ||
| ep.get_events_seen() # all event IDs ever ingested | ||
| ep[event_id] # alias for get_event_data | ||
| ``` | ||
|
|
||
| The backend determines how ingested data is stored and what queries are available. Choose the backend that fits your detector's needs. | ||
| ### Available backends | ||
|
|
||
| ### DataFrame backends | ||
| | Class | Use when | | ||
| |---|---| | ||
| | `persistency.EventDataFrame` | You need history and a Pandas DataFrame is the natural shape. | | ||
| | `persistency.ChunkedEventDataFrame` | High-volume / streaming workloads — Polars-backed with row-retention and automatic compaction. | | ||
| | `persistency.EventStabilityTracker` | You only care about how variables behave over time (`STATIC` / `STABLE` / `UNSTABLE` / `RANDOM`). Cheapest memory footprint. | | ||
|
|
||
| Store raw event data in tabular form. Useful when a detector needs to query or iterate over historical values. | ||
| All three are re-exported from the top of the package — `persistency.X` is the | ||
| canonical import; the deeply nested submodules are an implementation detail. | ||
|
|
||
| - **`EventDataFrame`** — Pandas-backed storage. Simple and familiar. | ||
| - **`ChunkedEventDataFrame`** — Polars-backed storage with configurable row retention and automatic compaction. Suited for high-volume or streaming workloads. | ||
| ### Persisting to disk | ||
|
|
||
| ```python | ||
| from detectmatelibrary.common.persistency.event_data_structures.dataframes import ( | ||
| EventDataFrame, | ||
| ChunkedEventDataFrame, | ||
| saver = persistency.PersistencySaver( | ||
| ep, | ||
| persistency.PersistencySaverConfig( | ||
| path="./state/my-detector", | ||
| save_interval_seconds=300, | ||
| events_until_save=10_000, # save after this many ingests, too | ||
| auto_load=False, | ||
| storage_options={}, # forwarded to fsspec | ||
| ), | ||
| ) | ||
| saver.start() | ||
| # ... detector runs ... | ||
| saver.stop() # final flush, stops the background timer | ||
| ``` | ||
|
|
||
| ### Tracker backends | ||
|
|
||
| Track variable behavior over time rather than storing raw data. Useful when a detector needs to understand how variables evolve (e.g., whether they converge to constant values). Is optimized for space efficiency since only extracted features from the logs are stored. | ||
| `PersistencySaver.save()` is thread-safe, and `stop()` is idempotent. The two | ||
| save triggers (`save_interval_seconds` and `events_until_save`) are | ||
| independent — whichever fires first wins. | ||
|
|
||
| - **`EventStabilityTracker`** — Classifies each variable as `STATIC`, `STABLE`, `UNSTABLE`, `RANDOM`, or `INSUFFICIENT_DATA` based on how its values change over time. | ||
| #### Restoring state | ||
|
|
||
| ```python | ||
| from detectmatelibrary.common.persistency.event_data_structures.trackers import ( | ||
| EventStabilityTracker, | ||
| saver = persistency.PersistencySaver( | ||
| ep, | ||
| persistency.PersistencySaverConfig(path="./state/my-detector", auto_load=True), | ||
| ) | ||
| # ep is now pre-populated from disk | ||
| ``` | ||
|
|
||
| ## Usage in detectors | ||
| If `auto_load=True` and no saved state exists, the constructor raises | ||
| `persistency.PersistencyLoadError` immediately — fail-fast rather than | ||
| silently starting empty. | ||
|
|
||
| Persistency is **optional**. A detector can function without it. When a detector does need to maintain state across events — for example, to learn normal values during training and flag deviations during detection — it can integrate persistency by following this pattern: | ||
| ### Storage backends (fsspec) | ||
|
|
||
| ### 1. Initialize in `__init__` | ||
| `PersistencySaverConfig.path` accepts any URI fsspec understands: a local path | ||
| (`./state`), `s3://bucket/key`, `gs://...`, `az://...`, and so on. Provider | ||
| credentials and tuning knobs go in `storage_options`. | ||
|
|
||
| Create one or more `EventPersistency` instances with the appropriate backend. | ||
| --- | ||
|
|
||
| ```python | ||
| class MyDetector(CoreDetector): | ||
| def __init__(self, name="MyDetector", config=MyDetectorConfig()): | ||
| super().__init__(name=name, ...) | ||
| self.persistency = EventPersistency( | ||
| event_data_class=EventStabilityTracker, | ||
| ) | ||
| ``` | ||
| ## Using persistency inside a detector | ||
|
|
||
| ### 2. Accumulate state in `train()` | ||
| The recommended path: declare `persist:` in the detector's config and let | ||
| `CoreDetector._register_persistency` build the saver for you. See | ||
| [Saving state (persist)](../detectors.md#saving-state-persist) for the config | ||
| schema. | ||
|
|
||
| During training, ingest each event so the backend builds up its internal state. | ||
| In detector code, the pattern is: | ||
|
|
||
| ```python | ||
| def train(self, input_): | ||
| variables = self.get_configured_variables(input_, self.config.events) | ||
| self.persistency.ingest_event( | ||
| event_id=input_["EventID"], | ||
| event_template=input_["template"], | ||
| named_variables=variables, | ||
| ) | ||
| ``` | ||
| from detectmatelibrary.common.detector import CoreDetector | ||
| from detectmatelibrary.utils import persistency | ||
|
|
||
| ### 3. Query state in `detect()` | ||
| class MyDetector(CoreDetector): | ||
| def __init__(self, name="MyDetector", config=MyDetectorConfig()): | ||
| super().__init__(name=name, config=config) | ||
| self.persistency = persistency.EventPersistency( | ||
| event_data_class=persistency.EventStabilityTracker, | ||
| ) | ||
| self._register_persistency(self.persistency) | ||
|
|
||
| During detection, query the accumulated state to decide whether the incoming event is anomalous. | ||
| def train(self, input_): | ||
| self.persistency.ingest_event( | ||
| event_id=input_["EventID"], | ||
| event_template=input_["template"], | ||
| named_variables={...}, | ||
| ) | ||
|
|
||
| ```python | ||
| def detect(self, input_, output_): | ||
| for event_id, backend in self.persistency.get_events_data().items(): | ||
| stored_data = backend.get_data() | ||
| # compare input_ against stored_data to produce alerts | ||
| def detect(self, input_, output_): | ||
| tracker = self.persistency.get_events_data().get(input_["EventID"]) | ||
| # compare against tracker to produce alerts | ||
| ``` | ||
|
|
||
| `_register_persistency` is a one-line wrapper around | ||
| [`init_persistency`](../../src/detectmatelibrary/common/persist.py); the helper | ||
| honours `config.persist` and returns `None` (so `self.saver` stays `None`) | ||
| when persistence is disabled. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets leave it like this for now, but I think this documentation can be a little hard to follow if you dont know how the persistency works