openrag/src/tui/screens/logs.py
2025-09-03 15:31:14 -04:00

182 lines
No EOL
6.4 KiB
Python

"""Logs viewing screen for OpenRAG TUI."""
import asyncio
from textual.app import ComposeResult
from textual.containers import Container, Vertical, Horizontal
from textual.screen import Screen
from textual.widgets import Header, Footer, Static, Button, Select, TextArea
from textual.timer import Timer
from rich.text import Text
from ..managers.container_manager import ContainerManager
class LogsScreen(Screen):
"""Logs viewing and monitoring screen."""
BINDINGS = [
("escape", "back", "Back"),
("f", "follow", "Follow Logs"),
("c", "clear", "Clear"),
("r", "refresh", "Refresh"),
]
def __init__(self, initial_service: str = "openrag-backend"):
super().__init__()
self.container_manager = ContainerManager()
self.current_service = initial_service
self.logs_area = None
self.following = False
self.follow_task = None
def compose(self) -> ComposeResult:
"""Create the logs screen layout."""
yield Header()
yield Container(
Vertical(
Static("Service Logs", id="logs-title"),
Horizontal(
Static("Service:", classes="label"),
Select([
("openrag-backend", "Backend"),
("openrag-frontend", "Frontend"),
("opensearch", "OpenSearch"),
("langflow", "Langflow"),
("dashboards", "Dashboards")
], value=self.current_service, id="service-select"),
Button("Refresh", variant="default", id="refresh-btn"),
Button("Follow", variant="primary", id="follow-btn"),
Button("Clear", variant="default", id="clear-btn"),
classes="controls-row"
),
self._create_logs_area(),
Horizontal(
Button("Back", variant="default", id="back-btn"),
classes="button-row"
),
id="logs-content"
),
id="main-container"
)
yield Footer()
def _create_logs_area(self) -> TextArea:
"""Create the logs text area."""
self.logs_area = TextArea(
text="Loading logs...",
read_only=True,
show_line_numbers=False,
id="logs-area"
)
return self.logs_area
async def on_mount(self) -> None:
"""Initialize the screen when mounted."""
await self._load_logs()
def on_unmount(self) -> None:
"""Clean up when unmounting."""
self._stop_following()
def on_select_changed(self, event: Select.Changed) -> None:
"""Handle service selection change."""
if event.select.id == "service-select":
self.current_service = event.value
self._stop_following()
self.run_worker(self._load_logs())
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button presses."""
if event.button.id == "refresh-btn":
self.action_refresh()
elif event.button.id == "follow-btn":
self.action_follow()
elif event.button.id == "clear-btn":
self.action_clear()
elif event.button.id == "back-btn":
self.action_back()
async def _load_logs(self, lines: int = 200) -> None:
"""Load recent logs for the current service."""
if not self.container_manager.is_available():
self.logs_area.text = "No container runtime available"
return
success, logs = await self.container_manager.get_service_logs(self.current_service, lines)
if success:
self.logs_area.text = logs
# Scroll to bottom
self.logs_area.cursor_position = len(logs)
else:
self.logs_area.text = f"Failed to load logs: {logs}"
def _stop_following(self) -> None:
"""Stop following logs."""
self.following = False
if self.follow_task and not self.follow_task.done():
self.follow_task.cancel()
# Update button text
follow_btn = self.query_one("#follow-btn")
follow_btn.label = "Follow"
follow_btn.variant = "primary"
async def _follow_logs(self) -> None:
"""Follow logs in real-time."""
if not self.container_manager.is_available():
return
try:
async for log_line in self.container_manager.follow_service_logs(self.current_service):
if not self.following:
break
# Append new line to logs area
current_text = self.logs_area.text
new_text = current_text + "\n" + log_line
# Keep only last 1000 lines to prevent memory issues
lines = new_text.split('\n')
if len(lines) > 1000:
lines = lines[-1000:]
new_text = '\n'.join(lines)
self.logs_area.text = new_text
# Scroll to bottom
self.logs_area.cursor_position = len(new_text)
except asyncio.CancelledError:
pass
except Exception as e:
if self.following: # Only show error if we're still supposed to be following
self.notify(f"Error following logs: {e}", severity="error")
finally:
self.following = False
def action_refresh(self) -> None:
"""Refresh logs."""
self._stop_following()
self.run_worker(self._load_logs())
def action_follow(self) -> None:
"""Toggle log following."""
if self.following:
self._stop_following()
else:
self.following = True
follow_btn = self.query_one("#follow-btn")
follow_btn.label = "Stop Following"
follow_btn.variant = "error"
# Start following
self.follow_task = self.run_worker(self._follow_logs(), exclusive=False)
def action_clear(self) -> None:
"""Clear the logs area."""
self.logs_area.text = ""
def action_back(self) -> None:
"""Go back to previous screen."""
self._stop_following()
self.app.pop_screen()