Kinro docs · v1
Початок роботи

Документація Kinro

Kinro стежить за процесами, що проходять через кілька сервісів, і лише читає дані. Спрямуйте його на логи, які у вас уже є: у Grafana Loki, Graylog або звичайних файлах, у будь-якому форматі. Він покаже, що в них є, зведе в часову шкалу подій (timeline) усе, що сталося з одним ідентифікатором, стежитиме за кожним сервісом загальними моніторами й запропонує правила, вивчені на ваших же даних. Для цього не потрібно писати правила чи змінювати формат логів.

Власні правила на Python, перевірки в базі даних і формат логів Kinro — необов'язкові покращення на потім. Ніщо в Kinro не прив'язане до бізнес-домену: йому достатньо сервісу, ідентифікатора й шаблону.

СервісДжерело рядків логу: значення мітки Loki, джерело Graylog або файл логу. Визначається автоматично.
ШаблонФорма рядка із замаскованими змінними, виділена з логів, наприклад job <hex> failed: upstream unavailable.
КлючІдентифікатор, що зв'язує сервіси: його знаходять за спільними значеннями або оголошують у файлі проєкту.
Інсайт (знахідка)Те, що знайшов загальний монітор: нова помилка, сплеск, сервіс, що замовк, цикл повторів.
ПравилоНеобов'язкове. Перевірка для кожного ключа, запропонована на основі даних або написана на Python.
АлертІнсайт, дайджест або порушення правила з доказами; надсилається в Telegram, Slack, файл і лог.
КомандаЩо ви отримуєтеЩо потрібно
kinro discoverСервіси, рівні логів, ключі між сервісами, процеси, виявлені проблеми, секрети й персональні дані — у звіті з маскуванням.Логи
kinro timeline <id>Усі події одного ідентифікатора в усіх сервісах у порядку часу.Результат discover
kinro runЗагальні монітори на кожному сервісі, щоденний дайджест, негайні сповіщення лише там, де ви їх увімкнули.Оточення KINRO_*
kinro suggestФайл проєкту з правилами, вивченими на даних, усі в тіньовому режимі.Результат discover
kinro run --projectВаші правила, вбудовані чи власні, з перевірками в базі даних.Файл проєкту
Початок роботи

Швидкий старт

Від нуля до щоденного дайджесту без жодного рядка коду. Потрібні Docker і доступ на читання до ваших логів: каталог файлів логів, Loki або Graylog. У прикладах три сервіси, edge/api, core/indexer і notify/mailer, та ідентифікатор задачі (job id), що передається між ними.

  1. Отримайте образ

    Рушій постачається як Docker-образ; на хост більше нічого не встановлюється. Поки що образ приватний: увійдіть із pull-токеном, який вам видали.

    $ docker login -u <user>
    $ docker pull kybalda/utyg:0.1.3
  2. Подивіться, що є у ваших логах

    discover лише читає. Він записує discovery.json, report.html і index.sqlite у вихідний каталог і виводить підсумок. Оберіть, де лежать ваші логи:

    $ mkdir -p discovery
    $ docker run --rm --user "$(id -u):$(id -g)" -v "$PWD/logs:/logs:ro" -v "$PWD/discovery:/out" \
        kybalda/utyg:0.1.3 discover --logs /logs --out /out

    Підсумок виглядає так. Усі секрети й персональні дані замасковано:

    kinro discover: 3 services, 1214 records, 0.1 MB from 3 files in 0.1 s
    services (tier, records, errors, entity ids):
      L1 core/indexer: 559 records, 84 errors; tenant
      L1 edge/api: 341 records, 0 errors; job_ref, tenant_id
      L0 notify/mailer: 314 records, 13 errors; job
    keys across services: 2 (2 confirmed links, 1 unconfirmed candidates)
      k1 job: edge/api:job_ref, notify/mailer:job
      k2 tenant: core/indexer:tenant, edge/api:tenant_id
    flows: 2
      job (cross-service): 300 entities, 4 transitions
      tenant (cross-service): 40 entities, 2 transitions
    findings: 1 error bursts, 1 repeated failures, 4 top error templates, 1 silent gaps
      core/indexer: 80 errors in 2 hot minutes (peak 43/min; usually 1 in an error minute, 0.01/min on average) [2026-10-16T10:30:00Z .. 2026-10-16T10:31:59Z]
        connection refused
      notify/mailer: 1 job values hit the same failure 5+ times (max 12) [2026-10-16T12:52:36Z .. 2026-10-16T13:03:36Z]
        job <hex> failed: upstream unavailable
    secrets: 2, personal data: 2 (values masked)
      edge/api: credential `access_token` ****y0Ge in 2 records
      notify/mailer: bearer_token `bearer_token` ****2Nzg in 1 records
    timeline index: 1957 events of 1410 id values in 889 records, 0.2 MB, 0.0 s; caps hit: none
    wrote /out/discovery.json, /out/report.html and /out/index.sqlite

    Відкрийте discovery/report.html у браузері, щоб побачити повну картину: шаблони із замаскованими прикладами рядків, процеси із затримками, усі виявлені проблеми, що дасть кожному сервісу наступний рівень логів, і поле пошуку за ідентифікаторами.

  3. Простежте один ідентифікатор

    Візьміть потрібний вам ідентифікатор, наприклад job id з логів або зі звернення в підтримку, і подивіться на всі події з ним у всіх сервісах. Звіт показує ідентифікатори замаскованими; timeline приймає повне значення, яке ви вводите:

    $ docker run --rm -v "$PWD/discovery:/out:ro" kybalda/utyg:0.1.3 timeline 9d37ff01fe80 --index /out --limit 4
    kinro timeline 9d37ff01fe80: 14 events in 2 services (edge/api, notify/mailer), 2026-10-16T12:52:34.872Z .. 2026-10-16T13:04:36.488Z
    showing the last 4 of 14 events (--limit)
    key k1 job (confirmed link across services)
    2026-10-16T13:01:36.488Z  notify/mailer  error    job 9d37ff01fe80 failed: upstream unavailable  [job]
    2026-10-16T13:02:36.488Z  notify/mailer  error    job 9d37ff01fe80 failed: upstream unavailable  [job]
    2026-10-16T13:03:36.488Z  notify/mailer  error    job 9d37ff01fe80 failed: upstream unavailable  [job]
    2026-10-16T13:04:36.488Z  notify/mailer  info     job 9d37ff01fe80 delivered  [job]
    related ids in the same records (keys followed one hop)
      26389  edge/api:latency_us  1 record
      b14aed54bb69e1f0  edge/api:trace  1 record
  4. Стежте без правил

    kinro run без --project читає всі сервіси джерел, указаних в оточенні, і запускає загальні монітори. Запишіть налаштування в .env:

    # sources: any of Loki, Graylog and files
    KINRO_LOKI_URL=https://loki.internal:3100
    KINRO_LOKI_TOKEN=...
    # KINRO_GRAYLOG_URL=https://graylog.internal:9000
    # KINRO_GRAYLOG_TOKEN=...
    # KINRO_LOG_GLOB=/logs/*/*.log
    KINRO_STATE_PATH=/state/kinro.db
    KINRO_PAGE=error_spike,silent_service
    # TELEGRAM_BOT_TOKEN=...
    # TELEGRAM_CHAT_ID=...

    Перевірте конфігурацію офлайн, потім запустіть Kinro з volume для стану:

    $ docker run --rm --env-file .env kybalda/utyg:0.1.3 check
    zero-config: sources loki; monitors new_error_template, error_spike, silent_service, repeated_failure; learning 24h per service; paging error_spike, silent_service; digest 09:00 UTC; state /state/kinro.db; 0 errors, 0 warnings
    $ docker run -d --name kinro --init --restart unless-stopped --env-file .env -v kinro-state:/state \
        kybalda/utyg:0.1.3 run

    Для файлів логів додайте -v /var/log/myapp:/logs:ro і задайте KINRO_LOG_GLOB. Наявні файли читаються з кінця, якщо не задано KINRO_LOG_START=beginning.

  5. Читайте дайджест

    Перші 24 години Kinro вивчає кожен сервіс. Після цього все, що знаходять монітори, зберігається як інсайт і потрапляє в один дайджест на день о 09:00 UTC. Одразу надсилаються лише монітори з KINRO_PAGE. Без налаштувань Telegram чи Slack повідомлення потрапляють у лог, а також у KINRO_ALERTS_PATH, якщо його задано.

    [KINRO DIGEST] 2026-10-17 (09:00 UTC)
    3 services watched (0 still learning), 1215 records, 97 errors
    New insights: 5
      new_error_template (2):
        - core/indexer: new error template "connection refused" first seen 2026-10-16 10:30 UTC
        - notify/mailer: new error template "job <hex> failed: upstream unavailable" first seen 2026-10-16 12:52 UTC
      repeated_failure (1):
        - notify/mailer: job=****fe80 hit "job <hex> failed: upstream unavailable" 5 times in 4m (a retry loop?)
      silent_service (2, 2 paged):
        - edge/api: no records since 2026-10-16 13:58:58 UTC (58m); its longest learned pause is 7m
        - notify/mailer: no records since 2026-10-16 13:59:00 UTC (57m); its longest learned pause is 12m
    Ongoing:
      - silent_service: edge/api silent since 2026-10-16 13:58 UTC
      - silent_service: notify/mailer silent since 2026-10-16 13:59 UTC
      - repeated_failure: notify/mailer "job <hex> failed: upstream unavailable", latest id looping at 2026-10-16 12:56 UTC

    status будь-коли показує прогрес навчання й кількість інсайтів; його безпечно запускати поруч із робочим контейнером:

    $ docker exec kinro kinro status
    ...
    zero-config: 3 services (0 learning, 24h each); paging error_spike, silent_service; digest 09:00 UTC, next 2026-10-18T09:00:00+00:00, last 2026-10-17
    monitors: new_error_template, error_spike, silent_service, repeated_failure
    insights: new_error_template=2 (0 paged), repeated_failure=1 (0 paged), silent_service=2 (2 paged)
    learning shadow: none; rate-limited: none
      core/indexer: 100% learned, 559 records, 84 errors, last 2026-10-16T14:57:04.513000+00:00
      edge/api: 100% learned, 341 records, 0 errors, last 2026-10-16T13:58:58.697000+00:00, silent
      notify/mailer: 100% learned, 315 records, 13 errors, last 2026-10-16T13:59:00.183000+00:00, silent
  6. Далі

    Перетворіть процеси, знайдені discover, на правила, як описано в розділі Запропоновані правила: вони стартують у тіньовому режимі й нікому не надсилають сповіщень, доки ви не переведете їх у робочий режим. Якщо потрібна перевірка, якої не покриває жодна пропозиція, див. Написання власних правил.

