feat(hands,jobs): длящаяся беда не пропадает - суточная сводка и всплеск против своей нормы

This commit is contained in:
hh committed 2026-09-20 21:14:31 +00:00
1 parent 69e427469a
commit d89e24ae09
4 files changed
+492 -24

No files matched your search

+190
View File
@@ -8,7 +8,9 @@ from beaver_agent.hands.komodo_logs import (
StackScan,
Sweep,
brief,
daily,
digest,
grouped,
read_stack,
)
from beaver_agent.jobs.logs import sweep as sweep_fleet
@@ -467,3 +469,191 @@ def test_a_bare_blob_stays_readable():
)
(hit,) = scan.hits
assert "kJ8sdfLKJ23kjhsdf98234x" in hit.sample
def stamps(day, hour, count, start=0):
return [
f"2026-09-{day:02d}T{hour:02d}:{m:02d}:00.0"
for m in range(start, start + count)
]
def burst(stack, times, text="ERROR всё та же беда", service="svc"):
"""Хвост стека, где одна и та же беда повторилась в перечисленные минуты."""
return read_stack(stack, log(*(line(service, t, text) for t in times)))
def at(day, hour, minute=0):
return datetime(2026, 9, day, hour, minute, tzinfo=UTC)
def told(state, report):
state.mark_told(report)
return report
def test_a_continuing_trouble_is_told_once_and_then_keeps_quiet(tmp_path):
state = seen(tmp_path)
first = told(
state, digest(state, swept([burst("gitea", stamps(20, 9, 3))], at=at(20, 10)))
)
assert len(first.new) == 1
# Хвост перечитан: те же строки плюс свежие - беда идёт, но новостью не стала.
later = digest(
state,
swept([burst("gitea", stamps(20, 9, 3) + stamps(20, 12, 4))], at=at(20, 13)),
)
assert later.new == () and later.surge == () and later.quiet == ()
assert not later.worth_telling
assert later.ongoing == () # за прошлые сутки её ещё не было
def test_the_reread_tail_does_not_inflate_the_count(tmp_path):
state = seen(tmp_path)
told(state, digest(state, swept([burst("gitea", stamps(20, 9, 3))], at=at(20, 10))))
for hour in (13, 16, 19):
digest(state, swept([burst("gitea", stamps(20, 9, 3))], at=at(20, hour)))
(entry,) = state.entries.values()
assert entry.count == 3 # три повтора, а не три на каждый из четырёх проходов
assert entry.passes == 1
def test_a_continuing_trouble_comes_back_in_the_daily_summary(tmp_path):
state = seen(tmp_path)
told(state, digest(state, swept([burst("gitea", stamps(20, 9, 5))], at=at(20, 10))))
# Первый проход следующих суток - 01:40, он же несёт сводку.
night = digest(
state,
swept([burst("gitea", stamps(20, 9, 5) + stamps(21, 1, 2))], at=at(21, 1, 40)),
)
assert night.summary and len(night.ongoing) == 1
assert night.new == () and not night.worth_telling
text = daily(night, UTC)
assert "длящееся, сводка за 2026-09-21" in text
assert "gitea/svc ошибка, с 20.09 09:00, за сутки 5, первые сутки" in text
assert "ERROR" not in text # образцов строк в сводке нет намеренно
told(state, night)
state.mark_daily(night)
# Тот же день, следующий проход: сводку уже отдали.
again = digest(
state,
swept([burst("gitea", stamps(20, 9, 5) + stamps(21, 1, 2))], at=at(21, 4, 40)),
)
assert again.ongoing and not again.summary
def test_the_summary_says_which_way_it_is_going(tmp_path):
state = seen(tmp_path)
told(state, digest(state, swept([burst("gitea", stamps(20, 9, 2))], at=at(20, 10))))
day_two = swept(
[burst("gitea", stamps(20, 9, 2) + stamps(21, 9, 8))], at=at(21, 10)
)
told(state, digest(state, day_two))
third = digest(
state,
swept(
[burst("gitea", stamps(20, 9, 2) + stamps(21, 9, 8) + stamps(22, 1, 1))],
at=at(22, 1, 40),
),
)
assert "за сутки 8, растёт (было 2)" in daily(third, UTC)
def test_a_surge_of_the_familiar_breaks_the_silence(tmp_path):
state = seen(tmp_path)
state_report = digest(
state, swept([burst("gitea", stamps(20, 9, 1))], at=at(20, 10))
)
told(state, state_report)
for hour, minute in ((13, 12), (16, 15)):
digest(
state,
swept([burst("gitea", stamps(20, hour - 1, 1, minute))], at=at(20, hour)),
)
(entry,) = state.entries.values()
assert entry.passes == 3 and entry.norm == 1.0
spike = digest(state, swept([burst("gitea", stamps(20, 18, 12))], at=at(20, 19)))
(item,) = spike.surge
assert item.gain == 12 and item.norm == 1.0
assert spike.new == () and spike.worth_telling
text = brief(spike)
assert "Всплеск знакомого (1)" in text
assert "×12 за проход при норме ×1.0" in text
assert "Нового в этой области нет" not in text
# Второй такой же проход внутри выдержки молчит: это уже не новость.
told(state, spike)
more = digest(state, swept([burst("gitea", stamps(20, 21, 12))], at=at(20, 22)))
assert more.surge == () and not more.worth_telling
def test_a_small_jump_is_not_a_surge(tmp_path):
state = seen(tmp_path)
told(state, digest(state, swept([burst("gitea", stamps(20, 9, 1))], at=at(20, 10))))
for hour in (13, 16):
digest(
state,
swept([burst("gitea", stamps(20, hour - 1, 1, hour))], at=at(20, hour)),
)
# Вчетверо больше, но всего четыре штуки: пол в SURGE_MIN держит тишину.
small = digest(state, swept([burst("gitea", stamps(20, 18, 4))], at=at(20, 19)))
assert small.surge == () and not small.worth_telling
def test_a_trouble_gone_silent_is_reported_even_while_its_line_lingers(tmp_path):
state = seen(tmp_path)
tail = burst("gitea", stamps(20, 9, 2))
told(state, digest(state, swept([tail], at=at(20, 10))))
# Сутки та же строка лежит в хвосте, новых событий нет.
assert not digest(state, swept([tail], at=at(20, 22))).quiet
stopped = digest(state, swept([tail], at=at(21, 12)))
(gone,) = stopped.quiet
assert gone.last == at(20, 9, 1)
assert "последнее в 20.09 09:01" in brief(stopped)
told(state, stopped)
assert digest(state, swept([tail], at=at(21, 16))).quiet == ()
def test_a_trouble_that_returns_after_the_all_clear_is_new_again(tmp_path):
state = seen(tmp_path)
tail = burst("gitea", stamps(20, 9, 2))
told(state, digest(state, swept([tail], at=at(20, 10))))
told(state, digest(state, swept([tail], at=at(21, 12)))) # стихла
back = digest(
state,
swept([burst("gitea", stamps(20, 9, 2) + stamps(21, 18, 3))], at=at(21, 19)),
)
assert len(back.new) == 1 and back.worth_telling
def test_the_background_of_the_baseline_never_claims_to_have_stopped(tmp_path):
# Про фон, принятый в базовую линию, мастер не слышал - «стихло» про него врёт.
state = seen(tmp_path, known=())
state.fresh = False
cold = digest(state, swept([burst("lodge", stamps(20, 9, 2))], at=at(20, 10)))
assert cold.new == () and cold.baseline == 1
gone = digest(state, swept([read_stack("lodge", log())], at=at(20, 13)))
assert gone.quiet == () and not gone.worth_telling
def test_the_summary_folds_the_variants_of_one_service_into_one_row(tmp_path):
state = seen(tmp_path)
# Разные по смыслу строки: отличие в одних цифрах - это одна и та же беда.
variants = [
burst("gitea", stamps(20, 9, 4), text=f"ERROR {what}")
for what in ("push отклонён", "миграция не прошла", "ssh-ключ отвергнут")
]
told(state, digest(state, swept(variants, at=at(20, 10))))
night = digest(state, swept(variants, at=at(21, 1, 40)))
(row,) = grouped(night.ongoing)
assert row.kinds == 3 and row.day_before == 12
text = daily(night, UTC)
assert "Идёт до сих пор: 1 рядов (3 разных строк)" in text
assert "gitea/svc ошибка, строк 3 видов, с 20.09 09:00, за сутки 12" in text