nginx日志管理

nginx是一个高性能的HTTP和反向代理服务器,以其轻量级、资源占用少、并发能力强等优点被广泛使用。

在使用过程中,我们常常希望能够将接到的请求记录到日志以便统计分析或问题排查,自然少不了做日志管理。本人近期刚刚开始学习nginx的使用,总结遇到的日志管理方面的问题整理如下(本文使用nginx-1.12.1版本):

自定义日志格式

在nginx 的日志目录logs下,默认有以下3个文件:

  • access.log:记录请求日志
  • error.log:记录错误日志,例如启动时端口被占用,请求的资源不存在等。
  • nginx.pid:记录启动时nginx的进程id

nginx的日志配置是在conf/nginx.conf文件中,初始内容如下(省略部分被注释的内容):

#user  nobody;#运行worker process的用户
worker_processes  1;#worker 进程数据,通常可以设置为cpu核数

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;#进程pid文件


events {
    worker_connections  1024;#每个worker process能处理的最大连接数
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #nginx默认access日志格式
    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #默认access日志路径
    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;#监听端口
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        #默认请求匹配路径
        location / {
            root   html;#请求的根目录是html,即从html目录查找资源
            index  index.html index.htm;#默认首页
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

从配置文件中我们可以看出,access的默认日志格式为:

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                  '$status $body_bytes_sent "$http_referer" '
                  '"$http_user_agent" "$http_x_forwarded_for"';
    

各字段含义如下:

字段 含义 示例
remote_addr 客户端地址 211.28.65.253
remote_user Auth Basic Module验证的用户名 test
time_local 访问时间和时区 24/Oct/2017:19:47:17 +0800
request 请求的URI和HTTP协议 GET /index.html HTTP/1.1
status HTTP请求状态 200
body_bytes_sent 发送给客户端文件内容大小 612
http_referer url跳转来源 https://www.baidu.com
http_user_agent 用户终端浏览器等信息 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36
http_x_forwarded_for 简称XFF头,它代表客户端,也就是HTTP的请求端真实的IP,只有在通过了HTTP 代理或者负载均衡服务器时才会添加该项,第一个值为真实的ip 10.21.74.53, 10.29.83.46

利用这些内置的变量,我们就可以进行日志格式的自定义,我们甚至可以将日志格式变为json的,只需修改配置如下:

log_format json_log escape=json '{"timestamp": "$time_local", '
    '"remote_addr": "$remote_addr", '
    '"remotr_user": "$remote_user", '
    '"referer": "$http_referer", '
    '"request": "$request", '
    '"status": $status, '
    '"bytes":$body_bytes_sent, '
    '"agent": "$http_user_agent", '
    '"x_forwarded": "$http_x_forwarded_for", '
    '"upstr_addr": "$upstream_addr",'
    '"upstr_host": "$upstream_http_host", '
    '"request_body": "$request_body", '
    '"ups_resp_time": "$upstream_response_time"}';

注意要在配置中加上escape=json

记录post请求内容

默认情况下,nginx只能响应GET方式的静态资源请求,如果发送POST方式的静态资源请求,则会得到如下信息:

<html>
    <head>
        <title>405 Not Allowed</title>
    </head>
    <body bgcolor="white">
        <center>
            <h1>405 Not Allowed</h1>
        </center>
        <hr>
        <center>nginx/1.12.1</center>
    </body>
</html>

有时,我们希望能够使用POST方式来请求静态资源,同时发送一些参数,比如我最近就遇到了这种需求,希望通过POST请求来记录请求日志,包括请求中的参数。

关于405的问题,网上给出的解决方法主要是两种:
(1)将405重定向到200
(2)修改源代码

个人觉得这两种方式都不是很好,最终查到了另一种更优雅的方式:nginx+Lua。

具体做法如下:
(1)下载LuaJIT并安装,本人安装的为2.0.5版本,下载地址:http://luajit.org/download.html
安装完成后,设置环境变量(编辑~/.bashrc):

export LUAJIT_LIB=/home/work/local/LuaJIT-2.0.5/usr/local/lib
export LUAJIT_INC=/home/work/local/LuaJIT-2.0.5/usr/local/include/luajit-2.0

其中变量指向安装目录中的相应资源。
(2)下载lua-nginx-module,并解压,无需安装,本人使用的是0.10.10版本。下载地址:https://github.com/openresty/lua-nginx-module/releases
(3)安装nginx:

./configure –prefix=安装目录 –with-pcre=pcre源码路径 –add-module=lua-nginx-modul解压后路径
make -j2
make install

额外补充一点,pcre是安装nginx依赖的另一个包,因为与主题无关,因此没有专门交代。

(4)修改nginx.conf配置文件

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" "$request_body" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
location / {
            lua_need_request_body on;
            content_by_lua 'local s = ngx.var.request_body';
            root   html;
            index  index.html index.htm;
        }

注意:我们使用到另一个内置变量:request_body,该变量的值就是客户发送的POST请求的请求体。

各位看官看到这里,难免会问:既然nginx已经给我们提供了request_body变量,那我们直接用不就行了吗?干嘛还要扯出Lua呢?

这个问题是这样:nginx中读取POST数据必须要调用ngx_http_read_client_request_body()函数,而默认情况下,这个函数是不会被调用的。(莫名其妙???so am i)

那么我们怎么能够调用这个函数呢?这就要借助于ngx_lua解决,只要在输出log之前读一遍request_body,就可以调用该函数,配置如上面所示。

日志分割

nginx默认不会对日志做分割,随着时间的积累,access.log和error.log将会越来越大,非常不便于我们查看日志。

在linux下进行日志分割的一个常用工具是logrotatelogrotate功能非常强大,除了支持日志按照一定规则分割,还支持对历史日志进行压缩,过期日志删除等常见操作,可谓是日志管理的瑞士军刀。

logrotate的使用非常简单,只需写一个配置文件,基本格式如下:

/home/work/local/nginx-1.12.1/logs/access.log {
    daily #日志分割的周期,共有daily,weekly,monthly,yearly几种
    rotate 7 #保留多少份历史日志,为7表示保留1周的历史日志
    nocompress  #是否将历史日志进行压缩
    missingok  # 如果遇到文件无法找到,继续执行,忽略错误
    dateext  #以日期作为历史文件结尾
    dateformat .%Y-%m-%d。#与dateext配合使用,设定后缀格式
    sharedscripts #在所有的日志文件都轮转完毕后统一执行一次脚本
    postrotate # 指定日志轮滚完成后执行的脚本
        kill -USR1 `cat /home/work/local/nginx-1.12.1/logs/nginx.pid`
    endscript
}

需要注意的是:kill -USR11的作用是给nginx进程发送一个指令,让nginx重新加载配置,目的是更新日志文件句柄,使新的日志输出到新的日志文件中。

logrotate只负责对日志的管理,定时执行还要靠crontab。有关crontab的配置和使用网上很多,这里就不再赘述了。

在crontab定时任务中配置:

0 0 * * * /usr/sbin/logrotate -f /home/work/local/nginx-1.12.1/conf/access.cron

这样,系统就会在每天0点0分执行日志轮滚操作。logrotate -f的作用是让轮滚操作强制执行。

如果定时任务是在非root账号下执行,在运行过程中可能会报如下错误:

error: error creating state file /var/lib/logrotate/status: Permission denied

这是由于logrotate默认会将执行记录写入到/var/lib/logrotate/status文件中,而非root账号不具有该文件的写权限。

解决该问题,可以通过在执行logrotate命令时指定-s参数,重新指定一个具有权限的文件即可:

/usr/sbin/logrotate -s /home/work/local/logrotate.status

使用crontab + logrotate是linux下非常理想的日志定时管理方式。略有遗憾的是,logrotate不支持小时粒度的轮滚。当然,我们是可以设置定时任务每小时执行一次,但是文件后缀不支持设置小时,最多只能设置到天级别。如果要使用logrotate做日志小时粒度的分割并以小时作为日志文件后缀,还需要结合一些shell脚本来实现(主要逻辑是对分割后的文件做一次重命名)。

参考资料:

本文已迁移至我的博客:http://ipenge.com/33504.html

推荐阅读更多精彩内容