Ver código fonte

feat(runtime): add system resource metrics to /server_info endpoint (#5207)

Co-authored-by: openhands <openhands@all-hands.dev>
Xingyao Wang 1 ano atrás
pai
commit
bb8b4a0b18

+ 7 - 1
openhands/runtime/action_execution_server.py

@@ -52,6 +52,7 @@ from openhands.runtime.utils.bash import BashSession
 from openhands.runtime.utils.files import insert_lines, read_lines
 from openhands.runtime.utils.runtime_init import init_user_and_working_directory
 from openhands.runtime.utils.system import check_port_available
+from openhands.runtime.utils.system_stats import get_system_stats
 from openhands.utils.async_utils import call_sync_from_async, wait_all
 
 
@@ -420,7 +421,12 @@ if __name__ == '__main__':
         current_time = time.time()
         uptime = current_time - client.start_time
         idle_time = current_time - client.last_execution_time
-        return {'uptime': uptime, 'idle_time': idle_time}
+
+        return {
+            'uptime': uptime,
+            'idle_time': idle_time,
+            'resources': get_system_stats(),
+        }
 
     @app.post('/execute_action')
     async def execute_action(action_request: ActionRequest):

+ 62 - 0
openhands/runtime/utils/system_stats.py

@@ -0,0 +1,62 @@
+"""Utilities for getting system resource statistics."""
+
+import time
+
+import psutil
+
+
+def get_system_stats() -> dict:
+    """Get current system resource statistics.
+
+    Returns:
+        dict: A dictionary containing:
+            - cpu_percent: CPU usage percentage for the current process
+            - memory: Memory usage stats (rss, vms, percent)
+            - disk: Disk usage stats (total, used, free, percent)
+            - io: I/O statistics (read/write bytes)
+    """
+    process = psutil.Process()
+    # Get initial CPU percentage (this will return 0.0)
+    process.cpu_percent()
+    # Wait a bit and get the actual CPU percentage
+    time.sleep(0.1)
+
+    with process.oneshot():
+        cpu_percent = process.cpu_percent()
+        memory_info = process.memory_info()
+        memory_percent = process.memory_percent()
+
+    disk_usage = psutil.disk_usage('/')
+
+    # Get I/O stats directly from /proc/[pid]/io to avoid psutil's field name assumptions
+    try:
+        with open(f'/proc/{process.pid}/io', 'rb') as f:
+            io_stats = {}
+            for line in f:
+                if line:
+                    try:
+                        name, value = line.strip().split(b': ')
+                        io_stats[name.decode('ascii')] = int(value)
+                    except (ValueError, UnicodeDecodeError):
+                        continue
+    except (FileNotFoundError, PermissionError):
+        io_stats = {'read_bytes': 0, 'write_bytes': 0}
+
+    return {
+        'cpu_percent': cpu_percent,
+        'memory': {
+            'rss': memory_info.rss,
+            'vms': memory_info.vms,
+            'percent': memory_percent,
+        },
+        'disk': {
+            'total': disk_usage.total,
+            'used': disk_usage.used,
+            'free': disk_usage.free,
+            'percent': disk_usage.percent,
+        },
+        'io': {
+            'read_bytes': io_stats.get('read_bytes', 0),
+            'write_bytes': io_stats.get('write_bytes', 0),
+        },
+    }

+ 60 - 0
tests/runtime/utils/test_system_stats.py

@@ -0,0 +1,60 @@
+"""Tests for system stats utilities."""
+
+import psutil
+
+from openhands.runtime.utils.system_stats import get_system_stats
+
+
+def test_get_system_stats():
+    """Test that get_system_stats returns valid system statistics."""
+    stats = get_system_stats()
+
+    # Test structure
+    assert isinstance(stats, dict)
+    assert set(stats.keys()) == {'cpu_percent', 'memory', 'disk', 'io'}
+
+    # Test CPU stats
+    assert isinstance(stats['cpu_percent'], float)
+    assert 0 <= stats['cpu_percent'] <= 100 * psutil.cpu_count()
+
+    # Test memory stats
+    assert isinstance(stats['memory'], dict)
+    assert set(stats['memory'].keys()) == {'rss', 'vms', 'percent'}
+    assert isinstance(stats['memory']['rss'], int)
+    assert isinstance(stats['memory']['vms'], int)
+    assert isinstance(stats['memory']['percent'], float)
+    assert stats['memory']['rss'] > 0
+    assert stats['memory']['vms'] > 0
+    assert 0 <= stats['memory']['percent'] <= 100
+
+    # Test disk stats
+    assert isinstance(stats['disk'], dict)
+    assert set(stats['disk'].keys()) == {'total', 'used', 'free', 'percent'}
+    assert isinstance(stats['disk']['total'], int)
+    assert isinstance(stats['disk']['used'], int)
+    assert isinstance(stats['disk']['free'], int)
+    assert isinstance(stats['disk']['percent'], float)
+    assert stats['disk']['total'] > 0
+    assert stats['disk']['used'] >= 0
+    assert stats['disk']['free'] >= 0
+    assert 0 <= stats['disk']['percent'] <= 100
+    # Verify that used + free is less than or equal to total
+    # (might not be exactly equal due to filesystem overhead)
+    assert stats['disk']['used'] + stats['disk']['free'] <= stats['disk']['total']
+
+    # Test I/O stats
+    assert isinstance(stats['io'], dict)
+    assert set(stats['io'].keys()) == {'read_bytes', 'write_bytes'}
+    assert isinstance(stats['io']['read_bytes'], int)
+    assert isinstance(stats['io']['write_bytes'], int)
+    assert stats['io']['read_bytes'] >= 0
+    assert stats['io']['write_bytes'] >= 0
+
+
+def test_get_system_stats_stability():
+    """Test that get_system_stats can be called multiple times without errors."""
+    # Call multiple times to ensure stability
+    for _ in range(3):
+        stats = get_system_stats()
+        assert isinstance(stats, dict)
+        assert stats['cpu_percent'] >= 0