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`, время в строке настоящее). прохода (логи берутся с `--timestamps`, время в строке настоящее).
Показанную строку лога чистит общий `security.redact` - тот же, что стоит на
логах и на выдаче MCP: правила в одном месте, и он знает литеральные значения
секретов из env. Голые блобы без имени рядом он не трогает намеренно - id
задач, сессий и коммитов это ровно то, по чему беду потом и опознают.
Показание всегда несёт свою область: сколько стеков в области, сколько из них Показание всегда несёт свою область: сколько стеков в области, сколько из них
прочиталось, какой отрезок логов попал в хвост. Без этого «всё чисто» прочиталось, какой отрезок логов попал в хвост. Без этого «всё чисто»
неотличимо от «половину не посмотрел», поэтому чего не смог - тоже в инжекте. неотличимо от «половину не посмотрел», поэтому чего не смог - тоже в инжекте.
@@ -20,6 +25,8 @@ from dataclasses import dataclass, field
from datetime import UTC, datetime, timedelta, tzinfo from datetime import UTC, datetime, timedelta, tzinfo
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from beaver_gateway.security.redact import redact
from beaver_agent.hands.komodo import ANSI from beaver_agent.hands.komodo import ANSI
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -43,9 +50,16 @@ SCOPE = ("dell-home", "personal-main")
"""Свои серверы. Машины заказчиков (md-cars-*, veris-*, ms-agents-*) не наши: """Свои серверы. Машины заказчиков (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], ...] = ( SIGNALS: tuple[tuple[str, str], ...] = (
("критично", r"\bCRITICAL\b|\bFATAL\b|\bEMERG\b"), ("критично", 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+") 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 SAMPLE_CHARS = 180
MAX_NEW = 25 MAX_NEW = 25
@@ -314,11 +323,6 @@ def _parse(raw: Any) -> datetime | None:
return at if at.tzinfo else at.replace(tzinfo=UTC) 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: def _normal(text: str) -> str:
"""Строка без чисел, хешей и адресов: по ней узнаётся «та же самая беда».""" """Строка без чисел, хешей и адресов: по ней узнаётся «та же самая беда»."""
return DIGITS_RE.sub("#", ANSI.sub("", text)).strip() return DIGITS_RE.sub("#", ANSI.sub("", text)).strip()
@@ -374,7 +378,7 @@ def read_stack(stack: str, log: dict[str, Any]) -> StackScan:
stack=stack, stack=stack,
service=service, service=service,
signal=signal, 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), at=at or (before.at if before else None),
count=(before.count if before else 0) + 1, count=(before.count if before else 0) + 1,
) )
+15 -11
View File
@@ -289,7 +289,7 @@ def test_the_scope_says_how_much_was_looked_at(tmp_path):
"Область: dell-home, personal-main - прочитано 1 стеков из 2" "Область: dell-home, personal-main - прочитано 1 стеков из 2"
) )
assert "во флоте 53" in head assert "во флоте 53" in head
assert "хвост 500 строк на стек: строки с 20.09 17:40 по 20.09 17:40" in head assert "хвост 2000 строк на стек: строки с 20.09 17:40 по 20.09 17:40" in head
assert "самый короткий хвост у beaver-agent - только с 20.09 17:40" in head assert "самый короткий хвост у beaver-agent - только с 20.09 17:40" in head
@@ -448,18 +448,22 @@ def test_a_first_pass_into_a_dead_komodo_does_not_burn_the_baseline(tmp_path):
assert later.cold and later.new == () assert later.cold and later.new == ()
def test_a_secret_in_the_line_does_not_ride_into_the_chat(): def test_a_named_secret_in_the_line_does_not_ride_into_the_chat():
scan = read_stack( scan = read_stack(
"gitea", "gitea",
log( log(line("svc", "2026-09-20T17:40:00.0", "ERROR auth failed token=ghp_ZZ123")),
line(
"svc",
"2026-09-20T17:40:00.0",
"ERROR auth failed token=ghp_ZZ123 for kJ8sdfLKJ23kjhsdf98234hkjsdf7823x",
)
),
) )
(hit,) = scan.hits (hit,) = scan.hits
assert "ghp_ZZ123" not in hit.sample assert "ghp_ZZ123" not in hit.sample
assert "kJ8sdfLKJ23kjhsdf98234hkjsdf7823x" not in hit.sample assert hit.sample.startswith("ERROR auth failed token=")
assert hit.sample.startswith("ERROR auth failed")
def test_a_bare_blob_stays_readable():
# Размен с открытыми глазами: id задач, сессий и коммитов - это сигнал,
# и замазывать их ради редкого безымянного ключа дороже, чем оставить.
scan = read_stack(
"komodo",
log(line("svc", "2026-09-20T17:40:00.0", "ERROR task kJ8sdfLKJ23kjhsdf98234x")),
)
(hit,) = scan.hits
assert "kJ8sdfLKJ23kjhsdf98234x" in hit.sample