Ориентация главы
Предыдущая глава написала все детали собственного Sink; эта собирает целую машину: пять встроенных Sink (консоль, файл, текстовое форматирование, JSON-форматирование, асинхронная обёртка) закрывают подавляющее большинство потребностей вывода в едином стиле конфигураторов (ConfigInit + настройка + создание). Эксплуатационная тройка — ротация (xrtLogFileRotate), переоткрытие (xrtLogFileReopen, восстановление после внешнего усечения) и статистика (xrtLogFileStats) — адаптирует файловые Sink к долгой работе. Асинхронный Sink — ключ к горячему пути: поток подачи только ставит в очередь, выделенный поток пишет на диск — журнал больше не тормозит бизнес.
39.1引入
Три порога при выходе журналирования в продакшен. Порог первый: журналы должны уходить в сборочный конвейер в формате JSON (ELK/Loki едят структурные) — собственный Sink с ручной склейкой JSON? Экранирование полей, формат времени, вложенные цепочки ошибок — всё детали-ловушки. Порог второй: файл журнала растёт бесконечно — диск забьётся за три месяца; нужна ротация по размеру, сжатие архива и способность продолжать запись после внешнего усечения logrotate. Порог третий: на пике десятки тысяч записей в секунду, и IO-задержка записи ложится прямо на длительность запросов — журнал превращается в налог на производительность.
Три порога соответствуют трём группам ответов: JSON-Sink из коробки (поля автоматически в объект, цепочки ошибок разворачиваются сами, время миллисекундной меткой); тройка Rotate/Reopen/Stats файлового семейства (встроенная семантика ротации + восстановление после внешнего усечения); асинхронный Sink, уводящий запись с пути подачи (буферизация в очереди + выделенный поток, сливающий на диск). Их общая предпосылка — фундамент главы 38: все Sink реализуют один и тот же контракт функции записи.
39.2概念
Пять встроенных Sink одной таблицей
| Sink | Вход создания | Типовое применение |
|---|---|---|
| Консоль | xrtLogConsoleConfigInit + настройка | терминал разработки, stdout контейнера |
| Файл | xrtLogTextFile / xrtLogJsonFile | архив на диск, выбор по формату |
| Текстовое форматирование | семейство xrtLogTextWrite | собственные текстовые макеты (переиспользование движка форматирования) |
| JSON-форматирование | json-конфигуратор | сборочные конвейеры, структурный поиск |
| Асинхронная обёртка | асинхронный конфигуратор | подача на горячем пути без блокировки |
Стиль конфигураторов един на всю библиотеку: ConfigInit заполняет умолчания → правите нужные поля → создаёте за один шаг. Форма вывода JSON-Sink (видна в json-образце): верхнего уровня time (миллисекунды)/level/logger/message плюс объект fields, собирающий структурные поля, — имена и типы сохранены как есть, конвейеру ноль стоимости разбора.
Файловая эксплуатационная тройка
Запуск ротации бывает встроенной политикой (автоматически по размеру) и ручным (xrtLogFileRotate, эксплуатационный интерфейс — ротацию по времени реализует cron, просто вызывая его); xrtLogFilePath запрашивает текущий путь файла — после ротации путь меняется, и мониторинг обязан знать, куда идёт запись сейчас.
Асинхронный Sink: очередь + выделенный поток
Правда пути подачи: при синхронном файловом Sink IO-задержка каждой записи (миллисекунды) ложится прямо на бизнес-вызов. Структура асинхронного Sink: поток подачи делает только «форматирование в очередь» (миллисекунды; пакетный интерфейс безблокировочной очереди — MPMC главы 21 и есть её основание); выделенный поток выбирает пакетами и пишет на диск (амортизированная эффективность пакета в 32). Цена и дисциплина: при остановке обязателен Flush (xrtLogFlush — чистый выход означает, что записи очереди легли на диск; потеряно будет лишь несколько сотен миллисекунд перед крахом); ёмкость очереди — клапан противодавления (при заполнении — сброс или блокировка по политике; счётчик потерь идёт в Stats — «потоп журналов жертвует журналами» есть верная жертва).
Переиспользование форматирования: TextWrite и printf полей
Семейство xrtLogTextWrite позволяет собственному пункту назначения переиспользовать текстовый движок форматирования (ваш Sink ведает только «куда писать», форматирование — дело библиотеки); семейство xrtLogFieldsPrintf — printf-сборка значений полей (слой удобства; правило выбора главы 39 прежнее). Валидаторы (xrtLogTextConfigValidate / JsonConfigValidate / RecordValidate) стоят на входе загрузки конфигурации — ошибочная строка формата вскрывается на старте, а не миной в рантайме.
39.3示例
Полная программа: структурный вывод JSON
Из образца репозитория examples/logging/json/main.c — поля автоматически в объект, цепочки ошибок развёрнуты:
*/ #include <stdio.h> #include <string.h> #include <xrt.h> /* 输出回调:每个 JSON 分片一段(本例直写 stdout)。 */ static bool exampleLogWrite(xbytesview Data, ptr pUserData) { FILE* pFile = (FILE*)pUserData; return fwrite(Data.Data, 1u, Data.Size, pFile) == Data.Size; } int main(void) { xlogjsonconfig Config; xlogfield Fields[2]; xlogrecord Record; /* 手工组装完整记录:时间、级别、来源、消息、两个结构化字段。 */ memset(&Record, 0, sizeof(Record)); Fields[0] = xrtLogFieldUInt(XRT_STR_LITERAL("request_id"), 42u); Fields[1] = xrtLogFieldBool(XRT_STR_LITERAL("cached"), false); Record.Time = xrtNow(); Record.Level = XLOG_INFO; Record.Logger = XRT_STR_LITERAL("http"); Record.Message = XRT_STR_LITERAL("request completed"); Record.Fields = Fields; Record.FieldCount = 2u; /* 默认配置全字段输出;分片经回调直达目标。 */ if ( !xrtLogJsonConfigInit(&Config) || !xrtLogJsonWrite( &Record, &Config, exampleLogWrite, stdout, NULL ) ) { fprintf(stderr, "log JSON failed: %s\n", xrtErrorMessage(xrtGetError())); return 1; } return 0; }
Что только что произошло. (1) Одна подача LogFields (целочисленное поле 42, булево false) — JSON-Sink автоматически собирает их в объект fields: поля с типами доставлены нетронутыми (42 — число, а не "42"; false — булево), конвейеру ноль стоимости разбора. (2) time — юникс-миллисекунды (мера xtime главы 3) — машине сортируемо, человеком форматируемо; право выбора формата у потребителя. (3) В выводе message и fields разделены — сообщение для человека и поля для машинного поиска каждый на своём месте; в этом весь смысл «структурного журнала». Контраст с текстовым Sink главы 38 (игнорирует поля, печатает только сообщение): один Logger несёт оба Sink — текст в консоль, JSON в конвейер — двойной формат параллельно и есть ценность этой модели.
Полная программа: асинхронный Sink
Из examples/logging/async/main.c — ноль блокировки у потока подачи, выделенный поток сливает на диск:
*/ #include <xrt.h> #include <stdio.h> #include <string.h> /* 目标 Sink:直接输出原始消息(异步层不关心格式)。 */ static xlogresult exampleLogAsyncWrite( const xlogrecord* pRecord, ptr pUserData ) { FILE* pFile = (FILE*)pUserData; if ( fwrite( pRecord->Message.Data, 1u, pRecord->Message.Size, pFile ) != pRecord->Message.Size || fputc('\n', pFile) == EOF ) { return XLOG_RESULT_ERROR; } return XLOG_RESULT_WRITTEN; } /* 目标 Flush:由异步栅栏在工作线程上按序调用。 */ static bool exampleLogAsyncFlush(ptr pUserData) { return fflush((FILE*)pUserData) == 0; } int main(void) { xlogsinkconfig TargetConfig; xlogasyncconfig AsyncConfig; xlogrecord Record; xlogsink* pTarget; xlogsink* pAsync; /* 先造同步目标 Sink(stdout 直写)。 */ memset(&TargetConfig, 0, sizeof(TargetConfig)); TargetConfig.Name = XRT_STR_LITERAL("stdout"); TargetConfig.Level = XLOG_TRACE; TargetConfig.Write = exampleLogAsyncWrite; TargetConfig.Flush = exampleLogAsyncFlush; TargetConfig.UserData = stdout; pTarget = xrtLogSinkCreate(&TargetConfig); if ( pTarget == NULL ) { return 1; } /* 配置背压参数后包装:Async 持有目标引用,本地引用可释放。 */ if ( !xrtLogAsyncConfigInit(&AsyncConfig) ) { xrtLogSinkFree(pTarget); return 2; } AsyncConfig.Capacity = 256u; AsyncConfig.ByteLimit = 2u * 1024u * 1024u; pAsync = xrtLogAsync(pTarget, &AsyncConfig); xrtLogSinkFree(pTarget); if ( pAsync == NULL ) { return 3; } /* 提交 = 入队即返回;业务线程不等待写盘。 */ memset(&Record, 0, sizeof(Record)); Record.Level = XLOG_INFO; Record.Message = XRT_STR_LITERAL("asynchronous sink ready"); if ( xrtLogSinkSubmit(pAsync, &Record) != XLOG_RESULT_WRITTEN ) { xrtLogSinkFree(pAsync); return 4; } /* 栅栏冲刷:返回即保证该条已真实写出。 */ if ( !xrtLogSinkFlush(pAsync) ) { xrtLogSinkFree(pAsync); return 5; } xrtLogSinkFree(pAsync); return 0; }
Что только что произошло. (1) Асинхронный Sink оборачивает настоящее место назначения (файл) — для Logger он обычный Sink, интерфейс подачи не меняется. (2) Главный поток подаёт записи только в очередь (asynchronous sink ready печатается немедленно — подача не блокировалась записью на диск); выделенный поток потребляет пакетами в фоне. (3) Flush на пути остановки: очередь опустошена, файловый дескриптор закрыт — чистый выход. Образец sink_tour оставлен как полное чтение эксплуатационных интерфейсов: он проходит семейство Ref/Name/Level/SetLevel/Count/Stats/Detach и файловое семейство (Path/Rotate/Reopen/Stats) от начала до конца — девять ok на девять групп интерфейсов.
39.4契约
- Единые конфигураторы:
ConfigInit→ настройка → создание; валидаторы стоят на входе конфигурации. - Форма JSON: time в миллисекундах / level / logger / message + объект fields — поля с типами, время машинного порядка.
- Семантика ротации: Rotate переименовывает в архив и открывает новый файл; автоматически по размеру плюс ручной запуск; Path запрашивает текущий путь.
- Восстановление после усечения: Reopen переоткрывает после внешнего move/усечения — встроенная поддержка сотрудничества с logrotate.
- Асинхронная дисциплина: Flush при остановке; ёмкость очереди — клапан противодавления, счётчик потерь в Stats; подача миллисекундная, слив амортизирован пакетами.
- Переиспользование форматирования: собственные пункты назначения — семейство TextWrite; машинные поля по-прежнему через LogFields.
От примеров к инженерии: три шаблона сборки Sink
Среда разработки: один консольный Sink (текстовый формат, порог DEBUG) — мгновенно читаемо в терминале, пять минут сборки. Продакшен-стандарт: JSON-файловый Sink (порог INFO, ротация по размеру) + асинхронная обёртка (противодавление очереди) + консольный Sink уровня ERROR (канал stderr-тревог в контейнерной среде) — три Sink, каждый на своём посту, одна сборка. Сборочный конвейер: JSON-формат напрямую в stdout (сбор stdout контейнера) или ротируемые файлы плюс агент класса Filebeat — формат унифицирован уже на слое Sink, конвейеру ноль адаптации. Общая форма трёх шаблонов: код сборки концентрируется в функции старта (дисциплина главы 38), конфигурация приходит из JSON-файла главы 32 (тип Sink, путь, уровень — всё настраивается) — «журнальная конфигурация журнальной системы» есть типовое упражнение сборки, ведомой конфигурацией.
Эксплуатационный взгляд: журнал — это поток данных
Считать журналирование «печатью» — взгляд разработчика; после выхода в продакшен это поток данных — с источником (бизнес-код), с трубопроводом (Sink и очереди), с пунктом назначения (файлы/системы сбора), с ёмкостью (диск и очередь) и с жизненным циклом (ротация и архив). Принимайте эксплуатационные решения с этой точки зрения: планирование ёмкости читает из Stats скорость записи × период хранения; защита от потопа — политика сброса очереди плюс динамическое понижение уровня (SetLevel регулируется на ходу); восстановление после сбоев репетирует Reopen (тренировка внешнего усечения); контроль качества сверяет «поданное против записанного» (асинхронные потери в пределах терпимости). Этот взгляд объясняет и повторную применимость конвейерного мышления главы 36 — журнальная система сама есть конвейер «запись → форматирование → очередь → диск», и навыки сборки из тома 4 заступают здесь напрямую.
Типичные ошибки
Симптом: при воспроизведении краха «последний отрезок журнала исчез» — именно те несколько сотен миллисекунд, что нужнее всего; грациозный путь ничего не теряет, но kill -9 теряет всегда.
Причина: процесс выходит, пока записи асинхронной очереди ещё не на диске — очередь энергозависима, Flush — обещание долговечности.
/* остановка */ xrtLogSinkFree(pAsyncSink); /* освобождено напрямую - содержимое очереди выброшено */
/* остановка: сперва слить очередь, потом освобождать */ xrtLogFlush(pLogger); /* или специализированный Flush асинхронного Sink */ xrtLogSinkFree(pAsyncSink);
Симптом: счёт файловых дескрипторов взлетает; ротации топчут друг друга; тревоги лимита дескрипторов процесса.
Причина: «место, где пишется журнал», приняли за «место, где строится Sink» — Sink это глобально собираемый ресурс, а не одноразовый объект на скорую руку.
void handle_request(void) { xlogsink* pSink = create_file_sink("app.log"); /* по одному на запрос */ xrtLogAttach(gLog, pSink); xrtLog(gLog, XLOG_INFO, ...); xrtLogSinkFree(pSink); /* построил-разобрал-построил */ }
/* сборка один раз на старте, глобальное переиспользование (дисциплина "концентрированной сборки" главы 38) */ static xlogsink* gFileSink; void init_logging(void) { gFileSink = create_file_sink("app.log"); xrtLogAttach(gLog, gFileSink); } void handle_request(void) { xrtLog(gLog, XLOG_INFO, ...); } /* только подача, никогда сборка */
Практические задания
Повесьте на один Logger текстовый Sink (консоль) + JSON-Sink (файл); подайте одну и ту же запись LogFields — проверьте раздельное представление полей в текстовой и JSON-строках.
Реализуйте «ротацию каждый час» через xrtLogFileRotate: фоновый таймер запускает, архив после ротации сжимается gzip-ом (разовое сжатие главы 29), xrtLogFilePath выводит текущий путь. Подсказка: таймером сойдёт простой поток опроса (в томе 6 есть настоящие таймеры).
Постройте нагрузку в сто тысяч подач в секунду: синхронный файловый Sink и асинхронный Sink гоняют по минуте; измерьте три группы — среднее время стороны подачи, счётчик потерь (Stats) и суммарный объём на диске. Критерии приёмки: время подачи в асинхронной версии ниже синхронной минимум на два порядка; при политике потерь наводнение не блокирует бизнес; отчёт содержит три группы цифр с анализом.
Итоги
| Тема | Кратко |
|---|---|
| Пять Sink | консоль / файл (текст/JSON) / переиспользование форматирования / асинхронная обёртка |
| Форма JSON | time миллисекунды + level + logger + message + объект fields (поля с типами) |
| Эксплуатационная тройка | Rotate / Reopen после усечения / Stats; Path запрашивает текущий путь |
| Асинхронная дисциплина | Flush при остановке; очередь = клапан противодавления; подача миллисекундная, слив пакетами |
| Дисциплина сборки | Sink собирается один раз на старте и переиспользуется глобально; бизнес-код только подаёт |
| Валидаторы | на входе загрузки конфигурации - ошибки формата вскрываются на старте |
📖 Справочники API в этой главе:logger (116 записей)io (62 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки