Главная/Проекты/Чеклист

Проект 1. Список проверок

Проходить после реализации и до чтения SOLUTION.md.

Пункт засчитывается, когда выполнена команда и сверен вывод. «Вроде работает» пунктом не является: три из десяти требований проекта нарушаются именно так, что приложение при этом продолжает работать.

Вывод самого приложения в этом файле получен фактическим запуском эталонной реализации. Обёртка docker run не выполнялась — Docker на машине, где готовился курс, не установлен (README). Различие касается только строки запуска: то, что печатает программа, от способа запуска не зависит.

Подготовка

bash
cat > sample.jsonl <<'EOF'
{"ts": "2026-08-04T10:00:00Z", "status": 200, "path": "/api/items", "method": "GET", "ms": 12}
{"ts": "2026-08-04T10:00:01Z", "status": 200, "path": "/api/items", "method": "GET", "ms": 8}
{"ts": "2026-08-04T10:00:02Z", "status": 404, "path": "/api/missing", "method": "GET", "ms": 3}
{"ts": "2026-08-04T10:00:03Z", "status": 500, "path": "/api/items", "method": "POST", "ms": 240}
не json вовсе
{"ts": "2026-08-04T10:00:05Z", "status": 200, "path": "/health", "method": "GET", "ms": 1}
[1, 2, 3]
{"ts": "2026-08-04T10:00:07Z", "status": 500, "path": "/api/items", "method": "POST", "ms": 190}
EOF

docker build -t logstat .

Восемь строк, из них две неразбираемые намеренно: без них не проверяются требования 4 и 5.

Почему во всех командах ниже есть аргумент. Образ задаёт ENTRYPOINT ["logstat"] и CMD ["--help"]: запуск без аргументов печатает справку и стандартный ввод не читает. Это осознанный выбор — docker run образ показывает, что делать, — но означает, что для чтения stdin нужен хотя бы один аргумент.

Проверено запуском: docker run --rm -i logstat < данные печатает справку, docker run --rm -i logstat --top 3 < данные — таблицу (verify/FACTS.md).


1. Аргументы

Что проверяется: аргументы docker run доходят до приложения.

bash
docker run --rm logstat --version
text
logstat 1.0.0
bash
docker run --rm logstat --help | head -3
text
usage: logstat [-h] [--group-by ПОЛЕ] [--top N] [--filter ПОЛЕ=ЗНАЧЕНИЕ]
               [--format {text,json}] [--strict] [--config ПУТЬ]
               [--show-config] [--version]

☐ Версия печатается
☐ Справка печатается
echo $? после --version равно 0

Если не работает: ENTRYPOINT записан в shell-форме. Тогда PID 1 — оболочка, аргументы docker run попадают ей, а не приложению (урок 4.6).


2. Конфигурация

Что проверяется: три источника и их приоритет.

bash
docker run --rm logstat --show-config
text
параметр         значение     источник
────────────────────────────────────────────────
top              10           умолчание
group_by         status       умолчание
output_format    text         умолчание
strict           False        умолчание
bash
docker run --rm -e LOGSTAT_GROUP_BY=path logstat --show-config --top 4
text
параметр         значение     источник
────────────────────────────────────────────────
top              4            аргумент
group_by         path         окружение
output_format    text         умолчание
strict           False        умолчание

Файл конфигурации:

bash
cat > logstat.toml <<'EOF'
[logstat]
top = 5
group_by = "path"
EOF

docker run --rm -v "$PWD/logstat.toml:/etc/logstat/config.toml:ro" logstat --show-config
text
параметр         значение     источник
────────────────────────────────────────────────
top              5            файл /etc/logstat/config.toml
group_by         path         файл /etc/logstat/config.toml
output_format    text         умолчание
strict           False        умолчание

Файл перекрывается окружением:

bash
docker run --rm -v "$PWD/logstat.toml:/etc/logstat/config.toml:ro" \
    -e LOGSTAT_TOP=2 logstat --show-config | grep '^top'
text
top              2            окружение

