diff --git a/.gitignore b/.gitignore index 6b08a83..df525fa 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,8 @@ data/traces/* # Keep data directories present in git if marker files are added. !data/models/.gitkeep !data/traces/.gitkeep + +# Ignore SQLite database files +*.db +*.sqlite3 +*.sqlite diff --git a/README.md b/README.md index 044fb97..2624089 100644 --- a/README.md +++ b/README.md @@ -14,48 +14,27 @@ CogniOS bridges this gap by collecting real-time system telemetry, analyzing wor The project is designed as both: -* A practical system observability platform for Linux -* A research framework for experimenting with AI-assisted operating system scheduling +- A practical system observability platform for Linux +- A research framework for experimenting with AI-assisted operating system scheduling --- ## Key Features -* ๐Ÿ“Š Real-time Linux system telemetry collection -* ๐Ÿฉบ Intelligent anomaly detection for performance degradation -* ๐ŸŽฏ AI-based workload classification and optimization -* ๐Ÿ“ฆ Rolling telemetry recording for crash replay -* ๐Ÿ”ฌ Scheduling algorithm benchmarking and research -* ๐Ÿ“ˆ Interactive Streamlit dashboard -* ๐Ÿ—„๏ธ Centralized SQLite telemetry database -* ๐Ÿงฉ Modular architecture for independent development and testing +- ๐Ÿ“Š Real-time Linux system telemetry collection +- ๐Ÿฉบ Intelligent anomaly detection for performance degradation +- ๐ŸŽฏ AI-based workload classification and optimization +- ๐Ÿ“ฆ Rolling telemetry recording for crash replay +- ๐Ÿ”ฌ Scheduling algorithm benchmarking and research +- ๐Ÿ“ˆ Interactive Streamlit dashboard +- ๐Ÿ—„๏ธ Centralized SQLite telemetry database +- ๐Ÿงฉ Modular architecture for independent development and testing --- # Architecture -``` - Linux System - โ”‚ - โ–ผ - Telemetry Collectors - (/proc, psutil, process APIs) - โ”‚ - โ–ผ - SQLite Database - โ”‚ - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ”‚ โ”‚ โ”‚ - โ–ผ โ–ผ โ–ผ - OS Doctor FocusOS BlackBox - โ”‚ โ”‚ โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ–ผ - Research Engine - โ”‚ - โ–ผ - Streamlit Dashboard -``` +![alt text](./assets/image.png) --- @@ -67,10 +46,10 @@ OS Doctor continuously monitors the system for abnormal behavior using machine l ### Objective -* Detect abnormal CPU, memory and I/O behavior -* Explain possible reasons for system slowdowns -* Identify unusual workload patterns -* Provide interpretable diagnostics from live telemetry +- Detect abnormal CPU, memory and I/O behavior +- Explain possible reasons for system slowdowns +- Identify unusual workload patterns +- Provide interpretable diagnostics from live telemetry --- @@ -80,10 +59,10 @@ FocusOS is responsible for intelligent workload classification and adaptive syst ### Objective -* Classify the current workload using machine learning -* Optimize process priorities -* Modify Linux scheduling parameters -* Improve overall system responsiveness +- Classify the current workload using machine learning +- Optimize process priorities +- Modify Linux scheduling parameters +- Improve overall system responsiveness Future versions may also support dynamic CPU affinity optimization and workload-aware scheduling. @@ -97,10 +76,10 @@ It continuously stores recent telemetry in a rolling buffer, allowing developers ### Objective -* Record recent system telemetry -* Preserve crash history -* Replay workload traces -* Assist in post-mortem debugging +- Record recent system telemetry +- Preserve crash history +- Replay workload traces +- Assist in post-mortem debugging --- @@ -110,18 +89,18 @@ The Research Engine provides an experimentation platform for scheduling algorith ### Objective -* Compare classical scheduling algorithms -* Evaluate AI-based schedulers -* Replay collected workloads -* Benchmark scheduling performance -* Support reinforcement learning experiments +- Compare classical scheduling algorithms +- Evaluate AI-based schedulers +- Replay collected workloads +- Benchmark scheduling performance +- Support reinforcement learning experiments Supported scheduling algorithms include: -* FCFS -* Shortest Job First -* Round Robin -* Priority Scheduling +- FCFS +- Shortest Job First +- Round Robin +- Priority Scheduling Future versions may include reinforcement learning schedulers. @@ -133,11 +112,11 @@ The Streamlit dashboard serves as the unified visualization layer of CogniOS. ### Objective -* Visualize system telemetry -* Display anomaly alerts -* Monitor workload classifications -* Compare scheduling results -* Provide a centralized monitoring interface +- Visualize system telemetry +- Display anomaly alerts +- Monitor workload classifications +- Compare scheduling results +- Provide a centralized monitoring interface --- @@ -147,12 +126,12 @@ Collectors gather telemetry directly from Linux using lightweight system interfa ### Objective -* CPU monitoring -* Memory monitoring -* Disk monitoring -* Process monitoring -* Network statistics -* I/O statistics +- CPU monitoring +- Memory monitoring +- Disk monitoring +- Process monitoring +- Network statistics +- I/O statistics These collectors act as the data source for every other module. @@ -164,11 +143,11 @@ The data layer stores all telemetry generated by the collectors. ### Responsibilities -* Store real-time telemetry -* Maintain workload traces -* Persist crash recordings -* Provide data for ML training -* Support benchmarking experiments +- Store real-time telemetry +- Maintain workload traces +- Persist crash recordings +- Provide data for ML training +- Support benchmarking experiments SQLite is used as the centralized storage backend. @@ -252,26 +231,26 @@ Each major subsystem is designed as an independent module with its own documenta Every module contains its own dedicated `README.md` describing: -* Module architecture -* Directory structure -* Components -* APIs -* Data flow -* Usage -* Future work +- Module architecture +- Directory structure +- Components +- APIs +- Data flow +- Usage +- Future work --- # Roadmap -* Real-time Linux telemetry collection -* Intelligent anomaly detection -* AI-assisted workload optimization -* Crash replay and forensic analysis -* Scheduling benchmark suite -* Reinforcement learning scheduler -* GPU telemetry support -* Windows and macOS support +- Real-time Linux telemetry collection +- Intelligent anomaly detection +- AI-assisted workload optimization +- Crash replay and forensic analysis +- Scheduling benchmark suite +- Reinforcement learning scheduler +- GPU telemetry support +- Windows and macOS support --- @@ -279,12 +258,12 @@ Every module contains its own dedicated `README.md` describing: We welcome contributions from developers interested in: -* Operating Systems -* Machine Learning -* Systems Programming -* Linux Internals -* Performance Engineering -* Data Engineering +- Operating Systems +- Machine Learning +- Systems Programming +- Linux Internals +- Performance Engineering +- Data Engineering Before contributing, please read the documentation for the module you wish to work on. diff --git a/assets/image.png b/assets/image.png new file mode 100644 index 0000000..1662e15 Binary files /dev/null and b/assets/image.png differ diff --git a/blackbox/README.md b/blackbox/README.md new file mode 100644 index 0000000..e292a82 --- /dev/null +++ b/blackbox/README.md @@ -0,0 +1,708 @@ +# BlackBox Module โ€” CogniOS + +> **"Airplane ka black box"** โ€” continuously record karta rehta hai, crash/freeze ke baad replay karke batata hai exactly kya hua tha aur kyun. + +--- + +## 1. Module Overview + +BlackBox is CogniOS's **flight recorder and forensic analysis engine**. It answers one core question: + +> _"My system froze/crashed โ€” what happened in the 30 minutes before it?"_ + +### What BlackBox is + +- A real-time telemetry recorder (rolling 30-minute window) +- An anomaly detection engine (Z-score + Isolation Forest) +- A root-cause explanation system (Correlation Engine + LLM) + +### Core pipeline + +``` +Layer 1/2/3/4 Telemetry + โ†“ +Rolling Event Store (last 30 min only) + โ†“ +Heartbeat System (crash/freeze detection) + โ†“ +Feature Engineering (120 rows โ†’ 1 feature vector) + โ†“ +Z-score Detector (sudden spikes + slow drift) + โ†“ +Isolation Forest (multi-metric anomaly confirmation) + โ†“ +Rule Engine (deterministic backup checks) + โ†“ +Correlation Engine (CAUSE โ†’ EFFECT chain) + โ†“ +Replay Timeline + โ†“ +LLM Explanation (human-readable root cause) + โ†“ +Dashboard Alert +``` + +--- + +## 2. File Structure + +``` +blackbox/ +โ”œโ”€โ”€ recorder.py # Rolling event store โ€” writes + prunes telemetry +โ”œโ”€โ”€ heartbeat.py # Crash/freeze detection via heartbeat timestamps +โ”œโ”€โ”€ feature_engineering.py # Converts raw DB rows โ†’ statistical feature vector +โ”œโ”€โ”€ zscore_detector.py # Z-score + slope (trend) based anomaly detector +โ”œโ”€โ”€ anomaly_model.py # Isolation Forest wrapper (sklearn) +โ”œโ”€โ”€ rule_engine.py # Deterministic threshold-based checks (backup) +โ”œโ”€โ”€ correlation.py # Builds CAUSE โ†’ EFFECT event chains +โ”œโ”€โ”€ replay.py # Reconstructs timeline from rolling window +โ”œโ”€โ”€ nl_query.py # Natural language query interface over crash data +``` + +``` +utils/ +โ”œโ”€โ”€ config.py # All BlackBox config constants +โ”œโ”€โ”€ db.py # SQLite connection + schema helpers +โ””โ”€โ”€ helpers.py # rate_mb_s() and other shared utilities +``` + +--- + +## 3. Function Definitions + +### `recorder.py` + +```python +def write_telemetry(conn, metrics: dict) -> None +``` + +**Purpose:** Inserts one Layer 1 metrics row into the rolling telemetry store. +Calls `prune_old_records()` after every write to enforce the 30-minute window. + +```python +def prune_old_records(conn) -> None +``` + +**Purpose:** Deletes rows older than `BLACKBOX_WINDOW_SEC` (default 1800s). + +```sql +DELETE FROM telemetry WHERE timestamp < (strftime('%s','now') - 1800) +``` + +--- + +### `heartbeat.py` + +```python +def update_heartbeat(conn) -> None +``` + +**Purpose:** Updates a single-row heartbeat table with `time.time()` every second. +If the daemon crashes, the last saved timestamp persists in SQLite. + +```python +def check_crash_on_startup(conn) -> tuple[bool, float] +``` + +**Purpose:** Called once at daemon startup. Reads last heartbeat timestamp. +Returns `(crash_detected: bool, gap_seconds: float)`. +If `graceful_shutdown = 0` AND `gap > BLACKBOX_CRASH_GAP_SEC` โ†’ crash was detected. +Note: Uses graceful shutdown flag approach (not hardcoded gap > 10s โ€” that was arbitrary). +Triggers `replay.py` to reconstruct the pre-crash timeline. + +--- + +### `feature_engineering.py` + +```python +def extract_feature_vector(rows: list[dict]) -> list[float] +``` + +**Purpose:** Converts the last 120 raw telemetry rows (2 minutes ร— 1 sample/sec) +into a single statistical feature vector for anomaly detection. + +**Input:** 120 dicts from `layer1_system` table +**Output:** +`[mean_cpu, max_cpu, cpu_growth_rate, cpu_variance, mean_ram, memory_growth_rate, disk_spike_frequency, context_switch_rate]` + +Why statistical summary and not raw rows? + +- Isolation Forest is **not a time-series model** โ€” it takes a single point in feature space +- Z-score also operates on aggregated values +- 8 features instead of 120ร—8=960 raw values = faster + less noise + +--- + +### `zscore_detector.py` + +```python +class ZScoreDetector: + def __init__(self, + zscore_window=1800, # seconds of rolling baseline + trend_window=600, # seconds for slope calculation + sustained_sec=30, # spike must last this long + z_threshold=2.8, + slope_threshold=0.003, + sustained_ratio=0.6) +``` + +```python + def update(self, val: float) -> None +``` + +**Purpose:** Appends new reading to all internal deques (zscore, trend, sustained). + +```python + def check(self, val: float, metric_name: str, unit: str) -> list[dict] +``` + +**Purpose:** Runs three independent checks and returns list of detected issues. + +**Check 1 โ€” Z-score (sudden spike):** + +``` +Z = (current_value - rolling_mean) / rolling_std +If |Z| > 2.8 AND spike sustained for 30s โ†’ ALERT +``` + +Catches: CPU suddenly 35% โ†’ 95% + +**Check 2 โ€” Slope/Trend (slow drift):** + +```python +slope = np.polyfit(time_axis, values, 1)[0] # linear regression +If slope > 0.003 %/sec โ†’ ALERT (memory leak pattern) +``` + +Catches: Memory slowly 40% โ†’ 80% over 2 hours (Z-score misses this) +Bonus: predicts `ETA to critical = (90 - current) / slope / 60` minutes + +**Check 3 โ€” Sustained filter (false positive prevention):** + +``` +If < 60% readings above threshold in last 30s โ†’ skip (compilation burst) +If โ‰ฅ 60% readings above threshold โ†’ real anomaly +``` + +Prevents: Short CPU bursts from `gcc` compilation triggering false alerts + +**Why Z-score alone is not enough:** + +- Rolling window adapts to slow drift โ†’ memory leak goes undetected +- Single-metric check โ†’ misses multi-metric correlations +- Solution: Z-score + Slope + Isolation Forest together + +--- + +### `anomaly_model.py` + +```python +def train(normal_feature_vectors: np.ndarray) -> IsolationForest +``` + +**Purpose:** Trains Isolation Forest on normal telemetry data only. +`contamination=0.05` means we expect ~5% of data to be anomalous. + +```python +def predict(model: IsolationForest, feature_vector: list) -> tuple[int, float] +``` + +**Purpose:** Returns `(label, score)` where: + +- `label = 1` โ†’ normal +- `label = -1` โ†’ anomaly +- `score` โ†’ negative = more anomalous + +**How Isolation Forest works:** +Isolation Forest is an **unsupervised algorithm that detects anomalies in a feature space** โ€” NOT a time-series model. + +Core intuition: + +- Normal points cluster together โ†’ many random splits needed to isolate them +- Anomaly points are sparse โ†’ isolated with very few splits +- Points isolated in fewer splits = higher anomaly score + +``` +Normal [cpu=35, mem=55, disk=2]: deep in tree โ†’ many splits โ†’ normal +Anomaly [cpu=40, mem=40, disk=40, net=40]: shallow in tree โ†’ few splits โ†’ anomaly +``` + +Key advantage over Z-score: detects **multi-metric combinations** that are individually normal but collectively anomalous. + +Example: `CPU=40%, MEM=40%, DISK=40%, NET=40%` โ€” each metric looks fine individually, but this combination never appears in normal usage . + +--- + +### `rule_engine.py` + +```python +def check_rules(metrics: dict) -> list[dict] +``` + +**Purpose:** Deterministic backup checks โ€” always runs alongside ML models. +Catches obvious anomalies even before warmup period ends. + +```python +RULES = [ + {"condition": lambda m: m["cpu"] > 95, "alert": "cpu_critical"}, + {"condition": lambda m: m["thread_growth_rate"] > 200, "alert": "thread_explosion"}, + {"condition": lambda m: m["zombie_count"] > 10, "alert": "zombie_buildup"}, + {"condition": lambda m: m["memory_growth_continuous"], "alert": "memory_leak"}, +] +``` + +Why rule engine alongside ML? + +- ML models need warmup period (60s) โ†’ rules work from second 1 +- Rules are 100% deterministic โ†’ never fail unexpectedly +- Double detection = higher confidence alerts + +--- + +### `correlation.py` + +```python +def build_event_chain(events: list[dict]) -> list[dict] +``` + +**Purpose:** Takes raw timestamped events and builds a CAUSE โ†’ EFFECT chain +by correlating events that occurred close together in time. + +Example output: + +``` +Chrome opened (10:01) + โ†“ +Renderer processes +12 (10:02) + โ†“ +Thread count +340 in 60s (10:03) + โ†“ +CPU saturation 87% (10:04) + โ†“ +Isolation Forest: ANOMALY -1 (10:05) + โ†“ +System freeze detected (10:05) +``` + +```python +def telemetry_to_events(rows: list[dict]) -> list[dict] +``` + +**Purpose:** Converts raw telemetry rows into discrete events with type + timestamp. + +```python +{"type": "cpu_spike", "time": "10:03:45", "detail": "cpu jumped to 87%"} +``` + +--- + +### `replay.py` + +```python +def replay(conn, crash_time: float, window_minutes: int = 30) -> list[dict] +``` + +**Purpose:** Fetches all telemetry rows from `crash_time - window_minutes` to `crash_time`. +Returns ordered list for dashboard timeline visualization. + +```python +def generate_narrative(events: list[dict], anomaly_type: str) -> str +``` + +**Purpose:** Formats the event chain into a structured JSON for LLM input. + +--- + +### `nl_query.py` + +```python +def query(conn, question: str) -> str +``` + +**Purpose:** Accepts free-text questions about crash data and returns answers +by querying SQLite + formatting context for LLM. + +Example: + +``` +Input: "Which process caused the freeze at 10:05?" +Output: "Chrome (PID 1823) spawned 340 additional threads between 10:01-10:04, + causing CPU saturation that led to the system freeze." +``` + +--- + +## 4. Useful Telemetry Data + +BlackBox consumes data from all 4 collector layers: + +### From `layer1_system` (Layer 1 โ€” every 1s) + +| Column | Use in BlackBox | +| ------------------- | --------------------------------------- | +| `cpu_usage_percent` | Primary Z-score metric, spike detection | +| `memory_percent` | Memory leak slope detection | +| `disk_read_mb_s` | I/O storm detection | +| `net_rate_mb_s` | Network exfiltration detection | +| `cpu_ctx_switches` | Scheduler congestion (feature vector) | +| `total_processes` | Thread explosion detection | +| `zombie_processes` | Zombie accumulation rule check | +| `load_avg_1` | Scheduler load feature | +| `swap_percent` | Memory pressure feature | + +### From `layer2_top_processes` (Layer 2 โ€” every 5s) + +| Column | Use in BlackBox | +| ------------- | ---------------------------------------------- | +| `pid`, `name` | Identify culprit process in correlation engine | +| `cpu_percent` | Which process caused CPU spike | +| `rss_memory` | Which process is leaking memory | +| `timestamp` | Timeline correlation | + +### From `process_metadata` (Layer 3 โ€” on first seen) + +| Column | Use in BlackBox | +| ------------- | -------------------------------------- | +| `cmdline` | What exact command was running | +| `exe_path` | Where executable came from | +| `username` | Who launched the process | +| `create_time` | When process started relative to crash | + +### From `process_diagnostics` (Layer 4 โ€” on anomaly) + +| Column | Use in BlackBox | +| ------------------ | ---------------------------------------- | +| `open_files_count` | File handle leak detection | +| `thread_details` | Which threads were consuming CPU | +| `net_connections` | Active network connections at crash time | +| `trigger_reason` | Why Layer 4 was triggered | + +### Rolling window SQL + +```sql +-- Fetch last 2 minutes for feature extraction +SELECT cpu_usage_percent, memory_percent, + disk_read_mb_s, net_rate_mb_s, + total_processes, cpu_ctx_switches +FROM layer1_system +ORDER BY timestamp DESC +LIMIT 120; + +--Trim to 30-minute window +DELETE FROM telemetry +WHERE timestamp < (strftime('%s', 'now') - 1800); +``` + +--- + +## 5. Additional Info + +### Config values (from `utils/config.py`) + +```python +# BlackBox โ€” all tunable from one place (mentor requirement) +BLACKBOX_WINDOW_SEC = 1800 # rolling window duration +BLACKBOX_WARMUP_SEC = 60 # seconds before detection starts +BLACKBOX_Z_THRESHOLD = 2.8 # standard deviations +BLACKBOX_SLOPE_THRESHOLD = 0.003 # %/sec rising = suspicious +BLACKBOX_SUSTAINED_SEC = 30 # spike must last this long +BLACKBOX_SUSTAINED_RATIO = 0.6 # 60% readings must be above threshold +BLACKBOX_TREND_WINDOW = 600 # 10 min for slope calculation +``` + +### Key design decisions + +**Why SQLite over other storage?** +Lightweight, local, crash-resistant (data survives daemon crash), queryable via SQL, replay-friendly. + +**Why rolling 30-minute window?** +30 minutes of pre-crash context is sufficient for root-cause analysis. Longer window = more disk usage with diminishing returns. + +**Why Isolation Forest and not supervised model?** +No labeled anomaly dataset exists initially. IF is unsupervised โ€” trained on normal data only, no labels needed. + +**Why Z-score threshold 2.8 and not 3.0?** +3.0 is the classic threshold but at 3.0, a developer's laptop (higher baseline CPU) barely crossed the threshold for genuine spikes. 2.8 gives slightly better sensitivity while keeping false positives low with the sustained filter. + +--- + +## 6. Architecture Diagram + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Linux System (laptop) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ psutil + /proc + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Telemetry Collector โ€” collectors/layer1_system.py โ”‚ +โ”‚ every 1 second โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ–ผ โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ cognios_telemetry โ”‚ โ”‚ blackbox/blackbox.db โ”‚ +โ”‚ .db (permanent) โ”‚ โ”‚ (rolling 30-min window only) โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ layer1_sys โ”‚ โ”‚ blackbox_telemetry โ”‚ +โ”‚ top_cpu_telemetry โ”‚ โ”‚ blackbox_heartbeat โ”‚ +โ”‚ top_ram_telemetry โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”˜ +โ”‚ process_metadata โ”‚ โ”‚ +โ”‚ process_diagnostics โ”‚ โ–ผ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ OS Doctor โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ BlackBox Module โ”‚ +โ”‚ FocusOS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ +โ”‚ Research Engine โ”€โ”€โ”€โ”€โ”˜ โ”‚ recorder.py โ”‚ +โ”‚ โ”‚ heartbeat.py โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ feature_engineering.py โ”‚ + โ”‚ zscore_detector.py โ”‚ + โ”‚ rule_engine.py โ”‚ + โ”‚ anomaly_model.py โ”‚ + โ”‚ correlation.py โ”‚ + โ”‚ replay.py โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”˜ + โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Streamlit Dashboard โ”‚ + โ”‚ Timeline ยท Alerts ยท NL Query โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Why two separate databases? + +| `cognios_telemetry.db` | `blackbox/blackbox.db` | +| ------------------------------------------- | ------------------------------------- | +| Permanent โ€” never pruned | Rolling window โ€” last 30 min only | +| All Layer 1/2/3/4 data | Only BlackBox-relevant metrics | +| Read by OS Doctor, FocusOS, Research Engine | Read only by BlackBox | +| Grows indefinitely | Max ~1800 rows (1 per second ร— 1800s) | + +--- + +## 7. Missing + Updated Function Definitions + +### `recorder.py` โ€” additional functions + +```python +def get_blackbox_conn() -> sqlite3.Connection +``` + +**Purpose:** Opens and returns a connection to `blackbox/blackbox.db` with WAL mode for crash-safe writes. +**Input:** None +**Output:** `sqlite3.Connection` + +```python +def get_recent_rows(conn, n: int = 120) -> list[dict] +``` + +**Purpose:** Returns last `n` rows from `blackbox_telemetry`, ordered oldest-first. +**Input:** `conn`, `n` (default 120 = last 2 minutes) +**Output:** `list[dict]` โ€” each dict has column names as keys +**Used by:** `feature_engineering.py`, `correlation.py` + +```python +def get_window_rows(conn, start_time: float, end_time: float) -> list[dict] +``` + +**Purpose:** Returns all rows between two Unix timestamps for crash replay. +**Input:** `conn`, `start_time` (Unix float), `end_time` (Unix float) +**Output:** `list[dict]` ordered by timestamp ASC +**Used by:** `replay.py` + +```python +def row_count(conn) -> int +``` + +**Purpose:** Returns total number of rows currently in `blackbox_telemetry`. +**Input:** `conn` +**Output:** `int` + +--- + +### `heartbeat.py` โ€” updated with graceful shutdown flag + +> **Mentor feedback:** "Why should we have gap only 10 seconds?" +> +> Hardcoded `gap > 10s` was arbitrary โ€” HDD systems take 20+ seconds to reboot causing false positives. We use a **graceful shutdown flag** instead. Gap is only a fallback for SIGKILL/power cuts. + +```python +def mark_graceful_shutdown(conn) -> None +``` + +**Purpose:** Sets `graceful_shutdown = 1` flag. Must be called in daemon's SIGTERM/SIGINT handler. +**Input:** `conn` +**Output:** None +**Note:** If NOT called (crash/SIGKILL/power cut), flag stays 0 โ†’ crash detected on next startup. + +```python +def check_crash_on_startup(conn) -> tuple[bool, float] +``` + +**Purpose:** Called once on daemon startup to detect whether previous session ended in a crash. +**Input:** `conn` +**Output:** `(crash_detected: bool, gap_seconds: float)` + +**Detection logic (priority order):** + +1. `graceful_shutdown = 1` โ†’ normal shutdown, no crash +2. `graceful_shutdown = 0` AND `gap > BLACKBOX_CRASH_GAP_SEC` โ†’ crash detected +3. Always resets flag to 0 for the next session + +**In daemon signal handler:** + +```python +def handle_signal(sig, frame): + global running + running = False + mark_graceful_shutdown(bb_conn) # โ† required for correct crash detection + print("[CogniOS] Graceful shutdown.") +``` + +--- + +### `anomaly_model.py` โ€” additional functions + +```python +def anomaly_severity(score: float) -> int +``` + +**Purpose:** Converts raw Isolation Forest `decision_function` score to a 0โ€“100 severity integer for dashboard. +**Input:** `score` (float โ€” more negative = more anomalous) +**Output:** `int` between 0 and 100 +**Example:** score=-0.3 โ†’ severity=80 + +```python +def save_model(model: IsolationForest, path: str = "blackbox/if_model.pkl") -> None +``` + +**Purpose:** Saves trained model to disk using pickle. +**Input:** trained `IsolationForest`, file path +**Output:** None + +```python +def load_model(path: str = "blackbox/if_model.pkl") -> IsolationForest +``` + +**Purpose:** Loads a previously saved model from disk. +**Input:** file path +**Output:** `IsolationForest` + +--- + +## 8. How to Run + +### Prerequisites + +```bash +pip install psutil numpy scikit-learn +``` + +### First time setup + +```bash +cd CogniOS + +python3 -c " +import sys +sys.path.insert(0, '.') +from blackbox.recorder import get_blackbox_conn, create_blackbox_table +from blackbox.heartbeat import create_heartbeat_table + +conn = get_blackbox_conn() +create_blackbox_table(conn) +create_heartbeat_table(conn) +print('BlackBox DB initialised at blackbox/blackbox.db') +" +``` + +### Run full daemon + +```bash +python3 cognios_as_daemon.py +``` + +### Run BlackBox integration test only + +```bash +python3 -c " +import sys, time +sys.path.insert(0, '.') +from collectors.layer1_system import collect_layer1_metrics +from blackbox.recorder import get_blackbox_conn, create_blackbox_table, write_telemetry, row_count +from blackbox.heartbeat import create_heartbeat_table, update_heartbeat +from blackbox.zscore_detector import ZScoreDetector +from blackbox.rule_engine import check_rules + +conn = get_blackbox_conn() +create_blackbox_table(conn) +create_heartbeat_table(conn) +detector = ZScoreDetector() + +for i in range(40): + m = collect_layer1_metrics() + write_telemetry(conn, m) + update_heartbeat(conn) + detector.update(m.get('cpu_usage_percent', 0)) + check_rules(m) + time.sleep(0.1) + +print('Rows in blackbox.db:', row_count(conn)) +print('Integration test PASSED โœ…') +" +``` + +### Generate Isolation Forest training data + +```bash +sudo apt install stress-ng +python3 cognios_as_daemon.py & + +stress-ng --cpu 8 --timeout 60s # cpu_overload +sleep 30 +stress-ng --vm 4 --vm-bytes 80% --timeout 60s # memory_pressure +sleep 30 +stress-ng --hdd 4 --timeout 60s # disk_io_stress +sleep 30 +stress-ng --pthread 100 --timeout 60s # thread_explosion +``` + +--- + +## 9. Improvements & Future Work + +### High priority (low effort) + +**WAL mode for crash-safe writes** (2 lines of code): + +```python +conn.execute("PRAGMA journal_mode=WAL") +conn.execute("PRAGMA synchronous=NORMAL") +``` + +Default SQLite write mode can lose the last few seconds of telemetry if daemon crashes mid-write. WAL mode writes to a separate file first โ€” crash during write means no data loss. + +**Continuous anomaly severity score:** +Replace binary `-1/1` output with 0โ€“100 score using `decision_function()`. Dashboard can show gradual escalation (40โ†’60โ†’80โ†’100) instead of a sudden binary alert. + +### Medium priority + +**Per-metric Isolation Forest models:** +One combined model learns a confused boundary across all metrics. Separate models per metric with different `contamination` rates give better detection accuracy. + +**Exponential Moving Average (EMA) baseline:** +Replace simple rolling mean with EMA for faster adaptation to regime changes, reducing false positives when user starts a new heavy workload. + +### Low priority (research phase) + +**LSTM Autoencoder hybrid:** +LSTM captures temporal dependencies that Isolation Forest cannot โ€” sequential patterns like memory growing over 2 hours. A hybrid approach (LSTM reconstruction error fed into Isolation Forest) significantly improves detection. Requires TensorFlow and more training data. + +### Known limitations + +| Limitation | Impact | Workaround | +| -------------------------------------- | ------------------------------------------ | --------------------------------------- | +| Z-score slow drift blind spot | Memory leaks over 2+ hours may not trigger | Slope detector partially covers this | +| IF needs warmup data | No anomaly detection for first 60s | Rule Engine covers warmup period | +| `stress-ng` training data is synthetic | Real anomalies may differ | Collect real anomaly data and retrain | +| SIGKILL bypasses graceful flag | Gap fallback may miss very fast restarts | `BLACKBOX_CRASH_GAP_SEC = 30` as buffer | diff --git a/blackbox/anomaly_model.py b/blackbox/anomaly_model.py new file mode 100644 index 0000000..9cd1d4e --- /dev/null +++ b/blackbox/anomaly_model.py @@ -0,0 +1 @@ +#Isolation Forest wrapper (sklearn) \ No newline at end of file diff --git a/blackbox/correlation.py b/blackbox/correlation.py new file mode 100644 index 0000000..40d86a8 --- /dev/null +++ b/blackbox/correlation.py @@ -0,0 +1 @@ +#builds cause-effect event chains \ No newline at end of file diff --git a/blackbox/feature_engineering.py b/blackbox/feature_engineering.py new file mode 100644 index 0000000..707ad23 --- /dev/null +++ b/blackbox/feature_engineering.py @@ -0,0 +1,45 @@ +#converts raw DB rows to statistical feature vector +import numpy as np +from utils.config import BLACKBOX_WARMUP_SEC + + +def extract_feature_vector(rows: list[dict]) -> list[float] | None: + if len(rows) < BLACKBOX_WARMUP_SEC: + return None + #not enough data yet + + rows = list(reversed(rows)) + + cpu = [r["cpu_usage_percent"] for r in rows if r["cpu_usage_percent"] is not None] + mem = [r["memory_percent"] for r in rows if r["memory_percent"] is not None] + disk_r = [r["disk_read"] for r in rows if r["disk_read"] is not None] + disk_w = [r["disk_write"] for r in rows if r["disk_write"] is not None] + ctx = [r["cpu_ctx_switches"] for r in rows if r["cpu_ctx_switches"] is not None] + + mean_cpu = float(np.mean(cpu)) if cpu else 0.0 + max_cpu = float(np.max(cpu)) if cpu else 0.0 + cpu_variance = float(np.var(cpu)) if cpu else 0.0 + cpu_growth = float(cpu[-1] - cpu[0]) / len(cpu) if len(cpu) > 1 else 0.0 + + mean_ram = float(np.mean(mem)) if mem else 0.0 + mem_growth = float(mem[-1] - mem[0]) / len(mem) if len(mem) > 1 else 0.0 + + disk_combined = [r + w for r, w in zip(disk_r, disk_w)] + if disk_combined: + disk_threshold = float(np.mean(disk_combined)) + float(np.std(disk_combined)) + disk_spike_freq = sum(1 for d in disk_combined if d > disk_threshold) / len(disk_combined) + else: + disk_spike_freq = 0.0 + + ctx_rate = float(np.mean(np.diff(ctx))) if len(ctx) > 1 else 0.0 + + return [ + mean_cpu, + max_cpu, + cpu_growth, + cpu_variance, + mean_ram, + mem_growth, + disk_spike_freq, + ctx_rate, + ] \ No newline at end of file diff --git a/blackbox/heartbeat.py b/blackbox/heartbeat.py new file mode 100644 index 0000000..6f18557 --- /dev/null +++ b/blackbox/heartbeat.py @@ -0,0 +1,72 @@ +#Crash/freeze detection via heartbeat timestamps + +import sqlite3 +import time +from config import BLACKBOX_CRASH_GAP_SEC +from blackbox.recorder import _get_lock, _noop_ctx + + +def create_heartbeat_table(conn: sqlite3.Connection) -> None: + # single row table to store the last heartbeat timestamp and graceful shutdown flag + lock = _get_lock(conn) + with lock if lock else _noop_ctx(): + conn.execute(""" + CREATE TABLE IF NOT EXISTS blackbox_heartbeat ( + id INTEGER PRIMARY KEY CHECK (id = 1), + last_beat REAL NOT NULL, + graceful_shutdown INTEGER NOT NULL DEFAULT 0 + ) + """) + # Insert an initial heartbeat record if it doesn't exist + conn.execute(""" + INSERT OR IGNORE INTO blackbox_heartbeat (id, last_beat, graceful_shutdown) + VALUES (1, ?, 0) + """, (time.time(),)) + conn.commit() + + +def update_heartbeat(conn: sqlite3.Connection) -> None: + lock = _get_lock(conn) + with lock if lock else _noop_ctx(): + conn.execute(""" + UPDATE blackbox_heartbeat + SET last_beat = ? + WHERE id = 1 + """, (time.time(),)) + conn.commit() + + +def mark_graceful_shutdown(conn: sqlite3.Connection) -> None: + lock = _get_lock(conn) + with lock if lock else _noop_ctx(): + conn.execute( + "UPDATE blackbox_heartbeat SET graceful_shutdown = 1 WHERE id = 1" + ) + conn.commit() + + +def check_crash_on_startup(conn: sqlite3.Connection) -> tuple[bool, float]: + # check if the last run ended gracefully or if there was a crash + # based on the heartbeat timestamp and graceful shutdown flag. + lock = _get_lock(conn) + with lock if lock else _noop_ctx(): + row = conn.execute( + "SELECT last_beat, graceful_shutdown FROM blackbox_heartbeat WHERE id = 1" + ).fetchone() + + if row is None: + return False, 0.0 + + last_beat, graceful = row + gap = round(time.time() - last_beat, 1) + + # Reset flag atomically in the same transaction + conn.execute( + "UPDATE blackbox_heartbeat SET graceful_shutdown = 0 WHERE id = 1" + ) + conn.commit() + + if graceful: + return False, gap + + return gap > BLACKBOX_CRASH_GAP_SEC, gap \ No newline at end of file diff --git a/blackbox/recorder.py b/blackbox/recorder.py index 5a772aa..d606ea6 100644 --- a/blackbox/recorder.py +++ b/blackbox/recorder.py @@ -1 +1,158 @@ """Rolling 30-minute telemetry recorder.""" +import time +import sqlite3 +import threading +from config import BLACKBOX_DB_PATH, BLACKBOX_WINDOW_SEC +from pathlib import Path + +_conn_locks: dict = {} + + +def _get_lock(conn: sqlite3.Connection): + """Return the Lock registered for this connection, or None.""" + return _conn_locks.get(id(conn)) + + +def get_blackbox_conn() -> sqlite3.Connection: + Path(BLACKBOX_DB_PATH).parent.mkdir(parents=True, exist_ok=True) + conn = sqlite3.connect(BLACKBOX_DB_PATH, check_same_thread=False) + + # WAL mode: crash-safe writes โ€” data survive karta hai daemon crash pe bhi + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("PRAGMA synchronous=NORMAL") + + # Register a dedicated Lock for this connection (Bug 3 fix) + _conn_locks[id(conn)] = threading.Lock() + return conn + + +def create_blackbox_table(conn: sqlite3.Connection) -> None: + lock = _get_lock(conn) + with lock if lock else _noop_ctx(): + conn.execute(""" + CREATE TABLE IF NOT EXISTS blackbox_telemetry ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp REAL NOT NULL, + + -- CPU Metrics + cpu_usage_percent REAL, + cpu_ctx_switches REAL, + cpu_busy_time REAL, + cpu_iowait_time REAL, + + -- Memory Metrics + memory_percent REAL, + memory_used INTEGER, + swap_percent REAL, + + -- Disk Metrics + disk_read REAL, + disk_write REAL, + + -- Network Metrics + net_rate_mb_s REAL, + net_bytes_sent INTEGER, + net_bytes_received INTEGER, + + -- Process Metrics + total_processes INTEGER, + running_processes INTEGER, + zombie_processes INTEGER, + + -- System load + load_avg1 REAL, + load_avg5 REAL, + + -- Hardware + avg_temp REAL + ) + """) + conn.commit() + +BLACKBOX_METRICS_KEYS: list[str] = [ + 'timestamp', + 'cpu_usage_percent', 'cpu_ctx_switches', 'cpu_busy_time', 'cpu_iowait_time', + 'memory_percent', 'memory_used', 'swap_percent', + 'disk_read', 'disk_write', + 'net_rate_mb_s', 'net_bytes_sent', 'net_bytes_received', + 'total_processes', 'running_processes', 'zombie_processes', + 'load_avg1', 'load_avg5', + 'avg_temp', +] + +_write_count = 0 + + +class _noop_ctx: + """Fallback context manager for connections created without a lock.""" + def __enter__(self): return self + def __exit__(self, *_): pass + + +def write_telemetry(conn: sqlite3.Connection, metrics: dict) -> None: + global _write_count + + # Build data in schema-column order (Bug 4) + data: dict = {k: metrics.get(k) for k in BLACKBOX_METRICS_KEYS} + # Always store a float timestamp for time-range queries + if data.get('timestamp') is None or not isinstance(data['timestamp'], (int, float)): + data['timestamp'] = time.time() + + cols = ", ".join(data.keys()) + placeholders = ", ".join("?" for _ in data) + + # Bug 3: acquire lock before writing (shared connection across threads) + lock = _get_lock(conn) + with lock if lock else _noop_ctx(): + conn.execute( + f"INSERT INTO blackbox_telemetry ({cols}) VALUES ({placeholders})", + list(data.values()) + ) + conn.commit() + + _write_count += 1 + if _write_count % 60 == 0: + prune_old_records(conn) + + +def prune_old_records(conn: sqlite3.Connection) -> None: + cutoff = time.time() - BLACKBOX_WINDOW_SEC + lock = _get_lock(conn) + with lock if lock else _noop_ctx(): + conn.execute( + "DELETE FROM blackbox_telemetry WHERE timestamp < ?", + (cutoff,) + ) + conn.commit() + + +def get_recent_rows(conn: sqlite3.Connection, n: int = 120) -> list[dict]: + + """ useful for feture engineering or correlation analysis of telemetry data """ + cursor = conn.execute(""" + SELECT * FROM blackbox_telemetry + ORDER BY timestamp DESC + LIMIT ? + """, (n,)) + cols = [d[0] for d in cursor.description] + rows = [dict(zip(cols, row)) for row in cursor.fetchall()] + return list(reversed(rows)) # Return in chronological order + + +def get_window_rows(conn: sqlite3.Connection, + start_time: float, + end_time: float) -> list[dict]: + """ fetch specific window of telemetry data for analysis """ + cursor = conn.execute(""" + SELECT * FROM blackbox_telemetry + WHERE timestamp BETWEEN ? AND ? + ORDER BY timestamp ASC + """, (start_time, end_time)) + cols = [d[0] for d in cursor.description] + return [dict(zip(cols, row)) for row in cursor.fetchall()] + + +def row_count(conn: sqlite3.Connection) -> int: + return conn.execute( + "SELECT COUNT(*) FROM blackbox_telemetry" + ).fetchone()[0] \ No newline at end of file diff --git a/blackbox/rule_engine.py b/blackbox/rule_engine.py new file mode 100644 index 0000000..080ae85 --- /dev/null +++ b/blackbox/rule_engine.py @@ -0,0 +1 @@ +#deterministic threshold-based checks (backup) \ No newline at end of file diff --git a/blackbox/zscore_detector.py b/blackbox/zscore_detector.py new file mode 100644 index 0000000..72f3a16 --- /dev/null +++ b/blackbox/zscore_detector.py @@ -0,0 +1 @@ +#z-score and slope(trend) \ No newline at end of file diff --git a/cognios_as_daemon.py b/cognios_as_daemon.py index 03d43a7..22c0ad0 100644 --- a/cognios_as_daemon.py +++ b/cognios_as_daemon.py @@ -1 +1,107 @@ -"""Daemon entry point for CogniOS.""" +"""Daemon entry point for CogniOS โ€” writes to both permanent DB and BlackBox rolling window.""" +import time +import json +import logging +from db import create_connection, write_layer1 +from collectors.layer1_system import collect_layer1_metrics +from config import DB_PATH + +from blackbox.recorder import get_blackbox_conn, create_blackbox_table, write_telemetry +from blackbox.heartbeat import ( + create_heartbeat_table, + update_heartbeat, + check_crash_on_startup, + mark_graceful_shutdown, +) + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + + +def run_daemon(): + logging.info(f"Starting CogniOS Daemon. Saving metrics to '{DB_PATH}' every 1 second.") + + # Permanent DB connection + conn = create_connection(DB_PATH) + + # BlackBox rolling-window DB setup + bb_conn = get_blackbox_conn() + create_blackbox_table(bb_conn) + create_heartbeat_table(bb_conn) + + crashed, gap = check_crash_on_startup(bb_conn) + if crashed: + logging.warning(f"Previous session may have crashed! Gap = {gap}s") + else: + logging.info(f"Previous session ended cleanly (gap = {gap:.1f}s)") + + try: + while True: + try: + metrics = collect_layer1_metrics() + + # --- Write to permanent DB (cognios_telemetry.db) --- + write_layer1( + conn, + metrics['timestamp'], + metrics['cpu_usage_percent'], + metrics['cpu_current_freq'], + metrics['cpu_user_time'], + metrics['cpu_system_time'], + metrics['cpu_idle_time'], + metrics['cpu_iowait_time'], + metrics['cpu_busy_time'], + metrics['cpu_ctx_switches'], + metrics['memory_percent'], + metrics['memory_used'], + metrics['memory_available'], + metrics['memory_cached'], + metrics['memory_buffers'], + metrics['swap_percent'], + metrics['swap_sin'], + metrics['swap_sout'], + metrics['disk_usage_percent'], + metrics['disk_read'], + metrics['disk_write'], + metrics['disk_read_time'], + metrics['disk_write_time'], + metrics['load_avg1'], + metrics['load_avg5'], + metrics['load_avg15'], + metrics['total_processes'], + metrics['running_processes'], + metrics['sleeping_processes'], + metrics['zombie_processes'], + metrics['avg_temp'], + metrics['max_temp'], + metrics['battery_percent'], + metrics['net_rate_mb_s'], + metrics['net_bytes_sent'], + metrics['net_bytes_received'], + metrics['net_packets_sent'], + metrics['net_packets_received'], + metrics['net_errs'], + metrics['net_drops'], + json.dumps(metrics['process_data']) + ) + + # --- Write to BlackBox rolling-window DB (blackbox/blackbox.db) --- + write_telemetry(bb_conn, metrics) + update_heartbeat(bb_conn) + + logging.info(f"Successfully saved metrics for timestamp: {metrics['timestamp']}") + + except Exception as e: + logging.error(f"Error collecting or writing metrics: {e}") + + time.sleep(1) + + except KeyboardInterrupt: + logging.info("Stopping CogniOS Daemon. Shutting down gracefully...") + mark_graceful_shutdown(bb_conn) + finally: + conn.close() + bb_conn.close() + + +if __name__ == "__main__": + run_daemon() \ No newline at end of file diff --git a/collectors/layer1_system.py b/collectors/layer1_system.py index 291e147..8086307 100644 --- a/collectors/layer1_system.py +++ b/collectors/layer1_system.py @@ -1 +1,276 @@ -"""Layer 1 system telemetry collection.""" +import psutil +import time +from datetime import datetime,timezone +import sys +import os +#adding path to locate the utils +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import utils +from utils.helpers import rate_mb_s + +# a dictionary to store previous values +_last = { + "time": None, + "disk_read_bytes": None, + "disk_write_bytes": None, + "net_bytes_sent": None, + "net_bytes_recv": None, +} +# collects metriics +def collect_layer1_metrics(): + now = time.time() + timestamp = datetime.now(timezone.utc).isoformat() + #process + try: + procs = {} + for p in psutil.process_iter(["name", "memory_percent"]): + try: + p.cpu_percent() + procs[p.pid] = p + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + time.sleep(0.1) + process_data = [] + for pid, p in procs.items(): + try: + cpu = round(p.cpu_percent(), 2) + mem = round(p.info.get("memory_percent") or 0.0, 2) + if cpu > 0.5 or mem > 0.5: + process_data.append((p.info.get("name"), cpu, mem)) + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + except Exception: + process_data = [] + # CPU Metrics + cpu_usage_percent = psutil.cpu_percent(interval=None) + cpu_times = psutil.cpu_times() + cpu_user_time = cpu_times.user + cpu_system_time = cpu_times.system + cpu_idle_time = cpu_times.idle + cpu_iowait_time = getattr(cpu_times, 'iowait', None) # iowait for Linux only + cpu_busy_time=cpu_user_time + cpu_system_time + + # freq , ctx_switches are not available on all systems + try: + freq=psutil.cpu_freq() + cpu_current_freq=freq.current if freq else None + except Exception: + cpu_current_freq=None + + try: + cpu_ctx_switches=psutil.cpu_stats().ctx_switches + except Exception: + cpu_ctx_switches=None + + # Memory Metrics + + vmem=psutil.virtual_memory() + memory_percent=vmem.percent + memory_used=vmem.used + memory_available=vmem.available + memory_cached=getattr(vmem, 'cached', None) #cached and buffers not available in the mac and windows + memory_buffers=getattr(vmem, 'buffers', None) # None- like value nahi mili aur 0- ek valid value hai + + swap=psutil.swap_memory() + swap_percent=swap.percent + swap_sin=swap.sin + swap_sout=swap.sout + + # Disk Metrics + + # same reason, in windows "/" is not valid + try: + disk_usage=psutil.disk_usage('/').percent + except Exception: + disk_usage=None + + try: + disk_io = psutil.disk_io_counters() + except Exception: + disk_io=None + + disk_read=None + disk_write=None + disk_read_time=None + disk_write_time=None + + if disk_io is not None: + elapsed_time = (now - _last["time"]) if _last["time"] else 0 + disk_read = rate_mb_s(disk_io.read_bytes, _last["disk_read_bytes"], elapsed_time) + disk_write = rate_mb_s(disk_io.write_bytes, _last["disk_write_bytes"], elapsed_time) + disk_read_time = getattr(disk_io, 'read_time', None) + disk_write_time = getattr(disk_io, 'write_time', None) + + _last["disk_read_bytes"] = disk_io.read_bytes + _last["disk_write_bytes"] = disk_io.write_bytes + + + # Network Metrics + net_io=psutil.net_io_counters() + net_bytes_sent=net_io.bytes_sent if net_io else 0 + net_bytes_received=net_io.bytes_recv if net_io else 0 + net_packets_sent=net_io.packets_sent if net_io else 0 + net_packets_received=net_io.packets_recv if net_io else 0 + net_errs=(net_io.errin + net_io.errout) if net_io else 0 + net_drops=(net_io.dropin + net_io.dropout) if net_io else 0 + + net_rate_mb_s=None + if _last["time"]: + elapsed_time = now - _last["time"] + sent_rate = rate_mb_s(net_bytes_sent, _last["net_bytes_sent"], elapsed_time) + recv_rate = rate_mb_s(net_bytes_received, _last["net_bytes_recv"], elapsed_time) + if sent_rate is not None and recv_rate is not None: + net_rate_mb_s = sent_rate + recv_rate + + _last["net_bytes_sent"] = net_bytes_sent + _last["net_bytes_recv"] = net_bytes_received + _last["time"] = now + + + #b Load Average Metrics + try: + load_avg1, load_avg5, load_avg15=psutil.getloadavg() + + # except AttributeError: # catches specific error when getloadavg is not support + except Exception: + load_avg1=load_avg5=load_avg15=None + + #initialising the process_type_count variables + total_processes, running_processes, sleeping_processes, zombie_processes = 0, 0, 0, 0 + for p in psutil.process_iter(['status']): + total_processes+= 1 + try: + status=p.info['status'] + if status==psutil.STATUS_RUNNING: + running_processes+=1 + elif status==psutil.STATUS_SLEEPING: + sleeping_processes+=1 + elif status==psutil.STATUS_ZOMBIE: + zombie_processes+=1 + except (psutil.NoSuchProcess,psutil.AccessDenied): + pass + + + # Temperature and Battery Metrics (if available) + temp_avg, temp_max = None, None + try: + temps=psutil.sensors_temperatures() + all_temps=[t.current for sensors in temps.values() for t in sensors] + if all_temps: + temp_avg = sum(all_temps) / len(all_temps) + temp_max = max(all_temps) + except Exception: + pass + battery_percent=None + try: + battery=psutil.sensors_battery() + if battery: + battery_percent=battery.percent + except Exception: + pass + + return { + "timestamp": timestamp, + "cpu_usage_percent": cpu_usage_percent, + "cpu_current_freq": cpu_current_freq, + "cpu_user_time": cpu_user_time, + "cpu_system_time": cpu_system_time, + "cpu_idle_time": cpu_idle_time, + "cpu_iowait_time": cpu_iowait_time, + "cpu_busy_time": cpu_busy_time, + "cpu_ctx_switches": cpu_ctx_switches, + + "memory_percent": memory_percent, + "memory_used":memory_used, + "memory_available":memory_available, + "memory_cached":memory_cached, + "memory_buffers":memory_buffers, + "swap_percent":swap_percent, + "swap_sin":swap_sin, + "swap_sout":swap_sout, + + "disk_usage_percent":disk_usage, + "disk_read":disk_read, + "disk_write":disk_write, + "disk_read_time":disk_read_time, + "disk_write_time":disk_write_time, + + "net_rate_mb_s":net_rate_mb_s, + "net_bytes_sent":net_bytes_sent, + "net_bytes_received":net_bytes_received, + "net_packets_sent":net_packets_sent, + "net_packets_received":net_packets_received, + "net_errs":net_errs, + "net_drops":net_drops, + + "load_avg1":load_avg1, + "load_avg5":load_avg5, + "load_avg15":load_avg15, + "total_processes":total_processes, + "running_processes":running_processes, + "sleeping_processes":sleeping_processes, + "zombie_processes":zombie_processes, + "avg_temp":temp_avg, + "max_temp":temp_max, + "battery_percent":battery_percent, + "process_data":process_data + } + +if __name__ == "__main__": + import json + from db import create_connection, write_layer1 + from config import DB_PATH + + metrics = collect_layer1_metrics() + print("Gathered metrics:") + print(json.dumps(metrics, indent=4)) + + print("\nSaving metrics to database...") + try: + conn = create_connection(DB_PATH) + write_layer1( + conn, + metrics['timestamp'], + metrics['cpu_usage_percent'], + metrics['cpu_current_freq'], + metrics['cpu_user_time'], + metrics['cpu_system_time'], + metrics['cpu_idle_time'], + metrics['cpu_iowait_time'], + metrics['cpu_busy_time'], + metrics['cpu_ctx_switches'], + metrics['memory_percent'], + metrics['memory_used'], + metrics['memory_available'], + metrics['memory_cached'], + metrics['memory_buffers'], + metrics['swap_percent'], + metrics['swap_sin'], + metrics['swap_sout'], + metrics['disk_usage_percent'], + metrics['disk_read'], + metrics['disk_write'], + metrics['disk_read_time'], + metrics['disk_write_time'], + metrics['load_avg1'], + metrics['load_avg5'], + metrics['load_avg15'], + metrics['total_processes'], + metrics['running_processes'], + metrics['sleeping_processes'], + metrics['zombie_processes'], + metrics['avg_temp'], + metrics['max_temp'], + metrics['battery_percent'], + metrics['net_rate_mb_s'], + metrics['net_bytes_sent'], + metrics['net_bytes_received'], + metrics['net_packets_sent'], + metrics['net_packets_received'], + metrics['net_errs'], + metrics['net_drops'], + json.dumps(metrics['process_data']) + ) + print(f"Successfully saved Layer 1 metrics to database table 'layer1_sys' in {DB_PATH}!") + except Exception as e: + print(f"Error saving to database: {e}") \ No newline at end of file diff --git a/collectors/layer2_process.py b/collectors/layer2_process.py index 87632ca..58ea9ee 100644 --- a/collectors/layer2_process.py +++ b/collectors/layer2_process.py @@ -1 +1,159 @@ """Layer 2 process telemetry collection.""" +import time +import psutil + + +def collect_process_telemetry(prev_states=None): + if prev_states is None: + prev_states = {} + + # --- Phase 0: CPU prime pass (t=0) --- + # First cpu_percent call per Process object always returns 0.0 or a stale + # cumulative rate. Discarding it gives the 5 sampling passes a clean 1-second + # baseline each. + for proc in psutil.process_iter(['pid']): + try: + proc.cpu_percent(interval=None) + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + continue + + # --- Phase 1: 5 ร— 1-second CPU + RAM sampling --- + cpu_samples = {} # pid -> [cpu_percent, ...] + ram_samples = {} # pid -> [rss_mb, ...] + for _ in range(5): + time.sleep(1.0) + for proc in psutil.process_iter(['pid']): + try: + pid = proc.pid + cpu_samples.setdefault(pid, []).append(proc.cpu_percent(interval=None)) + try: + ram_samples.setdefault(pid, []).append( + round(proc.memory_info().rss / (1024 * 1024), 3) + ) + except (psutil.AccessDenied, AttributeError): + pass + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + continue + + # --- Phase 2: Full metadata pass + aggregation --- + current_time = time.time() + current_states = {} + processed_snapshots = [] + + for proc in psutil.process_iter(['pid', 'name', 'num_threads', 'status', 'create_time']): + try: + info = proc.info + pid = info['pid'] + + if pid not in cpu_samples: + continue + + mem_info = proc.memory_info() + mem_percent = proc.memory_percent() + + try: + io = proc.io_counters() + read_bytes, write_bytes = io.read_bytes, io.write_bytes + except (psutil.AccessDenied, AttributeError): + read_bytes, write_bytes = 0, 0 + + try: + ctx = proc.num_ctx_switches() + ctx_vol = ctx.voluntary + ctx_invol = ctx.involuntary + except (psutil.AccessDenied, AttributeError): + ctx_vol, ctx_invol = 0, 0 + + try: + open_fds = proc.num_fds() + except (psutil.AccessDenied, AttributeError): + open_fds = 0 + + try: + net_conn_count = len(proc.net_connections()) + except (psutil.AccessDenied, AttributeError): + net_conn_count = 0 + + calc_read_rate = 0.0 + calc_write_rate = 0.0 + calc_ctx_vol_sec = 0.0 + calc_ctx_invol_sec = 0.0 + if pid in prev_states: + prev = prev_states[pid] + dt = current_time - prev['timestamp'] + if dt > 0: + calc_read_rate = max(0.0, (read_bytes - prev['read_bytes']) / dt) + calc_write_rate = max(0.0, (write_bytes - prev['write_bytes']) / dt) + calc_ctx_vol_sec = max(0.0, (ctx_vol - prev['ctx_vol']) / dt) + calc_ctx_invol_sec = max(0.0, (ctx_invol - prev['ctx_invol']) / dt) + + current_states[pid] = { + 'read_bytes': read_bytes, + 'write_bytes': write_bytes, + 'ctx_vol': ctx_vol, + 'ctx_invol': ctx_invol, + 'timestamp': current_time, + } + + cpu_s = cpu_samples[pid] + cpu_avg = round(sum(cpu_s) / len(cpu_s), 2) + cpu_peak = round(max(cpu_s), 2) + cpu_score = round(0.6 * cpu_peak + 0.4 * cpu_avg, 2) + + ram_s = ram_samples.get(pid, []) + if ram_s: + ram_avg = round(sum(ram_s) / len(ram_s), 3) + ram_peak = round(max(ram_s), 3) + ram_score = round(0.6 * ram_peak + 0.4 * ram_avg, 3) + rss_mb_now = ram_s[-1] + else: + rss_mb_now = round(mem_info.rss / (1024 * 1024), 3) + ram_avg = ram_peak = ram_score = rss_mb_now + + processed_snapshots.append({ + 'pid': pid, + 'name': info['name'] or 'Unknown', + 'cpu_percent': round(cpu_s[-1], 2), + 'cpu_avg': cpu_avg, + 'cpu_peak': cpu_peak, + 'cpu_score': cpu_score, + 'memory_percent': round(mem_percent, 2), + 'rss_mb': rss_mb_now, + 'ram_avg': ram_avg, + 'ram_peak': ram_peak, + 'ram_score': ram_score, + 'vms_gb': round(mem_info.vms / (1024 * 1024 * 1024), 4), + 'thread_count': info['num_threads'] or 1, + 'read_bytes_sec': round(calc_read_rate, 2), + 'write_bytes_sec': round(calc_write_rate, 2), + 'status': info['status'] or 'unknown', + 'age_sec': round(current_time - (info['create_time'] or current_time), 1), + 'open_fds': open_fds, + 'ctx_vol_sec': round(calc_ctx_vol_sec, 2), + 'ctx_invol_sec': round(calc_ctx_invol_sec, 2), + 'net_conn_count': net_conn_count, + }) + + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + continue + + top_cpu = sorted(processed_snapshots, key=lambda x: x['cpu_score'], reverse=True)[:5] + top_mem = sorted(processed_snapshots, key=lambda x: x['ram_score'], reverse=True)[:5] + + return top_cpu, top_mem, current_states + + +if __name__ == '__main__': + from db import init_db, insert_process_snapshot + + init_db() + baselines = {} + print("CogniOS Telemetry Daemon started. Press Ctrl+C to stop.") + + try: + while True: + top_cpu, top_mem, baselines = collect_process_telemetry(baselines) + insert_process_snapshot(top_cpu, top_mem) + print(f"Snapshot committed at t={time.time():.0f} | top_cpu={top_cpu[0]['name']} score={top_cpu[0]['cpu_score']}") + except KeyboardInterrupt: + print("\nDaemon safely terminated.") diff --git a/collectors/layer3_metadata.py b/collectors/layer3_metadata.py index 28a5786..34c5fad 100644 --- a/collectors/layer3_metadata.py +++ b/collectors/layer3_metadata.py @@ -1 +1,107 @@ """Layer 3 metadata telemetry collection.""" +import sqlite3 +from datetime import datetime +import psutil +from config import DB_PATH +db_path = DB_PATH + +class MetadataCollector: + + def __init__(self, db_path): + + self.conn = sqlite3.connect(db_path, check_same_thread = False) + self.cursor = self.conn.cursor() + + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS process_metadata( + pid INTEGER, + ppid INTEGER, + process_name TEXT, + create_time TEXT, + exe_path TEXT, + cmdline TEXT, + username TEXT, + nice_priority INTEGER, + first_seen TEXT, + PRIMARY KEY(pid, create_time) + ) + """) + + self.conn.commit() + def metadata_exists(self, pid, create_time): + self.cursor.execute("SELECT 1 FROM process_metadata WHERE pid=? AND create_time=?", (pid, create_time)) + return self.cursor.fetchone() is not None + + def save_metadata(self, pid): + + try: + p = psutil.Process(pid) + + create_time = datetime.fromtimestamp( + p.create_time() + ).isoformat() + + if self.metadata_exists(pid, create_time): + return + + record = ( + + p.pid, #pid + p.ppid(), + p.name(), + + create_time, + p.exe(), #to track the executable path + " ".join(p.cmdline()), #to track the command line arguments + p.username(), + p.nice(), + datetime.now().isoformat() #to track when we first saw this process + ) + + self.cursor.execute(""" + INSERT INTO process_metadata + VALUES(?,?,?,?,?,?,?,?,?) + """, record) + print( + f"[LAYER3] Metadata stored for PID {pid}" +) + + except ( + psutil.NoSuchProcess, + psutil.AccessDenied, + psutil.ZombieProcess + ) as e: + print(f"[LAYER3 ERROR] {e}") + + def commit(self): + self.conn.commit() + + def cleanup(self, days=7): #delete record older than 7 days + + self.cursor.execute(""" + DELETE FROM process_metadata + WHERE julianday('now')-julianday(first_seen)> ? + """, + (days,) + ) + + self.conn.commit() + + def close(self): # + self.conn.close() + + + +def process_top_processes(collector, top_cpu, top_mem): + + + top_pids = set() + for proc in top_cpu: #this will ensure that we only save metadata for unique PIDs, even if they appear in both top CPU and top memory lists + top_pids.add(proc["pid"]) + for proc in top_mem: + top_pids.add(proc["pid"]) + for pid in top_pids: + collector.save_metadata(pid) + + collector.commit() + collector.cleanup() diff --git a/collectors/layer4_diagnostics.py b/collectors/layer4_diagnostics.py index 6c3c5f7..3b1d465 100644 --- a/collectors/layer4_diagnostics.py +++ b/collectors/layer4_diagnostics.py @@ -1 +1,209 @@ """Layer 4 diagnostics telemetry collection.""" +from datetime import datetime +from pathlib import Path +import socket +import sqlite3 +import threading +import psutil + +from config import DB_PATH +db_path = DB_PATH + +_layer4_running = False +_layer4_lock = threading.Lock() + +class DiagnosticsCollector: + def __init__(self, db_path): + db_path = Path(db_path) + db_path.parent.mkdir(parents=True, exist_ok=True) + + self.conn = sqlite3.connect(db_path, check_same_thread=False) + self.cursor = self.conn.cursor() + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS process_diagnostics ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pid INTEGER, + process_name TEXT, + timestamp TEXT, + open_files_count INTEGER, + open_files TEXT, + thread_count INTEGER, + thread_details TEXT, + voluntary_ctx_switches INTEGER, + involuntary_ctx_switches INTEGER, + cpu_affinity TEXT, + io_priority INTEGER, + nice_value INTEGER, + trigger_reason TEXT, + net_connections_count INTEGER, + net_connections TEXT, + io_write_bytes INTEGER, + io_read_bytes INTEGER + ) + """) + self._ensure_columns( + { + "io_write_bytes": "INTEGER", + "io_read_bytes": "INTEGER", + } + ) + self.conn.commit() + + def _ensure_columns(self, columns): + existing_columns = { + row[1] + for row in self.cursor.execute("PRAGMA table_info(process_diagnostics)") + } + + for column_name, column_type in columns.items(): + if column_name not in existing_columns: + self.cursor.execute( + f"ALTER TABLE process_diagnostics ADD COLUMN {column_name} {column_type}" + ) + + def collect_and_save(self, pid, trigger_reason="anomaly"): + try: + p = psutil.Process(pid) + timestamp = datetime.now().isoformat() + + #open files + + try: + files = p.open_files() + open_files_count = len(files) + open_files = ",".join(f.path for f in files[:20]) + except (psutil.AccessDenied, Exception): + open_files_count = None + open_files = None + + #threads + try: + threads = p.threads() + thread_count = len(threads) + thread_details = ",".join( + f"{t.id}(u={round(t.user_time,2)},s={round(t.system_time,2)})" + for t in threads + ) + except (psutil.AccessDenied, Exception): + thread_count = None + thread_details = None + + #ctx switches + + try: + ctx = p.num_ctx_switches() + vol_ctx = ctx.voluntary + invol_ctx = ctx.involuntary + except (psutil.AccessDenied, Exception): + vol_ctx, invol_ctx = None, None + + #Cpu affinity + try: + affinity = ",".join(str(c) for c in p.cpu_affinity()) + except (psutil.AccessDenied, AttributeError, Exception): + affinity = None + + #I/O Priority + try: + ioprio = p.ionice().value + except (psutil.AccessDenied, AttributeError, Exception): + ioprio = None + + # Nice value + try: + nice = p.nice() + except (psutil.AccessDenied, Exception): + nice = None + + # Network Connections + try: + conns = p.net_connections(kind="all") + net_connections_count = len(conns) + + conn_parts = [] + for c in conns[:30]: + if c.type == socket.SOCK_STREAM: + proto = "tcp" + elif c.type == socket.SOCK_DGRAM: + proto = "udp" + else: + proto = "other" + + local = f"{c.laddr.ip}:{c.laddr.port}" if c.laddr else "-" + remote = f"{c.raddr.ip}:{c.raddr.port}" if c.raddr else "-" + status = c.status if c.status else "-" + + conn_parts.append(f"{proto}|{local}|{remote}|{status}") + + net_connections = ",".join(conn_parts) + + except (psutil.AccessDenied, AttributeError,Exception): + net_connections_count = None + net_connections = None + + # Per-process disk I/O bytes. + try: + io = p.io_counters() + io_write_bytes = io.write_bytes + io_read_bytes = io.read_bytes + except (psutil.AccessDenied, AttributeError, Exception): + io_write_bytes = None + io_read_bytes = None + + + self.cursor.execute(""" + INSERT INTO process_diagnostics ( + pid, process_name, timestamp, + open_files_count, open_files, + thread_count, thread_details, + voluntary_ctx_switches, involuntary_ctx_switches, + cpu_affinity, io_priority, nice_value, trigger_reason, + net_connections_count, net_connections, + io_write_bytes, io_read_bytes + + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + """, ( + pid, p.name(), timestamp, + open_files_count, open_files, + thread_count, thread_details, + vol_ctx, invol_ctx, + affinity, ioprio, nice, trigger_reason, + net_connections_count, net_connections, + io_write_bytes, io_read_bytes + )) + self.conn.commit() + + except (psutil.NoSuchProcess, psutil.ZombieProcess) as e: + print(f"[LAYER4 ERROR] {e}") + + def close(self): + self.conn.close() + + + +def _run_diagnostics(pid_list, trigger_reason): + global _layer4_running + collector = DiagnosticsCollector(db_path) + try: + for pid in pid_list: + collector.collect_and_save(pid, trigger_reason) + finally: + collector.close() + with _layer4_lock: + _layer4_running = False + + +def trigger_layer4(pid_list, trigger_reason="anomaly"): + global _layer4_running + with _layer4_lock: + if _layer4_running: + print("LAYER4 Already running, skipping trigger.") + return + _layer4_running = True + + t = threading.Thread( + target=_run_diagnostics, + args=(pid_list, trigger_reason), + daemon=True + ) + t.start() diff --git a/config.py b/config.py index be02e34..baa7aaa 100644 --- a/config.py +++ b/config.py @@ -1 +1,11 @@ -"""CogniOS configuration.""" +DB_PATH = "cognios_telemetry.db" +BLACKBOX_DB_PATH = "blackbox/blackbox.db" +BLACKBOX_WINDOW_SEC = 1800 # 30 minutes +BLACKBOX_WARMUP_SEC = 60 # wait before detection starts +BLACKBOX_CRASH_GAP_SEC = 30 # SIGKILL-only fallback, not the primary check + +BLACKBOX_Z_THRESHOLD = 2.8 # std deviations for spike +BLACKBOX_SLOPE_THRESHOLD = 0.003 # %/sec rise = suspicious +BLACKBOX_SUSTAINED_SEC = 30 # spike must last this long +BLACKBOX_SUSTAINED_RATIO = 0.6 # 60% readings above threshold +BLACKBOX_TREND_WINDOW = 600 # 10 min for slope calculation \ No newline at end of file diff --git a/db.py b/db.py index e5c2395..8d1ef2e 100644 --- a/db.py +++ b/db.py @@ -1 +1,110 @@ """Shared database schema and read/write interface.""" +import json +import sqlite3 +import time +from config import DB_PATH + +# layer 2 db code starts here + +def init_db(): + """Initializes the unified process_snapshot table for Layer 2 telemetry.""" + with sqlite3.connect(DB_PATH) as conn: + conn.execute(""" + CREATE TABLE IF NOT EXISTS process_snapshot ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp REAL NOT NULL, + top_cpu_processes TEXT NOT NULL, + top_ram_processes TEXT NOT NULL + ) + """) + conn.execute(""" + CREATE INDEX IF NOT EXISTS idx_snapshot_ts + ON process_snapshot (timestamp) + """) + conn.commit() + + +def insert_process_snapshot(top_cpu, top_ram): + """Writes one unified row per 5-second poll โ€” timestamp + two compact JSON arrays.""" + row = ( + time.time(), + json.dumps(top_cpu, separators=(',', ':')), + json.dumps(top_ram, separators=(',', ':')) + ) + with sqlite3.connect(DB_PATH) as conn: + conn.execute( + "INSERT INTO process_snapshot (timestamp, top_cpu_processes, top_ram_processes) VALUES (?, ?, ?)", + row + ) + conn.commit() + +# layer 2 db code ends here + +# layer 1 db code starts here + +db_path = DB_PATH + +def create_connection(db_path): + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + cursor.execute('''CREATE TABLE IF NOT EXISTS layer1_sys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL, + + cpu_usage_percent REAL, + cpu_freq REAL, + cpu_user_time REAL, + cpu_system_time REAL, + cpu_idle_time REAL, + cpu_iowait_time REAL, + cpu_busy_time REAL, + cpu_ctx_switches REAL, + + memory_percent REAL, + memory_used INTEGER, + memory_available INTEGER, + memory_cached INTEGER, + memory_buffers INTEGER, + swap_percent REAL, + swap_sin INTEGER, + swap_sout INTEGER, + + disk_usage_percent REAL, + disk_read_mb_s REAL, + disk_write_mb_s REAL, + disk_read_time INTEGER, + disk_write_time INTEGER, + + net_rate_mb_s REAL, + net_bytes_sent INTEGER, + net_bytes_recv INTEGER, + net_packets_sent INTEGER, + net_packets_recv INTEGER, + net_errs INTEGER, + net_drops INTEGER, + + load_avg_1 REAL, + load_avg_5 REAL, + load_avg_15 REAL, + total_processes INTEGER, + running_processes INTEGER, + sleeping_processes INTEGER, + zombie_processes INTEGER, + + avg_temp REAL, + max_temp REAL, + battery_percent REAL, + process_data TEXT + ) + ''') + + conn.commit() + return conn + +def write_layer1(conn, timestamp, cpu_usage_percent, cpu_freq, cpu_user_time, cpu_system_time, cpu_idle_time, cpu_iowait_time, cpu_busy_time, cpu_ctx_switches, memory_percent, memory_used, memory_available, memory_cached, memory_buffers, swap_percent, swap_sin, swap_sout, disk_usage_percent, disk_read_mb_s, disk_write_mb_s, disk_read_time, disk_write_time, load_avg_1, load_avg_5, load_avg_15, total_processes, running_processes, sleeping_processes, zombie_processes, avg_temp, max_temp, battery_percent, net_rate_mb_s, net_bytes_sent, net_bytes_recv, net_packets_sent, net_packets_recv, net_errs, net_drops, process_data): + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO layer1_sys (timestamp, cpu_usage_percent, cpu_freq, cpu_user_time, cpu_system_time, cpu_idle_time, cpu_iowait_time, cpu_busy_time, cpu_ctx_switches, memory_percent, memory_used, memory_available, memory_cached, memory_buffers, swap_percent, swap_sin, swap_sout, disk_usage_percent, disk_read_mb_s, disk_write_mb_s, disk_read_time, disk_write_time, net_rate_mb_s, net_bytes_sent, net_bytes_recv, net_packets_sent, net_packets_recv, net_errs, net_drops, load_avg_1, load_avg_5, load_avg_15, total_processes, running_processes, sleeping_processes, zombie_processes, avg_temp, max_temp, battery_percent, process_data) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', (timestamp, cpu_usage_percent, cpu_freq, cpu_user_time, cpu_system_time, cpu_idle_time, cpu_iowait_time, cpu_busy_time, cpu_ctx_switches, memory_percent, memory_used, memory_available, memory_cached, memory_buffers, swap_percent, swap_sin, swap_sout, disk_usage_percent, disk_read_mb_s, disk_write_mb_s, disk_read_time, disk_write_time, net_rate_mb_s, net_bytes_sent, net_bytes_recv, net_packets_sent, net_packets_recv, net_errs, net_drops, load_avg_1, load_avg_5, load_avg_15, total_processes, running_processes, sleeping_processes, zombie_processes, avg_temp, max_temp, battery_percent, process_data)) + conn.commit() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6d3b0e9..4d04dda 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -# Project dependencies go here. +psutil>=6.0 diff --git a/test.py b/test.py new file mode 100644 index 0000000..716377d --- /dev/null +++ b/test.py @@ -0,0 +1,21 @@ +"""CogniOS Layer 2 telemetry daemon entry point.""" +import time +from collectors.layer2_process import collect_process_telemetry +from db import init_db, insert_process_snapshot + +if __name__ == "__main__": + init_db() + baselines = {} + print("CogniOS Telemetry Daemon started. Press Ctrl+C to stop.") + + try: + while True: + top_cpu, top_mem, baselines = collect_process_telemetry(baselines) + insert_process_snapshot(top_cpu, top_mem) + print( + f"Snapshot committed at t={time.time():.0f} | " + f"top_cpu={top_cpu[0]['name']} score={top_cpu[0]['cpu_score']} | " + f"top_ram={top_mem[0]['name']} score={top_mem[0]['ram_score']}" + ) + except KeyboardInterrupt: + print("\nDaemon safely terminated.") \ No newline at end of file diff --git a/utils/helpers.py b/utils/helpers.py new file mode 100644 index 0000000..7a92516 --- /dev/null +++ b/utils/helpers.py @@ -0,0 +1,9 @@ + + +def rate_mb_s(current_bytes, last_bytes, elapsed_sec): + + if last_bytes is None or elapsed_sec <= 0: + return None + # how many bytes have been sent/received since the last check, and convert to MB/s by dividing by 1 MB + delta_bytes = current_bytes - last_bytes + return (delta_bytes / elapsed_sec) / (1024 * 1024) \ No newline at end of file