First commit

This commit is contained in:
2025-12-17 23:14:15 +01:00
commit eba1e1fcb2
13 changed files with 1294 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
#!/usr/bin/env python3
import os
import platform
import subprocess
import glob
from typing import Dict, Any, Optional, List, Tuple
import sys
# Default update interval in seconds
DEFAULT_INTERVAL = 30 # 30 seconds
def get_temperature_linux_coretemp() -> List[Tuple[float, str]]:
"""Get CPU temperatures using coretemp module."""
temps = []
try:
for hwmon_dir in glob.glob('/sys/class/hwmon/hwmon*'):
try:
with open(os.path.join(hwmon_dir, 'name'), 'r') as f:
if f.read().strip() == 'coretemp':
# Found coretemp, get all temperatures
for temp_file in glob.glob(os.path.join(hwmon_dir, 'temp*_input')):
try:
with open(temp_file, 'r') as tf:
temp = float(tf.read().strip()) / 1000.0
# Get label if available
label = "Package"
label_file = temp_file.replace('_input', '_label')
if os.path.exists(label_file):
with open(label_file, 'r') as lf:
label = lf.read().strip()
temps.append((temp, label))
except (FileNotFoundError, ValueError):
continue
except (FileNotFoundError, ValueError):
continue
except Exception:
pass
return temps
def get_temperature_linux_thermal() -> List[Tuple[float, str]]:
"""Get CPU temperatures using thermal zones."""
temps = []
try:
for thermal_dir in glob.glob('/sys/class/thermal/thermal_zone*'):
try:
with open(os.path.join(thermal_dir, 'type'), 'r') as f:
zone_type = f.read().strip()
if 'cpu' in zone_type.lower():
with open(os.path.join(thermal_dir, 'temp'), 'r') as tf:
temp = float(tf.read().strip()) / 1000.0
temps.append((temp, zone_type))
except (FileNotFoundError, ValueError):
continue
except Exception:
pass
return temps
def get_temperature_freebsd() -> List[Tuple[float, str]]:
"""Get CPU temperatures on FreeBSD systems."""
temps = []
try:
# Get number of CPUs
cpu_count = int(subprocess.check_output(['sysctl', '-n', 'hw.ncpu']).decode().strip())
# Get temperature for each CPU
for cpu in range(cpu_count):
try:
temp = subprocess.check_output(['sysctl', '-n', f'dev.cpu.{cpu}.temperature']).decode().strip()
temp_value = float(temp)
temps.append((temp_value, f'CPU {cpu}'))
except (subprocess.SubprocessError, ValueError):
continue
except (subprocess.SubprocessError, ValueError):
pass
return temps
def collect_metrics() -> Dict[str, Any]:
"""Collect CPU temperature metrics."""
metrics = {
"entities": []
}
temps = []
# Get CPU temperatures based on OS
if sys.platform.startswith('linux'):
# Try coretemp first (most reliable)
temps.extend(get_temperature_linux_coretemp())
# If no coretemp found, try thermal zones
if not temps:
temps.extend(get_temperature_linux_thermal())
elif sys.platform.startswith('freebsd'):
temps.extend(get_temperature_freebsd())
# Add temperature sensors
if temps:
# Only keep package temperatures
package_temps = [(t, l) for t, l in temps if 'Package' in l]
# Add package temperature
for temp, label in package_temps:
metrics['entities'].append({
'sensor_id': 'cpu_temperature',
'name': 'CPU Temperature',
'value': str(round(temp, 1)),
'state_class': 'measurement',
'unit_of_measurement': '°C',
'device_class': 'temperature',
'icon': 'mdi:thermometer',
'attributes': {
'friendly_name': 'CPU Temperature',
'source': 'coretemp'
}
})
return metrics
if __name__ == "__main__":
# Example usage
metrics = collect_metrics()
print(metrics)

View File

