cdf233378c
The API container and tool instances share the 'backend' Docker network
(connect_container_to_network at tool_instances.py:1576). cloudflared
runs INSIDE the api container, so localhost:host_port is unreachable.
The original container_name:internal_port is correct for networking.
The 'app error 0' is an application-level issue, not networking.
This reverts commit a8fbca9.
150 lines
4.5 KiB
Python
150 lines
4.5 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.instance_event import InstanceEvent
|
|
from src.models.tool_instance 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",
|
|
}
|
|
return mapping.get(
|
|
event_type,
|
|
event_type.replace("instance.", "").replace("_", " ").title(),
|
|
)
|
|
|
|
|
|
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)
|
|
# Skip intermediate "starting" notifications — only notify on terminal states
|
|
# (failed or successful attempts)
|
|
_is_starting_intermediate = event_type == "instance.started" and (
|
|
status or instance.status
|
|
) == "starting"
|
|
if _is_starting_intermediate:
|
|
return
|
|
|
|
severity = "error" if event_type == "instance.error" else "info"
|
|
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")},
|
|
)
|