Agora
Packages

@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/telescope

configure registers @adonis-agora/telescope/telescope_provider, plugs TelescopeMiddleware onto the server middleware stack, and publishes config/telescope.ts.

Built-in watchers

WatcherTypeHow it runsRecords
Requestrequestserver middlewaremethod, url (no query string), status, durationMs, traceId, optionally the body
Exceptionexceptionauto, in the request middlewarename, message, stack, method, url, traceId
Diagnosticsdiagnosticstarted at bootevery agora:<lib>:<event> publish envelope
Logslogstarted at boot, off by defaultlevel, 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:

config/telescope.ts
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.

GateDefaultWhat it does
maxBodyBytes131072 (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.
skipBodyContentTypesapplication/octet-stream, application/offset+octet-stream, multipart/form-dataContent 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.
skipBodynoneYour 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:

config/telescope.ts
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

config/telescope.ts
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: [],
})
KeyDefaultDescription
enabledtrueMaster 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.
requestCaptureoffRequest 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)
MethodReturns
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 }[].
telescopeStoreThe underlying TelescopeStore (advanced).

Notable exports

The package surface, grouped:

  • Entry modelEntryType, isBatchOrigin; types Entry, RecordInput, BatchOrigin, BuiltinEntryType.
  • Storage — the storage factory (storage.memory / storage.lucid), InMemoryTelescopeStore, LucidTelescopeStore, createTelescopeTable, createTableStatements, DEFAULT_TABLE_NAME; types TelescopeStore, EntryQuery, StoreProvider, MemoryStoreConfig, LucidStoreConfig, InMemoryStoreOptions, LucidStoreOptions.
  • Query APITelescopeService; type CountBucket.
  • WatchersDiagnosticsWatcher, buildDiagnosticEntry, DIAGNOSTIC_ENTRY_TYPE, recordRequest, recordException, recordExceptionInStore, buildExceptionInput, exceptionFamilyHash.
  • Extension SDKdefineTelescopeExtension, ExtensionRegistry, and the spec types (TelescopeExtension, DashboardSpec, Panel, DataProvider, …).
  • ConfigdefineConfig, resolveConfig, the storage factory; types TelescopeConfig, ResolvedTelescopeConfig, WatcherName, StoreProvider, StoreContext.
  • Runtime (advanced)getTelescopeRuntime, setTelescopeRuntime, resetTelescopeRuntime, setTelescopeExtensionRegistry.
  • Structural readerscurrentTraceId, getContextAccessor, getDiagnosticsRegistry, isDiagnosticEvent, isDiagnosticClaimed.
  • Protection (advanced)TelescopePruner, OverloadGuard, setTelescopePaused; types PruneRun, 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.

On this page