rk_syslog 0.2.0
rk_syslog: ^0.2.0 copied to clipboard
A syslog sink for Dart — RFC 5424 framing, RFC 5425 delivery over TLS, and a bounded on-disk spool so records survive a crash and never block the caller.
rk_syslog #
Приёмник журнала для Dart: обрамление по RFC 5424, доставка по RFC 5425
поверх TLS и ограниченный буфер на диске. Нативная часть на Rust, привязка через
dart:ffi.
Чем этот пакет не является #
Он не убирает вызовы журналирования из кода. Решение о том, пишет ли класс в журнал, что именно он пишет и какие из его полей — секрет, принимается в точке вызова, внутри самого класса. Нативная библиотека лежит ниже контракта и этих классов не видит вовсе; никакая обёртка над ней, ни на одном языке, не может дотянуться вверх и удалить тот код. Маскирование тем более остаётся у вызывающего: пакет не знает, что строка — это ПИН, и не угадывает.
Если вы пришли сюда за тем, чтобы стало меньше однообразного кода в каждом классе, — этого приёмник дать не может, ни на одной стороне границы FFI. Что он даёт, стоит того само по себе:
- записи переживают падение процесса;
- касса, чей сборщик недоступен, продолжает торговать на полной скорости: отправка записи никогда не ждёт сеть;
- журнал уходит в любую систему сбора, которая уже стоит у клиента, потому что он в том формате, который она уже читает.
Пример #
final config = RkSyslogConfig(spoolDirectory: '/var/lib/telepos/syslog')
..identify(hostName: 'till-01', appName: 'telepos', procId: '$pid')
..facility = RkFacility.local0
..set('collector_scheme', 'tls')
..set('collector_host', 'logs.shop.example')
..set('tls_server_fingerprint_sha256', 'a1b2…');
final opened = RkSyslogSink.open(config);
if (opened case RkSyslogFailure(:final status, :final detail)) {
return report(status, detail); // отказ — значение, а не исключение
}
final sink = opened.valueOrThrow();
sink.submit(
severity: RkSeverity.informational,
msgid: 'SALE',
structuredData: RkStructuredData()
..element('sale@0').param('total', '1250.00'),
message: 'чек закрыт',
);
sink.flush(); // блокирует: всё отправленное теперь на диске
sink.close(); // детерминированно
Что гарантируется #
| Порядок | записи уходят в порядке отправки, в том числе после перезапуска |
| Доставка | не менее одного раза, не «ровно один» |
| Долговечность | начинается с буфера на диске, не с момента submit |
| Потери | никогда не молча: см. правило буфера ниже |
| Блокировка | submit не ждёт сеть и диск; ждёт только flush, и только того, кто его позвал |
Почему не «ровно один раз». Курсор буфера сдвигается после записи в сокет. Машина, умершая между записью и сохранением курсора, отправит запись повторно. Обратный порядок превратил бы тот же промежуток в потерю, а для журнала дубль — неудобство, дыра — дефект. Сделать это точным нечем: в RFC 5425 нет подтверждения на уровне приложения.
Правило буфера и его цена #
Буфер ограничен по байтам. Что-то обязано уступить при достижении границы, и правило выбирается при открытии:
spool_policy |
Что происходит | Цена |
|---|---|---|
drop_oldest (по умолчанию) |
удаляется самый старый сегмент целиком | записи потеряны навсегда, и гранулярность — сегмент, то есть до spool_segment_bytes за раз, а не одна запись. Остаётся счётчик spool_dropped_records, и на место удалённых пишется запись о том, сколько их было |
reject |
не удаляется ничего | отказ переезжает к вызывающему: submit возвращает spoolFull, и запись остаётся у него. Для аудита и безопасности, где выброшенная запись — дефект, а у вызывающего есть своё надёжное хранилище |
Граница проверяется на пути записи, при каждом добавлении. Ни таймера, ни подметальщика: правило хранения, зависящее от того, что кто-то не забудет его запустить, однажды не сработает.
Три журнала (раздел 15 архитектуры) #
| Журнал | facility |
spool_policy |
|---|---|---|
| Технический | local0 |
drop_oldest |
| Аудит | audit (13) |
reject |
| Безопасность | authpriv (10) |
reject |
Настройки #
Устанавливаются по имени. Ключ, которого библиотека не знает, — отказ
(unknownConfigKey), а не молчаливое игнорирование: spool_max_byte остановит
открытие приёмника, а не оставит границу по умолчанию и не даст диску
заполниться через полгода.
| Ключ | По умолчанию | |
|---|---|---|
spool_dir |
— | обязателен |
host_name, app_name, proc_id |
- |
поля заголовка; проверяются один раз при открытии |
facility |
local0 |
по имени |
spool_max_bytes |
67108864 |
64 МиБ |
spool_segment_bytes |
1048576 |
1 МиБ — он же гранулярность потери |
spool_policy |
drop_oldest |
drop_oldest | reject |
queue_max_records |
4096 |
окно потери при падении процесса |
queue_policy |
reject |
drop_oldest | reject |
collector_scheme |
none |
none | tls | tcp |
collector_host |
— | |
collector_port |
6514 |
|
tls_server_name |
= collector_host |
SNI и имя для проверки |
tls_roots_pem |
— | путь к связке корней |
tls_server_fingerprint_sha256 |
— | закрепление, с двоеточиями или без |
tls_client_cert_pem, tls_client_key_pem |
— | взаимный TLS; только вместе |
max_message_bytes |
8192 |
RFC 5425 §4.2 просит поддерживать 8192 |
oversize |
truncate |
truncate | reject |
msg_bom |
true |
метка порядка байтов перед MSG, RFC 5424 §6.4 |
connect_timeout_ms, write_timeout_ms |
5000 |
|
retry_min_ms, retry_max_ms |
500, 30000 |
удвоение до предела |
Закрыто по умолчанию. collector_scheme — none: новая установка ничего
наружу не открывает. И у пакета нет встроенного хранилища корней: сборщик у
клиента обычно подписан своим удостоверяющим центром, и приёмник, который молча
доверял бы публичным корням, доверял бы не тому набору. Либо связка, либо
отпечаток — иначе открытие не проходит.
tcp — это не RFC 5425: ни шифрования, ни проверки другой стороны. Он есть
потому, что сборщик на той же машине — реальная установка, и потому, что
обрамление должно быть проверяемым без сертификата. Всё, что уходит с машины,
хочет tls.
Счётчики #
Читаются по имени; неизвестное имя даёт unknownStat, а не ноль — «не
измеряется» и «измеряется, и ноль» это разные ответы.
submitted, framing_refused, truncated, queue_refused, queue_dropped,
queue_depth, spooled, spool_refused, spool_dropped_records,
spool_dropped_segments, spool_torn_records, spool_bytes, spool_records,
spool_io_failures, sent, send_failures, connected.
Правила, которые надо знать #
- Отказ — значение (И144). Ничто здесь не бросает исключение при отказе
нативной части; см.
RkSyslogResult. Паника за границей ловится и приходит какpanicked. - Не на изоляте интерфейса (И145). Любой вызов сюда — вызов в чужой код.
close()обязателен (И146). Сборщик мусора Dart не знает ни о буфере, ни о рабочем потоке, ни о сокете.- Перечисления пересекают границу по имени (И147).
RkSyslogSink.verifyNameTables()сверяет таблицы пакета с таблицами загруженной библиотеки, а не предполагает их совпадение.
Сборка нативной части #
cd rust && cargo build --release
Обычная cdylib/staticlib с C ABI, без сборочного скрипта. Привязка находит
её по RK_SYSLOG_LIB или рядом с исполняемым файлом.
Пакет — FFI-плагин Flutter: flutter build сам вызывает cargo и кладёт
библиотеку в приложение на Windows, Linux и Android. Механизм, три разных пути
к cargo и порядок проверки на Mac — в
doc/native-build.md. Каталога hook/ здесь нет и не
будет: само его присутствие ломает dart run, dart test и flutter build.
panic = "abort" ставить нельзя: граница ловит паники, чтобы отказ дошёл
значением, и прерывание процесса делает это обещание ложью.
Куда библиотека доезжает:
| Цель | Состояние | Чем доказано |
|---|---|---|
| Windows | доезжает | rk_syslog.dll рядом с runner собранного приложения |
| Linux | доезжает | librk_syslog.so в bundle/lib/ приложения |
| Android | доезжает | найдена внутри распакованного APK на armeabi-v7a, arm64-v8a, x86_64 |
| macOS, iOS | написано, ни разу не собрано | Mac на проекте нет; файлы существуют, чтобы пакет не оказался неверным в день, когда Mac появится, и ни одно утверждение про них не проверено |
Лицензия #
MIT, автор Rob Kim. См. LICENSE.