Раздел 46 · Effect: Тихая Академия

История одного письма: трассы

middle~20 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Журнал из урока 98 отвечает на вопрос «что случилось». Записи хорошие: уровень, текст, пометки. Но каждая запись это точка во времени. Письмо же это путь: окошко, проверка адреса, книга башен, книги Почтамта, выдача. Чтобы понять, где письмо застряло, нужны не точки, а отрезки, вложенные друг в друга. Это и есть трасса.

Путевой лист письма

На старой почте к посылке привязывали путевой лист. Каждая станция писала в нём две вещи: когда посылка пришла и когда ушла. Если посылка пропала, по листу видно последнюю станцию, которая её видела, и сколько посылка там пролежала.

В Effect одна строчка такого листа называется спан. У спана есть имя (TowerBook.lookup), время начала и конца, атрибуты (tower: 'T-5') и родитель: спан, внутри которого он начался. Все спаны одного письма складываются в дерево, и это дерево называется трасса.

import { Effect } from 'effect';

const askBook = (code: string) =>
  Effect.sleep('2 seconds').pipe(Effect.withSpan('TowerBook.lookup', { attributes: { tower: code } }));

const sendLetter = (letter: number, to: string) =>
  Effect.gen(function* () {
    yield* askBook(to);
    yield* Effect.sleep('10 millis');
  }).pipe(Effect.withSpan('letters.send', { attributes: { letter } }));

Effect.withSpan(имя, { attributes }) оборачивает эффект в спан: спан начинается, когда эффект запустили, и кончается, когда эффект закончился, успехом или провалом. Родителя указывать не нужно. Спан TowerBook.lookup начался внутри letters.send, и Effect сам запишет letters.send ему в родители: текущий спан лежит в контексте файбера, как сервисы из урока 47.

Трасса этого письма выглядит так:

letters.send       letter=7         ████████████████████  2.01 s
  TowerBook.lookup tower=T-5        ███████████████████   2.00 s

Две секунды из двух с лишним письмо ждало книгу. Журнал этого не показывал, трасса показывает с первого взгляда.

Имя функции это имя спана

В уроке 38 Effect.fn('имя') давал функции имя для стека ошибок. Оно же становится именем спана: каждый вызов такой функции открывает свой спан.

import { Effect, Result } from 'effect';

export const checkTowers = Effect.fn('checkTowers')(function* (
  book: TowerBook['Service'],
  codes: ReadonlyArray<string>,
): Effect.fn.Return<ReadonlyArray<Result.Result<TowerInfo, TowerBookError>>> {
  yield* Effect.annotateCurrentSpan('towers', codes.length);
  return yield* Effect.forEach(codes, (code) => Effect.result(book.lookup(code)), { concurrency: 3 });
});

Это утренняя сверка Почтамта: спросить книгу про список башен, по три разом. Effect.annotateCurrentSpan(ключ, значение) пишет атрибут в текущий спан, здесь в спан checkTowers: сколько башен сверяли. Атрибут из withSpan известен до начала, а annotateCurrentSpan пишут по ходу, когда значение стало известно.

Effect.result из урока 42 превращает беду одной башни в значение, и сверка не падает из-за T-9, которой нет. В трассе беда всё равно видна: спан книги про T-9 кончится провалом.

Покрути трассу сам. В виджете две готовые трассы: письмо номер семь и утренняя сверка пяти башен, где T-9 отказывает сразу. Переключатель меняет у checkTowers concurrency с 1 на 3, и полосы вопросов к книге из лесенки становятся параллельными.

Книга в трассе

Как и журнал, трасса книги башен делается обёрткой, без правки сервиса из урока 90:

export const tracedBook = (book: TowerBook['Service']): TowerBook['Service'] => ({
  lookup: (code) =>
    book.lookup(code).pipe(
      Effect.tap((tower) => Effect.annotateCurrentSpan('tower.open', tower.open)),
      Effect.withSpan('TowerBook.lookup', { attributes: { tower: code } }),
    ),
});

Порядок важен: annotateCurrentSpan стоит внутри withSpan, поэтому пишет в спан книги. Если поставить Effect.tap после withSpan, атрибут уйдёт в спан выше, в окошко или в сверку. Ответ книги ложится атрибутом tower.open, беда закрывает спан провалом: у спана, который кончился ошибкой, атрибута ответа нет, есть причина.

Трасса в тесте

Кто собирает спаны? Сервис Tracer.Tracer. По умолчанию он ничего не хранит. В проде его заменяют слоем, который отправляет спаны в сборщик, а в тесте трассировщиком в памяти:

import { Tracer } from 'effect';

const spans: Tracer.NativeSpan[] = [];
const memoryTracer = Tracer.make({
  span(options) {
    const span = new Tracer.NativeSpan(options);
    spans.push(span);
    return span;
  },
});

const traced = Effect.provideService(checkTowers(book, ['T-2', 'T-9']), Tracer.Tracer, memoryTracer);

Tracer.make просит одну функцию: сделать спан по описанию. Tracer.NativeSpan это готовый спан Effect, который запоминает имя, атрибуты в Map, родителя в Option и статус. После запуска в массиве лежат все спаны сверки: у checkTowers атрибут towers: 2 и родителя нет, у двух TowerBook.lookup родитель checkTowers, и у спана T-9 статус Ended с exit провала.

В проде спаны уходят по протоколу OTLP в сборщик, где их рисуют такими же лентами, как в виджете. Для этого в Effect есть модуль effect/observability со слоем Otlp.layer: код Почтамта не меняется, меняется только слой трассировщика. Модуль в 4.1 помечен @stability unstable, поэтому в этом курсе трассы проверяются в памяти.

Под капотом

Где тут дыра

const night = (letters: ReadonlyArray<NewLetter>) =>
  Effect.forEach(letters, (letter) => send(letter), { concurrency: 10 }).pipe(Effect.withSpan('ночь'));

На его примере трасса есть: один спан ночь, двенадцать часов. Ночь тихая, писем двадцать, и всё дошло. Теперь соседний пример, ночь открытия: три тысячи писем, и письмо номер семь застряло. Где? В трассе одна полоса на двенадцать часов, внутри пусто: у отправки письма своего спана нет, у книги тоже. Длительность ночи это не история письма. Спан ставят на ту единицу работы, о которой потом спросят: на письмо, на вопрос к книге, на запрос к базе. Тогда под спаном ночи лягут три тысячи писем, и письмо номер семь найдётся по атрибуту letter.

Почтамт, этап 9

Почтамт учится рассказывать историю письма. Новый модуль src/trace.ts: книга башен в трассе и утренняя сверка. Тесты собирают спаны трассировщиком в памяти, сверяют имена, атрибуты, родителей и статус, а время двигают TestClock: четыре башни по три разом сверяются за две секунды, а не за четыре.

Практика

Глубже в архиве

Как трассы и метрики снимают с приложения на Node без Effect, через OpenTelemetry, разобрано в уроке про производительность и телеметрию раздела про Node.

Дальше

Лента письма номер семь лежит на столе Патриция. Ветрин читает её молча, до конца, и откладывает.

Ответ в числах ждёт в следующем уроке.