feat(logs): раздел «Логи» — хвост и свой grep (#16)
CI / ruff · mypy · pytest (pull_request) Successful in 9m40s
CI / ruff · mypy · pytest (pull_request) Successful in 9m40s
- app/logs/grep.py: подмножество grep (шаблон, -i -v -E -c -C N), разбор shlex без исполнения, BRE → regex, таймаут на строку и бюджет на поиск, лимиты 200 символов / 500 строк вывода - app/logs/journal.py: каждый поиск в log_queries (L-6) - бот: выбор контейнера, хвост 50/200 строк или 5 мин/1 ч/24 ч, свой grep по 24 ч, длинный результат — файлом .log (L-5) - логи Docker: не больше 50 000 строк и 5 МБ, сохраняются новейшие - fix: кнопки меню и /menu, /help больше не проглатываются шагами ввода (имя файла, содержимое, сообщение коммита, grep) — фильтр free_input Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+21
-18
@@ -171,7 +171,7 @@ class DockerClient:
|
||||
def __init__(self, settings: DockerSettings, transport: httpx.AsyncBaseTransport | None = None) -> None:
|
||||
self._s = settings
|
||||
self._http = httpx.AsyncClient(base_url=settings.url, timeout=settings.timeout, transport=transport)
|
||||
self._known: set[str] = set() # ids from the last listing: the only ones card/logs/stats may touch
|
||||
self._known: dict[str, Container] = {} # the last listing: the only ids card/logs/stats may touch
|
||||
|
||||
async def __aenter__(self) -> Self:
|
||||
return self
|
||||
@@ -204,22 +204,22 @@ class DockerClient:
|
||||
labels=labels,
|
||||
)
|
||||
)
|
||||
self._known = {c.id for c in result}
|
||||
self._known = {c.id: c for c in result}
|
||||
return sorted(result, key=lambda c: c.name)
|
||||
|
||||
async def resolve(self, short_id: str) -> str:
|
||||
"""Full id of a managed container by its 12-char prefix (callback data carries only that)."""
|
||||
if not self._known:
|
||||
await self.containers()
|
||||
for full in self._known:
|
||||
if full.startswith(short_id):
|
||||
return full
|
||||
await self.containers() # maybe recreated since the last listing
|
||||
for full in self._known:
|
||||
if full.startswith(short_id):
|
||||
return full
|
||||
async def get(self, short_id: str) -> Container:
|
||||
"""A managed container by its 12-char id prefix (callback data carries only that)."""
|
||||
for attempt in range(2):
|
||||
if attempt or not self._known:
|
||||
await self.containers() # first use, or recreated since the last listing
|
||||
for full, container in self._known.items():
|
||||
if short_id and full.startswith(short_id):
|
||||
return container
|
||||
raise DockerError(f"container {short_id} is not managed by tgit")
|
||||
|
||||
async def resolve(self, short_id: str) -> str:
|
||||
return (await self.get(short_id)).id
|
||||
|
||||
async def inspect(self, container_id: str) -> Details:
|
||||
self._guard(container_id)
|
||||
d = await self._get_json(f"/containers/{container_id}/json")
|
||||
@@ -246,15 +246,18 @@ class DockerClient:
|
||||
return parse_stats(await self._get_json(f"/containers/{container_id}/stats", params={"stream": "false"}))
|
||||
|
||||
async def logs(self, container_id: str, since: datetime | None = None, tail: int | None = None) -> str:
|
||||
"""stdout+stderr in order; at most max_log_lines lines and max_log_bytes — the newest ones are kept."""
|
||||
self._guard(container_id)
|
||||
params = {"stdout": "1", "stderr": "1"}
|
||||
lines = min(tail, self._s.max_log_lines) if tail is not None else self._s.max_log_lines
|
||||
params = {"stdout": "1", "stderr": "1", "tail": str(lines)}
|
||||
if since is not None:
|
||||
params["since"] = str(int(since.timestamp()))
|
||||
if tail is not None:
|
||||
params["tail"] = str(tail)
|
||||
response = await self._request(f"/containers/{container_id}/logs", params)
|
||||
raw = response.content[: self._s.max_log_bytes]
|
||||
return demux_logs(raw)
|
||||
text = demux_logs(response.content)
|
||||
if len(text) > self._s.max_log_bytes:
|
||||
cut = text[-self._s.max_log_bytes :]
|
||||
text = cut[cut.find("\n") + 1 :] # drop the torn first line
|
||||
return text
|
||||
|
||||
async def ping(self) -> bool:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user