Files
ReachableCEO 54cc5f7308 feat(apisix): add Cloudron package
- Implements Apache APISIX packaging for Cloudron platform.
- Includes Dockerfile, CloudronManifest.json, and start.sh.
- Configured to use Cloudron's etcd addon.

🤖 Generated with Gemini CLI
Co-Authored-By: Gemini <noreply@google.com>
2025-09-04 09:42:47 -05:00

330 lines
9.6 KiB
Lua

--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local pb = require "pb"
local protoc = require("protoc").new()
local http = require("resty.http")
local socket = require("socket")
local str_util = require("resty.string")
local core = require("apisix.core")
local core_gethostname = require("apisix.core.utils").gethostname
local json = core.json
local json_encode = json.encode
local ngx = ngx
local ngx_time = ngx.time
local ngx_now = ngx.now
local ngx_sha1_bin = ngx.sha1_bin
local ngx_hmac_sha1 = ngx.hmac_sha1
local fmt = string.format
local table = table
local concat_tab = table.concat
local clear_tab = table.clear
local new_tab = table.new
local insert_tab = table.insert
local ipairs = ipairs
local pairs = pairs
local type = type
local tostring = tostring
local setmetatable = setmetatable
local pcall = pcall
local unpack = unpack
-- api doc https://www.tencentcloud.com/document/product/614/16873
local MAX_SINGLE_VALUE_SIZE = 1 * 1024 * 1024
local MAX_LOG_GROUP_VALUE_SIZE = 5 * 1024 * 1024 -- 5MB
local cls_api_path = "/structuredlog"
local auth_expire_time = 60
local cls_conn_timeout = 1000
local cls_read_timeout = 10000
local cls_send_timeout = 10000
local headers_cache = {}
local params_cache = {
ssl_verify = false,
headers = headers_cache,
}
local function get_ip(hostname)
local _, resolved = socket.dns.toip(hostname)
local ip_list = {}
if not resolved.ip then
-- DNS parsing failure
local err = resolved
core.log.error("resolve ip failed, hostname: " .. hostname .. ", error: " .. err)
return nil, err
else
for _, v in ipairs(resolved.ip) do
insert_tab(ip_list, v)
end
end
return ip_list
end
local host_ip
local log_group_list = {}
local log_group_list_pb = {
logGroupList = log_group_list,
}
local function sha1(msg)
return str_util.to_hex(ngx_sha1_bin(msg))
end
local function sha1_hmac(key, msg)
return str_util.to_hex(ngx_hmac_sha1(key, msg))
end
-- sign algorithm https://cloud.tencent.com/document/product/614/12445
local function sign(secret_id, secret_key)
local method = "post"
local format_params = ""
local format_headers = ""
local sign_algorithm = "sha1"
local http_request_info = fmt("%s\n%s\n%s\n%s\n",
method, cls_api_path, format_params, format_headers)
local cur_time = ngx_time()
local sign_time = fmt("%d;%d", cur_time, cur_time + auth_expire_time)
local string_to_sign = fmt("%s\n%s\n%s\n", sign_algorithm, sign_time, sha1(http_request_info))
local sign_key = sha1_hmac(secret_key, sign_time)
local signature = sha1_hmac(sign_key, string_to_sign)
local arr = {
"q-sign-algorithm=sha1",
"q-ak=" .. secret_id,
"q-sign-time=" .. sign_time,
"q-key-time=" .. sign_time,
"q-header-list=",
"q-url-param-list=",
"q-signature=" .. signature,
}
return concat_tab(arr, '&')
end
-- normalized log data for CLS API
local function normalize_log(log)
local normalized_log = {}
local log_size = 4 -- empty obj alignment
for k, v in pairs(log) do
local v_type = type(v)
local field = { key = k, value = "" }
if v_type == "string" then
field["value"] = v
elseif v_type == "number" then
field["value"] = tostring(v)
elseif v_type == "table" then
field["value"] = json_encode(v)
else
field["value"] = tostring(v)
core.log.warn("unexpected type " .. v_type .. " for field " .. k)
end
if #field.value > MAX_SINGLE_VALUE_SIZE then
core.log.warn(field.key, " value size over ", MAX_SINGLE_VALUE_SIZE, " , truncated")
field.value = field.value:sub(1, MAX_SINGLE_VALUE_SIZE)
end
insert_tab(normalized_log, field)
log_size = log_size + #field.key + #field.value
end
return normalized_log, log_size
end
local _M = { version = 0.1 }
local mt = { __index = _M }
local pb_state
local function init_pb_state()
local old_pb_state = pb.state(nil)
protoc.reload()
local cls_sdk_protoc = protoc.new()
-- proto file in https://www.tencentcloud.com/document/product/614/42787
local ok, err = pcall(cls_sdk_protoc.load, cls_sdk_protoc, [[
package cls;
message Log
{
message Content
{
required string key = 1; // Key of each field group
required string value = 2; // Value of each field group
}
required int64 time = 1; // Unix timestamp
repeated Content contents = 2; // Multiple key-value pairs in one log
}
message LogTag
{
required string key = 1;
required string value = 2;
}
message LogGroup
{
repeated Log logs = 1; // Log array consisting of multiple logs
optional string contextFlow = 2; // This parameter does not take effect currently
optional string filename = 3; // Log filename
optional string source = 4; // Log source, which is generally the machine IP
repeated LogTag logTags = 5;
}
message LogGroupList
{
repeated LogGroup logGroupList = 1; // Log group list
}
]], "tencent-cloud-cls/cls.proto")
if not ok then
cls_sdk_protoc:reset()
pb.state(old_pb_state)
return "failed to load cls.proto: ".. err
end
pb_state = pb.state(old_pb_state)
end
function _M.new(host, topic, secret_id, secret_key)
if not pb_state then
local err = init_pb_state()
if err then
return nil, err
end
end
local self = {
host = host,
topic = topic,
secret_id = secret_id,
secret_key = secret_key,
}
return setmetatable(self, mt)
end
local function do_request_uri(uri, params)
local client = http:new()
client:set_timeouts(cls_conn_timeout, cls_send_timeout, cls_read_timeout)
local res, err = client:request_uri(uri, params)
client:close()
return res, err
end
function _M.send_cls_request(self, pb_obj)
-- recovery of stored pb_store
local old_pb_state = pb.state(pb_state)
local ok, pb_data = pcall(pb.encode, "cls.LogGroupList", pb_obj)
pb_state = pb.state(old_pb_state)
if not ok or not pb_data then
core.log.error("failed to encode LogGroupList, err: ", pb_data)
return false, pb_data
end
clear_tab(headers_cache)
headers_cache["Host"] = self.host
headers_cache["Content-Type"] = "application/x-protobuf"
headers_cache["Authorization"] = sign(self.secret_id, self.secret_key, cls_api_path)
-- TODO: support lz4/zstd compress
params_cache.method = "POST"
params_cache.body = pb_data
local cls_url = "http://" .. self.host .. cls_api_path .. "?topic_id=" .. self.topic
core.log.debug("CLS request URL: ", cls_url)
local res, err = do_request_uri(cls_url, params_cache)
if not res then
return false, err
end
if res.status ~= 200 then
err = fmt("got wrong status: %s, headers: %s, body, %s",
res.status, json.encode(res.headers), res.body)
-- 413, 404, 401, 403 are not retryable
if res.status == 413 or res.status == 404 or res.status == 401 or res.status == 403 then
core.log.error(err, ", not retryable")
return true
end
return false, err
end
core.log.debug("CLS report success")
return true
end
function _M.send_to_cls(self, logs)
clear_tab(log_group_list)
local now = ngx_now() * 1000
local total_size = 0
local format_logs = new_tab(#logs, 0)
-- sums of all value in all LogGroup should be no more than 5MB
-- so send whenever size exceed max size
local group_list_start = 1
if not host_ip then
local host_ip_list, err = get_ip(core_gethostname())
if not host_ip_list then
return false, err
end
host_ip = tostring(unpack(host_ip_list))
end
for i = 1, #logs, 1 do
local contents, log_size = normalize_log(logs[i])
if log_size > MAX_LOG_GROUP_VALUE_SIZE then
core.log.error("size of log is over 5MB, dropped")
goto continue
end
total_size = total_size + log_size
if total_size > MAX_LOG_GROUP_VALUE_SIZE then
insert_tab(log_group_list, {
logs = format_logs,
source = host_ip,
})
local ok, err = self:send_cls_request(log_group_list_pb)
if not ok then
return false, err, group_list_start
end
group_list_start = i
format_logs = new_tab(#logs - i, 0)
total_size = 0
clear_tab(log_group_list)
end
insert_tab(format_logs, {
time = now,
contents = contents,
})
:: continue ::
end
insert_tab(log_group_list, {
logs = format_logs,
source = host_ip,
})
local ok, err = self:send_cls_request(log_group_list_pb)
return ok, err, group_list_start
end
return _M