@@ -0,0 +1,169 @@
#!/usr/bin/env python3
import psutil
import time
from datetime import datetime
from typing import Dict, Any
# Default update interval in seconds
DEFAULT_INTERVAL = 60 # 1 minute
def collect_metrics() -> Dict[str, Any]:
"""Collect system metrics and return them in the required format."""
# Get system metrics
boot_time = datetime.fromtimestamp(psutil.boot_time())
load_avg = psutil.getloadavg()
memory = psutil.virtual_memory()
swap = psutil.swap_memory()
cpu_percent = psutil.cpu_percent(interval=1)
# Convert bytes to GB or TB conditionally
def to_size(bytes_value: int):
tb = 1024**4
gb = 1024**3
if bytes_value >= tb:
return round(bytes_value / tb, 2), 'TB'
return round(bytes_value / gb, 2), 'GB'
return {
"version": "1.0",
"entities": [
{
"name": "Last Boot",
"sensor_id": "last_boot",
"state_class": "total",
"device_class": "timestamp",
"unit_of_measurement": "",
"value": boot_time.astimezone().isoformat(),
"icon": "mdi:clock-time-four",
"attributes": {
"friendly_name": "Last Boot Time"
}
},
{
"name": "Load Average (15m)",
"sensor_id": "load_15m",
"state_class": "measurement",
"unit_of_measurement": "",
"device_class": "power_factor",
"value": str(round(load_avg[2], 1)),
"icon": "mdi:speedometer",
"attributes": {
"friendly_name": "System Load (15m)"
}
},
{
"name": "Load Average (5m)",
"sensor_id": "load_5m",
"state_class": "measurement",
"unit_of_measurement": "",
"device_class": "power_factor",
"value": str(round(load_avg[1], 1)),
"icon": "mdi:speedometer",
"attributes": {
"friendly_name": "System Load (5m)"
}
},
{
"name": "Load Average (1m)",
"sensor_id": "load_1m",
"state_class": "measurement",
"unit_of_measurement": "",
"device_class": "power_factor",
"value": str(round(load_avg[0], 1)),
"icon": "mdi:speedometer",
"attributes": {
"friendly_name": "System Load (1m)"
}
},
{
"name": "Memory Free",
"sensor_id": "memory_free",
"state_class": "measurement",
"unit_of_measurement": to_size(memory.available)[1],
"device_class": "data_size",
"value": str(to_size(memory.available)[0]),
"icon": "mdi:memory",
"attributes": {
"friendly_name": "Available Memory"
}
},
{
"name": "Memory Used",
"sensor_id": "memory_used",
"state_class": "measurement",
"unit_of_measurement": to_size(memory.used)[1],
"device_class": "data_size",
"value": str(to_size(memory.used)[0]),
"icon": "mdi:memory",
"attributes": {
"friendly_name": "Used Memory"
}
},
{
"name": "Memory Usage",
"sensor_id": "memory_usage",
"state_class": "measurement",
"unit_of_measurement": "%",
"device_class": "power_factor",
"value": str(memory.percent),
"icon": "mdi:chart-line",
"attributes": {
"friendly_name": "Memory Usage"
}
},
{
"name": "CPU Usage",
"sensor_id": "cpu_usage",
"state_class": "measurement",
"unit_of_measurement": "%",
"device_class": "power_factor",
"value": str(cpu_percent),
"icon": "mdi:cpu-64-bit",
"attributes": {
"friendly_name": "CPU Usage"
}
},
{
"name": "Swap Free",
"sensor_id": "swap_free",
"state_class": "measurement",
"unit_of_measurement": to_size(swap.free)[1],
"device_class": "data_size",
"value": str(to_size(swap.free)[0]),
"icon": "mdi:harddisk",
"attributes": {
"friendly_name": "Free Swap"
}
},
{
"name": "Swap Used",
"sensor_id": "swap_used",
"state_class": "measurement",
"unit_of_measurement": to_size(swap.used)[1],
"device_class": "data_size",
"value": str(to_size(swap.used)[0]),
"icon": "mdi:harddisk",
"attributes": {
"friendly_name": "Used Swap"
}
},
{
"name": "Swap Usage",
"sensor_id": "swap_usage",
"state_class": "measurement",
"unit_of_measurement": "%",
"device_class": "power_factor",
"value": str(swap.percent),
"icon": "mdi:chart-line",
"attributes": {
"friendly_name": "Swap Usage"
}
}
]
}
if __name__ == "__main__":
# Example usage
metrics = collect_metrics()
print(metrics)

178
collectors/zfs_pools.py Normal file
View File

