OpenResty实现简单的记录操作laravel请求/响应日志

首先确保安装好了OpenResty:
介绍: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 等都进行一致的高性能响应。

我的nginx配置大致如下

    server
{
    listen 80;
    server_name xxxxx.com;
    index index.php index.html index.htm default.php default.htm default.html;

    #请求日志
    lua_need_request_body   on;
    log_by_lua_file '/www/wwwlogs/logs/config/log.lua';
    set $response_body      "";
    body_filter_by_lua_block     {
        local response_body = string.sub(ngx.arg[1],1,10000)
        ngx.ctx.buffered =  (ngx.ctx.buffered or "")   .. response_body  
        if ngx.arg[2] then
            ngx.var.response_body = ngx.ctx.buffered
        end
    }
    
    # 路径不存在api 则走dist目录
    if ( $request_uri !~* /api ) {
        set $root_path /www/wwwroot/larvael/web/dist;
    }

    # 否则走后端入口
    if ( $request_uri ~* /(api|storage|\.well-known) ) {
        set $root_path  /www/wwwroot/laravel/public;
    }
    
    root $root_path;
    
    location / {
            try_files $uri $uri @router;
            index index.html;
            add_header Cache-Control "no-cache, no-store";
    }
        
        location @router {
            rewrite ^.*$ /index.html break;
        }
    
    location /api/
    {
        index index.php;
        if (!-e $request_filename) {
            rewrite ^/(.*)$ /index.php?s=$1 last;
            break;
        }
    }
    # =============================================

    #SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
    #error_page 404/404.html;
    #SSL-END

    #ERROR-PAGE-START  错误页配置,可以注释、删除或修改
    #error_page 404 /404.html;
    #error_page 502 /502.html;
    #ERROR-PAGE-END

    #PHP-INFO-START  PHP引用配置,可以注释或修改
    include enable-php-80.conf;
    #PHP-INFO-END

    #禁止访问的文件或目录
    location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md)
    {
        return 404;
    }

    #一键申请SSL证书验证目录相关设置
    location ~ \.well-known{
        allow all;
    }

    #禁止在证书验证目录放入敏感文件
    if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
        return 403;
    }

    location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
    {
        expires      30d;
        error_log /dev/null;
        access_log /dev/null;
    }

    location ~ .*\.(js|css)?$
    {
        expires      12h;
        error_log /dev/null;
        access_log /dev/null;
    }
    access_log  /www/wwwlogs/xxxxx.com.log;
    error_log  /www/wwwlogs/xxxxx.com.error.log;
}

nginx配置中关于lua的配置如下

#请求日志
    lua_need_request_body   on;
    log_by_lua_file '/www/wwwlogs/logs/config/log.lua';
    set $response_body      "";
    body_filter_by_lua_block     {
        local response_body = string.sub(ngx.arg[1],1,10000)#截断,防止过长
        ngx.ctx.buffered =  (ngx.ctx.buffered or "")   .. response_body  
        if ngx.arg[2] then
            ngx.var.response_body = ngx.ctx.buffered
        end
    }

我在/www/wwwlogs/logs/config下放了两个lua文件用于写记录日志的逻辑,当然你也可以用一个,我只是之前设计的时候考虑到服务器可能有多个网站所以抽了一个出来打算复用,后来好像没啥用

--这个文件是/www/wwwlogs/logs/config/log.lua
--用户IP
local headers = ngx.req.get_headers()
local request_id = headers["request_id"] or headers["REQUEST_ID"] or "xxxxxxxxxx"

if request_id == "xxxxxxxxxx" and ngx.var.request_method == "GET" then
  return;
end

if ngx.var.request_method == "OPTIONS" then
  return;
end

local host = ngx.var.host;
local file_path_sub = host;
if host == "demo.com" then
    file_path_sub = "demo";
end
if host == "xxxxx.com" then
    file_path_sub = "xxxxx";
end
if host == "abcdefg.com" then
    file_path_sub = "随便写,就是生产的文件夹的名称";
end


package.path = package.path .. ";/www/wwwlogs/logs/config/?.lua"
require("common")
local file_args = {};
local args = {};
local url = '';
args, file_args, url = ModuleT.init_form_args()


-- 记录请求日志

-- 引⼊lua所有解析json的库
local cjson = require "cjson"