Початок роботи

Рівні логів

Kinro працює з будь-якими логами. discover присвоює кожному сервісу рівень залежно від того, що містять його рядки. Вищий рівень робить результати точнішими, але жоден рівень не обов'язковий: discover, timeline і загальні монітори працюють на L0.

РівеньРядкиЩо з ними робить Kinro
L0Звичайний текст у будь-якому форматі.Виділяє шаблони, бере мітку часу й рівень із заголовка, вгадує ідентифікатори з формулювань (k=v, k: v, /items/<id>, [name:<id>], word <id>).
L1Структуровані, з полями ідентифікаторів: JSON-рядки, GELF або logfmt.Читає поля як є, тож ключі й зв'язки між сервісами більше не залежать від формулювання повідомлень.
L2Контракт логів Kinro.Використовує явні назви подій і ключі замість виділених шаблонів; нумерація inst/seq виявляє загублені рядки, heartbeat-и відрізняють тихий сервіс від зупиненого, а час події береться з рядка.

Сервіс отримує L2, якщо щонайменше половина його записів — рядки контракту; L1, якщо щонайменше половина записів структуровані й у нього є поля ідентифікаторів; інакше — L0. Звіт пояснює, що дасть кожному сервісу наступний рівень, наприклад:

next tier for 1 L0 services: Structured lines (JSON or logfmt) with ID fields: fields are read as they are instead of being guessed from the text, so entity keys and cross-service links no longer depend on message wording.
next tier for 2 L1 services: The Kinro contract (ContractFormatter, one line in the logging setup): explicit event names and keys instead of mined templates, so there are no template or join mistakes.

Заголовки, які Kinro розпізнає сам: мітки часу ISO, [ts +zone], access-лог [16/Oct/2026:…], celery [ts,ms: LEVEL/…], syslog і мітка часу далі в рядку; слово рівня; JSON-рядки, GELF, logfmt, Docker json-file і текст. Traceback-и й дампи з відступом приєднуються до рядка, якому належать.

i

Рекомендовано, але не обов'язково. Піднімайте сервіс на рівень вище, коли виявлене в ньому залежить від формулювань (з L0 на L1) або коли потрібні виявлення загублених рядків і точний порядок (з L1 на L2). Сервіси, які ви не можете змінити, залишаються корисними як є.

Без правил

Аналіз логів

kinro discover читає каталог файлів логів або робить вибірку з робочого Loki чи Graylog і звітує про знайдене — без правил, без SDK і без обов'язкового формату логів. Він лише читає логи. Результат — три файли в --out і короткий підсумок.

$ kinro discover --logs ./logs --out ./discovery
$ kinro discover --loki https://loki.internal:3100 --since 24h --out ./discovery
$ kinro discover --graylog https://graylog.internal:9000 --query 'environment:prod' --since 24h --out ./discovery

Команди на цій сторінці наведено як команду kinro з образу. У Docker запускайте їх як docker run --rm --user "$(id -u):$(id -g)" -v ... kybalda/utyg:0.1.3 discover ..., як у розділі Швидкий старт.

ФайлЩо містить
discovery.jsonПовна модель: сервіси, шаблони, поля, зв'язки, ключі, процеси, виявлені проблеми. Його читає kinro suggest.
report.htmlТе саме у вигляді автономної сторінки з полем пошуку за замаскованою вибіркою ідентифікаторів.
index.sqliteІндекс часової шкали подій, який читають kinro timeline і kinro suggest.

