apisix/utils/log-util.lua (309 lines of code) (raw):
--
-- 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 core = require("apisix.core")
local plugin = require("apisix.plugin")
local expr = require("resty.expr.v1")
local content_decode = require("apisix.utils.content-decode")
local ngx = ngx
local pairs = pairs
local ngx_now = ngx.now
local ngx_header = ngx.header
local os_date = os.date
local str_byte = string.byte
local str_sub = string.sub
local math_floor = math.floor
local ngx_update_time = ngx.update_time
local req_get_body_data = ngx.req.get_body_data
local is_http = ngx.config.subsystem == "http"
local req_get_body_file = ngx.req.get_body_file
local MAX_REQ_BODY = 524288 -- 512 KiB
local MAX_RESP_BODY = 524288 -- 512 KiB
local io = io
local lru_log_format = core.lrucache.new({
ttl = 300, count = 512
})
local _M = {}
local function get_request_body(max_bytes)
local req_body = req_get_body_data()
if req_body then
if max_bytes and #req_body >= max_bytes then
req_body = str_sub(req_body, 1, max_bytes)
end
return req_body
end
local file_name = req_get_body_file()
if not file_name then
return nil
end
core.log.info("attempt to read body from file: ", file_name)
local f, err = io.open(file_name, 'r')
if not f then
return nil, "fail to open file " .. err
end
req_body = f:read(max_bytes)
f:close()
return req_body
end
local function gen_log_format(format)
local log_format = {}
for k, var_name in pairs(format) do
if var_name:byte(1, 1) == str_byte("$") then
log_format[k] = {true, var_name:sub(2)}
else
log_format[k] = {false, var_name}
end
end
core.log.info("log_format: ", core.json.delay_encode(log_format))
return log_format
end
local function get_custom_format_log(ctx, format, max_req_body_bytes)
local log_format = lru_log_format(format or "", nil, gen_log_format, format)
local entry = core.table.new(0, core.table.nkeys(log_format))
for k, var_attr in pairs(log_format) do
if var_attr[1] then
local key = var_attr[2]
if key == "request_body" then
local max_req_body_bytes = max_req_body_bytes or MAX_REQ_BODY
local req_body, err = get_request_body(max_req_body_bytes)
if err then
core.log.error("fail to get request body: ", err)
else
entry[k] = req_body
end
else
entry[k] = ctx.var[var_attr[2]]
end
else
entry[k] = var_attr[2]
end
end
local matched_route = ctx.matched_route and ctx.matched_route.value
if matched_route then
entry.service_id = matched_route.service_id
entry.route_id = matched_route.id
end
return entry
end
-- export the log getter so we can mock in tests
_M.get_custom_format_log = get_custom_format_log
-- for test
function _M.inject_get_custom_format_log(f)
get_custom_format_log = f
_M.get_custom_format_log = f
end
local function latency_details_in_ms(ctx)
local latency = (ngx_now() - ngx.req.start_time()) * 1000
local upstream_latency, apisix_latency = nil, latency
if ctx.var.upstream_response_time then
upstream_latency = ctx.var.upstream_response_time * 1000
apisix_latency = apisix_latency - upstream_latency
-- The latency might be negative, as Nginx uses different time measurements in
-- different metrics.
-- See https://github.com/apache/apisix/issues/5146#issuecomment-928919399
if apisix_latency < 0 then
apisix_latency = 0
end
end
return latency, upstream_latency, apisix_latency
end
_M.latency_details_in_ms = latency_details_in_ms
local function get_full_log(ngx, conf)
local ctx = ngx.ctx.api_ctx
local var = ctx.var
local service_id
local route_id
local url = var.scheme .. "://" .. var.host .. ":" .. var.server_port
.. var.request_uri
local matched_route = ctx.matched_route and ctx.matched_route.value
if matched_route then
service_id = matched_route.service_id or ""
route_id = matched_route.id
else
service_id = var.host
end
local consumer
if ctx.consumer then
consumer = {
username = ctx.consumer.username
}
end
local latency, upstream_latency, apisix_latency = latency_details_in_ms(ctx)
local log = {
request = {
url = url,
uri = var.request_uri,
method = ngx.req.get_method(),
headers = ngx.req.get_headers(),
querystring = ngx.req.get_uri_args(),
size = var.request_length
},
response = {
status = ngx.status,
headers = ngx.resp.get_headers(),
size = var.bytes_sent
},
server = {
hostname = core.utils.gethostname(),
version = core.version.VERSION
},
upstream = var.upstream_addr,
service_id = service_id,
route_id = route_id,
consumer = consumer,
client_ip = core.request.get_remote_client_ip(ngx.ctx.api_ctx),
start_time = ngx.req.start_time() * 1000,
latency = latency,
upstream_latency = upstream_latency,
apisix_latency = apisix_latency
}
if ctx.resp_body then
log.response.body = ctx.resp_body
end
if conf.include_req_body then
local log_request_body = true
if conf.include_req_body_expr then
if not conf.request_expr then
local request_expr, err = expr.new(conf.include_req_body_expr)
if not request_expr then
core.log.error('generate request expr err ' .. err)
return log
end
conf.request_expr = request_expr
end
local result = conf.request_expr:eval(ctx.var)
if not result then
log_request_body = false
end
end
if log_request_body then
local max_req_body_bytes = conf.max_req_body_bytes or MAX_REQ_BODY
local body, err = get_request_body(max_req_body_bytes)
if err then
core.log.error("fail to get request body: ", err)
return
end
log.request.body = body
end
end
return log
end
_M.get_full_log = get_full_log
-- for test
function _M.inject_get_full_log(f)
get_full_log = f
_M.get_full_log = f
end
local function is_match(match, ctx)
local match_result
for _, m in pairs(match) do
local expr, _ = expr.new(m)
match_result = expr:eval(ctx.var)
if match_result then
break
end
end
return match_result
end
function _M.get_log_entry(plugin_name, conf, ctx)
-- If the "match" configuration is set and the matching conditions are not met,
-- then do not log the message.
if conf.match and not is_match(conf.match, ctx) then
return
end
local metadata = plugin.plugin_metadata(plugin_name)
core.log.info("metadata: ", core.json.delay_encode(metadata))
local entry
local customized = false
local has_meta_log_format = metadata and metadata.value.log_format
and core.table.nkeys(metadata.value.log_format) > 0
if conf.log_format or has_meta_log_format then
customized = true
entry = get_custom_format_log(ctx, conf.log_format or metadata.value.log_format,
conf.max_req_body_bytes)
else
if is_http then
entry = get_full_log(ngx, conf)
else
-- get_full_log doesn't work in stream
core.log.error(plugin_name, "'s log_format is not set")
end
end
return entry, customized
end
function _M.get_req_original(ctx, conf)
local data = {
ctx.var.request, "\r\n"
}
for k, v in pairs(ngx.req.get_headers()) do
core.table.insert_tail(data, k, ": ", v, "\r\n")
end
core.table.insert(data, "\r\n")
if conf.include_req_body then
local max_req_body_bytes = conf.max_req_body_bytes or MAX_REQ_BODY
local req_body = get_request_body(max_req_body_bytes)
core.table.insert(data, req_body)
end
return core.table.concat(data, "")
end
function _M.check_log_schema(conf)
if conf.include_req_body_expr then
local ok, err = expr.new(conf.include_req_body_expr)
if not ok then
return nil, "failed to validate the 'include_req_body_expr' expression: " .. err
end
end
if conf.include_resp_body_expr then
local ok, err = expr.new(conf.include_resp_body_expr)
if not ok then
return nil, "failed to validate the 'include_resp_body_expr' expression: " .. err
end
end
return true, nil
end
function _M.collect_body(conf, ctx)
if conf.include_resp_body then
local log_response_body = true
if conf.include_resp_body_expr then
if not conf.response_expr then
local response_expr, err = expr.new(conf.include_resp_body_expr)
if not response_expr then
core.log.error('generate response expr err ' .. err)
return
end
conf.response_expr = response_expr
end
if ctx.res_expr_eval_result == nil then
ctx.res_expr_eval_result = conf.response_expr:eval(ctx.var)
end
if not ctx.res_expr_eval_result then
log_response_body = false
end
end
if log_response_body then
local max_resp_body_bytes = conf.max_resp_body_bytes or MAX_RESP_BODY
if ctx._resp_body_bytes and ctx._resp_body_bytes >= max_resp_body_bytes then
return
end
local final_body = core.response.hold_body_chunk(ctx, true, max_resp_body_bytes)
if not final_body then
return
end
local response_encoding = ngx_header["Content-Encoding"]
if not response_encoding then
ctx.resp_body = final_body
return
end
local decoder = content_decode.dispatch_decoder(response_encoding)
if not decoder then
core.log.warn("unsupported compression encoding type: ",
response_encoding)
ctx.resp_body = final_body
return
end
local decoded_body, err = decoder(final_body)
if err ~= nil then
core.log.warn("try decode compressed data err: ", err)
ctx.resp_body = final_body
return
end
ctx.resp_body = decoded_body
end
end
end
function _M.get_rfc3339_zulu_timestamp(timestamp)
ngx_update_time()
local now = timestamp or ngx_now()
local second = math_floor(now)
local millisecond = math_floor((now - second) * 1000)
return os_date("!%Y-%m-%dT%T.", second) .. core.string.format("%03dZ", millisecond)
end
return _M