Skip to main content

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 уже жила в двух местах и дала бесконечную петлю самообновления).

Три обещания перед вызывающим:

  1. исключение проходит насквозь — кит его записывает, но не глотает: код возврата принадлежит вызывающему;
  2. кит молчит — ни строки в stdout/stderr, ни настроенного logging;
  3. кит не падает — нет прав, диск полон, битый файл, сломанный резолв: любая внутренняя ошибка гасится и наружу не выходит.

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"]
}
  • statusOK | 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 (стандарт требует неделимости «сдвиг в конец + запись»). На Windows O_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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

s_telemetrykit-0.2.0.tar.gz (55.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

s_telemetrykit-0.2.0-py3-none-any.whl (45.5 kB view details)

Uploaded Python 3

File details

Details for the file s_telemetrykit-0.2.0.tar.gz.

File metadata

  • Download URL: s_telemetrykit-0.2.0.tar.gz
  • Upload date:
  • Size: 55.4 kB
  • Tags: Source
  • Uploaded using 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}

File hashes

Hashes for s_telemetrykit-0.2.0.tar.gz
Algorithm Hash digest
SHA256 1eecc0cca068d1e07b38fe0f265bb29f09c0887cb2991a88bbb06ec215092062
MD5 a8cd547a0db4770b53138e3861604691
BLAKE2b-256 efbbb94167ec1bb53ecddf06638085d29504b0894d7c493c793d5d1cd3676484

See more details on using hashes here.

File details

Details for the file s_telemetrykit-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: s_telemetrykit-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 45.5 kB
  • Tags: Python 3
  • Uploaded using 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}

File hashes

Hashes for s_telemetrykit-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 447a8446837d4454d7ee253e32a43638b9d64a078405cab3cab30fbe40575f53
MD5 5b19de8b02f8190eedccdf1de7f5330a
BLAKE2b-256 28867d3bb8f0ca17d9470d6ba8c0aa814478b32ea43bc2ea50dee04f8fb72259

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.3

2 files

0.1.2

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page