Major refactoring of the codebase, including restructuring of files and directories, renaming of modules and classes, and improvements to the overall organization and readability of the code. This refactoring aims to enhance maintainability, scalability, and clarity of the codebase while preserving existing functionality. The changes include:
- Restructuring of the project directory into client and server components - Renaming of modules and classes to better reflect their purpose and functionality - Moving common utilities and configurations to a shared location - Updating import statements to reflect the new structure - Adding new documentation files for better clarity on various aspects of the project - Removing deprecated or unused code to streamline the codebase - Ensuring that all existing functionality is preserved and that the codebase remains functional after the refactoring.
This commit is contained in:
@@ -0,0 +1,544 @@
|
||||
# Plugin Development Guide
|
||||
|
||||
This guide explains how to create custom plugins for the Heartbeat monitoring system.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Plugin Architecture](#plugin-architecture)
|
||||
- [Plugin Types](#plugin-types)
|
||||
- [Creating a Plugin](#creating-a-plugin)
|
||||
- [Plugin Lifecycle](#plugin-lifecycle)
|
||||
- [Configuration](#configuration)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Examples](#examples)
|
||||
- [Testing](#testing)
|
||||
|
||||
## Plugin Architecture
|
||||
|
||||
Heartbeat's plugin system is designed to be simple yet powerful. Plugins are Python classes that inherit from one of the base plugin types and implement a few key methods.
|
||||
|
||||
### Key Concepts
|
||||
|
||||
- **Plugin Registry**: Central registry that manages all loaded plugins
|
||||
- **Plugin Loader**: Automatically discovers and loads plugins from the `hbd/plugins/` directory
|
||||
- **Plugin Types**: InfoPlugin (static data) and MonitorPlugin (periodic metrics)
|
||||
- **Async/Await**: All plugin methods are async for non-blocking operation
|
||||
|
||||
## Plugin Types
|
||||
|
||||
### InfoPlugin
|
||||
|
||||
InfoPlugins collect static information that doesn't change frequently (OS version, hardware specs, etc.).
|
||||
|
||||
- **Runs once** at startup (interval = 0)
|
||||
- **Cached** - data is collected once and reused
|
||||
- **Lightweight** - no periodic overhead
|
||||
|
||||
**Use InfoPlugin for:**
|
||||
- Operating system details
|
||||
- Hardware information
|
||||
- Software versions
|
||||
- Configuration data
|
||||
- Static inventory
|
||||
|
||||
### MonitorPlugin
|
||||
|
||||
MonitorPlugins collect metrics that change over time (CPU usage, memory, network traffic).
|
||||
|
||||
- **Runs periodically** based on configured interval
|
||||
- **Scheduled** - collected at regular intervals
|
||||
- **Dynamic** - captures changing system state
|
||||
|
||||
**Use MonitorPlugin for:**
|
||||
- Resource usage (CPU, memory, disk, network)
|
||||
- Performance metrics
|
||||
- Counters and gauges
|
||||
- Time-series data
|
||||
|
||||
## Creating a Plugin
|
||||
|
||||
### Step 1: Choose Plugin Type
|
||||
|
||||
Decide whether your plugin collects static information (InfoPlugin) or dynamic metrics (MonitorPlugin).
|
||||
|
||||
### Step 2: Create Plugin File
|
||||
|
||||
Create a new Python file in `hbd/plugins/` directory:
|
||||
|
||||
```python
|
||||
"""
|
||||
My awesome plugin for Heartbeat.
|
||||
|
||||
Brief description of what this plugin does.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
# Import psutil or other dependencies if needed
|
||||
try:
|
||||
import psutil
|
||||
except ImportError:
|
||||
psutil = None
|
||||
|
||||
from hbd.plugin import MonitorPlugin # or InfoPlugin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MyAwesomePlugin(MonitorPlugin): # or InfoPlugin
|
||||
"""
|
||||
One-line description of the plugin.
|
||||
|
||||
Collects:
|
||||
- List of metrics/data collected
|
||||
- Another metric
|
||||
|
||||
Configuration:
|
||||
interval: Collection interval in seconds (default: 60)
|
||||
option1: Description of option1 (default: value)
|
||||
option2: Description of option2 (default: value)
|
||||
"""
|
||||
|
||||
name = "my_awesome_plugin" # Unique plugin name
|
||||
interval = 60 # For MonitorPlugin, use 0 for InfoPlugin
|
||||
|
||||
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
||||
"""Initialize the plugin with optional configuration."""
|
||||
super().__init__(config)
|
||||
|
||||
# Extract configuration options
|
||||
self.option1 = self.config.get('option1', 'default_value')
|
||||
self.option2 = self.config.get('option2', True)
|
||||
|
||||
# Check dependencies
|
||||
if psutil is None:
|
||||
raise ImportError("psutil is required for my_awesome_plugin")
|
||||
|
||||
async def initialize(self):
|
||||
"""
|
||||
Initialize the plugin.
|
||||
|
||||
This is called once when the plugin is loaded.
|
||||
Use this to verify dependencies, establish connections, etc.
|
||||
|
||||
Returns:
|
||||
True if initialization successful, False otherwise
|
||||
"""
|
||||
logger.info(f"My awesome plugin initialized (option1: {self.option1})")
|
||||
return True
|
||||
|
||||
async def collect(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Collect data.
|
||||
|
||||
This is called periodically (MonitorPlugin) or once (InfoPlugin).
|
||||
|
||||
Returns:
|
||||
Dictionary of collected data (will be sent to server)
|
||||
"""
|
||||
try:
|
||||
data = await self._collect_metrics()
|
||||
logger.debug(f"Collected {len(data)} metrics")
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.error(f"Error collecting data: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
async def _collect_metrics(self) -> Dict[str, Any]:
|
||||
"""Internal method to collect actual metrics."""
|
||||
metrics = {}
|
||||
|
||||
# Collect your data here
|
||||
metrics['metric1'] = self._get_metric1()
|
||||
metrics['metric2'] = self._get_metric2()
|
||||
|
||||
return metrics
|
||||
|
||||
def _get_metric1(self):
|
||||
"""Helper method for metric collection."""
|
||||
# Implementation here
|
||||
return 42
|
||||
|
||||
def _get_metric2(self):
|
||||
"""Helper method for metric collection."""
|
||||
# Implementation here
|
||||
return "hello"
|
||||
|
||||
async def cleanup(self):
|
||||
"""
|
||||
Cleanup resources.
|
||||
|
||||
This is called when the plugin is unloaded or the client shuts down.
|
||||
Use this to close connections, release resources, etc.
|
||||
"""
|
||||
logger.info("My awesome plugin cleanup")
|
||||
|
||||
|
||||
# Plugin instance for automatic discovery
|
||||
plugin = MyAwesomePlugin
|
||||
```
|
||||
|
||||
### Step 3: Test Your Plugin
|
||||
|
||||
Create a test script to verify your plugin works:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from hbd.plugins.my_awesome_plugin import MyAwesomePlugin
|
||||
|
||||
async def test():
|
||||
# Create plugin instance
|
||||
plugin = MyAwesomePlugin({'option1': 'test_value'})
|
||||
|
||||
# Initialize
|
||||
if not await plugin.initialize():
|
||||
print("Failed to initialize")
|
||||
return False
|
||||
|
||||
# Collect data
|
||||
data = await plugin.collect()
|
||||
print(f"Collected data: {data}")
|
||||
|
||||
# Cleanup
|
||||
await plugin.cleanup()
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == '__main__':
|
||||
success = asyncio.run(test())
|
||||
sys.exit(0 if success else 1)
|
||||
```
|
||||
|
||||
## Plugin Lifecycle
|
||||
|
||||
Understanding the plugin lifecycle helps you implement plugins correctly:
|
||||
|
||||
```
|
||||
1. Plugin Discovery
|
||||
└─> Loader scans hbd/plugins/ directory
|
||||
└─> Finds Python files (except those starting with _)
|
||||
└─> Imports modules
|
||||
|
||||
2. Plugin Instantiation
|
||||
└─> Creates instance with configuration
|
||||
└─> __init__() is called
|
||||
|
||||
3. Plugin Initialization
|
||||
└─> initialize() is called
|
||||
└─> Plugin verifies dependencies, establishes connections
|
||||
└─> Returns True/False for success/failure
|
||||
|
||||
4. Plugin Registration
|
||||
└─> If initialization succeeds, plugin is registered
|
||||
└─> Plugin becomes active
|
||||
|
||||
5. Data Collection
|
||||
└─> For InfoPlugin: collect() called once after initialization
|
||||
└─> For MonitorPlugin: collect() called periodically based on interval
|
||||
└─> Data is sent to server via PLG message
|
||||
|
||||
6. Plugin Shutdown
|
||||
└─> cleanup() is called
|
||||
└─> Plugin releases resources, closes connections
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Plugin-Specific Configuration
|
||||
|
||||
Plugins receive configuration through the `config` parameter in `__init__`:
|
||||
|
||||
```python
|
||||
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
||||
super().__init__(config)
|
||||
|
||||
# Access configuration with defaults
|
||||
self.interval = self.config.get('interval', 60)
|
||||
self.threshold = self.config.get('threshold', 80)
|
||||
self.enabled_features = self.config.get('features', ['feature1', 'feature2'])
|
||||
```
|
||||
|
||||
### Client Configuration File
|
||||
|
||||
Users configure plugins in the client configuration YAML:
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
my_awesome_plugin:
|
||||
enabled: true
|
||||
interval: 120
|
||||
option1: custom_value
|
||||
option2: false
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Error Handling
|
||||
|
||||
Always handle errors gracefully:
|
||||
|
||||
```python
|
||||
async def collect(self) -> Dict[str, Any]:
|
||||
try:
|
||||
return await self._collect_metrics()
|
||||
except Exception as e:
|
||||
logger.error(f"Error collecting metrics: {e}")
|
||||
return {"error": str(e)}
|
||||
```
|
||||
|
||||
### 2. Logging
|
||||
|
||||
Use appropriate log levels:
|
||||
|
||||
```python
|
||||
logger.debug("Detailed information for debugging")
|
||||
logger.info("Normal operation messages")
|
||||
logger.warning("Warning messages for unusual but handled situations")
|
||||
logger.error("Error messages for failures")
|
||||
```
|
||||
|
||||
### 3. Dependencies
|
||||
|
||||
Check for optional dependencies:
|
||||
|
||||
```python
|
||||
try:
|
||||
import some_optional_library
|
||||
except ImportError:
|
||||
some_optional_library = None
|
||||
|
||||
# Later in __init__:
|
||||
if some_optional_library is None:
|
||||
raise ImportError("some_optional_library is required")
|
||||
```
|
||||
|
||||
### 4. Performance
|
||||
|
||||
- Keep collection methods fast (< 1 second)
|
||||
- Use async/await for I/O operations
|
||||
- Cache expensive computations
|
||||
- Don't block the event loop
|
||||
|
||||
### 5. Data Structure
|
||||
|
||||
Return clean, structured data:
|
||||
|
||||
```python
|
||||
{
|
||||
'metric_name': value,
|
||||
'nested_data': {
|
||||
'sub_metric': value
|
||||
},
|
||||
'list_data': [item1, item2],
|
||||
'timestamp': time.time() # Optional timestamp
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Documentation
|
||||
|
||||
Document your plugin thoroughly:
|
||||
|
||||
- Class docstring with description and configuration
|
||||
- Method docstrings explaining purpose and return values
|
||||
- Inline comments for complex logic
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Simple InfoPlugin
|
||||
|
||||
```python
|
||||
from hbd.plugin import InfoPlugin
|
||||
import platform
|
||||
|
||||
class SimpleInfoPlugin(InfoPlugin):
|
||||
"""Collect basic system information."""
|
||||
|
||||
name = "simple_info"
|
||||
interval = 0 # InfoPlugin
|
||||
|
||||
async def initialize(self):
|
||||
return True
|
||||
|
||||
async def collect(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'hostname': platform.node(),
|
||||
'system': platform.system(),
|
||||
'python_version': platform.python_version()
|
||||
}
|
||||
|
||||
async def cleanup(self):
|
||||
pass
|
||||
|
||||
plugin = SimpleInfoPlugin
|
||||
```
|
||||
|
||||
### Example 2: MonitorPlugin with State
|
||||
|
||||
```python
|
||||
from hbd.plugin import MonitorPlugin
|
||||
import time
|
||||
|
||||
class CounterPlugin(MonitorPlugin):
|
||||
"""Track a counter over time."""
|
||||
|
||||
name = "counter"
|
||||
interval = 30
|
||||
|
||||
def __init__(self, config=None):
|
||||
super().__init__(config)
|
||||
self._counter = 0
|
||||
self._start_time = time.time()
|
||||
|
||||
async def initialize(self):
|
||||
return True
|
||||
|
||||
async def collect(self) -> Dict[str, Any]:
|
||||
self._counter += 1
|
||||
uptime = time.time() - self._start_time
|
||||
|
||||
return {
|
||||
'count': self._counter,
|
||||
'uptime': uptime,
|
||||
'rate': self._counter / uptime
|
||||
}
|
||||
|
||||
async def cleanup(self):
|
||||
pass
|
||||
|
||||
plugin = CounterPlugin
|
||||
```
|
||||
|
||||
### Example 3: Plugin with External Command
|
||||
|
||||
```python
|
||||
from hbd.plugin import MonitorPlugin
|
||||
import asyncio
|
||||
|
||||
class CommandPlugin(MonitorPlugin):
|
||||
"""Execute external command and capture output."""
|
||||
|
||||
name = "command_executor"
|
||||
interval = 60
|
||||
|
||||
def __init__(self, config=None):
|
||||
super().__init__(config)
|
||||
self.command = self.config.get('command', 'echo "no command"')
|
||||
|
||||
async def initialize(self):
|
||||
return True
|
||||
|
||||
async def collect(self) -> Dict[str, Any]:
|
||||
try:
|
||||
process = await asyncio.create_subprocess_shell(
|
||||
self.command,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
process.communicate(),
|
||||
timeout=30
|
||||
)
|
||||
|
||||
return {
|
||||
'exit_code': process.returncode,
|
||||
'stdout': stdout.decode('utf-8'),
|
||||
'stderr': stderr.decode('utf-8')
|
||||
}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
async def cleanup(self):
|
||||
pass
|
||||
|
||||
plugin = CommandPlugin
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Testing
|
||||
|
||||
Create unit tests for your plugins:
|
||||
|
||||
```python
|
||||
import unittest
|
||||
import asyncio
|
||||
|
||||
class TestMyPlugin(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.plugin = MyAwesomePlugin({'option1': 'test'})
|
||||
|
||||
def test_initialization(self):
|
||||
result = asyncio.run(self.plugin.initialize())
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_collection(self):
|
||||
asyncio.run(self.plugin.initialize())
|
||||
data = asyncio.run(self.plugin.collect())
|
||||
|
||||
self.assertIsInstance(data, dict)
|
||||
self.assertIn('metric1', data)
|
||||
self.assertGreater(data['metric1'], 0)
|
||||
|
||||
def tearDown(self):
|
||||
asyncio.run(self.plugin.cleanup())
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
|
||||
Test your plugin with the actual client:
|
||||
|
||||
```bash
|
||||
# Create test configuration
|
||||
cat > test_config.yaml <<EOF
|
||||
server: localhost
|
||||
plugins:
|
||||
my_awesome_plugin:
|
||||
enabled: true
|
||||
interval: 10
|
||||
option1: test_value
|
||||
EOF
|
||||
|
||||
# Run client in test mode
|
||||
python -m hbd.hbc -c test_config.yaml --verbose
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### My plugin isn't loading
|
||||
|
||||
1. Check filename doesn't start with underscore
|
||||
2. Verify plugin class inherits from InfoPlugin or MonitorPlugin
|
||||
3. Check `initialize()` returns True
|
||||
4. Look for import errors in logs
|
||||
|
||||
### Plugin loads but doesn't collect data
|
||||
|
||||
1. Check `interval` is set correctly (0 for InfoPlugin, > 0 for MonitorPlugin)
|
||||
2. Verify `collect()` returns a dictionary
|
||||
3. Check for exceptions in `collect()` method
|
||||
4. Enable DEBUG logging to see detailed errors
|
||||
|
||||
### Data isn't appearing on server
|
||||
|
||||
1. Verify client is connected to server
|
||||
2. Check server logs for PLG message handling
|
||||
3. Verify returned data is JSON-serializable
|
||||
4. Check for large data sizes (may exceed UDP packet size)
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Plugin Framework Source](../hbd/plugin.py) - Core plugin implementation
|
||||
- [Built-in Plugins](../hbd/plugins/) - Examples of working plugins
|
||||
- [Nagios Integration](NAGIOS_INTEGRATION.md) - Running external plugins
|
||||
- [Configuration Guide](../hbd/config_example.yaml) - Full configuration reference
|
||||
Reference in New Issue
Block a user