Files
headquarter/apps/api/src/services/lifecycle_hooks.py
T
alex 0591b00ded refactor: reorganize models into subpackages
Move models into domain subpackages (max 4 files each):
- models/tool/ — tool_type, tool_instance, tool_definition_manifest
- models/config/ — config_profile
- models/user/ — user, user_config, ssh_key
- models/project/ — project, git_repository, workspace
- models/system/ — health_check, notification, instance_event, terminal_session

models/__init__.py continues to re-export all symbols, so consumers
using 'from src.models import X' are unaffected. Updated direct file
imports across the backend to use the new paths.

Quality gates: py_compile passed, ruff passed.
2026-06-04 10:00:10 +02:00

163 lines
5.0 KiB
Python

"""Lifecycle hook helpers for instrumenting tool instance transitions."""
import logging
import uuid
from datetime import datetime, timezone
from sqlalchemy.ext.asyncio import AsyncSession
from src.models import InstanceEvent
from src.models import ToolInstance
from src.services.correlation import get_correlation_id
from src.services.event_bus import InstanceEventBus, InstanceEventPayload
from src.services.notification_service import notification_service
logger = logging.getLogger(__name__)
def _derive_title(event_type: str) -> str:
"""Map lifecycle event type to a human-readable notification title."""
mapping = {
"instance.created": "Container created",
"instance.started": "Container started",
"instance.stopped": "Container stopped",
"instance.restarted": "Container restarted",
"instance.deleted": "Container deleted",
"instance.error": "Container error",
"instance.health_changed": "Container ready",
}
return mapping.get(
event_type,
event_type.replace("instance.", "").replace("_", " ").title(),
)
def _should_notify(event_type: str, status: str | None) -> bool:
"""Determine whether a lifecycle event should generate a notification.
Only warnings, errors, and "container is ready" (health_changed running)
are sent to users.
"""
if event_type == "instance.error":
return True
if event_type == "instance.health_changed" and status == "running":
return True
# Filter out: created, started, stopped, restarted, deleted, and any
# health_changed that is not "running" (unhealthy is handled by health_monitor)
return False
def _build_payload(
event_type: str,
instance: ToolInstance,
status: str | None = None,
message: str | None = None,
metadata: dict | None = None,
) -> InstanceEventPayload:
"""Construct a standard event payload."""
return {
"event": event_type,
"instance_id": str(instance.id),
"status": status or instance.status,
"message": message,
"metadata": metadata or {},
"timestamp": datetime.now(timezone.utc).isoformat(),
"correlation_id": get_correlation_id(),
}
async def _write_audit_row(
session: AsyncSession,
instance: ToolInstance,
event_type: str,
created_by: uuid.UUID | None = None,
status: str | None = None,
message: str | None = None,
metadata: dict | None = None,
) -> InstanceEvent:
"""Persist an instance_events audit row."""
row = InstanceEvent(
instance_id=instance.id,
event_type=event_type.replace("instance.", ""),
status=status or instance.status,
message=message,
created_by=created_by,
event_metadata=metadata or {},
)
session.add(row)
await session.commit()
return row
async def publish_lifecycle_event(
event_bus: InstanceEventBus,
session: AsyncSession,
instance: ToolInstance,
event_type: str,
created_by: uuid.UUID | None = None,
status: str | None = None,
message: str | None = None,
metadata: dict | None = None,
) -> None:
"""Publish a lifecycle event and write an audit row after DB commit.
Args:
event_bus: The global event bus.
session: Active async DB session.
instance: The affected tool instance.
event_type: One of instance.created, instance.started, etc.
created_by: User ID for user-initiated actions; None for system.
status: Optional status override.
message: Optional human-readable message.
metadata: Optional extra metadata.
"""
payload = _build_payload(
event_type=event_type,
instance=instance,
status=status,
message=message,
metadata=metadata,
)
# Write audit row
await _write_audit_row(
session=session,
instance=instance,
event_type=event_type,
created_by=created_by,
status=status or instance.status,
message=message,
metadata=metadata,
)
# Publish to bus
await event_bus.publish(event_type, payload)
# Create notification for instance owner (fire-and-forget)
# Only send warnings, errors, and "container is ready" notifications.
effective_status = status or instance.status
if not _should_notify(event_type, effective_status):
return
severity = "error" if event_type == "instance.error" else "success"
title = _derive_title(event_type)
try:
await notification_service.create_notification(
session=session,
user_id=instance.owner_id,
category="instance",
severity=severity,
title=title,
message=message,
source_type="tool_instances",
source_id=instance.id,
metadata=metadata,
)
except Exception:
logger.exception(
"Failed to create notification for lifecycle event %s",
event_type,
extra={"correlation_id": payload.get("correlation_id", "unknown")},
)