@@ -0,0 +1,178 @@
#!/usr/bin/env python3
import subprocess
from typing import Dict, Any, List
import json
import shutil
import os
# Default update interval in seconds
DEFAULT_INTERVAL = 300 # 5 minutes
def get_zfs_pools() -> List[Dict[str, Any]]:
"""Get information about ZFS pools."""
try:
# Get list of pools
pools = subprocess.check_output(['zpool', 'list', '-H', '-o', 'name,size,alloc,free,health,readonly,dedup,altroot']).decode().strip().split('\n')
pool_info = []
for pool in pools:
if not pool: # Skip empty lines
continue
name, size, alloc, free, health, readonly, dedup, altroot = pool.split('\t')
# Get detailed pool status
status = subprocess.check_output(['zpool', 'status', name]).decode()
# Get pool properties
properties = subprocess.check_output(['zpool', 'get', 'all', name]).decode()
pool_info.append({
'name': name,
'size': size,
'allocated': alloc,
'free': free,
'health': health,
'readonly': readonly == 'on',
'dedup': dedup,
'altroot': altroot,
'status': status,
'properties': properties
})
return pool_info
except subprocess.SubprocessError as e:
print(f"Error getting ZFS pool information: {e}")
return []
def convert_size_to_bytes(size_str: str) -> int:
"""Convert ZFS size string to bytes."""
units = {
'B': 1,
'K': 1024,
'M': 1024**2,
'G': 1024**3,
'T': 1024**4,
'P': 1024**5
}
try:
number = float(size_str[:-1])
unit = size_str[-1].upper()
return int(number * units[unit])
except (ValueError, KeyError):
return 0
def collect_metrics() -> Dict[str, Any]:
"""Collect ZFS pool metrics. Skips cleanly if ZFS is unavailable."""
metrics = {"entities": []}
# Check binary availability
zpool_path = shutil.which('zpool')
zfs_path = shutil.which('zfs')
if not zpool_path or not zfs_path:
# Skip gracefully when binaries are missing
return {"entities": []}
# Check device node (required for libzfs operations)
if not os.path.exists('/dev/zfs'):
# Skip if device not present in container/host
return {"entities": []}
pools = get_zfs_pools()
def fmt_size(bytes_value: int):
tb = 1024**4
gb = 1024**3
if bytes_value >= tb:
return round(bytes_value / tb, 2), 'TB'
return round(bytes_value / gb, 2), 'GB'
for pool in pools:
# Pool health status
metrics['entities'].append({
'sensor_id': f'zfs_pool_{pool["name"]}_health',
'name': f'ZFS Pool {pool["name"]} Health',
'value': pool['health'],
'state_class': 'measurement',
'unit_of_measurement': '',
'device_class': 'enum',
'icon': 'mdi:database-check',
'attributes': {
'friendly_name': f'ZFS Pool {pool["name"]} Health Status',
'readonly': pool['readonly'],
'dedup': pool['dedup'],
'altroot': pool['altroot']
}
})
# Pool size
size_bytes = convert_size_to_bytes(pool['size'])
size_value, size_unit = fmt_size(size_bytes)
metrics['entities'].append({
'sensor_id': f'zfs_pool_{pool["name"]}_size',
'name': f'ZFS Pool {pool["name"]} Size',
'value': str(size_value),
'state_class': 'measurement',
'unit_of_measurement': size_unit,
'device_class': 'data_size',
'icon': 'mdi:database',
'attributes': {
'friendly_name': f'ZFS Pool {pool["name"]} Total Size'
}
})
# Pool allocated space
alloc_bytes = convert_size_to_bytes(pool['allocated'])
alloc_value, alloc_unit = fmt_size(alloc_bytes)
metrics['entities'].append({
'sensor_id': f'zfs_pool_{pool["name"]}_allocated',
'name': f'ZFS Pool {pool["name"]} Allocated',
'value': str(alloc_value),
'state_class': 'measurement',
'unit_of_measurement': alloc_unit,
'device_class': 'data_size',
'icon': 'mdi:database-minus',
'attributes': {
'friendly_name': f'ZFS Pool {pool["name"]} Allocated Space'
}
})
# Pool free space
free_bytes = convert_size_to_bytes(pool['free'])
free_value, free_unit = fmt_size(free_bytes)
metrics['entities'].append({
'sensor_id': f'zfs_pool_{pool["name"]}_free',
'name': f'ZFS Pool {pool["name"]} Free',
'value': str(free_value),
'state_class': 'measurement',
'unit_of_measurement': free_unit,
'device_class': 'data_size',
'icon': 'mdi:database-plus',
'attributes': {
'friendly_name': f'ZFS Pool {pool["name"]} Free Space'
}
})
# Pool usage percentage
usage_percent = (alloc_bytes / size_bytes * 100) if size_bytes > 0 else 0
metrics['entities'].append({
'sensor_id': f'zfs_pool_{pool["name"]}_usage',
'name': f'ZFS Pool {pool["name"]} Usage',
'value': str(round(usage_percent, 1)),
'state_class': 'measurement',
'unit_of_measurement': '%',
'device_class': 'power_factor',
'icon': 'mdi:chart-donut',
'attributes': {
'friendly_name': f'ZFS Pool {pool["name"]} Usage Percentage'
}
})
return metrics
if __name__ == "__main__":
# Example usage
metrics = collect_metrics()
print(json.dumps(metrics, indent=2))