s-telemetrykit
Продуктово-нейтральный SDK учёта запусков: фиксирует факт и результат каждого запуска компонента и кладёт событие в локальный outbox. Доставкой занимается воркер на стороне продукта — кит в сеть не ходит вообще.
Кит не знает ни одного конкретного продукта. Ни бренда, ни каталога-дома, ни
имён env-переменных, ни канона файлов меты, ни доменного термина потребителя.
Единица учёта называется компонентом (component) — это то, чей запуск
измеряется: инструмент, скрипт, подключаемый модуль. Специфика продукта задаётся
одним вызовом configure() (см. ниже). Сторож против регресса слоёв —
tests/test_layering.py.
Ноль зависимостей, только stdlib. Это требование, а не текущее состояние:
кит импортирует каждый компонент-потребитель, поэтому цена его импорта — часть
цены любого вызова. Для сравнения на одной машине: import telemetrykit — 68 мс,
import librarykit — 750 мс (корень китов тянет httpx, cryptography, keyring,
браузерный слой). Отсюда же собственный минимальный санитайзер секретов вместо
librarykit.redaction — см. докстринг telemetrykit/sanitize.py.
Зачем
Событий «этим инструментом воспользовались» в системе обычно не эмитится вообще — мы не знаем, пользуются им или нет. Просить LLM «отчитаться о вызове» ненадёжно: отчёт зависит от того, вспомнит ли модель про него. Факт запуска должен фиксировать КОД.
Подключение
from telemetrykit import track_run
@track_run() # id и версия определяются САМИ
def main() -> int:
...
# либо как контекст-менеджер
with track_run():
do_work()
Аргументы не нужны. track_run("vk", "1.2.3") тоже работает, но это запасной
путь: захардкоженная в десятках мест версия гарантированно разъедется с
реальной (в одном из проектов версия CLI уже жила в двух местах и дала
бесконечную петлю самообновления).
Три обещания перед вызывающим:
- исключение проходит насквозь — кит его записывает, но не глотает: код возврата принадлежит вызывающему;
- кит молчит — ни строки в stdout/stderr, ни настроенного
logging; - кит не падает — нет прав, диск полон, битый файл, сломанный резолв: любая внутренняя ошибка гасится и наружу не выходит.
track_skill()иSkillTracker— устаревшие имена версии 0.1.x. Они работают (импорты уже опубликованной версии ломать нельзя), но канон —track_run()/RunTracker.
Настройка под продукт — один configure()
from pathlib import Path
from telemetrykit import TelemetryConfig, configure
configure(TelemetryConfig(
home=Path.home() / ".myproduct", # каталог outbox'а и installation_id
env_prefix="MYPRODUCT", # MYPRODUCT_DISABLED, MYPRODUCT_HOME, …
meta_files=("_myproduct_meta.json", "MANIFEST.md"),
meta_id_keys=("slug", "myproduct_id", "name"),
default_kind="tool_run", # kind конверта в outbox'е
))
| Поле | Что задаёт | Дефолт |
|---|---|---|
home / home_provider |
каталог-дом (провайдер — если он зависит от рантайма) | ~/.telemetrykit |
env_prefix |
префикс ВСЕХ env-переменных кита | TELEMETRYKIT |
extra_env_names |
доп. имена переменных по логическому ключу (исторические алиасы продукта) | {} |
meta_files |
файлы меты, которые ищутся вверх по дереву | ("component.json", "pyproject.toml") |
meta_id_keys |
ключи идентификатора внутри меты | ("component_id", "id", "slug", "name") |
meta_version_keys |
ключи версии внутри меты | ("component_version", "version") |
default_kind |
kind конверта для событий запуска |
"run" |
component_id / component_version |
явная идентичность, если каскад не нужен | None |
Свойства: вызов идемпотентен, повторный перезаписывает конфигурацию
целиком (а не сливает с прежней — иначе состояние кита перестало бы быть
выводимым из одного вызова), и сбрасывает кэши, зависящие от конфигурации.
configure() принимает и отдельные поля: configure(default_kind="tool_run").
reset_to_defaults() возвращает нейтральные дефолты (нужно тестам потребителя).
Место такого вызова — тонкий модуль-адаптер на стороне продукта, а не кит.
Как определяются id и версия
Каскад, первый сработавший побеждает (telemetrykit/detect.py):
| # | Источник | Почему он |
|---|---|---|
| 1 | env <PREFIX>_COMPONENT_ID / <PREFIX>_COMPONENT_VERSION |
явная воля запускающего: тесты, headless, нестандартные раскладки. Половинчатый override допустим — задан только id, версия ищется дальше |
| 2 | configure(component_id=…, component_version=…) |
воля продукта, встроившего кит, когда идентичность ему известна |
| 3 | файлы меты из meta_files |
канон продукта (их обычно пишет установщик). Ищем вверх по дереву каталогов от файла вызывающего, максимум 10 уровней. Обработчик выбирается по расширению: .json / .toml (верхний уровень, затем [project]) / .md (YAML-frontmatter) |
| 4 | importlib.metadata.version(dist) |
метаданные УСТАНОВЛЕННОГО дистрибутива, а не константа в коде: их проставляет сборка, разъехаться с колесом они не могут |
| 5 | имя top-level пакета + "unknown" |
неопределённость не должна ронять вызывающего |
Вызывающий модуль определяется по стеку, а не по cwd. В момент применения
декоратора кит идёт вверх по кадрам (sys._getframe) до первого кадра, чей
модуль не наш и не служебная обёртка (contextlib/functools), и берёт его
__file__ и __name__. Рабочий каталог у процесса произвольный и про компонент
не знает ничего, а sys.argv[0] — это интерпретатор или трамплин. inspect не
используется намеренно: он дороже, чем весь остальной кит.
Запуск из исходников (компонент не установлен). Обычно срабатывает шаг 3 —
рядом с кодом лежит файл меты его репозитория. Если и его нет, шаг 4 не найдёт
дистрибутива, и мы честно отдадим <имя пакета> + "unknown": события всё
равно попадут в приёмник, просто без версии.
Контракт события (kind = default_kind, по умолчанию "run")
{
"event_id": "uuid4-hex",
"component_id": "vk",
"component_version": "1.2.3",
"installation_id": "uuid4-hex",
"session_id": "uuid4-hex",
"timestamp": "2026-07-28T10:00:00.123456+00:00",
"subcommand": "post create",
"duration_ms": 42,
"status": "OK",
"error_details": null,
"sys_info": {"os": "Windows", "arch": "AMD64", "python_version": "3.13.5"},
"arg_names": ["--text", "--dry-run"]
}
status—OK|ERROR|INTERRUPTED.KeyboardInterrupt— отдельный статус (иначе прерванные запуски раздуют долю ERROR и спрячут настоящие поломки);SystemExit(0)— этоOK,SystemExit(2)—ERRORс кодом.error_details(илиnull):{error_type, error_code, message_template, sanitized_stack_trace}.error_codeберётся из атрибута исключения (code/error_code/exit_code/errno/status_code) — по коду, а не по тексту, строят алерты.event_id— ключ дедупликации на приёмнике (доставка at-least-once).
Приватность
- Значения аргументов не логируются никогда — только имена флагов
(
--phone,-v); у--name=Иванберётся левая часть. Подкомандой считается только ведущий позиционный токен, похожий на имя команды (строчная латиница, 2..32 символа) — это отсекает телефоны, пути, e-mail и имена собственные. Разбор останавливается на первом флаге: всё после флага — его значение. - Стек и сообщение проходят санитайзер:
Bearer …,token=/password=/api_key=, префиксные токены (ghp_,github_pat_,glpat-,xoxb-,sk-,AKIA), JWT, длинные hex, e-mail,user:pass@host. Абсолютные пути →~/…, причём не только текущего пользователя (трейс может прийти из чужого venv или CI). message_template— шаблон: длинные числа →<num>, содержимое кавычек →<str>(короткий идентификатор вродеKeyError: 'phone'сохраняется — он нужен для агрегации и значением не является).installation_id— анонимный uuid4 в<home>/installation_id, не выводится из имени пользователя, hostname или MAC.
Outbox — общий транспорт (не «очередь телеметрии»)
telemetrykit/outbox.py — публичная библиотека для любых исходящих
сущностей: сегодня запуски компонентов и логи (kind="log"), завтра что-то
ещё. Второй такой механизм заводить нельзя: разъехавшиеся очереди — это
разъехавшиеся гарантии доставки.
Файл один — <home>/outbox.jsonl. Конверт:
{"id": "…", "kind": "run", "ts": "…", "schema_version": 1, "payload": {…}}
kind — обычная строка: новый тип не требует правки модуля, валидация
payload лежит на продюсере, который один знает форму своих данных.
schema_version — версия КОНВЕРТА, не payload'а.
from telemetrykit import outbox
ident = outbox.append("log", {"level": "ERROR", "message": "boom"}) # id или None
batch = outbox.read_batch(100) # читаем, НЕ удаляя
... # отправили
outbox.remove([e["id"] for e in batch]) # ack
outbox.path() # где лежит файл
Порядок «прочитал → отправил → подтвердил» даёт at-least-once: перезапуск между
отправкой и ack приведёт к повтору, поэтому в конверте и есть id.
Читатель outbox'а (воркер) обязан применить ТУ ЖЕ конфигурацию, что и писатели, — иначе он будет смотреть в другой каталог. На практике это значит: импортировать общий модуль-адаптер продукта перед обращением к
outbox.
Параллельная запись
Пишущих процессов много (компоненты запускаются одновременно), читающий один (воркер).
- Запись — одна строка за один системный вызов. На POSIX достаточно
O_APPEND(стандарт требует неделимости «сдвиг в конец + запись»). На WindowsO_APPENDэтого не даёт: CRT реализует его как «seek, потом write» двумя вызовами, и между ними вклинивается другой процесс. Замерено на живой машине: 6 процессов × 200 строк дали 1048 строк из 1200 — 13% событий пропали молча. Настоящий атомарный append в Windows — файл, открытый с правомFILE_APPEND_DATA(и безFILE_WRITE_DATA), тогда позицию двигает ядро; тот же замер с ним — 1200 из 1200. Биндинги черезctypesсобираются лениво, при первой записи; если не сложилось — откат на обычныйos.write. - Перезапись (ротация и ack) — под lock-файлом и через
os.replace, а дозаписанный конкурентами хвост переносится в новый файл по смещению: событие, приехавшее во время ack, не теряется.
Ротация
Порог 5 МБ (<PREFIX>_OUTBOX_MAX_BYTES). Сверху него самая старая половина
строк выбрасывается, а на их месте остаётся конверт kind="outbox.rotated" со
счётчиком dropped — потеря становится ВИДИМОЙ на приёмнике, а не молча
случившейся. Ротирует ровно один процесс (lock-файл), остальные в этот момент
просто дописывают.
Переменные окружения
Префикс TELEMETRYKIT — дефолтный; после configure(env_prefix="MYPRODUCT")
те же имена читаются как MYPRODUCT_…. Исторические имена продукта добавляются
через extra_env_names и проверяются ПОСЛЕ основного.
| Переменная | Что делает |
|---|---|
TELEMETRYKIT_DISABLED=1 |
выключает учёт запусков |
TELEMETRYKIT_OUTBOX_DISABLED=1 |
выключает исходящий транспорт целиком |
TELEMETRYKIT_OUTBOX_PATH |
полный путь к файлу outbox'а |
TELEMETRYKIT_OUTBOX_DIR |
каталог outbox'а |
TELEMETRYKIT_OUTBOX_MAX_BYTES |
порог ротации (по умолчанию 5 МБ) |
TELEMETRYKIT_HOME |
каталог-дом (по умолчанию ~/.telemetrykit) |
TELEMETRYKIT_COMPONENT_ID / TELEMETRYKIT_COMPONENT_VERSION |
override идентичности компонента |
TELEMETRYKIT_SESSION_ID |
общий id сессии для нескольких процессов |
Env сильнее конфигурации: это воля ЗАПУСКАЮЩЕГО, а configure — воля продукта.
Выключателя два намеренно: пользователь вправе отказаться от статистики запусков, не отключая доставку остального (например логов, которые сам же попросил собрать для разбора инцидента).
Публичный API
from telemetrykit import track_run # главное
from telemetrykit import TelemetryConfig, configure, current, reset_to_defaults
from telemetrykit import TelemetryEvent # контракт события
from telemetrykit import outbox # общий транспорт (нужен воркеру)
from telemetrykit import outbox_path # алиас outbox.path
Остальное — ErrorDetails, SysInfo, RunTracker, STATUS_*, KIND_RUN,
telemetry_disabled и под-модули args / config / detect / identity /
sanitize / paths. Устаревшие имена 0.1.x: track_skill, SkillTracker.
Разработка
uv venv && uv pip install pytest ruff
python -m pytest -q
python -m ruff check telemetrykit tests
Публикация на PyPI — по семвер-тегу vX.Y.Z через GitLab Trusted Publishing
(OIDC, токены нигде не хранятся), см. .gitlab-ci.yml. Версии поднимаются
только патчами от PyPI-latest.
Metadata
Release files for s-telemetrykit 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| s_telemetrykit-0.3.0.tar.gz | 70.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| s_telemetrykit-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 111.3 kB
Release files / s_telemetrykit-0.3.0.tar.gz
| Download URL | s_telemetrykit-0.3.0.tar.gz |
|---|---|
| Size | 70.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
75c3bb4e7ef6fd04b5b3c57a61c42c9b5030862c849b4d15699e76f7a3947a0f
|
|
BLAKE2b-256 checksum How to use checksums |
2b30614906b4cbd53f4136b57320e0a3c59e8bb52a0f8bc05ce0a929c883ebba
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / s_telemetrykit-0.3.0-py3-none-any.whl
| Download URL | s_telemetrykit-0.3.0-py3-none-any.whl |
|---|---|
| Size | 41.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4b9d273673bf6eeb00639da9dcf9a8ad712a4cf5baf6dfbc27cabd774f21fe52
|
|
BLAKE2b-256 checksum How to use checksums |
fcfb2e6278e63def586d865e40dbd05e592e8c56eb76f13dc30f1c0133225739
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|