Agora
Packages

Pulse health rollup

Pulse is Telescope's aggregated "at a glance" health rollup — throughput, request error rate and latency percentiles, slowest entries, slow route/outgoing/job hotspots, N+1 suspects, top exception families, cache hit rate, and load-by-user — computed on demand from stored entries and served at <path>/api/metrics/pulse, from the headless getHealth API, and via the MCP get_health tool.

Individual entries answer "what happened on this request?". Pulse answers "how is the app doing right now?" — a single aggregated snapshot over a trailing window: throughput, the request error rate and latency percentiles, the slowest entries, consistently-slow route/outgoing/job hotspots, N+1 query suspects, the top exception families, the cache hit ratio, and load-by-user. It is computed on demand from already-stored entries (a single windowed store scan), so there is no extra write cost and nothing to keep in sync.

Pulse is part of the core — it is configured on config/telescope.ts and read three ways:

  • the headless TelescopeService.getHealth() API;
  • the dashboard JSON route GET <path>/api/metrics/pulse (served by the UI provider);
  • the MCP get_health tool.

Configuration

config/telescope.ts
import { defineConfig } from '@adonis-agora/telescope'

export default defineConfig({
  pulse: {
    enabled: true,
    windowMs: 3_600_000,   // trailing 1h
    topN: 5,
    buckets: 60,
    slowRouteMs: 1000,
    // cards: ['throughput', 'requests', 'exceptions', 'nPlusOne'],  // trim the payload
  },
})
KeyDefaultDescription
enabledtrueMaster switch for the rollup and its <path>/api/metrics/pulse route.
windowMs3_600_000Trailing window (ms) the rollup aggregates over (1h).
topN5How many rows each top-N list returns.
buckets60Throughput bucket count (clamped 1–500).
slowRouteMs1000Min p99 (ms) for a route/outgoing family to count as a slow hotspot.
cardsall cardsWhich cards to compute. counts and the window meta are always present.

The togglable cards are throughput, requests, cache, slowest, slowRoutes, slowOutgoing, slowJobs, exceptions, nPlusOne, and loadByUser. Omit cards for all of them; restrict it to trim the payload on a busy host.

The headless API

TelescopeService.getHealth(query?) builds the snapshot; query.windowMs overrides the configured default for a single call.

import { TelescopeService } from '@adonis-agora/telescope'

const telescope = await app.container.make(TelescopeService)
const health = await telescope.getHealth({ windowMs: 15 * 60_000 })   // last 15m
// → { windowStart, windowEnd, windowMs, counts, throughput, requests,
//     cache?, slowest, slowRoutes, slowOutgoing, slowJobs,
//     topExceptions, nPlusOne, loadByUser, scanned, truncated }

The PulseSummary shape:

interface PulseSummary {
  windowStart: string
  windowEnd: string
  windowMs: number
  counts: Record<string, number>          // per-type entry counts in the window
  throughput: PulseThroughput             // total, perMinute, per-bucket over time
  requests: PulseRequestHealth            // total, errorRate, status breakdown, latency?
  cache?: CacheStats                      // present when the cache card is on + cache entries exist
  slowest: PulseSlowEntry[]               // slowest entries across all types
  slowRoutes: PulseHotspot[]              // consistently-slow request families by p99
  slowOutgoing: PulseHotspot[]            // slow outbound HTTP-client families
  slowJobs: PulseHotspot[]                // slow queue/job families
  topExceptions: PulseExceptionGroup[]    // grouped exception families (server + browser)
  nPlusOne: PulseNPlusOne[]               // N+1 query loops aggregated across traces
  loadByUser: PulseUserLoad[]             // share of load by `user:<id>` tag
  scanned: number                         // entries scanned to build this summary
  truncated: boolean                      // whether the scan hit its cap
}

topExceptions spans both exception (server) and client_exception (browser-reported) entries — a browser error counts as an error. Until 0.12.0 only the server type was classified here, so a front-end-only incident showed up as "Recent failures: no exceptions" on the overview while the alert poller, which has always read both, was paging on it.

requests.errorRate is a different number and stays request-only: it is the share of requests answered 4xx/5xx, derived from the status breakdown. (If you are on a version before 0.12.0 and that rate reads 0% no matter what, see the response-status note in capture — Adonis exposes getStatus(), not statusCode, and the watcher used to read the wrong one.)

The rollup reuses the existing metrics primitives rather than re-deriving anything: summarizeStats (status breakdown, cache hit ratio, exception groups), bucketTimeseries (throughput), percentile (slow-hotspot p99), and detectNPlusOne (N+1 loops) — so a Pulse number means exactly what the corresponding dashboard view shows. The scan is capped (50,000 entries) and surfaces truncated: true if it hit the cap.

The dashboard route

When both the UI and Pulse are enabled, the UI provider registers:

GET <path>/api/metrics/pulse?windowMs=<ms>

behind the dashboard auth guard, returning the same PulseSummary. The windowMs query param overrides the configured window for that request. The bundled dashboard SPA renders it as the Pulse section; the MCP get_health tool returns it to a coding agent.

Hotspots vs. slowest

slowest is a flat list of the individual slowest entries. The slow* hotspots are different: they group by family (familyHash) and rank by p99, gated by a minimum p99 (slowRouteMs, default 1000ms) so a quiet host's fastest-of-the-slow route (e.g. /health at 18ms) is never a false alarm — a family only surfaces once it is consistently slow.

On this page