JG Docs
Mechanic

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.lua to choose where each logging category is sent.
  • config/config.webhooks.lua to add Discord webhook URLs.

Logging Categories

Logging is split into three core routes:

RouteUsed for
LOGDebug logs (can keep this off unless running into issues or asked to by support)
EVENTNormal audit logs, such as invoices, orders, tuning, shops, repairs, nitrous, and duty changes
SECURITYDenied 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:

DestinationDescription
consolePrints to the server console
ox_lib

Sends to lib.logger

Learn more: ox_lib logger documentation

discord

Sends to Discord webhooks from config.webhooks.lua

(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 = false

Discord 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.routing sends specific businesses or events to different channels.
  • Webhooks.DiscordSettings.embed controls 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/config.lua
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:

config/config.webhooks.lua
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-mechanic

Trigger 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:

config/config.webhooks.lua
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:

config/config.webhooks.lua
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:

config/config.webhooks.lua
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:

  1. A webhookUrl supplied by a server-side log entry.
  2. The matching businessEvents entry.
  3. The matching businesses entry.
  4. The matching events entry.
  5. The matching category URL, such as Webhooks.Invoices.
  6. 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

config/config.webhooks.lua
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

config/config.webhooks.lua
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:

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"
},
  • showPlayer adds the player name and server ID when a player triggered the event.
  • showLogDetails adds the log type and category to the footer.
  • fieldsInline = false gives every event value a full-width row. Set it to true for a compact grid.
  • timestamp adds the time Discord received the log.
  • Leave footerText and footerIconUrl empty 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:

config/config.webhooks.lua
Webhooks.DiscordSettings.customise = function(payload, entry, context)
  payload.embeds[1].thumbnail = {
    url = "https://example.com/server-logo.png"
  }

  return payload
end

The 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:

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:

config/config.webhooks.lua
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:

config/config.webhooks.lua
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

Last updated 29 October 2026

On this page