Що він знаходить

  1. Записи. Файли читаються рекурсивно (приховані й бінарні пропускаються, .gz читаються), кольори термінала видаляються. Рядок без мітки часу отримує мітку попереднього.
  2. Шаблони. Змінні маскуються (uuid, hex, числа, рядки в лапках, параметри URL, IP-адреси, мітки часу), потім дерево Drain групує рядки в шаблони зі стабільними ідентифікаторами. Рядки access-логу групуються за маршрутом.
  3. Поля та ідентифікатори. Пари «назва — значення» з ключів JSON, k=v, k: v, сегментів шляху URL і контекстних тегів. Обсяг пам'яті не залежить від обсягу логів: кожне поле зберігає обмежену вибірку своїх значень.
  4. Зв'язки та ключі. Поля різних сервісів зі спільними значеннями. Зв'язок підтверджується, якщо спільних значень щонайменше --min-shared, вкладення з одного боку не менше за --min-containment, значення з обох боків схожі на ідентифікатори, а перетин більший, ніж дала б випадковість. Слабші кандидати перелічуються як непідтверджені з причиною. Поля, об'єднані підтвердженими зв'язками, утворюють ключі (k1, k2, ...).
  5. Процеси. Для кожного ключа — події кожної сутності в усіх сервісах у порядку часу: переходи з кількістю, P(next | prev), p50/p95/p99 часу між ними й найчастіші шляхи.
  6. Виявлені проблеми. Найчастіші шаблони помилок; сплески помилок (хвилина, у якій помилок не менше за 10 і не менше за 10-кратний звичний рівень помилок сервісу, а пік щонайменше втричі вищий за його звичну хвилину з помилками; «гарячі» хвилини в межах 30 хвилин об'єднуються); повторювані збої (одна сутність натрапляє на той самий збій щонайменше --retry-min разів); періоди тиші (довші і за 30 хвилин, і за 10 медіанних пауз сервісу); секрети й персональні дані.
i

Маскування всюди. Будь-який результат показує щонайбільше чотири останні символи чутливого значення, а значення, коротше за десять символів, не показує взагалі. Секрети видаляються ще до того, як збирається будь-яка статистика. В індексі email-адреси, телефони й імена стають <email>, <phone>, <name> і зберігаються лише як хеш із ключем, тож пошук за введеним email чи телефоном усе одно працює.

Параметри для файлів

ПараметрЗа замовчуваннямЗначення
--logs DIRКаталог файлів логів, читається рекурсивно. --out не може бути всередині нього.
--service-from path|dir|filepathНазва сервісу з каталогів і назви файлу, лише з каталогів або лише з назви файлу. Хеші подів Kubernetes, суфікси ротації та .gz відкидаються: edge/api-7d9f8c6b5-x2k4p.log стає edge/api.
--min-shared N3Скільки спільних значень потрібно, щоб підтвердити зв'язок між сервісами.
--min-containment X0.5Частка значень однієї сторони, яка має бути в іншої.
--retry-min N5Скільки разів одна сутність має натрапити на той самий збій, щоб це вважалося повторюваним збоєм.
--max-mb-per-file N256Більший файл читається лише в останніх N MB.
--index-max-per-value N1000Індекс часової шкали: скільки подій зберігається для одного значення ідентифікатора в кожному сервісі.
--index-max-rows N2000000Індекс часової шкали: скільки подій зберігається загалом.
--index-max-mb N512Індекс часової шкали: коли файл досягає цього розміру, нові події не додаються.

З Loki або Graylog

Ні файли, ні файл проєкту не потрібні: discover може зробити вибірку з наявного сховища логів із доступом лише на читання. Токен завжди читається зі змінної оточення (або з .env); прапорець лише називає цю змінну, тож токен ніколи не потрапляє в історію shell чи список процесів.

$ export LOKI_URL=https://loki.internal:3100 LOKI_TOKEN=...     # LOKI_TENANT for X-Scope-OrgID, if any
$ kinro discover --loki --selector '{namespace="prod"}' --service-label app \
    --since 2026-10-15T00:00Z --until 2026-10-16T00:00Z --sample 20000 --out ./discovery
$ export GRAYLOG_TOKEN=...                                      # an access token of a Reader user
$ kinro discover --graylog https://graylog.internal:9000 --service-field source --fields request_id --out ./discovery
ПараметрЗа замовчуваннямЗначення
--loki [URL], --graylog [URL]Сховище, яке читати. Без URL він береться з $LOKI_URL або $GRAYLOG_URL; --url-env NAME задає іншу змінну.
--since, --until24h, nowВікно: now, тривалість назад від поточного моменту (30m, 24h, 7d) або час ISO (UTC, якщо зону не вказано).
--sample N50000Максимум рядків, що читаються з одного сервісу.
--slices N12На скільки рівномірно розподілених відрізків ділиться вікно під час читання.
--max-mb N256Скільки MB рядків читається загалом, на всі сервіси разом.
--max-qps N2Максимум запитів на секунду (0 — без обмеження).
--timeout S30Секунд на один запит.
--token-env NAMELOKI_TOKEN / GRAYLOG_TOKENЗмінна з токеном: для Loki необов'язкова (bearer), для Graylog обов'язкова.
--tenant-env, --username-envLOKI_TENANT, немаєLoki: заголовок тенанта й користувач basic-auth, паролем якого є токен.
--selector '{...}'усі потоки з міткою сервісуLoki: LogQL, у межах якого читати; фільтр рядків після селектора зберігається.
--service-label NAMEперша з service_name, service, app, job, containerLoki: мітка з назвою сервісу.
--query Q, --streams ID,...*, усіGraylog: пошуковий запит та ідентифікатори потоків, у межах яких читати.
--service-field NAMEsourceGraylog: поле повідомлення з назвою сервісу.
--fields A,BнемаєGraylog: додаткові поля повідомлення, які читаються як поля JSON.
  • Вибірка. Кожен сервіс читає кожен відрізок від початку, доки відрізок не закінчиться або не буде вичерпано його частку --sample і --max-mb; тихий відрізок передає невикористану частку далі. Невеликий сервіс читається повністю, завантажений — рівномірно розподіленими фрагментами. Розділ Source у звіті показує, яку частку вікна покриває кожен сервіс; для сервісу, прочитаного вибірково, періоди тиші не визначаються.
  • Підказки. Якщо Loki їх підтримує, index/volume, detected_fields і patterns додають розміри, кардинальність полів і кількість патернів. Відсутній endpoint позначається як недоступний і більше не запитується.
  • Лише читання за побудовою. Клієнт Loki надсилає тільки GET до query_range, labels, label/<name>/values, index/volume, detected_fields і patterns; клієнт Graylog — тільки POST /api/search/messages (Graylog 5.1+). Будь-який інший запит спричиняє виняток ще до надсилання.

Коди виходу: 0 — готово, 1 — сховище неможливо прочитати, 2 — помилка використання.

Без правил

Часова шкала подій

Введіть ідентифікатор — задачі, запиту чи сутності — і побачите його події в усіх сервісах у порядку часу, без жодних правил. timeline читає index.sqlite, записаний discover, лише на читання.

$ kinro timeline cf9df4e64fe6 --index ./discovery
kinro timeline cf9df4e64fe6: 2 events in 2 services (edge/api, notify/mailer), 2026-10-16T08:59:16.502Z .. 2026-10-16T08:59:17.988Z
key k1 job (confirmed link across services)
2026-10-16T08:59:16.502Z  edge/api       info     job submitted  [job_ref]
2026-10-16T08:59:17.988Z  notify/mailer  info     job cf9df4e64fe6 delivered  [job]
related ids in the same records (keys followed one hop)
  95513  edge/api:latency_us  1 record
  cb8389fbea81ad63  edge/api:trace  1 record

Перша подія надходить з edge/api, де задачу вказано в полі job_ref; discover підтвердив, що job_ref і поле job у mailer — один ключ, тож видно обидві події. Кожен рядок події показує час, сервіс, рівень, очищений фрагмент і поле, яке збіглося.

ПараметрЗа замовчуваннямЗначення
valueІдентифікатор: uuid, число, hex або токен; порівнюється в нижньому регістрі. Email чи телефон порівнюються за хешем із ключем.
--index DIRпоточний каталогКаталог discover --out або файл index.sqlite.
--expand / --no-expandувімкненоІдентифікатори з тих самих записів, що належать до підтвердженого ключа між сервісами, додають і свої події з інших полів цього ключа — на один крок і не далі. Інші ідентифікатори перелічуються як пов'язані.
--limit N200Показати останні N подій.
--jsonвимкненоЧасова шкала у форматі JSON.

Коди виходу: 0 — знайдено; 1 — не знайдено, із найближчими значеннями з індексу й приміткою, якщо індекс досяг якогось ліміту; 2 — помилка використання або індекс відсутній.

$ kinro timeline 9d37ff01fe8 --index ./discovery
kinro timeline 9d37ff01fe8: not found in the index
  closest indexed values: 9d37ff01fe80
i

Що містить індекс. Значення, схожі на ідентифікатори (uuid, числа з 4+ цифр, hex, токени з цифрами, зокрема одноразові ідентифікатори запитів), кожне із сервісом, полем, часом, шаблоном, рівнем і однорядковим фрагментом, очищеним так само, як у звіті: секрети туди ніколи не потрапляють. Поле пошуку в report.html охоплює вибірку (до 20 значень кожного підтвердженого ключа, по 30 останніх подій для кожного); повний пошук — це kinro timeline.

Без правил

Стеження без правил

kinro run без --project — це режим без налаштувань. Він читає всі сервіси з Loki, Graylog і/або glob-шаблону файлів логів, указаних у змінних KINRO_*, виділяє шаблон для кожного рядка й запускає чотири загальні монітори за часом події. Те, що вони знаходять, — це інсайт: він зберігається в стані, потрапляє в щоденний дайджест і надсилається одразу лише для моніторів, які ви для цього обрали. Власні правила (--project) працюють як раніше; в одному процесі ці два режими не поєднуються.

$ kinro check                  # validates the KINRO_* variables offline
$ kinro run                    # learn, then report
$ kinro status                 # monitors, learning progress, insight counts, next digest ($KINRO_STATE_PATH)
$ docker run -d --init --env-file .env -v kinro-state:/state -e KINRO_STATE_PATH=/state/kinro.db kybalda/utyg:0.1.3 run

Команді образу за замовчуванням потрібен файл проєкту, тому для режиму без налаштувань указуйте run явно.

Монітори

МоніторКоли спрацьовуєПороги
new_error_templateПісля навчання вперше з'являється шаблон рівня error або вище.Один інсайт на сервіс за хвилину з переліком нових шаблонів; не більше одного на сервіс за годину.
error_spikeКількість помилок сервісу за хвилину значно перевищує його власний ковзний базовий рівень.3 хвилини поспіль із кількістю помилок ≥ max(10, 3 × медіана, медіана + 6 × 1.4826 × MAD, 3 × звичайна ненульова хвилина з помилками, щойно таких набереться 30), за останні 24 год завершених хвилин (щонайменше 60).
silent_serviceСервіс перестає писати логи на довше, ніж будь-коли робив паузу.Після запису, що надійшов уже після навчання, і 100 записів: тиша > max(15 хв, 3 × найдовша вивчена пауза); один раз на кожен період тиші.
repeated_failureОдне значення ідентифікатора знову й знову натрапляє на той самий шаблон помилки чи попередження: цикл повторів.5 збігів протягом 1 год від першого. Про триваючий цикл одного шаблону повідомляється один раз, повторно — лише після 24 год без такого циклу.
загублені логиinst/seq контракту виявляють загублені рядки.Мета-алерт kinro.source_gap.

Навчання, дайджест і негайні сповіщення

  • Навчання. Протягом KINRO_LEARN_HOURS (24) від першого запису монітори сервісу лише навчаються; те, про що вони повідомили б, рахується як тіньові інсайти навчання в kinro status і дайджесті.
  • Дайджест. Одне повідомлення на день о KINRO_DIGEST_AT (09:00 UTC): нові інсайти, згруповані за моніторами, триваючі (сервіси, що мовчать, поточні сплески й цикли), кількість тіньових інсайтів навчання та інсайтів, обмежених за частотою. Його ідентифікатор виводиться з дати, тож після перезапуску він ніколи не надсилається двічі.
  • Негайні сповіщення. Одразу доставляються лише монітори, перелічені в KINRO_PAGE (наприклад, error_spike,silent_service або all). За замовчуванням — жодного: усе чекає на дайджест. Мета-алерти про стан джерел (kinro.source_stalled, kinro.source_gap, kinro.parse_errors) завжди доставляються одразу, зокрема й під час навчання.
  • Канали. Ті самі, що й з правилами: Telegram і Slack, якщо задано їхні змінні, файл у KINRO_ALERTS_PATH і лог.
[KINRO INSIGHT] repeated_failure: notify/mailer
notify/mailer: job=****fe80 hit "job <hex> failed: upstream unavailable" 5 times in 4m (a retry loop?)
at: 2026-10-16T12:52:36.488000+00:00, detected: 2026-10-16T12:56:36.488000+00:00
template: {"id": "t:a34f17d99e", "template": "job <hex> failed: upstream unavailable", "example": "job ****fe80 failed: upstream unavailable", "total": 5, "errors": 5}
field: "job"
value: "****fe80"
hits: 5
window: ["2026-10-16T12:52:36.488000+00:00", "2026-10-16T12:56:36.488000+00:00"]
window_limit_minutes: 60.0
retry_min: 5
alert_id: 062fc30ce6e15caa

Оточення

Джерела; їх можна поєднувати, і кожне читає багато сервісів:

ЗміннаЗа замовчуваннямЗначення
KINRO_LOKI_URLнемаєБазовий URL Loki; лише GET-запити на читання до query_range.
KINRO_LOKI_SELECTOR{<label>=~".+"}Селектор LogQL: усі потоки з міткою сервісу.
KINRO_LOKI_SERVICE_LABELservice_nameМітка потоку з назвою сервісу.
KINRO_LOKI_TOKEN, KINRO_LOKI_TENANT, KINRO_LOKI_USERNAMEнемаєТокен лише на читання (bearer або basic auth із користувачем) і X-Scope-OrgID.
KINRO_GRAYLOG_URL, KINRO_GRAYLOG_TOKENнемаєБазовий URL Graylog і access-токен лише на читання; задайте обидва або жодного.
KINRO_GRAYLOG_QUERY, KINRO_GRAYLOG_STREAMS*, усіПошуковий запит та ідентифікатори потоків через кому.
KINRO_GRAYLOG_SERVICE_FIELDsourceПоле повідомлення з назвою сервісу.
KINRO_LOG_GLOBнемаєGlob-шаблон файлів, наприклад /logs/*/*.log (без рекурсивного **).
KINRO_LOG_SERVICE_FROMpathНазва сервісу зі шляху нижче фіксованої частини glob-шаблону: path, dir або file; хеші подів відкидаються, як у discover.
KINRO_LOG_STARTendЗвідки читаються наявні файли під час першого запуску: end або beginning.
KINRO_LOOKBACK2 × затримка прийманняLoki і Graylog: як далеко в минуле починає читати перший запуск (30m, 24h); довший період дає змогу одразу навчитися на історії.
KINRO_MAX_QPS2Запитів на секунду на одне сховище (0 — без обмеження).

Монітори й доставка:

ЗміннаЗа замовчуваннямЗначення
KINRO_LEARN_HOURS24Для кожного сервісу, від його першого запису: монітори лише навчаються.
KINRO_PAGEнемаєМонітори, що доставляються одразу: назви через кому, all або none.
KINRO_DIGEST_AT09:00Час щоденного дайджесту за UTC, HH:MM.
KINRO_STATE_PATHkinro.dbФайл стану: шаблони, вивчена статистика, інсайти, outbox. Його читає й kinro status.
KINRO_ALERTS_PATHнемаєДодатково дописувати кожне доставлене повідомлення в цей JSONL-файл.

Як читаються рядки

Рядки контракту зберігають свої назви подій, ключі, inst і seq. Будь-який інший рядок проходить через парсер заголовків discover, виділювач шаблонів (подією стає t:<template id>) і екстрактор полів (ідентифікатори: uuid, числа, hex, токени; ніколи не email-адреси й не поля з назвами, схожими на персональні дані). Важливість рядка — це його рівень; якщо рівня немає, traceback, статус 5xx або слово про збій піднімають її до error. Кожен рядок доказів маскується (<secret>, <email>, <phone>), а ідентифікатори приховуються (****1234) ще до того, як потраплять у стан, outbox чи нотифікатор.

  • Перезапуски. Шаблони, лічильники, базові рівні, паузи й лічильники повторів фіксуються разом із курсорами джерел в одній транзакції SQLite, тож після перезапуску той самий рядок отримує той самий ідентифікатор шаблону. Кожен інсайт має детермінований alert_id, тому перезапуск ніколи його не дублює.
  • Обмежена пам'ять. 300 сервісів (решта рахуються як <other services>), 1000 шаблонів на сервіс, 20 000 відстежуваних ідентифікаторів, 24 год похвилинних лічильників помилок на сервіс.
  • Виміряний шум. На синтетичних справних сервісах, де всі монітори надсилали негайні сповіщення, 3 дні та 48 тис. рядків дали 0 інсайтів, а 14 днів і 223 тис. рядків — 1: навмисно підкладену рідкісну помилку, що вперше з'являється на 3-й день.
  • Відомі обмеження. Сервіс, вивчений у будні, на вихідних може бути позначений як такий, що замовк; кожен рядок файлу — окремий запис; сховища перечитуються в минуле лише в межах KINRO_LOOKBACK.
Без правил

Запропоновані правила

kinro suggest перетворює каталог discover --out на файл проєкту, який можна переглянути, перевірити й запустити. Його правила — вбудовані chain, no_duplicates і ordering з порогами, вивченими на ваших даних, і кожне правило містить свої докази в коментарі. Файл записується, лише якщо проходить валідацію.

$ kinro suggest --from ./discovery --out project.py --logs-root /logs
suggest: 2 services (1 left out), 1 keys (job), 4 rules (1 chain, 2 no_duplicates, 1 ordering), 3 candidates skipped (support: 3); 300 entities, 0 ambiguous
wrote project.py: 2 services, 4 shadow rules; validated. Next: kinro check --project project.py, then kinro replay --project project.py
$ kinro check --project project.py
suggested: 4 rules, 2 services, 0 dbs; 0 errors, 0 warnings
$ kinro replay --project project.py           # shadow violations print as SHADOW <rule> ...
SHADOW chain_job_submitted_2c772f job=9d37ff01fe80 008fe6efee80e596: missing step notify/mailer/job_delivered within 0:00:11
replay: 1 violations (1 shadow), 0 meta-alerts, 0 late follow-ups; 600 observations, 0 late, 0 rule errors

Єдине тіньове порушення — це задача, яка постійно збоїла: її доставлено лише після циклу повторів, значно пізніше за вивчені 11 секунд.

Фрагмент записаного файлу:

project.service(
    'notify/mailer',
    LogSource('/logs/notify/mailer-*.log', parser=TemplateParser(
        {
            # t733289dd8b, 300 records, rule step; parser check 100.0% of 300 lines
            'job_delivered': 'job <*> delivered',
        },
        fields=['job'],
    )),
    keys={
        'job': Field('job', normalize=str.lower),
    },
    # Discovery found secrets or personal data under these names.
    redact=['email'],
)

project.add(
    # 'job_submitted' (edge/api) is followed by 'job_delivered' (notify/mailer) within 11s: 298 of 299 flows by job
    # (cross-service key, confidence 0.997); latency to the slowest step p50 1.5s, p95 1.65s, p99 1.75s; within =
    # p99 x 3 + 5s, rounded up. Examples ****b10e, ****960e, ****01a2; source 3 files under /logs.
    chain(
        'chain_job_submitted_2c772f',
        key='job',
        steps=[API_JOB_SUBMITTED, MAILER_JOB_DELIVERED],
        within=timedelta(seconds=11),
        shadow=True,
    ),
)

Що потрапляє у файл

  • Сервіси. Один project.service(...) на кожен знайдений сервіс, потрібний якомусь правилу: LogSource(<glob>) для файлів (хеш пода стає *; --logs-root вказує, де Kinro знайде файли), LokiLogs(LokiConfig(selector=...), url_env='LOKI_URL') або GraylogLogs(GraylogConfig(query=...), url_env='GRAYLOG_URL', token_env='GRAYLOG_TOKEN'), якщо discover читав сховище. Сервіси з контрактом зберігають StandardParser; усі інші отримують TemplateParser зі знайденими шаблонами, які використовують їхні правила. Сервіси, не потрібні жодному правилу, не включаються, а лише згадуються в коментарі.
  • Ключі. Один ключ на кожну групу підтверджених зв'язків, названий за її полями. Текстові й hex-ідентифікатори читаються через Field(name, normalize=str.lower), оскільки discover порівнював їх без урахування регістру. Поля, у яких discover знайшов секрети чи персональні дані, потрапляють у redact=[...].
  • chain, якщо кожен крок слідує за першим щонайменше у 98% з не менш ніж 50 процесів; within = p99 затримки до найповільнішого кроку × 3 + 5 с, округлене вгору до цілих секунд. Ланцюжки, повільніші за 1 год, залишаються кандидатами.
  • no_duplicates, якщо менше ніж 0.1% сутностей повторюють подію протягом 1 год (інакше — найдовше з вікон 30, 10, 5 або 1 хв, що проходить перевірку).
  • ordering для послідовних кроків ланцюжка в різних сервісах, якщо не більше ніж у 0.1% процесів їхній порядок інвертовано більш ніж на 1 с; лише для сервісів, рядки яких містять власний час.
  • Перевірка парсера. Кожен шаблон проганяється на проіндексованих рядках свого сервісу; якщо шаблон не відображається сам на себе в 99% із них, його правила переходять у кандидати.
  • Кандидати. Правила нижче порогу перелічуються закоментованими, кожне з причиною. Повторювані збої подаються як закоментоване власне правило RepeatedFailure, перевірки в базі даних — як закоментовані приклади db_invariant/status_desync.

Ідентифікатор правила — це slug і короткий хеш типу правила, ключа й подій, тож повторний запуск на схожих даних дає ті самі ідентифікатори, і два запуски можна порівняти.

ПараметрЗа замовчуваннямЗначення
--from DIRобов'язковийКаталог discover --out (discovery.json і index.sqlite).
--out FILEобов'язковийФайл проєкту для запису; наявний перезаписується лише з --force.
--logs-root DIRкаталог, який читав discoverДе Kinro знайде файли логів, наприклад /logs у контейнері.
--name, --state-path, --alerts-pathsuggested, /state/kinro.db, /state/alerts.jsonlНазва проєкту, файл стану й файл алертів, у який пише тіньовий канал.
--min-confidence0.98Частка процесів, у яких крок ланцюжка має завершитися.
--min-support50Скільки процесів чи сутностей потрібно, щоб вивчити правило.
--margin3within ланцюжка = p99 затримки × margin + max_lateness.
--max-within3600Секунди; повільніші ланцюжки лише кандидати.
--max-duplicate-rate0.001no_duplicates лише тоді, коли частка сутностей із повторами нижча за цю.
--max-inversion-rate0.001ordering лише тоді, коли частка процесів з інвертованим порядком не перевищує цієї.
--tolerance1Допуск ordering у секундах.

Коди виходу: 0 — записано, 1 — запропонований проєкт не пройшов валідацію (нічого не записано), 2 — помилка використання.

Тіньові правила й тіньовий канал

Кожне запропоноване правило має shadow=True. Воно працює як звичайно, але його порушення потрапляють лише в канал shadow, який пише у файл алертів (alerts_path) і в лог (SHADOW ALERT, із заголовком [KINRO SHADOW]) і ніколи — у Telegram чи Slack. Такі порушення зберігають свій alert_id і рахуються в kinro status:

shadow violations (shadow channel only): chain_job_submitted_2c772f=1

Тіньовим може бути будь-яке правило: chain(..., shadow=True), MyRule('id', shadow=True) або shadow = True на класі.

Переведення правила в робочий режим

  1. Запустіть у тіньовому режимі

    Змонтуйте каталог із project.py у /app, а логи — у --logs-root, як у розділі Написання власних правил. Якщо Kinro у режимі без налаштувань і далі працює, дайте проєкту окремий файл стану через --state-path: два процеси ніколи не мають ділити один файл.

  2. Перегляньте

    Кілька днів читайте тіньові алерти у файлі алертів, як під час тіньового тижня. Перевіряйте змінений поріг через kinro replay.

  3. Переведіть у робочий режим

    Видаліть у правилі shadow=True, запустіть kinro check і перезапустіть Kinro. Відтоді його алерти йдуть в усі канали. Так само розкоментуйте кандидата, щоб випробувати його в тіньовому режимі.

Власні правила

Написання власних правил

Правила необов'язкові. Пишіть їх, коли процесу потрібна перевірка, якої не покриває жодна пропозиція: порівняння з базою даних, поле payload (корисних даних події), обіцянка, про яку знаєте лише ви. Почніть із файлу, який записав kinro suggest, або з шаблону, який створює kinro init. Нижче розглянуто процес із двох сервісів: orders пише логи в Loki, billing пише в текстовий файл старого формату, а Kinro надсилає алерт, якщо створене замовлення не оплачено протягом 15 хвилин.

  1. Встановіть SDK

    Для правил і файлів проєкту потрібен лише пакет kinro-sdk: без рушія й без залежностей, з типами для IDE, під ліцензією Apache License 2.0. Сам рушій працює як Docker-образ Kinro.

    Пакет опубліковано на PyPI, тож достатньо однієї команди:

    $ uv add kinro-sdk

    Зафіксуйте версію, яку використовує ваш образ Kinro, наприклад kinro-sdk==0.1.3. Без доступу до інтернету встановіть wheel із вашого релізу Kinro або з приватного індексу:

    $ pip install kinro_sdk-0.1.3-py3-none-any.whl
    $ pip install kinro-sdk --index-url https://<your-private-index>/simple

    Щоб локально запускати тести правил із RuleHarness, встановіть ще пакет рушія kinro; він підтягне відповідну версію SDK.

  2. Опишіть проєкт

    Файл проєкту — це Python-модуль зі змінною project на рівні модуля.

    from datetime import timedelta
    
    from kinro.sdk import Field, LogSource, LokiConfig, LokiLogs, Project, RegexParser, chain, on
    
    project = Project('shop', state_path='/state/kinro.db', alerts_path='/state/alerts.jsonl')
    
    project.service(
        'orders',
        LokiLogs(LokiConfig(selector='{service="orders"}')),
        keys={'order': Field('uid', normalize=str.lower)},
    )
    project.service(
        'billing',
        LogSource('/logs/billing/billing.log', parser=RegexParser(
            r'^(?P<ts>\S+ \S+) (?P<level>\w+) (?P<event>payment captured) ext=(?P<external_id>\w+)$',
            events={'payment captured': 'payment.captured'},
            ts_format='%Y-%m-%d %H:%M:%S,%f',
        )),
        keys={'order': Field('external_id', normalize=str.lower)},
    )
    
    project.add(
        chain('order_paid', key='order',
              steps=[on('orders', 'order.created'), on('billing', 'payment.captured')],
              within=timedelta(minutes=15)),
    )
  3. Створіть каталог правил

    Створіть каталог правил із шаблону, вбудованого в образ зі Швидкого старту. У ньому є project.py з робочими прикладами, rules/, parsers/, tests/, compose.yaml, .env.example і Dockerfile. Спершу створіть каталог самі, щоб він належав вашому користувачу.

    $ mkdir shop-rules
    $ docker run --rm --user "$(id -u):$(id -g)" -v "$PWD/shop-rules:/out" kybalda/utyg:0.1.3 init /out
    $ cd shop-rules
  4. Додайте compose.yaml і .env

    Тримайте все в одному каталозі правил і монтуйте його цілим, лише на читання, у /app. Це рекомендований варіант: Kinro додає /app до шляху імпорту, тож rules/ і parsers/ імпортуються, а новий файл із правилом не потребує змін у compose.yaml. Каталог логів теж монтується лише на читання; стан зберігається у volume.

    shop-rules/
      project.py        # services, databases and the rules to run
      rules/            # custom rules, one module per business flow
      parsers/          # custom parsers for legacy log formats
      compose.yaml
      .env              # secrets; keep it out of git
    services:
      kinro:
        image: kybalda/utyg:0.1.3
        env_file: .env
        init: true                  # SIGTERM reaches Kinro, which commits before exit
        restart: unless-stopped
        volumes:
          - kinro-state:/state
          - ./:/app:ro                # the rules folder: project.py, rules/, parsers/
          - /var/log/billing:/logs/billing:ro
    volumes:
      kinro-state:
    LOKI_URL=https://loki.internal
    # TELEGRAM_BOT_TOKEN=...
    # TELEGRAM_CHAT_ID=...

    Оновлення правил. Змініть або додайте файли в каталозі, перевірте, потім перезапустіть. Kinro завантажує правила під час старту, тож перезапуск їх застосовує; стан і дедлайни, що очікують, його переживають. Підвищуйте version правила, коли змінюється його логіка: це скидає збережений стан цього правила. Тримайте каталог у git, щоб кожну зміну правил переглядали й могли відкотити.

    $ docker compose run --rm kinro check --project /app/project.py
    $ docker compose restart kinro
  5. Перевірте конфігурацію

    check перевіряє правила, джерела та оточення, нікуди не підключаючись. Entrypoint образу — kinro, тому підкоманда йде одразу після назви сервісу.

    $ docker compose run --rm kinro check --project /app/project.py
    shop: 1 rules, 2 services, 0 dbs; 0 errors, 0 warnings
  6. Запустіть

    За замовчуванням контейнер виконує kinro run --project /app/project.py. Без налаштувань Telegram чи Slack алерти потрапляють у лог і в alerts_path. Саме так рекомендовано провести перший тиждень; див. Тіньовий тиждень. Правила з shadow=True залишаються в тіньовому каналі (файл алертів і лог), навіть якщо чати налаштовано.

    $ docker compose up -d
    $ docker compose logs -f kinro
Власні правила

Контракт логів

LogContract v1 — формат рядків, який Kinro читає нативно: один JSON-об'єкт на рядок. Він рекомендований, але не обов'язковий: це рівень L2, найвищий. Сервіси пишуть його через ContractFormatter. Discover, timeline і загальні монітори читають будь-який інший формат як є; для правил на такому сервісі використовуйте парсер, наприклад TemplateParser, який записує kinro suggest.

{"kinro_v": 1, "ts": "2026-10-15T12:00:00.123456+00:00", "svc": "orders", "event": "order.created",
 "level": "INFO", "cid": "0192f0c4-7c1e-7d3a-9b1e-5f2a1c3d4e5f", "keys": {"uid": "A1B2"},
 "data": {"amount": "10.50"}, "msg": "order A1B2 created", "inst": "orders-7f9c:12", "seq": 41}
ПолеОбов'язковеЗначення
kinro_vтакВерсія контракту, завжди 1.
tsтакЧас події у форматі RFC 3339 зі зміщенням. Час без зміщення відхиляється.
svcтакСервіс-джерело. Kinro використовує назву сервісу з конфігурації, а не це поле.
eventтакНазва події через крапку, наприклад order.created. Назва kinro.heartbeat зарезервована.
cidніІдентифікатор кореляції, що передається між сервісами (UUID7 або hex).
keysніБізнес-ключі, наприклад {"uid": "A1B2"}. Мапінги ключів посилаються на ці назви.
dataніPayload, який правила читають як obs.payload.
msg, excніТекст для людини й traceback. Правила їх ніколи не читають.
inst, seqніhost:pid і лічильник без пропусків у межах процесу. Пропуск означає, що рядки загубилися дорогою до Loki.
i

Обмеження. Рядок, більший за 1 MiB або з некоректними полями, вважається помилкою парсингу й ніколи не зупиняє приймання логів. Payload обмежено 2 KiB: спершу відкидаються найбільші поля data, і встановлюється truncated. Якщо за 5 хвилин помилок парсингу більше 1% і у вікні щонайменше 100 рядків, Kinro створює алерт kinro.parse_errors.

Власні правила

Ключі кореляції

У кожного правила є key: бізнес-сутність, за якою воно стежить, наприклад order. Кожен сервіс вказує, яке з його полів містить цей ключ. Kinro зв'язує спостереження двома способами.

  • Спільний ідентифікатор. Сервіси, що логують однаковий cid, зв'язуються напряму. Сервіс, який логує лише cid (shipping у прикладі нижче), однаково потрапляє в timeline замовлення через інший сервіс, що логує обидва значення.
  • Мапінг ключів. Якщо назви полів різняться, зіставте їх з одним логічним ключем: uid в orders і external_id у billing означають order. Field(name, normalize=...) вирівнює регістр або формат; нормалізуйте всі сторони однаково, інакше A1B2 і a1b2 залишаться двома різними ключами.
project.service('orders', orders_logs, keys={'order': Field('uid', normalize=str.lower)})
project.service('billing', billing_logs, keys={'order': Field('external_id', normalize=str.lower)})
project.service('shipping', shipping_logs)          # only cid; joined through orders

# A rule keyed by cid sees shipments grouped by correlation id instead of order.
no_duplicates('single_shipment', key='cid', trigger=on('shipping', 'shipment.created'),
              within=timedelta(hours=1))

Пошук поля перевіряє спершу cid, потім keys, потім payload, тож групу regex, якої немає в keys, теж можна змапити.

!

Конфлікти. Коли один cid пов'язує два різні замовлення, група стає неоднозначною. Kinro враховує це в link_conflicts і не надсилає алертів про дедлайни, які не може перевірити.

Власні правила

Час події та дедлайни

Правила працюють за часом, коли подія сталася, а не коли надійшов рядок логу. Кожен сервіс має watermark: момент часу, до якого Kinro вже бачив усі події. Watermark відстає від найновішого часу події на max_lateness.

  • Дедлайн спрацьовує лише тоді, коли його мине watermark кожного сервісу, який ще може вплинути на результат. Повільний конвеєр логів затримує алерти, але ніколи не спричиняє хибних.
  • Для сервісів у Loki heartbeat-и від start_heartbeat просувають уперед watermark продюсера, у якого немає подій.
  • Крок, що надійшов уже після спрацювання свого дедлайну, створює додатковий алерт <alert_id>#late1.
  • Коли Kinro знає, що логи загубилися (пропуск у seq, retention у Loki), дедлайни, що перетинаються з цим пропуском, неможливо перевірити: замість багатьох хибних алертів надходить один kinro.source_gap.
LATENESS = timedelta(seconds=10)    # about 2x the p99 push delay you see in Grafana

# loki() is the helper from the Grafana Loki section below.
project.service('orders', loki('orders'), keys={'order': 'uid'}, max_lateness=LATENESS)

Значення за замовчуванням: max_lateness=5s, poll_interval=0.5s. Кожен алерт про дедлайн чекає щонайменше max_lateness.

Власні правила

Вбудовані правила

П'ять правил покривають типові збої. Вони написані на тому самому публічному SDK, що й ваші власні правила. Перші три kinro suggest запише за вас, із вивченими порогами.

CREATED = on('orders', 'order.created')
PAID = on('billing', 'payment.captured')
SHIPPED = on('shipping', 'shipment.created')

project.add(
    # Every step arrives within 30 minutes of the first one.
    chain('order_flow', key='order', steps=[CREATED, PAID, SHIPPED], within=timedelta(minutes=30)),

    # At most one payment per order per hour.
    no_duplicates('single_payment', key='order', trigger=PAID, within=timedelta(hours=1)),

    # Payment comes before shipment; 2 s of clock skew is tolerated.
    ordering('paid_before_shipped', key='order', first=PAID, then=SHIPPED, tolerance=timedelta(seconds=2)),

    # The status a service logged is in the database 60 s later.
    status_desync(
        'order_status_in_db', key='order', trigger=on('orders', 'order.status_changed'), field='status',
        db='orders_db', settle=timedelta(seconds=60),
        lookup='SELECT uid AS key, status, updated_at FROM orders WHERE uid = ANY(:keys)',
    ),

    # Rows that must never exist.
    db_invariant(
        'paid_orders_have_amount', key='order', db='orders_db', every=timedelta(minutes=1),
        sql="SELECT uid AS key, amount FROM orders WHERE status = 'paid' AND amount <= 0",
    ),
)
ПравилоЩо виявляєПараметри
chainКрок, якого немає після завершення вікнаkey, steps, within, window
no_duplicatesОдна й та сама подія двічі для одного ключаkey, trigger, within
orderingПодії в неправильному порядкуkey, first, then, tolerance, window
status_desyncЛог і база даних не збігаютьсяkey, trigger, field, db, lookup, settle, updated_at
db_invariantРядки, що порушують правилоkey, db, sql, every

Алерт chain має вигляд missing step shipping/shipment.created within 0:30:00, а зламаним кроком називає останній крок, який таки надійшов. Кожне вбудоване правило також приймає shadow=True: тоді його порушення потрапляють лише в тіньовий канал.

Власні правила

Класи правил

Правило — це клас. Воно оголошує свій ключ і тригери, реагує на спостереження та власні дедлайни, зберігає стан для кожного ключа й викликає ctx.violate, коли щось не так. Рушій ізолює правила: виняток в одному створює алерт kinro.rule_error і ніколи не зупиняє інші.

Для кожного повернення коштів протягом 10 хвилин має з'явитися тікет у підтримці.

from datetime import timedelta

from kinro.sdk import Observation, Rule, RuleContext, on

REFUNDED = on('payments', 'refund.issued')
TICKET = on('support', 'ticket.created')


class RefundHasTicket(Rule):
    key = 'order'
    version = 1
    triggers = (REFUNDED, TICKET)
    window = timedelta(hours=2)

    def on_observation(self, ctx: RuleContext, obs: Observation) -> None:
        if obs.event == REFUNDED.event:
            ctx.set_deadline('ticket', obs.ts + timedelta(minutes=10), wait_for=['support'])
        elif obs.event == TICKET.event:
            ctx.cancel_deadline('ticket')
            ctx.close()

    def on_deadline(self, ctx: RuleContext, name: str) -> None:
        ctx.violate('refund issued without a support ticket within 10 minutes',
                    expected='support/ticket.created')
        ctx.close()


project.add(RefundHasTicket('refund_has_ticket'))

Атрибути правила

АтрибутЗначення
keyЛогічний ключ, за яким правило групує дані, наприклад 'order' або 'cid'.
versionЗбільшуйте, коли змінюється логіка; збережений стан старої версії скидається.
triggersПари on(service, event), які отримує правило.
windowСкільки зберігаються стан і timeline для ключа.
schedules, lookupsОголошення Query і Lookup для перевірок у базі даних.
requires_body_tsВідхиляти сервіси, у яких час події — це лише час push у Loki.
shadowЯкщо true, порушення потрапляють лише в тіньовий канал (файл алертів і лог). Також MyRule('id', shadow=True).

RuleContext

ЕлементЩо робить
ctx.now, ctx.timelineГодинник правила за часом подій і наскрізний timeline ключа між сервісами.
ctx.stateJSON-словник для кожного ключа; зберігається з кожним комітом.
ctx.set_deadline(name, at, wait_for=None)Викликати on_deadline(name), щойно watermark-и wait_for минуть at.
ctx.cancel_deadline(name)Скасувати дедлайн, що очікує.
ctx.request_lookup(name, not_before=...)Запросити Lookup для цього ключа.
ctx.violate(message, broken_step=None, expected=None)Створити алерт. Ідентифікатор алерту виводиться з правила, версії, ключа й причини, тому він не змінюється між перезапусками.
ctx.close(tombstone=True)Завершити ключ; tombstone змушує ігнорувати подальші спостереження для нього.
ctx.count(name), ctx.unverifiable(services)Збільшити лічильник статистики; дізнатися, чи не приховує пропуск у джерелі частину вікна.
ctx.lateTrue, якщо спостереження надійшло після того, як дедлайн уже спрацював.

Хуки: on_observation(ctx, obs), on_deadline(ctx, name) і on_unverifiable(ctx, name); останній викликається замість on_deadline, коли втрачені логи перетинаються з вікном дедлайну.

Власні правила

Тестування правил

RuleHarness проганяє одне правило через справжній рушій без Loki, файлів, бази даних і нотифікаторів. Подавайте спостереження, зсувайте watermark, читайте порушення. Він постачається з пакетом рушія kinro, а не з kinro-sdk.

from datetime import UTC, datetime, timedelta

from kinro.contract.model import FileObsId, Observation
from kinro.testing import RuleHarness

from rules.refunds import RefundHasTicket

T0 = datetime(2026, 10, 16, 12, 0, tzinfo=UTC)


def obs(service: str, event: str, seconds: int, order: str, offset: int) -> Observation:
    return Observation.create(
        obs_id=FileObsId(service, 1, 0, offset), service=service, event=event,
        ts=T0 + timedelta(seconds=seconds), payload={'order': order},
    )


def test_refund_without_ticket_alerts() -> None:
    harness = RuleHarness(RefundHasTicket('refund_has_ticket'))
    harness.feed(obs('payments', 'refund.issued', 0, 'A1B2', 1))
    harness.advance(T0 + timedelta(minutes=11))

    [violation] = harness.violations
    assert violation.message.startswith('refund issued without a support ticket')


def test_ticket_in_time_is_quiet() -> None:
    harness = RuleHarness(RefundHasTicket('refund_has_ticket'))
    harness.feed(obs('payments', 'refund.issued', 0, 'A1B2', 1), obs('support', 'ticket.created', 120, 'A1B2', 2))
    harness.advance(T0 + timedelta(minutes=11))

    assert harness.violations == []

Без проєкту harness бере rule.key з однойменного поля payload у кожному сервісі-тригері. Передайте свій справжній project, щоб тестувати з його мапінгами ключів. harness.poll(db, name, rows, at) і harness.lookup(...) подають результати з бази даних.

Джерела

Логування із сервісу

Python-сервісу потрібні один форматер на Loki-хендлері та один heartbeat. kinro.contract використовує лише стандартну бібліотеку.

import logging

import logging_loki
from kinro.contract import ContractFormatter, start_heartbeat

loki = logging_loki.LokiHandler(url=LOKI_PUSH_URL, tags={'service': 'orders'}, version='1')
loki.setFormatter(ContractFormatter('orders'))      # one formatter instance per handler
logging.getLogger().addHandler(loki)
start_heartbeat(logging.getLogger())                # every 2 s, ignores LOG_LEVEL

Далі вкажіть бізнес-подію в тих викликах логування, які сервіс уже робить:

logger.info('order %s created', uid, extra={
    'event': 'order.created',
    'cid': cid,
    'keys': {'uid': uid},
    'data': {'amount': str(amount)},
})

Записи без event зберігають звичний JSON-формат, і Kinro їх ігнорує. Сервіси іншими мовами пишуть такий самий JSON-рядок: контракт і є весь інтерфейс.

i

Задайте Loki-хендлеру таймаут запиту. logging_loki його не встановлює, тож Loki, що завис, назавжди блокує потік логування.

Джерела

Grafana Loki

LokiLogs читає селектор потоків тільки через /loki/api/v1/query_range із токеном лише на читання. На кожну пару «URL Loki + облікові дані» створюється один клієнт, і він дотримується max_qps.

def loki(service: str) -> LokiLogs:
    return LokiLogs(
        LokiConfig(
            selector=f'{{service="{service}", environment="production"}}',
            ingest_lateness=timedelta(seconds=10),
            retention=timedelta(days=7),
            start_lookback=timedelta(minutes=15),
        ),
        url_env='LOKI_URL',
        token_env='LOKI_TOKEN',
        tenant_env='LOKI_TENANT',
        max_qps=2.0,
    )
ПараметрЗа замовчуваннямЗначення
selectorобов'язковийСелектор потоків LogQL. Фільтруйте лише за мітками з низькою кардинальністю.
parserStandardParser()Парсер рядків; за замовчуванням LogContract.
ingest_lateness5 сНаскільки пізно рядок може потрапити в Loki після своєї мітки часу. Має бути не меншим за max_lateness сервісу.
sweep_intervalingest_latenessЯк часто Kinro перечитує останнє вікно, щоб підхопити запізнілі рядки.
limit1000Розмір сторінки, не більше за серверний max_entries_limit_per_query.
retentionнемаєRetention у Loki; читання за його межами створює алерт kinro.source_gap.
start_lookback2 × ingest_latenessЗвідки починається читання під час першого розгортання.
url_env, token_env, tenant_envLOKI_URL, немає, немаєЗмінні оточення з URL, bearer-токеном і тенантом X-Scope-OrgID. Токен і тенант надсилаються, лише якщо їх вказано.
max_qps2.0Ліміт запитів на URL Loki та облікові дані.
Джерела

Graylog

Сервіси, що пишуть логи в Graylog через GELF (UDP, TCP або HTTP), читаються через GraylogLogs. Kinro викликає лише пошуковий API POST /api/search/messages з access-токеном лише на читання. Він ніколи не пише в Graylog і сам не приймає GELF. Потрібен Graylog 5.1 або новіший.

У сервісі

graypy і так надсилає кожен елемент extra={...} як GELF-поле, тож сервіс зберігає свій хендлер. Додайте один фільтр, що проставляє inst і seq, та heartbeat.

import logging

import graypy
from kinro.contract import GelfContractFilter, start_heartbeat

gelf = graypy.GELFHTTPHandler('graylog.internal', 12201, timeout=5)   # or GELFUDPHandler / GELFTCPHandler
gelf.addFilter(GelfContractFilter())                                  # one instance per handler
logging.getLogger().addHandler(gelf)
start_heartbeat(logging.getLogger())

logger.info('order %s created', uid, extra={
    'event': 'order.created', 'cid': cid, 'key_uid': uid, 'amount': str(amount),
})

У проєкті

orders = GraylogLogs(
    GraylogConfig(
        query='facility:orders',               # Lucene query that selects the service
        streams=('5f1e2d3c4b5a69788796a5b4',),  # stream ids; empty means every readable stream
        fields=('amount', 'status'),           # payload fields any rule reads
        ingest_lateness=timedelta(seconds=10),
    ),
    url_env='GRAYLOG_URL', token_env='GRAYLOG_TOKEN', max_qps=2,
)
project.service('orders', orders, keys={'order': 'uid'}, max_lateness=timedelta(seconds=5))
GELF-полеСпостереження
_eventevent; повідомлення без нього пропускається.
timestampts: час створення запису, а не час, коли його отримав Graylog.
_cid, _inst, _seqcid, inst, seq.
_key_<name>keys[<name>]. Рекомендований стиль — пласкі ключі.
_keys, _dataОб'єднуються з keys і payload; приймається JSON-рядок або літерал Python.
інші _<name>payload[<name>].
!

Перелічіть поля, які читають правила. Пошуковий API повертає лише ті поля, які запитує Kinro. Поля ключів запитуються автоматично; будь-яке інше поле payload, яке читає правило, наприклад field у status_desync, має бути в GraylogConfig(fields=...).

  • Втрачені повідомлення. GELF через UDP мовчки губить датаграми. Пропущений seq створює алерт kinro.source_gap, а для дедлайнів на цьому проміжку алерти не надсилаються. Там, де втрати критичні, використовуйте HTTP- або TCP-хендлер.
  • Однакові мітки часу. Graylog зберігає час із точністю до мілісекунд, тож багато повідомлень мають однакову мітку. Kinro гортає сторінки за часом і розрізняє повідомлення за gl2_message_id, який зберігається після ротації індексів і перезапусків. Понад 10 000 повідомлень в одній мілісекунді створюють алерт kinro.graylog_page_overflow.
  • Затримка індексації. ingest_lateness має покривати час, доки повідомлення не стане доступним для пошуку: транспорт, пакетну відправку output і оновлення індексу. На типовому налаштуванні це близько 2 с.
  • Retention. Задайте retention рівним retention набору індексів, щоб діапазон, який Graylog видалив раніше, ніж Kinro його прочитав, створював алерт kinro.source_gap.
  • Access-токен належить користувачеві з роллю Reader, який має доступ на читання лише до потоків, у яких ведеться пошук.
Джерела

Файли логів

LogSource читає нові рядки з файлів, що відповідають glob-шаблону. Він ніколи не перечитує файл повністю, зберігає власний курсор і переживає ротацію через rename або copytruncate без втрати рядків.

LogSource(
    '/logs/billing/*.log',
    parser=RegexParser(...),
    rotated='{path}.[0-9]*',        # where rotated copies appear; *.gz never matches
    start_at='end',                 # files that exist on first deploy: 'end' or 'beginning'
)

Монтуйте каталог лише на читання. Сервіс, який пише логи тільки у файли, має працювати на хості Kinro.

volumes:
  - /var/log/billing:/logs/billing:ro
Джерела

Власні парсери

Сервіси, які ви не можете змінити, теж можна підключити. Оберіть парсер під їхній формат.

Для будь-якого текстового формату. Іменовані групи ts і event обов'язкові, level і cid необов'язкові. Групи, перелічені в keys, стають ключами, решта йде в payload.

RegexParser(
    r'^(?P<ts>\S+ \S+) (?P<level>\w+) \[billing\] (?P<event>payment captured|payment failed)'
    r' ext=(?P<external_id>\w+) amount=(?P<amount>[\d.]+) card=(?P<card>\S+)$',
    events={'payment captured': 'payment.captured', 'payment failed': 'payment.failed'},
    ts_format='%Y-%m-%d %H:%M:%S,%f',
    tz='Europe/Kyiv',               # naive timestamps are read in this zone
)

Цей рядок:

2026-10-16 15:00:05,532 INFO [billing] payment captured ext=A1B2 amount=10.50 card=4111111111111111

перетворюється на таке спостереження (із redact=['card'] на сервісі):

{"service": "billing", "event": "payment.captured", "ts": "2026-10-16T12:00:05.532+00:00",
 "payload": {"external_id": "A1B2", "amount": "10.50", "card": "[redacted]"}}

Рядок, що не збігся з шаблоном, рахується як unmatched, а не як помилка.

Джерела

PostgreSQL

Правила можуть запитувати базу даних за розкладом (Query) або шукати конкретні ключі на вимогу (Lookup). Kinro підключається з роллю, яка може лише читати.

CREATE ROLE kinro_ro LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE orders TO kinro_ro;
GRANT USAGE ON SCHEMA public TO kinro_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO kinro_ro;
ALTER ROLE kinro_ro SET default_transaction_read_only = on;
ALTER ROLE kinro_ro SET statement_timeout = '5s';
ALTER ROLE kinro_ro SET lock_timeout = '1s';
ALTER ROLE kinro_ro SET idle_in_transaction_session_timeout = '10s';
project.db('orders_db', url_env='ORDERS_DB_URL', min_interval=timedelta(seconds=30), pgbouncer=True)
  • Кожен SQL-вираз має бути одним SELECT або WITH; validate відхиляє все інше.
  • Кожен запит повертає стовпець key. Рядки надходять у правила як спостереження з подією db.query:<name>; для lookup — db.lookup:<name> і db.lookup_missing:<name>.
  • Невдале опитування нічого не змінює: правила ніколи не бачать порожнього результату, який насправді був помилкою.
  • Якщо маєте репліку, спрямуйте Kinro на неї.
!

PgBouncer. Він відкидає невідомі стартові параметри, тому сесійні налаштування, які надсилає Kinro, не доходять до Postgres. За PgBouncer базу захищають саме наведені вище налаштування ролі. Для transaction pooling використовуйте pgbouncer=True.

Джерела

MySQL

MySQL 8.x підключається тим самим викликом project.db(...). Kinro визначає діалект за URL, тож правила не змінюються, крім lookup-запитів.

CREATE USER 'kinro_ro'@'%' IDENTIFIED BY '...';
GRANT SELECT ON shop.* TO 'kinro_ro'@'%';
ALTER USER 'kinro_ro'@'%' WITH MAX_USER_CONNECTIONS 2;
SHOP_DB_URL=mysql://kinro_ro:password@db.internal:3306/shop
project.db('shop_db', url_env='SHOP_DB_URL')

status_desync(
    'order_status', key='order', trigger=on('orders', 'order.status_changed'), field='status',
    db='shop_db', settle=timedelta(seconds=30),
    lookup='SELECT uid AS `key`, status, updated_at FROM orders WHERE uid IN :keys',
)
  • Lookup-запити використовують IN :keys. У MySQL немає масивів, тому = ANY(:keys) працює лише в Postgres. IN :keys працює в обох.
  • key — зарезервоване слово. Пишіть AS `key` зі зворотними лапками. kinro check повідомляє про обидві помилки.
  • Сесія лише на читання. Під час підключення Kinro переводить сесію в режим лише на читання, задає max_execution_time 5 с, очікування блокувань 1 с і часовий пояс UTC. Записи по-справжньому блокує лише право SELECT, тому не видавайте жодних інших прав.
  • Час. Значення DATETIME не мають часового поясу й читаються як UTC. Якщо застосунок зберігає локальний час, конвертуйте його в запиті через CONVERT_TZ.
  • Значення. BOOLEAN повертається як 0/1, а стовпці JSON — як рядки; приводьте типи в запиті, якщо правило їх порівнює.
  • MariaDB не підтримується, а pgbouncer=True стосується лише Postgres.
Алерти

Формат алертів і канали

Кожен алерт містить правило, ключ, наскрізний timeline сервісів і зламаний крок, позначений >>.

[KINRO] order_flow v1
alert_id: 7f3c9e21a4d0b8e6
key: order=A1B2
missing step shipping/shipment.created within 0:30:00
broken step: 2026-10-16T12:00:05Z billing payment.captured external_id=a1b2 {"amount": "10.50"}
expected: "shipment.created"
deadline: complete @ 2026-10-16T12:30:02Z
detected: 2026-10-16T12:30:09Z
timeline (2):
   2026-10-16T12:00:02Z orders order.created cid=0192f0c4 uid=A1B2 {"amount": "10.50"}
>> 2026-10-16T12:00:05Z billing payment.captured external_id=a1b2 {"amount": "10.50"}
КаналЯк увімкнути
TelegramTELEGRAM_BOT_TOKEN і TELEGRAM_CHAT_ID; необов'язково TELEGRAM_API_BASE. Дотримується лімітів Telegram і retry_after.
SlackSLACK_WEBHOOK_URL.
Файл (JSONL)Project(alerts_path=...).
ЛогЗавжди увімкнено.
ТіньовийЗавжди увімкнено. Отримує лише порушення тіньових правил і пише їх у файл алертів (якщо його задано) і лог, ніколи — у Telegram чи Slack.
ЗаголовокДля чого надсилається
[KINRO] <rule> v<n>Порушення правила; в усі канали, крім тіньового.
[KINRO SHADOW] <rule> v<n>Порушення тіньового правила; лише в тіньовий канал.
[KINRO INSIGHT] <monitor>: <service>Режим без налаштувань: інсайт монітора з KINRO_PAGE разом із доказами.
[KINRO DIGEST] <day>Режим без налаштувань: щоденний дайджест.
[KINRO META] kinro.<check>Сам Kinro: див. Мета-алерти.

Алерти записуються в outbox у тій самій транзакції, що й стан, який їх породив, і лише потім доставляються. Перезапуск ніколи не втрачає алерт. Доставка гарантується щонайменше один раз (at-least-once): збій між надсиланням і записом може повторно надіслати алерт із тим самим alert_id. Файловий канал пропускає ідентифікатори, які вже записав. Якщо канал відповідає постійною помилкою, алерт позначається як failed, а не повторюється нескінченно.

Алерти

Мета-алерти

Алерти про сам Kinro починаються з kinro. і відображаються як [KINRO META].

ID правилаЩо означає
kinro.source_gapЛоги загубилися (пропуск у seq або retention у Loki). Для дедлайнів на цьому проміжку алерти не надсилаються.
kinro.source_stalledДжерело перестало просуватися.
kinro.parse_errorsЗа 5 хвилин понад 1% рядків не вдалося розібрати (у вікні щонайменше 100 рядків).
kinro.rule_errorПравило викинуло виняток; інші правила працюють далі.
kinro.state_overflowДосягнуто max_pending_keys, і найстаріші ключі витіснено.
kinro.loki_page_overflowРядків з однією міткою часу більше, ніж уміщує сторінка; збільште limit.
kinro.graylog_page_overflowПонад 10 000 повідомлень Graylog мають ту саму мілісекунду; решту не прочитано.
Алерти

Маскування полів

Перелічіть поля payload, які ніколи не мають залишати Kinro. Їхні значення замінюються на [redacted] одразу після читання рядка, ще до збереження, правил і алертів.

# loki() is the helper from the Grafana Loki section.
project.service('orders', loki('orders'), keys={'order': 'uid'}, redact=['email', 'phone', 'address'])
project.service('billing', billing_file, keys={'order': Field('external_id')}, redact=['card'])
  • Назви мають точно збігатися з полями payload верхнього рівня. Словник під замаскованою назвою замінюється повністю.
  • cid і keys визначають ідентичність і ніколи не маскуються, тому не кладіть у них персональні дані. validate відхиляє замасковане поле ключа або замасковане поле status_desync.
  • Рядки з бази даних не маскуються. Вибирайте лише ті стовпці, які потрібні правилу.
  • Discover, timeline, suggest і монітори режиму без налаштувань самі маскують секрети й персональні дані, без списку redact; kinro suggest додає поля, у яких їх знайшов, у redact=[...].
Експлуатація

Довідник CLI

kinro завжди потребує підкоманди; без неї він виводить довідку з використання й завершується з кодом 2. В образі entrypoint — kinro, тож підкоманда йде після назви образу (docker run ... kybalda/utyg:0.1.3 discover ...) або назви сервісу compose (docker compose run --rm kinro check ...).

КомандаЩо робить
discover (--logs DIR | --loki [URL] | --graylog [URL]) --out DIRЧитає файли або робить вибірку зі сховища й записує discovery.json, report.html і index.sqlite. Див. Аналіз логів.
timeline VALUE [--index DIR] [--no-expand] [--limit N] [--json]Усі події одного ідентифікатора в усіх сервісах. Див. Часова шкала подій.
suggest --from DIR --out FILE [--force] [--logs-root DIR]Перевірений файл проєкту з вивченими тіньовими правилами. Див. Запропоновані правила.
runРежим без налаштувань: джерела й монітори з оточення KINRO_*. Див. Стеження без правил.
run --project FILE [--commit-interval S] [--stats-interval S]Стежити за вашими правилами й надсилати алерти; SIGTERM фіксує стан і завершує роботу з кодом 0.
check [--project FILE]Перевірити правила, джерела й оточення; нікуди не підключається. Без --project — оточення KINRO_*.
replay --project FILE [--since S] [--db] [--json]Прогнати проєкт на минулих логах (за замовчуванням --since 3600 секунд) зі свіжим тимчасовим станом; нічого не доставляє. --db виконує перевірки в базі даних наживо.
status [--project FILE | --state FILE] [--alert ID] [--json]Затримка, лічильники, outbox, тіньові порушення; для режиму без налаштувань — навчання й інсайти. Без жодного з параметрів читає $KINRO_STATE_PATH, інакше kinro.db. Безпечно запускати поруч із робочим Kinro.
init DIR [--force] [--image NAME:TAG]Створити репозиторій правил: проєкт, правила, тести, compose.yaml, Dockerfile.
$ docker run --rm --user "$(id -u):$(id -g)" -v "$PWD/logs:/logs:ro" -v "$PWD/discovery:/out" kybalda/utyg:0.1.3 discover --logs /logs --out /out
$ docker run --rm -v "$PWD/discovery:/out:ro" kybalda/utyg:0.1.3 timeline 9d37ff01fe80 --index /out
$ docker run --rm --env-file .env kybalda/utyg:0.1.3 check                # zero-config env
$ docker exec kinro kinro status                                          # zero-config container
$ docker compose run --rm kinro check --project /app/project.py          # validate; connects nowhere
$ docker compose up -d                                                    # run: watch and alert
$ docker compose run --rm kinro replay --project /app/project.py --since 3600 --json   # past logs, delivers nothing
$ docker compose run --rm kinro replay --project /app/project.py --since 3600 --db     # also run DB checks live
$ docker compose run --rm kinro status --project /app/project.py --json   # lag, counters, outbox
$ docker compose run --rm kinro status --state /state/kinro.db --alert 7f3c9e21a4d0b8e6
$ docker compose run --rm kinro --help

Задавайте --commit-interval і --stats-interval через command: у compose.yaml. Коди виходу: 0 — успіх, 1 — збій (помилки check, аварійне завершення, сховище неможливо прочитати, ідентифікатор не знайдено, пропозиція не пройшла валідацію), 2 — помилка конфігурації чи використання.

ЗміннаПризначення
KINRO_LOKI_URL, KINRO_LOKI_SELECTOR, KINRO_LOKI_SERVICE_LABEL, KINRO_LOKI_TOKEN, KINRO_LOKI_TENANT, KINRO_LOKI_USERNAMEДжерело Loki в режимі без налаштувань.
KINRO_GRAYLOG_URL, KINRO_GRAYLOG_TOKEN, KINRO_GRAYLOG_QUERY, KINRO_GRAYLOG_STREAMS, KINRO_GRAYLOG_SERVICE_FIELDДжерело Graylog в режимі без налаштувань.
KINRO_LOG_GLOB, KINRO_LOG_SERVICE_FROM, KINRO_LOG_STARTФайлове джерело в режимі без налаштувань.
KINRO_LOOKBACK, KINRO_MAX_QPSРежим без налаштувань: глибина читання в минуле під час першого запуску й ліміт запитів до сховищ.
KINRO_LEARN_HOURS, KINRO_PAGE, KINRO_DIGEST_AT, KINRO_ALERTS_PATHРежим без налаштувань: навчання, негайні сповіщення, час дайджесту й файл алертів. Значення за замовчуванням — у розділі Стеження без правил.
KINRO_STATE_PATHФайл стану режиму без налаштувань, а також файл, який читає kinro status без --project чи --state; за замовчуванням kinro.db.
KINRO_IMAGEОбраз, який kinro init записує в compose.yaml і Dockerfile; за замовчуванням kybalda/utyg:<engine version>.
LOKI_URLБазовий URL Loki; назву змінної задає LokiLogs(url_env=...). Також це URL, який читає discover --loki, якщо URL не вказано.
LOKI_TOKEN, LOKI_TENANTТокен лише на читання й тенант. Режим правил за замовчуванням їх не читає: вкажіть їх у LokiLogs(token_env='LOKI_TOKEN', tenant_env='LOKI_TENANT'). discover --loki читає їх за замовчуванням.
GRAYLOG_URL, GRAYLOG_TOKENБазовий URL Graylog і access-токен лише на читання; назви задаються через url_env, token_env. discover --graylog читає їх за замовчуванням.
ORDERS_DB_URL, SHOP_DB_URLURL Postgres або MySQL для користувача лише на читання; назву задає project.db(url_env=...).
TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_IDКанал Telegram; задайте обидві змінні або жодної. kinro check завершується з помилкою, якщо задано лише одну.
TELEGRAM_API_BASEБазова адреса API Telegram, за замовчуванням https://api.telegram.org.
SLACK_WEBHOOK_URLКанал Slack.
i

Kinro читає ці змінні з оточення процесу та з .env у робочому каталозі; оточення процесу має пріоритет. У Docker Compose їх передає env_file, а з docker run — --env-file.

Експлуатація

Розгортання

Запускайте рівно один процес Kinro на проєкт, поруч із його volume стану. Сервіси на будь-яких хостах і далі відправляють логи в Loki. Режиму без налаштувань не потрібні ні проєкт, ні compose-файл: достатньо одного docker run ... kybalda/utyg:0.1.3 run з оточенням KINRO_* і volume /state, як у розділі Швидкий старт. З правилами використовуйте compose-файл нижче.

host A: orders, shipping ──push──> Grafana Loki <──query_range (read-only)── Kinro
host B: billing ──> /var/log/billing/*.log ──:ro mount──────────────────────┘  │
                         Postgres (kinro_ro, replica or PgBouncer) <──SELECT───┘
                         state: SQLite WAL on a volume (/state/kinro.db)
services:
  kinro:
    image: kybalda/utyg:0.1.3
    env_file: .env
    init: true                  # SIGTERM reaches Kinro, which commits before exit
    restart: unless-stopped
    volumes:
      - kinro-state:/state
      - /var/log/billing:/logs/billing:ro
      - ./:/app:ro                # the rules folder: project.py, rules/, parsers/
volumes:
  kinro-state:
!

Один процес на файл стану. Два процеси на одному файлі стану конкурують за курсори й надсилають алерти двічі. Kinro у режимі без налаштувань і Kinro з правилами, запущені поруч, потребують кожен свого файлу стану.

Оцінка ресурсів. Близько 2 KB пам'яті на кожне активне спостереження, яке зберігається протягом window правила. RSS ≈ 2 KB × рядків за секунду × вікно: 50 рядків/с за 30 хвилин — це близько 180 MB.

Експлуатація

Моніторинг

Кожні --stats-interval секунд (за замовчуванням 60) Kinro пише в лог один рядок:

runtime stats: {"lag_seconds": {"orders": 0.8, "billing": 0.4, "orders_db": 12.1},
  "late_observations": 0, "rule_errors": 0, "desync_unverifiable": 0, "chain_unverifiable": 0,
  "link_conflicts": 0, "pending_keys": 1840, "db_queries": 96, "rotation_gaps": 0, "parse_errors": 0}

Ознаки здорового стабільного стану: затримка (lag) тримається біля max_lateness плюс інтервал опитування; rule_errors, rotation_gaps, link_conflicts і обидва лічильники *_unverifiable не зростають; pending_keys залишається нижче max_pending_keys (за замовчуванням 200 000).

kinro status показує затримку, збережені лічильники, статуси outbox і кількість рядків. Ще дві частини з'являються, коли вони актуальні:

  • Тіньові правила. shadow violations (shadow channel only): <rule>=<n> для кожного правила. Ці алерти потрапили лише в тіньовий канал.
  • Режим без налаштувань. Сервіси та прогрес їхнього навчання, монітори з негайними сповіщеннями, наступний і останній дайджест, кількість інсайтів за моніторами (і скільки з них надіслано одразу), кількість тіньових інсайтів навчання та обмежених за частотою, а для кожного сервісу — його записи, помилки, останній запис і те, чи він мовчить або має сплеск помилок.
Експлуатація

Тіньовий тиждень

  1. Розгорніть без сповіщень

    Задайте alerts_path і не вказуйте змінних Telegram і Slack. Алерти потраплятимуть лише у файл і лог. Щоб одні правила мовчали, поки інші надсилають сповіщення, задайте першим shadow=True: їхні порушення потраплятимуть лише в тіньовий канал, який пише в той самий файл і лог. Запропоновані правила стартують саме так. У режимі без налаштувань залиште KINRO_PAGE порожнім: кожен інсайт чекатиме на дайджест.

  2. Переглядайте щодня

    Позначайте кожен алерт як справжній баг або шум. Запізнілі додаткові алерти (#late<n>) належать до свого алерту й не рахуються.

    $ jq -r 'select(.alert_id | contains("#late") | not) | [.delivered_at[0:10], .payload.rule_id] | @tsv' \
        /state/alerts.jsonl | sort | uniq -c
  3. Налаштуйте

    Ціль: не більше 3 хибних алертів на день протягом 7 днів і жодного дня з понад 6. Коригуйте within, settle або max_lateness і перевіряйте кожну зміну через kinro replay.

  4. Увімкніть сповіщення

    Додайте змінні Telegram або Slack і перезапустіть. Щоб перевести тіньове правило в робочий режим, видаліть у ньому shadow=True; щоб монітор надсилав негайні сповіщення, додайте його в KINRO_PAGE. Залиште alerts_path як журнал аудиту.

Експлуатація

Відновлення

  • Під час старту Kinro виконує PRAGMA quick_check. Якщо файл стану пошкоджено, Kinro зупиняється й пише зрозумілий рядок у лог; на зламаному стані він ніколи не працює.
  • Робіть резервну копію стану через SQLite backup API або після коректної зупинки; файли -wal і -shm є його частиною.
  • Після видалення стану Kinro починає з нуля від start_at і start_lookback: дедлайни, що очікують, втрачаються, а нещодавні алерти можуть один раз повторитися.
  • Переповнений диск або заблокований файл призупиняють коміти, і Kinro повторює запис того самого знімка; нічого не застосовується двічі.