Files
2026-07-13 12:32:21 +08:00

213 lines
5.1 KiB
Lua

local _M = {
maybe_push = function() end,
get_request_logs = function() return {} end,
get_worker_logs = function() return {} end,
}
if ngx.config.subsystem ~= "http" then
return _M
end
local request_id_get = require "kong.observability.tracing.request_id".get
local time_ns = require "kong.tools.time".time_ns
local table_merge = require "kong.tools.table".table_merge
local deep_copy = require "kong.tools.table".deep_copy
local get_log_level = require "resty.kong.log".get_log_level
local constants_log_levels = require "kong.constants".LOG_LEVELS
local table_new = require "table.new"
local string_buffer = require "string.buffer"
local ngx = ngx
local kong = kong
local table = table
local tostring = tostring
local native_ngx_log = _G.native_ngx_log or ngx.log
local ngx_null = ngx.null
local table_pack = table.pack -- luacheck: ignore
local MAX_WORKER_LOGS = 1000
local MAX_REQUEST_LOGS = 1000
local INITIAL_SIZE_WORKER_LOGS = 100
local NGX_CTX_REQUEST_LOGS_KEY = "o11y_logs_request_scoped"
local worker_logs = table_new(INITIAL_SIZE_WORKER_LOGS, 0)
local logline_buf = string_buffer.new()
local notified_buffer_full = false
-- WARNING: avoid using `ngx.log` in this function to prevent recursive loops
local function configured_log_level()
local ok, level = pcall(get_log_level)
if not ok then
-- This is unexpected outside of the context of unit tests
local level_str = kong.configuration.log_level
native_ngx_log(ngx.WARN,
"[observability] OpenTelemetry logs failed reading dynamic log level. " ..
"Using log level: " .. level_str .. " from configuration."
)
level = constants_log_levels[level_str]
end
return level
end
-- needed because table.concat doesn't like booleans
local function concat_tostring(tab)
local tab_len = #tab
if tab_len == 0 then
return ""
end
for i = 1, tab_len do
local value = tab[i]
if value == ngx_null then
value = "nil"
else
value = tostring(value)
end
logline_buf:put(value)
end
return logline_buf:get()
end
local function generate_log_entry(request_scoped, inj_attributes, log_level, log_str, request_id, debug_info)
local span_id
if request_scoped then
-- add tracing information if tracing is enabled
local active_span = kong and kong.tracing and kong.tracing.active_span()
if active_span then
span_id = active_span.span_id
end
end
local attributes = {
["request.id"] = request_id,
["introspection.current.line"] = debug_info.currentline,
["introspection.name"] = debug_info.name,
["introspection.namewhat"] = debug_info.namewhat,
["introspection.source"] = debug_info.source,
["introspection.what"] = debug_info.what,
}
if inj_attributes then
attributes = table_merge(attributes, inj_attributes)
end
local now_ns = time_ns()
return {
time_unix_nano = now_ns,
observed_time_unix_nano = now_ns,
log_level = log_level,
body = log_str,
attributes = attributes,
span_id = span_id,
}
end
local function get_request_log_buffer()
local log_buffer = ngx.ctx[NGX_CTX_REQUEST_LOGS_KEY]
if not log_buffer then
log_buffer = table_new(10, 0)
ngx.ctx[NGX_CTX_REQUEST_LOGS_KEY] = log_buffer
end
return log_buffer
end
-- notifies the user that the log buffer is full, once (per worker)
local function notify_buffer_full_once()
if not notified_buffer_full then
notified_buffer_full = true
native_ngx_log(ngx.NOTICE,
"[observability] OpenTelemetry logs buffer is full: dropping new log entries."
)
end
end
local function notify_if_resumed()
-- if we are in a "resumed" state
if notified_buffer_full then
notified_buffer_full = false
native_ngx_log(ngx.NOTICE,
"[observability] OpenTelemetry logs buffer resumed accepting log entries."
)
end
end
function _M.maybe_push(stack_level, attributes, log_level, ...)
-- WARNING: do not yield in this function, as it is called from ngx.log
-- Early return cases:
-- log level too low
if log_level and configured_log_level() < log_level then
return
end
local log_buffer, max_logs
local request_id = request_id_get()
local request_scoped = request_id ~= nil
-- get the appropriate log buffer depending on the current context
if request_scoped then
log_buffer = get_request_log_buffer()
max_logs = MAX_REQUEST_LOGS
else
log_buffer = worker_logs
max_logs = MAX_WORKER_LOGS
end
-- return if log buffer is full
if #log_buffer >= max_logs then
notify_buffer_full_once()
return
end
notify_if_resumed()
local args = table_pack(...)
local log_str = concat_tostring(args)
-- generate & push log entry
local debug_info = debug.getinfo(stack_level, "nSl")
local log_entry = generate_log_entry(
request_scoped,
attributes,
log_level,
log_str,
request_id,
debug_info
)
table.insert(log_buffer, log_entry)
end
function _M.get_worker_logs()
local wl = worker_logs
worker_logs = table_new(INITIAL_SIZE_WORKER_LOGS, 0)
return wl
end
function _M.get_request_logs()
local request_logs = get_request_log_buffer()
return deep_copy(request_logs)
end
return _M