Dashboard
Mount the Telescope console — the @adonis-agora/telescope-ui React SPA served under the same prefix and auth guard as the core JSON API + SSE it reads from the headless TelescopeService — browse entries by type, family and trace, and reach the same data programmatically.
The Telescope console is the @adonis-agora/telescope-ui React SPA (pre-built) for browsing
recorded entries. It's served under the same prefix and behind the same
auth guard as the core @adonis-agora/telescope/ui subpath it
reads: the core mounts the JSON API and SSE stream directly onto your app's router — no separate
process, no build step of your own — all served from the headless
TelescopeService; the SPA
package mounts the page on top of it.
Mount it
The core JSON API + SSE is the @adonis-agora/telescope/ui subpath of the one telescope
package — re-run configure and pick UI:
npm i @adonis-agora/telescope
node ace configure @adonis-agora/telescope # pick "UI"configure registers @adonis-agora/telescope/ui_provider, publishes config/telescope_ui.ts,
and mounts the API + SSE routes under the configured prefix against the same live store the
watchers write to. It does not install a page — that's the separate
@adonis-agora/telescope-ui package, and the core configure prompt above doesn't add it for
you:
npm i @adonis-agora/telescope-uiproviders: [
// …
() => import('@adonis-agora/telescope/ui_provider'),
() => import('@adonis-agora/telescope-ui/telescope_ui_dashboard_provider'),
]Register @adonis-agora/telescope-ui/telescope_ui_dashboard_provider after the core
ui_provider. Only once both are registered does http://localhost:3333/telescope serve a
page — with just the core installed, that prefix only exposes <path>/api/*.
The UI needs @adonis-agora/telescope to be enabled and booted — it reads the same store the
core publishes. If the store isn't booted, the provider registers no routes (and
warn-logs why) rather than crashing the host. So the dashboard simply won't exist until
the core is running.
What it shows
The console is a client of the JSON API below and nothing else. Its left sidebar carries ten sections:
- Overview — the triage landing page: error rate, failed jobs, slow routes, recent failures, N+1 hotspots, load by user, and the current retention posture.
- Entries — every captured entry, newest-first, filterable by type and free-text search, with
a Live tail toggle that streams new ones in over SSE. Opening a row shows the full
type-specific
content— the SQL and bindings for a query, the stack for an exception, the payload for a diagnostic. - Traces — every entry recorded under one trace id, so you can replay a whole request's story: the request, its queries, the diagnostics it published, and the exception that ended it, laid out as a waterfall with any N+1 loops flagged.
- Pulse — the health rollup over a selectable window.
- Exceptions — exceptions grouped by class and message, with occurrence counts and a when-it-fired sparkline.
- Queues and Schedules — the live consoles for jobs and scheduled tasks.
- Exports — download a filtered slice of the entries feed as JSON or CSV.
- Profiles — captured CPU flamegraphs.
- Extensions — dashboards contributed by installed extensions.
A Watchers sub-nav below them jumps straight to Entries filtered to one entry type, and ⌘K
(Ctrl-K) opens a command palette over everything. The full tour, including what each section
shows and how the capability-gated views behave, is on the
@adonis-agora/telescope-ui page.
The JSON API
The dashboard is just a client of these endpoints; you can call them directly (curl, a
script, your own front end). They mount under <path>/api and each runs the authorize
guard first.
| Endpoint | Returns |
|---|---|
GET <path>/api/entries | List entries — ?type=, ?tag=, ?traceId=, ?search=, ?before= (ISO), plus ?page= (1-based, default 1) and ?size= (capped at 500, default 50). Drops content for a compact summary. |
GET <path>/api/entries/:id | One entry with its full content, or 404. |
GET <path>/api/trace/:traceId | Every entry under a trace, newest-first. |
GET <path>/api/stats | { count, topFamilies, topTags }. ?limit= caps each top-N; ?type= scopes topFamilies. |
GET <path>/api/retention | The retention posture: whether a pruner is armed, its cutoff and cadence, and which entry types record below 100%. Configuration, not live pruner state. |
GET <path>/api/meta | What this install can do: extension-contributed entryTypes and dashboards, plus ai, profiling and queueManager enabled flags. Always available. |
GET <path>/api/stream | Server-Sent Events: every newly recorded entry, already redacted and post-sampling. Present unless stream is disabled in config/telescope.ts. |
Analytics, all windowed and all reading the same stored entries the tables show:
| Endpoint | Returns |
|---|---|
GET <path>/api/metrics/stats | Latency, throughput and error aggregates over a window. |
GET <path>/api/metrics/timeseries | The same aggregates bucketed for charting. |
GET <path>/api/metrics/traces | Recent traces, newest first — paginated with ?page=/?size=. |
GET <path>/api/metrics/waterfall/:traceId | One trace laid out as a waterfall. |
GET <path>/api/metrics/n-plus-one/:traceId | Repeated-query loops detected on one trace. |
GET <path>/api/metrics/pulse | The Pulse health rollup. Present unless pulse is disabled. |
Actions, each off by default and answering 403 until you turn it on:
| Endpoint | Returns |
|---|---|
POST <path>/api/requests/:id/replay | Re-issues a captured request against the local server — see Request replay. |
POST <path>/api/exceptions/:id/diagnose | An AI diagnosis of one exception as Markdown, cached per family; ?force=true spends a fresh call. Needs ai. |
POST <path>/api/profiles/arm | Arms a CPU capture for the next requests. |
POST <path>/api/queues/live/:queue/jobs/:id/retry | Retries one job in the live queue console. |
POST <path>/api/queues/live/:queue/enqueue | Dispatches a new job from the console. |
And the read-only surfaces the console's newer views bind to:
| Endpoint | Returns |
|---|---|
GET <path>/api/profiles, .../profiles/:id, .../profiles/status | Captured CPU profiles, one flamegraph, and whether profiling is armed. |
GET <path>/api/schedules/live | Registered schedules with their computed next run, joined with their last recorded run. |
GET <path>/api/queues/live, .../queues/live/:queue/jobs/:id | Live queue state and one job's record. |
GET <path>/api/ext/:ext/data/:provider | One extension data provider's result (namespaced by owner). |
Each list entry comes back as an EntrySummary — id, type, familyHash, tags,
traceId, durationMs, sequence, createdAt (ISO), plus a derived one-line summary
(e.g. GET /users/42 → 200, billing:invoice-paid, or an exception message).
curl 'http://localhost:3333/telescope/api/entries?type=exception&size=20&page=1'
curl 'http://localhost:3333/telescope/api/trace/abc123'
curl 'http://localhost:3333/telescope/api/stats?type=diagnostic'Configuration
import { defineConfig } from '@adonis-agora/telescope/ui'
export default defineConfig({
enabled: true, // false → register no routes at all
path: '/telescope', // API at <path>/api/*; the telescope-ui SPA mounts the page at the root
// authorize: (ctx) => {
// const { auth } = ctx as unknown as { auth: { user?: { isAdmin?: boolean } } }
// return auth.user?.isAdmin === true
// },
// credentials: { token: env.get('TELESCOPE_UI_TOKEN') },
})| Key | Default | What it does |
|---|---|---|
enabled | true | Master switch. false registers no routes — the dashboard does not exist. |
path | /telescope | URL prefix. Normalized to a leading slash, no trailing slash. |
authorize | default policy | Access-decision hook. See Dashboard auth. |
credentials | {} | Built-in token / basic gate for the default policy. |
dashboardAuth | unset | Built-in login page + signed session cookie. See Dashboard auth. |
replay | { enabled: false } | Gate for request replay. |
cpuProfiling | { armEnabled: false } | Gate for arming a CPU capture from the console. |
queueActions | { enabled: false } | Gate for the queue console's retry / enqueue actions. |
Change path to something non-obvious (e.g. /__telescope) as defense in depth, but it
is not a substitute for the authorize guard — covered next.
Gating the dashboard
By default the console is open outside production and denied in production unless you
configure a credential or an authorize hook. The whole gate — token, HTTP Basic, a
delegate-to-your-app-auth hook, and the 401-vs-403 behaviour — has its own page:
See Dashboard auth.
Build your own
Because the data lives behind the documented JSON API, the shipped dashboard is optional.
You can disable it (enabled: false) and build your own UI — an internal admin page, a
Grafana panel hitting the endpoints, a CLI — against the same <path>/api/* surface, or
read TelescopeService
directly from a controller. The dashboard is a convenience over a stable API, not a lock-in.
Retention & overload protection
Two core safeguards that keep Telescope bounded and self-limiting — a background pruner that deletes stale entries on a timer so the store never grows without bound, and an event-loop overload guard that pauses capture when the p99 lag crosses a threshold so Telescope can never amplify an incident.
Dashboard auth
How the Telescope dashboard is gated — the default allow-in-dev / deny-in-prod policy, the built-in token and HTTP Basic credentials, delegating to your own app auth with an authorize hook, and the 401-vs-403 fail-closed guard behaviour.