一、OpenResty与Lua的日志系统概述

在Web服务运行过程中,日志就像系统的"黑匣子",记录着每一次访问和每一个错误。OpenResty作为基于Nginx的增强平台,配合Lua脚本能力,可以让我们对日志进行高度定制化处理。不同于传统的Nginx日志,我们可以通过Lua实现更灵活的日志记录方式,比如按条件过滤、格式化输出,甚至实时分析。

传统Nginx的日志配置是静态的,修改后需要重启服务。而OpenResty的Lua模块让我们能够动态调整日志行为,这在生产环境中特别有价值。想象一下,当你的服务突然出现异常,你不需要停服就能立即开启详细调试日志,这种灵活性对运维人员来说简直是福音。

二、自定义访问日志实战

让我们从一个实际例子开始,看看如何用Lua定制访问日志。假设我们需要记录每个请求的处理时间、后端服务器IP以及自定义的业务ID。

-- OpenResty Lua示例:自定义访问日志
server {
    listen 8080;
    
    location / {
        access_by_lua_block {
            -- 记录请求开始时间
            ngx.ctx.start_time = ngx.now()
            
            -- 生成唯一请求ID
            ngx.ctx.request_id = ngx.var.request_id or 
                                ngx.md5(ngx.var.remote_addr .. ngx.now())
        }
        
        log_by_lua_block {
            -- 计算请求处理耗时(毫秒)
            local elapsed = (ngx.now() - ngx.ctx.start_time) * 1000
            
            -- 自定义日志格式
            local log_msg = string.format(
                '[%s] %s "%s" status=%s time=%.2fms request_id=%s upstream=%s',
                ngx.var.time_iso8601,
                ngx.var.remote_addr,
                ngx.var.request,
                ngx.var.status,
                elapsed,
                ngx.ctx.request_id,
                ngx.var.upstream_addr or "-"
            )
            
            -- 写入自定义访问日志
            local custom_log = io.open("/var/log/nginx/custom_access.log", "a")
            if custom_log then
                custom_log:write(log_msg, "\n")
                custom_log:close()
            end
        }
        
        proxy_pass http://backend;
    }
}

这个示例展示了几个关键点:

  1. 使用access_by_lua_block在请求处理前记录时间戳
  2. log_by_lua_block阶段计算并记录完整日志
  3. 包含了业务关心的自定义字段(request_id)
  4. 记录了上游服务器信息,便于排查负载均衡问题

三、错误日志的收集与分析

错误日志比访问日志更重要,因为它直接反映了系统的健康状况。OpenResty提供了多层次的错误记录机制:

-- OpenResty Lua示例:多级错误日志处理
location /api {
    content_by_lua_block {
        local ok, err = pcall(function()
            -- 业务逻辑代码
            if ngx.var.arg_debug == "1" then
                error("debug mode enabled")
            end
            
            -- 模拟数据库操作
            local db = require "resty.mysql"
            local db_conn, err = db:new()
            if not db_conn then
                ngx.log(ngx.ERR, "failed to create DB connection: ", err)
                return ngx.exit(500)
            end
        end)
        
        if not ok then
            -- 记录完整错误堆栈
            ngx.log(ngx.ERR, "API handler failed: ", err)
            
            -- 同时写入错误统计
            local stats = ngx.shared.error_stats
            local key = "api_error_" .. ngx.var.host
            stats:incr(key, 1)
            
            return ngx.exit(500)
        end
    }
}

这个错误处理方案有几个亮点:

  1. 使用pcall捕获Lua运行时错误
  2. 不同级别日志(ngx.ERR用于关键错误)
  3. 使用共享内存(ngx.shared)进行错误统计
  4. 包含了上下文信息(hostname)

对于错误日志的分析,我们可以结合logrotate和ELK栈:

# 日志轮转配置示例
/var/log/nginx/error.log {
    daily
    missingok
    rotate 30
    compress
    delaycompress
    notifempty
    sharedscripts
    postrotate
        /bin/kill -USR1 `cat /var/run/nginx.pid 2>/dev/null` 2>/dev/null || true
    endscript
}

四、高级日志处理技巧

当系统规模扩大后,简单的文件日志会遇到性能瓶颈。这时我们可以考虑以下优化方案:

  1. 缓冲写入:减少磁盘IO
-- 缓冲日志示例
local buffer = {}
local buffer_size = 100  -- 每100条刷新一次

