Первый прогон: поставить, обмазать, запустить, прочитать
В первой главе сказано, что инструмент записывает. Здесь — полный проход от пустой машины до прочитанной трассы. Все команды и весь вывод ниже сняты с настоящего прогона; повторить их можно строка в строку.
Что должно стоять на машине
| нужно | когда |
|---|---|
| Python 3.12 или новее | всегда |
gcc или clang, g++ или clang++ |
только чтобы обмазать и собрать C и C++ |
| Node | только для JavaScript и TypeScript |
elixir |
только чтобы собрать и запустить Elixir |
Разбор C и C++ идёт через libclang, но доставлять его отдельно не надо: он
приезжает вместе с пакетом. Разбор JavaScript и TypeScript уложен внутрь пакета
тем же образом, поэтому npm install не нужен — нужен только сам node.
Больше ничего: ни базы, ни отдельной службы, ни ключа.
Поставить
Выпуск 0.6.1 выложен, и обычных способов установки три. Любой из них
выносит наружу две команды — ouroboros и ouroboros-mcp.
Homebrew — хранилище формул digitable-lol/tap подключается само, отдельная
команда brew tap не нужна:
brew install digitable-lol/tap/ouroboros
asdf — держит рядом несколько версий и переключает их по .tool-versions.
Адрес хранилища пишется явно: в общем списке плагинов asdf инструмента пока
нет, и без адреса plugin add его не найдёт.
asdf plugin add ouroboros https://github.com/digitable-lol/ouroboros.git
asdf install ouroboros latest
asdf set ouroboros latest
uv — самый короткий путь:
uv tool install git+https://github.com/digitable-lol/ouroboros
Из исходников тоже можно, и отдельной сборки не требуется — uv сам заводит
окружение и ставит зависимости:
git clone https://github.com/digitable-lol/ouroboros
cd ouroboros
uv sync
Проверка, что всё встало, — одна и та же при любом способе:
ouroboros languages # после brew, asdf или uv tool install
uv run ouroboros languages # если работаете из исходников
{"languages": ["python", "javascript", "c", "cpp", "elixir", "go", "java", "csharp"]}
Дальше в главе команды пишутся коротко — ouroboros …. Если ставили из
исходников, перед каждой нужен запускатель: uv run ouroboros ….
Файл, на котором всё показано
Обычный расчёт скидки. В нём три функции и одна беда: последний вызов передаёт строку туда, где ждут число.
def discount_rate(total, member):
if member:
return 0.15
if total >= 10000:
return 0.10
return 0.0
def apply_discount(total, member=False):
rate = discount_rate(total, member)
return round(total * (1 - rate), 2)
def main():
for total, member in [(9999, False), (10000, False), (500, True)]:
print(total, member, apply_discount(total, member=member))
apply_discount("free")
main()
Шаг 1. Обмазать
ouroboros wrap-file discount.py
{"ok": true, "path": "discount.py", "language": "python", "functions_wrapped": 3, "runtime_header": "ouroboros_runtime.py"}
Три функции обмазаны. Рядом с файлом положен ouroboros_runtime.py — маленький
помощник, который и пишет записи; он нужен обмазанному файлу, чтобы работать.
Посмотрим, что случилось с исходником:
from ouroboros_runtime import log as _ouro_log
@_ouro_log
def discount_rate(total, member):
if member:
return 0.15
if total >= 10000:
return 0.10
return 0.0
Добавились ровно две вещи: строка ввоза наверху и по одной строке @_ouro_log
перед каждым def. Наверху — значит выше первой функции, но ниже #!,
объявления кодировки, строки описания модуля и from __future__: у этих строк
место в файле особое, и обмазка его не занимает.
Тело функции не тронуто. Поэтому ранние выходы, вложенные функции и уже
написанные try/finally продолжают работать как раньше — обмазка
перехватывает вызов снаружи, а не переписывает его изнутри.
У других языков вставка выглядит иначе, но правило одно: находим границы функции и вставляем по краям, а не перепечатываем текст. Пятая глава показывает все восемь.
Файл переписан на месте.
wrap-fileменяет ваш файл, а не создаёт копию. Работайте под системой контроля версий или на копии. Если менять исходное дерево нельзя вовсе — в инструменте есть отдельная песочница, она разобрана в главе про работу с агентом.
Шаг 2. Запустить
Обмазанная программа запускается обычным способом. Единственное добавление — переменная среды, которая говорит, куда писать:
OUROBOROS_DEBUG_INFO=./debug.info python3 discount.py
Переменную можно и не задавать: тогда записи лягут в debug.info в текущем
каталоге. Задавать её удобно, когда прогонов несколько и путать их не хочется.
Программа отработала как всегда: напечатала три строки и упала на четвёртом
вызове с TypeError. Обмазка ничего в поведении не изменила.
Шаг 3. Посмотреть, что записалось
Файл читается и глазами — это обычный текст:
{"p":"in","t":"2026-09-10T19:59:29.262","id":"36c9ba0c-e827-4e03-9020-3f9745d1a37c","ci":-1,"th":"7.129140652823040","fn":"discount_rate","a":"9999, False","k":""}
{"p":"out","id":"36c9ba0c-e827-4e03-9020-3f9745d1a37c","fn":"discount_rate","r":"0.0","d":2e-06}
Первая строка: вошли в discount_rate с доводами 9999, False. Вторая: вышли,
вернули 0.0, заняло две миллионных секунды. Номер id у них общий — это и есть
один вызов.
Читать глазами хорошо на девяти записях и плохо на девяти тысячах. Для второго есть две команды.
Шаг 4. Спросить у трассы
trace — выбрать нужные вызовы
Самый частый вопрос: что упало и с чем его позвали.
ouroboros trace ./debug.info --outcome raised --function discount_rate
{
"ok": true,
"path": "debug.info",
"calls_parsed": 9,
"malformed": 0,
"matched": 1,
"returned": 1,
"next_cursor": null,
"in_flight": [],
"in_flight_truncated": false,
"records": [
{
"index": 6,
"started": "2026-09-10T19:59:29.262",
"call_id": "0bec3caf-a258-4571-85f8-0b3313bf3a50",
"name": "discount_rate",
"args": "'free', False",
"kwargs": "",
"outcome_kind": "raised",
"outcome": "TypeError: '>=' not supported between instances of 'str' and 'int'",
"duration": 3e-06,
"cpu": null,
"thread": "7.129140652823040"
}
]
}
Вот ради этой строки всё и делалось:
"args": "'free', False"
Стек вызовов показывает, что сравнение упало. Он не показывает, что в
функцию приехала строка 'free'. Трасса показывает.
Заодно в ответе видно, сколько всего разобрано (calls_parsed), сколько строк
оказалось битыми (malformed) и сколько подошло под условие (matched).
trace-stats — свести весь прогон
ouroboros trace-stats ./debug.info
{
"ok": true,
"path": "debug.info",
"calls_parsed": 9,
"malformed": 0,
"total_calls": 9,
"in_flight": [],
"by_function": [
{
"name": "apply_discount",
"count": 4,
"result": 3,
"raised": 1,
"unknown": 0,
"duration_seconds": {
"min": 7.8e-05,
"max": 0.000106,
"mean": 9.3e-05,
"total": 0.000372,
"count": 4
}
},
{
"name": "discount_rate",
"count": 4,
"result": 3,
"raised": 1,
"unknown": 0,
"duration_seconds": {
"min": 1e-06,
"max": 3e-06,
"mean": 2e-06,
"total": 7e-06,
"count": 4
}
},
{
"name": "main",
"count": 1,
"result": 0,
"raised": 1,
"unknown": 0,
"duration_seconds": {
"min": 0.000746,
"max": 0.000746,
"mean": 0.000746,
"total": 0.000746,
"count": 1
}
}
],
"by_thread": [
{
"thread": "7.129140652823040",
"count": 9,
"functions": 3,
"cpus": []
}
],
"duration_seconds": {
"min": 1e-06,
"max": 0.000746,
"mean": 0.000125,
"total": 0.001125,
"count": 9
},
"timespan": {
"first": "2026-09-10T19:59:29.262",
"last": "2026-09-10T19:59:29.262",
"seconds": 0.0,
"timestamps_parsed": 9,
"timestamps_unparsed": 0
},
"note": "counts/durations are over completed calls; `duration_seconds` are REAL per-call durations (exit−entry) from each call's `d`. `by_thread` groups calls by the `th` token (CPUs each thread ran on); empty for traces with no thread field. `in_flight` = entered (`p:in`) but never completed. `timespan` is first→last entry time."
}
Девять вызовов, три функции. У каждой видно, сколько раз она вернула значение
(result) и сколько раз бросила (raised), и сколько заняли её вызовы. Поле
in_flight пустое — значит все вызовы, которые вошли, вышли. Когда оно
непустое, это интересно, и про это
следующая глава.
Последним полем идёт note — сам инструмент говорит, что именно посчитано:
только завершённые вызовы, длительности настоящие, in_flight — вошедшие и не
вышедшие.
Что проверить после первого прогона
- В ответе
wrap-fileчислоfunctions_wrappedбольше нуля. Ноль означает, что обмазывать было нечего или файл уже обмазан. - Файл
debug.infoпоявился и непустой. Пустой — значит обмазанный код не исполнялся: например, запустили не тот файл. malformedравно нулю. Не ноль — в трассу попало что-то, кроме записей.in_flightпуст. Не пуст — какие-то вызовы не вернулись.- Обмазанный файл не уехал в общую ветку. Он полезен ровно на время разбора.
Дальше — про отказы: что инструмент делать откажется и почему это его лучшее свойство.