Documentación / Intégration ox_lib

Intégration ox_lib

Captura automática de logs de ox_inventory y otros recursos que usan lib.logger.

Para ver en el panel los logs de ox_inventory (y otros que usan lib.logger) hay que adaptar el logger de ox_lib. No hace falta set ox:logger: el parche detecta si Adictos_WebLogs está iniciado.

Elige tu versión

  • 3.39+ — existe la carpeta imports/logger/providers/ → sección de abajo.
  • Anterior a 3.39 — solo un server.lua → sección siguiente.

ox_lib 3.39 o superior

Sustituye solo /ox_lib/imports/logger/server.lua por este archivo (no toques providers/). Luego reinicia ox_lib y Adictos_WebLogs.

En 3.39+ no basta pegar el bloque al final del oficial: ese archivo hace return anticipado y el parche no cargaría.
--[[
    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 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 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

---@class LogContext
---@field hostname string
---@field formatTags fun(source: any, tags: string?): string?

---@class LogProvider
---@field endpoint string
---@field headers table<string, string>
---@field okStatus number
---@field append fun(buffer: table, source: any, event: string, message: string, ...): nil
---@field encode fun(buffer: table): string
---@field parseError fun(status: number, response: any, body: string): any|nil

local KNOWN = { datadog = true, fivemanage = true, loki = true }
---@type fun(ctx: LogContext): LogProvider?
local providerFactory = KNOWN[service] and lib.require(('imports.logger.providers.%s'):format(service))
local provider = providerFactory and providerFactory({ hostname = hostname, formatTags = formatTags })

-- Sin early-return: el oficial sale con `return lib.logger` si no hay proveedor
-- (Datadog/FiveManage/Loki). Eso impediría el parche de Adictos_WebLogs.
if provider then
    local buffer

    function lib.logger(source, event, message, ...)
        if not buffer then
            buffer = {}

            SetTimeout(500, function()
                local body = provider.encode(buffer)
                buffer = nil

                PerformHttpRequest(provider.endpoint, function(status, _, _, response)
                    if status == provider.okStatus then return end

                    local err = provider.parseError(status, response, body)
                    if err == nil then return end

                    badResponse(provider.endpoint, status, err)
                end, 'POST', body, provider.headers)
            end)
        end

        provider.append(buffer, source, event, message, ...)
    end
end

-- Adictos_WebLogs — integración con el panel web (ver documentación WebLogs)
do
    local upstreamLogger = lib.logger
    local webLogsResource = 'Adictos_WebLogs'
    local webLogsActive = GetResourceState(webLogsResource) == 'started'

    AddEventHandler('onResourceStart', function(name)
        if name == webLogsResource then webLogsActive = true end
    end)

    AddEventHandler('onResourceStop', function(name)
        if name == webLogsResource then webLogsActive = false end
    end)

    function lib.logger(source, event, message, ...)
        if webLogsActive then
            exports.Adictos_WebLogs:OxLibLogRaw({
                source = source,
                identifier = type(source) == 'string' and source or nil,
                event = event,
                message = message,
                tags = { ... },
            })
            return
        end

        if upstreamLogger then
            return upstreamLogger(source, event, message, ...)
        end
    end
end

return lib.logger

ox_lib anterior a 3.39

Pega este bloque justo antes de return lib.logger al final de /ox_lib/imports/logger/server.lua. Reinicia ox_lib y Adictos_WebLogs.

-- Adictos_WebLogs — integración con el panel web (ver documentación WebLogs)
do
    local upstreamLogger = lib.logger
    local webLogsResource = 'Adictos_WebLogs'
    local webLogsActive = GetResourceState(webLogsResource) == 'started'

    AddEventHandler('onResourceStart', function(name)
        if name == webLogsResource then webLogsActive = true end
    end)

    AddEventHandler('onResourceStop', function(name)
        if name == webLogsResource then webLogsActive = false end
    end)

    function lib.logger(source, event, message, ...)
        if webLogsActive then
            exports.Adictos_WebLogs:OxLibLogRaw({
                source = source,
                identifier = type(source) == 'string' and source or nil,
                event = event,
                message = message,
                tags = { ... },
            })
            return
        end

        if upstreamLogger then
            return upstreamLogger(source, event, message, ...)
        end
    end
end

Alternativa: sustituir el archivo completo:

--[[
    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

-- Adictos_WebLogs — integración con el panel web (ver documentación WebLogs)
do
    local upstreamLogger = lib.logger
    local webLogsResource = 'Adictos_WebLogs'
    local webLogsActive = GetResourceState(webLogsResource) == 'started'

    AddEventHandler('onResourceStart', function(name)
        if name == webLogsResource then webLogsActive = true end
    end)

    AddEventHandler('onResourceStop', function(name)
        if name == webLogsResource then webLogsActive = false end
    end)

    function lib.logger(source, event, message, ...)
        if webLogsActive then
            exports.Adictos_WebLogs:OxLibLogRaw({
                source = source,
                identifier = type(source) == 'string' and source or nil,
                event = event,
                message = message,
                tags = { ... },
            })
            return
        end

        if upstreamLogger then
            return upstreamLogger(source, event, message, ...)
        end
    end
end

return lib.logger

server.cfg y panel

ensure oxmysql
ensure ox_lib
ensure ox_inventory
ensure Adictos_WebLogs

ox_lib antes de Adictos_WebLogs. En el panel: Mis servidores → ox_lib → módulo activo.