Skip to content

Latest commit

 

History

History
149 lines (114 loc) · 10.5 KB

File metadata and controls

149 lines (114 loc) · 10.5 KB

Выгрузка журнала прогонов: формат stepik-grader/usage/1

Что это. Контракт, по которому соседний инструмент читает журнал прогонов грейдера: состав записи, гарантии и границы. Команда — stepik-grader usage.

Чего здесь нет. Описания самого журнала (это core/stats.py и configuration.md) и рассуждений о том, куда данные отправлять: экспорт пишет файл, и на этом его работа кончается.


Зачем понадобилось

Журнал прогонов ведётся давно и отдавался только человеку — сводкой на экран (stepik-grader stats). Соседнему инструменту нужен тот же материал записями, а не сводкой, и в формате, на который можно опереться: сводка меняется вместе с тем, что интересно показать человеку, и опираться на неё значит ломаться от каждой правки вывода.

Что выгружается

Ровно то, что уже лежит в журнале, — ни одного нового измерения. Список полей закрытый и живёт константой _FIELDS в core/usage_export.py; тест test_export_carries_only_declared_fields падает, если наружу поедет что-то сверх него.

Поле Тип Что означает
schema str stepik-grader/usage/1 — имя и версия формата
ts float момент прогона, epoch-секунды
mode int режим проверки: 1–4
os str platform.system()Linux, Windows, Darwin
verdicts object счётчики вердиктов прогона: {"OK": 3, "WA": 1}
total_time float суммарное время прогона, секунды
isolation str backend песочницы, если прогон шёл под --sandbox

Поля, которых в записи журнала не было, в событие не попадают вовсе — вместо null. Потребитель отличает «не измеряли» от «измерили ноль» по наличию ключа.

Чего в выгрузке нет и не будет: путей к решениям, имён файлов и задач, кода, текстов ошибок, идентификаторов пользователя или машины. Журнал их и не хранит — это свойство core/stats.py, а не фильтр на выходе.

Формат файла

JSON Lines: одна запись — одна строка, порядок от старых к новым.

{"mode": 1, "os": "Linux", "schema": "stepik-grader/usage/1", "total_time": 1.5, "ts": 1756000000.0, "verdicts": {"OK": 3}}
{"isolation": "bwrap", "mode": 4, "os": "Linux", "schema": "stepik-grader/usage/1", "total_time": 0.7, "ts": 1756000100.0, "verdicts": {"WA": 1}}

Строки независимы: обрыв записи теряет одну строку, а не файл. Ключи отсортированы — diff двух выгрузок читается глазами.

Версия стоит в каждой строке, а не в имени файла и не в заголовке. Потребитель читает построчно, и файл, собранный из двух выгрузок разного возраста, остаётся разбираемым.

Команда

stepik-grader usage                         # JSON Lines в стандартный вывод
stepik-grader usage --usage-out usage.jsonl # в файл (каталог создаётся)

Счётчик записанного при выводе в stdout уходит в stderr: иначе он попал бы в конвейер вместе с данными. При записи в файл он печатается обычным образом.

Три исхода различаются:

  • журнал пуст — сообщение о том, что статистика выключена (она opt-in), код 0;
  • часть записей не разобралась — число пропущенных и путь к журналу, код 0: остальные записи выгружены, а покалеченный журнал чинится удалением;
  • записать не удалось — причина целиком и код 1: команду звали ради файла, и молчаливый успех без файла хуже отказа.

Гарантии

  • Сети нет. Экспорт читает файл и пишет файл. Отправку наружу, если она кому-то нужна, делает тот, кто читает выгрузку, — осознанно и своими руками. Проверяется тестом: socket в прогоне экспорта подменён на отказ.
  • Ничего не собирается сверх журнала. Включение экспорта не включает сбор: журнал ведётся, только когда включена статистика (--stats или stats = true в pyproject.toml).
  • Выгрузка не меняет журнал. Чтение и только чтение: ротация, очистка и запись остаются за core/stats.py.

Правила эволюции

Формат назван версией в каждой записи (schema) именно затем, чтобы его можно было менять, не ломая читателя молча.

  1. Стабильны имя и смысл перечисленных полей. Переименование ts, mode, os, verdicts, total_time, isolation или смена их семантики — ломающее изменение: оно поднимает версию до stepik-grader/usage/2 и объявляется в CHANGELOG.md.
  2. Расширение аддитивно, и незнакомое поле потребитель игнорирует. Новое поле добавляется как необязательное, версия при этом не меняется — читатель, который падает на незнакомом ключе, написан неверно. Отсутствие ключа по-прежнему значит «не измеряли», а не «ноль».
  3. Как добавляют новое. Поле входит в _FIELDS (core/usage_export.py), в таблицу выше и в тест закрытого списка — тремя правками сразу, иначе гейт test_export_carries_only_declared_fields покажет расхождение. Новое поле допустимо, только если оно уже лежит в журнале: экспорт ничего не собирает сам, и обратное означало бы новый сбор под видом нового формата.
  4. Удаление поля — тоже смена версии. Читатель вправе рассчитывать на объявленный состав; молча пропавший ключ неотличим от «не измеряли».

Границы

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

Прогресс по задачам живёт в другом месте (история обучения, SQLite) и сюда не попадает: у него другой контракт и другая чувствительность — там есть имена задач.

Связанное

  • SECURITY.md — что покидает машину и что нет;
  • configuration.md — как включается статистика;
  • api.md — HTTP-контракты веб-слоя (выгрузка в них не входит).

Через HTTP: GET /api/v1/usage

Тот же материал отдаётся локальным сервером — для инструмента, которому удобнее спросить, чем читать файл. Эндпоинт выключен по умолчанию и включается явным флагом вместе с сервером:

stepik-grader --serve --expose-usage
curl 'http://127.0.0.1:8000/api/v1/usage?since=1735689600'

Ответ — {schema, events, skipped}; поля записей те же, что у файлового экспорта, и список их закрыт той же константой. Без флага эндпоинт отвечает 404: выключенное снаружи должно выглядеть отсутствующим, а не запертым.

Почему отдельный флаг, а не часть --serve: поднять интерфейс для себя и открыть накопленное соседнему инструменту — разные решения. Первое человек принимает, чтобы работать; второе — чтобы поделиться, и оно не должно случаться заодно.

Полный контракт эндпоинта — api.md.