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:
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}`)| Level | Console | Use it for |
|---|---|---|
debug | console.debug | Something you want to see once while you work out what happened. |
warn | console.warn | A recoverable surprise - a fallback taken, a retry, a skipped step. |
error | console.error | Something 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:
| Column | Where it comes from |
|---|---|
content | Your message. |
type | The method you called. |
pluginId | The pluginId the route was built with - core when there is no plugin on the context. |
method | The request method, upper-cased. |
path | The request path. |
statusCode | c.res.status at the moment you call it - see the gotcha below. |
ipAddress | The request's forwarded IP, or 127.0.0.1 when no proxy header is present. |
userAgent | The User-Agent header. |
userId | The signed-in user, or null for a guest. |
createdAt | Now. |
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:
- The Debug Panel at
/admin/core/debug- sortable by type, plugin or date, with a detail dialog per row. - The server console - the same lines, live, prefixed
[VitNode] Error (@your/plugin):. - The API, if you want to build your own view:
GET /api/@vitnode/core/admin/debug/logs, cursor-paginated,orderByone oftype,createdAtorpluginId, gated on thedebug.can_viewstaff 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.