GenerateSaaS

Performance Monitoring

Log every API request's method, path, status, duration, and cache outcome via a Hono timing middleware, gated by config.performanceMonitor.

A Hono middleware (packages/api/src/middleware/performance.ts) times every request and logs one line through the @repo/runtime logger, gated by config.performanceMonitor.enabled. This is the local logging layer - everything stays in your own stdout; to ship exceptions to a third party see error reporting, an independent flag that can run alongside it.

What you get

One line per request - METHOD /path STATUS durationMs <cache-tag> - with wall-clock duration from performance.now(), rounded to ms.

Cache tagMeaning
(empty)Served fresh (the handler ran)
cachedReturned from the Redis response cache (c.set("cached", true))
etag304 Not Modified - client revalidated, no body sent
GET /api/posts 200 12ms cached
POST /api/posts 201 84ms
GET /api/posts 304 3ms etag

Configuration

config.performanceMonitor is { enabled: boolean }. The middleware registers when the value is not false, so a missing key still logs.

packages/config/src/index.ts
performanceMonitor: {
  enabled: true
}
KeyTypeDefaultDescription
enabledbooleantrueWhen false, the middleware is not mounted and the API logs no request lines.

The fullstack target mounts the API inside a host framework that already logs requests natively, so the CLI writes enabled: false there to avoid double-logging. Standalone backend targets default to true.

How it works

The middleware is the first app.use in packages/api/src/index.ts, registered only when the flag is on:

packages/api/src/index.ts
if (config.performanceMonitor?.enabled !== false) {
  app.use(createPerformanceMonitor(requestLogSkips));
}

createPerformanceMonitor takes predicates naming requests to serve without a line - high-frequency machine traffic a human never reads - and returns the middleware; pass none to log everything. It records start before await next() and computes the duration after, so the timing covers all downstream middleware (CORS, rate limiting, cache, handler).

Reading the logs

Output goes to the @repo/runtime logger (consola), which writes to stdout: your terminal in dev, your host's log stream in production. There are no environment variables for this feature; verbosity follows the logger's level, which is higher in development (NODE_ENV).

Frequently asked questions

Does this add a /metrics endpoint or a dashboard? No. It is a structured log line per request, not a scrape target or UI. Pipe stdout to your platform's log aggregator.

Why is the cache tag empty on most requests? The tag appears only on a cache hit (cached) or a revalidation (etag). Fresh handler responses log no tag. See Caching.

Will it log requests the rate limiter rejects? Yes. It wraps the entire app, so a 429 is timed and logged like any other response.

On this page