lolly/internal/lua/api_log.go
xfy e733273139 fix(server,app,proxy,resolver,middleware,lua): add nil guards and safe defaults
- server: reject Start() when config is nil to prevent panic
- app_common: guard empty Servers slice in initHTTP2/3 and logServerAddresses
- proxy/health: handle nil HealthCheckConfig with defaults
- resolver: handle nil ResolverConfig by returning noopResolver
- middleware/headers: skip UpdateConfig when cfg is nil
- middleware/sliding_window: enforce minimum window duration of 1s
- lua/api_log: map EMERG/ALERT/CRIT to Error() instead of Fatal()
  to prevent Lua scripts from killing the entire server process
2026-06-11 16:23:04 +08:00

393 lines
13 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package lua 提供 ngx.log 和输出控制 API 实现。
//
// 该文件实现与 OpenResty/ngx_lua 兼容的日志和输出控制 API包括
// - ngx.log日志输出兼容 OpenResty 日志级别常量)
// - ngx.say/print内容输出追加到响应缓冲区
// - ngx.flush刷新输出缓冲区
// - ngx.exit终止请求处理
// - ngx.redirectHTTP 重定向
// - HTTP 状态码常量(如 ngx.HTTP_OK、ngx.HTTP_NOT_FOUND 等)
//
// 注意事项:
// - ngx.exit/ngx.redirect 通过 RaiseError 终止 Lua 执行
// - Scheduler 模式下 ngx.log 不依赖 RequestCtx仅输出到标准日志
//
// 作者xfy
package lua
import (
"strconv"
"strings"
"github.com/rs/zerolog"
"github.com/valyala/fasthttp"
glua "github.com/yuin/gopher-lua"
)
// 日志级别常量(与 OpenResty/ngx_lua 兼容)
const (
// LogStderr 标准错误日志级别。
LogStderr = 0
// LogEmerg 紧急日志级别。
LogEmerg = 1
// LogAlert 警报日志级别。
LogAlert = 2
// LogCrit 严重日志级别。
LogCrit = 3
// LogErr 错误日志级别。
LogErr = 4
// LogWarn 警告日志级别。
LogWarn = 5
// LogNotice 通知日志级别。
LogNotice = 6
// LogInfo 信息日志级别。
LogInfo = 7
// LogDebug 调试日志级别。
LogDebug = 8
)
// HTTP 状态码常量
const (
// HTTPContinue HTTP 100 继续状态码。
HTTPContinue = 100
// HTTPSwitchingProtocols HTTP 101 切换协议状态码。
HTTPSwitchingProtocols = 101
// HTTPOK HTTP 200 成功状态码。
HTTPOK = 200
// HTTPCreated HTTP 201 已创建状态码。
HTTPCreated = 201
// HTTPAccepted HTTP 202 已接受状态码。
HTTPAccepted = 202
// HTTPNoContent HTTP 204 无内容状态码。
HTTPNoContent = 204
// HTTPPartialContent HTTP 206 部分内容状态码。
HTTPPartialContent = 206
// HTTPMovedPermanently HTTP 301 永久重定向状态码。
HTTPMovedPermanently = 301
// HTTPFound HTTP 302 找到状态码。
HTTPFound = 302
// HTTPSeeOther HTTP 303 查看其他状态码。
HTTPSeeOther = 303
// HTTPNotModified HTTP 304 未修改状态码。
HTTPNotModified = 304
// HTTPTemporaryRedirect HTTP 307 临时重定向状态码。
HTTPTemporaryRedirect = 307
// HTTPPermanentRedirect HTTP 308 永久重定向状态码。
HTTPPermanentRedirect = 308
// HTTPBadRequest HTTP 400 错误请求状态码。
HTTPBadRequest = 400
// HTTPUnauthorized HTTP 401 未授权状态码。
HTTPUnauthorized = 401
// HTTPForbidden HTTP 403 禁止访问状态码。
HTTPForbidden = 403
// HTTPNotFound HTTP 404 未找到状态码。
HTTPNotFound = 404
// HTTPMethodNotAllowed HTTP 405 方法不允许状态码。
HTTPMethodNotAllowed = 405
// HTTPRequestTimeout HTTP 408 请求超时状态码。
HTTPRequestTimeout = 408
// HTTPConflict HTTP 409 冲突状态码。
HTTPConflict = 409
// HTTPGone HTTP 410 已移除状态码。
HTTPGone = 410
// HTTPLengthRequired HTTP 411 需要长度状态码。
HTTPLengthRequired = 411
// HTTPPayloadTooLarge HTTP 413 请求实体过大状态码。
HTTPPayloadTooLarge = 413
// HTTPURITooLong HTTP 414 URI 过长状态码。
HTTPURITooLong = 414
// HTTPUnsupportedMedia HTTP 415 不支持的媒体类型状态码。
HTTPUnsupportedMedia = 415
// HTTPRangeNotSatisfiable HTTP 416 范围不可满足状态码。
HTTPRangeNotSatisfiable = 416
// HTTPTooManyRequests HTTP 429 请求过多状态码。
HTTPTooManyRequests = 429
// HTTPInternalServerError HTTP 500 内部服务器错误状态码。
HTTPInternalServerError = 500
// HTTPNotImplemented HTTP 501 未实现状态码。
HTTPNotImplemented = 501
// HTTPBadGateway HTTP 502 错误网关状态码。
HTTPBadGateway = 502
// HTTPServiceUnavailable HTTP 503 服务不可用状态码。
HTTPServiceUnavailable = 503
// HTTPGatewayTimeout HTTP 504 网关超时状态码。
HTTPGatewayTimeout = 504
// HTTPHTTPVersionNotSupported HTTP 505 HTTP 版本不支持状态码。
HTTPHTTPVersionNotSupported = 505
)
// ngxLogAPI 封装 ngx.log 和输出控制相关的 API。
//
// 包含请求上下文、Lua 上下文和日志记录器,用于:
// - 将 Lua 日志消息转发到 zerolog 记录器
// - 通过 ngx.say/print 写入响应缓冲区
// - 通过 ngx.exit/redirect 终止请求处理
type ngxLogAPI struct {
// ctx 关联的 fasthttp 请求上下文,用于直接写入响应
ctx *fasthttp.RequestCtx
// luaCtx Lua 上下文,用于访问输出缓冲区
luaCtx *LuaContext
// logger zerolog 日志记录器,用于结构化日志输出
logger *zerolog.Logger
}
// newNgxLogAPI 创建 ngx.log API 实例。
//
// 参数:
// - ctx: fasthttp 请求上下文,用于直接写入响应
// - luaCtx: Lua 上下文,用于访问输出缓冲区
// - logger: zerolog 日志记录器,为 nil 时禁用结构化日志
//
// 返回值:
// - *ngxLogAPI: 初始化的 API 实例
func newNgxLogAPI(ctx *fasthttp.RequestCtx, luaCtx *LuaContext, logger *zerolog.Logger) *ngxLogAPI {
return &ngxLogAPI{
ctx: ctx,
luaCtx: luaCtx,
logger: logger,
}
}
// RegisterNgxLogAPI 在 Lua 状态机中注册 ngx.log 和输出控制 API。
//
// 常量日志级别、HTTP状态码等只在首次注册时写入避免并发写入冲突。
// 每次请求都会重新注册请求特定的函数log, say, print, flush, exit, redirect
func RegisterNgxLogAPI(L *glua.LState, api *ngxLogAPI) {
// 获取或创建 ngx 表
ngx := GetOrCreateNgxTable(L)
// 检查常量是否已注册(通过 STDERR 常量判断)
// 如果已注册,跳过常量写入,避免并发写入全局表
if ngx.RawGetString("STDERR") == glua.LNil {
// 注册日志级别常量
ngx.RawSetString("STDERR", glua.LNumber(LogStderr))
ngx.RawSetString("EMERG", glua.LNumber(LogEmerg))
ngx.RawSetString("ALERT", glua.LNumber(LogAlert))
ngx.RawSetString("CRIT", glua.LNumber(LogCrit))
ngx.RawSetString("ERR", glua.LNumber(LogErr))
ngx.RawSetString("WARN", glua.LNumber(LogWarn))
ngx.RawSetString("NOTICE", glua.LNumber(LogNotice))
ngx.RawSetString("INFO", glua.LNumber(LogInfo))
ngx.RawSetString("DEBUG", glua.LNumber(LogDebug))
// 注册 HTTP 状态码常量
ngx.RawSetString("HTTP_CONTINUE", glua.LNumber(HTTPContinue))
ngx.RawSetString("HTTP_SWITCHING_PROTOCOLS", glua.LNumber(HTTPSwitchingProtocols))
ngx.RawSetString("HTTP_OK", glua.LNumber(HTTPOK))
ngx.RawSetString("HTTP_CREATED", glua.LNumber(HTTPCreated))
ngx.RawSetString("HTTP_ACCEPTED", glua.LNumber(HTTPAccepted))
ngx.RawSetString("HTTP_NO_CONTENT", glua.LNumber(HTTPNoContent))
ngx.RawSetString("HTTP_PARTIAL_CONTENT", glua.LNumber(HTTPPartialContent))
ngx.RawSetString("HTTP_MOVED_PERMANENTLY", glua.LNumber(HTTPMovedPermanently))
ngx.RawSetString("HTTP_FOUND", glua.LNumber(HTTPFound))
ngx.RawSetString("HTTP_SEE_OTHER", glua.LNumber(HTTPSeeOther))
ngx.RawSetString("HTTP_NOT_MODIFIED", glua.LNumber(HTTPNotModified))
ngx.RawSetString("HTTP_TEMPORARY_REDIRECT", glua.LNumber(HTTPTemporaryRedirect))
ngx.RawSetString("HTTP_PERMANENT_REDIRECT", glua.LNumber(HTTPPermanentRedirect))
ngx.RawSetString("HTTP_BAD_REQUEST", glua.LNumber(HTTPBadRequest))
ngx.RawSetString("HTTP_UNAUTHORIZED", glua.LNumber(HTTPUnauthorized))
ngx.RawSetString("HTTP_FORBIDDEN", glua.LNumber(HTTPForbidden))
ngx.RawSetString("HTTP_NOT_FOUND", glua.LNumber(HTTPNotFound))
ngx.RawSetString("HTTP_METHOD_NOT_ALLOWED", glua.LNumber(HTTPMethodNotAllowed))
ngx.RawSetString("HTTP_REQUEST_TIMEOUT", glua.LNumber(HTTPRequestTimeout))
ngx.RawSetString("HTTP_CONFLICT", glua.LNumber(HTTPConflict))
ngx.RawSetString("HTTP_GONE", glua.LNumber(HTTPGone))
ngx.RawSetString("HTTP_LENGTH_REQUIRED", glua.LNumber(HTTPLengthRequired))
ngx.RawSetString("HTTP_PAYLOAD_TOO_LARGE", glua.LNumber(HTTPPayloadTooLarge))
ngx.RawSetString("HTTP_URI_TOO_LONG", glua.LNumber(HTTPURITooLong))
ngx.RawSetString("HTTP_UNSUPPORTED_MEDIA_TYPE", glua.LNumber(HTTPUnsupportedMedia))
ngx.RawSetString("HTTP_RANGE_NOT_SATISFIABLE", glua.LNumber(HTTPRangeNotSatisfiable))
ngx.RawSetString("HTTP_TOO_MANY_REQUESTS", glua.LNumber(HTTPTooManyRequests))
ngx.RawSetString("HTTP_INTERNAL_SERVER_ERROR", glua.LNumber(HTTPInternalServerError))
ngx.RawSetString("HTTP_NOT_IMPLEMENTED", glua.LNumber(HTTPNotImplemented))
ngx.RawSetString("HTTP_BAD_GATEWAY", glua.LNumber(HTTPBadGateway))
ngx.RawSetString("HTTP_SERVICE_UNAVAILABLE", glua.LNumber(HTTPServiceUnavailable))
ngx.RawSetString("HTTP_GATEWAY_TIMEOUT", glua.LNumber(HTTPGatewayTimeout))
ngx.RawSetString("HTTP_VERSION_NOT_SUPPORTED", glua.LNumber(HTTPHTTPVersionNotSupported))
// 特殊常量
ngx.RawSetString("ERROR", glua.LNumber(-1))
ngx.RawSetString("OK", glua.LNumber(0))
ngx.RawSetString("AGAIN", glua.LNumber(-2))
ngx.RawSetString("DONE", glua.LNumber(-4))
ngx.RawSetString("DECLINED", glua.LNumber(-5))
}
// 注册 ngx.log 函数(每次请求重新注册以绑定正确的 ctx
ngx.RawSetString("log", L.NewFunction(api.luaLog))
// 注册输出控制函数
ngx.RawSetString("say", L.NewFunction(api.luaSay))
ngx.RawSetString("print", L.NewFunction(api.luaPrint))
ngx.RawSetString("flush", L.NewFunction(api.luaFlush))
ngx.RawSetString("exit", L.NewFunction(api.luaExit))
ngx.RawSetString("redirect", L.NewFunction(api.luaRedirect))
// 注册 ngx 全局变量
L.SetGlobal("ngx", ngx)
}
// luaLog 实现 ngx.log(level, ...) - 日志输出
// Lua 调用: ngx.log(ngx.ERR, "error message")
// 返回: 无
func (api *ngxLogAPI) luaLog(L *glua.LState) int {
// 获取日志级别
level := L.CheckInt(1)
// 收集所有参数并拼接
var parts []string
n := L.GetTop()
for i := 2; i <= n; i++ {
parts = append(parts, L.ToString(i))
}
msg := strings.Join(parts, " ")
// 根据级别映射到 zerolog
if api.logger != nil {
switch level {
case LogEmerg, LogAlert, LogCrit:
api.logger.Error().Str("lua_level", "critical").Msg(msg)
case LogErr:
api.logger.Error().Msg(msg)
case LogWarn:
api.logger.Warn().Msg(msg)
case LogNotice:
api.logger.Info().Msg(msg)
case LogInfo:
api.logger.Info().Msg(msg)
case LogDebug:
api.logger.Debug().Msg(msg)
default:
api.logger.Info().Msg(msg)
}
}
return 0
}
// luaSay 实现 ngx.say(...) - 输出内容并附加换行符
// Lua 调用: ngx.say("hello", "world")
// 返回: 无
func (api *ngxLogAPI) luaSay(L *glua.LState) int {
// 收集所有参数
var parts []string
n := L.GetTop()
for i := 1; i <= n; i++ {
parts = append(parts, L.ToString(i))
}
msg := strings.Join(parts, "") + "\n"
// 写入到 LuaContext 的输出缓冲
if api.luaCtx != nil {
api.luaCtx.Write([]byte(msg))
} else if api.ctx != nil {
// 直接写入响应
_, _ = api.ctx.Write([]byte(msg))
}
return 0
}
// luaPrint 实现 ngx.print(...) - 输出内容不附加换行符
// Lua 调用: ngx.print("hello", "world")
// 返回: 无
func (api *ngxLogAPI) luaPrint(L *glua.LState) int {
// 收集所有参数
var parts []string
n := L.GetTop()
for i := 1; i <= n; i++ {
parts = append(parts, L.ToString(i))
}
msg := strings.Join(parts, "")
// 写入到 LuaContext 的输出缓冲
if api.luaCtx != nil {
api.luaCtx.Write([]byte(msg))
} else if api.ctx != nil {
// 直接写入响应
_, _ = api.ctx.Write([]byte(msg))
}
return 0
}
// luaFlush 实现 ngx.flush(wait?) - 刷新输出缓冲区
// Lua 调用: ngx.flush() 或 ngx.flush(true)
// 返回: 1 (boolean表示是否成功)
func (api *ngxLogAPI) luaFlush(L *glua.LState) int {
// 可选的 wait 参数
wait := false
if L.GetTop() >= 1 {
wait = L.ToBool(1)
}
// 刷新输出缓冲
if api.luaCtx != nil {
api.luaCtx.FlushOutput()
}
// fasthttp 没有显式的 flush 方法,数据会自动发送
// wait 参数在此实现中被忽略(阻塞式 flush
_ = wait
L.Push(glua.LTrue)
return 1
}
// luaExit 实现 ngx.exit(status) - 结束请求处理
// Lua 调用: ngx.exit(ngx.HTTP_OK) 或 ngx.exit(200)
// 返回: 无(抛出错误以终止执行)
func (api *ngxLogAPI) luaExit(L *glua.LState) int {
status := L.CheckInt(1)
// 设置退出状态
if api.luaCtx != nil {
api.luaCtx.Exit(status)
} else if api.ctx != nil {
api.ctx.SetStatusCode(status)
}
// 抛出错误以终止 Lua 执行
L.RaiseError("%s", "ngx.exit: "+strconv.Itoa(status))
return 0
}
// luaRedirect 实现 ngx.redirect(uri, status?) - HTTP 重定向
// Lua 调用: ngx.redirect("/new/path") 或 ngx.redirect("/new/path", 301)
// 返回: 无(抛出错误以终止执行)
func (api *ngxLogAPI) luaRedirect(L *glua.LState) int {
uri := L.CheckString(1)
// 默认状态码为 302 (HTTPFound)
status := HTTPFound
if L.GetTop() >= 2 {
status = L.CheckInt(2)
}
// 验证重定向状态码
if status != HTTPMovedPermanently &&
status != HTTPFound &&
status != HTTPSeeOther &&
status != HTTPTemporaryRedirect &&
status != HTTPPermanentRedirect {
L.ArgError(2, "invalid redirect status code")
return 0
}
// 设置重定向头
if api.ctx != nil {
api.ctx.Response.Header.Set("Location", uri)
api.ctx.SetStatusCode(status)
}
if api.luaCtx != nil {
api.luaCtx.Exited = true
}
// 抛出错误以终止 Lua 执行
L.RaiseError("%s", "ngx.redirect: "+uri)
return 0
}