Debug & Event Logging
Available in v1.7.0 and newer.
JG Mechanic now uses a central logging system. You only need to edit:
config/config.luato choose where each logging category is sent.config/config.webhooks.luato add Discord webhook URLs.
Logging Categories
Logging is split into three core routes:
| Route | Used for |
|---|---|
LOG | Debug logs (can keep this off unless running into issues or asked to by support) |
EVENT | Normal audit logs, such as invoices, orders, tuning, shops, repairs, nitrous, and duty changes |
SECURITY | Denied actions, invalid values, suspicious requests, and vehicle data warnings |
Configure Destinations
Set destinations in config/config.lua:
Config.Logs = {
LOG = {
enabled = true,
destinations = { "console" }
},
EVENT = {
enabled = true,
destinations = { "ox_lib" }
},
SECURITY = {
enabled = true,
destinations = { "console", "ox_lib" }
}
}Supported destinations are:
| Destination | Description |
|---|---|
console | Prints to the server console |
ox_lib | Sends to |
discord | Sends to Discord webhooks from (Not recommended) |
To disable a route:
Config.Logs = {
...
LOG = {
enabled = false,
destinations = ...
}
}Common Examples
Use ox_lib for normal events and security logs:
Config.Logs = {
LOG = {
enabled = true,
destinations = { "console" }
},
EVENT = {
enabled = true,
destinations = { "ox_lib" }
},
SECURITY = {
enabled = true,
destinations = { "ox_lib" }
}
}Use Discord for events, but keep security warnings in console and ox_lib:
Config.Logs = {
LOG = {
enabled = true,
destinations = { "console" }
},
EVENT = {
enabled = true,
destinations = { "discord" }
},
SECURITY = {
enabled = true,
destinations = { "console", "ox_lib" }
}
}Send events to both Discord and ox_lib:
Config.Logs = {
LOG = {
enabled = true,
destinations = { "console" }
},
EVENT = {
enabled = true,
destinations = { "discord", "ox_lib" }
},
SECURITY = {
enabled = true,
destinations = { "console", "ox_lib" }
}
}Disable debug output:
Config.Logs = {
LOG = {
enabled = false,
destinations = { "console" }
},
EVENT = {
enabled = true,
destinations = { "ox_lib" }
},
SECURITY = {
enabled = true,
destinations = { "console", "ox_lib" }
}
}Visual debug zone markers are separate from logging:
Config.DebugZones = falseDiscord webhook setup
Business/event routing and layout options require JG Mechanic v1.10.0 or newer. Existing category URLs still work without enabling routing.
Keep webhook URLs in the server-only config/config.webhooks.lua, never the shared config/config.lua.
All Discord webhook settings are in config/config.webhooks.lua.
- Category URLs send a group of events, such as all invoice events, to one channel.
Webhooks.DiscordSettings.routingsends specific businesses or events to different channels.Webhooks.DiscordSettings.embedcontrols the message layout.
Edit the existing Webhooks.DiscordSettings table in the file. Don't add a second table with the same name.
Set up one Discord webhook
Enable Discord logging
Open config/config.lua. Add "discord" to the destinations that you want to send:
Config.Logs = {
LOG = {
enabled = true,
destinations = { "console" }
},
EVENT = {
enabled = true,
destinations = { "discord" }
},
SECURITY = {
enabled = true,
destinations = { "console", "ox_lib", "discord" }
}
}EVENT covers normal activity. SECURITY covers denied or suspicious actions. Routing only changes the Discord destination. It doesn't affect console or ox_lib.
Add the fallback URL
Open config/config.webhooks.lua. Find Webhooks.DiscordSettings, then paste your URL into its existing routing.fallback setting:
Webhooks.DiscordSettings = {
username = "JG Mechanic Webhook",
avatarUrl = "https://forum.cfx.re/user_avatar/forum.cfx.re/jgscripts/288/3621910_2.png",
colors = {
success = 0x2ecc71,
danger = 0xe74c3c,
warning = 0xf1c40f,
debug = 0x3498db,
default = 0xff6700
},
embed = {
showPlayer = true,
showLogDetails = true,
fieldsInline = false,
timestamp = true,
footerText = "JG Mechanic",
footerIconUrl = ""
},
routing = {
fallback = "https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN",
businesses = {},
events = {},
businessEvents = {}
}
}The fallback receives any Discord log that doesn't match another route. Existing category URLs, such as Webhooks.Invoices, take priority over it. Leave the category URLs empty when every unmatched log should use the fallback.
Restart and test
Restart jg-mechanic:
restart jg-mechanicTrigger an event, such as going on duty. Confirm the message appears in the fallback channel.
Route one mechanic business
Add the mechanic's config name to routing.businesses. This is the name used inside Config.MechanicLocations, such as lscustoms:
routing = {
fallback = "https://discord.com/api/webhooks/DEFAULT_ID/DEFAULT_TOKEN",
businesses = {
lscustoms = "https://discord.com/api/webhooks/LSCUSTOMS_ID/LSCUSTOMS_TOKEN"
},
events = {},
businessEvents = {}
}Every event for lscustoms now goes to its webhook. You can also use the framework job name configured for the mechanic. JG Mechanic checks the config name first, then its job name.
Route one event
Add the exact event name to routing.events. Event names are case-sensitive.
This sends every paid invoice to one channel:
routing = {
fallback = "https://discord.com/api/webhooks/DEFAULT_ID/DEFAULT_TOKEN",
businesses = {},
events = {
["Invoices: Invoice Paid"] = "https://discord.com/api/webhooks/INVOICE_ID/INVOICE_TOKEN"
},
businessEvents = {}
}Route one event for one business
Put the event inside the mechanic's routing.businessEvents entry:
routing = {
fallback = "https://discord.com/api/webhooks/DEFAULT_ID/DEFAULT_TOKEN",
businesses = {
lscustoms = "https://discord.com/api/webhooks/LSCUSTOMS_ID/LSCUSTOMS_TOKEN"
},
events = {
["Invoices: Invoice Paid"] = "https://discord.com/api/webhooks/ALL_INVOICES_ID/ALL_INVOICES_TOKEN"
},
businessEvents = {
lscustoms = {
["Invoices: Invoice Paid"] = "https://discord.com/api/webhooks/LSCUSTOMS_INVOICES_ID/LSCUSTOMS_INVOICES_TOKEN"
}
}
}A paid invoice for lscustoms uses the combined route. Other lscustoms events use the business route. Paid invoices for other mechanics use the event route.
Routing order
JG Mechanic uses the first matching URL in this order:
- A
webhookUrlsupplied by a server-side log entry. - The matching
businessEventsentry. - The matching
businessesentry. - The matching
eventsentry. - The matching category URL, such as
Webhooks.Invoices. routing.fallback.
Blank or missing entries are skipped. If no URL matches, that Discord log isn't sent. Other configured destinations still receive it.
Available event names
Copy event names exactly, including spaces, hyphens and capital letters. Normal events require "discord" in Config.Logs.EVENT.destinations. Denied and suspicious events require it in Config.Logs.SECURITY.destinations.
Normal events
events = {
-- Admin
-- ["Admin: Mechanic Data Deleted"] = "",
-- ["Admin: Mechanic Owner Updated"] = "",
-- Duty
-- ["Duty: Status Changed"] = "",
-- Invoices
-- ["Invoices: Invoice Sent"] = "",
-- ["Invoices: Invoice Re-sent"] = "",
-- ["Invoices: Invoice Deleted"] = "",
-- ["Invoices: Invoice Paid"] = "",
-- Mechanic management and employees
-- ["Mechanic: Employee Hired"] = "",
-- ["Mechanic: Employee Fired"] = "",
-- ["Mechanic: Employee Updated"] = "",
-- ["Mechanic: Money Deposited"] = "",
-- ["Mechanic: Money Withdraw"] = "",
-- ["Mechanic: Name Updated"] = "",
-- Nitrous
-- ["Nitrous: Bottle Installed"] = "",
-- ["Nitrous: Bottle Refilled"] = "",
-- Orders
-- ["Orders: Order Placed"] = "",
-- ["Orders: Order Marked as Fulfilled"] = "",
-- ["Orders: Order Deleted"] = "",
-- Repairs and servicing
-- ["Repair: Self-Service Vehicle Repaired"] = "",
-- ["Self-Service Tuning Completed"] = "",
-- ["Servicing: Vehicle Serviced"] = "",
-- Shop
-- ["Shop: Item Purchased"] = "",
-- Tuning
-- ["Tuning: Tune Applied via Tablet"] = "",
-- ["Tuning: Tune Removed"] = ""
}Security events
events = {
-- Admin
-- ["Admin: Full Repair Command Denied"] = "",
-- ["Admin: Mechanic Admin Command Denied"] = "",
-- ["Admin: Mechanic Data Access Denied"] = "",
-- ["Admin: Mechanic Data Deletion Denied"] = "",
-- ["Admin: Mechanic Owner Update Denied"] = "",
-- Duty
-- ["Duty: Toggle Denied"] = "",
-- Invoices
-- ["Invoices: Send Denied"] = "",
-- ["Invoices: Resend Denied"] = "",
-- ["Invoices: Save Denied"] = "",
-- ["Invoices: Delete Denied"] = "",
-- ["Invoices: Invalid Payment Method"] = "",
-- Mechanic management and employees
-- ["Mechanic: Deposit Denied"] = "",
-- ["Mechanic: Employee Fire Denied"] = "",
-- ["Mechanic: Employee Hire Denied"] = "",
-- ["Mechanic: Employee Hire Request Denied"] = "",
-- ["Mechanic: Employee Role Update Denied"] = "",
-- ["Mechanic: Invalid Deposit Amount"] = "",
-- ["Mechanic: Invalid Employee Hire Request"] = "",
-- ["Mechanic: Invalid Withdraw Amount"] = "",
-- ["Mechanic: Settings Update Denied"] = "",
-- ["Mechanic: Tablet Settings Save Denied"] = "",
-- ["Mechanic: Withdraw Denied"] = "",
-- Nitrous
-- ["Nitrous: Install Denied"] = "",
-- ["Nitrous: Refill Denied"] = "",
-- Orders
-- ["Orders: Category Install Denied"] = "",
-- ["Orders: Deletion Denied"] = "",
-- ["Orders: Fulfilment Denied"] = "",
-- ["Orders: Installation Payment Denied"] = "",
-- ["Orders: Invalid Commission Amount"] = "",
-- ["Orders: Invalid Refund Amount"] = "",
-- Repairs and servicing
-- ["Repair: Invalid Payment Method"] = "",
-- ["Self-Service: Invalid Payment Method"] = "",
-- ["Self-Service: No Payment Denied"] = "",
-- ["Servicing: Service Payment Denied"] = "",
-- Tuning
-- ["Tuning: Apply Denied"] = "",
-- ["Tuning: Remove Denied"] = "",
-- Vehicle data
-- ["Vehicle Data: Empty Plate Write"] = "",
-- ["Vehicle Data: Invalid Retrieve and Apply"] = "",
-- ["Vehicle Data: Invalid Statebag Mutation"] = "",
-- ["Vehicle Data: Missing Plate for Statebag Mutation"] = "",
-- ["Vehicle Data: Missing Plate for Statebag Mutations"] = ""
}Change the Discord message layout
Edit Webhooks.DiscordSettings in config/config.webhooks.lua:
username = "My Server Logs",
avatarUrl = "https://example.com/avatar.png",
colors = {
success = 0x2ecc71,
danger = 0xe74c3c,
warning = 0xf1c40f,
debug = 0x3498db,
default = 0xff6700
},
embed = {
showPlayer = true,
showLogDetails = true,
fieldsInline = false,
timestamp = true,
footerText = "My Server",
footerIconUrl = "https://example.com/footer-icon.png"
},showPlayeradds the player name and server ID when a player triggered the event.showLogDetailsadds the log type and category to the footer.fieldsInline = falsegives every event value a full-width row. Set it totruefor a compact grid.timestampadds the time Discord received the log.- Leave
footerTextandfooterIconUrlempty to remove them.
The Discord implementation is in the escrow-ignored server/webhooks.lua file. You can read or edit the complete routing and payload code.
For an update-safe payload change, add a customise function after the Webhooks.DiscordSettings table in config/config.webhooks.lua:
Webhooks.DiscordSettings.customise = function(payload, entry, context)
payload.embeds[1].thumbnail = {
url = "https://example.com/server-logo.png"
}
return payload
endThe default formatter truncates long embeds safely. If your hook replaces or extends the payload, keep it within Discord's embed limits.
The function receives the completed Discord payload, the original log entry, and a context table containing category, event, level, logType, player and route.
Migrate from v1.6.6
Copy category URLs
Copy URLs from the old server/sv-webhooks.lua file into the matching category entries at the top of config/config.webhooks.lua:
Webhooks = {
SelfService = "",
Orders = "",
TabletTuning = "",
Servicing = "",
Invoices = "",
Mechanic = "",
Admin = ""
}These entries keep working. You don't need to move them into routing unless you want a more specific rule.
Move event rules
If your old sendWebhook() function chose a URL from its title, add that title to routing.events:
events = {
["Invoices: Invoice Paid"] = "https://discord.com/api/webhooks/INVOICE_ID/INVOICE_TOKEN",
["Orders: Order Placed"] = "https://discord.com/api/webhooks/ORDER_ID/ORDER_TOKEN"
}Move business rules
If your old function chose a URL from a mechanic config name or job, add those URLs to routing.businesses. Use routing.businessEvents when the old rule checked both values:
businesses = {
lscustoms = "https://discord.com/api/webhooks/LSCUSTOMS_ID/LSCUSTOMS_TOKEN"
},
businessEvents = {
lscustoms = {
["Invoices: Invoice Paid"] = "https://discord.com/api/webhooks/LSCUSTOMS_INVOICES_ID/LSCUSTOMS_INVOICES_TOKEN"
}
}Restart and test
Restart jg-mechanic. Trigger one category event, one business event and one combined business-and-event rule. Confirm each message appears in the expected channel.
Troubleshooting
Confirm the matching Config.Logs destination contains "discord". Then check that a category URL or routing.fallback contains the complete Discord webhook URL.
The key in routing.businesses must match the entry name in Config.MechanicLocations or that mechanic's framework job value. Check capitalisation and spelling.
Copy the event name from the list above. Event names are case-sensitive. Check routing.businessEvents and routing.businesses first because they take priority over routing.events.
Last updated 29 October 2026