feat(storage): group usage by application and runtimes

This commit is contained in:
Hyu
2026-09-03 00:11:05 +08:00
parent 380bdfeca6
commit be0214cf4d
8 changed files with 543 additions and 41 deletions
+109 -2
View File
@@ -117,8 +117,6 @@ class MaintenanceService:
('logs', Path('data/logs')),
('storage', Path('data/storage')),
('vector_store', Path('data/chroma')),
('plugins', Path('data/plugins')),
('mcp', Path('data/mcp')),
('temp', Path('data/temp')),
]
else:
@@ -126,6 +124,11 @@ class MaintenanceService:
roots = [('storage', scoped_storage_path)]
sections = await asyncio.to_thread(self._collect_sections, roots)
runtime_processes = await self._runtime_storage_processes(
context,
core_sections=sections,
include_instance_storage=is_oss_singleton,
)
monitoring_counts = await self._monitoring_counts(context)
binary_storage = await self._binary_storage_stats(context)
@@ -146,6 +149,12 @@ class MaintenanceService:
'log_retention_days': log_retention_days,
},
'sections': sections,
'processes': runtime_processes,
'total_size_bytes': sum(
int(process.get('size_bytes') or 0)
for process in runtime_processes
if process.get('status') == 'available'
),
'database': {
'type': database_type,
'monitoring_counts': monitoring_counts,
@@ -158,6 +167,104 @@ class MaintenanceService:
'tasks': self.ap.task_mgr.get_stats() if is_oss_singleton and self.ap.task_mgr else {},
}
async def _runtime_storage_processes(
self,
context: TenantContext,
*,
core_sections: list[dict[str, Any]],
include_instance_storage: bool,
) -> list[dict[str, Any]]:
core_process = {
'key': 'langbot',
'status': 'available',
'source': 'local_process',
'size_bytes': sum(int(item.get('size_bytes') or 0) for item in core_sections),
'directories': [
{
**item,
'kind': 'root',
'error_count': 0,
}
for item in core_sections
],
}
plugin_task = self._plugin_runtime_storage_process(include_instance_storage)
box_task = self._box_runtime_storage_process(context)
plugin_process, box_process = await asyncio.gather(plugin_task, box_task)
return [core_process, plugin_process, box_process]
async def _plugin_runtime_storage_process(self, include_instance_storage: bool) -> dict[str, Any]:
if not include_instance_storage:
return self._unavailable_process(
'plugin_runtime',
'not_applicable',
'Instance-wide Plugin Runtime storage is hidden in multi-Workspace deployments',
)
connector = getattr(self.ap, 'plugin_connector', None)
if connector is None or not getattr(connector, 'is_enable_plugin', True):
return self._unavailable_process('plugin_runtime', 'disabled', 'Plugin Runtime is disabled')
runtime_handler = getattr(connector, 'handler', None)
get_analysis = getattr(runtime_handler, 'get_storage_analysis', None)
if not callable(get_analysis):
return self._unavailable_process(
'plugin_runtime',
'unavailable',
'Plugin Runtime does not support storage analysis',
)
try:
result = await get_analysis()
except Exception as exc:
self.ap.logger.warning(f'Failed to collect Plugin Runtime storage analysis: {exc}')
return self._unavailable_process('plugin_runtime', 'unavailable', str(exc))
return self._available_process('plugin_runtime', result, source='runtime_rpc')
async def _box_runtime_storage_process(self, context: TenantContext) -> dict[str, Any]:
box_service = getattr(self.ap, 'box_service', None)
if box_service is None or not getattr(box_service, 'enabled', True):
return self._unavailable_process('box_runtime', 'disabled', 'Box Runtime is disabled')
get_analysis = getattr(box_service, 'get_storage_analysis', None)
if not callable(get_analysis):
return self._unavailable_process(
'box_runtime',
'unavailable',
'Box Runtime does not support storage analysis',
)
try:
result = await get_analysis(context)
except Exception as exc:
self.ap.logger.warning(f'Failed to collect Box Runtime storage analysis: {exc}')
return self._unavailable_process('box_runtime', 'unavailable', str(exc))
return self._available_process('box_runtime', result, source='runtime_rpc')
@staticmethod
def _available_process(key: str, result: Any, *, source: str) -> dict[str, Any]:
payload = result if isinstance(result, dict) else {}
directories = payload.get('directories')
if not isinstance(directories, list):
directories = []
return {
'key': key,
'status': 'available',
'source': source,
'size_bytes': int(payload.get('size_bytes') or 0),
'directories': directories,
'active_sessions': payload.get('active_sessions'),
'managed_processes': payload.get('managed_processes'),
}
@staticmethod
def _unavailable_process(key: str, status: str, error: str) -> dict[str, Any]:
return {
'key': key,
'status': status,
'source': 'runtime_rpc',
'size_bytes': None,
'directories': [],
'error': error,
}
def _collect_sections(
self,
roots: list[tuple[str, Path | None]],
+10
View File
@@ -1409,6 +1409,16 @@ class BoxService:
except Exception:
return []
async def get_storage_analysis(self, context: TenantContext) -> dict:
"""Return Workspace-scoped storage measured by the Box Runtime."""
if not self._enabled:
raise BoxError('Box runtime is disabled')
if not self._available:
raise BoxError(self._connector_error or 'Box runtime is not available')
execution_context = await self._validated_execution_context(context)
return await self.client.get_storage_analysis(action_context=self._action_context(execution_context))
def build_spec(self, spec_payload: dict, skip_host_mount_validation: bool = False) -> BoxSpec:
spec_payload = dict(spec_payload)
spec_payload.setdefault('env', {})
+10
View File
@@ -2578,6 +2578,16 @@ class RuntimeConnectionHandler(handler.Handler):
timeout=10,
)
async def get_storage_analysis(self) -> dict[str, Any]:
"""Return instance-scoped filesystem usage measured by Plugin Runtime."""
with self.installation_scope(None):
return await self.call_action(
LangBotToRuntimeAction.GET_STORAGE_ANALYSIS,
{},
timeout=60,
)
async def reconcile_plugin_installations(
self,
installations: tuple[PluginInstallationDesiredState, ...],