Private
Public Access
1
0
Files
lua-prometheus-exporter/send-utils.lua
2026-01-10 18:24:52 +01:00

165 lines
5.7 KiB
Lua

local function SendUDP(content)
if content then
helpers.send_udp(options.udpPort, content, options.senderIndex)
else
error("Missing udpPort or Content")
end
end
---Extracts the header from content if it exists (format: ---<headerline>---)
---@param content string
---@return string|nil header, number headerEndPos
local function ExtractHeader(content)
local headerStart, headerEnd, headerText = content:find("^%-%-%-(.-)%-%-%-\n", 1)
if headerStart then
return "---" .. headerText .. "---", headerEnd
end
return nil, 0
end
---Finds all header positions in content once (format: ---<headerline>---)
---@param content string
---@return table headers Array of {endPos=position_after_newline, text=header_text}
local function FindAllHeaders(content)
local headers = {}
local pos = 1
while true do
local start, endPos, headerText = content:find("%-%-%-(.-)%-%-%-\n", pos)
if not start then break end
table.insert(headers, {endPos = endPos + 1, text = "---" .. headerText .. "---"})
pos = endPos + 1
end
return headers
end
---Binary search to find nearest header before targetPos
---@param headers table Pre-computed header positions
---@param targetPos number Maximum position to search for
---@return number|nil index, number|nil headerEndPos, string|nil headerText
local function BinarySearchHeader(headers, targetPos)
local left, right = 1, #headers
local result = nil
while left <= right do
local mid = math.floor((left + right) / 2)
if headers[mid].endPos <= targetPos then
result = mid
left = mid + 1
else
right = mid - 1
end
end
if result then
return result, headers[result].endPos, headers[result].text
end
return nil, nil, nil
end
---Chunks content into appropriately sized pieces, respecting header lines
---Chunks content greedily to minimize UDP calls
---Fills 200KB buffer as completely as possible, including multiple headers if they fit
---Only prepends header when chunk starts mid-section
---@param content string
---@param maxSizekB number|nil Default 200KB
---@return table chunks Array of {header=string|nil, content=string}
local function ChunkContent(content, maxSizekB)
local maxSize = (maxSizekB or 200) * 1024
local contentLength = #content
if contentLength < maxSize then
return {{header = nil, content = content}}
end
-- Extract initial header if present
local initialHeader, headerEndPos = ExtractHeader(content)
local contentStart = headerEndPos > 0 and headerEndPos + 1 or 1
local remainingContent = content:sub(contentStart)
-- Pre-compute all headers once for efficient lookup
local headers = FindAllHeaders(remainingContent)
local chunks = {}
local start = 1
local len = #remainingContent
local lastStart = 0
while start <= len do
-- Safety check to prevent infinite loops
if start == lastStart then
error("Infinite loop detected in ChunkContent: cannot make progress")
end
lastStart = start
-- Determine the "Active Context" - which header does this position belong to?
local _, _, activeHeaderText = BinarySearchHeader(headers, start)
-- Check if we're starting exactly at a header boundary (after a newline)
local startingAtHeader = false
for _, hdr in ipairs(headers) do
if hdr.endPos == start then
startingAtHeader = true
break
end
end
-- Calculate available space for content
local needsPrepend = not startingAtHeader and activeHeaderText ~= nil
local overhead = needsPrepend and (#activeHeaderText + 1) or 0
local availableSpace = maxSize - overhead
local target = start + availableSpace - 1
-- If remaining content fits in one chunk
if target >= len then
local chunk = remainingContent:sub(start, len)
table.insert(chunks, {header = needsPrepend and activeHeaderText or nil, content = chunk})
break
end
-- Greedy approach: fill buffer completely
-- Search backward from target for a newline to split cleanly
local splitPos = target
while splitPos > start and remainingContent:byte(splitPos) ~= 10 do
splitPos = splitPos - 1
end
-- Fallback if no newline found in buffer (force split at target)
if splitPos <= start then
splitPos = target
end
local chunk = remainingContent:sub(start, splitPos)
table.insert(chunks, {header = needsPrepend and activeHeaderText or nil, content = chunk})
start = splitPos + 1
end
return chunks
end
---Sends content in chunks with optional header
---If content starts with header (---<headerline>---), it's automatically repeated in each chunk
---@param content string
---@param maxSizekB number|nil Default 200KB
function SendChunked(content, maxSizekB)
if not content or #content == 0 then
error("Missing or empty content")
end
local maxSize = (maxSizekB or 200) * 1024
local initialHeader = ExtractHeader(content)
if initialHeader and (#initialHeader + 1) > maxSize then
error("Header itself exceeds maximum chunk size of " .. (maxSize / 1024) .. "kB")
end
local chunks = ChunkContent(content, maxSizekB)
for _, chunk in ipairs(chunks) do
local toSend = ""
if chunk.header then
toSend = chunk.header .. "\n"
end
toSend = toSend .. chunk.content
SendUDP(toSend)
end
end