первая встреча непокрытой секции стала наблюдаемым событием
- свёртка спрашивает журнал, встречалось ли имя строго раньше по паре (received_at, id), и пишет WARN с атрибутом uncovered_new; повторные молчат. Признак выводится, а не хранится — реестр был бы второй копией факта - добавлена подкоманда `healthlog uncovered`: перечень накопленного, чтение только на чтение, экранированные имена и названные границы носителя - синк документации: ADR о выводе новизны из журнала, две записи в журнал дефектов, два правила промоутом в конвенции, терминал оператора назван адресатом недоверенного входа
This commit is contained in:
+119
-4
@@ -92,6 +92,17 @@ type Stats struct {
|
||||
Uncovered []string
|
||||
// UncoveredDropped — сколько имён отброшено границей списка.
|
||||
UncoveredDropped int
|
||||
// UncoveredNew — имена непокрытых секций, которых не было ни в одной
|
||||
// доставке, стоящей в журнале раньше этой. Событие однократное за всю жизнь
|
||||
// имени: поток дописывает метрики на телефоне молча, и момент появления
|
||||
// секции наблюдать больше нечем.
|
||||
UncoveredNew []string
|
||||
// UncoveredSeenUnknown — сверка с журналом не состоялась, и потому все
|
||||
// непокрытые имена доставки объявлены новыми. Лишняя запись стоит внимания
|
||||
// один раз, промолчавшее событие не восстанавливается ничем.
|
||||
UncoveredSeenUnknown bool
|
||||
// UncoveredSeenError — почему не состоялась.
|
||||
UncoveredSeenError error
|
||||
|
||||
// Categoricals — сколько РАЗЛИЧНЫХ категориальных значений наблюдалось;
|
||||
// CategoricalUnknown — сколько из них словарь не знает;
|
||||
@@ -158,6 +169,16 @@ func (s *Service) Fold(ctx context.Context, deliveryID string) (stats Stats, err
|
||||
FallbackLayer: hae.Layer(fallback),
|
||||
Locale: localeOf(d.Headers),
|
||||
})
|
||||
|
||||
// Сверка с журналом идёт ДО ветвления на успех и отказ. Список непокрытых
|
||||
// секций переживает отказ разбора, то есть имя уже записано в учёт; смолчи
|
||||
// здесь — и следующая доставка сочтёт его виденным, а событие не вернётся
|
||||
// ничем, кроме ручного запроса в базу.
|
||||
novelty := s.novelty(ctx, parsed.Uncovered, store.DeliveryRef{
|
||||
ID: d.ID,
|
||||
ReceivedAt: d.ReceivedAt,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
// Список непокрытых секций переживает отказ: доставка, у которой не
|
||||
// определился слой, обязана остаться записью о том, что в теле есть
|
||||
@@ -166,12 +187,15 @@ func (s *Service) Fold(ctx context.Context, deliveryID string) (stats Stats, err
|
||||
// А вот число пропущенных сущностей — НЕ переживает: разбор, вернувший
|
||||
// ошибку, отдаёт нулевые счётчики по построению, а не по измерению, и
|
||||
// записать этот ноль значило бы объявить доставку проверенной.
|
||||
s.fail(ctx, deliveryID, err, residueOf(parsed))
|
||||
s.fail(ctx, deliveryID, err, residueOf(parsed, novelty))
|
||||
return stats, err
|
||||
}
|
||||
|
||||
stats.Uncovered = parsed.Uncovered
|
||||
stats.UncoveredDropped = parsed.UncoveredDropped
|
||||
stats.UncoveredNew = novelty.fresh
|
||||
stats.UncoveredSeenUnknown = novelty.unknown
|
||||
stats.UncoveredSeenError = novelty.cause
|
||||
stats.Metrics = parsed.Metrics
|
||||
stats.Points = len(parsed.Points)
|
||||
stats.SkippedNoTime = parsed.SkippedNoTime
|
||||
@@ -196,6 +220,11 @@ func (s *Service) Fold(ctx context.Context, deliveryID string) (stats Stats, err
|
||||
s.fail(ctx, deliveryID, err, parseResidue{
|
||||
uncovered: parsed.Uncovered,
|
||||
skipped: skippedEntities(parsed),
|
||||
// Новизна доезжает и сюда. Эта ветвь пишет имя в учёт ровно так же,
|
||||
// как ветвь отказа разбора, — значит и терять событие ей нельзя:
|
||||
// следующая доставка сочтёт имя виденным, а отказ слияния бывает
|
||||
// нетранзиентным (исчерпанный дедлайн свёртки под большим телом).
|
||||
novelty: novelty,
|
||||
})
|
||||
return stats, err
|
||||
}
|
||||
@@ -283,6 +312,18 @@ func (s *Service) logResult(ctx context.Context, deliveryID string, st Stats) {
|
||||
// построчный разбор логов. Содержимого секций здесь нет.
|
||||
"uncovered", st.Uncovered,
|
||||
"uncovered_dropped", st.UncoveredDropped,
|
||||
// Имена, встреченные впервые по журналу, — атрибутом ВСЕГДА, а уровень
|
||||
// поднимается отдельной ветвью ниже. Наблюдаемый признак события это
|
||||
// он: имя непокрытой секции стоит в атрибуте `uncovered` у каждой
|
||||
// доставки, которая её принесла, и по нему первую встречу не отличить.
|
||||
"uncovered_new", st.UncoveredNew,
|
||||
"uncovered_seen_unknown", st.UncoveredSeenUnknown,
|
||||
// Причина несостоявшейся сверки — рядом с признаком. Занятость базы
|
||||
// проходит сама, испорченная колонка не проходит никогда и поднимает
|
||||
// признак на каждой доставке; по одному булеву это неразличимо.
|
||||
// Значений точек в ошибке нет: до текста доезжает только имя секции, и
|
||||
// оно обрезано.
|
||||
"uncovered_seen_error", st.UncoveredSeenError,
|
||||
// Категориальные значения — ЧИСЛАМИ. Ни строк, ни выведенных кодов:
|
||||
// «Сидячий образ жизни» — это контекст пульса, то есть данные о
|
||||
// здоровье. Какие именно строки ждут словаря, отвечает реестр в базе.
|
||||
@@ -318,6 +359,15 @@ func (s *Service) logResult(ctx context.Context, deliveryID string, st Stats) {
|
||||
allEntitiesSkipped := st.Workouts == 0 && st.Records == 0 && skippedEntities > 0
|
||||
|
||||
switch {
|
||||
case len(st.UncoveredNew) > 0:
|
||||
// ПЕРВОЙ ветвью, и это существенно. Все прочие говорят о событиях,
|
||||
// повторяющихся на живом потоке; это — однократное за всю жизнь имени, и
|
||||
// замаскировать его перезаписью точек значило бы потерять ровно то, ради
|
||||
// чего наблюдение заведено. Уровень `WARN`, а не `ERROR`: приезд новой
|
||||
// секции — штатное событие внешнего мира, «посмотри», а не «разбери
|
||||
// сбой». Имена секций в лог попадать могут: имя ключа — форма пакета, а
|
||||
// не измерение.
|
||||
s.log.WarnContext(ctx, "delivery folded, new uncovered section", attrs...)
|
||||
case st.EntitiesHeld > 0:
|
||||
// Приехавшая версия сущности отклонена как теряющая содержание. Плата
|
||||
// за отказ объединять поля: событие обязано быть видно, потому что на
|
||||
@@ -446,10 +496,61 @@ type parseResidue struct {
|
||||
// считал». Ноль означал бы «проверено, терять нечего», а по этому числу
|
||||
// ретеншен принимает необратимое решение об удалении тела.
|
||||
skipped *int64
|
||||
// novelty в учёте не участвует — она едет в запись лога об отказе. Полем, а
|
||||
// не пятым параметром `fail`: параметры путают местами, а поле называет
|
||||
// себя само.
|
||||
novelty sectionNovelty
|
||||
}
|
||||
|
||||
func residueOf(parsed hae.Result) parseResidue {
|
||||
return parseResidue{uncovered: parsed.Uncovered}
|
||||
func residueOf(parsed hae.Result, novelty sectionNovelty) parseResidue {
|
||||
return parseResidue{uncovered: parsed.Uncovered, novelty: novelty}
|
||||
}
|
||||
|
||||
// sectionNovelty — исход сверки имён непокрытых секций с журналом.
|
||||
type sectionNovelty struct {
|
||||
// fresh — имена, которых не было ни в одной доставке раньше этой.
|
||||
fresh []string
|
||||
// unknown — сверка не состоялась, и потому новыми объявлены ВСЕ имена
|
||||
// доставки.
|
||||
unknown bool
|
||||
// cause — почему не состоялась. Без неё занятость базы (пройдёт сама) и
|
||||
// испорченное содержимое колонки (не пройдёт никогда, и признак будет
|
||||
// подниматься на каждой доставке) неотличимы, а разбираться пришлось бы тем
|
||||
// самым ручным запросом в базу, от которого задача избавляет.
|
||||
cause error
|
||||
}
|
||||
|
||||
// novelty спрашивает журнал, какие из непокрытых имён встречаются впервые.
|
||||
//
|
||||
// Отказ запроса свёртку не роняет и исходом доставки не становится: правила
|
||||
// классификации исходов наблюдение не трогает, занятая база и отменённый
|
||||
// контекст остаются обстоятельствами. Но и молчания здесь быть не может — имя,
|
||||
// о котором смолчали, уже записано в учёт, — поэтому при отказе новыми
|
||||
// объявляются все имена, а признак несостоявшейся сверки идёт в запись.
|
||||
//
|
||||
// Запрос берётся только при непустом списке: на живом потоке все три
|
||||
// приезжающие секции покрыты, то есть в штатном режиме сверка не стоит ничего.
|
||||
func (s *Service) novelty(ctx context.Context, uncovered []string, at store.DeliveryRef) sectionNovelty {
|
||||
if len(uncovered) == 0 {
|
||||
return sectionNovelty{}
|
||||
}
|
||||
|
||||
seen, err := s.store.SectionsSeenBefore(ctx, uncovered, at)
|
||||
if err != nil {
|
||||
return sectionNovelty{fresh: uncovered, unknown: true, cause: err}
|
||||
}
|
||||
|
||||
fresh := make([]string, 0, len(uncovered))
|
||||
for _, name := range uncovered {
|
||||
if _, ok := seen[name]; ok {
|
||||
continue
|
||||
}
|
||||
fresh = append(fresh, name)
|
||||
}
|
||||
if len(fresh) == 0 {
|
||||
return sectionNovelty{}
|
||||
}
|
||||
return sectionNovelty{fresh: fresh}
|
||||
}
|
||||
|
||||
// skippedEntities — сколько сущностей с собственным `id` разбор пропустил.
|
||||
@@ -469,6 +570,20 @@ func (s *Service) fail(ctx context.Context, deliveryID string, cause error, resi
|
||||
return
|
||||
}
|
||||
|
||||
// Новые имена доезжают до записи об отказе: она уже выше рутинного уровня,
|
||||
// а событие опознаётся атрибутом. В отложенном исходе выше их нет намеренно
|
||||
// — там учётная запись не меняется, доставка вернётся следующим проходом, и
|
||||
// повторение признака на каждом проходе занятой базы превратило бы
|
||||
// однократное событие в дребезг.
|
||||
attrs := []any{"error", cause, "delivery_id", deliveryID}
|
||||
if len(residue.novelty.fresh) > 0 {
|
||||
attrs = append(attrs, "uncovered_new", residue.novelty.fresh)
|
||||
}
|
||||
if residue.novelty.unknown {
|
||||
attrs = append(attrs, "uncovered_seen_unknown", true,
|
||||
"uncovered_seen_error", residue.novelty.cause)
|
||||
}
|
||||
|
||||
level := slog.LevelError
|
||||
switch {
|
||||
case errors.Is(cause, hae.ErrLayerUnknown):
|
||||
@@ -481,7 +596,7 @@ func (s *Service) fail(ctx context.Context, deliveryID string, cause error, resi
|
||||
// одного класса ошибки давали бы постоянный ERROR-шум.
|
||||
level = slog.LevelWarn
|
||||
}
|
||||
s.log.Log(ctx, level, "delivery fold failed", "error", cause, "delivery_id", deliveryID)
|
||||
s.log.Log(ctx, level, "delivery fold failed", attrs...)
|
||||
|
||||
// Слой НЕ затирается: доставка могла свернуться успешно раньше, и пустая
|
||||
// строка здесь оборвала бы цепочку наследования, то есть изменила бы
|
||||
|
||||
Reference in New Issue
Block a user