From a56d9dd323531a1995486b07976e001a6c756156 Mon Sep 17 00:00:00 2001 From: wunder957 Date: Mon, 11 Sep 2023 10:34:43 +0800 Subject: [PATCH 01/13] Update design docs --- docs/design/README.md | 5 +- docs/design/image/architecture.png | Bin 41845 -> 76949 bytes docs/design/image/dataflow.png | Bin 49791 -> 56366 bytes docs/design/src/architecture.drawio | 144 ++++++++++++++++++++++------ docs/design/src/dataflow.drawio | 92 ++++++++++-------- 5 files changed, 170 insertions(+), 71 deletions(-) diff --git a/docs/design/README.md b/docs/design/README.md index 12ecf22..927a357 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -6,6 +6,7 @@ Key Components and Features: - [ ] **HTTP / RPC Server**: PIP Server, providing API for PDP to get data usage information. - [ ] **Analyzer**: Analyze data usage information and generate data usage behavior. + - [ ] **DBAnalyzer**: Analyze data usage information from database. - [X] **CLI**: CLI for administrator to manage duetector. - [X] **BccMonitor**: Monitor data usage behavior in kernel space. Use BCC to implement. - [X] **ShMonitor**: A general monitor for custom command. Polling the output of command. @@ -32,5 +33,5 @@ Current data flow implementation: The following are not yet realized and may be subject to change. -- [ ] **Collector** will get data from **Collector**. -- [ ] **HTTP / RPC Server** will get data from **Collector**. +- [ ] **Analyzer**'s data stracture and API. +- [ ] **Query Service** will get data from **Analyzer**. diff --git a/docs/design/image/architecture.png b/docs/design/image/architecture.png index 18828246f8960724019755cdbfa5d541bfa1c8c3..40510aa5d9d6f5867e847c625f1604e11f4fe8ef 100644 GIT binary patch literal 76949 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 Kcmat3dyi#g5K4$Ftce0=sf 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 diff --git a/docs/design/image/dataflow.png b/docs/design/image/dataflow.png index 96a809125c8888643b8bf8ec0b518a7c9fe40274..326a25bb3ae446ee030055658886b0d51f2f52e3 100644 GIT binary patch literal 56366 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 O2s~Z=T-G@yGywoq#0%U2 delta 32180 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 - + + + + + + + + + + + + + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - - + + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/docs/design/src/dataflow.drawio b/docs/design/src/dataflow.drawio index 5ac4266..410780c 100644 --- a/docs/design/src/dataflow.drawio +++ b/docs/design/src/dataflow.drawio @@ -1,37 +1,37 @@ - + - + - + - + - + - + - + - + - + @@ -42,19 +42,19 @@ - + - + - + - + @@ -64,43 +64,43 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -110,7 +110,7 @@ - + @@ -120,7 +120,7 @@ - + @@ -130,51 +130,63 @@ - + - + - + - + - + - + - + - + - + - + - + + + + - + + + + + + + + + + From ae017254b17ccccfb4a10a1d4af1c2d579f12fcd Mon Sep 17 00:00:00 2001 From: wunder957 Date: Mon, 11 Sep 2023 11:20:17 +0800 Subject: [PATCH 02/13] Prepare class and docs --- docs/source/analyzer/db.rst | 10 ++++++++++ docs/source/analyzer/index.rst | 25 +++++++++++++++++++++++++ docs/source/analyzer/models.rst | 7 +++++++ docs/source/collectors/index.rst | 12 +++++++++++- docs/source/filters/base.rst | 10 ---------- docs/source/filters/index.rst | 11 +++++++++-- docs/source/index.rst | 7 ++----- docs/source/managers/index.rst | 7 +++++-- docs/source/monitors/index.rst | 6 ++++-- docs/source/tracers/index.rst | 6 ++++-- docs/source/utilities.rst | 12 ++++++++++++ duetector/analyzer/__init__.py | 0 duetector/analyzer/base.py | 23 +++++++++++++++++++++++ duetector/analyzer/db.py | 23 +++++++++++++++++++++++ duetector/analyzer/models.py | 0 duetector/static/config.toml | 9 +++++++++ duetector/tools/config_generator.py | 8 ++++++++ tests/test_db_analyzer.py | 8 ++++++++ 18 files changed, 160 insertions(+), 24 deletions(-) create mode 100644 docs/source/analyzer/db.rst create mode 100644 docs/source/analyzer/index.rst create mode 100644 docs/source/analyzer/models.rst delete mode 100644 docs/source/filters/base.rst create mode 100644 docs/source/utilities.rst create mode 100644 duetector/analyzer/__init__.py create mode 100644 duetector/analyzer/base.py create mode 100644 duetector/analyzer/db.py create mode 100644 duetector/analyzer/models.py create mode 100644 tests/test_db_analyzer.py diff --git a/docs/source/analyzer/db.rst b/docs/source/analyzer/db.rst new file mode 100644 index 0000000..fec6378 --- /dev/null +++ b/docs/source/analyzer/db.rst @@ -0,0 +1,10 @@ +DBAnalyzer +=============================== + +``DBAnalyzer`` + +.. autoclass:: duetector.analyzer.db.DBAnalyzer + :members: + :undoc-members: + :private-members: + :show-inheritance: diff --git a/docs/source/analyzer/index.rst b/docs/source/analyzer/index.rst new file mode 100644 index 0000000..ae2bcff --- /dev/null +++ b/docs/source/analyzer/index.rst @@ -0,0 +1,25 @@ +Analyzer +========================================= + + +.. autoclass:: duetector.analyzer.base.Analyzer + :members: + :undoc-members: + :show-inheritance: + + +Avaliable Analyzer +----------------------------------------------- + +.. toctree:: + :maxdepth: 2 + + DB Analyzer + +Data Models +----------------------------------------------- + +.. toctree:: + :maxdepth: 2 + + Data Models diff --git a/docs/source/analyzer/models.rst b/docs/source/analyzer/models.rst new file mode 100644 index 0000000..551fc30 --- /dev/null +++ b/docs/source/analyzer/models.rst @@ -0,0 +1,7 @@ +Data Models +=============================== + +.. .. autoclass:: duetector.collectors.models.Tracking +.. :members: +.. :undoc-members: +.. :show-inheritance: diff --git a/docs/source/collectors/index.rst b/docs/source/collectors/index.rst index 718d445..b7f4ba6 100644 --- a/docs/source/collectors/index.rst +++ b/docs/source/collectors/index.rst @@ -18,9 +18,19 @@ and store them in a somewhere. :show-inheritance: +Avaliable Collector +--------------------------------------------------------- + .. toctree:: :maxdepth: 2 - :caption: Avaliable Collector and Data Models DB Collectors + + +Data Models +--------------------------------------------------------- + +.. toctree:: + :maxdepth: 2 + Data Models diff --git a/docs/source/filters/base.rst b/docs/source/filters/base.rst deleted file mode 100644 index 77a9c34..0000000 --- a/docs/source/filters/base.rst +++ /dev/null @@ -1,10 +0,0 @@ -BaseFilter -================== - -Base class for all filters. - - -.. autoclass:: duetector.filters.base.Filter - :members: - :undoc-members: - :private-members: diff --git a/docs/source/filters/index.rst b/docs/source/filters/index.rst index 5361bac..249463f 100644 --- a/docs/source/filters/index.rst +++ b/docs/source/filters/index.rst @@ -3,9 +3,16 @@ Filter ``Filter`` will filter the data based on the given criteria. +.. autoclass:: duetector.filters.base.Filter + :members: + :undoc-members: + :private-members: + + +Avaliable Filter +------------------------------------------------------ + .. toctree:: :maxdepth: 2 - :caption: Avaliable Filter - Base Filter Pattern Filter diff --git a/docs/source/index.rst b/docs/source/index.rst index 6b1106e..4381a54 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,6 +15,7 @@ Reference CLI + Analyzer Monitors Managers @@ -22,11 +23,7 @@ Reference Filters Collectors - Exceptions - - Database utilities - Config utilities - Tools utilities + Utilities Indices and tables diff --git a/docs/source/managers/index.rst b/docs/source/managers/index.rst index 7ae567d..7defda4 100644 --- a/docs/source/managers/index.rst +++ b/docs/source/managers/index.rst @@ -10,9 +10,12 @@ Managers provides a way to get instances of both built-in implementations and ex :inherited-members: :show-inheritance: + +Avaliable Manager +------------------------------------------- + .. toctree:: - :maxdepth: 1 - :caption: Avaliable Manager + :maxdepth: 2 Collector Manager Filter Manager diff --git a/docs/source/monitors/index.rst b/docs/source/monitors/index.rst index f9e51bb..b46da89 100644 --- a/docs/source/monitors/index.rst +++ b/docs/source/monitors/index.rst @@ -14,9 +14,11 @@ entry point for the polling, filtering and collecting of the data. :show-inheritance: +Avaliable Monitor +------------------------------------------- + .. toctree:: - :maxdepth: 1 - :caption: Avaliable Monitor + :maxdepth: 2 Bcc Monitor Shell Monitor diff --git a/docs/source/tracers/index.rst b/docs/source/tracers/index.rst index 36e9245..d26116d 100644 --- a/docs/source/tracers/index.rst +++ b/docs/source/tracers/index.rst @@ -7,9 +7,11 @@ Tracer :inherited-members: +Avaliable Tracer +-------------------------------------- + .. toctree:: - :maxdepth: 1 - :caption: Avaliable Tracer + :maxdepth: 2 CloneTracer OpenTracer diff --git a/docs/source/utilities.rst b/docs/source/utilities.rst new file mode 100644 index 0000000..fad7411 --- /dev/null +++ b/docs/source/utilities.rst @@ -0,0 +1,12 @@ +Utilities +=============================== + +.. toctree:: + :maxdepth: 2 + :caption: Utilities Documentation: + + Exceptions + + Database utilities + Config utilities + Tools utilities diff --git a/duetector/analyzer/__init__.py b/duetector/analyzer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/duetector/analyzer/base.py b/duetector/analyzer/base.py new file mode 100644 index 0000000..237cbd9 --- /dev/null +++ b/duetector/analyzer/base.py @@ -0,0 +1,23 @@ +from duetector.config import Configuable + + +class Analyzer(Configuable): + """ + A base class for all analyzers. + """ + + default_config = { + "disabled": False, + } + """ + Default config for ``Analyzer``. + """ + + config_scope = "analyzer" + + @property + def disabled(self): + """ + If current analyzer is disabled. + """ + return self.config.disabled diff --git a/duetector/analyzer/db.py b/duetector/analyzer/db.py new file mode 100644 index 0000000..39f81a8 --- /dev/null +++ b/duetector/analyzer/db.py @@ -0,0 +1,23 @@ +from typing import Any, Dict, Optional + +from duetector.analyzer.base import Analyzer +from duetector.collectors.db import DBCollector +from duetector.db import SessionManager + + +class DBAnalyzer(Analyzer): + """ + A analyzer using database. + """ + + default_config = { + **Analyzer.default_config, + "db": {**DBCollector.default_config.get("db", {})}, + } + + config_scope = "db_analyzer" + + def __init__(self, config: Optional[Dict[str, Any]] = None, *args, **kwargs): + super().__init__(config, *args, **kwargs) + # Init as a submodel + self.sm = SessionManager(self.config._config_dict) diff --git a/duetector/analyzer/models.py b/duetector/analyzer/models.py new file mode 100644 index 0000000..e69de29 diff --git a/duetector/static/config.toml b/duetector/static/config.toml index 540bd46..5e4e6bc 100644 --- a/duetector/static/config.toml +++ b/duetector/static/config.toml @@ -98,3 +98,12 @@ max_workers = 10 [monitor.sh.poller] interval_ms = 500 + +[db_analyzer] +disabled = false + +[db_analyzer.db] +table_prefix = "duetector_tracking" + +[db_analyzer.db.engine] +url = "sqlite:///duetector-dbcollector.sqlite3" diff --git a/duetector/tools/config_generator.py b/duetector/tools/config_generator.py index 92e8416..7b6703d 100644 --- a/duetector/tools/config_generator.py +++ b/duetector/tools/config_generator.py @@ -4,6 +4,7 @@ import tomli_w +from duetector.analyzer.db import DBAnalyzer from duetector.config import ConfigLoader from duetector.log import logger from duetector.managers import CollectorManager, FilterManager, TracerManager @@ -56,6 +57,11 @@ class ConfigGenerator: All monitors to inspect. """ + analyzer = [DBAnalyzer] + """ + All analyzers to inspect. + """ + def __init__( self, load: bool = True, @@ -79,6 +85,8 @@ def __init__( for m in self.monitors: _recursive_load(m.config_scope, self.dynamic_config, m.default_config) + for a in self.analyzer: + _recursive_load(a.config_scope, self.dynamic_config, a.default_config) # This will generate default config file if not exists if load: self.loaded_config = ConfigLoader(path, load_env, dump_when_load=False).load_config() diff --git a/tests/test_db_analyzer.py b/tests/test_db_analyzer.py new file mode 100644 index 0000000..6baaf38 --- /dev/null +++ b/tests/test_db_analyzer.py @@ -0,0 +1,8 @@ +import pytest + +from duetector.analyzer.db import DBAnalyzer + + +@pytest.fixture +def db_analyzer(full_config): + yield DBAnalyzer(full_config) From fe63ce24024b56ae1f950f1e43746c99be0dc8b6 Mon Sep 17 00:00:00 2001 From: wunder957 Date: Mon, 11 Sep 2023 15:00:51 +0800 Subject: [PATCH 03/13] Add inspect for db and some improve --- duetector/collectors/db.py | 2 +- duetector/db.py | 56 +++++++++++++++++++++++++++++--- duetector/monitors/sh_monitor.py | 2 +- duetector/static/config.toml | 2 +- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/duetector/collectors/db.py b/duetector/collectors/db.py index 846a403..a1519c8 100644 --- a/duetector/collectors/db.py +++ b/duetector/collectors/db.py @@ -56,7 +56,7 @@ def summary(self) -> Dict: .first()[0] .to_tracking(), } - for tracer, m in self.sm.get_all_model().items() + for tracer, m in self.sm.get_all_models().items() } diff --git a/duetector/db.py b/duetector/db.py index 457a698..622c263 100644 --- a/duetector/db.py +++ b/duetector/db.py @@ -54,9 +54,21 @@ class SessionManager(Configuable): .. code-block:: python from duetector.db import SessionManager + from duetector.collectors.models import Tracking sessionmanager = SessionManager() + t = Tracking( + tracer="t", + ) + m = sessionmanager.get_tracking_model(t.tracer, "id") + with sessionmanager.begin() as session: - session.add(...) + session.add(m(**t.model_dump(exclude=["tracer"]))) + session.commit() + + assert sessionmanager.inspect_all_tables() == [ + sessionmanager.get_table_names("t", "id") + ] + assert sessionmanager.inspect_all_tables("not-exist") == [] """ @@ -149,6 +161,15 @@ def begin(self) -> Generator[Session, None, None]: with self.sessionmaker.begin() as session: yield session + def get_table_names(self, tracer: str = "unknown", collector_id: str = "") -> str: + return f"{self.table_prefix}:{tracer}@{collector_id}" + + def table_name_to_tracer(self, table_name: str) -> str: + return table_name.split(":")[1].split("@")[0] + + def table_name_to_collector_id(self, table_name: str) -> str: + return table_name.split(":")[1].split("@")[1] + def get_tracking_model(self, tracer: str = "unknown", collector_id: str = "") -> type: """ Get a sqlalchemy model for tracking, each tracer will create a table in database. @@ -169,7 +190,7 @@ class Base(DeclarativeBase): pass class TrackingModel(Base, TrackingMixin): - __tablename__ = f"{self.table_prefix}:{tracer}@{collector_id}" + __tablename__ = self.get_table_names(tracer, collector_id) def to_tracking(self) -> Tracking: return Tracking( @@ -191,9 +212,25 @@ def to_tracking(self) -> Tracking: raise return self._tracking_models[tracer] - def get_all_model(self) -> Dict[str, type]: + def get_all_models(self) -> Dict[str, type]: return self._tracking_models + def inspect_all_tables( + self, tracer: Optional[str] = None, collector_id: Optional[str] = None + ) -> str: + def _filter(t): + if tracer and self.table_name_to_tracer(t) != tracer: + return False + if collector_id and self.table_name_to_collector_id(t) != collector_id: + return False + return True + + return [ + t + for t in sqlalchemy.inspect(self.engine).get_table_names() + if t.startswith(self.table_prefix) and _filter(t) + ] + def _init_tracking_model(self, tracking_model: type) -> type: if not sqlalchemy.inspect(self.engine).has_table(tracking_model.__tablename__): tracking_model.__table__.create(self.engine) @@ -201,4 +238,15 @@ def _init_tracking_model(self, tracking_model: type) -> type: if __name__ == "__main__": - print(SessionManager()) + sessionmanager = SessionManager() + t = Tracking( + tracer="t", + ) + m = sessionmanager.get_tracking_model(t.tracer, "id") + + with sessionmanager.begin() as session: + session.add(m(**t.model_dump(exclude=["tracer"]))) + session.commit() + + assert sessionmanager.inspect_all_tables() == [sessionmanager.get_table_names("t", "id")] + assert sessionmanager.inspect_all_tables("not-exist") == [] diff --git a/duetector/monitors/sh_monitor.py b/duetector/monitors/sh_monitor.py index b999992..b9044c1 100644 --- a/duetector/monitors/sh_monitor.py +++ b/duetector/monitors/sh_monitor.py @@ -84,7 +84,7 @@ class ShMonitor(Monitor): default_config = { **Monitor.default_config, "auto_init": True, - "timeout": 30, + "timeout": 5, } """ Default config for this monitor. diff --git a/duetector/static/config.toml b/duetector/static/config.toml index 5e4e6bc..fbdd4e9 100644 --- a/duetector/static/config.toml +++ b/duetector/static/config.toml @@ -91,7 +91,7 @@ interval_ms = 500 [monitor.sh] disabled = false auto_init = true -timeout = 30 +timeout = 5 [monitor.sh.backend_args] max_workers = 10 From d3ee3b5537e65693e7fba4456fb67c4e13cf35c4 Mon Sep 17 00:00:00 2001 From: wunder957 Date: Mon, 11 Sep 2023 17:15:50 +0800 Subject: [PATCH 04/13] Support query and inspect trackings --- duetector/analyzer/base.py | 30 +++++++--- duetector/analyzer/db.py | 103 ++++++++++++++++++++++++++++++++++- duetector/db.py | 8 ++- duetector/filters/base.py | 23 ++++---- duetector/static/config.toml | 3 - tests/config.toml | 9 ++- tests/test_db_analyzer.py | 49 ++++++++++++++++- 7 files changed, 196 insertions(+), 29 deletions(-) diff --git a/duetector/analyzer/base.py b/duetector/analyzer/base.py index 237cbd9..e039583 100644 --- a/duetector/analyzer/base.py +++ b/duetector/analyzer/base.py @@ -1,3 +1,5 @@ +from typing import List + from duetector.config import Configuable @@ -6,18 +8,32 @@ class Analyzer(Configuable): A base class for all analyzers. """ - default_config = { - "disabled": False, - } + default_config = {} """ Default config for ``Analyzer``. """ config_scope = "analyzer" + """ + Config scope for this analyzer. - @property - def disabled(self): + Subclasses cloud override this. + """ + + def get_all_tracers(self) -> List[str]: """ - If current analyzer is disabled. + Get all tracers from storage. + + Returns: + List[str]: List of tracer's name. + """ + raise NotImplementedError + + def get_all_collector_ids(self) -> List[str]: + """ + Get all collector id from storage. + + Returns: + List[str]: List of collector id. """ - return self.config.disabled + raise NotImplementedError diff --git a/duetector/analyzer/db.py b/duetector/analyzer/db.py index 39f81a8..2f571b6 100644 --- a/duetector/analyzer/db.py +++ b/duetector/analyzer/db.py @@ -1,23 +1,120 @@ -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional + +from sqlalchemy import select from duetector.analyzer.base import Analyzer from duetector.collectors.db import DBCollector +from duetector.collectors.models import Tracking from duetector.db import SessionManager class DBAnalyzer(Analyzer): """ A analyzer using database. + + As a top model, it will init a ``SessionManager`` and pass it to submodels. + + Config scope is ``db_analyzer``. + + Example: + + .. code-block:: python + + from duetector.analyzer.db import DBAnalyzer + from duetector.collectors.models import Tracking + + collector_id = "db_analyzer_tests_collector" + tracking = Tracking( + tracer="t", + ) + db_analyzer = DBAnalyzer() + m = db_analyzer.sm.get_tracking_model(tracking.tracer, collector_id) + with db_analyzer.sm.begin() as session: + session.add(m(**tracking.model_dump(exclude=["tracer"]))) + session.commit() + + assert tracking in db_analyzer.query_all() + assert tracking in db_analyzer.query_all(tracer=tracking.tracer) + assert tracking in db_analyzer.query_all(collector_id=collector_id) + assert tracking in db_analyzer.query_all( + tracer=tracking.tracer, collector_id=collector_id + ) + assert not db_analyzer.query_all(tracer="not-exist") + assert not db_analyzer.query_all(collector_id="not-exist") + + Note: + Currently, it will **NOT** be configured by ``DBcollector``'s config, + as we design it to be a standalone model. """ default_config = { **Analyzer.default_config, - "db": {**DBCollector.default_config.get("db", {})}, + "db": { + **DBCollector.default_config.get("db", {}) + }, # Use the same default config as DBCollector } + """ + Default config for ``DBAnalyzer``. + """ config_scope = "db_analyzer" + """ + Config scope for this analyzer. + """ def __init__(self, config: Optional[Dict[str, Any]] = None, *args, **kwargs): super().__init__(config, *args, **kwargs) # Init as a submodel - self.sm = SessionManager(self.config._config_dict) + self.sm: SessionManager = SessionManager(self.config._config_dict) + + def query_all( + self, + tracer: Optional[str] = None, + collector_id: Optional[str] = None, + ) -> List[Tracking]: + """ + Query all tracking records from database. + + Args: + tracer (Optional[str], optional): Tracer's name. Defaults to None, all tracers will be queried. + collector_id (Optional[str], optional): Collector id. Defaults to None, all collector id will be queried. + + Returns: + List[Tracking]: List of tracking records. + + TODO: + Time range support. Depends on tracer's implementation. + """ + + tables = self.sm.inspect_all_tables() + if tracer: + tables = [t for t in tables if self.sm.table_name_to_tracer(t) == tracer] + if collector_id: + tables = [t for t in tables if self.sm.table_name_to_collector_id(t) == collector_id] + + r = [] + for t in tables: + tracer = self.sm.table_name_to_tracer(t) + collector_id = self.sm.table_name_to_collector_id(t) + m = self.sm.get_tracking_model(tracer, collector_id) + with self.sm.begin() as session: + r.extend([t.to_tracking() for t, *_ in session.execute(select(m)).fetchall()]) + return r + + def get_all_tracers(self) -> List[str]: + """ + Get all tracers from database. + + Returns: + List[str]: List of tracer's name. + """ + return self.sm.inspect_all_tracers() + + def get_all_collector_ids(self) -> List[str]: + """ + Get all collector id from database. + + Returns: + List[str]: List of collector id. + """ + return self.sm.inspect_all_collector_ids() diff --git a/duetector/db.py b/duetector/db.py index 622c263..dbfb926 100644 --- a/duetector/db.py +++ b/duetector/db.py @@ -1,6 +1,6 @@ from contextlib import contextmanager from threading import Lock -from typing import Any, Dict, Generator, Optional +from typing import Any, Dict, Generator, List, Optional import sqlalchemy # type: ignore from sqlalchemy.orm import ( # type: ignore @@ -231,6 +231,12 @@ def _filter(t): if t.startswith(self.table_prefix) and _filter(t) ] + def inspect_all_tracers(self) -> List[str]: + return list(set(self.table_name_to_tracer(t) for t in self.inspect_all_tables())) + + def inspect_all_collector_ids(self) -> List[str]: + return list(set(self.table_name_to_collector_id(t) for t in self.inspect_all_tables())) + def _init_tracking_model(self, tracking_model: type) -> type: if not sqlalchemy.inspect(self.engine).has_table(tracking_model.__tablename__): tracking_model.__table__.create(self.engine) diff --git a/duetector/filters/base.py b/duetector/filters/base.py index 7915a7e..fa8d78c 100644 --- a/duetector/filters/base.py +++ b/duetector/filters/base.py @@ -14,22 +14,23 @@ class Filter(Configuable): subclass should override ``filter`` method. User should call Filter() directly to filter data, + Example: - .. code-block:: python + .. code-block:: python - from duetector.filters import Filter - from duetector.collectors.models import Tracking + from duetector.filters import Filter + from duetector.collectors.models import Tracking - class MyFilter(Filter): - def filter(self, data: Tracking) -> Optional[Tracking]: - if data.fname == "/etc/passwd": - return None - return data + class MyFilter(Filter): + def filter(self, data: Tracking) -> Optional[Tracking]: + if data.fname == "/etc/passwd": + return None + return data - f = MyFilter() - f(Tracking(fname="/etc/passwd")) # None - f(Tracking(fname="/etc/shadow")) # Tracking(fname="/etc/shadow") + f = MyFilter() + f(Tracking(fname="/etc/passwd")) # None + f(Tracking(fname="/etc/shadow")) # Tracking(fname="/etc/shadow") """ default_config = { diff --git a/duetector/static/config.toml b/duetector/static/config.toml index fbdd4e9..c5ea18d 100644 --- a/duetector/static/config.toml +++ b/duetector/static/config.toml @@ -99,9 +99,6 @@ max_workers = 10 [monitor.sh.poller] interval_ms = 500 -[db_analyzer] -disabled = false - [db_analyzer.db] table_prefix = "duetector_tracking" diff --git a/tests/config.toml b/tests/config.toml index 5a42e6a..7b3dc63 100644 --- a/tests/config.toml +++ b/tests/config.toml @@ -20,7 +20,7 @@ re_exclude_fname = [ "/run", "/usr/lib", "/etc/ld.so.cache", - "/re/*" + "/re/*", ] re_exclude_comm = [] exclude_pid = [] @@ -61,8 +61,13 @@ maxlen = 1024 disabled = false auto_init = true - [monitor.sh] disabled = false auto_init = true timeout = 5 + +[db_analyzer.db] +table_prefix = "duetector_tracking" + +[db_analyzer.db.engine] +url = "sqlite:///:memory:" diff --git a/tests/test_db_analyzer.py b/tests/test_db_analyzer.py index 6baaf38..a0daf79 100644 --- a/tests/test_db_analyzer.py +++ b/tests/test_db_analyzer.py @@ -1,8 +1,53 @@ import pytest from duetector.analyzer.db import DBAnalyzer +from duetector.collectors.models import Tracking @pytest.fixture -def db_analyzer(full_config): - yield DBAnalyzer(full_config) +def tracer_name(): + return "db_analyzer_tests" + + +@pytest.fixture +def collector_id(): + return "db_analyzer_tests_collector" + + +@pytest.fixture +def tracking(tracer_name): + yield Tracking( + tracer=tracer_name, + ) + + +@pytest.fixture +def db_analyzer(full_config, tracking, collector_id): + db_analyzer = DBAnalyzer(full_config) + sessionmanager = db_analyzer.sm + + m = sessionmanager.get_tracking_model(tracking.tracer, collector_id) + + with sessionmanager.begin() as session: + session.add(m(**tracking.model_dump(exclude=["tracer"]))) + session.commit() + + assert sessionmanager.inspect_all_tables() == [ + sessionmanager.get_table_names(tracking.tracer, collector_id) + ] + assert sessionmanager.inspect_all_tables("not-exist") == [] + yield db_analyzer + + +def test_query_all(db_analyzer: DBAnalyzer, tracking, collector_id): + assert tracking in db_analyzer.query_all() + assert tracking in db_analyzer.query_all(tracer=tracking.tracer) + assert tracking in db_analyzer.query_all(collector_id=collector_id) + assert tracking in db_analyzer.query_all(tracer=tracking.tracer, collector_id=collector_id) + + assert not db_analyzer.query_all(tracer="not-exist") + assert not db_analyzer.query_all(collector_id="not-exist") + + +if __name__ == "__main__": + pytest.main(["-vv", "-s", __file__]) From 454665c2f8f9344fa391862afca47d4bbfbc9641 Mon Sep 17 00:00:00 2001 From: wunder957 Date: Mon, 11 Sep 2023 18:30:13 +0800 Subject: [PATCH 05/13] Convert timestamp to datetime --- duetector/collectors/base.py | 4 ++-- duetector/collectors/db.py | 2 +- duetector/collectors/models.py | 25 ++++++++++++++++++++----- duetector/db.py | 7 ++++--- duetector/tracers/tcpconnect.py | 11 +++++++++-- duetector/utils.py | 25 +++++++++++++++++++++++++ tests/test_bcc_monitor.py | 10 ++++++---- tests/test_collector.py | 10 +++++++--- 8 files changed, 74 insertions(+), 20 deletions(-) diff --git a/duetector/collectors/base.py b/duetector/collectors/base.py index 3242179..7d8ff5f 100644 --- a/duetector/collectors/base.py +++ b/duetector/collectors/base.py @@ -132,8 +132,8 @@ def summary(self) -> Dict: return { tracer: { "count": len(trackings), - "first": trackings[0].timestamp, - "last": trackings[-1].timestamp, + "first": trackings[0].dt, + "last": trackings[-1].dt, "most_recent": trackings[-1].model_dump(), } for tracer, trackings in self._trackings.items() diff --git a/duetector/collectors/db.py b/duetector/collectors/db.py index a1519c8..a258c7b 100644 --- a/duetector/collectors/db.py +++ b/duetector/collectors/db.py @@ -51,7 +51,7 @@ def summary(self) -> Dict: return { tracer: { "count": len(session.execute(select(m)).fetchall()), - "first at": session.execute(select(m)).first()[0].timestamp, + "first at": session.execute(select(m)).first()[0].dt, "last": session.execute(select(m).order_by(m.id.desc())) # type: ignore .first()[0] .to_tracking(), diff --git a/duetector/collectors/models.py b/duetector/collectors/models.py index 169edb9..fd1d131 100644 --- a/duetector/collectors/models.py +++ b/duetector/collectors/models.py @@ -1,9 +1,12 @@ from __future__ import annotations +from datetime import datetime from typing import Any, Dict, NamedTuple, Optional import pydantic +from duetector.utils import get_datetime_duration_ns + class Tracking(pydantic.BaseModel): """ @@ -43,9 +46,9 @@ class Tracking(pydantic.BaseModel): File name which is being accessed """ - timestamp: Optional[int] = None + dt: Optional[datetime] = None """ - Timestamp of event, ns since boot + datetime of event """ extended: Dict[str, Any] = {} @@ -53,6 +56,13 @@ class Tracking(pydantic.BaseModel): Extended fields, will be stored in ``extended`` field as a dict """ + @classmethod + def normalize_field(cls, field, data): + if field == "timestamp": + field = "dt" + data = get_datetime_duration_ns(data) + return field, data + @staticmethod def from_namedtuple(tracer, data: NamedTuple) -> Tracking: # type: ignore """ @@ -71,10 +81,11 @@ def from_namedtuple(tracer, data: NamedTuple) -> Tracking: # type: ignore "extended": {}, } for field in data._fields: # type: ignore - if field in Tracking.model_fields: - args[field] = getattr(data, field) + k, v = Tracking.normalize_field(field, getattr(data, field)) + if k in Tracking.model_fields: + args[k] = v else: - args["extended"][field] = getattr(data, field) + args["extended"][k] = v if not args.get("cwd"): # Try get cwd from /proc//cwd @@ -86,3 +97,7 @@ def from_namedtuple(tracer, data: NamedTuple) -> Tracking: # type: ignore pass return Tracking(**args) + + +if __name__ == "__main__": + Tracking(tracer="test", dt=datetime.now()) diff --git a/duetector/db.py b/duetector/db.py index dbfb926..89247c4 100644 --- a/duetector/db.py +++ b/duetector/db.py @@ -1,4 +1,5 @@ from contextlib import contextmanager +from datetime import datetime from threading import Lock from typing import Any, Dict, Generator, List, Optional @@ -29,7 +30,7 @@ class TrackingMixin: pid: Mapped[Optional[int]] uid: Mapped[Optional[int]] gid: Mapped[Optional[int]] - timestamp: Mapped[Optional[int]] + dt: Mapped[Optional[datetime]] comm: Mapped[Optional[str]] cwd: Mapped[Optional[str]] @@ -38,7 +39,7 @@ class TrackingMixin: extended: Mapped[Dict[str, Any]] = mapped_column(type_=JSON, default={}) def __repr__(self): - return f"" + return f"" class SessionManager(Configuable): @@ -198,7 +199,7 @@ def to_tracking(self) -> Tracking: pid=self.pid, uid=self.uid, gid=self.gid, - timestamp=self.timestamp, + dt=self.dt, comm=self.comm, cwd=self.cwd, fname=self.fname, diff --git a/duetector/tracers/tcpconnect.py b/duetector/tracers/tcpconnect.py index c199e16..69e9915 100644 --- a/duetector/tracers/tcpconnect.py +++ b/duetector/tracers/tcpconnect.py @@ -27,7 +27,10 @@ class TcpconnectTracer(BccTracer): def poll_args(self): return {"timeout": int(self.config.poll_timeout)} - data_t = namedtuple("TcpTracking", ["pid", "uid", "gid", "comm", "saddr", "daddr", "dport"]) + data_t = namedtuple( + "TcpTracking", + ["pid", "uid", "gid", "comm", "saddr", "daddr", "dport", "timestamp"], + ) prog = """ #include @@ -45,6 +48,8 @@ def poll_args(self): u32 pid; u32 uid; u32 gid; + + u64 timestamp; char comm[TASK_COMM_LEN]; }; int do_trace(struct pt_regs *ctx, struct sock *sk) @@ -88,6 +93,7 @@ def poll_args(self): event.pid = pid; event.uid = bpf_get_current_uid_gid(); event.gid = bpf_get_current_uid_gid() >> 32; + event.timestamp = bpf_ktime_get_ns(); bpf_get_current_comm(&event.comm, sizeof(event.comm)); // output buffer.ringbuf_output(&event, sizeof(event), 0); @@ -102,7 +108,8 @@ def poll_args(self): def _convert_data(self, data) -> NamedTuple: data = super()._convert_data(data) return data._replace( - saddr=inet_ntoa(data.saddr).decode("utf-8"), daddr=inet_ntoa(data.daddr).decode("utf-8") + saddr=inet_ntoa(data.saddr).decode("utf-8"), + daddr=inet_ntoa(data.daddr).decode("utf-8"), ) # type: ignore def set_callback(self, host, callback: Callable[[NamedTuple], None]): diff --git a/duetector/utils.py b/duetector/utils.py index 8c18017..0a27adf 100644 --- a/duetector/utils.py +++ b/duetector/utils.py @@ -1,3 +1,11 @@ +from datetime import datetime, timedelta + +try: + from functools import cache +except ImportError: + from functools import lru_cache as cache + + class Singleton(type): _instances = {} @@ -15,3 +23,20 @@ def inet_ntoa(addr) -> bytes: dq = dq + b"." addr = addr >> 8 return dq + + +@cache +def get_boot_time() -> datetime: + with open("/proc/stat", "r") as f: + for line in f: + if line.startswith("btime"): + return datetime.fromtimestamp(int(line.split()[1])) + raise RuntimeError("Could not find btime in /proc/stat") + + +def get_datetime_duration_ns(ns) -> datetime: + return get_boot_time() + timedelta(seconds=ns / 1e9) + + +if __name__ == "__main__": + print(get_boot_time()) diff --git a/tests/test_bcc_monitor.py b/tests/test_bcc_monitor.py index e7c2922..ba3e0fd 100644 --- a/tests/test_bcc_monitor.py +++ b/tests/test_bcc_monitor.py @@ -4,11 +4,13 @@ import pytest from duetector.collectors.models import Tracking -from duetector.config import Configuable from duetector.managers import CollectorManager, FilterManager, TracerManager from duetector.monitors.bcc_monitor import BccMonitor, Monitor from duetector.tracers.base import BccTracer, Tracer -from duetector.tracers.dummy import DummyBPF, DummyTracer +from duetector.utils import get_datetime_duration_ns + +timestamp = 13205215231927 +datetime = get_datetime_duration_ns(timestamp) class MockTracer(Tracer): @@ -24,7 +26,7 @@ def get_dummy_data(cls): gid=9999, comm="dummy", fname="dummy.file", - timestamp=13205215231927, + timestamp=timestamp, custom="dummy-xargs", ) @@ -118,7 +120,7 @@ def test_bcc_monitor(bcc_monitor: MockMonitor): comm="dummy", cwd=None, fname="dummy.file", - timestamp=13205215231927, + dt=datetime, extended={"custom": "dummy-xargs"}, ) diff --git a/tests/test_collector.py b/tests/test_collector.py index 9b0424f..e9b9b8f 100644 --- a/tests/test_collector.py +++ b/tests/test_collector.py @@ -5,6 +5,10 @@ from duetector.collectors.db import DBCollector from duetector.collectors.models import Tracking from duetector.managers import CollectorManager +from duetector.utils import get_datetime_duration_ns + +timestamp = 13205215231927 +datetime = get_datetime_duration_ns(timestamp) @pytest.fixture @@ -27,7 +31,7 @@ def data_t(): gid=9999, comm="dummy", fname="dummy.file", - timestamp=13205215231927, + timestamp=timestamp, custom="dummy-xargs", ) @@ -38,7 +42,7 @@ def test_dbcollector(dbcollector: DBCollector, data_t): assert dbcollector.summary() == { "dummy": { "count": 1, - "first at": 13205215231927, + "first at": datetime, "last": Tracking( tracer="dummy", pid=9999, @@ -47,7 +51,7 @@ def test_dbcollector(dbcollector: DBCollector, data_t): comm="dummy", cwd=None, fname="dummy.file", - timestamp=13205215231927, + dt=datetime, extended={"custom": "dummy-xargs"}, ), } From e37dfff870542345053623bb39820ba78a739981 Mon Sep 17 00:00:00 2001 From: wunder957 Date: Mon, 11 Sep 2023 18:42:23 +0800 Subject: [PATCH 06/13] More accuracy of datettime --- duetector/collectors/models.py | 4 ++-- duetector/utils.py | 6 ++++-- tests/test_bcc_monitor.py | 4 ++-- tests/test_collector.py | 4 ++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/duetector/collectors/models.py b/duetector/collectors/models.py index fd1d131..7c13fdf 100644 --- a/duetector/collectors/models.py +++ b/duetector/collectors/models.py @@ -5,7 +5,7 @@ import pydantic -from duetector.utils import get_datetime_duration_ns +from duetector.utils import get_boot_time_duration_ns class Tracking(pydantic.BaseModel): @@ -60,7 +60,7 @@ class Tracking(pydantic.BaseModel): def normalize_field(cls, field, data): if field == "timestamp": field = "dt" - data = get_datetime_duration_ns(data) + data = get_boot_time_duration_ns(data) return field, data @staticmethod diff --git a/duetector/utils.py b/duetector/utils.py index 0a27adf..c67bfa2 100644 --- a/duetector/utils.py +++ b/duetector/utils.py @@ -34,9 +34,11 @@ def get_boot_time() -> datetime: raise RuntimeError("Could not find btime in /proc/stat") -def get_datetime_duration_ns(ns) -> datetime: - return get_boot_time() + timedelta(seconds=ns / 1e9) +def get_boot_time_duration_ns(ns) -> datetime: + ns = int(ns) + return get_boot_time() + timedelta(microseconds=ns / 1000) if __name__ == "__main__": print(get_boot_time()) + print(get_boot_time_duration_ns("13205215231927")) diff --git a/tests/test_bcc_monitor.py b/tests/test_bcc_monitor.py index ba3e0fd..81d779f 100644 --- a/tests/test_bcc_monitor.py +++ b/tests/test_bcc_monitor.py @@ -7,10 +7,10 @@ from duetector.managers import CollectorManager, FilterManager, TracerManager from duetector.monitors.bcc_monitor import BccMonitor, Monitor from duetector.tracers.base import BccTracer, Tracer -from duetector.utils import get_datetime_duration_ns +from duetector.utils import get_boot_time_duration_ns timestamp = 13205215231927 -datetime = get_datetime_duration_ns(timestamp) +datetime = get_boot_time_duration_ns(timestamp) class MockTracer(Tracer): diff --git a/tests/test_collector.py b/tests/test_collector.py index e9b9b8f..0006a36 100644 --- a/tests/test_collector.py +++ b/tests/test_collector.py @@ -5,10 +5,10 @@ from duetector.collectors.db import DBCollector from duetector.collectors.models import Tracking from duetector.managers import CollectorManager -from duetector.utils import get_datetime_duration_ns +from duetector.utils import get_boot_time_duration_ns timestamp = 13205215231927 -datetime = get_datetime_duration_ns(timestamp) +datetime = get_boot_time_duration_ns(timestamp) @pytest.fixture From aa0f40d76142e387b8e844ff436d8c660e5dfc34 Mon Sep 17 00:00:00 2001 From: wunder957 Date: Mon, 11 Sep 2023 22:11:50 +0800 Subject: [PATCH 07/13] More on analyzer & Resolving circular dependencies --- CONTRIBUTING.md | 9 +++ docs/source/tracers/index.rst | 10 ++- duetector/analyzer/db.py | 125 +++++++++++++++++++++++++------ duetector/analyzer/models.py | 85 +++++++++++++++++++++ duetector/collectors/__init__.py | 6 -- duetector/collectors/db.py | 6 +- duetector/collectors/models.py | 3 + duetector/collectors/register.py | 4 + duetector/db.py | 44 +++++++++-- duetector/filters/__init__.py | 6 -- duetector/filters/register.py | 4 + duetector/managers/collector.py | 4 +- duetector/managers/filter.py | 4 +- duetector/managers/tracer.py | 4 +- duetector/tracers/__init__.py | 5 -- duetector/tracers/register.py | 4 + tests/test_db_analyzer.py | 40 ++++++---- 17 files changed, 296 insertions(+), 67 deletions(-) create mode 100644 duetector/collectors/register.py create mode 100644 duetector/filters/register.py create mode 100644 duetector/tracers/register.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73bc6b2..4ff7504 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,6 +63,7 @@ Use [start-docs-host.sh](dev-tools/start-docs-host.sh) to deploy a local http se ```bash cd ./dev-tools && ./start-docs-host.sh ``` + Access `http://localhost:8080` for docs. ## Typing @@ -72,3 +73,11 @@ Access `http://localhost:8080` for docs. ``` pytype ./duetector ``` + + +## Contributing a new tracer/filter/collector + +1. Create a new file in `duetector/tracer`, `duetector/filter` or `duetector/collector` directory, with the name `{name}.py` +2. Implement the new tracer/filter/collector +3. Add the new tracer/filter/collector to `registers` list in `duetector/tracer/register.py`, `duetector/filter/register.py` or `duetector/collector/register.py` +4. Test the new tracer/filter/collector diff --git a/docs/source/tracers/index.rst b/docs/source/tracers/index.rst index d26116d..15e5ad5 100644 --- a/docs/source/tracers/index.rst +++ b/docs/source/tracers/index.rst @@ -1,5 +1,13 @@ Tracer -========================================= +===================================== + +``Tracer`` will be capturing information in some way. +:doc:`Collector ` will convert ``Tracer``'s ``data_t`` to :doc:`Tracking `. + +.. note:: + Some filed of ``data_t`` will be converted to other more readable filed, + if you want to fit this feature, you should refer to :doc:`Tracking.normalize_field `. + .. automodule:: duetector.tracers :members: diff --git a/duetector/analyzer/db.py b/duetector/analyzer/db.py index 2f571b6..59bbaa5 100644 --- a/duetector/analyzer/db.py +++ b/duetector/analyzer/db.py @@ -1,10 +1,10 @@ +from datetime import datetime from typing import Any, Dict, List, Optional -from sqlalchemy import select +from sqlalchemy import func, select from duetector.analyzer.base import Analyzer -from duetector.collectors.db import DBCollector -from duetector.collectors.models import Tracking +from duetector.analyzer.models import AnalyzerBrief, Brief, Tracking from duetector.db import SessionManager @@ -21,26 +21,31 @@ class DBAnalyzer(Analyzer): .. code-block:: python from duetector.analyzer.db import DBAnalyzer - from duetector.collectors.models import Tracking + from duetector.analyzer.models import Tracking as AT + from duetector.collectors.models import Tracking as CT + collector_id = "db_analyzer_tests_collector" - tracking = Tracking( + c_tracking = CT( tracer="t", ) db_analyzer = DBAnalyzer() - m = db_analyzer.sm.get_tracking_model(tracking.tracer, collector_id) + m = db_analyzer.sm.get_tracking_model(c_tracking.tracer, collector_id) with db_analyzer.sm.begin() as session: - session.add(m(**tracking.model_dump(exclude=["tracer"]))) + session.add(m(**c_tracking.model_dump(exclude=["tracer"]))) session.commit() - assert tracking in db_analyzer.query_all() - assert tracking in db_analyzer.query_all(tracer=tracking.tracer) - assert tracking in db_analyzer.query_all(collector_id=collector_id) - assert tracking in db_analyzer.query_all( - tracer=tracking.tracer, collector_id=collector_id + a_tracking = AT( + tracer=c_tracking.tracer, + ) + assert a_tracking in db_analyzer.query() + assert a_tracking in db_analyzer.query(tracer=a_tracking.tracer) + assert a_tracking in db_analyzer.query(collector_id=collector_id) + assert a_tracking in db_analyzer.query( + tracer=a_tracking.tracer, collector_id=collector_id ) - assert not db_analyzer.query_all(tracer="not-exist") - assert not db_analyzer.query_all(collector_id="not-exist") + assert not db_analyzer.query(tracer="not-exist") + assert not db_analyzer.query(collector_id="not-exist") Note: Currently, it will **NOT** be configured by ``DBcollector``'s config, @@ -50,8 +55,11 @@ class DBAnalyzer(Analyzer): default_config = { **Analyzer.default_config, "db": { - **DBCollector.default_config.get("db", {}) - }, # Use the same default config as DBCollector + **SessionManager.default_config, + "engine": { + "url": "sqlite:///duetector-dbcollector.sqlite3", + }, + }, } """ Default config for ``DBAnalyzer``. @@ -67,10 +75,14 @@ def __init__(self, config: Optional[Dict[str, Any]] = None, *args, **kwargs): # Init as a submodel self.sm: SessionManager = SessionManager(self.config._config_dict) - def query_all( + def query( self, tracer: Optional[str] = None, collector_id: Optional[str] = None, + start_datetime: Optional[datetime] = None, + end_datetime: Optional[datetime] = None, + start: int = 0, + limit: int = 20, ) -> List[Tracking]: """ Query all tracking records from database. @@ -78,12 +90,13 @@ def query_all( Args: tracer (Optional[str], optional): Tracer's name. Defaults to None, all tracers will be queried. collector_id (Optional[str], optional): Collector id. Defaults to None, all collector id will be queried. - + start_datetime (Optional[datetime], optional): Start time. Defaults to None. + end_datetime (Optional[datetime], optional): End time. Defaults to None. + start (Optional[int], optional): Start index. Defaults to 0. + limit (int, optional): Limit of records. Defaults to 20. ``0`` means no limit. Returns: List[Tracking]: List of tracking records. - TODO: - Time range support. Depends on tracer's implementation. """ tables = self.sm.inspect_all_tables() @@ -97,8 +110,19 @@ def query_all( tracer = self.sm.table_name_to_tracer(t) collector_id = self.sm.table_name_to_collector_id(t) m = self.sm.get_tracking_model(tracer, collector_id) + + statm = select(m) + if start_datetime: + statm = statm.where(m.dt >= start_datetime) + if end_datetime: + statm = statm.where(m.dt <= end_datetime) + if start: + statm = statm.offset(start) + if limit: + statm = statm.limit(limit) + with self.sm.begin() as session: - r.extend([t.to_tracking() for t, *_ in session.execute(select(m)).fetchall()]) + r.extend([t.to_analyzer_tracking() for t, *_ in session.execute(statm).fetchall()]) return r def get_all_tracers(self) -> List[str]: @@ -118,3 +142,62 @@ def get_all_collector_ids(self) -> List[str]: List[str]: List of collector id. """ return self.sm.inspect_all_collector_ids() + + def _table_brief( + self, + table_name: str, + start_datetime: Optional[datetime] = None, + end_datetime: Optional[datetime] = None, + ) -> Brief: + """ + Get a brief of a table. + + Args: + table_name (str): Table's name. + + Returns: + Brief: A brief of this table. + """ + tracer = self.sm.table_name_to_tracer(table_name) + collector_id = self.sm.table_name_to_collector_id(table_name) + m = self.sm.get_tracking_model(tracer, collector_id) + + start_statm = select(m).order_by(m.dt.asc()) + end_statm = select(m).order_by(m.dt.desc()) + count_statm = select(func.count()).select_from(m) + if start_datetime: + start_statm = start_statm.where(m.dt >= start_datetime) + count_statm = count_statm.where(m.dt >= start_datetime) + if end_datetime: + end_statm = end_statm.where(m.dt <= end_datetime) + count_statm = count_statm.where(m.dt <= end_datetime) + + with self.sm.begin() as session: + return Brief( + tracer=tracer, + collector_id=collector_id, + start=session.execute(start_statm).first()[0].dt, + end=session.execute(end_statm).first()[0].dt, + count=session.execute(count_statm).scalar(), + ) + + def brief( + self, + start_datetime: Optional[datetime] = None, + end_datetime: Optional[datetime] = None, + ) -> AnalyzerBrief: + """ + Get a brief of this analyzer. + + Returns: + AnalyzerBrief: A brief of this analyzer. + """ + briefs = [ + self._table_brief(t, start_datetime, end_datetime) for t in self.sm.inspect_all_tables() + ] + + return AnalyzerBrief( + tracers=[brief.tracer for brief in briefs], + collector_ids=[brief.collector_id for brief in briefs], + briefs=briefs, + ) diff --git a/duetector/analyzer/models.py b/duetector/analyzer/models.py index e69de29..e12006e 100644 --- a/duetector/analyzer/models.py +++ b/duetector/analyzer/models.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Any, Dict, List, Optional + +import pydantic + + +class Tracking(pydantic.BaseModel): + """ + Tracking model for analyzer. + + Currently, this is a copy of ``duetector.collectors.models.Tracking``. + And as an ACL(anti-corruption layer), we will not use ``duetector.collectors.models.Tracking`` directly. + """ + + tracer: str + """ + Tracer's name + """ + + pid: Optional[int] = None + """ + Process ID + """ + uid: Optional[int] = None + """ + User ID + """ + gid: Optional[int] = None + """ + Group ID of user + """ + comm: Optional[str] = "Unknown" + """ + Command name + """ + cwd: Optional[str] = None + """ + Current working directory of process + """ + fname: Optional[str] = None + """ + File name which is being accessed + """ + + dt: Optional[datetime] = None + """ + datetime of event + """ + + extended: Dict[str, Any] = {} + """ + Extended fields, will be stored in ``extended`` field as a dict + """ + + +class Brief(pydantic.BaseModel): + """ + Brief of a tracking set, mostly a table. + """ + + tracer: str + collector_id: str + start: Optional[datetime] + end: Optional[datetime] + count: int + + +class AnalyzerBrief(pydantic.BaseModel): + """ + Brief of analyzer. + """ + + tracers: List[str] + """ + List of tracers + """ + + collector_ids: List[str] + """ + List of collector ids + """ + + briefs: List[Brief] diff --git a/duetector/collectors/__init__.py b/duetector/collectors/__init__.py index 14b1a86..1fb7d9d 100644 --- a/duetector/collectors/__init__.py +++ b/duetector/collectors/__init__.py @@ -1,9 +1,3 @@ from .base import Collector __all__ = ["Collector"] - - -# Expose for plugin system -from . import base, db - -registers = [base, db] diff --git a/duetector/collectors/db.py b/duetector/collectors/db.py index a258c7b..ccc1469 100644 --- a/duetector/collectors/db.py +++ b/duetector/collectors/db.py @@ -1,7 +1,7 @@ import copy from typing import Any, Dict, Optional -from sqlalchemy import select # type: ignore +from sqlalchemy import func, select # type: ignore from duetector.collectors.base import Collector from duetector.collectors.models import Tracking @@ -50,11 +50,11 @@ def summary(self) -> Dict: with self.sm.begin() as session: return { tracer: { - "count": len(session.execute(select(m)).fetchall()), + "count": session.execute(select(func.count()).select_from(m)).scalar(), "first at": session.execute(select(m)).first()[0].dt, "last": session.execute(select(m).order_by(m.id.desc())) # type: ignore .first()[0] - .to_tracking(), + .to_collector_tracking(), } for tracer, m in self.sm.get_all_models().items() } diff --git a/duetector/collectors/models.py b/duetector/collectors/models.py index 7c13fdf..b4084fe 100644 --- a/duetector/collectors/models.py +++ b/duetector/collectors/models.py @@ -58,6 +58,9 @@ class Tracking(pydantic.BaseModel): @classmethod def normalize_field(cls, field, data): + """ + Normalize field name and data + """ if field == "timestamp": field = "dt" data = get_boot_time_duration_ns(data) diff --git a/duetector/collectors/register.py b/duetector/collectors/register.py new file mode 100644 index 0000000..ac07b16 --- /dev/null +++ b/duetector/collectors/register.py @@ -0,0 +1,4 @@ +# Expose for plugin system +from . import base, db + +registers = [base, db] diff --git a/duetector/db.py b/duetector/db.py index 89247c4..6c4ea10 100644 --- a/duetector/db.py +++ b/duetector/db.py @@ -13,7 +13,8 @@ ) from sqlalchemy.types import JSON # type: ignore -from duetector.collectors.models import Tracking +from duetector.analyzer.models import Tracking as AT +from duetector.collectors.models import Tracking as CT from duetector.config import Configuable @@ -42,6 +43,24 @@ def __repr__(self): return f"" +class TrackingInterface: + """ + A interface for tracking + """ + + def to_collector_tracking(self) -> CT: + """ + Convert to collector's tracking model + """ + raise NotImplementedError + + def to_analyzer_tracking(self) -> AT: + """ + Convert to analyzer's tracking model + """ + raise NotImplementedError + + class SessionManager(Configuable): """ A wrapper for sqlalchemy session @@ -171,7 +190,9 @@ def table_name_to_tracer(self, table_name: str) -> str: def table_name_to_collector_id(self, table_name: str) -> str: return table_name.split(":")[1].split("@")[1] - def get_tracking_model(self, tracer: str = "unknown", collector_id: str = "") -> type: + def get_tracking_model( + self, tracer: str = "unknown", collector_id: str = "" + ) -> TrackingInterface: """ Get a sqlalchemy model for tracking, each tracer will create a table in database. @@ -190,11 +211,24 @@ def get_tracking_model(self, tracer: str = "unknown", collector_id: str = "") -> class Base(DeclarativeBase): pass - class TrackingModel(Base, TrackingMixin): + class TrackingModel(Base, TrackingMixin, TrackingInterface): __tablename__ = self.get_table_names(tracer, collector_id) - def to_tracking(self) -> Tracking: - return Tracking( + def to_collector_tracking(self) -> CT: + return CT( + tracer=tracer, + pid=self.pid, + uid=self.uid, + gid=self.gid, + dt=self.dt, + comm=self.comm, + cwd=self.cwd, + fname=self.fname, + extended=self.extended, + ) + + def to_analyzer_tracking(self) -> AT: + return AT( tracer=tracer, pid=self.pid, uid=self.uid, diff --git a/duetector/filters/__init__.py b/duetector/filters/__init__.py index 1cc0953..5fb10ae 100644 --- a/duetector/filters/__init__.py +++ b/duetector/filters/__init__.py @@ -1,9 +1,3 @@ from .base import Filter __all__ = ["Filter"] - - -# Expose for plugin system -from . import pattern - -registers = [pattern] diff --git a/duetector/filters/register.py b/duetector/filters/register.py new file mode 100644 index 0000000..41f34ed --- /dev/null +++ b/duetector/filters/register.py @@ -0,0 +1,4 @@ +# Expose for plugin system +from . import pattern + +registers = [pattern] diff --git a/duetector/managers/collector.py b/duetector/managers/collector.py index 0c40c4d..5ad7bf3 100644 --- a/duetector/managers/collector.py +++ b/duetector/managers/collector.py @@ -3,7 +3,7 @@ import pluggy -import duetector.collectors +import duetector.collectors.register from duetector.collectors.base import Collector from duetector.extension.collector import project_name from duetector.log import logger @@ -42,7 +42,7 @@ def __init__(self, config: Optional[Dict[str, Any]] = None, *args, **kwargs): self.pm = pluggy.PluginManager(PROJECT_NAME) self.pm.add_hookspecs(sys.modules[__name__]) self.pm.load_setuptools_entrypoints(PROJECT_NAME) - self.register(duetector.collectors) + self.register(duetector.collectors.register) def init(self, ignore_disabled=True) -> List[Collector]: """ diff --git a/duetector/managers/filter.py b/duetector/managers/filter.py index 16f541b..d9a8bd3 100644 --- a/duetector/managers/filter.py +++ b/duetector/managers/filter.py @@ -3,7 +3,7 @@ import pluggy -import duetector.filters +import duetector.filters.register from duetector.extension.filter import project_name from duetector.filters.base import Filter from duetector.log import logger @@ -40,7 +40,7 @@ def __init__(self, config: Optional[Dict[str, Any]] = None, *args, **kwargs): self.pm = pluggy.PluginManager(PROJECT_NAME) self.pm.add_hookspecs(sys.modules[__name__]) self.pm.load_setuptools_entrypoints(PROJECT_NAME) - self.register(duetector.filters) + self.register(duetector.filters.register) def init(self, ignore_disabled=True) -> List[Filter]: """ diff --git a/duetector/managers/tracer.py b/duetector/managers/tracer.py index e7ce10b..0afedf6 100644 --- a/duetector/managers/tracer.py +++ b/duetector/managers/tracer.py @@ -3,7 +3,7 @@ import pluggy -import duetector.tracers +import duetector.tracers.register from duetector.extension.tracer import project_name from duetector.log import logger from duetector.managers import Manager @@ -40,7 +40,7 @@ def __init__(self, config: Optional[Dict[str, Any]] = None, *args, **kwargs): self.pm = pluggy.PluginManager(PROJECT_NAME) self.pm.add_hookspecs(sys.modules[__name__]) self.pm.load_setuptools_entrypoints(PROJECT_NAME) - self.register(duetector.tracers) + self.register(duetector.tracers.register) def init(self, tracer_type=Tracer, ignore_disabled=True) -> List[Tracer]: """ diff --git a/duetector/tracers/__init__.py b/duetector/tracers/__init__.py index 6a3ee06..5d8875f 100644 --- a/duetector/tracers/__init__.py +++ b/duetector/tracers/__init__.py @@ -1,8 +1,3 @@ from .base import BccTracer, ShellTracer, Tracer __all__ = ["Tracer", "BccTracer", "ShellTracer"] - -# Expose for plugin system -from . import clone, openat2, tcpconnect, uname - -registers = [openat2, uname, tcpconnect, clone] diff --git a/duetector/tracers/register.py b/duetector/tracers/register.py new file mode 100644 index 0000000..df08838 --- /dev/null +++ b/duetector/tracers/register.py @@ -0,0 +1,4 @@ +# Expose for plugin system +from . import clone, openat2, tcpconnect, uname + +registers = [openat2, uname, tcpconnect, clone] diff --git a/tests/test_db_analyzer.py b/tests/test_db_analyzer.py index a0daf79..df7f6e5 100644 --- a/tests/test_db_analyzer.py +++ b/tests/test_db_analyzer.py @@ -1,7 +1,8 @@ import pytest from duetector.analyzer.db import DBAnalyzer -from duetector.collectors.models import Tracking +from duetector.analyzer.models import Tracking as AT +from duetector.collectors.models import Tracking as CT @pytest.fixture @@ -15,38 +16,49 @@ def collector_id(): @pytest.fixture -def tracking(tracer_name): - yield Tracking( +def c_tracking(tracer_name): + yield CT( tracer=tracer_name, ) @pytest.fixture -def db_analyzer(full_config, tracking, collector_id): +def a_tracking(tracer_name): + yield AT( + tracer=tracer_name, + ) + + +@pytest.fixture +def db_analyzer(full_config, c_tracking, collector_id): db_analyzer = DBAnalyzer(full_config) sessionmanager = db_analyzer.sm - m = sessionmanager.get_tracking_model(tracking.tracer, collector_id) + m = sessionmanager.get_tracking_model(c_tracking.tracer, collector_id) with sessionmanager.begin() as session: - session.add(m(**tracking.model_dump(exclude=["tracer"]))) + session.add(m(**c_tracking.model_dump(exclude=["tracer"]))) session.commit() assert sessionmanager.inspect_all_tables() == [ - sessionmanager.get_table_names(tracking.tracer, collector_id) + sessionmanager.get_table_names(c_tracking.tracer, collector_id) ] assert sessionmanager.inspect_all_tables("not-exist") == [] yield db_analyzer -def test_query_all(db_analyzer: DBAnalyzer, tracking, collector_id): - assert tracking in db_analyzer.query_all() - assert tracking in db_analyzer.query_all(tracer=tracking.tracer) - assert tracking in db_analyzer.query_all(collector_id=collector_id) - assert tracking in db_analyzer.query_all(tracer=tracking.tracer, collector_id=collector_id) +def test_query(db_analyzer: DBAnalyzer, a_tracking, collector_id): + assert a_tracking in db_analyzer.query() + assert a_tracking in db_analyzer.query(tracer=a_tracking.tracer) + assert a_tracking in db_analyzer.query(collector_id=collector_id) + assert a_tracking in db_analyzer.query(tracer=a_tracking.tracer, collector_id=collector_id) + + assert not db_analyzer.query(tracer="not-exist") + assert not db_analyzer.query(collector_id="not-exist") + - assert not db_analyzer.query_all(tracer="not-exist") - assert not db_analyzer.query_all(collector_id="not-exist") +def test_brief(db_analyzer: DBAnalyzer, a_tracking, collector_id): + assert db_analyzer.brief() if __name__ == "__main__": From 8c8a2a9ab70a9d013dca677b23af53376bac50c4 Mon Sep 17 00:00:00 2001 From: wunder957 Date: Mon, 11 Sep 2023 22:23:48 +0800 Subject: [PATCH 08/13] Intro autodoc-pydantic --- docs/source/analyzer/models.rst | 8 ++++---- docs/source/collectors/models.rst | 2 +- docs/source/conf.py | 1 + pyproject.toml | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/source/analyzer/models.rst b/docs/source/analyzer/models.rst index 551fc30..59d394d 100644 --- a/docs/source/analyzer/models.rst +++ b/docs/source/analyzer/models.rst @@ -1,7 +1,7 @@ Data Models =============================== -.. .. autoclass:: duetector.collectors.models.Tracking -.. :members: -.. :undoc-members: -.. :show-inheritance: +.. automodule:: duetector.analyzer.models + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/collectors/models.rst b/docs/source/collectors/models.rst index 251938e..dbb757c 100644 --- a/docs/source/collectors/models.rst +++ b/docs/source/collectors/models.rst @@ -1,7 +1,7 @@ Models for collectors ==================================== -.. autoclass:: duetector.collectors.models.Tracking +.. automodule:: duetector.collectors.models :members: :undoc-members: :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py index 587145c..0846cba 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -37,6 +37,7 @@ "sphinx.ext.viewcode", "sphinx.ext.autosectionlabel", "sphinx_click", + "sphinxcontrib.autodoc_pydantic", ] # Add any paths that contain templates here, relative to this directory. diff --git a/pyproject.toml b/pyproject.toml index 54edec7..135da34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ ] [project.optional-dependencies] test = ["pytest", "pytest-cov", "pytype"] -docs = ["Sphinx<=7.2.4", "sphinx-rtd-theme", "sphinx-click"] +docs = ["Sphinx<=7.2.4", "sphinx-rtd-theme", "sphinx-click", "autodoc_pydantic"] [project.scripts] duectl = "duetector.cli.main:cli" From 6e858307227f30fe0ee5faa95ecee397d3015517 Mon Sep 17 00:00:00 2001 From: wunder957 Date: Mon, 11 Sep 2023 22:43:48 +0800 Subject: [PATCH 09/13] Fix docs and more on base --- docs/source/analyzer/db.rst | 2 +- duetector/analyzer/base.py | 32 +++++++++++++++++++++++++++++++- duetector/analyzer/db.py | 4 ++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/docs/source/analyzer/db.rst b/docs/source/analyzer/db.rst index fec6378..8d2cffd 100644 --- a/docs/source/analyzer/db.rst +++ b/docs/source/analyzer/db.rst @@ -3,7 +3,7 @@ DBAnalyzer ``DBAnalyzer`` -.. autoclass:: duetector.analyzer.db.DBAnalyzer +.. automodule:: duetector.analyzer.db :members: :undoc-members: :private-members: diff --git a/duetector/analyzer/base.py b/duetector/analyzer/base.py index e039583..2e8416d 100644 --- a/duetector/analyzer/base.py +++ b/duetector/analyzer/base.py @@ -1,5 +1,7 @@ -from typing import List +from datetime import datetime +from typing import List, Optional +from duetector.analyzer.models import AnalyzerBrief, Tracking from duetector.config import Configuable @@ -37,3 +39,31 @@ def get_all_collector_ids(self) -> List[str]: List[str]: List of collector id. """ raise NotImplementedError + + def query( + self, + tracer: Optional[str] = None, + collector_id: Optional[str] = None, + start_datetime: Optional[datetime] = None, + end_datetime: Optional[datetime] = None, + start: int = 0, + limit: int = 20, + ) -> List[Tracking]: + """ + Query tracking data from storage. + """ + raise NotImplementedError + + def brief( + self, + start_datetime: Optional[datetime] = None, + end_datetime: Optional[datetime] = None, + ) -> AnalyzerBrief: + """ + Get brief of analyzer. + """ + raise NotImplementedError + + def analyze(self): + # TODO: Not design yet. + pass diff --git a/duetector/analyzer/db.py b/duetector/analyzer/db.py index 59bbaa5..d73bf9a 100644 --- a/duetector/analyzer/db.py +++ b/duetector/analyzer/db.py @@ -92,10 +92,10 @@ def query( collector_id (Optional[str], optional): Collector id. Defaults to None, all collector id will be queried. start_datetime (Optional[datetime], optional): Start time. Defaults to None. end_datetime (Optional[datetime], optional): End time. Defaults to None. - start (Optional[int], optional): Start index. Defaults to 0. + start (int, optional): Start index. Defaults to 0. limit (int, optional): Limit of records. Defaults to 20. ``0`` means no limit. Returns: - List[Tracking]: List of tracking records. + List[duetector.analyzer.models.Tracking]: List of tracking records. """ From 1e6bcca599bfa9d3d026968e7d8c2d9e96f2e924 Mon Sep 17 00:00:00 2001 From: wunder957 Date: Tue, 12 Sep 2023 11:06:35 +0800 Subject: [PATCH 10/13] Support detailed query --- duetector/analyzer/db.py | 26 +++++++++++++++---- duetector/analyzer/models.py | 1 + duetector/db.py | 21 +++++++++++++--- tests/test_db_analyzer.py | 49 ++++++++++++++++++++++++++++-------- 4 files changed, 79 insertions(+), 18 deletions(-) diff --git a/duetector/analyzer/db.py b/duetector/analyzer/db.py index d73bf9a..433743c 100644 --- a/duetector/analyzer/db.py +++ b/duetector/analyzer/db.py @@ -82,7 +82,10 @@ def query( start_datetime: Optional[datetime] = None, end_datetime: Optional[datetime] = None, start: int = 0, - limit: int = 20, + limit: int = 0, + columns: Optional[List[str]] = None, + where: Optional[Dict[str, Any]] = None, + distinct: bool = False, ) -> List[Tracking]: """ Query all tracking records from database. @@ -94,6 +97,8 @@ def query( end_datetime (Optional[datetime], optional): End time. Defaults to None. start (int, optional): Start index. Defaults to 0. limit (int, optional): Limit of records. Defaults to 20. ``0`` means no limit. + columns (Optional[List[str]], optional): Columns to query. Defaults to None, all columns will be queried. + where (Optional[Dict[str, Any]], optional): Where clause. Defaults to None. Returns: List[duetector.analyzer.models.Tracking]: List of tracking records. @@ -111,18 +116,28 @@ def query( collector_id = self.sm.table_name_to_collector_id(t) m = self.sm.get_tracking_model(tracer, collector_id) - statm = select(m) + columns = columns or m.inspect_fields().keys() + statm = select(*[getattr(m, k) for k in columns]).offset(start) + if start_datetime: statm = statm.where(m.dt >= start_datetime) if end_datetime: statm = statm.where(m.dt <= end_datetime) - if start: - statm = statm.offset(start) if limit: statm = statm.limit(limit) + if where: + statm = statm.where(*[getattr(m, k) == v for k, v in where.items()]) + if distinct: + statm = statm.distinct() with self.sm.begin() as session: - r.extend([t.to_analyzer_tracking() for t, *_ in session.execute(statm).fetchall()]) + r.extend( + [ + Tracking(tracer=tracer, **{k: v for k, v in zip(columns, r)}) + for r in session.execute(statm).fetchall() + ] + ) + return r def get_all_tracers(self) -> List[str]: @@ -179,6 +194,7 @@ def _table_brief( start=session.execute(start_statm).first()[0].dt, end=session.execute(end_statm).first()[0].dt, count=session.execute(count_statm).scalar(), + fields=m.inspect_fields(), ) def brief( diff --git a/duetector/analyzer/models.py b/duetector/analyzer/models.py index e12006e..5c63e1e 100644 --- a/duetector/analyzer/models.py +++ b/duetector/analyzer/models.py @@ -65,6 +65,7 @@ class Brief(pydantic.BaseModel): start: Optional[datetime] end: Optional[datetime] count: int + fields: Dict[str, Any] = {} class AnalyzerBrief(pydantic.BaseModel): diff --git a/duetector/db.py b/duetector/db.py index 6c4ea10..ebcf18e 100644 --- a/duetector/db.py +++ b/duetector/db.py @@ -45,18 +45,25 @@ def __repr__(self): class TrackingInterface: """ - A interface for tracking + A interface for tracking. """ def to_collector_tracking(self) -> CT: """ - Convert to collector's tracking model + Convert to collector's tracking model. """ raise NotImplementedError def to_analyzer_tracking(self) -> AT: """ - Convert to analyzer's tracking model + Convert to analyzer's tracking model. + """ + raise NotImplementedError + + @classmethod + def inspect_fields(cls) -> Dict[str, Any]: + """ + Inspect fields of this model. """ raise NotImplementedError @@ -240,6 +247,12 @@ def to_analyzer_tracking(self) -> AT: extended=self.extended, ) + @classmethod + def inspect_fields(cls) -> Dict[str, Any]: + return { + c.name: c.type.python_type for c in cls.__table__.columns if c.name != "id" + } + try: self._tracking_models[tracer] = self._init_tracking_model(TrackingModel) except Exception as e: @@ -279,6 +292,8 @@ def _init_tracking_model(self, tracking_model: type) -> type: if __name__ == "__main__": + from duetector.collectors.models import Tracking + sessionmanager = SessionManager() t = Tracking( tracer="t", diff --git a/tests/test_db_analyzer.py b/tests/test_db_analyzer.py index df7f6e5..533d423 100644 --- a/tests/test_db_analyzer.py +++ b/tests/test_db_analyzer.py @@ -1,9 +1,25 @@ +from datetime import datetime, timedelta + import pytest from duetector.analyzer.db import DBAnalyzer from duetector.analyzer.models import Tracking as AT from duetector.collectors.models import Tracking as CT +now = datetime.now() + +tracking_kwargs = dict( + tracer="db_analyzer_tests", + pid=9999, + uid=9999, + gid=9999, + comm="dummy", + cwd=None, + fname="dummy.file", + dt=datetime.now(), + extended={"custom": "dummy-xargs"}, +) + @pytest.fixture def tracer_name(): @@ -16,17 +32,13 @@ def collector_id(): @pytest.fixture -def c_tracking(tracer_name): - yield CT( - tracer=tracer_name, - ) +def c_tracking(): + yield CT(**tracking_kwargs) @pytest.fixture -def a_tracking(tracer_name): - yield AT( - tracer=tracer_name, - ) +def a_tracking(): + yield AT(**tracking_kwargs) @pytest.fixture @@ -37,6 +49,7 @@ def db_analyzer(full_config, c_tracking, collector_id): m = sessionmanager.get_tracking_model(c_tracking.tracer, collector_id) with sessionmanager.begin() as session: + session.add(m(**c_tracking.model_dump(exclude=["tracer"]))) session.add(m(**c_tracking.model_dump(exclude=["tracer"]))) session.commit() @@ -52,13 +65,29 @@ def test_query(db_analyzer: DBAnalyzer, a_tracking, collector_id): assert a_tracking in db_analyzer.query(tracer=a_tracking.tracer) assert a_tracking in db_analyzer.query(collector_id=collector_id) assert a_tracking in db_analyzer.query(tracer=a_tracking.tracer, collector_id=collector_id) + assert a_tracking in db_analyzer.query(start_datetime=now - timedelta(days=1)) + assert a_tracking in db_analyzer.query(end_datetime=now + timedelta(days=1)) + + assert len(db_analyzer.query()) == 2 + assert len(db_analyzer.query(distinct=True)) == 1 + + assert AT( + tracer=a_tracking.tracer, + pid=a_tracking.pid, + fname=a_tracking.fname, + ) in db_analyzer.query(columns=["pid", "fname"]) assert not db_analyzer.query(tracer="not-exist") assert not db_analyzer.query(collector_id="not-exist") + assert not db_analyzer.query(start_datetime=now + timedelta(days=1)) + assert not db_analyzer.query(end_datetime=now - timedelta(days=1)) + assert not db_analyzer.query(start=100) + assert not db_analyzer.query(where={"pid": 1}) -def test_brief(db_analyzer: DBAnalyzer, a_tracking, collector_id): - assert db_analyzer.brief() +# def test_brief(db_analyzer: DBAnalyzer, a_tracking, collector_id): +# assert db_analyzer.brief() +# # print(db_analyzer.brief()) if __name__ == "__main__": From b90b255860b8a5c6fe7dacc8c20146e6bb4ece79 Mon Sep 17 00:00:00 2001 From: wunder957 Date: Tue, 12 Sep 2023 11:11:05 +0800 Subject: [PATCH 11/13] Support orderby in query --- duetector/analyzer/db.py | 10 +++++++++- tests/test_db_analyzer.py | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/duetector/analyzer/db.py b/duetector/analyzer/db.py index 433743c..e3e0d79 100644 --- a/duetector/analyzer/db.py +++ b/duetector/analyzer/db.py @@ -86,6 +86,8 @@ def query( columns: Optional[List[str]] = None, where: Optional[Dict[str, Any]] = None, distinct: bool = False, + order_by_asc: Optional[List[str]] = None, + order_by_desc: Optional[List[str]] = None, ) -> List[Tracking]: """ Query all tracking records from database. @@ -99,6 +101,9 @@ def query( limit (int, optional): Limit of records. Defaults to 20. ``0`` means no limit. columns (Optional[List[str]], optional): Columns to query. Defaults to None, all columns will be queried. where (Optional[Dict[str, Any]], optional): Where clause. Defaults to None. + distinct (bool, optional): Distinct. Defaults to False. + order_by_asc (Optional[List[str]], optional): Order by asc. Defaults to None. + order_by_desc (Optional[List[str]], optional): Order by desc. Defaults to None. Returns: List[duetector.analyzer.models.Tracking]: List of tracking records. @@ -118,7 +123,6 @@ def query( columns = columns or m.inspect_fields().keys() statm = select(*[getattr(m, k) for k in columns]).offset(start) - if start_datetime: statm = statm.where(m.dt >= start_datetime) if end_datetime: @@ -129,6 +133,10 @@ def query( statm = statm.where(*[getattr(m, k) == v for k, v in where.items()]) if distinct: statm = statm.distinct() + if order_by_asc: + statm = statm.order_by(*[getattr(m, k).asc() for k in order_by_asc]) + if order_by_desc: + statm = statm.order_by(*[getattr(m, k).desc() for k in order_by_desc]) with self.sm.begin() as session: r.extend( diff --git a/tests/test_db_analyzer.py b/tests/test_db_analyzer.py index 533d423..71f7ca0 100644 --- a/tests/test_db_analyzer.py +++ b/tests/test_db_analyzer.py @@ -67,6 +67,8 @@ def test_query(db_analyzer: DBAnalyzer, a_tracking, collector_id): assert a_tracking in db_analyzer.query(tracer=a_tracking.tracer, collector_id=collector_id) assert a_tracking in db_analyzer.query(start_datetime=now - timedelta(days=1)) assert a_tracking in db_analyzer.query(end_datetime=now + timedelta(days=1)) + assert a_tracking in db_analyzer.query(order_by_asc=["pid"]) + assert a_tracking in db_analyzer.query(order_by_desc=["pid"]) assert len(db_analyzer.query()) == 2 assert len(db_analyzer.query(distinct=True)) == 1 From 0e726aca4232e76b233ce125c81334eb5cc8800e Mon Sep 17 00:00:00 2001 From: wunder957 Date: Tue, 12 Sep 2023 11:18:56 +0800 Subject: [PATCH 12/13] Add with_details on brief --- duetector/analyzer/db.py | 10 +++++++++- duetector/analyzer/models.py | 6 +++--- tests/test_db_analyzer.py | 7 ++++--- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/duetector/analyzer/db.py b/duetector/analyzer/db.py index e3e0d79..a80e20f 100644 --- a/duetector/analyzer/db.py +++ b/duetector/analyzer/db.py @@ -171,6 +171,7 @@ def _table_brief( table_name: str, start_datetime: Optional[datetime] = None, end_datetime: Optional[datetime] = None, + inspect: bool = True, ) -> Brief: """ Get a brief of a table. @@ -183,6 +184,8 @@ def _table_brief( """ tracer = self.sm.table_name_to_tracer(table_name) collector_id = self.sm.table_name_to_collector_id(table_name) + if not inspect: + return Brief(tracer=tracer, collector_id=collector_id) m = self.sm.get_tracking_model(tracer, collector_id) start_statm = select(m).order_by(m.dt.asc()) @@ -209,15 +212,20 @@ def brief( self, start_datetime: Optional[datetime] = None, end_datetime: Optional[datetime] = None, + with_details: bool = True, ) -> AnalyzerBrief: """ Get a brief of this analyzer. Returns: AnalyzerBrief: A brief of this analyzer. + + TODO: + Support specify tracers/collector ids/(distinct)fields """ briefs = [ - self._table_brief(t, start_datetime, end_datetime) for t in self.sm.inspect_all_tables() + self._table_brief(t, start_datetime, end_datetime, inspect=with_details) + for t in self.sm.inspect_all_tables() ] return AnalyzerBrief( diff --git a/duetector/analyzer/models.py b/duetector/analyzer/models.py index 5c63e1e..d55da4a 100644 --- a/duetector/analyzer/models.py +++ b/duetector/analyzer/models.py @@ -62,9 +62,9 @@ class Brief(pydantic.BaseModel): tracer: str collector_id: str - start: Optional[datetime] - end: Optional[datetime] - count: int + start: Optional[datetime] = None + end: Optional[datetime] = None + count: Optional[int] = None fields: Dict[str, Any] = {} diff --git a/tests/test_db_analyzer.py b/tests/test_db_analyzer.py index 71f7ca0..b529d12 100644 --- a/tests/test_db_analyzer.py +++ b/tests/test_db_analyzer.py @@ -87,9 +87,10 @@ def test_query(db_analyzer: DBAnalyzer, a_tracking, collector_id): assert not db_analyzer.query(where={"pid": 1}) -# def test_brief(db_analyzer: DBAnalyzer, a_tracking, collector_id): -# assert db_analyzer.brief() -# # print(db_analyzer.brief()) +def test_brief(db_analyzer: DBAnalyzer, a_tracking, collector_id): + assert db_analyzer.brief() + assert db_analyzer.brief(with_details=False) + # print(db_analyzer.brief()) if __name__ == "__main__": From 1badf4ed872ca82a1cd49278c4847de7a2719785 Mon Sep 17 00:00:00 2001 From: wunder957 Date: Tue, 12 Sep 2023 12:51:55 +0800 Subject: [PATCH 13/13] Support tracers, collector_ids and distinct --- duetector/analyzer/db.py | 98 +++++++++++++++++++++++++++++---------- tests/test_db_analyzer.py | 24 +++++++--- 2 files changed, 91 insertions(+), 31 deletions(-) diff --git a/duetector/analyzer/db.py b/duetector/analyzer/db.py index a80e20f..ebc750c 100644 --- a/duetector/analyzer/db.py +++ b/duetector/analyzer/db.py @@ -77,8 +77,8 @@ def __init__(self, config: Optional[Dict[str, Any]] = None, *args, **kwargs): def query( self, - tracer: Optional[str] = None, - collector_id: Optional[str] = None, + tracers: Optional[List[str]] = None, + collector_ids: Optional[List[str]] = None, start_datetime: Optional[datetime] = None, end_datetime: Optional[datetime] = None, start: int = 0, @@ -93,8 +93,8 @@ def query( Query all tracking records from database. Args: - tracer (Optional[str], optional): Tracer's name. Defaults to None, all tracers will be queried. - collector_id (Optional[str], optional): Collector id. Defaults to None, all collector id will be queried. + tracers (Optional[List[str]], optional): Tracer's name. Defaults to None, all tracers will be queried. + collector_ids (Optional[List[str]], optional): Collector id. Defaults to None, all collector id will be queried. start_datetime (Optional[datetime], optional): Start time. Defaults to None. end_datetime (Optional[datetime], optional): End time. Defaults to None. start (int, optional): Start index. Defaults to 0. @@ -110,10 +110,10 @@ def query( """ tables = self.sm.inspect_all_tables() - if tracer: - tables = [t for t in tables if self.sm.table_name_to_tracer(t) == tracer] - if collector_id: - tables = [t for t in tables if self.sm.table_name_to_collector_id(t) == collector_id] + if tracers: + tables = [t for t in tables if self.sm.table_name_to_tracer(t) in tracers] + if collector_ids: + tables = [t for t in tables if self.sm.table_name_to_collector_id(t) in collector_ids] r = [] for t in tables: @@ -141,7 +141,7 @@ def query( with self.sm.begin() as session: r.extend( [ - Tracking(tracer=tracer, **{k: v for k, v in zip(columns, r)}) + self._convert_row_to_tracking(columns, r, tracer) for r in session.execute(statm).fetchall() ] ) @@ -172,6 +172,7 @@ def _table_brief( start_datetime: Optional[datetime] = None, end_datetime: Optional[datetime] = None, inspect: bool = True, + distinct: bool = False, ) -> Brief: """ Get a brief of a table. @@ -184,48 +185,95 @@ def _table_brief( """ tracer = self.sm.table_name_to_tracer(table_name) collector_id = self.sm.table_name_to_collector_id(table_name) - if not inspect: - return Brief(tracer=tracer, collector_id=collector_id) + m = self.sm.get_tracking_model(tracer, collector_id) - start_statm = select(m).order_by(m.dt.asc()) - end_statm = select(m).order_by(m.dt.desc()) - count_statm = select(func.count()).select_from(m) + if not inspect: + return Brief(tracer=tracer, collector_id=collector_id, fields=m.inspect_fields()) + columns = m.inspect_fields().keys() + statm = select(*[getattr(m, k) for k in columns]) + if distinct: + statm = statm.distinct() if start_datetime: - start_statm = start_statm.where(m.dt >= start_datetime) - count_statm = count_statm.where(m.dt >= start_datetime) + statm = statm.where(m.dt >= start_datetime) if end_datetime: - end_statm = end_statm.where(m.dt <= end_datetime) - count_statm = count_statm.where(m.dt <= end_datetime) + statm = statm.where(m.dt <= end_datetime) + start_statm = statm.order_by(m.dt.asc()) + end_statm = statm.order_by(m.dt.desc()) + count_statm = select(func.count()).select_from(statm.subquery()) with self.sm.begin() as session: + start_tracking = self._convert_row_to_tracking( + columns, session.execute(start_statm).first(), tracer + ) + end_tracking = self._convert_row_to_tracking( + columns, session.execute(end_statm).first(), tracer + ) + return Brief( tracer=tracer, collector_id=collector_id, - start=session.execute(start_statm).first()[0].dt, - end=session.execute(end_statm).first()[0].dt, + start=start_tracking.dt, + end=end_tracking.dt, count=session.execute(count_statm).scalar(), fields=m.inspect_fields(), ) + def _convert_row_to_tracking(self, columns: List[str], row: Any, tracer: str) -> Tracking: + """ + Convert a row to a tracking record. + + Args: + columns (List[str]): Columns. + row (Any): Row. + tracer (str): Tracer's name. + + Returns: + duetector.analyzer.models.Tracking: A tracking record. + """ + if not row: + return Tracking(tracer=tracer) + + return Tracking(tracer=tracer, **{k: v for k, v in zip(columns, row)}) + def brief( self, + tracers: Optional[List[str]] = None, + collector_ids: Optional[List[str]] = None, start_datetime: Optional[datetime] = None, end_datetime: Optional[datetime] = None, with_details: bool = True, + distinct: bool = False, ) -> AnalyzerBrief: """ Get a brief of this analyzer. + Args: + tracers (Optional[List[str]], optional): + Tracers. Defaults to None, all tracers will be queried. + If a specific tracer is not found, it will be ignored. + collector_ids (Optional[List[str]], optional): + Collector ids. Defaults to None, all collector ids will be queried. + If a specific collector id is not found, it will be ignored. + start_datetime (Optional[datetime], optional): Start time. Defaults to None. + end_datetime (Optional[datetime], optional): End time. Defaults to None. + with_details (bool, optional): With details. Defaults to True. + distinct (bool, optional): Distinct. Defaults to False. + Returns: AnalyzerBrief: A brief of this analyzer. - - TODO: - Support specify tracers/collector ids/(distinct)fields """ + tables = self.sm.inspect_all_tables() + if tracers: + tables = [t for t in tables if self.sm.table_name_to_tracer(t) in tracers] + if collector_ids: + tables = [t for t in tables if self.sm.table_name_to_collector_id(t) in collector_ids] + briefs = [ - self._table_brief(t, start_datetime, end_datetime, inspect=with_details) - for t in self.sm.inspect_all_tables() + self._table_brief( + t, start_datetime, end_datetime, inspect=with_details, distinct=distinct + ) + for t in tables ] return AnalyzerBrief( diff --git a/tests/test_db_analyzer.py b/tests/test_db_analyzer.py index b529d12..1595687 100644 --- a/tests/test_db_analyzer.py +++ b/tests/test_db_analyzer.py @@ -62,9 +62,11 @@ def db_analyzer(full_config, c_tracking, collector_id): def test_query(db_analyzer: DBAnalyzer, a_tracking, collector_id): assert a_tracking in db_analyzer.query() - assert a_tracking in db_analyzer.query(tracer=a_tracking.tracer) - assert a_tracking in db_analyzer.query(collector_id=collector_id) - assert a_tracking in db_analyzer.query(tracer=a_tracking.tracer, collector_id=collector_id) + assert a_tracking in db_analyzer.query(tracers=[a_tracking.tracer]) + assert a_tracking in db_analyzer.query(collector_ids=[collector_id]) + assert a_tracking in db_analyzer.query( + tracers=[a_tracking.tracer], collector_ids=[collector_id] + ) assert a_tracking in db_analyzer.query(start_datetime=now - timedelta(days=1)) assert a_tracking in db_analyzer.query(end_datetime=now + timedelta(days=1)) assert a_tracking in db_analyzer.query(order_by_asc=["pid"]) @@ -79,8 +81,8 @@ def test_query(db_analyzer: DBAnalyzer, a_tracking, collector_id): fname=a_tracking.fname, ) in db_analyzer.query(columns=["pid", "fname"]) - assert not db_analyzer.query(tracer="not-exist") - assert not db_analyzer.query(collector_id="not-exist") + assert not db_analyzer.query(tracers=["not-exist"]) + assert not db_analyzer.query(collector_ids=["not-exist"]) assert not db_analyzer.query(start_datetime=now + timedelta(days=1)) assert not db_analyzer.query(end_datetime=now - timedelta(days=1)) assert not db_analyzer.query(start=100) @@ -89,8 +91,18 @@ def test_query(db_analyzer: DBAnalyzer, a_tracking, collector_id): def test_brief(db_analyzer: DBAnalyzer, a_tracking, collector_id): assert db_analyzer.brief() + assert db_analyzer.brief(tracers=[a_tracking.tracer]) + assert db_analyzer.brief(collector_ids=[collector_id]) + assert db_analyzer.brief(tracers=[a_tracking.tracer], collector_ids=[collector_id]) + assert db_analyzer.brief(start_datetime=now - timedelta(days=1)) + assert db_analyzer.brief(end_datetime=now + timedelta(days=1)) assert db_analyzer.brief(with_details=False) - # print(db_analyzer.brief()) + assert db_analyzer.brief(distinct=True) + + assert not db_analyzer.brief(tracers=["not-exist"]).tracers + assert not db_analyzer.brief(collector_ids=["not-exist"]).collector_ids + assert not db_analyzer.brief(start_datetime=now + timedelta(days=1)).briefs[0].count + assert not db_analyzer.brief(end_datetime=now - timedelta(days=1)).briefs[0].count if __name__ == "__main__":