Introduction
Having server logs in Discord = near real-time monitoring, accessible to your whole staff, no panel needed. Combined with Discord mobile notifications, you know immediately if something goes wrong. This guide shows you how to do it cleanly on your VeryCloud Wisp FiveM.
Prerequisites
- A FiveM server at VeryCloud
- A Discord server where you have admin rights
- Access to Files in Wisp
Step 1: Create the Discord webhook
- Open your Discord server
- Create a channel (e.g.
#fivem-logs) - Right-click → Edit Channel → Integrations → Webhooks
- New Webhook → name it, copy the URL
Keep this URL secret: anyone with it can post in your channel.
Step 2: Choose an approach
Several options:
| Approach | Difficulty | Flexibility |
|---|---|---|
| Existing resource (logs-system, ox_logger) | Easy | Limited to predefined events |
| Custom Lua logger | Medium | Total |
| External bridge (Node.js + RCON) | More complex | Very flexible |
This guide shows option 2 — most versatile.
Step 3: Create the logger resource
In Wisp → Files → /resources/[utils]/discord_logger/:
fxmanifest.lua:
fx_version 'cerulean'
game 'gta5'
author 'VeryCloud'
description 'Discord webhook logger'
version '1.0.0'
server_scripts {
'config.lua',
'logger.lua'
}
config.lua:
Config = {}
Config.Webhooks = {
general = 'https://discord.com/api/webhooks/.../GENERAL',
admin = 'https://discord.com/api/webhooks/.../ADMIN',
economy = 'https://discord.com/api/webhooks/.../ECONOMY',
moderation = 'https://discord.com/api/webhooks/.../MODERATION',
}
Config.BotName = 'VeryCloud Logs'
Config.BotAvatar = 'https://verycloud.fr/static/brand.svg'
logger.lua:
local function send(category, embed)
local url = Config.Webhooks[category]
if not url then return end
PerformHttpRequest(url, function(err, _, _)
if err and err ~= 204 then
print('[discord_logger] HTTP error: ' .. err)
end
end, 'POST', json.encode({
username = Config.BotName,
avatar_url = Config.BotAvatar,
embeds = { embed }
}), { ['Content-Type'] = 'application/json' })
end
exports('log', function(category, title, description, color, fields)
send(category, {
title = title,
description = description,
color = color or 3447003,
fields = fields or {},
timestamp = os.date('!%Y-%m-%dT%H:%M:%SZ'),
footer = { text = GetCurrentResourceName() }
})
end)
Step 4: Add the resource
In /server.cfg:
ensure discord_logger
Restart.
Step 5: Use from other resources
From any resource:
exports.discord_logger:log(
'moderation',
'Player kick',
'Mathys kicked PlayerXYZ',
15158332, -- red
{
{ name = 'Moderator', value = 'Mathys', inline = true },
{ name = 'Player', value = 'PlayerXYZ', inline = true },
{ name = 'Reason', value = 'AFK too long', inline = false }
}
)
Step 6: Hook native FiveM events
Log connections/disconnections automatically:
AddEventHandler('playerConnecting', function(name, _, deferrals)
local id = source
local identifiers = GetPlayerIdentifiers(id)
exports.discord_logger:log('general', 'Connection', name, 3066993, {
{ name = 'Steam', value = identifiers[1] or 'N/A', inline = true },
{ name = 'Player ID', value = tostring(id), inline = true }
})
end)
AddEventHandler('playerDropped', function(reason)
local name = GetPlayerName(source) or 'Unknown'
exports.discord_logger:log('general', 'Disconnect', name, 10038562, {
{ name = 'Reason', value = reason, inline = false }
})
end)
Step 7: Log admin commands
RegisterCommand('kick', function(source, args)
if IsPlayerAceAllowed(source, 'command.kick') then
local target = tonumber(args[1])
local reason = table.concat(args, ' ', 2)
DropPlayer(target, reason)
exports.discord_logger:log('moderation',
'Admin kick',
('%s kicked %s'):format(GetPlayerName(source), GetPlayerName(target)),
15158332,
{ { name = 'Reason', value = reason } }
)
end
end)
Step 8: Rate-limiting
Discord webhook: 30 requests / minute / webhook. Beyond = 429 Too Many Requests.
Batch logs on busy servers:
local queue = {}
local flushInterval = 5
CreateThread(function()
while true do
Wait(flushInterval * 1000)
for category, embeds in pairs(queue) do
if #embeds > 0 then
send(category, { embeds = embeds })
queue[category] = {}
end
end
end
end)
Step 9: Webhook security
- Never commit webhook URL to Git (add
config.luato.gitignore) - Regenerate the URL if leaked (Discord → Edit Webhook → Reset)
- Private channel for staff only
- Never log sensitive data (passwords, full payments, personal data): GDPR
Step 10: Turnkey alternatives
If you don't want to code:
- screenshot-basic + logs-system: free resources that do this
- ox_logger (Overextended): integrated to ox frameworks
- es_extended ships a configurable Discord logging system
For quick setup, use these. For fine control, your custom logger above.
Troubleshooting
No Discord message, no error — wrong webhook URL (paste exactly from Discord). Channel deleted / webhook revoked. 429 Too Many Requests — too many logs per minute → batch or reduce verbosity. Broken embed — Discord limits fields (1024 chars / field, 6000 chars total). Truncate long texts.
Useful commands
# Test a webhook from CLI
curl -X POST -H "Content-Type: application/json" \
-d '{"content":"CLI test"}' \
https://discord.com/api/webhooks/.../...
# Reset a webhook Discord-side
# -> Edit Channel / Integrations / Webhooks / [...] / Reset URL
Conclusion
Discord logger = mandatory for any community FiveM server. 30 minutes to drop in a custom resource and your staff has total real-time visibility. Choose what you log carefully (GDPR), batch on heavy volume, keep webhooks private.
Going further: log levels (info/warn/error), threshold alerting with @here, integration with external APM.


















