@adonis-agora/telescope
The headless core — the request, exception and diagnostics watchers, the uniform Entry model, the TelescopeStore contract and in-memory ring buffer, the TelescopeService query API, the extension SDK, and the structural readers for context and diagnostics.
The core is the only package you strictly need. It records inbound requests, captures
exceptions, records every Agora diagnostics event, stores them in a bounded in-memory
buffer, and exposes a headless query API — plus the contracts (TelescopeStore, the
extension SDK) the rest of the ecosystem builds on.
Install
npm i @adonis-agora/telescope
node ace configure @adonis-agora/telescopeconfigure registers @adonis-agora/telescope/telescope_provider, plugs TelescopeMiddleware
onto the server middleware stack, and publishes config/telescope.ts.
Built-in watchers
| Watcher | Type | How it runs | Records |
|---|---|---|---|
| Request | request | server middleware | method, url (no query string), status, durationMs, traceId, optionally the body |
| Exception | exception | auto, in the request middleware | name, message, stack, method, url, traceId |
| Diagnostics | diagnostic | started at boot | every agora:<lib>:<event> publish envelope |
| Logs | log | started at boot, off by default | level, message, structured context, traceId |
The request and exception watchers come from the same middleware: it records the request
in a finally, and catches + records any thrown error before re-throwing it untouched.
The diagnostics watcher subscribes to every Agora channel (current and future). See
Capture & correlation for the full mechanics.
Request body capture
By default a request entry has no body field at all. That is the right default — bodies are
the most likely place for a password, a card number or a 40 MB upload — but it is also the
difference between seeing the payload that broke a checkout and guessing at it. Turn it on with a
requestCapture block:
export default defineConfig({
requestCapture: {}, // on, with the safe defaults below
})Three gates decide, in that order, whether a body is captured. They run before redaction, so
a huge or binary body is never walked at all; a body that any gate rejects is replaced by a short
marker string like [Skipped: 200000 bytes > 131072 bytes], leaving every other field intact.
| Gate | Default | What it does |
|---|---|---|
maxBodyBytes | 131072 (128 KiB) | Skips anything bigger. The size is read in constant time from content-length or a string/buffer length — never by serializing a parsed body. Set false to drop the size gate entirely. |
skipBodyContentTypes | application/octet-stream, application/offset+octet-stream, multipart/form-data | Content types whose payload is never worth capturing. A string matches as a case-insensitive prefix, so 'multipart/form-data' still matches a ; boundary=… suffix; a RegExp is tested as-is. |
skipBody | none | Your own predicate over { method, url, contentType, body }. Return true to skip. |
requestCapture: {
maxBodyBytes: 65_536,
skipBody: ({ url }) => url.startsWith('/webhooks/'),
}Captured bodies go through the normal redaction pass like any other content, so configured sensitive keys are still masked — but a body you never capture is the only body that definitely cannot leak.
Muting noisy diagnostics
The diagnostics watcher records every event any Agora library publishes, which for a chatty
channel (an upload progress event, say) can drown the timeline. diagnostics.exclude takes the
exact lib:event keys the dashboard's "busiest events" panel shows:
export default defineConfig({
diagnostics: {
exclude: ['media:upload.progress'],
},
})The events stay live on their channel for every other subscriber — an OTel exporter, your own watcher — they simply are not recorded here.
The second knob is recordClaimed. When a sibling library ships its own Telescope watcher, it
claims the events it records as typed entries, and the generic diagnostics watcher skips them
so the same event isn't stored twice. Set recordClaimed: true to record both the typed entry
and the raw envelope — useful while debugging a bridge, wasteful in production. exclude mutes
an event either way. isDiagnosticClaimed(key) is exported if you want to check who owns a key.
Manual exception capture
For non-HTTP code (queue workers, ace commands, your exception handler), call
recordException — it needs no injection, is a no-op when Telescope is off, and never
throws:
import { recordException } from '@adonis-agora/telescope'
await recordException(error, { method: 'POST', url: '/checkout' })Configuration
import { defineConfig, storage } from '@adonis-agora/telescope'
export default defineConfig({
enabled: true,
store: 'memory',
stores: {
memory: storage.memory({ limit: 1000 }),
// lucid: storage.lucid({ connection: 'pg' }),
},
watchers: ['request', 'diagnostics'],
extensions: [],
})| Key | Default | Description |
|---|---|---|
enabled | true | Master switch; false records nothing and starts no watcher. |
store | 'memory' | Which named driver in stores is active, or a TelescopeStore instance. |
stores | {} | Named storage drivers (storage.memory / storage.lucid). |
watchers | ['request', 'diagnostics'] | Which built-in watchers run — any of request, diagnostics, logs. |
diagnostics | { exclude: [], recordClaimed: false } | Mutes noisy events and decides whether already-claimed ones are recorded twice. |
logs | { minLevel: 'trace', tags: [] } | Floor and extra tags for the logs watcher. |
requestCapture | off | Request body capture and its gates. |
extensions | [] | Extensions to register at boot. |
The full key list — sampling, pulse, prune, overload, redaction, client errors — is on the configuration reference.
Headless API
TelescopeService is bound into the container, resolving against whichever store the
provider builds at boot from config/telescope.ts.
import { TelescopeService } from '@adonis-agora/telescope'
const telescope = await app.container.make(TelescopeService)| Method | Returns |
|---|---|
list(query?) | Entries matching an EntryQuery, newest-first. |
find(id) | One Entry, or null. |
byTrace(traceId) | Every entry on a trace, newest-first. |
count() | Total stored entries. |
topFamilies(limit?, type?) | Busiest groups by familyHash — returns { key, count }[]. |
topTags(limit?, prefix?) | Most common tags — returns { key, count }[]. |
telescopeStore | The underlying TelescopeStore (advanced). |
Notable exports
The package surface, grouped:
- Entry model —
EntryType,isBatchOrigin; typesEntry,RecordInput,BatchOrigin,BuiltinEntryType. - Storage — the
storagefactory (storage.memory/storage.lucid),InMemoryTelescopeStore,LucidTelescopeStore,createTelescopeTable,createTableStatements,DEFAULT_TABLE_NAME; typesTelescopeStore,EntryQuery,StoreProvider,MemoryStoreConfig,LucidStoreConfig,InMemoryStoreOptions,LucidStoreOptions. - Query API —
TelescopeService; typeCountBucket. - Watchers —
DiagnosticsWatcher,buildDiagnosticEntry,DIAGNOSTIC_ENTRY_TYPE,recordRequest,recordException,recordExceptionInStore,buildExceptionInput,exceptionFamilyHash. - Extension SDK —
defineTelescopeExtension,ExtensionRegistry, and the spec types (TelescopeExtension,DashboardSpec,Panel,DataProvider, …). - Config —
defineConfig,resolveConfig, thestoragefactory; typesTelescopeConfig,ResolvedTelescopeConfig,WatcherName,StoreProvider,StoreContext. - Runtime (advanced) —
getTelescopeRuntime,setTelescopeRuntime,resetTelescopeRuntime,setTelescopeExtensionRegistry. - Structural readers —
currentTraceId,getContextAccessor,getDiagnosticsRegistry,isDiagnosticEvent,isDiagnosticClaimed. - Protection (advanced) —
TelescopePruner,OverloadGuard,setTelescopePaused; typesPruneRun,PruneTrigger. See Retention & overload protection.
The runtime exports (getTelescopeRuntime, …) are how the other packages reach the live
store without DI. You rarely need them in app code — inject(TelescopeService) is the
normal path — but they're public for advanced integrations and custom watchers.
Decoupling
The core depends only on @adonisjs/core (a peer). It reads
@adonis-agora/context and @adonis-agora/diagnostics
structurally through global Symbol.for(...) slots, so it adds no hard dependency on
either and works (with reduced features) when they aren't installed.
Packages
Everything ships as one package — @adonis-agora/telescope — exposing the headless core (with config-driven memory + Lucid storage drivers) plus opt-in subpaths for per-technology watchers, the dashboard UI, alerts, and AI exception diagnosis.
Storage drivers
Telescope's config-driven storage — the in-memory ring buffer and the SQL-backed Lucid driver built into the core with the storage factory, plus the Lucid migration, JSON-text columns, integer epoch timestamps, and per-driver options.