如何对nginx的日志进行按天切割

这篇博文分享的技巧非常不错,我转载一下他的博文:https://jingsam.github.io/2019/01/15/nginx-access-log.html

nginx日志分割是很常见的运维工作,关于这方面的文章也很多,通常无外乎两种做法:一是采用cron定期执行shell脚本对日志文件进行归档;二是使用专门日志归档工作logrotate。

第一种写shell脚本的方法用得不多,毕竟太原始。相比之下,使用logrotate则要省心得多,配置logrotate很简单。关于如何配置logrotate不是本文要讲的内容,感兴趣的话可以自行搜索。

虽然大多数Linux发行版都自带了logrotate,但在有些情况下不见得安装了logrotate,比如nginx的docker镜像、较老版本的Linux发行版。虽然我们可以使用包管理器安装logrotate,但前提是服务器能够访问互联网,企业内部的服务器可不一定能够联网。

其实我们有更简单的方法,从nginx 0.7.6版本开始access_log的路径配置可以包含变量,我们可以利用这个特性来实现日志分割。例如,我们想按天来分割日志,那么我们可以这样配置:

access_log logs/access-$logdate.log main;

那么接下来的问题是我们怎么提取出$logdate这个变量?网上有建议使用下面的方法:

if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})") {
  set $year $1;
  set $month $2;
  set $day $3;
}

access_log logs/access-$year-$month-$day.log main;

上面的方法有两个问题:一是如果if条件不成立,那么$year、$month和$month这三个变量将不会被设置,那么日志将会记录到access-$year-$month-$day.log这个文件中;二是if只能出现在server和location块中,而access_log通常会配置到顶层的http块中,这时候if就不适用。

如果要在http块中设置access_log,更好的方法是使用map指令:

map $time_iso8601 $logdate {
  '~^(?<ymd>\d{4}-\d{2}-\d{2})' $ymd;
  default                       'date-not-found';
}

access_log logs/access-$logdate.log main;

map指令通过设置默认值,保证$logdate始终有值,并且可以出现在http块中,完美地解决了if指令的问题。

补充上面的博文一点,因为一般nginx的user是nobody,所以上面写access_log logs/access-$logdate.log main;有可能报错,需要将那个目录所有者授予nobody这个用户。

通过.jks生成nginx所需要的.crt和.key证书

有时候拿到一个证书压缩包,里面包含着各种格式的证书,有.jks的,就是没有.crt和.key证书。

所以,需要将.jks的证书转换成nginx所需要的.crt和.key证书。

比如有一个abc.com.jks

  1. 查看.jks文件中有哪些内容
    keytool -list -keystore abc.com.jks

  2. 将”.jks”转为”.pkcs12”(PKCS12格式的证书库)
    keytool -importkeystore -srckeystore abc.com.jks -destkeystore abc.com.pkcs12 -deststoretype PKCS12

  3. 提取2张证书
    openssl pkcs12 -in abc.com.pkcs12 -nokeys -clcerts -out abc.com-ssl.crt
    openssl pkcs12 -in abc.com.pkcs12 -nokeys -cacerts -out abc.com-ca.crt

abc.com-ssl.crt是SSL证书,abc.com-ca.crt是中级证书,俩个合并到一起才是nginx服务器所需要的证书,这个非常重要。假如仅用一个,尽管在浏览器中可以使用,但是在程序中调用的时候,假如用了validate ssl功能的话,就会报错。

  1. 合并证书
    cat abc.com-ssl.crt abc.com-ca.crt > abc.com.crt
    此时abc.com.crt是一个完成的证书

  2. 提取私钥
    openssl pkcs12 -nocerts -nodes -in abc.com.pkcs12 -out abc.com.key

至此,.crt和.key证书转换完成。

nginx中proxy_set_header的一个坑

一个应用有多个域名同时指向,所以在程序中需要通过servername来获得访问的域名来进行不同的处理。
在测试环境没有任何问题,但是到了线上就出问题了,上网查了相关资料,解决的方法是在 location / 的设置中,增加 proxy_set_header Host $host即可。可是在我们的线上nginx中的http设置中已经设置了proxy_set_header Host $host,对于nginx的配置来说 http > server > location,所以按理说应该有这个设置了,应该有效的。
anyway,线上的问题总需要解决,于是尝试着在 location 中又增加了一遍 proxy_set_header Host $host,启动后验证,居然生效了!!
这个到底是怎么回事啊?于是到官网查文档,看到下面一段话:

