fix(hands): хвост логов 2000 строк по замеру, образец чистит общий redact

This commit is contained in:
hh committed 2026-09-20 18:30:49 +00:00
1 parent 47df446462
commit 69e427469a
2 files changed
+33 -25

No files matched your search

+18 -14
View File
@@ -6,6 +6,11 @@
и обратный переход «было → нет» - с временем последнего события, не с временем
прохода (логи берутся с `--timestamps`, время в строке настоящее).
Показанную строку лога чистит общий `security.redact` - тот же, что стоит на
логах и на выдаче MCP: правила в одном месте, и он знает литеральные значения
секретов из env. Голые блобы без имени рядом он не трогает намеренно - id
задач, сессий и коммитов это ровно то, по чему беду потом и опознают.
Показание всегда несёт свою область: сколько стеков в области, сколько из них
прочиталось, какой отрезок логов попал в хвост. Без этого «всё чисто»
неотличимо от «половину не посмотрел», поэтому чего не смог - тоже в инжекте.
@@ -20,6 +25,8 @@ from dataclasses import dataclass, field
from datetime import UTC, datetime, timedelta, tzinfo
from typing import TYPE_CHECKING, Any
from beaver_gateway.security.redact import redact
from beaver_agent.hands.komodo import ANSI
if TYPE_CHECKING:
@@ -43,9 +50,16 @@ SCOPE = ("dell-home", "personal-main")
"""Свои серверы. Машины заказчиков (md-cars-*, veris-*, ms-agents-*) не наши:
там чужой прод и чужие секреты, и читать их логи в свой контекст - нельзя."""
TAIL = 500
"""Строк на стек. Болтливый стек столько строк проживает за минуты - об этом
в инжекте говорится отдельно, а не выдаётся за просмотренный промежуток."""
TAIL = 2000
"""Строк на контейнер, и число из замера, а не «чтобы хватало».
Замер 20.09.2026 по всем 36 стекам области: при 500 строках хвост покрывал
меньше трёх часов (промежуток между проходами) ровно у одного стека -
`dell-home-music`, ~1170 строк/ч, то есть 51 минута; следующий по болтливости
`personal-main-caddy` покрывал 11,6 часа, `beaver-agent` - 20 ч, медиана по
области около 110 часов. 2000 строк закрывают и music (~3,4 часа) почти
бесплатно: тихие стеки отдают столько, сколько у них есть, растут три-четыре
болтливых. Где хвост всё равно короче промежутка, инжект говорит об этом сам."""
SIGNALS: tuple[tuple[str, str], ...] = (
("критично", r"\bCRITICAL\b|\bFATAL\b|\bEMERG\b"),
@@ -72,11 +86,6 @@ STAMP_RE = re.compile(
"""Пустая строка тоже со штампом (`сервис | время` и ничего) - это не мусор."""
DIGITS_RE = re.compile(r"\b[0-9a-f]{7,}\b|\d+")
"""Числа и хеши в строке - шум: без них «то же самое» узнаётся между проходами."""
SECRET_RE = re.compile(
r"(?i)\b(?:token|secret|api[-_ ]?key|password|passwd|authorization)\b\s*[=:]\s*\S+"
r"|\b[A-Za-z0-9_\-]{32,}\b"
)
"""Инжект уезжает в чат и в vault: похожее на ключ в показании не нужно."""
SAMPLE_CHARS = 180
MAX_NEW = 25
@@ -314,11 +323,6 @@ def _parse(raw: Any) -> datetime | None:
return at if at.tzinfo else at.replace(tzinfo=UTC)
def _mask(text: str) -> str:
"""Замазать похожее на секрет: строка лога едет мастеру как есть."""
return SECRET_RE.sub("<скрыто>", text)
def _normal(text: str) -> str:
"""Строка без чисел, хешей и адресов: по ней узнаётся «та же самая беда»."""
return DIGITS_RE.sub("#", ANSI.sub("", text)).strip()
@@ -374,7 +378,7 @@ def read_stack(stack: str, log: dict[str, Any]) -> StackScan:
stack=stack,
service=service,
signal=signal,
sample=(before.sample if before else _mask(rest)[:SAMPLE_CHARS]),
sample=(before.sample if before else redact(rest)[:SAMPLE_CHARS]),
at=at or (before.at if before else None),
count=(before.count if before else 0) + 1,
)