【OpenResty】lua脚本实现nginx自定义log

1,OpenResty(Nginx)

Nginx (engine x) 是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP服务器。Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的Rambler.ru站点(俄文:Рамблер)开发的。 Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,并在一个BSD-like协议下发行。其特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:百度、京东、新浪、网易、腾讯、淘宝等。国外的网站用户有:Dropbox, Netflix, Wordpress.com, FastMail.FM。

OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

OpenResty® 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。

OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis等都进行一致的高性能响应。

也就是说其实OpenResty对nginx进行了自己的第三方库开发,原有的nginx功能均可使用,并自定义了一些优秀的lua库,方便开发者调用。

2,Lua

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。 Lua 是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组,由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo所组成并于1993年开发。

Lua 应用场景
游戏开发
独立应用脚本
Web 应用脚本
扩展和数据库插件如:MySQL Proxy 和 MySQL WorkBench
安全系统,如入侵检测系统

3,两者之间的关系

OpenResty是基于nginx开发的服务器,OpenResty里面的很多组件都是都是基于lua开发的,lua作为一门动态解释型语言,可以快速开发并支持热更新,Lua和nginx结合,可以完成高效得http请求的处理。OpenResty集合了一个ngx_lua模块,该模块的地址就是:https://github.com/openresty/lua-nginx-module#installation 上面指明了:

It is highly recommended to use OpenResty releases which integrate Nginx, ngx_lua, LuaJIT 2.1, as well as other powerful companion Nginx modules and Lua libraries. It is discouraged to build this module with nginx yourself since it is tricky to set up exactly right. Also, the stock nginx cores have various limitations and long standing bugs that can make some of this modules’ features become disabled, not work properly, or run slower.

也就是说OpenResty已经集成了ngx_lua模块,推荐使用OpenResty,但是你也可以使用nginx原版+ngx_lua模块自己使用。

这样,我们就可以自己编写lua脚本,实现自定义的功能。

4,如何调用,怎么调试

可以利用日志输出打印实现调试功能,也就是在你的lua脚本里使用ngx.log()函数来打印日志。当然也可以利用IDE实现调试功能,可以在IDE里面打断点调试。后一种方法需要实验,请参考这篇文章:Debugging OpenResty and Nginx Lua scripts with ZeroBrane Studio

5,本实例demo

本篇博文主要讲解一下怎么使用lua脚本实现自定义log。实际的逻辑就是:OpenResty的conf目录下,有一个nginx.conf文件,这个里面可以通过配置一个lua脚本,nginx调用这个脚本,这个脚本实现具体逻辑,然后就可以生成log。

实例说明:

本实例在本地实验,操作系统为windows,与网上常用的linux不太一样

在 localhost/test 路径下实现上面所说的处理逻辑,有以下要求:

  • localhost/test 是一个api接口路径,请求时需要三个参数,分别为a,b,c。其中,a参数是必须的,b和c参数可传可不传
  • 日志按小时级进行落地
  • 一次请求为一行日志,一行日志按顺序包括以下字段:time, ip, ua, a, b, c 。一行日志总共有5个字段,每个字段之间用 \x02 字符分割。

5.1 下载OpenResty

下载地址:OpenResty下载地址

5.2配置nginx.conf文件

nginx.conf文件内容如下:

只是在下载的OpenResty的原文件里面加了如下几行:

在http下:

        init_by_lua_file /lua_test/init.lua;
        lua_shared_dict example_test_dict 1M;

在server下:

location = /test {
                    content_by_lua_file /lua_test/content_test.lua;
                }

#user  nobody;
worker_processes  1;

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

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


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

    #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  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;
        init_by_lua_file /lua_test/init.lua;
        lua_shared_dict example_test_dict 1M;
    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }


                location = /test {
                    content_by_lua_file /lua_test/content_test.lua;
                }

        #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;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}

5.3 content_tes.lua

--- 设置返回的html的header
ngx.header.content_type = 'text/html; charset=utf-8';
local req = ngx.req.get_uri_args()
--- 获取a参数,并判断是否有值,若没有返回701的status,表示请求不成功
local a  = req["a"]
if a == nil then
    ngx.exit(701)
end
--- 获取time,ip,ua,b,c参数
local req_headers = ngx.req.get_headers()
local b = req["b"]
local c = req["c"]
local curTime = ngx.localtime()
local ip = ngx.var.remote_addr
local ua = req_headers["User-Agent"]
--- 若请求中未带b或c参数,置其为空字符串
if nil == b then
    b = ""
end
if nil == c then
    c = ""
