Проект 1. Список проверок
Проходить после реализации и до чтения SOLUTION.md.
Пункт засчитывается, когда выполнена команда и сверен вывод. «Вроде работает» пунктом не является: три из десяти требований проекта нарушаются именно так, что приложение при этом продолжает работать.
Вывод самого приложения в этом файле получен фактическим запуском эталонной реализации. Обёртка
docker runне выполнялась — Docker на машине, где готовился курс, не установлен (README). Различие касается только строки запуска: то, что печатает программа, от способа запуска не зависит.
Подготовка
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 доходят до приложения.
docker run --rm logstat --version
logstat 1.0.0
docker run --rm logstat --help | head -3
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. Конфигурация
Что проверяется: три источника и их приоритет.
docker run --rm logstat --show-config
параметр значение источник
────────────────────────────────────────────────
top 10 умолчание
group_by status умолчание
output_format text умолчание
strict False умолчание
docker run --rm -e LOGSTAT_GROUP_BY=path logstat --show-config --top 4
параметр значение источник
────────────────────────────────────────────────
top 4 аргумент
group_by path окружение
output_format text умолчание
strict False умолчание
Файл конфигурации:
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
параметр значение источник
────────────────────────────────────────────────
top 5 файл /etc/logstat/config.toml
group_by path файл /etc/logstat/config.toml
output_format text умолчание
strict False умолчание
Файл перекрывается окружением:
docker run --rm -v "$PWD/logstat.toml:/etc/logstat/config.toml:ro" \
-e LOGSTAT_TOP=2 logstat --show-config | grep '^top'
top 2 окружение
☐ Умолчания применяются, когда нет других источников
☐ Аргумент перекрывает окружение
☐ Окружение перекрывает файл
☐ Источник каждого значения виден
Частая ошибка: аргумент со значением None записывается поверх окружения. Проверяется предпоследней командой: если top окажется равным умолчанию, а не 2, — приоритет нарушен.
3. Потоки
Что проверяется: результат отделён от диагностики.
docker run --rm -i logstat --top 3 < sample.jsonl 2>/dev/null
status записей доля
─────────────────────────
200 3 50.0%
500 2 33.3%
404 1 16.7%
docker run --rm -i logstat --top 3 < sample.jsonl 2>&1 >/dev/null
прочитано записей: 6, отобрано: 6
пропущено неразобранных строк: 2 (первая — строка 5: не JSON (Expecting value))
подсказка: --strict прерывает работу на первой такой строке
Главная проверка требования — вывод в JSON остаётся разбираемым:
docker run --rm -i logstat --format json --top 3 < sample.jsonl 2>/dev/null | python3 -m json.tool >/dev/null
echo $?
0
☐ В stdout только результат
☐ В stderr только диагностика
☐ JSON из stdout разбирается без ошибок
☐ Число пропущенных строк сообщается, а не замалчивается
Почему последний пункт важен: инструмент, молча пропускающий половину журнала, даёт правдоподобный и неверный ответ. Это хуже, чем отказ.
Замечание о порядке строк. При перенаправлении stdout буферизуется блоками, а stderr — нет, поэтому в объединённом выводе (2>&1 без разделения) диагностика может опередить таблицу. Это не ошибка программы; проверять потоки нужно раздельно, как в командах выше.
4. Коды возврата
Что проверяется: четыре различимых исхода.
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 "файл не найден: $?"
успех: 0
нет совпадений: 3
ошибка данных: 1
ошибка использования: 2
файл не найден: 1
☐ Четыре различных кода получены
☐ «Нет совпадений» отличается от «успех»
☐ «Ошибка использования» отличается от «ошибка выполнения»
Если все коды нулевые: приложение вызывается через sh -c или результат main() не передан в sys.exit().
Проверка самой проверки. Сломайте одно место намеренно — верните 0 вместо EXIT_NO_MATCH — и убедитесь, что вторая строка изменилась. Проверка, которая не падает на сломанном коде, ничего не проверяет.
5. Источники данных
Что проверяется: файл и стандартный ввод.
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
status записей доля
status записей доля
Конвейер не должен приводить к трассировке:
docker run --rm -i logstat --top 3 < sample.jsonl 2>/dev/null | head -2; echo "код: ${PIPESTATUS[0]}"
status записей доля
─────────────────────────
код: 0
☐ Чтение из stdin работает при -i
☐ Чтение из смонтированного файла работает
☐ | head не вызывает BrokenPipeError
Если забыть -i: стандартный ввод не подключён, программа получит пустой поток и вернёт 3. Это не ошибка программы.
6. Тесты
Что проверяется: тесты существуют, покрывают требования и останавливают сборку.
docker build --target test -t logstat:test . 2>&1 | tail -12
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.
Главная проверка — не «тесты проходят», а «провал останавливает сборку»:
# Сломать один тест намеренно
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 # вернуть
код сборки: 1
☐ Все тесты проходят
☐ Покрытие не ниже 90 %
☐ Сломанный тест останавливает сборку
☐ docker build . без --target тоже прогоняет тесты или явно задокументировано, что нет
Про последний пункт. Стадия, на которую никто не ссылается, при сборке итогового образа не выполняется (урок 15.5). Это нормально и даже полезно — но должно быть решением, а не неожиданностью. Если тесты обязательны в CI, их запускают отдельным шагом с --target test.
7. Кэш сборки
Что проверяется: изменение кода не переустанавливает зависимости.
docker build -t logstat . # первая сборка
touch src/logstat/stats.py
docker build -t logstat . 2>&1 | grep -c CACHED
# число слоёв, взятых из кэша — должно быть больше нуля
Более показательная проверка — по времени:
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, идентификатор числовой.
docker image inspect logstat --format '{{.Config.User}}'
10001:10001
docker run --rm --entrypoint sh logstat -c 'id -u' 2>/dev/null || echo "оболочки в образе нет — это хорошо"
☐ User задан
☐ User числовой, а не имя
☐ Значение не 0 и не пустое
Почему числовой. Имя пользователя внутри образа не разрешается снаружи. Kubernetes с runAsNonRoot: true не может убедиться, что имя соответствует ненулевому UID, и не запускает pod (урок 18.2).
Дополнительно (не входит в обязательные требования):
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.
Не все — сначала доделать. Эталонное решение полезно как предмет сравнения и бесполезно как источник: прочитанное решение всегда кажется очевидным.
Навигация
← Техническое задание
Эталонное решение →
Вернуться к проектам
Главное оглавление