SQLite можно настроить на использование функции обратного вызова, которая передаёт код ошибки и краткое сообщение об ошибке при возникновении ошибок. Этот механизм помогает отслеживать редкие и трудноразличимые проблемы в реальной эксплуатации. Разработчикам приложений настоятельно рекомендуется воспользоваться возможностями журнала ошибок SQLite: это требует минимальных затрат ресурсов и заметно упрощает процесс отладки

Регистрация callback-функции журнала ошибок
На один процесс может быть зарегистрирован только один обратный вызов журналирования ошибок. Он регистрируется при запуске с помощью кода на языке C, аналогичного следующему:
sqlite3_config(SQLITE_CONFIG_LOG, errorLogCallback, pData);
Функция обратного вызова журналирования ошибок может выглядеть примерно так:
void errorLogCallback(void *pArg, int iErrCode, const char *zMsg){
fprintf(stderr, "(%d) %s\n", iErrCode, zMsg);
}
Приведённый выше пример иллюстрирует сигнатуру функции обратного вызова. Однако во встроенных приложениях сообщения обычно не выводятся в stderr. Вместо этого их лучше сохранять в заранее выделенном кольцевом буфере (ring buffer), откуда они доступны для получения диагностической информации во время отладки. Также можно направлять сообщения в Syslog
В любом случае информация должна храниться в доступном для разработчиков месте, а не выводиться пользователям
Технически, показывать сообщения журнала ошибок конечным пользователям не является чем-то предосудительным. Эти сообщения не содержат конфиденциальной информации и, скорее, предназначены для специалистов по базам данных, чем для широкой аудитории. Однако такие сообщения следует предоставлять только тем, кто владеет необходимыми знаниями для их правильного понимания
По этой теме полезно отдельно посмотреть EXPLAIN QUERY PLAN: план выполнения SQL-запроса в SQLite, чтобы расширить контекст и сравнить подходы
По этой теме полезно отдельно посмотреть Создание Flutter-приложения с SQLite, BLoC и Streams, чтобы расширить контекст и сравнить подходы
Интерфейс SQLITE_CONFIG_LOG: аргументы и ограничения
Третий аргумент интерфейса sqlite3_config(SQLITE_CONFIG_LOG, ...) — аргумент pData в приведённом выше примере — это указатель на произвольные данные. SQLite передаёт этот указатель в качестве первого аргумента функции обратного вызова журналирования ошибок. При необходимости указатель можно использовать для передачи специфической для приложения информации о настройке или состоянии
Или это может быть просто указатель NULL, который игнорируется функцией обратного вызова
Второй аргумент функции обратного вызова — это целочисленный код ошибки, а третий — текст сообщения. Текст сообщения хранится в буфере на стеке вызывающей функции и действителен только во время выполнения функции обратного вызова. Если требуется сохранить сообщение, необходимо скопировать его в постоянное хранилище до завершения работы функции
С функцией обратного вызова журналирования ошибок следует обращаться так, как с обработчиком сигналов. Приложение должно сохранять или иным образом обрабатывать ошибку, а затем как можно быстрее возвращать управление. Никакие другие API SQLite не должны вызываться из журналировщика ошибок
SQLite не будет работать правильно, если вызывать его из этого контекста, так как обратный вызов может происходить при сбое выделения памяти. Поэтому попытка выделять память внутри обработчика ошибок обычно приводит к ошибкам. Не следует даже предполагать возможность сохранения сообщения об ошибке в другой базе данных SQLite
При желании приложения могут использовать API sqlite3_log(E, F, ..) для отправки новых сообщений в журнал, однако это не рекомендуется. Интерфейс sqlite3_log() предназначен для использования только расширениями, а не приложениями
Какие сообщения попадают в журнал ошибок SQLite
Сообщения об ошибках и их формат могут меняться от одной версии к другой, поэтому приложениям не следует зависеть от конкретного формата текста или кодов ошибок. Хотя изменения не происходят произвольно, но они могут случаться
Ниже приведён неполный список видов сообщений, которые могут появляться в функции обратного вызова журналирования ошибок
При возникновении ошибки, как во время компиляции SQL-оператора (через sqlite3_prepare_v2()), так и во время его выполнения (через sqlite3_step()), ошибка фиксируется в журнале
Когда происходит изменение схемы, требующее повторного разбора и подготовки операторов, такое событие также фиксируется в журнале с кодом ошибки SQLITE_SCHEMA. Обычно повторный разбор и подготовка выполняются автоматически при первом использовании sqlite3_prepare_v2(), что рекомендовано, поэтому такие записи оказываются единственным инструментом для отслеживания повторных подготовок
Сообщения SQLITE_NOTICE фиксируются в журнале каждый раз, когда база данных восстанавливается после неожиданного завершения предыдущего процесса записи, не завершившего транзакцию. Код ошибки — SQLITE_NOTICE_RECOVER_ROLLBACK в случае восстановления журнала отката и SQLITE_NOTICE_RECOVER_WAL при восстановлении с упреждающей записью (WAL)
Сообщения SQLITE_WARNING записываются в журнал, когда файлы базы данных переименовываются или создаются псевдонимы способами, которые могут привести к повреждению базы данных
Условия ошибки нехватки памяти (OOM, Out of Memory) генерируют события журналирования с кодом ошибки SQLITE_NOMEM и сообщением, указывающим, сколько байт памяти было запрошено при неудавшемся выделении
Ошибки ввода-вывода в интерфейсе операционной системы генерируют события журналирования ошибок. Сообщение для этих событий содержит номер строки в исходном коде, где возникла ошибка, и имя файла, связанного с событием, если соответствующий файл существует
При обнаружении повреждения базы данных вызывается обратный вызов журналирования ошибок SQLITE_CORRUPT. Как и в случае ошибок ввода-вывода, текст сообщения об ошибке содержит номер строки в исходном коде, где ошибка была обнаружена впервые
Обратный вызов журналирования ошибок вызывается при ошибках SQLITE_MISUSE. Это полезно для обнаружения проблем проектирования приложения в случаях, когда коды возврата не проверяются последовательно в коде приложения
SQLite стремится поддерживать низкий трафик журналирования ошибок и отправлять сообщения только тогда, когда действительно что-то идёт не так. Приложения могут дополнительно сократить трафик сообщений об ошибках, намеренно игнорируя определённые классы сообщений, которые их не интересуют. Например, приложение, часто изменяющее схему базы данных, может захотеть игнорировать все ошибки SQLITE_SCHEMA
Частые ошибки при реализации error log callback
На практике я встречал несколько устойчивых паттернов неправильного использования обратного вызова журналирования ошибок, которые стоит перечислить отдельно
Вызов других API SQLite внутри обратного вызова. Это нарушает требование реентерабельности. SQLite не гарантирует корректное поведение, если из журналировщика ошибок вызывается любой другой интерфейс библиотеки — даже кажущийся безопасным
Выделение памяти внутри журналировщика. Обратный вызов может быть вызван именно в момент сбоя выделения памяти. Попытка выделить память в этот момент с высокой вероятностью приведёт к повторному сбою или неопределённому поведению
Хранение указателя на строку сообщения. Текст сообщения об ошибке живёт в буфере на стеке вызывающей функции и перестаёт быть действительным сразу после возврата из обратного вызова. Если сообщение нужно сохранить, его необходимо скопировать немедленно
Использование sqlite3_log() из кода приложения. Этот интерфейс предназначен исключительно для расширений SQLite. Использование его в прикладном коде формально не запрещено, но не рекомендуется документацией
Регистрация нескольких обратных вызовов. На один процесс допускается только один обратный вызов журналирования ошибок. Повторный вызов sqlite3_config(SQLITE_CONFIG_LOG, ...) заменяет предыдущий обратный вызов, а не добавляет новый
Рекомендации по внедрению журнала ошибок в приложение
Я рекомендую реализовывать обратный вызов журналирования ошибок на самом раннем этапе цикла разработки — это позволяет быстро обнаруживать неожиданное поведение ещё до того, как оно превратится в трудновоспроизводимую проблему в продакшене. По моему опыту, журналировщик чаще всего обнаруживает проблемы именно там, где их меньше всего ожидают
Несколько практических советов по организации хранения сообщений:
- Кольцевой буфер в памяти — подходит для встраиваемых систем, где нет возможности писать в файл или Syslog. Буфер фиксированного размера перезаписывает старые записи новыми, что позволяет всегда иметь под рукой последние N сообщений при подключении отладчика.
- Syslog — удобен для серверных приложений, где уже настроена централизованная агрегация логов.
- Файл журнала — простейший вариант для десктопных приложений, однако требует аккуратной ротации, чтобы файл не рос бесконтрольно.
Независимо от выбранного хранилища, обратный вызов должен работать максимально быстро: скопировать данные и вернуть управление. Любая блокирующая операция внутри журналировщика может привести к непредсказуемым задержкам в работе SQLite
Использование обратного вызова журнала ошибок настоятельно рекомендуется. Диагностическая информация, предоставляемая обработчиком журнала ошибок (error log callback), оказалась очень полезной для выявления труднообнаруживаемых проблем, возникающих в приложениях после их развёртывания в реальных условиях эксплуатации. Обратный вызов журнала ошибок также оказался полезным для обнаружения случайных ошибок, которые приложение пропускает из-за непоследовательной проверки кодов возврата API. Разработчикам рекомендуется реализовать обратный вызов журнала ошибок на раннем этапе цикла разработки, чтобы быстро обнаруживать неожиданное поведение, и оставлять обратный вызов включённым на протяжении всего развёртывания. Если обработчик журнала ошибок никогда не обнаружит проблему, никакого вреда не будет. Но отсутствие надлежащего обработчика журнала ошибок может впоследствии существенно снизить диагностические возможности
Связанные материалы
Можно ли зарегистрировать несколько обратных вызовов журналирования ошибок в одном процессе?
Нет. SQLite поддерживает только один обратный вызов журналирования ошибок на процесс. Повторный вызов sqlite3_config(SQLITE_CONFIG_LOG, ...) заменяет ранее зарегистрированный обратный вызов
Безопасно ли вызывать другие функции SQLite внутри обратного вызова журналирования ошибок?
Нет. SQLite не является реентерабельным через обратный вызов журналирования ошибок. Вызов любых других API SQLite из журналировщика — прямо или косвенно — приведёт к неопределённому поведению
Как долго действителен указатель на текст сообщения об ошибке?
Только в течение времени выполнения функции обратного вызова. Текст сообщения хранится в буфере на стеке вызывающей функции. Если сообщение нужно сохранить, его необходимо скопировать до возврата из обратного вызова
Когда вызывается обратный вызов с кодом SQLITE_MISUSE?
При некорректном использовании API SQLite — например, когда приложение вызывает функции в неправильном порядке или не проверяет коды возврата. Это помогает обнаруживать архитектурные ошибки в коде приложения на ранних этапах
Стоит ли оставлять обработчик журнала ошибок включённым в продакшен-сборке?
Да. Накладные расходы минимальны, а диагностическая ценность в случае возникновения проблем очень высока. Отключение журналировщика в продакшене лишает разработчика важного инструмента для анализа редких и труднообнаруживаемых сбоев



