feat(hands,jobs): длящаяся беда не пропадает - суточная сводка и всплеск против своей нормы
This commit is contained in:
1 parent
69e427469a
commit
d89e24ae09
4 files changed
+492
-24
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user