feat(lua): 实现 ngx.log API 和输出控制
提供日志输出、响应输出、HTTP 状态码常量 API: ngx.log, ngx.say, ngx.print, ngx.flush, ngx.exit, ngx.redirect Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
51061d68ff
commit
86e5b0e6f1
340
internal/lua/api_log.go
Normal file
340
internal/lua/api_log.go
Normal file
@ -0,0 +1,340 @@
|
||||
// Package lua 提供 ngx.log 和输出控制 API 实现
|
||||
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 实现
|
||||
type ngxLogAPI struct {
|
||||
// 请求上下文
|
||||
ctx *fasthttp.RequestCtx
|
||||
|
||||
// Lua 上下文(用于访问输出缓冲等)
|
||||
luaCtx *LuaContext
|
||||
|
||||
// 日志记录器
|
||||
logger *zerolog.Logger
|
||||
}
|
||||
|
||||
// newNgxLogAPI 创建 ngx.log 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
|
||||
func RegisterNgxLogAPI(L *glua.LState, api *ngxLogAPI) {
|
||||
// 获取或创建 ngx 表
|
||||
var ngx *glua.LTable
|
||||
existingNgx := L.GetGlobal("ngx")
|
||||
if existingNgx != nil && existingNgx.Type() == glua.LTTable {
|
||||
ngx = existingNgx.(*glua.LTable)
|
||||
} else {
|
||||
ngx = L.NewTable()
|
||||
}
|
||||
|
||||
// 注册日志级别常量
|
||||
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 函数
|
||||
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
|
||||
}
|
||||
}
|
||||
395
internal/lua/api_log_test.go
Normal file
395
internal/lua/api_log_test.go
Normal file
@ -0,0 +1,395 @@
|
||||
// Package lua 提供 ngx.log API 测试
|
||||
package lua
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/valyala/fasthttp"
|
||||
glua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// mockRequestCtxForLog 创建模拟的 RequestCtx
|
||||
func mockRequestCtxForLog() *fasthttp.RequestCtx {
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
return ctx
|
||||
}
|
||||
|
||||
// TestNgxLogLevelConstants 测试日志级别常量
|
||||
func TestNgxLogLevelConstants(t *testing.T) {
|
||||
// 验证日志级别常量值
|
||||
assert.Equal(t, 0, LogStderr)
|
||||
assert.Equal(t, 1, LogEmerg)
|
||||
assert.Equal(t, 2, LogAlert)
|
||||
assert.Equal(t, 3, LogCrit)
|
||||
assert.Equal(t, 4, LogErr)
|
||||
assert.Equal(t, 5, LogWarn)
|
||||
assert.Equal(t, 6, LogNotice)
|
||||
assert.Equal(t, 7, LogInfo)
|
||||
assert.Equal(t, 8, LogDebug)
|
||||
}
|
||||
|
||||
// TestNgxHTTPStatusConstants 测试 HTTP 状态码常量
|
||||
func TestNgxHTTPStatusConstants(t *testing.T) {
|
||||
// 验证常用 HTTP 状态码
|
||||
assert.Equal(t, 200, HTTPOK)
|
||||
assert.Equal(t, 201, HTTPCreated)
|
||||
assert.Equal(t, 301, HTTPMovedPermanently)
|
||||
assert.Equal(t, 302, HTTPFound)
|
||||
assert.Equal(t, 303, HTTPSeeOther)
|
||||
assert.Equal(t, 307, HTTPTemporaryRedirect)
|
||||
assert.Equal(t, 308, HTTPPermanentRedirect)
|
||||
assert.Equal(t, 400, HTTPBadRequest)
|
||||
assert.Equal(t, 404, HTTPNotFound)
|
||||
assert.Equal(t, 500, HTTPInternalServerError)
|
||||
}
|
||||
|
||||
// TestNgxLogAPIRegistration 测试 API 注册
|
||||
func TestNgxLogAPIRegistration(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
// 创建测试请求上下文
|
||||
ctx := mockRequestCtxForLog()
|
||||
luaCtx := NewContext(engine, ctx)
|
||||
|
||||
// 创建 zerolog logger
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
// 创建 API 实例
|
||||
api := newNgxLogAPI(ctx, luaCtx, &logger)
|
||||
|
||||
// 在 Lua 状态机中注册 API
|
||||
L := engine.L
|
||||
RegisterNgxLogAPI(L, api)
|
||||
|
||||
// 验证 ngx 表已创建
|
||||
ngx := L.GetGlobal("ngx")
|
||||
require.NotEqual(t, nil, ngx)
|
||||
|
||||
// 验证日志级别常量已注册
|
||||
ngxTable := ngx.(*glua.LTable)
|
||||
assert.Equal(t, glua.LNumber(LogStderr), ngxTable.RawGetString("STDERR"))
|
||||
assert.Equal(t, glua.LNumber(LogEmerg), ngxTable.RawGetString("EMERG"))
|
||||
assert.Equal(t, glua.LNumber(LogErr), ngxTable.RawGetString("ERR"))
|
||||
assert.Equal(t, glua.LNumber(LogWarn), ngxTable.RawGetString("WARN"))
|
||||
assert.Equal(t, glua.LNumber(LogInfo), ngxTable.RawGetString("INFO"))
|
||||
assert.Equal(t, glua.LNumber(LogDebug), ngxTable.RawGetString("DEBUG"))
|
||||
|
||||
// 验证 HTTP 状态码常量已注册
|
||||
assert.Equal(t, glua.LNumber(HTTPOK), ngxTable.RawGetString("HTTP_OK"))
|
||||
assert.Equal(t, glua.LNumber(HTTPNotFound), ngxTable.RawGetString("HTTP_NOT_FOUND"))
|
||||
assert.Equal(t, glua.LNumber(HTTPInternalServerError), ngxTable.RawGetString("HTTP_INTERNAL_SERVER_ERROR"))
|
||||
|
||||
// 验证函数已注册
|
||||
assert.NotEqual(t, glua.LNil, ngxTable.RawGetString("log"))
|
||||
assert.NotEqual(t, glua.LNil, ngxTable.RawGetString("say"))
|
||||
assert.NotEqual(t, glua.LNil, ngxTable.RawGetString("print"))
|
||||
assert.NotEqual(t, glua.LNil, ngxTable.RawGetString("flush"))
|
||||
assert.NotEqual(t, glua.LNil, ngxTable.RawGetString("exit"))
|
||||
assert.NotEqual(t, glua.LNil, ngxTable.RawGetString("redirect"))
|
||||
}
|
||||
|
||||
// TestNgxLogAPILog 测试 ngx.log API
|
||||
func TestNgxLogAPILog(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
ctx := mockRequestCtxForLog()
|
||||
luaCtx := NewContext(engine, ctx)
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
api := newNgxLogAPI(ctx, luaCtx, &logger)
|
||||
L := engine.L
|
||||
RegisterNgxLogAPI(L, api)
|
||||
|
||||
// 测试日志输出
|
||||
err = L.DoString(`
|
||||
ngx.log(ngx.INFO, "test message")
|
||||
`)
|
||||
// 日志调用不应返回错误
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 验证日志内容
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "test message")
|
||||
}
|
||||
|
||||
// TestNgxLogAPISay 测试 ngx.say API
|
||||
func TestNgxLogAPISay(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
ctx := mockRequestCtxForLog()
|
||||
luaCtx := NewContext(engine, ctx)
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
api := newNgxLogAPI(ctx, luaCtx, &logger)
|
||||
L := engine.L
|
||||
RegisterNgxLogAPI(L, api)
|
||||
|
||||
// 测试 say 输出
|
||||
err = L.DoString(`
|
||||
ngx.say("Hello, ")
|
||||
ngx.say("World!")
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证输出缓冲包含换行符
|
||||
assert.Equal(t, "Hello, \nWorld!\n", string(luaCtx.OutputBuffer))
|
||||
}
|
||||
|
||||
// TestNgxLogAPIPrint 测试 ngx.print API
|
||||
func TestNgxLogAPIPrint(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
ctx := mockRequestCtxForLog()
|
||||
luaCtx := NewContext(engine, ctx)
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
api := newNgxLogAPI(ctx, luaCtx, &logger)
|
||||
L := engine.L
|
||||
RegisterNgxLogAPI(L, api)
|
||||
|
||||
// 测试 print 输出
|
||||
err = L.DoString(`
|
||||
ngx.print("Hello")
|
||||
ngx.print(", ")
|
||||
ngx.print("World")
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证输出缓冲不包含换行符
|
||||
assert.Equal(t, "Hello, World", string(luaCtx.OutputBuffer))
|
||||
}
|
||||
|
||||
// TestNgxLogAPIFlush 测试 ngx.flush API
|
||||
func TestNgxLogAPIFlush(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
ctx := mockRequestCtxForLog()
|
||||
luaCtx := NewContext(engine, ctx)
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
api := newNgxLogAPI(ctx, luaCtx, &logger)
|
||||
L := engine.L
|
||||
RegisterNgxLogAPI(L, api)
|
||||
|
||||
// 测试 flush
|
||||
err = L.DoString(`
|
||||
ngx.print("before flush")
|
||||
local ok = ngx.flush()
|
||||
assert(ok == true, "flush should return true")
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestNgxLogAPIExit 测试 ngx.exit API
|
||||
func TestNgxLogAPIExit(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
ctx := mockRequestCtxForLog()
|
||||
luaCtx := NewContext(engine, ctx)
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
api := newNgxLogAPI(ctx, luaCtx, &logger)
|
||||
L := engine.L
|
||||
RegisterNgxLogAPI(L, api)
|
||||
|
||||
// 测试 exit - 应该抛出错误
|
||||
err = L.DoString(`
|
||||
ngx.exit(ngx.HTTP_OK)
|
||||
`)
|
||||
// exit 应该返回错误
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "ngx.exit")
|
||||
|
||||
// 验证状态码已设置
|
||||
assert.Equal(t, 200, ctx.Response.StatusCode())
|
||||
}
|
||||
|
||||
// TestNgxLogAPIRedirect 测试 ngx.redirect API
|
||||
func TestNgxLogAPIRedirect(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
ctx := mockRequestCtxForLog()
|
||||
luaCtx := NewContext(engine, ctx)
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
api := newNgxLogAPI(ctx, luaCtx, &logger)
|
||||
L := engine.L
|
||||
RegisterNgxLogAPI(L, api)
|
||||
|
||||
// 测试 redirect - 默认状态码 302
|
||||
err = L.DoString(`
|
||||
ngx.redirect("/new/path")
|
||||
`)
|
||||
// redirect 应该返回错误以终止执行
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "ngx.redirect")
|
||||
|
||||
// 验证重定向头和状态码
|
||||
assert.Equal(t, 302, ctx.Response.StatusCode())
|
||||
assert.Equal(t, "/new/path", string(ctx.Response.Header.Peek("Location")))
|
||||
}
|
||||
|
||||
// TestNgxLogAPIRedirectWithStatus 测试带状态码的 redirect
|
||||
func TestNgxLogAPIRedirectWithStatus(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
ctx := mockRequestCtxForLog()
|
||||
luaCtx := NewContext(engine, ctx)
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
api := newNgxLogAPI(ctx, luaCtx, &logger)
|
||||
L := engine.L
|
||||
RegisterNgxLogAPI(L, api)
|
||||
|
||||
// 测试 redirect - 301
|
||||
err = L.DoString(`
|
||||
ngx.redirect("/permanent/path", ngx.HTTP_MOVED_PERMANENTLY)
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
|
||||
// 验证状态码
|
||||
assert.Equal(t, 301, ctx.Response.StatusCode())
|
||||
}
|
||||
|
||||
// TestNgxLogAPIRedirectInvalidStatus 测试无效状态码的 redirect
|
||||
func TestNgxLogAPIRedirectInvalidStatus(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
ctx := mockRequestCtxForLog()
|
||||
luaCtx := NewContext(engine, ctx)
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
api := newNgxLogAPI(ctx, luaCtx, &logger)
|
||||
L := engine.L
|
||||
RegisterNgxLogAPI(L, api)
|
||||
|
||||
// 测试 redirect - 无效状态码 (400)
|
||||
err = L.DoString(`
|
||||
ngx.redirect("/path", 400)
|
||||
`)
|
||||
// 应该返回参数错误
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// TestNgxLogAPILogLevels 测试不同日志级别
|
||||
func TestNgxLogAPILogLevels(t *testing.T) {
|
||||
testCases := []struct {
|
||||
level int
|
||||
expected zerolog.Level
|
||||
}{
|
||||
{LogEmerg, zerolog.FatalLevel},
|
||||
{LogAlert, zerolog.FatalLevel},
|
||||
{LogCrit, zerolog.FatalLevel},
|
||||
{LogErr, zerolog.ErrorLevel},
|
||||
{LogWarn, zerolog.WarnLevel},
|
||||
{LogNotice, zerolog.InfoLevel},
|
||||
{LogInfo, zerolog.InfoLevel},
|
||||
{LogDebug, zerolog.DebugLevel},
|
||||
{999, zerolog.InfoLevel}, // 未知级别默认为 Info
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := LogLevelToZerolog(tc.level)
|
||||
assert.Equal(t, tc.expected, result, "level %d should map to %v", tc.level, tc.expected)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNgxLogAPIWithoutLogger 测试无 logger 的情况
|
||||
func TestNgxLogAPIWithoutLogger(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
ctx := mockRequestCtxForLog()
|
||||
luaCtx := NewContext(engine, ctx)
|
||||
|
||||
// 创建无 logger 的 API
|
||||
api := newNgxLogAPI(ctx, luaCtx, nil)
|
||||
L := engine.L
|
||||
RegisterNgxLogAPI(L, api)
|
||||
|
||||
// 测试日志 - 不应 panic
|
||||
err = L.DoString(`
|
||||
ngx.log(ngx.INFO, "message without logger")
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestNgxLogAPIIntegration 集成测试
|
||||
func TestNgxLogAPIIntegration(t *testing.T) {
|
||||
engine, err := NewEngine(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
|
||||
ctx := mockRequestCtxForLog()
|
||||
luaCtx := NewContext(engine, ctx)
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
api := newNgxLogAPI(ctx, luaCtx, &logger)
|
||||
L := engine.L
|
||||
RegisterNgxLogAPI(L, api)
|
||||
|
||||
// 综合测试
|
||||
err = L.DoString(`
|
||||
-- 记录日志
|
||||
ngx.log(ngx.INFO, "Starting request")
|
||||
|
||||
-- 输出内容
|
||||
ngx.say("Line 1")
|
||||
ngx.print("Line 2")
|
||||
ngx.say("")
|
||||
|
||||
-- 使用常量
|
||||
ngx.say("HTTP OK: " .. ngx.HTTP_OK)
|
||||
ngx.say("HTTP NOT FOUND: " .. ngx.HTTP_NOT_FOUND)
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证输出
|
||||
output := string(luaCtx.OutputBuffer)
|
||||
assert.Contains(t, output, "Line 1")
|
||||
assert.Contains(t, output, "Line 2")
|
||||
assert.Contains(t, output, "HTTP OK: 200")
|
||||
assert.Contains(t, output, "HTTP NOT FOUND: 404")
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user