Логирование в NGINX
Логи каждые важны, Логи каждые нужны — (с) Любой Бэкендер
Наши коллеги-бэкендеры уже давно поняли важность алертинга и логирования и учатся этому ещё в детском саду, а нас, как правило, этому не учат — чего в наших кнопках логировать? Однако в следующий раз, когда у вас сломается DNS, кто-то придет ломать ваши сервера DDoS’ом или кто-то попытается загрузить картинку в 20 ГБ, как вы об этом узнаете?
На самом деле, большинство балансировщиков и реверс-прокси включают логирование по дефолту (и слава богу!). Сегодня мы посмотрим на стандартное логирование в NGINX и научимся его настраивать так, чтобы потом смотреть логи, не залезая по SSH на сервер.
В вводной статье про NGINX я обошел стороной главный конфигурационный файл — /etc/nginx/nginx.conf. Он не был нам нужен ни для настройки раздачи статики, ни для настройки SSL, но именно в нём хранятся общие настройки NGINX, в том числе логирование. Давайте посмотрим на все части дефолтного конфига, которые отвечают за логирование:
Логирование ошибок: директива error_log
В самом верхнем контексте у нас определена директива error_log; она определяет, как мы будем логировать ошибки. Её два параметра — путь к файлу для логов и уровень ошибок, начиная с которого мы будем их логировать.
Всего уровней ошибок семь, по убыванию критичности: emerg, alert, crit, error, warn, notice, debug. Их можно разделить на три группы:
- emerg, alert, crit — ошибки, из-за которых приложение может быть крайне нестабильным. Часто после одного подобного лога NGINX умирает. Видите их — бегите чинить;
- error, warn — что-то пошло не так во время обработки запроса: например, сервер, в который попытались сходить, недоступен;
- notice, debug — всякие системные логи от NGINX о том, что происходит у него под капотом в данный момент.
NGINX будет записывать в файл ошибки с уровнем, равным или выше указанному нами. Например, если мы укажем error_log file warn, то в лог попадут ошибки уровней warn, error, crit, alert, emerg. Если же мы решим не указывать уровень для логирования, по дефолту включится error.
Логирование запросов: директива access_log
Так как логировать в запросах мы можем сотни разных параметров, NGINX позволяет нам определить, какие из них нам действительно нужно видеть, и в каком формате и порядке. Директива log_format отвечает за настройку формата логов и принимает два параметра: название формата и строку-шаблон.
В шаблоне можно использовать любую из доступных переменных, а также специальные переменные, валидные только внутри этого шаблона.
Директива access_log, как и error_log, принимает два параметра: первый — тоже путь к файлу для логирования, а второй — выбранный формат.
Возможность определять шаблон отдельно от access_log нужна потому, что мы можем использовать access_log не только в контексте http, но и в контексте server и даже location. Хоть и редко, но возможность записывать логи в разных форматах и файлах помогает. Например, когда вы включаете на каком-нибудь из серверов gzip и хотите посмотреть, насколько хорошо он справляется, можете записывать в отдельный лог запросы с переменной $gzip_ratio.
Важно помнить, что хоть access_log мы можем определить где угодно, директивы log_format могут располагаться только внутри контекста http.
Логирование в формате JSON
Если мы захотим получить доступ к логам из другой системы или собрать из них какую-нибудь таблицу, то нам придется парсить те строки, которые мы форматировали, что очень неудобно. Придется писать парсер для того, что мы сами и сериализовали :)
Лучше всего access_log форматировать как JSON, например, так:
log_format response escape=json '{' '"@timestamp": "$time_iso8601", ' '"client": "$remote_addr", ' '"request":"$request",' '"host": "$host",' '"request_length": $request_length, ' '"status": $status,' '"system": "<если у вас несколько балансеров, можете здесь указать название нужного>",' '"bytes_sent": $bytes_sent, ' '"body_bytes_sent": $body_bytes_sent, ' '"referer": "$http_referer", ' '"user_agent": "$http_user_agent", ' '"upstream_addr": "$upstream_addr", ' '"upstream_status": $upstream_status, ' '"request_time": $request_time, ' '"upstream_response_time": $upstream_response_time, ' '"upstream_connect_time": $upstream_connect_time, ' '"upstream_header_time": $upstream_header_time, ' '"forwarded_for": "$http_x_forwarded_for"' '}';
Это формат, которым обычно пользуюсь я: здесь достаточно контекста, чтобы узнать всю необходимую для выявления проблем информацию.
Обратите внимание на появившийся третий параметр — escape=json. Он отвечает за экранирование нашего лога так, чтобы в результате валидность JSON формата не нарушалась: " превратится в \", \ в \\ и так далее.