203 lines
5.5 KiB
Lua
203 lines
5.5 KiB
Lua
local Queue = require "kong.tools.queue"
|
|
local cjson = require "cjson"
|
|
local url = require "socket.url"
|
|
local http = require "resty.http"
|
|
local sandbox = require "kong.tools.sandbox".sandbox
|
|
local kong_meta = require "kong.meta"
|
|
|
|
|
|
local kong = kong
|
|
local ngx = ngx
|
|
local encode_base64 = ngx.encode_base64
|
|
local tostring = tostring
|
|
local tonumber = tonumber
|
|
local fmt = string.format
|
|
local pairs = pairs
|
|
local max = math.max
|
|
|
|
|
|
-- Create a function that concatenates multiple JSON objects into a JSON array.
|
|
-- This saves us from rendering all entries into one large JSON string.
|
|
-- Each invocation of the function returns the next bit of JSON, i.e. the opening
|
|
-- bracket, the entries, delimiting commas and the closing bracket.
|
|
local function make_json_array_payload_function(conf, entries)
|
|
if conf.queue.max_batch_size == 1 then
|
|
return #entries[1], entries[1]
|
|
end
|
|
|
|
local nentries = #entries
|
|
|
|
local content_length = 1
|
|
for i = 1, nentries do
|
|
content_length = content_length + #entries[i] + 1
|
|
end
|
|
|
|
local i = 0
|
|
local last = max(2, nentries * 2 + 1)
|
|
return content_length, function()
|
|
i = i + 1
|
|
|
|
if i == 1 then
|
|
return '['
|
|
|
|
elseif i < last then
|
|
return i % 2 == 0 and entries[i / 2] or ','
|
|
|
|
elseif i == last then
|
|
return ']'
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local parsed_urls_cache = {}
|
|
-- Parse host url.
|
|
-- @param `url` host url
|
|
-- @return `parsed_url` a table with host details:
|
|
-- scheme, host, port, path, query, userinfo
|
|
local function parse_url(host_url)
|
|
local parsed_url = parsed_urls_cache[host_url]
|
|
|
|
if parsed_url then
|
|
return parsed_url
|
|
end
|
|
|
|
parsed_url = url.parse(host_url)
|
|
if not parsed_url.port then
|
|
if parsed_url.scheme == "http" then
|
|
parsed_url.port = 80
|
|
|
|
elseif parsed_url.scheme == "https" then
|
|
parsed_url.port = 443
|
|
end
|
|
end
|
|
if not parsed_url.path then
|
|
parsed_url.path = "/"
|
|
end
|
|
|
|
parsed_urls_cache[host_url] = parsed_url
|
|
|
|
return parsed_url
|
|
end
|
|
|
|
|
|
-- Sends the provided entries to the configured plugin host
|
|
-- @return true if everything was sent correctly, falsy if error
|
|
-- @return error message if there was an error
|
|
local function send_entries(conf, entries)
|
|
local content_length, payload
|
|
if conf.queue.max_batch_size == 1 then
|
|
assert(
|
|
#entries == 1,
|
|
"internal error, received more than one entry in queue handler even though max_batch_size is 1"
|
|
)
|
|
content_length = #entries[1]
|
|
payload = entries[1]
|
|
else
|
|
content_length, payload = make_json_array_payload_function(conf, entries)
|
|
end
|
|
|
|
local method = conf.method
|
|
local timeout = conf.timeout
|
|
local keepalive = conf.keepalive
|
|
local content_type = conf.content_type
|
|
local http_endpoint = conf.http_endpoint
|
|
|
|
local parsed_url = parse_url(http_endpoint)
|
|
local host = parsed_url.host
|
|
local port = tonumber(parsed_url.port)
|
|
local userinfo = parsed_url.userinfo
|
|
|
|
local httpc = http.new()
|
|
httpc:set_timeout(timeout)
|
|
|
|
local headers = {
|
|
["Content-Type"] = content_type,
|
|
["Content-Length"] = content_length,
|
|
["Authorization"] = userinfo and "Basic " .. encode_base64(userinfo) or nil
|
|
}
|
|
if conf.headers then
|
|
for h, v in pairs(conf.headers) do
|
|
headers[h] = headers[h] or v -- don't override Host, Content-Type, Content-Length, Authorization
|
|
end
|
|
end
|
|
|
|
local log_server_url = fmt("%s://%s:%d%s", parsed_url.scheme, host, port, parsed_url.path)
|
|
|
|
local res, err = httpc:request_uri(log_server_url, {
|
|
method = method,
|
|
headers = headers,
|
|
body = payload,
|
|
keepalive_timeout = keepalive,
|
|
ssl_verify = false,
|
|
})
|
|
if not res then
|
|
return nil, "failed request to " .. host .. ":" .. tostring(port) .. ": " .. err
|
|
end
|
|
|
|
-- always read response body, even if we discard it without using it on success
|
|
local response_body = res.body
|
|
|
|
kong.log.debug(fmt("http-log sent data log server, %s:%s HTTP status %d",
|
|
host, port, res.status))
|
|
|
|
if res.status < 300 then
|
|
return true
|
|
|
|
else
|
|
return nil, "request to " .. host .. ":" .. tostring(port)
|
|
.. " returned status code " .. tostring(res.status) .. " and body "
|
|
.. response_body
|
|
end
|
|
end
|
|
|
|
|
|
local HttpLogHandler = {
|
|
PRIORITY = 12,
|
|
VERSION = kong_meta.version,
|
|
}
|
|
|
|
|
|
-- Create a queue name from the same legacy parameters that were used in the
|
|
-- previous queue implementation. This ensures that http-log instances that
|
|
-- have the same log server parameters are sharing a queue. It deliberately
|
|
-- uses the legacy parameters to determine the queue name, even though they may
|
|
-- be nil in newer configurations. Note that the modernized queue related
|
|
-- parameters are not included in the queue name determination.
|
|
local function make_queue_name(conf)
|
|
return fmt("%s:%s:%s:%s:%s:%s",
|
|
conf.http_endpoint,
|
|
conf.method,
|
|
conf.content_type,
|
|
conf.timeout,
|
|
conf.keepalive,
|
|
conf.retry_count,
|
|
conf.queue_size,
|
|
conf.flush_timeout)
|
|
end
|
|
|
|
|
|
function HttpLogHandler:log(conf)
|
|
if conf.custom_fields_by_lua then
|
|
local set_serialize_value = kong.log.set_serialize_value
|
|
for key, expression in pairs(conf.custom_fields_by_lua) do
|
|
set_serialize_value(key, sandbox(expression)())
|
|
end
|
|
end
|
|
|
|
local queue_conf = Queue.get_plugin_params("http-log", conf, make_queue_name(conf))
|
|
kong.log.debug("Queue name automatically configured based on configuration parameters to: ", queue_conf.name)
|
|
|
|
local ok, err = Queue.enqueue(
|
|
queue_conf,
|
|
send_entries,
|
|
conf,
|
|
cjson.encode(kong.log.serialize())
|
|
)
|
|
if not ok then
|
|
kong.log.err("Failed to enqueue log entry to log server: ", err)
|
|
end
|
|
end
|
|
|
|
return HttpLogHandler
|