Arcwayv0.3.0

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-Id response 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
}
FieldDescription
methodHTTP method
pathRequest path
routeMatched route pattern
statusHTTP status code
durationMsTotal request duration in milliseconds
dbQueriesNumber of database queries executed during the request
dbDurationMsTotal time spent in database queries (ms)
middlewareArray of middleware names that ran
userIdUser ID from session (included if session.userId or session.id exists)
errorError message (only present on 5xx responses)

Canonical log lines are emitted automatically — no configuration needed. They're useful for:

  • Monitoring — alert on high durationMs or error rates
  • Debugging — find the exact request that failed and see its full context
  • Performance — identify requests with high dbQueries or dbDurationMs
  • 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 included

Child 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 transactionId

Log 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 errors

Query Filters

FilterTypeDescription
levelstringLog level (debug, info, warn, error)
loggerstringLogger name
requestIdstringExact request ID match
messagestringExact message match
methodstringHTTP method (case-insensitive)
pathstringURL path substring match
statusnumberHTTP status code
minDurationMsnumberMinimum request duration
eventNamestringEvent name
sincestringISO timestamp — only entries after this time
limitnumberMaximum 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)
  },
};
OptionDefaultDescription
level'debug' (dev) / 'info' (prod)Minimum log level to output
buffer1000Number of log entries to keep in the ring buffer for queries
prettytrue (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.

On this page