OTel для Claude Code — рентген твоих скиллов
Grafana — это дашборд, который показывает, какие из моих скиллов в Claude Code реально вызываются, а какие висят мёртвым грузом. Нативного счётчика на скилл не существует — тикет #35319 «Skill invocation tracking» висит открытым с 17 марта. Решается через стандартный OpenTelemetry-стек, который агент собирает локально в Docker за один вечер.
Зачем мне вообще видеть скиллы
У меня в ~/.claude/skills/ лежит около 40 своих скиллов. Сверху — плагины (superpowers, codex, frontend-design и ещё несколько). На круг получается ~80 установленных. Подозрение давно простое: половина — мёртвый груз. Кто-то писался под разовую задачу и больше не звался ни разу. Кто-то дублирует другой скилл. Кто-то вообще не подхватывается, потому что триггер сформулирован криво.
Команда /stats в Claude Code показывает токены и streak. Не скиллы. Я хотел увидеть простую картину: какие скиллы за неделю стрельнули хотя бы раз, какие — ни разу, и кто в топе.
Про сам факт, что скиллы синкаются между четырьмя машинами через приватный репо cc-skills, я уже писал. Это решило задачу «одинаковая среда везде». Но не решило задачу «понять, что из этой среды я реально использую». Пока коллекция растёт быстрее, чем я её ревьюю — без счётчика я слепой.
Что такое Grafana, Prometheus, Loki и OpenTelemetry — для вайбкодера
Если ты не из observability-мира, четыре названия в стеке выглядят как магия. Расшифровка:
- OpenTelemetry (OTel) — стандарт «как софт сообщает наружу, что он делает». Метрики, логи, трейсы. Claude Code умеет это с одной env-переменной.
- OTel Collector — посредник. Слушает
:4317(gRPC) или:4318(HTTP) и раскладывает сигналы по нужным базам. - Prometheus — БД для time-series чисел. «Скилл X вызвали 47 раз за день» — её работа.
- Loki — БД для логов от Grafana Labs. В отличие от Elasticsearch индексирует только лейблы, а не содержимое. Поэтому компактная.
- Grafana — UI поверх обеих. Дашборды, графики, поиск по логам.
Картинка целиком:
┌─────────────────────┐
│ Claude Code CLI │
│ ~/.claude/skills/ │
└──────────┬──────────┘
│ OTLP/gRPC
│ event: skill_activated
│ attr: skill.name
▼
┌─────────────────────┐
│ OTel Collector │ :4317 gRPC
│ (роутер сигналов) │ :4318 HTTP
└────┬───────────┬────┘
│ │
metrics │ │ logs
▼ ▼
┌──────────┐ ┌──────────┐
│Prometheus│ │ Loki │
│ :9090 │ │ :3100 │
└─────┬────┘ └────┬─────┘
│ │
└─────┬──────┘
▼
┌───────────────┐
│ Grafana │ :13737
│ (5 дашбордов)│
└───────────────┘
Claude Code как клиент шлёт OTLP-события в коллектор, тот делит метрики и логи между Prometheus и Loki, а Grafana поверх рисует.
Setup: один промпт, четыре контейнера
Готовый шаблон для этого кейса нашёлся — публичный репо ColeMurray/claude-code-otel с пятью преднастроенными дашбордами, docker-compose.yml и Makefile. Ничего своего ради эксперимента писать не пришлось — клонируем и поднимаем.
Был выбор Docker engine на macOS: Docker Desktop или Colima. Выбрал Colima — без GUI, без лицензионных ограничений, спокойнее по памяти на M2 Pro. Написал Claude Code на Opus 4.7:
~/Documents/GitHub/ и подними make up. Проверь что 4 контейнера живые. Если порт 3000 занят (у меня там Next.js dev-сервер крутится) — переедь Grafana на свободный высокий порт.
Дальше — уже не я. Агент поставил Colima через brew, поднял VM, склонировал репо, прочитал Makefile и попытался запустить стек. Тут начались грабли.
Грабли
Грабля 1: docker compose v1 vs v2
make up упал с unknown shorthand flag: 'd' in -d. Агент разобрался: Makefile написан под docker compose v2 (плагин), а brew по умолчанию ставит docker-compose v1 (отдельным бинарём через дефис). Флаги в них слегка разные. Решение — донастроить compose-плагин: создать папку для CLI-плагинов и слинковать туда v2. Это я не делал руками, агент сам выполнил mkdir и ln, перезапустил make up, контейнеры поднялись.
Грабля 2: порт 3000 уже занят
Grafana по дефолту слушает 3000. У меня на 3000 живёт Next.js dev-сервер другого проекта — он стартует с launchd при логине. curl localhost:3000 возвращал HTML с заголовком Next-Router-State-Tree, а не Grafana login page. Заметил, попросил агента переехать. Агент поправил docker-compose.yml: проброс "3000:3000" сменил на "13737:3000", перезапустил Grafana через docker compose up -d grafana --force-recreate. Логин page открылся.
Маленькая деталь, но без неё запуск ломается без понятной ошибки в логах — просто «сайт открывается, но не тот».
Грабля 3: главная — без OTEL_LOG_TOOL_DETAILS всё бесполезно
Это та грабля, ради которой стоит писать пост. Без отдельной env-переменной все user-defined и plugin-скиллы в логах сливаются в один общий placeholder custom_skill. Дашборд показывает «47 вызовов custom_skill» — и я не знаю, что внутри: daily-tasks, gh-issues, meeting-copilot или какой-нибудь скилл из superpowers. Имя скилла теряется на уровне инструментирования, не на уровне Loki-запроса.
Эту деталь я выловил из research’а заранее, до запуска. Иначе пришлось бы неделю смотреть в красивый, но бесполезный график. После того как разобрался, попросил агента дописать env-блок:
~/.claude/settings.json. Кроме базовых OTel переменных (CLAUDE_CODE_ENABLE_TELEMETRY, exporter настройки) добавь обязательно OTEL_LOG_TOOL_DETAILS=1 — иначе все мои user-defined и plugin skills сольются в placeholder custom_skill в Loki, и я не увижу реальных имён. Я бы это пропустил.
Бонусом — проверка пайплайна до того, как я рестартану claude-процесс. Если стек битый, лучше узнать это сразу, а не через сутки тишины.
test_skill_invocation_total{skill_name="manager"}. Подожди 15 секунд (scrape interval), запроси Prometheus и убедись что метрика появилась. Если pipe работает — значит проблема не в стеке, а только в том что claude пока не отправляет.
Тестовая метрика появилась в Prometheus за 15 секунд. Рестарт claude, первый запрос — и в дашборде поползли реальные имена скиллов. Тот же паттерн «настроил один раз и забыл», как в посте про хуки Claude Code и GitHub-issues — раз поднял, дальше работает само.
Что смотрю в дашборде
ColeMurray-репо приходит с пятью преднастроенными дашбордами: Cost & Usage, Tool Performance, User Activity, Performance & Errors, Event Logs. Из коробки они показывают токены, частоту инструментов, ошибки. Под скиллы я хочу одну простую штуку — top и zero-usage. Запрос в Loki через LogQL короткий:
sum by (skill_name) (count_over_time({event_name="skill_activated"} [7d]))
Что я ищу:
- Топ-N — скиллы, которые срабатывают часто. Их хочу шлифовать: точнее триггеры, лучше references-материалы.
- Zero-usage за 14 дней — кандидаты на удаление или мердж в более общий скилл. Если за две недели ни разу не позвался — либо триггер кривой, либо я уже забыл, что он есть.
Privacy-нюанс: у Claude Code есть отдельная переменная OTEL_LOG_USER_PROMPTS=1, которая логирует мои промпты целиком в Loki. Для локального self-host у меня на ноутбуке это допустимо — данные никуда не утекают. Для shared infra я бы не включал. У меня выключено: для трекинга скиллов имя скилла важнее текста промпта.
Outro
Backfill невозможен — данные пошли с момента рестарта. Через две недели, около 13 мая, сделаю первый проход: топ-10 оставлю и шлифую, zero-usage за 14 дней удаляю или сливаю в более общий скилл.
Это не observability ради observability. Коллекция скиллов растёт быстрее, чем я её ревьюю, — без счётчика «полезный но редкий» неотличим от «забытый и сломанный». Та же логика, по которой я писал скилл для аудита продуктовых данных: аудит — повторяемая практика, и для своих инструментов нужна та же дисциплина, что и для бизнеса.
Подписаться на обновления — @sereja_tech