end
--- 拼接一行日志中的内容
local msg = curTime .. "\02" .. ip .. "\x02" .. ua .. "\x02" .. a .. "\02" .. b .. "\02" .. c .. "\n"
--- 获取当前时间周期,用于判断是否需要进行文件句柄的滚动  
local cur_hour_level = string.sub(ngx.localtime(), 1, 13)
--- 当大于时,表示到了需要滚动日志文件句柄的时候
if cur_hour_level > example_test_log_hour_level then
    --- 在共享字典中,用add命令实现类似于锁的用途。
    --- 只有当共享字典中原来没有要add的key时,才能操作成功,否则失败。
    --- 这样的话,有多个请求时,只能有一个请求add成功,而其他请求失败,休眠0.01秒后重试。
    --- 唯一add成功的那个请求,则关闭老文件句柄,并滚动新文件句柄,并更新表示文件句柄的时间周期的那个全局变量。
    local shared_dict = ngx.shared.example_test_dict
    local rotate_key = "log_rotate" 
    while true do
        if cur_hour_level == example_test_log_hour_level then
            break
        else
            -- the exptime, is to prevent dead-locking 
            local ok, err = shared_dict:add(rotate_key, 1, 10) 
            if not ok then 
                ngx.sleep(0.01)
            else
                if cur_hour_level > example_test_log_hour_level then
                    example_test_log_closeFile()
                    example_test_log_openFile()
                    if example_test_log_fo == nil then
                        ngx.log(ngx.ERR, "example_test_log_openFile error")
                        ngx.exit(911)
                    end
                    example_test_log_hour_level_update(cur_hour_level)
                end
                shared_dict:delete(rotate_key)
                break
            end
        end
    end
end
--- 落地日志内容
example_test_log_fo:write(msg)
example_test_log_fo:flush()
--- 正常退出,返回请求,表示成功
ngx.exit(ngx.HTTP_OK)

5.4 init.lua

---- example.com init
--- 方便更改日志落地的基本目录
local EXAMPLE_TEST_LOG_DIR_BASE = "path\\to\\log\\dir"
--- 日志文件中名称的前面部分,方便识别
local EXAMPLE_TEST_FILENAME_PRE = "example"
--- 日志所属的当前周期的全局变量,小时级
example_test_log_hour_level = string.sub(ngx.localtime(), 1, 13)
--- 更新日志所属的当前周期的全局变量的函数
--- 之所以用函数更新,因为全局变量在跨文件时是无法更新的,下面的函数也是同理
function example_test_log_hour_level_update(hour_level)
    example_test_log_hour_level = hour_level
end
--- 日志落地的文件句柄,全局变量
example_test_log_fo = nil
--- 更新日志落地文件句柄的全局变量的函数
function example_test_log_openFile()
    local curT = ngx.time()     --unix timestamp, in seconds
    local dir_path = EXAMPLE_TEST_LOG_DIR_BASE .. "\\" .. os.date("%Y%m\\%d\\%H", curT)
    local exec_code = os.execute("mkdir " .. dir_path)
    if nil ~= exec_code then
        ngx.log(ngx.ERR, "can't mkdir " .. dir_path)
        return nil
    end
    local file_path = dir_path .. "\\" .. EXAMPLE_TEST_FILENAME_PRE .. os.date("_%Y%m%d%H.log", curT)

    local err_msg, err_code
    example_test_log_fo, err_msg, err_code = io.open(file_path, "a")
    if nil == example_test_log_fo then
        ngx.log(ngx.ERR, "can't open file: " .. file_path .. ", " .. err_msg .. ", " ..  err_code)
        return nil
    else
        return example_test_log_fo
    end
end
--- 关闭日志句柄的函数
function example_test_log_closeFile()
    if example_test_log_fo ~= nil then
        example_test_log_fo:close()
        ngx.log(mgx.ERR,example_test_log_fo)
    end
end
--- 在init时调用一次,初始化文件句柄
example_test_log_openFile()

5.5 测试脚本是否有效生成log

(1)开启OpenResty服务器
(2)在浏览器上输入localhost/test网址
(3)查看在OpenResty文件目录下是否生成了一个path/to/log/dir/yyyyMM/dd/HH的文件目录,目录下会有一个以example_开头的log日志

注意:

1,如何调试

使用ngx.log()函数调试,这样就会在OpenResty得logs目录下相应文件里输出日志。(或者使用IDE)

2,如何测试lua脚本

OpenResty下继承了luaJIT,下载得目录里有一个luajit.exe得执行程序,可以在里面输入代码进行测试

3,windwos和linux下的区别

在实验的时候,很多网上的例子都是linux环境下的,因此需要注意甄别,例如windows环境下的文件分割符和linux下的就不一样,另外linux里面的调用操作系统的一些API(例如文件删除,打开,创建文件/文件目录等等)都不一样。本文的例子就是根据网上的一个linux环境下的例子改写的,因此读者可以对比 nginx + lua实现逻辑处理与日志周期性落地 这篇文章,看看有什么区别。

4,如何查看某些指令的使用语法

具体用法详见:openresty/lua-nginx-module

你可能感兴趣的:(Nginx,OpenResty,Lua)