182 lines
No EOL
6.4 KiB
Python
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() |