☐ Умолчания применяются, когда нет других источников
☐ Аргумент перекрывает окружение
☐ Окружение перекрывает файл
☐ Источник каждого значения виден

Частая ошибка: аргумент со значением None записывается поверх окружения. Проверяется предпоследней командой: если top окажется равным умолчанию, а не 2, — приоритет нарушен.


3. Потоки

Что проверяется: результат отделён от диагностики.

bash
docker run --rm -i logstat --top 3 < sample.jsonl 2>/dev/null
text
status   записей     доля
─────────────────────────
200            3    50.0%
500            2    33.3%
404            1    16.7%
bash
docker run --rm -i logstat --top 3 < sample.jsonl 2>&1 >/dev/null
text
прочитано записей: 6, отобрано: 6
пропущено неразобранных строк: 2 (первая — строка 5: не JSON (Expecting value))
подсказка: --strict прерывает работу на первой такой строке

Главная проверка требования — вывод в JSON остаётся разбираемым:

bash
docker run --rm -i logstat --format json --top 3 < sample.jsonl 2>/dev/null | python3 -m json.tool >/dev/null
echo $?
text
0

☐ В stdout только результат
☐ В stderr только диагностика
☐ JSON из stdout разбирается без ошибок
☐ Число пропущенных строк сообщается, а не замалчивается

Почему последний пункт важен: инструмент, молча пропускающий половину журнала, даёт правдоподобный и неверный ответ. Это хуже, чем отказ.

Замечание о порядке строк. При перенаправлении stdout буферизуется блоками, а stderr — нет, поэтому в объединённом выводе (2>&1 без разделения) диагностика может опередить таблицу. Это не ошибка программы; проверять потоки нужно раздельно, как в командах выше.


4. Коды возврата

Что проверяется: четыре различимых исхода.

bash
docker run --rm -i logstat --top 3 < sample.jsonl >/dev/null 2>&1; echo "успех: $?"
docker run --rm -i logstat --filter method=DELETE < sample.jsonl >/dev/null 2>&1; echo "нет совпадений: $?"
docker run --rm -i logstat --strict < sample.jsonl >/dev/null 2>&1; echo "ошибка данных: $?"
docker run --rm -e LOGSTAT_TOP=ноль -i logstat --group-by status < sample.jsonl >/dev/null 2>&1; echo "ошибка использования: $?"
docker run --rm logstat /нет/такого/файла >/dev/null 2>&1; echo "файл не найден: $?"
text
успех: 0
нет совпадений: 3
ошибка данных: 1
ошибка использования: 2
файл не найден: 1

☐ Четыре различных кода получены
☐ «Нет совпадений» отличается от «успех»
☐ «Ошибка использования» отличается от «ошибка выполнения»

Если все коды нулевые: приложение вызывается через sh -c или результат main() не передан в sys.exit().

Проверка самой проверки. Сломайте одно место намеренно — верните 0 вместо EXIT_NO_MATCH — и убедитесь, что вторая строка изменилась. Проверка, которая не падает на сломанном коде, ничего не проверяет.


5. Источники данных

Что проверяется: файл и стандартный ввод.

bash
docker run --rm -i logstat --top 3 < sample.jsonl 2>/dev/null | head -1
docker run --rm -v "$PWD/sample.jsonl:/data/sample.jsonl:ro" logstat /data/sample.jsonl 2>/dev/null | head -1
text
status   записей     доля
status   записей     доля

Конвейер не должен приводить к трассировке:

bash
docker run --rm -i logstat --top 3 < sample.jsonl 2>/dev/null | head -2; echo "код: ${PIPESTATUS[0]}"
text
status   записей     доля
─────────────────────────
код: 0

☐ Чтение из stdin работает при -i
☐ Чтение из смонтированного файла работает
| head не вызывает BrokenPipeError

Если забыть -i: стандартный ввод не подключён, программа получит пустой поток и вернёт 3. Это не ошибка программы.


6. Тесты

Что проверяется: тесты существуют, покрывают требования и останавливают сборку.

