1. 程式人生 > >【OpenResty】lua指令碼實現nginx自定義log

【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

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,如何檢視某些指令的使用語法