--加入判断文件夹是否存在
local file_path = "/www/wwwlogs/logs/".. file_path_sub;
local fdr = io.open(file_path,"rb")
if fdr then fdr:close() end 
if fdr == nil then
    os.execute("mkdir "..file_path .. " -p")
end


-- 使⽤lua的io打开⼀个⽂件,如果⽂件不存在,就创建,a为append模式
local file = io.open(file_path.. "/" ..file_path_sub .. os.date("%Y-%m-%d",unixtime)  .. ".log", "a")


--消息内容

--时间
--serverTimeTable是把服务器时间转换成当地时间格式的、形如{year = 2022, month = 9, day = 1, hour = 9, min = 17, sec = 50}的表
local now = os.time()
local timeZone = os.difftime(now, os.time(os.date("!*t", now))) / 3600  -- 获取系统时区
local deltaTimeZone = 8 - timeZone
local chinaTime = os.time(serverTimeTable) + deltaTimeZone * 3600 - (isDst and 1 or 0) * 3600
local dateTime = '[' .. os.date("%Y-%m-%d %H:%M:%S", chinaTime) .. ']'
-- --IP
local ip = headers["X-REAL-IP"] or headers["X_FORWARDED_FOR"] or ngx.var.remote_addr or "0.0.0.0"
-- -- 请求方式
local request_method = ngx.var.request_method


local request_params = {};
for key, value in pairs(args) do
    request_params[tostring(key)] = tostring(value);
end
for key, value in pairs(file_args) do
    request_params[tostring(key)] = tostring(value);
end



local response_body = ModuleT.unicode_to_utf8(ngx.var.response_body)

---------------------------

local logString = file_path.. dateTime .. ' ' .. ip .. ' ' .. request_method .. ' ' .. url .. ' \n';
local logString = logString .. 'request-id: ' .. request_id .. ' \n';
local logString = logString .. 'Request: ' .. cjson.encode(request_params) .. ' \n';
local logString = logString .. 'Response: ' ..  response_body .. ' \n';

-- ngx.say(logString);

-- 将json写⼊到指定的log⽂件,末尾追加换⾏
file:write(logString, "\n")
-- 将数据写⼊
file:flush()

--这个文件是/www/wwwlogs/logs/config/common.lua
ModuleT = {}
-- 字符串处理
local function explode(_str, seperator)
    local pos, arr = 0, {}
    for st, sp in
    function()
        return string.find(_str, seperator, pos, true)
    end
    do
        table.insert(arr, string.sub(_str, pos, st - 1));
        pos = sp + 1;
    end
    table.insert(arr, string.sub(_str, pos));
    return arr;
end


