@adonis-agora/telescope 0.8.4 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/src/ai/index.d.ts +1 -1
- package/dist/src/ai/index.js +1 -1
- package/dist/src/alerts/index.d.ts +1 -1
- package/dist/src/alerts/index.js +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/mcp/index.d.ts +1 -1
- package/dist/src/mcp/index.js +1 -1
- package/dist/src/metrics/traces.d.ts +2 -0
- package/dist/src/metrics/traces.d.ts.map +1 -1
- package/dist/src/metrics/traces.js +18 -0
- package/dist/src/metrics/traces.js.map +1 -1
- package/dist/src/profiling/index.d.ts +1 -1
- package/dist/src/profiling/index.js +1 -1
- package/dist/src/request_watcher.d.ts +30 -0
- package/dist/src/request_watcher.d.ts.map +1 -1
- package/dist/src/request_watcher.js +26 -0
- package/dist/src/request_watcher.js.map +1 -1
- package/dist/src/ui/api.d.ts +5 -0
- package/dist/src/ui/api.d.ts.map +1 -1
- package/dist/src/ui/api.js +13 -0
- package/dist/src/ui/api.js.map +1 -1
- package/dist/src/ui/index.d.ts +1 -1
- package/dist/src/ui/index.js +1 -1
- package/dist/src/watchers/index.d.ts +1 -1
- package/dist/src/watchers/index.js +1 -1
- package/package.json +6 -3
- package/skills/telescope-access-mcp/SKILL.md +280 -0
- package/skills/telescope-alerts-ai/SKILL.md +290 -0
- package/skills/telescope-setup/SKILL.md +255 -0
- package/skills/telescope-storage-retention/SKILL.md +292 -0
- package/skills/telescope-watchers/SKILL.md +317 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: telescope-watchers
|
|
3
|
+
description: >-
|
|
4
|
+
Capture sources with @adonis-agora/telescope — built-in request/diagnostics/logs
|
|
5
|
+
watchers, the @adonis-agora/telescope/watchers subpath (LucidQueryWatcher db:query,
|
|
6
|
+
MailWatcher mail:sent, CacheWatcher cache:* events, HttpClientWatcher fetch wrap,
|
|
7
|
+
QueueWatcher boringqueue.job.execute, EventsWatcher onAny, RedisWatcher
|
|
8
|
+
sendCommand, profile()/startProfile(), scheduleTask()/registerSchedule()), and
|
|
9
|
+
custom watchers via the Watcher contract + safeRecord + currentTraceId. Use for
|
|
10
|
+
"record SQL queries", "watch outbound HTTP", "no queries recorded", "write a
|
|
11
|
+
custom watcher", "correlate entries to a trace", "AdonisJS telemetry".
|
|
12
|
+
license: MIT
|
|
13
|
+
metadata:
|
|
14
|
+
type: core
|
|
15
|
+
library: "@adonis-agora/telescope"
|
|
16
|
+
library_version: "0.8.4"
|
|
17
|
+
framework: adonisjs
|
|
18
|
+
sources:
|
|
19
|
+
- "DavideCarvalho/adonis-telescope:docs/concepts/capture.mdx"
|
|
20
|
+
- "DavideCarvalho/adonis-telescope:docs/packages/watchers.mdx"
|
|
21
|
+
- "DavideCarvalho/adonis-telescope:docs/recipes/custom-watcher.mdx"
|
|
22
|
+
- "DavideCarvalho/adonis-telescope:packages/core/src/watchers/index.ts"
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Watchers & capture
|
|
26
|
+
|
|
27
|
+
The core records requests, exceptions and diagnostics automatically. Every other
|
|
28
|
+
source — Lucid SQL, outbound fetch, mail, cache, queue jobs, events, Redis, logs,
|
|
29
|
+
timing spans, scheduled tasks — comes from a watcher. Per-technology watchers live
|
|
30
|
+
in the `@adonis-agora/telescope/watchers` subpath of the same package; nothing extra
|
|
31
|
+
to install.
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
Re-run configure and pick **Watchers**, then tune the published config:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
node ace configure @adonis-agora/telescope # pick "Watchers" at the prompt
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// config/telescope_watchers.ts
|
|
43
|
+
import { defineConfig } from '@adonis-agora/telescope/watchers'
|
|
44
|
+
|
|
45
|
+
export default defineConfig({
|
|
46
|
+
enabled: true,
|
|
47
|
+
watchers: ['query'], // only the verified Lucid query watcher is on by default
|
|
48
|
+
query: { slowMs: 500, captureBindings: false, normalize: true },
|
|
49
|
+
httpClient: { slowMs: 1000, ignoreHosts: [], captureBodies: false }, // body SIZES, never bytes
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Enable more watchers by adding names: `'query'`, `'mail'`, `'cache'`,
|
|
54
|
+
`'http-client'`, `'logs'`, `'queue'`, `'events'`, `'redis'`, `'profiling'`,
|
|
55
|
+
`'schedule'`, `'queue-manager'`.
|
|
56
|
+
|
|
57
|
+
Source: `docs/packages/watchers.mdx` (Configuration).
|
|
58
|
+
|
|
59
|
+
## Core patterns
|
|
60
|
+
|
|
61
|
+
### Pattern 1 — record every Lucid query
|
|
62
|
+
|
|
63
|
+
The query watcher subscribes to Lucid's `db:query` event. Entries group by a
|
|
64
|
+
normalized SQL template hash (`select * from users where id = ?`), so every
|
|
65
|
+
execution of the same statement rolls into one family for `topFamilies()`.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// config/telescope_watchers.ts
|
|
69
|
+
export default defineConfig({
|
|
70
|
+
watchers: ['query'],
|
|
71
|
+
// Bindings carry PII/secrets: redacted to [REDACTED] placeholders unless you opt in.
|
|
72
|
+
query: { slowMs: 500, captureBindings: false, normalize: true },
|
|
73
|
+
// ignoreConnections: ['telescope'] — drop e.g. telescope's own lucid-store writes.
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Lucid only **emits** `db:query` when the connection's `debug` flag is on or a
|
|
78
|
+
listener exists at report time; subscribing the watcher is enough to make Lucid
|
|
79
|
+
report, but `debug: true` in your DB config guarantees it:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// config/database.ts
|
|
83
|
+
export const databaseConfig = defineConfig({
|
|
84
|
+
connections: {
|
|
85
|
+
pg: {
|
|
86
|
+
connection: { host: '127.0.0.1', database: 'app', user: 'postgres', password: 'secret' },
|
|
87
|
+
debug: true, // guarantee db:query flows even before any other listener attaches
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Source: `docs/packages/watchers.mdx` (Query watcher callouts),
|
|
94
|
+
`docs/getting-started.mdx` Step 5.
|
|
95
|
+
|
|
96
|
+
### Pattern 2 — user-instrumented spans and scheduled runs
|
|
97
|
+
|
|
98
|
+
`profile()`/`startProfile()` and `scheduleTask()`/`registerSchedule()` are helpers
|
|
99
|
+
you call around code — nothing is captured until the watcher is enabled AND a
|
|
100
|
+
helper runs. While disabled they are zero-cost no-ops that still execute your
|
|
101
|
+
closure.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import {
|
|
105
|
+
profile,
|
|
106
|
+
scheduleTask,
|
|
107
|
+
registerSchedule,
|
|
108
|
+
} from '@adonis-agora/telescope/watchers'
|
|
109
|
+
|
|
110
|
+
// Timing span — times fn, records completed/failed, re-throws on error.
|
|
111
|
+
const total = await profile('checkout', async (span) => {
|
|
112
|
+
span.mark('cart-loaded')
|
|
113
|
+
return await charge(cart)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Scheduled run — wraps the closure you actually schedule.
|
|
117
|
+
await scheduleTask('prune-sessions', () => sessions.pruneExpired(), {
|
|
118
|
+
schedule: '0 * * * *',
|
|
119
|
+
kind: 'cron',
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Register what EXISTS so the dashboard can compute next-run
|
|
123
|
+
// (optional cron-parser peer; without it nextRunAt is null, not an error).
|
|
124
|
+
registerSchedule({ name: 'prune-sessions', schedule: '0 * * * *', kind: 'cron' })
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Source: `docs/packages/watchers.mdx` (Profiling watcher, Schedule watcher).
|
|
128
|
+
|
|
129
|
+
### Pattern 3 — a custom watcher
|
|
130
|
+
|
|
131
|
+
Implement `{ type, start(emitter), stop() }` over the structural `EmitterLike`
|
|
132
|
+
(`on` returns an unsubscribe function). Record through `safeRecord(input, source)`:
|
|
133
|
+
it resolves the runtime store, backfills `currentTraceId()`, and swallows every
|
|
134
|
+
failure so your watcher cannot break the path it observes.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
// app/telescope/queue_watcher.ts
|
|
138
|
+
import { EntryType, type RecordInput, currentTraceId } from '@adonis-agora/telescope'
|
|
139
|
+
import { safeRecord, type Watcher, type EmitterLike } from '@adonis-agora/telescope/watchers'
|
|
140
|
+
|
|
141
|
+
export class QueueWatcher implements Watcher {
|
|
142
|
+
readonly type = EntryType.Job
|
|
143
|
+
private unsubscribe: (() => void) | null = null
|
|
144
|
+
|
|
145
|
+
start(emitter: EmitterLike): void {
|
|
146
|
+
if (this.unsubscribe) return // idempotent
|
|
147
|
+
this.unsubscribe = emitter.on('queue:processed', (data) => this.handle(data))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
stop(): void {
|
|
151
|
+
this.unsubscribe?.()
|
|
152
|
+
this.unsubscribe = null
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private handle(data: unknown): void {
|
|
156
|
+
if (!isQueueEvent(data)) return // tolerate junk on the channel
|
|
157
|
+
safeRecord(buildJobEntry(data), 'QueueWatcher') // guarded; never throws
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isQueueEvent(d: unknown): d is { queue: string; jobId: string; durationMs: number; failed: boolean } {
|
|
162
|
+
return typeof d === 'object' && d !== null && 'queue' in d && 'jobId' in d
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildJobEntry(e: { queue: string; jobId: string; durationMs: number; failed: boolean }): RecordInput {
|
|
166
|
+
const traceId = currentTraceId()
|
|
167
|
+
return {
|
|
168
|
+
type: EntryType.Job,
|
|
169
|
+
familyHash: e.queue, // topFamilies(10, 'job') rolls up here
|
|
170
|
+
durationMs: e.durationMs,
|
|
171
|
+
traceId,
|
|
172
|
+
tags: [`queue:${e.queue}`, ...(e.failed ? ['failed'] : [])],
|
|
173
|
+
content: { queue: e.queue, jobId: e.jobId, failed: e.failed, durationMs: e.durationMs },
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Start it yourself in a provider (`boot()` → resolve `'emitter'` from the container
|
|
179
|
+
→ `watcher.start(emitter)`; `shutdown()` → `stop()`). The watchers config controls
|
|
180
|
+
only the built-ins — a custom watcher is just a class you start.
|
|
181
|
+
|
|
182
|
+
Source: `docs/recipes/custom-watcher.mdx`.
|
|
183
|
+
|
|
184
|
+
### Pattern 4 — tag entries by tenant/user
|
|
185
|
+
|
|
186
|
+
`getContextAccessor()` reads `@adonis-agora/context` structurally; add tags at
|
|
187
|
+
record time so `list({ tag })` pivots on them.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { getContextAccessor, type RecordInput } from '@adonis-agora/telescope'
|
|
191
|
+
|
|
192
|
+
export function withTenantTag<T>(input: RecordInput<T>): RecordInput<T> {
|
|
193
|
+
const tenant = getContextAccessor()?.tenantId()
|
|
194
|
+
if (tenant === undefined) return input
|
|
195
|
+
return { ...input, tags: [...(input.tags ?? []), `tenant:${tenant}`] }
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Source: `docs/recipes/tags-and-redaction.mdx`, `docs/recipes/request-context.mdx`.
|
|
200
|
+
|
|
201
|
+
## Common mistakes
|
|
202
|
+
|
|
203
|
+
### CRITICAL Enabling the query watcher without Lucid debug
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// Wrong — the watcher is listed, yet zero queries appear.
|
|
207
|
+
export default defineConfig({ watchers: ['query'] })
|
|
208
|
+
// config/database.ts has no debug flag and no other db:query listener exists.
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
// Correct — turn on debug for the connection (or attach another db:query listener).
|
|
213
|
+
pg: { /* ... */ debug: true }
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Mechanism: Lucid builds the `db:query` event only when `debug` is on or a listener
|
|
217
|
+
exists at report time — otherwise the event is never even constructed, so the
|
|
218
|
+
enabled watcher silently records nothing while looking healthy.
|
|
219
|
+
Source: `docs/packages/watchers.mdx` (Query watcher warn callout),
|
|
220
|
+
`docs/getting-started.mdx` Step 5.
|
|
221
|
+
|
|
222
|
+
### HIGH Enabling the logs watcher in both config files
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
// Wrong — 'logs' in config/telescope.ts AND config/telescope_watchers.ts.
|
|
226
|
+
// config/telescope.ts
|
|
227
|
+
export default defineConfig({ watchers: ['request', 'diagnostics', 'logs'], logs: { minLevel: 'warn' } })
|
|
228
|
+
// config/telescope_watchers.ts
|
|
229
|
+
export default defineConfig({ watchers: ['query', 'logs'] })
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
// Correct — enable it in exactly one place.
|
|
234
|
+
export default defineConfig({ watchers: ['request', 'diagnostics', 'logs'], logs: { minLevel: 'warn' } })
|
|
235
|
+
export default defineConfig({ watchers: ['query'] })
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Mechanism: whichever provider boots first owns the logger tap; the other warns and
|
|
239
|
+
stays inert, so its `minLevel`/`tags` options are silently ignored.
|
|
240
|
+
Source: `docs/reference/configuration.mdx` ('logs' warn callout).
|
|
241
|
+
|
|
242
|
+
### HIGH Building entry content outside safeRecord
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
// Wrong — content building runs on the HOST thread; one circular payload breaks
|
|
246
|
+
// the very operation being observed.
|
|
247
|
+
emitter.on('queue:processed', (d) => {
|
|
248
|
+
safeRecord({ type: EntryType.Job, content: JSON.parse(JSON.stringify(d.raw)) }, 'QueueWatcher')
|
|
249
|
+
})
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
// Correct — validate defensively, build plain content, let safeRecord guard the store.
|
|
254
|
+
private handle(data: unknown): void {
|
|
255
|
+
if (!isQueueEvent(data)) return
|
|
256
|
+
safeRecord(buildJobEntry(data), 'QueueWatcher')
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Mechanism: recording paths are fire-and-forget and fully guarded — a missing store,
|
|
261
|
+
a throwing `record`, or a rejected promise is swallowed and warn-logged, never
|
|
262
|
+
thrown into the request/job/query being watched.
|
|
263
|
+
Source: `docs/concepts/capture.mdx` (warn callout), `docs/recipes/custom-watcher.mdx`.
|
|
264
|
+
|
|
265
|
+
### MEDIUM Non-idempotent start/leaky stop in a custom watcher
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
// Wrong — double-subscribe duplicates every entry; a leaked subscription keeps
|
|
269
|
+
// recording after shutdown.
|
|
270
|
+
start(emitter: EmitterLike): void {
|
|
271
|
+
emitter.on('queue:processed', this.handle)
|
|
272
|
+
}
|
|
273
|
+
stop(): void {}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
// Correct — keep the unsubscribe fn; guard re-entry; release in stop().
|
|
278
|
+
start(emitter: EmitterLike): void {
|
|
279
|
+
if (this.unsubscribe) return
|
|
280
|
+
this.unsubscribe = emitter.on('queue:processed', this.handle)
|
|
281
|
+
}
|
|
282
|
+
stop(): void {
|
|
283
|
+
this.unsubscribe?.()
|
|
284
|
+
this.unsubscribe = null
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Mechanism: `emitter.on` returns an unsubscribe function; a watcher that leaks it
|
|
289
|
+
records entries into a torn-down process and double-starts duplicate everything.
|
|
290
|
+
Source: `docs/recipes/custom-watcher.mdx` (warn callout).
|
|
291
|
+
|
|
292
|
+
### MEDIUM Waiting on queue/redis watchers whose engines were never installed
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
// Wrong — assuming pnpm installed the engines because the watcher names are valid.
|
|
296
|
+
export default defineConfig({ watchers: ['queue', 'redis'] })
|
|
297
|
+
// package.json has no @adonisjs/queue / @adonisjs/redis entry.
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
// Correct — add the packages yourself; they are NOT declared peers.
|
|
302
|
+
{
|
|
303
|
+
"dependencies": {
|
|
304
|
+
"@adonisjs/queue": "^...",
|
|
305
|
+
"@adonisjs/redis": "^..."
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Mechanism: those two are deliberately not declared peers — their watchers tap a
|
|
311
|
+
Node diagnostics channel / wrap a manager they're handed, idle when nobody feeds
|
|
312
|
+
them, and npm will never install them for you.
|
|
313
|
+
Source: `docs/packages/watchers.mdx` (Peer dependencies).
|
|
314
|
+
|
|
315
|
+
See also: `telescope-storage-retention/SKILL.md` — where captured entries land and
|
|
316
|
+
how bounds/sampling shape what survives;
|
|
317
|
+
`telescope-alerts-ai/SKILL.md` — rules only ever see what was recorded.
|