Debugging

Logging

Write log lines from a Hono route with c.get('log') - three levels, stored in the database and printed to the server console at the same time.

Every VitNode request carries a logger. Call it and you get two things at once: a row in the core_logs table, readable in the AdminCP Debug Panel, and a coloured line on the server's stdout. No setup, no adapter, no configuration.

Quick start

c.get('log') is available in any handler that has a Hono context - a route, a middleware, a cron job, a queue task, an event listener:

src/api/modules/orders/routes/refund.route.ts
import { buildRoute } from '@vitnode/core/api/lib/route'

import { CONFIG_PLUGIN } from '@/const'

export const refundOrderRoute = buildRoute({
  pluginId: CONFIG_PLUGIN.pluginId,
  route: { method: 'post', path: '/refund' },
  handler: async (c) => {
    await c.get('log').warn('Refund requested with no matching payment') 

    return c.json({ ok: true })
  },
})

Open the Debug Panel from the AdminCP avatar menu and the line is in System Logs, with the plugin, the path, the status code and the signed-in user already attached.

The three levels

There is no filtering: all three are always written, to both destinations. The level is a label for whoever reads the log later, and it decides the badge colour in the AdminCP and which console method the server uses.

await c.get('log').debug('Search index rebuilt in 412ms')
await c.get('log').warn('Storage adapter returned no public URL')
await c.get('log').error(`Payment webhook rejected: ${reason}`)
LevelConsoleUse it for
debugconsole.debugSomething you want to see once while you work out what happened.
warnconsole.warnA recoverable surprise - a fallback taken, a retry, a skipped step.
errorconsole.errorSomething failed and somebody has to know.

What every line records

You pass a string. The logger reads the rest off the request, so a log row is already an audit record:

ColumnWhere it comes from
contentYour message.
typeThe method you called.
pluginIdThe pluginId the route was built with - core when there is no plugin on the context.
methodThe request method, upper-cased.
pathThe request path.
statusCodec.res.status at the moment you call it - see the gotcha below.
ipAddressThe request's forwarded IP, or 127.0.0.1 when no proxy header is present.
userAgentThe User-Agent header.
userIdThe signed-in user, or null for a guest.
createdAtNow.

That is also why you should not build any of it into the message yourself: the Debug Panel's detail dialog shows every column, and a message that repeats the path just makes the table harder to scan.

Read it back

Three ways, in descending order of convenience:

  1. The Debug Panel at /admin/core/debug - sortable by type, plugin or date, with a detail dialog per row.
  2. The server console - the same lines, live, prefixed [VitNode] Error (@your/plugin):.
  3. The API, if you want to build your own view: GET /api/@vitnode/core/admin/debug/logs, cursor-paginated, orderBy one of type, createdAt or pluginId, gated on the debug.can_view staff permission.

Gotchas

Nothing prunes core_logs

There is no retention policy and no cleanup job - the hourly clean cron removes expired sessions and tokens, and never touches logs. Every call is an INSERT that stays. So log events, not requests: a debug line in a handler that runs on every page view will outgrow the rest of your database.

Await it

All three methods are async because they hit the database. A floating promise can lose the row if the response ends first, and an insert that fails becomes an unhandled rejection instead of an error you can see. await every call.

The status code is the status so far

statusCode is read off c.res when you log, and a Hono response you have not built yet reads as 200. A line written before return c.json(…, 400) records 200. If the status is the interesting part, log after you know it, or put it in the message.

The plugin column follows the route, not the caller

It comes from the pluginId passed to buildRoute, so a plugin's cron job - which runs inside core's cron endpoint - is logged as @vitnode/core. Name your plugin in the message when the attribution matters.

Unhandled errors are logged for you

Anything a handler throws that is not an HTTPException is caught by the API's error handler, written here as Unhandled error: …, and answered to the browser as a bare Internal Server Error in production. You do not need a try/catch just to get a stack into the log.

Next