Logging
Structured logging, request ID tracing, canonical log lines, child loggers, and log queries
Arcway provides structured logging built on Pino for high performance. Every handler, listener, and job receives a logger via ctx.log with the request ID automatically attached.
Basic Usage
export const POST = {
handler: async (ctx) => {
const { log } = ctx;
log.info('User created', { userId: 123 });
log.warn('Slow query detected', { durationMs: 500 });
log.error('Failed to send email', { error: err.message });
log.debug('Debug info', { details });
},
};Log levels: debug, info, warn, error
Structured Data
All log methods accept a message string and an optional data object:
export const POST = {
handler: async (ctx) => {
const { log } = ctx;
log.info('Order placed', {
orderId: order.id,
userId: ctx.req.session.userId,
total: order.total,
});
},
};In production, logs are output as JSON lines for machine parsing. In development, they're pretty-printed with colors.
Request ID Tracing
Every HTTP request gets a unique ID (UUID) that flows through the entire request lifecycle. If the client sends an X-Request-Id header, Arcway uses it; otherwise a new one is generated.
The request ID is:
- Automatically included in all log calls
- Returned in the
X-Request-Idresponse header - Available in handlers via
ctx.req.id - Queryable in the log ring buffer
// All logs from this handler automatically include the requestId
export const GET = {
handler: async (ctx) => {
const { log, db } = ctx;
log.info('Fetching user'); // { requestId: "a1b2c3d4-..." }
const user = await db('users').where({ id: ctx.req.query.id }).first();
log.info('User found', { userId: user.id });
return { data: user };
},
};Canonical Log Lines
Every completed request emits a single structured info-level log entry (message: "request") that summarizes everything about that request in one line. This is the most important log entry for monitoring and debugging.
{
"requestId": "a1b2c3d4-...",
"method": "POST",
"path": "/api/users",
"route": "/api/users",
"status": 201,
"durationMs": 42,
"dbQueries": 3,
"dbDurationMs": 12,
"middleware": ["auth", "rateLimit"],
"userId": 7
}| Field | Description |
|---|---|
method | HTTP method |
path | Request path |
route | Matched route pattern |
status | HTTP status code |
durationMs | Total request duration in milliseconds |
dbQueries | Number of database queries executed during the request |
dbDurationMs | Total time spent in database queries (ms) |
middleware | Array of middleware names that ran |
userId | User ID from session (included if session.userId or session.id exists) |
error | Error message (only present on 5xx responses) |
Canonical log lines are emitted automatically — no configuration needed. They're useful for:
- Monitoring — alert on high
durationMsor error rates - Debugging — find the exact request that failed and see its full context
- Performance — identify requests with high
dbQueriesordbDurationMs - Auditing — track which user made which request
Enriching Canonical Logs with log.addContext()
Use log.addContext() to attach custom fields to the canonical log line without emitting a new log entry. This is useful for enriching the request trace with business-specific data:
export const POST = {
handler: async (ctx) => {
const { log, db } = ctx;
// These fields will appear in the canonical log line at end of request
log.addContext({ orderId: 'ord_123', source: 'web' });
const items = await db('items').where({ orderId: 'ord_123' });
log.addContext({ itemCount: items.length });
await db('orders').where({ id: 'ord_123' }).update({ status: 'shipped' });
return { data: { ok: true } };
},
};The canonical log line for this request would include:
{
"method": "POST",
"path": "/api/orders/ship",
"status": 200,
"durationMs": 15,
"dbQueries": 2,
"dbDurationMs": 8,
"orderId": "ord_123",
"source": "web",
"itemCount": 3
}You can call log.addContext() multiple times — fields are merged. Later calls overwrite earlier values for the same key.
Child Loggers
Create a logger with extra fields that are automatically included in every log entry:
const { log } = ctx;
const userLog = log.extend({ userId: 123, action: 'signup' });
userLog.info('Starting signup flow'); // Includes userId and action
userLog.info('Email verified'); // Same fields automatically includedChild loggers are immutable — extend() returns a new logger without modifying the parent. You can chain extends:
const { log } = ctx;
const baseLog = log.extend({ module: 'billing' });
const txLog = baseLog.extend({ transactionId: 'tx_abc' });
txLog.info('Payment processed'); // Has requestId, module, and transactionIdLog Queries
Arcway maintains an in-memory ring buffer of recent logs. You can query it programmatically for debugging:
const { log } = ctx;
// Query recent logs
const logs = log.query({
level: 'error',
path: '/api/users',
since: '2026-01-01T00:00:00Z',
limit: 50,
});
// Shorthand for recent errors
const errors = log.errors(10); // Last 10 errorsQuery Filters
| Filter | Type | Description |
|---|---|---|
level | string | Log level (debug, info, warn, error) |
logger | string | Logger name |
requestId | string | Exact request ID match |
message | string | Exact message match |
method | string | HTTP method (case-insensitive) |
path | string | URL path substring match |
status | number | HTTP status code |
minDurationMs | number | Minimum request duration |
eventName | string | Event name |
since | string | ISO timestamp — only entries after this time |
limit | number | Maximum number of results |
Querying Canonical Log Lines
Since canonical log lines use the message "request", you can filter specifically for them:
const { log } = ctx;
// Slow requests (over 500ms)
log.query({ message: 'request', minDurationMs: 500 });
// Failed requests
log.query({ message: 'request', status: 500 });
// All requests to a specific endpoint
log.query({ message: 'request', path: '/api/payments', method: 'POST' });
// Requests for a specific user (by request ID from their error report)
log.query({ requestId: 'a1b2c3d4-...' });Configuration
// arcway.config.js
export default {
log: {
level: 'info', // Minimum level to output (default: 'debug' in dev, 'info' in prod)
buffer: 1000, // Ring buffer size for log queries (default: 1000)
pretty: true, // Pretty-print output (default: true in dev, false in prod)
},
};| Option | Default | Description |
|---|---|---|
level | 'debug' (dev) / 'info' (prod) | Minimum log level to output |
buffer | 1000 | Number of log entries to keep in the ring buffer for queries |
pretty | true (dev) / false (prod) | Pretty-print with colors vs JSON lines |
MCP Debug API
The built-in MCP debug tools provide access to logs for AI-powered debugging. When MCP is enabled, the debug API exposes:
- Recent logs — query the ring buffer with filters
- Error summary — quick view of recent errors
- Request tracing — find all logs for a specific request ID
See Internals for MCP debug tool details.
Sessions & Auth
Cookie-based encrypted sessions and crypto utilities for passwords, JWT, and encryption
Plugins & Capabilities
Plugins let you package a slice of functionality — its handlers, jobs, tools, and UI — as a self-contained unit that Arcway discovers, loads, and wires into the rest of the app. Plugins are first-clas