Files
ox_lib/imports/logger/server.lua
Linden fb2225e373 chore: add copyright notices to each file
Some people accidentally, mistakenly, and without any malicious intent forget to attribute code from this resource to its source. This change will hopefully assist people, so that they do not make that mistake again.
2025-03-18 18:46:09 +11:00

302 lines
9.3 KiB
Lua

--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright (c) 2025 Linden <https://github.com/thelindat/fivem>
]]
local service = GetConvar('ox:logger', 'datadog')
local buffer
local bufferSize = 0
local function removeColorCodes(str)
-- replace ^[0-9] with nothing
str = string.gsub(str, "%^%d", "")
-- replace ^#[0-9A-F]{3,6} with nothing
str = string.gsub(str, "%^#[%dA-Fa-f]+", "")
-- replace ~[a-z]~ with nothing
str = string.gsub(str, "~[%a]~", "")
return str
end
local hostname = removeColorCodes(GetConvar('ox:logger:hostname', GetConvar('sv_projectName', 'fxserver')))
local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local function base64encode(data)
return ((data:gsub(".", function(x)
local r, b = "", x:byte()
for i = 8, 1, -1 do
r = r .. (b % 2 ^ i - b % 2 ^ (i - 1) > 0 and "1" or "0")
end
return r;
end) .. "0000"):gsub("%d%d%d?%d?%d?%d?", function(x)
if (#x < 6) then
return ""
end
local c = 0
for i = 1, 6 do
c = c + (x:sub(i, i) == "1" and 2 ^ (6 - i) or 0)
end
return b:sub(c + 1, c + 1)
end) .. ({"", "==", "="})[#data % 3 + 1])
end
local function getAuthorizationHeader(user, password)
return "Basic " .. base64encode(user .. ":" .. password)
end
local function badResponse(endpoint, status, response)
warn(('unable to submit logs to %s (status: %s)\n%s'):format(endpoint, status, json.encode(response, { indent = true })))
end
local playerData = {}
AddEventHandler('playerDropped', function()
playerData[source] = nil
end)
local function formatTags(source, tags)
if type(source) == 'number' and source > 0 then
local data = playerData[source]
if not data then
local _data = {
('username:%s'):format(GetPlayerName(source))
}
local num = 1
---@cast source string
for i = 0, GetNumPlayerIdentifiers(source) - 1 do
local identifier = GetPlayerIdentifier(source, i)
if not identifier:find('ip') then
num += 1
_data[num] = identifier
end
end
data = table.concat(_data, ',')
playerData[source] = data
end
tags = tags and ('%s,%s'):format(tags, data) or data
end
return tags
end
if service == 'fivemanage' then
local key = GetConvar('fivemanage:key', '')
if key ~= '' then
local endpoint = 'https://api.fivemanage.com/api/logs/batch'
local headers = {
['Content-Type'] = 'application/json',
['Authorization'] = key,
['User-Agent'] = 'ox_lib'
}
function lib.logger(source, event, message, ...)
if not buffer then
buffer = {}
SetTimeout(500, function()
PerformHttpRequest(endpoint, function(status, _, _, response)
if status ~= 200 then
if type(response) == 'string' then
response = json.decode(response) or response
badResponse(endpoint, status, response)
end
end
end, 'POST', json.encode(buffer), headers)
buffer = nil
bufferSize = 0
end)
end
bufferSize += 1
buffer[bufferSize] = {
level = "info",
message = message,
resource = cache.resource,
metadata = {
hostname = hostname,
service = event,
source = source,
tags = formatTags(source, ... and string.strjoin(',', string.tostringall(...)) or nil),
}
}
end
end
end
if service == 'datadog' then
local key = GetConvar('datadog:key', ''):gsub("[\'\"]", '')
if key ~= '' then
local endpoint = ('https://http-intake.logs.%s/api/v2/logs'):format(GetConvar('datadog:site', 'datadoghq.com'))
local headers = {
['Content-Type'] = 'application/json',
['DD-API-KEY'] = key,
}
function lib.logger(source, event, message, ...)
if not buffer then
buffer = {}
SetTimeout(500, function()
PerformHttpRequest(endpoint, function(status, _, _, response)
if status ~= 202 then
if type(response) == 'string' then
response = json.decode(response:sub(10)) or response
badResponse(endpoint, status, type(response) == 'table' and response.errors[1] or response)
end
end
end, 'POST', json.encode(buffer), headers)
buffer = nil
bufferSize = 0
end)
end
bufferSize += 1
buffer[bufferSize] = {
hostname = hostname,
service = event,
message = message,
resource = cache.resource,
ddsource = tostring(source),
ddtags = formatTags(source, ... and string.strjoin(',', string.tostringall(...)) or nil),
}
end
end
end
if service == 'loki' then
local lokiUser = GetConvar('loki:user', '')
local lokiPassword = GetConvar('loki:password', GetConvar('loki:key', ''))
local lokiEndpoint = GetConvar('loki:endpoint', '')
local lokiTenant = GetConvar('loki:tenant', '')
local startingPattern = '^http[s]?://'
local headers = {
['Content-Type'] = 'application/json'
}
if lokiUser ~= '' then
headers['Authorization'] = getAuthorizationHeader(lokiUser, lokiPassword)
end
if lokiTenant ~= '' then
headers['X-Scope-OrgID'] = lokiTenant
end
if not lokiEndpoint:find(startingPattern) then
lokiEndpoint = 'https://' .. lokiEndpoint
end
local endpoint = ('%s/loki/api/v1/push'):format(lokiEndpoint)
-- Converts a string of comma seperated kvp string to a table of kvps
-- example `discord:blahblah,fivem:blahblah,license:blahblah` -> `{discord="blahblah",fivem="blahblah",license="blahblah"}`
local function convertDDTagsToKVP(tags)
if not tags or type(tags) ~= 'string' then
return {}
end
local tempTable = { string.strsplit(',', tags) } -- outputs a number index table wth k:v strings as values
local bTable = table.create(0, #tempTable) -- buffer table
-- Loop through table and grab only values
for _, v in pairs(tempTable) do
local key, value = string.strsplit(':', v) -- splits string on ':' character
bTable[key] = value
end
return bTable -- Return the new table of kvps
end
function lib.logger(source, event, message, ...)
if not buffer then
buffer = {}
SetTimeout(500, function()
-- Strip string keys from buffer
local tempBuffer = {}
for _,v in pairs(buffer) do
tempBuffer[#tempBuffer+1] = v
end
local postBody = json.encode({streams = tempBuffer})
PerformHttpRequest(endpoint, function(status, _, _, _)
if status ~= 204 then
badResponse(endpoint, status, ("%s"):format(status, postBody))
end
end, 'POST', postBody, headers)
buffer = nil
end)
end
-- Generates a nanosecond unix timestamp
---@diagnostic disable-next-line: param-type-mismatch
local timestamp = ('%s000000000'):format(os.time(os.date('*t')))
-- Initializes values table with the message
local values = {message = message}
-- Format the args into strings
local tags = formatTags(source, ... and string.strjoin(',', string.tostringall(...)) or nil)
local tagsTable = convertDDTagsToKVP(tags)
-- Concatenates tags kvp table to the values table
for k,v in pairs(tagsTable) do
values[k] = v -- Store the tags in the values table ready for logging
end
-- initialise stream payload
local payload = {
stream = {
server = hostname,
resource = cache.resource,
event = event
},
values = {
{
timestamp,
json.encode(values)
}
}
}
-- Safety check incase it throws index issue
if not buffer then
buffer = {}
end
-- Checks if the event exists in the buffer and adds to the values if found
-- else initialises the stream
if not buffer[event] then
buffer[event] = payload
else
local lastIndex = #buffer[event].values
lastIndex += 1
buffer[event].values[lastIndex] = {
timestamp,
json.encode(values)
}
end
end
end
return lib.logger