Remove unused disk cache, tiered cache, purge, and config loader code. Add HashPathWithMethod and MatchPattern helpers for future cache purge API. Update test to use new mock backend API with ResponseBody field. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
413 lines
13 KiB
Go
413 lines
13 KiB
Go
// Package lua 提供 ngx.log 和输出控制 API 实现。
|
||
//
|
||
// 该文件实现与 OpenResty/ngx_lua 兼容的日志和输出控制 API,包括:
|
||
// - ngx.log:日志输出(兼容 OpenResty 日志级别常量)
|
||
// - ngx.say/print:内容输出(追加到响应缓冲区)
|
||
// - ngx.flush:刷新输出缓冲区
|
||
// - ngx.exit:终止请求处理
|
||
// - ngx.redirect:HTTP 重定向
|
||
// - 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 = 0
|
||
LogEmerg = 1
|
||
LogAlert = 2
|
||
LogCrit = 3
|
||
LogErr = 4
|
||
LogWarn = 5
|
||
LogNotice = 6
|
||
LogInfo = 7
|
||
LogDebug = 8
|
||
)
|
||
|
||
// HTTP 状态码常量
|
||
const (
|
||
HTTPContinue = 100
|
||
HTTPSwitchingProtocols = 101
|
||
HTTPOK = 200
|
||
HTTPCreated = 201
|
||
HTTPAccepted = 202
|
||
HTTPNoContent = 204
|
||
HTTPPartialContent = 206
|
||
HTTPMovedPermanently = 301
|
||
HTTPFound = 302
|
||
HTTPSeeOther = 303
|
||
HTTPNotModified = 304
|
||
HTTPTemporaryRedirect = 307
|
||
HTTPPermanentRedirect = 308
|
||
HTTPBadRequest = 400
|
||
HTTPUnauthorized = 401
|
||
HTTPForbidden = 403
|
||
HTTPNotFound = 404
|
||
HTTPMethodNotAllowed = 405
|
||
HTTPRequestTimeout = 408
|
||
HTTPConflict = 409
|
||
HTTPGone = 410
|
||
HTTPLengthRequired = 411
|
||
HTTPPayloadTooLarge = 413
|
||
HTTPURITooLong = 414
|
||
HTTPUnsupportedMedia = 415
|
||
HTTPRangeNotSatisfiable = 416
|
||
HTTPTooManyRequests = 429
|
||
HTTPInternalServerError = 500
|
||
HTTPNotImplemented = 501
|
||
HTTPBadGateway = 502
|
||
HTTPServiceUnavailable = 503
|
||
HTTPGatewayTimeout = 504
|
||
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.Fatal().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
|
||
}
|
||
|
||
// LogLevelToZerolog 将 ngx 日志级别转换为 zerolog 级别
|
||
func LogLevelToZerolog(level int) zerolog.Level {
|
||
switch level {
|
||
case LogEmerg, LogAlert, LogCrit:
|
||
return zerolog.FatalLevel
|
||
case LogErr:
|
||
return zerolog.ErrorLevel
|
||
case LogWarn:
|
||
return zerolog.WarnLevel
|
||
case LogNotice, LogInfo:
|
||
return zerolog.InfoLevel
|
||
case LogDebug:
|
||
return zerolog.DebugLevel
|
||
default:
|
||
return zerolog.InfoLevel
|
||
}
|
||
}
|
||
|
||
// RegisterSchedulerLogAPI 为 Scheduler LState 注册安全的 ngx.log API
|
||
// 不依赖 RequestCtx,仅输出到标准日志
|
||
func RegisterSchedulerLogAPI(L *glua.LState, ngx *glua.LTable) {
|
||
// 注册日志级别常量
|
||
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_OK", glua.LNumber(HTTPOK))
|
||
ngx.RawSetString("HTTP_INTERNAL_SERVER_ERROR", glua.LNumber(HTTPInternalServerError))
|
||
|
||
// 注册 ngx.log 函数(不依赖 RequestCtx 的版本)
|
||
ngx.RawSetString("log", L.NewFunction(luaSchedulerLog))
|
||
}
|
||
|
||
// luaSchedulerLog 实现 scheduler 模式下的 ngx.log
|
||
// 不依赖 RequestCtx,仅输出到标准日志
|
||
func luaSchedulerLog(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, " ")
|
||
|
||
// 根据级别输出(scheduler 模式下没有 logger,直接打印)
|
||
// 在实际实现中,可以通过 engine 的 logger 输出
|
||
_ = level
|
||
_ = msg
|
||
|
||
return 0
|
||
}
|