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:
xfy 2026-04-11 12:17:35 +08:00
parent 51061d68ff
commit 86e5b0e6f1
2 changed files with 735 additions and 0 deletions

340
internal/lua/api_log.go Normal file
View 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
}
}

View 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")
}