Журнал из урока 98 и трасса из урока 99 отвечают на вопрос «что случилось». С одним письмом, с одной башней, в одну минуту. Патриций спрашивает другое: «сколько». Сколько писем ушло, сколько стало мёртвыми, как долго книга отвечает обычно и как долго в плохой день.
Можно посчитать по журналу: пройти все строки за день и сложить. Ночью, когда Почтамт лежит, это поздно, а днём, когда строк миллион, это дорого. Нужно число, которое система держит сама и отдаёт по первому вопросу. Такое число называют метрикой.
Число, которое держит Почтамт
Самая простая метрика это счётчик: он только растёт, как цифры на турникете у входа в Почтамт.
import { Effect, Metric } from 'effect';
const letters = Metric.counter('pochtamt_letters', { description: 'письма, принятые в окошке' });
export const program = Effect.gen(function* () {
yield* Metric.update(letters, 1);
yield* Metric.update(letters, 1);
const state = yield* Metric.value(letters);
return state.count; // 2
});
Metric.counter только описывает счётчик: имя, по которому его найдут снаружи, и описание для людей. Как и эффект, описание само ничего не считает. Metric.update(letters, 1) это эффект «прибавь один»; он случится, когда эффект запустят. Metric.value читает текущее состояние: у счётчика это count.
Где живёт число между вызовами? В реестре метрик, одном на всю программу. Описание letters можно создать в любом модуле, хоть дважды: по одному имени они попадут в одно число. Отсюда и правило для тестов этапа: счётчик общий на весь прогон, поэтому тест не ждёт, что он начнётся с нуля, а смотрит, на сколько он вырос.
Метки делят число на ряды
Патрицию мало «писем всего». Ему нужно «сколько ушло» и «сколько умерло». Заводить по счётчику на каждый статус значит менять имена: статусы добавятся, и договор с теми, кто читает метрики, сломается. Вместо этого у счётчика есть метки:
const byStatus = (status: 'sent' | 'dead') => Metric.withAttributes(letters, { status });
export const night = Effect.gen(function* () {
yield* Metric.update(byStatus('sent'), 1);
yield* Metric.update(byStatus('sent'), 1);
yield* Metric.update(byStatus('dead'), 1);
const sent = yield* Metric.value(byStatus('sent'));
const all = yield* Metric.value(letters);
return [sent.count, all.count]; // [2, 0]
});
Metric.withAttributes не создаёт новый счётчик, а называет ряд: то же имя pochtamt_letters, но с меткой status: sent. Каждое сочетание меток копит своё число.
Обрати внимание на второй ответ: 0. Счётчик без меток это тоже ряд, свой, и в него никто не писал. Сумму по всем статусам метрика сама не держит; её считает тот, кто читает: таблица на стене Почтамта или система мониторинга, которая складывает ряды по имени.
Как долго
Счётчик отвечает «сколько раз». Для «как долго» нужен другой вид метрики. Среднее время ответа книги звучит заманчиво, но врёт: девятьсот девяносто девять ответов по десять миллисекунд и одно молчание на десять секунд дают в среднем двадцать миллисекунд. Почтамт в этот день лежал, а среднее говорит, что всё отлично.
Поэтому время копят гистограммой:
const latency = Metric.histogram('pochtamt_book_ms', { boundaries: [100, 500, 2000] });
export const answers = Effect.gen(function* () {
yield* Metric.update(latency, 40);
yield* Metric.update(latency, 300);
return yield* Metric.value(latency);
// { buckets: [[100, 1], [500, 2], [2000, 2]], count: 2, min: 40, max: 300, sum: 340 }
});
Границы задаёшь ты: здесь 100 миллисекунд, полсекунды и две секунды. Корзина [500, 2] значит «два ответа не дольше 500». Кроме корзин гистограмма держит count, sum, min и max, так что среднее из неё тоже можно получить: sum / count. Но смотреть будут на корзины: сколько ответов вылезло за две секунды.
Для времени есть готовая гистограмма Metric.timer: она принимает Duration и копит миллисекунды. Это ровно то, что отдаёт Effect.timed.
Счётчики на книге башен
Теперь всё вместе. Книга башен из урока 90 это сервис с одним методом lookup. Счётчики ставятся обёрткой, как журнал в уроке 98 и трасса в уроке 99, и код книги не меняется:
import { Effect, Metric } from 'effect';
import type { TowerBook } from './towers';
export const bookAnswers = Metric.counter('pochtamt_book_answers', {
description: 'ответы книги башен по исходу',
});
export const bookLatency = Metric.timer('pochtamt_book_latency', {
description: 'время ответа книги башен',
});
export const answersFor = (outcome: string) => Metric.withAttributes(bookAnswers, { outcome });
export const meteredBook = (book: TowerBook['Service']): TowerBook['Service'] => ({
lookup: (code) =>
book.lookup(code).pipe(
Effect.tap((tower) => Metric.update(answersFor(tower.open ? 'open' : 'closed'), 1)),
Effect.tapError((error) => Metric.update(answersFor(error.reason), 1)),
Effect.exit,
Effect.timed,
Effect.flatMap(([duration, exit]) => Effect.andThen(Metric.update(bookLatency, duration), exit)),
),
});
Разберём сверху вниз.
Effect.tapприбавляет к рядуopenилиclosed, когда книга ответила. Ответ идёт дальше нетронутым.Effect.tapErrorприбавляет к ряду с причиной беды:нет такой башни,книга не отвечает. Причин четыре, их список знает книга, поэтому причина годится в метку.Effect.exitпревращает и ответ, и беду в значениеExit. Зачем? Затем, что следующая строка,Effect.timed, меряет время только успешного эффекта. Безexitмолчание книги не попало бы в таймер вовсе. А молчание дольше любого ответа, и его Патриций хочет видеть прежде всего.Effect.timedотдаёт пару: сколько шло и что получилось.- Последняя строка пишет время в таймер и возвращает
exitобратно в эффект: ответ снова ответ, беда снова беда. СнаружиmeteredBookвыглядит в точности как книга.
Сто процентов книги это число, которое книга посчитала про себя сама. Счётчик pochtamt_book_answers считает Почтамт, на своей стороне провода, по каждому ответу, который дошёл или не дошёл. Если эти два числа расходятся, врёт одно из них. Чтобы понять, какое, Почтамту нужны свои числа, а не чужой отчёт.
Под капотом
Где тут дыра
const answered = (letter: number) => Metric.withAttributes(letters, { letter: String(letter) });
export const day = Effect.forEach([1, 2, 3, 4, 5], (letter) => Metric.update(answered(letter), 1));
На пяти письмах всё работает: Metric.snapshot честно показывает ряд на каждое письмо. В том и беда. Каждое новое сочетание меток это новый ряд в реестре, и он там остаётся. Пять писем это пять рядов, а утро открытия Почтамта это четыре тысячи рядов по одному числу в каждом. Реестр пухнет, экспорт тяжелеет, а таблица на стене бесполезна: на ней четыре тысячи строк с единицей.
Ответ на вопрос «что было с письмом сорок два» живёт в журнале и в трассе. В метку идёт то, у чего значений мало и их список известен заранее: статус, исход, причина беды, башня. Число возможных значений метки называют мощностью, и у меток она должна быть маленькой.
Почтамт, этап 10
Почтамт начинает считать себя сам. Новый модуль src/meter.ts: счётчик ответов книги по исходу, таймер её ответов и обёртка над книгой, которая пишет и то, и другое. Тесты двигают часы и смотрят, на сколько выросли ряды: открытые и закрытые башни отдельно, беда по причине, время и у отказов.
Практика
Глубже в архиве
Метрики, трассы и журналы без Effect, на голом Node, разобраны в уроке про производительность и телеметрию раздела про Node.
Дальше
Гилт уходит, оставив отчёт на столе. Пондер весь вечер сидит над новыми счётчиками, а под утро приносит Рыдкуллу один лист.
Почему Почтамт спрашивал одно и то же сто раз, разберёмся в следующем уроке.