local function flush_log()
    if #buffer == 0 then return end
    
    local log_file = io.open("/var/log/nginx/buffered.log", "a")
    if log_file then
        log_file:write(table.concat(buffer, "\n"), "\n")
        log_file:close()
        buffer = {}
    end
end

local function log(msg)
    table.insert(buffer, msg)
    if #buffer >= buffer_size then
        flush_log()
    end
end

-- 定时刷新剩余日志
local delay = 5  -- 5秒
local handler
handler = function()
    flush_log()
    ngx.timer.at(delay, handler)
end
ngx.timer.at(delay, handler)
  1. 结构化日志:便于后续分析
-- JSON格式日志示例
local cjson = require "cjson"

local log_data = {
    timestamp = ngx.now(),
    host = ngx.var.host,
    uri = ngx.var.request_uri,
    status = ngx.status,
    upstream = ngx.var.upstream_addr,
    request_time = ngx.var.request_time,
    http_referer = ngx.var.http_referer or "",
    http_user_agent = ngx.var.http_user_agent or "",
    remote_ip = ngx.var.remote_addr
}

local log_file = io.open("/var/log/nginx/structured.log", "a")
if log_file then
    log_file:write(cjson.encode(log_data), "\n")
    log_file:close()
end
  1. 动态日志级别:根据条件调整日志详细程度
-- 动态日志级别控制
local _M = {}

function _M.debug(...)
    if ngx.var.debug_mode == "1" then
        ngx.log(ngx.DEBUG, ...)
    end
end

function _M.info(...)
    ngx.log(ngx.INFO, ...)
end

function _M.error(...)
    ngx.log(ngx.ERR, ...)
    -- 错误时自动触发告警
    ngx.timer.at(0, function()
        send_alert(...)
    end)
end

return _M

五、应用场景与技术选型

在实际项目中,日志系统的设计需要根据业务特点来定制。以下是几种典型场景:

  1. 高并发API服务
  • 使用缓冲日志减少IO压力
  • 记录关键性能指标(request_time, upstream_time)
  • 采样记录完整请求/响应体
  1. 电商促销活动
  • 重点监控支付相关接口错误
  • 实时统计各接口成功率
  • 动态调整日志级别应对突发流量
  1. 微服务架构
  • 统一request_id贯穿所有服务
  • 集中式错误日志收集
  • 服务拓扑关联分析

技术选型上,OpenResty+Lua的组合有以下优势:

  • 极高性能:基于Nginx事件驱动模型
  • 灵活扩展:随时添加自定义日志字段
  • 实时处理:可在日志记录阶段进行分析

但也要注意一些限制:

  • Lua的字符串处理性能一般,复杂日志格式要考虑性能影响
  • 共享内存的使用要注意竞争条件
  • 错误处理不当可能导致请求阻塞

六、注意事项与最佳实践

在实施过程中,我总结了一些经验教训:

  1. 日志轮转
  • 使用logrotate时注意文件权限
  • 大型日志文件压缩可以考虑pigz并行压缩
  • 保留足够的磁盘空间
  1. 敏感信息
-- 敏感信息过滤示例
local function filter_sensitive(data)
    -- 过滤密码字段
    data = data:gsub("password=[^&]*", "password=***")
    -- 过滤信用卡号
    data = data:gsub("card_number=%d(%d%d%d)%d%d%d%d", "card_number=****%1")
    return data
end

ngx.log(ngx.INFO, filter_sensitive(ngx.var.request_body))
  1. 性能考量
  • 避免在热路径中进行复杂日志处理
  • 磁盘IO是主要瓶颈,考虑内存缓冲
  • 大量日志写入时监控iowait指标
  1. 监控告警
  • 设置错误率阈值告警
  • 监控日志文件增长速率
  • 关键错误设置即时通知

七、总结

完善的日志系统是服务可观测性的基石。通过OpenResty和Lua的组合,我们可以构建出既灵活又高效的日志解决方案。从基础的自定义格式,到高级的错误分析和实时统计,这套技术栈能够满足各种复杂场景的需求。

在实践中,建议采用渐进式策略:

  1. 先实现基本日志功能
  2. 逐步添加业务关键指标
  3. 最后完善监控告警体系

记住,好的日志系统不是一蹴而就的,而是随着业务发展不断演进的。每次故障排查都是改进日志系统的机会,只有持续优化,才能打造出真正可靠的运维基础设施。