Allows redefining or appending fields to the request header passed to the proxied server. 
The value can contain text, variables, and their combinations. 
These directives are inherited from the previous configuration level if and only if there are no proxy_set_header directives defined on the current level. 

注意到上面有一句话:”These directives are inherited from the previous configuration level if and only if there are no proxy_set_header directives defined on the current level.” 也就是说确实是按照 http > server > location 的顺序来传递关系,但是假如当前的级别中出现了任意一个proxy_set_header,那么在 http 或者 server 中定义的 proxy_set_header 都将失效,需要重新再定义一遍。

oh,my god,还有这么大的一个坑。

BTW:在nginx的关于proxy_set_header文档中,还说明了:
proxy_set_header Host $http_host;
proxy_set_header Host $host;
这两者的区别。
$http_host只认用户的HEADER中的HOST,假如没有那就是为空;而$host是首先认用户的HEADER中的HOST,假如为空,就用 nginx 中的 server_name 设置。所以,$host要比 $http_host 好。

error_page的正确打开方式

我们往往用nginx来总控所有upstream出现的404,500等错误页面,有以下几种方法来定义error_page,比如:

upstream someservice {      
        server 192.168.122.37:80;
}
server {
    error_page 404             /404.html;
    error_page 500 502 503 504 /50x.html;
    #error_page 404 http://www.xxx.com/notfound/page404.html;

    location / {
        proxy_pass    http://someservice;
        proxy_set_header        Host                                    $host;
        proxy_set_header        X-Forwarded-For                 $proxy_add_x_forwarded_for;
        proxy_connect_timeout   300s;
        proxy_send_timeout      300s;
        proxy_read_timeout      300s;
    }
}

但是发现在someservice中报出的404,500等错误却没有转到特定错误页面。

上述定义的error_page,只有在本nginx自己负责的本地页面报错的时候,才会触发,对于someservice的错误无动于衷。

原来是要设定一个参数:
server {
proxy_intercept_errors on;
}
这个proxy_intercept_errors参数,从字面意义上也能明白,拦截代理错误,所以明白了吧。

log_format和access_log的正确打开方式

nginx默认的log_format和access_log如下:

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
access_log  logs/access.log;

上面的日志格式存在以下问题:
1,因为由于CDN和还有其他的nginx在请求的前面,上面的日志格式不能反映真实的用户IP。
2,缺少访问时长。
所以,我会把上面的日志格式log_format改写成:

    map $proxy_add_x_forwarded_for  $clientRealIp {
        ""  $remote_addr;
        ~^(?P<firstAddr>[0-9\.]+),?.*$      $firstAddr;
    }

    log_format  main  '$remote_addr:$remote_port - $remote_user [$time_local] "$request" '
                  '$status $body_bytes_sent "$http_referer" '
                  '"$http_user_agent" "$clientRealIp" "$proxy_add_x_forwarded_for" $request_time';

可是怎么都不生效,于是我查看了一下nginx的文档,其中对 log_format 的定义如下:

Syntax: log_format name [escape=default|json|none] string ...;
Default:    log_format combined "...";
Context:    http

对access_log的定义如下:

Syntax: access_log path [format [buffer=size] [gzip[=level]] [flush=time] [if=condition]];
        access_log off;
Default:    access_log logs/access.log combined;
Context:    http, server, location, if in location, limit_except

在上面的access_log定义中的format,究竟是个什么鬼?原来这个format对应的就是上面log_format中的name,也就是说我们对我们自己定义的log_format取一个name(可以随便,默认是combined),然后在access_log中一定要指定这个name,否则access_log就是用了默认的combined的log_format,这样也就是你无论如何改了都没有效。

所以,对于一开始定义的那个log_format,在access_log中,应该如下定义:

access_log  logs/access.log main;

这个main也就是上面的log_format定义的name。