bash
docker build --target test -t logstat:test . 2>&1 | tail -12
text
48 passed in 0.15s
Name                      Stmts   Miss  Cover   Missing
-------------------------------------------------------
src/logstat/__init__.py       1      0   100%
src/logstat/__main__.py       5      5     0%   2-9
src/logstat/cli.py           89      7    92%   55-56, 142-147
src/logstat/config.py        86      5    94%   68, 81, 111, 113, 123
src/logstat/errors.py        11      0   100%
src/logstat/stats.py         69      1    99%   75
-------------------------------------------------------
TOTAL                       261     18    93%
Required test coverage of 90% reached.

Главная проверка — не «тесты проходят», а «провал останавливает сборку»:

bash
# Сломать один тест намеренно
sed -i 's/assert code == EXIT_NO_MATCH/assert code == 0/' tests/test_cli.py
docker build --target test -t logstat:test . ; echo "код сборки: $?"
git checkout tests/test_cli.py   # вернуть
text
код сборки: 1

☐ Все тесты проходят
☐ Покрытие не ниже 90 %
☐ Сломанный тест останавливает сборку
docker build . без --target тоже прогоняет тесты или явно задокументировано, что нет

Про последний пункт. Стадия, на которую никто не ссылается, при сборке итогового образа не выполняется (урок 15.5). Это нормально и даже полезно — но должно быть решением, а не неожиданностью. Если тесты обязательны в CI, их запускают отдельным шагом с --target test.


7. Кэш сборки

Что проверяется: изменение кода не переустанавливает зависимости.

bash
docker build -t logstat .                    # первая сборка
touch src/logstat/stats.py
docker build -t logstat . 2>&1 | grep -c CACHED
text
# число слоёв, взятых из кэша — должно быть больше нуля

Более показательная проверка — по времени:

bash
time docker build -t logstat .               # без изменений: секунды
sed -i 's/__version__ = .*/__version__ = "1.0.1"/' src/logstat/__init__.py
time docker build -t logstat .               # изменился код: тоже секунды

☐ Повторная сборка без изменений почти мгновенна
☐ Изменение файла в src/ не запускает установку зависимостей заново
☐ Изменение pyproject.toml установку запускает

Если кэш не работает: COPY . . стоит раньше установки. Тогда любое изменение любого файла сбрасывает всё последующее (урок 5.7).

Тонкость. RUN --mount=type=cache ускоряет установку локально, но не переносится в CI через cache-to: экспортируются слои, а не содержимое cache mount (урок 16.3). Для этого проекта это не проблема, но знать полезно.


8. Безопасность

Что проверяется: запуск не от root, идентификатор числовой.

bash
docker image inspect logstat --format '{{.Config.User}}'
text
10001:10001
bash
docker run --rm --entrypoint sh logstat -c 'id -u' 2>/dev/null || echo "оболочки в образе нет — это хорошо"

User задан
User числовой, а не имя
☐ Значение не 0 и не пустое

Почему числовой. Имя пользователя внутри образа не разрешается снаружи. Kubernetes с runAsNonRoot: true не может убедиться, что имя соответствует ненулевому UID, и не запускает pod (урок 18.2).

Дополнительно (не входит в обязательные требования):

bash
docker history logstat --no-trunc | grep -i -E 'password|secret|token' || echo "секретов в истории нет"
docker image inspect logstat --format '{{.Size}}' | numfmt --to=iec

Сводка

ПроверкаТребованияОтметка
1Аргументы1
2Конфигурация2, 3
3Потоки4
4Коды возврата5
5Источники данных6
6Тесты7, 8
7Кэш сборки9
8Безопасность10

Все восемь отмечены — можно открывать SOLUTION.md.

Не все — сначала доделать. Эталонное решение полезно как предмет сравнения и бесполезно как источник: прочитанное решение всегда кажется очевидным.


Навигация

← Техническое задание
Эталонное решение →
Вернуться к проектам
Главное оглавление

Markdown на GitHub ↗