Ориентация главы

Предыдущая глава написала все детали собственного 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, собирающий структурные поля, — имена и типы сохранены как есть, конвейеру ноль стоимости разбора.

Файловая эксплуатационная тройка

1Rotateротация запускается немедленно - текущий файл переименовывается в архив, по исходному пути открывается новый, запись продолжается
2Reopenпереоткрытие после внешнего усечения (logrotate move) - дескриптор инвалидирован, восстановление автоматическое
3Statsнакопленный объём записи/потерь и другая статистика на ходу - источник данных для планирования ёмкости и тревог

Запуск ротации бывает встроенной политикой (автоматически по размеру) и ручным (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 — поля автоматически в объект, цепочки ошибок развёрнуты:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/logging/json/main.c -lws2_32 -liphlpapi{"time":1788575322193322,"level":"INFO","logger":"http", "message":"request completed","fields":{"request_id":42,"cached":false}}

Что только что произошло. (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 — ноль блокировки у потока подачи, выделенный поток сливает на диск:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/logging/async/main.c -lws2_32 -liphlpapiasynchronous sink ready

Что только что произошло. (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 заступают здесь напрямую.

Типичные ошибки

Ловушка 1: остановка асинхронного Sink без Flush:Ловушка 1: остановка асинхронного Sink без Flush

Симптом: при воспроизведении краха «последний отрезок журнала исчез» — именно те несколько сотен миллисекунд, что нужнее всего; грациозный путь ничего не теряет, но kill -9 теряет всегда.

Причина: процесс выходит, пока записи асинхронной очереди ещё не на диске — очередь энергозависима, Flush — обещание долговечности.

错误写法
/* остановка */
xrtLogSinkFree(pAsyncSink);   /* освобождено напрямую - содержимое очереди выброшено */
正确写法
/* остановка: сперва слить очередь, потом освобождать */
xrtLogFlush(pLogger);          /* или специализированный Flush асинхронного Sink */
xrtLogSinkFree(pAsyncSink);
Ловушка 2: файловый Sink на каждую запись:Ловушка 2: файловый Sink на каждую запись

Симптом: счёт файловых дескрипторов взлетает; ротации топчут друг друга; тревоги лимита дескрипторов процесса.

Причина: «место, где пишется журнал», приняли за «место, где строится 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) / переиспользование форматирования / асинхронная обёртка
Форма JSONtime миллисекунды + level + logger + message + объект fields (поля с типами)
Эксплуатационная тройкаRotate / Reopen после усечения / Stats; Path запрашивает текущий путь
Асинхронная дисциплинаFlush при остановке; очередь = клапан противодавления; подача миллисекундная, слив пакетами
Дисциплина сборкиSink собирается один раз на старте и переиспользуется глобально; бизнес-код только подаёт
Валидаторына входе загрузки конфигурации - ошибки формата вскрываются на старте

📖 Справочники API в этой главе:logger (116 записей)io (62 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки