fix(hands): хвост логов 2000 строк по замеру, образец чистит общий redact
This commit is contained in:
1 parent
dac34e505b
commit
b6c5050d69
2 files changed
+33
-25
No files matched your search
@@ -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
@@ -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
|
||||||
Reference in new issue
Block a user