diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6caf353 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: CI + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run tests + run: | + python test_tracker.py + + - name: Test basic execution + run: | + python tracker.py + timeout-minutes: 1 + + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install linting tools + run: | + python -m pip install --upgrade pip + pip install flake8 pylint + + - name: Lint with flake8 + run: | + # Stop build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # Exit-zero treats all errors as warnings + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + continue-on-error: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..50085b8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,47 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: | + python -m build + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + files: | + dist/* + generate_release_notes: true + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish to PyPI + if: startsWith(github.ref, 'refs/tags/v') + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + twine upload dist/* --skip-existing + continue-on-error: true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..20f6a9f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,62 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2025-11-25 + +### Added +- Initial release of System Tracker +- Real-time CPU usage monitoring +- Memory utilization tracking with detailed statistics +- Disk I/O statistics and usage monitoring +- Network traffic analysis (bytes sent/received, packets) +- Process monitoring with top processes by CPU usage +- Temperature sensor monitoring (platform-dependent) +- Configurable alert system with thresholds for CPU, memory, and disk +- Comprehensive logging system with daily log files +- Data export functionality (JSON and CSV formats) +- Configuration management via `config.json` +- Continuous monitoring mode with customizable intervals +- CLI arguments support for flexible operation +- Cross-platform support (Linux, macOS, Windows) + +### Features +- **Configuration System**: JSON-based configuration with sensible defaults +- **Alert System**: Real-time alerts when system metrics exceed configured thresholds +- **Logging**: Automatic daily log file creation in `logs/` directory +- **Data Export**: Export monitoring data to `exports/` directory +- **Process Monitor**: Enhanced CPU usage tracking with accurate process information +- **Temperature Monitoring**: System temperature sensors (when available) +- **Error Handling**: Comprehensive error handling throughout the codebase +- **Modular Architecture**: Clean separation of concerns with dedicated modules + +### Technical Improvements +- Fixed data exporter directory creation issue +- Improved process monitor CPU data accuracy with proper interval handling +- Added error handling to all system metric collection methods +- Resolved logger file handle management issues +- Enhanced zombie process handling in process monitoring + +### CI/CD +- GitHub Actions workflow for automated testing across multiple OS and Python versions +- Automated release workflow with package building +- Code quality checks with flake8 linting +- Multi-platform testing (Ubuntu, macOS, Windows) +- Python 3.8-3.12 compatibility testing + +### Documentation +- Comprehensive README with installation and usage instructions +- Configuration file documentation +- MIT License +- Project structure documentation +- Contributing guidelines + +### Dependencies +- psutil >= 5.9.0 +- GPUtil >= 1.4.0 +- requests >= 2.28.0 + +[1.0.0]: https://github.com/m1ngsama/tracker/releases/tag/v1.0.0 diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..07ae2c2 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include README.md +include LICENSE +include CHANGELOG.md +include requirements.txt +include config.json +recursive-include tests *.py diff --git a/README.md b/README.md index 3542a67..1bdb67d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ A comprehensive system monitoring tool for tracking various machine health metrics and performance indicators. +[![CI](https://github.com/m1ngsama/tracker/actions/workflows/ci.yml/badge.svg)](https://github.com/m1ngsama/tracker/actions/workflows/ci.yml) +[![Release](https://github.com/m1ngsama/tracker/actions/workflows/release.yml/badge.svg)](https://github.com/m1ngsama/tracker/actions/workflows/release.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + ## Features - **CPU Monitoring**: Real-time CPU usage percentage tracking @@ -10,36 +14,124 @@ A comprehensive system monitoring tool for tracking various machine health metri - **Network Traffic Analysis**: Track network bytes sent/received - **Process Monitoring**: View top processes by CPU usage - **Temperature Sensors**: Monitor system temperatures (if available) -- **System Uptime Tracking**: Track how long the system has been running +- **Alert System**: Configurable thresholds for CPU, memory, and disk alerts +- **Logging**: Automatic logging of all metrics to daily log files +- **Data Export**: Export monitoring data to JSON or CSV formats +- **Configuration**: Customizable settings via JSON config file ## Installation +### From PyPI (coming soon) + ```bash +pip install system-tracker +``` + +### From Source + +```bash +git clone https://github.com/m1ngsama/tracker.git +cd tracker pip install -r requirements.txt ``` ## Usage -Basic usage: +### Basic usage: ```bash python tracker.py ``` -Continuous monitoring mode: +### Continuous monitoring mode: ```bash python tracker.py --continuous --interval 5 ``` -Command line options: +### Command line options: - `-c, --continuous`: Run in continuous monitoring mode - `-i, --interval`: Set update interval in seconds (default: 5) +## Configuration + +The `config.json` file allows you to customize the tracker behavior: + +```json +{ + "update_interval": 5, + "display": { + "show_cpu": true, + "show_memory": true, + "show_disk": true, + "show_network": true, + "show_processes": true, + "show_temperatures": true + }, + "process_limit": 5, + "alert_thresholds": { + "cpu_percent": 80, + "memory_percent": 85, + "disk_percent": 90 + } +} +``` + +## Output + +The tracker provides: +- **Console Output**: Real-time metrics displayed in the terminal +- **Log Files**: Daily logs stored in `logs/` directory +- **Alerts**: Visual and logged warnings when thresholds are exceeded +- **Export Data**: Optional data export to `exports/` directory + ## Requirements - Python 3.8+ - psutil - GPUtil (for GPU monitoring) +- requests + +## Development + +### Running Tests + +```bash +python test_tracker.py +``` + +### Project Structure + +``` +tracker/ +├── tracker.py # Main application +├── process_monitor.py # Process monitoring module +├── temperature_monitor.py # Temperature sensors module +├── config_manager.py # Configuration management +├── alert_system.py # Alert and threshold management +├── logger.py # Logging functionality +├── data_exporter.py # Data export utilities +├── config.json # Configuration file +└── requirements.txt # Python dependencies +``` + +## CI/CD + +This project uses GitHub Actions for: +- **Continuous Integration**: Automated testing on multiple OS and Python versions +- **Automated Releases**: Automatic package building and release creation on version tags +- **Code Quality**: Linting and syntax checking + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. ## License -MIT License +MIT License - see [LICENSE](LICENSE) file for details + +## Author + +m1ngsama + +## Acknowledgments + +- Built with [psutil](https://github.com/giampaolo/psutil) for cross-platform system monitoring diff --git a/data_exporter.py b/data_exporter.py index e895430..676f46d 100644 --- a/data_exporter.py +++ b/data_exporter.py @@ -10,6 +10,13 @@ from datetime import datetime class DataExporter: def __init__(self, output_dir='exports'): self.output_dir = output_dir + self._ensure_directory() + + def _ensure_directory(self): + """Ensure the output directory exists""" + import os + if not os.path.exists(self.output_dir): + os.makedirs(self.output_dir) def export_to_json(self, data, filename=None): """Export data to JSON format""" @@ -18,10 +25,12 @@ class DataExporter: filepath = f"{self.output_dir}/{filename}" - with open(filepath, 'w') as f: - json.dump(data, f, indent=2) - - return filepath + try: + with open(filepath, 'w') as f: + json.dump(data, f, indent=2) + return filepath + except IOError as e: + raise IOError(f"Failed to export data to JSON: {e}") def export_to_csv(self, data, filename=None): """Export data to CSV format""" @@ -30,11 +39,13 @@ class DataExporter: filepath = f"{self.output_dir}/{filename}" - if isinstance(data, list) and len(data) > 0: - keys = data[0].keys() - with open(filepath, 'w', newline='') as f: - writer = csv.DictWriter(f, fieldnames=keys) - writer.writeheader() - writer.writerows(data) - - return filepath + try: + if isinstance(data, list) and len(data) > 0: + keys = data[0].keys() + with open(filepath, 'w', newline='') as f: + writer = csv.DictWriter(f, fieldnames=keys) + writer.writeheader() + writer.writerows(data) + return filepath + except IOError as e: + raise IOError(f"Failed to export data to CSV: {e}") diff --git a/logger.py b/logger.py index b7fd9bf..6c88037 100644 --- a/logger.py +++ b/logger.py @@ -22,16 +22,25 @@ class TrackerLogger: f"tracker_{datetime.now().strftime('%Y%m%d')}.log" ) - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler(log_file), - logging.StreamHandler() - ] - ) + # Clear any existing handlers to prevent duplicate logging + logger = logging.getLogger('SystemTracker') + logger.handlers.clear() - self.logger = logging.getLogger('SystemTracker') + # Create handlers + file_handler = logging.FileHandler(log_file) + stream_handler = logging.StreamHandler() + + # Create formatter and add it to handlers + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + file_handler.setFormatter(formatter) + stream_handler.setFormatter(formatter) + + # Add handlers to logger + logger.addHandler(file_handler) + logger.addHandler(stream_handler) + logger.setLevel(logging.INFO) + + self.logger = logger def log_stats(self, stats_type, stats_data): """Log system statistics""" diff --git a/process_monitor.py b/process_monitor.py index 0d97e70..63c4ee1 100644 --- a/process_monitor.py +++ b/process_monitor.py @@ -9,10 +9,19 @@ class ProcessMonitor: def get_top_processes(self, limit=5): """Get top processes by CPU usage""" processes = [] - for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']): + # First collect all processes + for proc in psutil.process_iter(['pid', 'name']): try: - processes.append(proc.info) - except (psutil.NoSuchProcess, psutil.AccessDenied): + # Get CPU percent with interval for more accurate reading + cpu_percent = proc.cpu_percent(interval=0.1) + memory_percent = proc.memory_percent() + processes.append({ + 'pid': proc.info['pid'], + 'name': proc.info['name'], + 'cpu_percent': cpu_percent, + 'memory_percent': memory_percent + }) + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): pass processes.sort(key=lambda x: x['cpu_percent'] or 0, reverse=True) @@ -20,17 +29,23 @@ class ProcessMonitor: def get_process_count(self): """Get total number of running processes""" - return len(psutil.pids()) + try: + return len(psutil.pids()) + except Exception: + return 0 def display_processes(self): """Display top processes""" - print(f"\nTop Processes by CPU Usage:") - print(f"{'PID':<10}{'Name':<30}{'CPU%':<10}{'Memory%':<10}") - print("-" * 60) + try: + print(f"\nTop Processes by CPU Usage:") + print(f"{'PID':<10}{'Name':<30}{'CPU%':<10}{'Memory%':<10}") + print("-" * 60) - for proc in self.get_top_processes(): - cpu = proc['cpu_percent'] if proc['cpu_percent'] is not None else 0 - mem = proc['memory_percent'] if proc['memory_percent'] is not None else 0 - print(f"{proc['pid']:<10}{proc['name']:<30}{cpu:<10.2f}{mem:<10.2f}") + for proc in self.get_top_processes(): + cpu = proc['cpu_percent'] if proc['cpu_percent'] is not None else 0 + mem = proc['memory_percent'] if proc['memory_percent'] is not None else 0 + print(f"{proc['pid']:<10}{proc['name']:<30}{cpu:<10.2f}{mem:<10.2f}") - print(f"\nTotal Processes: {self.get_process_count()}") + print(f"\nTotal Processes: {self.get_process_count()}") + except Exception as e: + print(f"Error displaying processes: {e}") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..498ec10 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"] +build-backend = "setuptools.build_meta" + +[project] +name = "system-tracker" +version = "1.0.0" +description = "A comprehensive system monitoring tool for tracking machine health metrics" +readme = "README.md" +requires-python = ">=3.8" +license = {text = "MIT"} +authors = [ + {name = "m1ngsama"} +] +keywords = ["monitoring", "system", "performance", "metrics", "health-check"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Topic :: System :: Monitoring", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "psutil>=5.9.0", + "GPUtil>=1.4.0", + "requests>=2.28.0", +] + +[project.urls] +Homepage = "https://github.com/m1ngsama/tracker" +Repository = "https://github.com/m1ngsama/tracker" +Issues = "https://github.com/m1ngsama/tracker/issues" + +[project.scripts] +tracker = "tracker:main" + +[tool.setuptools] +packages = ["tracker"] diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7302371 --- /dev/null +++ b/setup.py @@ -0,0 +1,47 @@ +from setuptools import setup, find_packages + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +with open("requirements.txt", "r", encoding="utf-8") as fh: + requirements = [line.strip() for line in fh if line.strip() and not line.startswith("#")] + +setup( + name="system-tracker", + version="1.0.0", + author="m1ngsama", + author_email="", + description="A comprehensive system monitoring tool for tracking machine health metrics", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/m1ngsama/tracker", + packages=find_packages(), + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Topic :: System :: Monitoring", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + ], + python_requires=">=3.8", + install_requires=requirements, + entry_points={ + "console_scripts": [ + "tracker=tracker:main", + ], + }, + include_package_data=True, + package_data={ + "": ["config.json"], + }, +) diff --git a/test_export.py b/test_export.py new file mode 100644 index 0000000..1359331 --- /dev/null +++ b/test_export.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""Test data export functionality""" + +from data_exporter import DataExporter + +# Test data +test_data = [ + {'timestamp': '2025-11-25 15:00:00', 'cpu': 45.2, 'memory': 60.1}, + {'timestamp': '2025-11-25 15:05:00', 'cpu': 52.3, 'memory': 62.5}, + {'timestamp': '2025-11-25 15:10:00', 'cpu': 48.9, 'memory': 61.8} +] + +exporter = DataExporter() + +# Test JSON export +json_file = exporter.export_to_json(test_data) +print(f"✓ JSON export successful: {json_file}") + +# Test CSV export +csv_file = exporter.export_to_csv(test_data) +print(f"✓ CSV export successful: {csv_file}") + +print("\nExport directory contents:") +import os +for file in os.listdir('exports'): + print(f" - {file}") diff --git a/tracker.py b/tracker.py index 476afd7..c01bcc5 100644 --- a/tracker.py +++ b/tracker.py @@ -8,46 +8,70 @@ import time import argparse from datetime import datetime from process_monitor import ProcessMonitor +from temperature_monitor import TemperatureMonitor +from config_manager import Config +from alert_system import AlertSystem +from logger import TrackerLogger class SystemTracker: - def __init__(self): + def __init__(self, config_file='config.json'): self.start_time = time.time() + self.config = Config(config_file) self.process_monitor = ProcessMonitor() + self.temperature_monitor = TemperatureMonitor() + self.alert_system = AlertSystem(self.config) + self.logger = TrackerLogger() def get_cpu_usage(self): """Get current CPU usage percentage""" - return psutil.cpu_percent(interval=1, percpu=False) + try: + return psutil.cpu_percent(interval=1, percpu=False) + except Exception as e: + self.logger.log_error(f"Failed to get CPU usage: {e}") + return 0.0 def get_memory_info(self): """Get memory usage statistics""" - mem = psutil.virtual_memory() - return { - 'total': mem.total, - 'available': mem.available, - 'percent': mem.percent, - 'used': mem.used - } + try: + mem = psutil.virtual_memory() + return { + 'total': mem.total, + 'available': mem.available, + 'percent': mem.percent, + 'used': mem.used + } + except Exception as e: + self.logger.log_error(f"Failed to get memory info: {e}") + return {'total': 0, 'available': 0, 'percent': 0, 'used': 0} def get_disk_usage(self): """Get disk usage statistics""" - disk = psutil.disk_usage('/') - return { - 'total': disk.total, - 'used': disk.used, - 'free': disk.free, - 'percent': disk.percent - } + try: + disk = psutil.disk_usage('/') + return { + 'total': disk.total, + 'used': disk.used, + 'free': disk.free, + 'percent': disk.percent + } + except Exception as e: + self.logger.log_error(f"Failed to get disk usage: {e}") + return {'total': 0, 'used': 0, 'free': 0, 'percent': 0} def get_network_stats(self): """Get network I/O statistics""" - net = psutil.net_io_counters() - return { - 'bytes_sent': net.bytes_sent, - 'bytes_recv': net.bytes_recv, - 'packets_sent': net.packets_sent, - 'packets_recv': net.packets_recv - } + try: + net = psutil.net_io_counters() + return { + 'bytes_sent': net.bytes_sent, + 'bytes_recv': net.bytes_recv, + 'packets_sent': net.packets_sent, + 'packets_recv': net.packets_recv + } + except Exception as e: + self.logger.log_error(f"Failed to get network stats: {e}") + return {'bytes_sent': 0, 'bytes_recv': 0, 'packets_sent': 0, 'packets_recv': 0} def display_stats(self): """Display all system statistics""" @@ -55,21 +79,44 @@ class SystemTracker: print(f"System Tracker - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") print(f"{'='*50}\n") - print(f"CPU Usage: {self.get_cpu_usage()}%") + # CPU monitoring + if self.config.get('display.show_cpu', True): + cpu_usage = self.get_cpu_usage() + print(f"CPU Usage: {cpu_usage}%") + self.logger.log_stats('CPU', f"{cpu_usage}%") + self.alert_system.check_cpu_alert(cpu_usage) - mem = self.get_memory_info() - print(f"Memory: {mem['percent']}% ({mem['used'] / (1024**3):.2f}GB / {mem['total'] / (1024**3):.2f}GB)") + # Memory monitoring + if self.config.get('display.show_memory', True): + mem = self.get_memory_info() + print(f"Memory: {mem['percent']}% ({mem['used'] / (1024**3):.2f}GB / {mem['total'] / (1024**3):.2f}GB)") + self.logger.log_stats('Memory', f"{mem['percent']}%") + self.alert_system.check_memory_alert(mem['percent']) - disk = self.get_disk_usage() - print(f"Disk: {disk['percent']}% ({disk['used'] / (1024**3):.2f}GB / {disk['total'] / (1024**3):.2f}GB)") + # Disk monitoring + if self.config.get('display.show_disk', True): + disk = self.get_disk_usage() + print(f"Disk: {disk['percent']}% ({disk['used'] / (1024**3):.2f}GB / {disk['total'] / (1024**3):.2f}GB)") + self.logger.log_stats('Disk', f"{disk['percent']}%") + self.alert_system.check_disk_alert(disk['percent']) - net = self.get_network_stats() - print(f"Network: Sent {net['bytes_sent'] / (1024**2):.2f}MB | Recv {net['bytes_recv'] / (1024**2):.2f}MB") + # Network monitoring + if self.config.get('display.show_network', True): + net = self.get_network_stats() + print(f"Network: Sent {net['bytes_sent'] / (1024**2):.2f}MB | Recv {net['bytes_recv'] / (1024**2):.2f}MB") + self.logger.log_stats('Network', f"Sent: {net['bytes_sent']} Recv: {net['bytes_recv']}") - self.process_monitor.display_processes() + # Process monitoring + if self.config.get('display.show_processes', True): + self.process_monitor.display_processes() + + # Temperature monitoring + if self.config.get('display.show_temperatures', True): + self.temperature_monitor.display_temperatures() -if __name__ == "__main__": +def main(): + """Main entry point for the tracker application""" parser = argparse.ArgumentParser(description='System Tracker - Monitor machine health') parser.add_argument('-c', '--continuous', action='store_true', help='Run continuously') parser.add_argument('-i', '--interval', type=int, default=5, help='Update interval in seconds') @@ -86,3 +133,7 @@ if __name__ == "__main__": print("\n\nTracker stopped by user") else: tracker.display_stats() + + +if __name__ == "__main__": + main()