lolly/internal/lua/api_log.go
xfy 8b382606df Merge branch 'lint-fix' - resolve sendfile.go conflict
Conflict: sendfile.go (!linux build tag) was incorrectly modified to
include linuxSendfile and getSocketFd functions which already exist
in sendfile_linux.go.

Resolution: Keep HEAD version (simple fallback returning ENOTSUP) as
Linux implementation is properly separated in sendfile_linux.go.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 09:26:48 +08:00

387 lines
12 KiB
Go
Raw 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 实现
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 {
//nolint:errcheck // 类型断言检查
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)) //nolint:errcheck
}
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)) //nolint:errcheck
}
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
// fmt.Printf("[timer] %s\n", msg)
return 0
}