Документація Kinro
Kinro стежить за процесами, що проходять через кілька сервісів, і лише читає дані. Спрямуйте його на логи, які у вас уже є: у Grafana Loki, Graylog або звичайних файлах, у будь-якому форматі. Він покаже, що в них є, зведе в часову шкалу подій (timeline) усе, що сталося з одним ідентифікатором, стежитиме за кожним сервісом загальними моніторами й запропонує правила, вивчені на ваших же даних. Для цього не потрібно писати правила чи змінювати формат логів.
Власні правила на Python, перевірки в базі даних і формат логів Kinro — необов'язкові покращення на потім. Ніщо в Kinro не прив'язане до бізнес-домену: йому достатньо сервісу, ідентифікатора й шаблону.
job <hex> failed: upstream unavailable.| Команда | Що ви отримуєте | Що потрібно |
|---|---|---|
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), що передається між ними.
- Отримайте образ
Рушій постачається як Docker-образ; на хост більше нічого не встановлюється. Поки що образ приватний: увійдіть із pull-токеном, який вам видали.
$ docker login -u <user> $ docker pull kybalda/utyg:0.1.3
- Подивіться, що є у ваших логах
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$ export LOKI_URL=https://loki.internal:3100 LOKI_TOKEN=... $ mkdir -p discovery $ docker run --rm --user "$(id -u):$(id -g)" -e LOKI_URL -e LOKI_TOKEN -v "$PWD/discovery:/out" \ kybalda/utyg:0.1.3 discover --loki --since 24h --out /out$ export GRAYLOG_TOKEN=... # an access token of a Reader user $ mkdir -p discovery $ docker run --rm --user "$(id -u):$(id -g)" -e GRAYLOG_TOKEN -v "$PWD/discovery:/out" \ kybalda/utyg:0.1.3 discover --graylog https://graylog.internal:9000 --since 24h --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у браузері, щоб побачити повну картину: шаблони із замаскованими прикладами рядків, процеси із затримками, усі виявлені проблеми, що дасть кожному сервісу наступний рівень логів, і поле пошуку за ідентифікаторами. - Простежте один ідентифікатор
Візьміть потрібний вам ідентифікатор, наприклад 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
- Стежте без правил
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. - Читайте дайджест
Перші 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 UTCstatusбудь-коли показує прогрес навчання й кількість інсайтів; його безпечно запускати поруч із робочим контейнером:$ 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
- Далі
Перетворіть процеси, знайдені 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-и й дампи з відступом приєднуються до рядка, якому належать.
Рекомендовано, але не обов'язково. Піднімайте сервіс на рівень вище, коли виявлене в ньому залежить від формулювань (з 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. |
Що він знаходить
- Записи. Файли читаються рекурсивно (приховані й бінарні пропускаються,
.gzчитаються), кольори термінала видаляються. Рядок без мітки часу отримує мітку попереднього. - Шаблони. Змінні маскуються (uuid, hex, числа, рядки в лапках, параметри URL, IP-адреси, мітки часу), потім дерево Drain групує рядки в шаблони зі стабільними ідентифікаторами. Рядки access-логу групуються за маршрутом.
- Поля та ідентифікатори. Пари «назва — значення» з ключів JSON,
k=v,k: v, сегментів шляху URL і контекстних тегів. Обсяг пам'яті не залежить від обсягу логів: кожне поле зберігає обмежену вибірку своїх значень. - Зв'язки та ключі. Поля різних сервісів зі спільними значеннями. Зв'язок підтверджується, якщо спільних значень щонайменше
--min-shared, вкладення з одного боку не менше за--min-containment, значення з обох боків схожі на ідентифікатори, а перетин більший, ніж дала б випадковість. Слабші кандидати перелічуються як непідтверджені з причиною. Поля, об'єднані підтвердженими зв'язками, утворюють ключі (k1,k2, ...). - Процеси. Для кожного ключа — події кожної сутності в усіх сервісах у порядку часу: переходи з кількістю, P(next | prev), p50/p95/p99 часу між ними й найчастіші шляхи.
- Виявлені проблеми. Найчастіші шаблони помилок; сплески помилок (хвилина, у якій помилок не менше за 10 і не менше за 10-кратний звичний рівень помилок сервісу, а пік щонайменше втричі вищий за його звичну хвилину з помилками; «гарячі» хвилини в межах 30 хвилин об'єднуються); повторювані збої (одна сутність натрапляє на той самий збій щонайменше
--retry-minразів); періоди тиші (довші і за 30 хвилин, і за 10 медіанних пауз сервісу); секрети й персональні дані.
Маскування всюди. Будь-який результат показує щонайбільше чотири останні символи чутливого значення, а значення, коротше за десять символів, не показує взагалі. Секрети видаляються ще до того, як збирається будь-яка статистика. В індексі email-адреси, телефони й імена стають <email>, <phone>, <name> і зберігаються лише як хеш із ключем, тож пошук за введеним email чи телефоном усе одно працює.
Параметри для файлів
| Параметр | За замовчуванням | Значення |
|---|---|---|
--logs DIR | Каталог файлів логів, читається рекурсивно. --out не може бути всередині нього. | |
--service-from path|dir|file | path | Назва сервісу з каталогів і назви файлу, лише з каталогів або лише з назви файлу. Хеші подів Kubernetes, суфікси ротації та .gz відкидаються: edge/api-7d9f8c6b5-x2k4p.log стає edge/api. |
--min-shared N | 3 | Скільки спільних значень потрібно, щоб підтвердити зв'язок між сервісами. |
--min-containment X | 0.5 | Частка значень однієї сторони, яка має бути в іншої. |
--retry-min N | 5 | Скільки разів одна сутність має натрапити на той самий збій, щоб це вважалося повторюваним збоєм. |
--max-mb-per-file N | 256 | Більший файл читається лише в останніх N MB. |
--index-max-per-value N | 1000 | Індекс часової шкали: скільки подій зберігається для одного значення ідентифікатора в кожному сервісі. |
--index-max-rows N | 2000000 | Індекс часової шкали: скільки подій зберігається загалом. |
--index-max-mb N | 512 | Індекс часової шкали: коли файл досягає цього розміру, нові події не додаються. |
З 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, --until | 24h, now | Вікно: now, тривалість назад від поточного моменту (30m, 24h, 7d) або час ISO (UTC, якщо зону не вказано). |
--sample N | 50000 | Максимум рядків, що читаються з одного сервісу. |
--slices N | 12 | На скільки рівномірно розподілених відрізків ділиться вікно під час читання. |
--max-mb N | 256 | Скільки MB рядків читається загалом, на всі сервіси разом. |
--max-qps N | 2 | Максимум запитів на секунду (0 — без обмеження). |
--timeout S | 30 | Секунд на один запит. |
--token-env NAME | LOKI_TOKEN / GRAYLOG_TOKEN | Змінна з токеном: для Loki необов'язкова (bearer), для Graylog обов'язкова. |
--tenant-env, --username-env | LOKI_TENANT, немає | Loki: заголовок тенанта й користувач basic-auth, паролем якого є токен. |
--selector '{...}' | усі потоки з міткою сервісу | Loki: LogQL, у межах якого читати; фільтр рядків після селектора зберігається. |
--service-label NAME | перша з service_name, service, app, job, container | Loki: мітка з назвою сервісу. |
--query Q, --streams ID,... | *, усі | Graylog: пошуковий запит та ідентифікатори потоків, у межах яких читати. |
--service-field NAME | source | Graylog: поле повідомлення з назвою сервісу. |
--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 N | 200 | Показати останні N подій. |
--json | вимкнено | Часова шкала у форматі JSON. |
Коди виходу: 0 — знайдено; 1 — не знайдено, із найближчими значеннями з індексу й приміткою, якщо індекс досяг якогось ліміту; 2 — помилка використання або індекс відсутній.
$ kinro timeline 9d37ff01fe8 --index ./discovery kinro timeline 9d37ff01fe8: not found in the index closest indexed values: 9d37ff01fe80
Що містить індекс. Значення, схожі на ідентифікатори (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_LABEL | service_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_FIELD | source | Поле повідомлення з назвою сервісу. |
KINRO_LOG_GLOB | немає | Glob-шаблон файлів, наприклад /logs/*/*.log (без рекурсивного **). |
KINRO_LOG_SERVICE_FROM | path | Назва сервісу зі шляху нижче фіксованої частини glob-шаблону: path, dir або file; хеші подів відкидаються, як у discover. |
KINRO_LOG_START | end | Звідки читаються наявні файли під час першого запуску: end або beginning. |
KINRO_LOOKBACK | 2 × затримка приймання | Loki і Graylog: як далеко в минуле починає читати перший запуск (30m, 24h); довший період дає змогу одразу навчитися на історії. |
KINRO_MAX_QPS | 2 | Запитів на секунду на одне сховище (0 — без обмеження). |
Монітори й доставка:
| Змінна | За замовчуванням | Значення |
|---|---|---|
KINRO_LEARN_HOURS | 24 | Для кожного сервісу, від його першого запису: монітори лише навчаються. |
KINRO_PAGE | немає | Монітори, що доставляються одразу: назви через кому, all або none. |
KINRO_DIGEST_AT | 09:00 | Час щоденного дайджесту за UTC, HH:MM. |
KINRO_STATE_PATH | kinro.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-path | suggested, /state/kinro.db, /state/alerts.jsonl | Назва проєкту, файл стану й файл алертів, у який пише тіньовий канал. |
--min-confidence | 0.98 | Частка процесів, у яких крок ланцюжка має завершитися. |
--min-support | 50 | Скільки процесів чи сутностей потрібно, щоб вивчити правило. |
--margin | 3 | within ланцюжка = p99 затримки × margin + max_lateness. |
--max-within | 3600 | Секунди; повільніші ланцюжки лише кандидати. |
--max-duplicate-rate | 0.001 | no_duplicates лише тоді, коли частка сутностей із повторами нижча за цю. |
--max-inversion-rate | 0.001 | ordering лише тоді, коли частка процесів з інвертованим порядком не перевищує цієї. |
--tolerance | 1 | Допуск 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 на класі.
Переведення правила в робочий режим
- Запустіть у тіньовому режимі
Змонтуйте каталог із
project.pyу/app, а логи — у--logs-root, як у розділі Написання власних правил. Якщо Kinro у режимі без налаштувань і далі працює, дайте проєкту окремий файл стану через--state-path: два процеси ніколи не мають ділити один файл. - Перегляньте
Кілька днів читайте тіньові алерти у файлі алертів, як під час тіньового тижня. Перевіряйте змінений поріг через
kinro replay. - Переведіть у робочий режим
Видаліть у правилі
shadow=True, запустітьkinro checkі перезапустіть Kinro. Відтоді його алерти йдуть в усі канали. Так само розкоментуйте кандидата, щоб випробувати його в тіньовому режимі.
Написання власних правил
Правила необов'язкові. Пишіть їх, коли процесу потрібна перевірка, якої не покриває жодна пропозиція: порівняння з базою даних, поле payload (корисних даних події), обіцянка, про яку знаєте лише ви. Почніть із файлу, який записав kinro suggest, або з шаблону, який створює kinro init. Нижче розглянуто процес із двох сервісів: orders пише логи в Loki, billing пише в текстовий файл старого формату, а Kinro надсилає алерт, якщо створене замовлення не оплачено протягом 15 хвилин.
- Встановіть SDK
Для правил і файлів проєкту потрібен лише пакет
kinro-sdk: без рушія й без залежностей, з типами для IDE, під ліцензією Apache License 2.0. Сам рушій працює як Docker-образ Kinro.Пакет опубліковано на PyPI, тож достатньо однієї команди:
$ uv add kinro-sdk
$ pip install 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. - Опишіть проєкт
Файл проєкту — це 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)), ) - Створіть каталог правил
Створіть каталог правил із шаблону, вбудованого в образ зі Швидкого старту. У ньому є
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
- Додайте 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
- Перевірте конфігурацію
checkперевіряє правила, джерела та оточення, нікуди не підключаючись. Entrypoint образу —kinro, тому підкоманда йде одразу після назви сервісу.$ docker compose run --rm kinro check --project /app/project.py shop: 1 rules, 2 services, 0 dbs; 0 errors, 0 warnings
- Запустіть
За замовчуванням контейнер виконує
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. |
Обмеження. Рядок, більший за 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'))Замовлення, які в базі даних досі мають статус created через 30 хвилин після оформлення.
class StuckInCreated(Rule):
key = 'order'
version = 1
schedules = (Query(
'orders_db', name='stuck_created', every=timedelta(minutes=5),
sql="SELECT uid AS key, created_at FROM orders WHERE status = 'created' "
"AND created_at < now() - interval '30 minutes' LIMIT 100",
),)
def on_observation(self, ctx: RuleContext, obs: Observation) -> None:
if obs.event == self.schedules[0].row_event:
ctx.violate('order stuck in created for more than 30 minutes', broken_step=obs)
ctx.close()Атрибути правила
| Атрибут | Значення |
|---|---|
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.state | JSON-словник для кожного ключа; зберігається з кожним комітом. |
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.late | True, якщо спостереження надійшло після того, як дедлайн уже спрацював. |
Хуки: 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-рядок: контракт і є весь інтерфейс.
Задайте 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. Фільтруйте лише за мітками з низькою кардинальністю. |
parser | StandardParser() | Парсер рядків; за замовчуванням LogContract. |
ingest_lateness | 5 с | Наскільки пізно рядок може потрапити в Loki після своєї мітки часу. Має бути не меншим за max_lateness сервісу. |
sweep_interval | ingest_lateness | Як часто Kinro перечитує останнє вікно, щоб підхопити запізнілі рядки. |
limit | 1000 | Розмір сторінки, не більше за серверний max_entries_limit_per_query. |
retention | немає | Retention у Loki; читання за його межами створює алерт kinro.source_gap. |
start_lookback | 2 × ingest_lateness | Звідки починається читання під час першого розгортання. |
url_env, token_env, tenant_env | LOKI_URL, немає, немає | Змінні оточення з URL, bearer-токеном і тенантом X-Scope-OrgID. Токен і тенант надсилаються, лише якщо їх вказано. |
max_qps | 2.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-поле | Спостереження |
|---|---|
_event | event; повідомлення без нього пропускається. |
timestamp | ts: час створення запису, а не час, коли його отримав Graylog. |
_cid, _inst, _seq | cid, 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, а не як помилка.
Для сервісів, які вже пишуть звичайний JSON (time, level, logger, message), або для «голих» тіл Loki. Кожне MessageRule шукає збіг у повідомленні.
TemplateJsonParser([
MessageRule(r'order (?P<uid>\w+) created', event='order.created', keys=('uid',)),
MessageRule(r'order (?P<uid>\w+) cancelled: (?P<reason>.+)', event='order.cancelled', keys=('uid',)),
])Час push. «Голе» тіло не має власного часу, тому отримує час push у Loki. Цей час може відставати від події, тож ordering та інші правила з допуском для таких сервісів відхиляються.
Для звичайного тексту, logfmt або JSON-рядків, події яких задаються шаблонами повідомлень, як-от ті, що показує kinro discover; kinro suggest записує такі парсери за вас. Плейсхолдери: <*> — будь-який токен, <n>, <hex>, <uuid>, <id>, <ts>, <ip>, <email>, '<str>', а <...> у кінці позначає решту довгого повідомлення.
TemplateParser(
{
'job_submitted': 'job submitted',
'job_failed': 'job <hex> failed: upstream unavailable',
},
fields=['job_ref', 'job'],
)Рядок отримує подію найконкретнішого шаблону, що з ним збігся; заголовок (час, рівень, logger, [context]) пропускається. Зберігаються лише названі fields; вони читаються з полів JSON чи logfmt або з name=value, name: value, [name:value], /name/value і name value у тексті. Мапінг на кшталт {'job': ['job_ref', 'job']} зберігає під однією назвою першу з кількох. Рядки, що не збіглися з жодним шаблоном, рахуються як unmatched.
Успадкуйте Parser і реалізуйте _parse. Повертайте None для рядків без події; для пошкоджених рядків викидайте ParseError. Базовий клас рахує обидва випадки й ніколи не дає винятку зупинити приймання логів.
import csv
from datetime import datetime
from kinro.contract.model import Observation
from kinro.parsers.base import LineMeta, ParseError, Parser
class WarehouseCsvParser(Parser):
"""Lines like `2026-10-16T12:00:09Z;shipped;A1B2;dhl`."""
def _parse(self, line: str, meta: LineMeta) -> Observation | None:
row = next(csv.reader([line], delimiter=';'))
if len(row) != 4:
raise ParseError(f'expected 4 columns, got {len(row)}')
ts, status, order, carrier = row
if status != 'shipped':
return None
return Observation.create(
obs_id=meta.obs_id, service=meta.service, event='shipment.created',
ts=datetime.fromisoformat(ts), keys={'order': order}, payload={'carrier': carrier},
)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_time5 с, очікування блокувань 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"}| Канал | Як увімкнути |
|---|---|
| Telegram | TELEGRAM_BOT_TOKEN і TELEGRAM_CHAT_ID; необов'язково TELEGRAM_API_BASE. Дотримується лімітів Telegram і retry_after. |
| Slack | SLACK_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_URL | URL 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. |
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>для кожного правила. Ці алерти потрапили лише в тіньовий канал. - Режим без налаштувань. Сервіси та прогрес їхнього навчання, монітори з негайними сповіщеннями, наступний і останній дайджест, кількість інсайтів за моніторами (і скільки з них надіслано одразу), кількість тіньових інсайтів навчання та обмежених за частотою, а для кожного сервісу — його записи, помилки, останній запис і те, чи він мовчить або має сплеск помилок.
Тіньовий тиждень
- Розгорніть без сповіщень
Задайте
alerts_pathі не вказуйте змінних Telegram і Slack. Алерти потраплятимуть лише у файл і лог. Щоб одні правила мовчали, поки інші надсилають сповіщення, задайте першимshadow=True: їхні порушення потраплятимуть лише в тіньовий канал, який пише в той самий файл і лог. Запропоновані правила стартують саме так. У режимі без налаштувань залиштеKINRO_PAGEпорожнім: кожен інсайт чекатиме на дайджест. - Переглядайте щодня
Позначайте кожен алерт як справжній баг або шум. Запізнілі додаткові алерти (
#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 хибних алертів на день протягом 7 днів і жодного дня з понад 6. Коригуйте
within,settleабоmax_latenessі перевіряйте кожну зміну черезkinro replay. - Увімкніть сповіщення
Додайте змінні Telegram або Slack і перезапустіть. Щоб перевести тіньове правило в робочий режим, видаліть у ньому
shadow=True; щоб монітор надсилав негайні сповіщення, додайте його вKINRO_PAGE. Залиштеalerts_pathяк журнал аудиту.
Відновлення
- Під час старту Kinro виконує
PRAGMA quick_check. Якщо файл стану пошкоджено, Kinro зупиняється й пише зрозумілий рядок у лог; на зламаному стані він ніколи не працює. - Робіть резервну копію стану через SQLite backup API або після коректної зупинки; файли
-walі-shmє його частиною. - Після видалення стану Kinro починає з нуля від
start_atіstart_lookback: дедлайни, що очікують, втрачаються, а нещодавні алерти можуть один раз повторитися. - Переповнений диск або заблокований файл призупиняють коміти, і Kinro повторює запис того самого знімка; нічого не застосовується двічі.