-- 格式化参数
function ModuleT.init_form_args()
    local args = {};
    local file_args = {};
    local uri = '';
    local is_have_file_param = false;

    local cjson = require "cjson";
    local headers = ngx.req.get_headers();
    local method = ngx.var.request_method;
    local request_uri_args = ngx.req.get_uri_args();

    -- 获取url
    for k, v in pairs(request_uri_args) do
        if k == 's' then
            uri = v
        end
    end

    if "GET" == method or "DELETE" == method then
        for k, v in pairs(request_uri_args) do
            if k ~= 's' then
                args[k] = v
            end
        end
    elseif "POST" == method or "PUT" == method then
        -- ngx.req.read_body();
        --获取参数传递类型
        local req_type = '';
        local i, j = string.find(headers["content-type"], "application/x-www-form-urlencoded")
        if i ~= nil and j ~= nil then
            --判断是否是application/x-www-form-urlencoded类型
            req_type = 'x-www-form-urlencoded';
        end
        local i, j = string.find(headers["content-type"], "application/json")
        if i ~= nil and j ~= nil then
            --判断是否是application/json类型
            req_type = 'json';
        end
        local i, j = string.find(headers["content-type"], "multipart/form-data")
        if i ~= nil and j ~= nil then
            --判断是否是form-data类型
            req_type = 'form-data';
        end
        --根据不同的参数传递类型进行逻辑处理
        if req_type == 'x-www-form-urlencoded' then
            args = ngx.req.get_post_args();
        elseif req_type == 'json' then
            local json = ngx.req.get_body_data();
            if json ~= nil then
                args = cjson.decode(json);
            end
        elseif req_type == 'form-data' then
            --判断是否是multipart/form-data类型的表单
            is_have_file_param = true;
            content_type = headers["content-type"];
            body_data = ngx.req.get_body_data();
            --body_data可是符合http协议的请求体,不是普通的字符串
            --请求体的size大于nginx配置里的client_body_buffer_size,则会导致请求体被缓冲到磁盘临时文件里,client_body_buffer_size默认是8k或者16k
            if not body_data then
                local datafile = ngx.req.get_body_file()
                if not datafile then
                    error_code = 1
                    error_msg = "no request body found"
                else
                    local fh, err = io.open(datafile, "r")
                    if not fh then
                        error_code = 2
                        error_msg = "failed to open " .. tostring(datafile) .. "for reading: " .. tostring(err)
                    else
                        fh:seek("set")
                        body_data = fh:read("*a")
                        fh:close()
                        if body_data == "" then
                            error_code = 3
                            error_msg = "request body is empty"
                        end
                    end
                end
            end
            local new_body_data = {}
            --确保取到请求体的数据
            if not error_code then
                local boundary = "--" .. string.sub(headers["content-type"], 31); -- 分割符
                --                ngx.say(boundary);
                local body_data_table = explode(tostring(body_data), boundary);   -- 按分隔符分割
                --                ngx.say(body_data_table);
                local first_string = table.remove(body_data_table, 1);
                local last_string = table.remove(body_data_table);
                for _, v in ipairs(body_data_table) do
                    local start_pos, end_pos, capture, capture2 = string.find(v, 'Content%-Disposition: form%-data; name="(.+)"; filename="(.*)"')
                    if not start_pos then
                        --普通参数
                        local t = explode(v, "\r\n\r\n");
                        local temp_param_name = string.sub(t[1], 41, -2);
                        local temp_param_value = string.sub(t[2], 1, -3);
                        args[temp_param_name] = temp_param_value;
                    else
                        --文件类型的参数,capture是参数名称,capture2是文件名
                        file_args[capture] = capture2;
                        table.insert(new_body_data, v);
                    end
                end
                table.insert(new_body_data, 1, first_string);
                table.insert(new_body_data, last_string);
                --去掉app_key,app_secret等几个参数,把业务级别的参数传给内部的API
                body_data = table.concat(new_body_data, boundary);
                --body_data可是符合http协议的请求体,不是普通的字符串
            end
        else
            args = ngx.req.get_post_args();
        end
    end
    return args, file_args, uri;
end


--unicode转汉字
function ModuleT.unicode_to_utf8(convertStr)

    if type(convertStr)~="string" then
        return convertStr
    end

    local bit = require("bit")
    local resultStr=""
    local i=1
    while true do
        
        local num1=string.byte(convertStr,i)
        local unicode
        
        if num1~=nil and string.sub(convertStr,i,i+1)=="\\u" then
            unicode=tonumber("0x"..string.sub(convertStr,i+2,i+5))
            i=i+6
        elseif num1~=nil then
            unicode=num1
            i=i+1
        else
            break
        end

        if unicode <= 0x007f then
            resultStr=resultStr..string.char(bit.band(unicode,0x7f))
        elseif unicode >= 0x0080 and unicode <= 0x07ff then
            resultStr=resultStr..string.char(bit.bor(0xc0,bit.band(bit.rshift(unicode,6),0x1f)))
            resultStr=resultStr..string.char(bit.bor(0x80,bit.band(unicode,0x3f)))
        elseif unicode >= 0x0800 and unicode <= 0xffff then
            resultStr=resultStr..string.char(bit.bor(0xe0,bit.band(bit.rshift(unicode,12),0x0f)))
            resultStr=resultStr..string.char(bit.bor(0x80,bit.band(bit.rshift(unicode,6),0x3f)))
            resultStr=resultStr..string.char(bit.bor(0x80,bit.band(unicode,0x3f)))
        end
    end
    resultStr=resultStr..'\0'
    return resultStr
end


return ModuleT

也可以自行添加记录的内容和封装,本文件仅支持laravel,其他框架得自己调试一下

可以选择给nginx配置中的

 log_by_lua_file '/www/wwwlogs/logs/config/log.lua';

改成

 content_by_lua_file '/www/wwwlogs/logs/config/log.lua';

来调试输出内容,或者查看nginx配置中的

 /www/wwwlogs/xxxxx.com.error.log;

来分析报错行数

补充一下,laravel中好像也仅支持GET/DELETE和POST/PUT中的Raw类型的参数记录,其他的没试过,主要是前端的请求方式没有很多,可以写但是没必要

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容