Расширения PostgreSQL: логирование и исключения

28 марта 2022

Хотелось бы продолжить рассказ о расширениях PostgreSQL и поговорить о логировании и исключениях. Рассматривать их нужно вместе, поскольку в PostgreSQL это связанные механизмы. Хотя ранее мы уже и использовали макрос elog(), тема была затронута лишь поверхностно.

Большую часть того, что нужно знать о логировании и исключениях, можно почерпнуть из файла src/include/utils/elog.h в исходном коде PostgreSQL.

В простейшем случае логирование осуществляется при помощи elog():

int64 now = (int64)GetCurrentTransactionStartTimestamp();
elog(NOTICE, "Transaction start timestamp: %lld", (long long)now);

Форматирование практически такое же, как в обычном printf(), но с некоторыми отличиями. Например, можно написать %m, и это будет аналогично строке, возвращаемой strerror(errno). Дополнительных аргументов для %m передавать не нужно. Для форматирования 64-х битных чисел можно воспользоваться %lld или %llu. Чтобы это правильно работало, соответствующие аргументы нужно явно преобразовать в long long или unsigned long long соответственно. Все подробности касаемо форматированного вывода в PostgreSQL можно найти в файле src/port/snprintf.c.

Есть разные уровни логирования — DEBUG5…DEBUG1, INFO, NOTICE, WARNING, ERROR, FATAL, PANIC и другие. Они ведут себя по-разному. Например, NOTICE отображается в пользовательской сессии, но по-умолчанию не пишется в лог-файл. ERROR бросает исключение (о них ниже) и отменяет исполнение текущей транзакции. FATAL приводит к завершению работы текущего процесса, а PANIC останавливает работу всей СУБД. Подробнее о всех существующих уровнях логирования можно прочитать в elog.h. Что пишется в лог-файл определяется настройками, описанными в разделе Error Reporting and Logging документации.

Макрос elog() определен, как обвязка над ereport():

#define elog(elevel, ...)  \
    ereport(elevel, errmsg_internal(__VA_ARGS__))

В новом коде рекомендуется использовать именно ereport(). Типичный пример:

const char* arg = TextDatumGetCString(PG_GETARG_DATUM(0));
ereport(ERROR,
    (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     errmsg("Invalid argument \"%s\"", arg),
     errhint("This is a hint message.")));

Указывать errhint() не обязательно. Хотя использовать elog() и не рекомендуется в новом коде, иногда он полезен. Например, бывает так, что какая-то ошибка воспроизводится на CI, но не локально. Как правило, в подобных случаях быстрее и проще всего диагностировать проблему при помощи отладочного вывода, и elog() оказывается чуточку удобнее, чем ereport().

PostgreSQL имеет собственную реализацию исключений, основанную на setjmp() и longjmp(). Чтобы бросить исключение, нужно вызвать elog(ERROR, ... или ereport(ERROR, .... Также предусмотрены макросы PG_TRY(), PG_CATCH(), PG_FINALLY(), и другие. Их семантика аналогична try, catch и finally в языках программирования с исключениями.

Типичный пример использования:

PG_TRY();
{
    elog(ERROR, "oops...");
}
PG_FINALLY();
{
    elog(NOTICE, "cleaning up");
}
PG_END_TRY();

Обычно эти макросы используют для освобождения ресурсов. Например, если в PG_TRY() мы открыли сетевое соединение, после чего возникло исключение, соединение можно закрыть в PG_FINALLY(). Важно понимать, что здесь мы пишем не на C++ и не на Java. Если было брошено исключение, это значит, что транзакция не может быть завершена. Поэтому код, который ловит исключение, обрабатывает, и затем пытается продолжить что-то делать, обычно бесмысленен. В большинстве расширений вы вообще не увидите PG_TRY().

Это по большому счету все, что нужно знать об исключениях и логировании в PostgreSQL. Здесь нет ничего сложного, лишь кое-какая местная специфика. Полную версию исходников к посту вы найдете на GitHub.

Дополнение: Расширения PostgreSQL: управление памятью

Метки: , , .