301 lines
		
	
	
	
		
			9.3 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			301 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 © 2025 Linden <https://github.com/thelindat>
 | |
| ]]
 | |
| 
 | |
| 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
 | 
