@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,255 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: telescope-setup
|
|
3
|
+
description: >-
|
|
4
|
+
Set up @adonis-agora/telescope in an AdonisJS app. Covers node ace configure
|
|
5
|
+
codemods (telescope_provider + TelescopeMiddleware on the server stack +
|
|
6
|
+
config/telescope.ts), defineConfig keys (enabled/store/stores/watchers/redact),
|
|
7
|
+
the storage.memory and storage.lucid drivers, the headless TelescopeService query
|
|
8
|
+
API (list/find/byTrace/count/topFamilies/topTags/getHealth), recordException for
|
|
9
|
+
non-HTTP paths, opt-in requestCapture body gates, and why /telescope 404s without
|
|
10
|
+
@adonis-agora/telescope-ui. Use for "install telescope", "telemetry is empty",
|
|
11
|
+
"query captured entries", "capture request bodies", "AdonisJS observability".
|
|
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:README.md"
|
|
20
|
+
- "DavideCarvalho/adonis-telescope:docs/getting-started.mdx"
|
|
21
|
+
- "DavideCarvalho/adonis-telescope:docs/packages/core.mdx"
|
|
22
|
+
- "DavideCarvalho/adonis-telescope:docs/reference/configuration.mdx"
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Setting up @adonis-agora/telescope
|
|
26
|
+
|
|
27
|
+
`@adonis-agora/telescope` is Laravel Telescope-style **headless** observability for
|
|
28
|
+
AdonisJS: it records every inbound HTTP request, every unhandled exception and every
|
|
29
|
+
`agora:<lib>:<event>` diagnostics publish as a queryable `Entry`. The core ships no
|
|
30
|
+
page — mounting a UI is a separate package (`telescope-ui-dashboard` skill).
|
|
31
|
+
|
|
32
|
+
## Setup
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
pnpm add @adonis-agora/telescope
|
|
36
|
+
node ace configure @adonis-agora/telescope
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`configure` runs codemods that register `@adonis-agora/telescope/telescope_provider`
|
|
40
|
+
in `adonisrc.ts`, plug `TelescopeMiddleware` onto the **server** middleware stack
|
|
41
|
+
(so it wraps the whole pipeline and observes the final status), and publish
|
|
42
|
+
`config/telescope.ts`. That alone records `request` + `diagnostic` entries.
|
|
43
|
+
|
|
44
|
+
The published config is all-defaults:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// config/telescope.ts
|
|
48
|
+
import { defineConfig, storage } from '@adonis-agora/telescope'
|
|
49
|
+
|
|
50
|
+
export default defineConfig({
|
|
51
|
+
enabled: true, // master switch
|
|
52
|
+
store: 'memory', // which driver in `stores` is active
|
|
53
|
+
stores: {
|
|
54
|
+
memory: storage.memory({ limit: 1000 }), // bounded ring buffer, lost on restart
|
|
55
|
+
// lucid: storage.lucid({ connection: 'pg' }), // persistent SQL store (optional peer)
|
|
56
|
+
},
|
|
57
|
+
watchers: ['request', 'diagnostics'], // omit one to disable it
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Read entries back from anywhere via the container:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { TelescopeService } from '@adonis-agora/telescope'
|
|
65
|
+
|
|
66
|
+
const telescope = await app.container.make(TelescopeService)
|
|
67
|
+
|
|
68
|
+
await telescope.list({ type: 'request', limit: 50 }) // newest-first
|
|
69
|
+
await telescope.byTrace('abc123') // one request's whole story
|
|
70
|
+
await telescope.list({ tag: 'lib:billing', search: 'invoice' })
|
|
71
|
+
await telescope.topFamilies(10, 'diagnostic') // busiest lib:event pairs
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Core patterns
|
|
75
|
+
|
|
76
|
+
### Pattern 1 — the headless API inside a controller
|
|
77
|
+
|
|
78
|
+
Controllers have no `app` of their own; use `@inject()` so the container resolves
|
|
79
|
+
the constructor dependency.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// app/controllers/inspector_controller.ts
|
|
83
|
+
import { inject } from '@adonisjs/core'
|
|
84
|
+
import type { HttpContext } from '@adonisjs/core/http'
|
|
85
|
+
import { TelescopeService } from '@adonis-agora/telescope'
|
|
86
|
+
|
|
87
|
+
@inject()
|
|
88
|
+
export default class InspectorController {
|
|
89
|
+
constructor(private telescope: TelescopeService) {}
|
|
90
|
+
|
|
91
|
+
async index({ response }: HttpContext) {
|
|
92
|
+
return response.json(await this.telescope.list({ limit: 50 }))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`EntryQuery` composes filters with AND: `{ type, tag, familyHash, traceId, before,
|
|
98
|
+
after, search, limit }`. `search` matches JSON content **and** tags, so a request
|
|
99
|
+
matches by url, a diagnostic by event name, a query by SQL — all from one box.
|
|
100
|
+
For React usage in a custom console, see the `telescope-ui-dashboard` skill.
|
|
101
|
+
|
|
102
|
+
Source: `docs/getting-started.mdx` (Step 3).
|
|
103
|
+
|
|
104
|
+
### Pattern 2 — manual exception capture outside HTTP
|
|
105
|
+
|
|
106
|
+
Queue workers, ace commands and `app/exceptions/handler.ts` are not wrapped by the
|
|
107
|
+
middleware. `recordException` reads the live store with nothing injected, is a no-op
|
|
108
|
+
when Telescope is off, and never throws.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// app/exceptions/handler.ts
|
|
112
|
+
import { recordException } from '@adonis-agora/telescope'
|
|
113
|
+
|
|
114
|
+
async report(error: unknown, ctx: HttpContext) {
|
|
115
|
+
recordException(error, { method: ctx.request.method(), url: ctx.request.url() })
|
|
116
|
+
return super.report(error, ctx)
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Exceptions group by `exceptionFamilyHash` = `name:message:topStackFrame`, so alerts
|
|
121
|
+
and the dashboard's exception groups stay deterministic across processes.
|
|
122
|
+
|
|
123
|
+
Source: `docs/concepts/capture.mdx` (Manual capture), `docs/packages/core.mdx`.
|
|
124
|
+
|
|
125
|
+
### Pattern 3 — opt-in request-body capture
|
|
126
|
+
|
|
127
|
+
By default a `request` entry has **no `body` field at all**. Turn it on with gates
|
|
128
|
+
that run before redaction; a rejected body becomes a marker string like
|
|
129
|
+
`[Skipped: 200000 bytes > 131072 bytes]`.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// config/telescope.ts
|
|
133
|
+
export default defineConfig({
|
|
134
|
+
requestCapture: {
|
|
135
|
+
maxBodyBytes: 65_536,
|
|
136
|
+
skipBodyContentTypes: ['multipart/form-data'],
|
|
137
|
+
skipBody: ({ url }) => url.startsWith('/webhooks/'),
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Captured bodies still pass through central redaction — but a body never captured is
|
|
143
|
+
the only body that definitely cannot leak.
|
|
144
|
+
|
|
145
|
+
Source: `docs/packages/core.mdx` (Request body capture).
|
|
146
|
+
|
|
147
|
+
### Pattern 4 — production storage
|
|
148
|
+
|
|
149
|
+
Switch to the built-in Lucid driver when entries must survive restarts (see the
|
|
150
|
+
`telescope-storage-retention` skill for the migration and pruning):
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
pnpm add @adonisjs/lucid
|
|
154
|
+
node ace migration:run # create_telescope_entries_table, shipped by configure
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
export default defineConfig({
|
|
159
|
+
store: 'lucid',
|
|
160
|
+
stores: { lucid: storage.lucid() }, // or storage.lucid({ connection: 'pg' })
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Source: `docs/getting-started.mdx` (Step 4).
|
|
165
|
+
|
|
166
|
+
## Common mistakes
|
|
167
|
+
|
|
168
|
+
### HIGH Expecting a dashboard page from the core alone
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// Wrong — nothing mounts a page; http://localhost:3333/telescope answers 404.
|
|
172
|
+
// adonisrc.ts
|
|
173
|
+
providers: [
|
|
174
|
+
() => import('@adonis-agora/telescope/telescope_provider'),
|
|
175
|
+
// ...app providers
|
|
176
|
+
]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
// Correct — install @adonis-agora/telescope-ui and register its provider AFTER ui_provider.
|
|
181
|
+
providers: [
|
|
182
|
+
() => import('@adonis-agora/telescope/telescope_provider'),
|
|
183
|
+
() => import('@adonis-agora/telescope/ui_provider'), // JSON API + SSE
|
|
184
|
+
() => import('@adonis-agora/telescope-ui/telescope_ui_dashboard_provider'), // the SPA
|
|
185
|
+
]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Mechanism: the core `ui` subpath serves only `<path>/api/*`; the prefix root belongs
|
|
189
|
+
to the separate SPA package, whose provider must also be registered after it.
|
|
190
|
+
Source: `docs/packages/ui.mdx` (Install), `docs/packages/telescope-ui.mdx` (warn).
|
|
191
|
+
|
|
192
|
+
### HIGH Assuming request bodies are captured by default
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// Wrong — agents assume they can read the payload that broke checkout:
|
|
196
|
+
const entry = await telescope.find(id)
|
|
197
|
+
entry.content.body // undefined — bodies are opt-in
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
// Correct — enable requestCapture explicitly (gates run before redaction).
|
|
202
|
+
defineConfig({
|
|
203
|
+
requestCapture: { maxBodyBytes: 65_536 },
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Mechanism: the right default keeps passwords/card numbers out of storage; the entry
|
|
208
|
+
records method/url/status/duration only until you opt in per environment.
|
|
209
|
+
Source: `docs/packages/core.mdx` (Request body capture).
|
|
210
|
+
|
|
211
|
+
### HIGH Hand-rolling the provider without the server-stack middleware
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
// Wrong — provider registered but TelescopeMiddleware missing: no requests recorded,
|
|
215
|
+
// no exceptions captured, and nothing warns about it.
|
|
216
|
+
providers: [
|
|
217
|
+
() => import('@adonis-agora/telescope/telescope_provider'),
|
|
218
|
+
]
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
# Correct — let the configure codemod wire both pieces.
|
|
223
|
+
node ace configure @adonis-agora/telescope
|
|
224
|
+
# verifies: telescope_provider present AND TelescopeMiddleware on the server stack
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Mechanism: capture comes from `TelescopeMiddleware` on the `server` middleware
|
|
228
|
+
stack; the provider alone starts only the boot-time watchers, so the timeline stays
|
|
229
|
+
empty while everything looks installed.
|
|
230
|
+
Source: `docs/reference/configuration.mdx` (Providers table), `docs/getting-started.mdx` Step 1.
|
|
231
|
+
|
|
232
|
+
### MEDIUM Resolving TelescopeService via this.app inside a controller
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
// Wrong — a controller has no `app`; this does not compile.
|
|
236
|
+
constructor(private app: ApplicationService) {}
|
|
237
|
+
async index() {
|
|
238
|
+
const telescope = await this.app.container.make(TelescopeService)
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
// Correct — constructor injection through @inject().
|
|
244
|
+
@inject()
|
|
245
|
+
export default class InspectorController {
|
|
246
|
+
constructor(private telescope: TelescopeService) {}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Mechanism: `@inject()` asks the container to build the controller's dependencies
|
|
251
|
+
from their constructor types; `TelescopeService` arrives already resolved.
|
|
252
|
+
Source: `docs/getting-started.mdx` (Step 3 note).
|
|
253
|
+
|
|
254
|
+
See also: `telescope-ui-dashboard/SKILL.md` — mounting the page this setup lacks;
|
|
255
|
+
`telescope-storage-retention/SKILL.md` — swapping the dev-only memory driver.
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: telescope-storage-retention
|
|
3
|
+
description: >-
|
|
4
|
+
Storage, retention, redaction and sampling in @adonis-agora/telescope — the
|
|
5
|
+
storage.memory ring buffer vs the storage.lucid SQL driver (optional peer,
|
|
6
|
+
create_telescope_entries_table migration), the TelescopeStore contract
|
|
7
|
+
(record/get/list/count/prune/clear), prune {after,keepLast,intervalMs}, the
|
|
8
|
+
overload guard + setTelescopePaused, redact {keys,perType}, tail-sampling rules
|
|
9
|
+
{rate,keepErrors,keepSlowMs}, and writing a custom store that resolves
|
|
10
|
+
traceId/origin. Use for "persistent storage", "entries lost on restart",
|
|
11
|
+
"table keeps growing", "redact secrets", "sample noisy entry types", "custom
|
|
12
|
+
store".
|
|
13
|
+
license: MIT
|
|
14
|
+
metadata:
|
|
15
|
+
type: core
|
|
16
|
+
library: "@adonis-agora/telescope"
|
|
17
|
+
library_version: "0.8.4"
|
|
18
|
+
framework: adonisjs
|
|
19
|
+
sources:
|
|
20
|
+
- "DavideCarvalho/adonis-telescope:docs/concepts/storage.mdx"
|
|
21
|
+
- "DavideCarvalho/adonis-telescope:docs/concepts/protection.mdx"
|
|
22
|
+
- "DavideCarvalho/adonis-telescope:docs/packages/storages.mdx"
|
|
23
|
+
- "DavideCarvalho/adonis-telescope:docs/recipes/custom-storage.mdx"
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# Storage, retention & protection
|
|
27
|
+
|
|
28
|
+
A store sits between capture and query: watchers `record` into it, and
|
|
29
|
+
`TelescopeService` reads out of it. Storage is **config-driven** — build named
|
|
30
|
+
drivers with the `storage` factory, pick one with `store`. Two ship in the box:
|
|
31
|
+
`memory` (bounded ring buffer) and `lucid` (persistent SQL via an optional
|
|
32
|
+
`@adonisjs/lucid` peer imported lazily only when selected).
|
|
33
|
+
|
|
34
|
+
## Setup
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// config/telescope.ts
|
|
38
|
+
import { defineConfig, storage } from '@adonis-agora/telescope'
|
|
39
|
+
|
|
40
|
+
export default defineConfig({
|
|
41
|
+
store: 'lucid',
|
|
42
|
+
stores: {
|
|
43
|
+
memory: storage.memory({ limit: 5000 }), // dev/tests — per-process, lost on restart
|
|
44
|
+
lucid: storage.lucid(), // or storage.lucid({ connection: 'pg', tableName: 'tscope' })
|
|
45
|
+
},
|
|
46
|
+
// Retention pruner — OFF unless a prune block is present.
|
|
47
|
+
prune: { after: '24h', keepLast: 50_000, intervalMs: 60_000 },
|
|
48
|
+
// Event-loop overload guard (on by default): pauses ingestion when p99 lag >= maxEventLoopLagMs.
|
|
49
|
+
overload: { enabled: true, maxEventLoopLagMs: 200, startupGraceMs: 5_000 },
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The lucid driver needs its table; `node ace configure @adonis-agora/telescope`
|
|
54
|
+
publishes it next to the config:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
node ace migration:run # create_telescope_entries_table (+ indexes on created_at/type/trace_id/family_hash)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Source: `docs/concepts/storage.mdx`, `docs/packages/storages.mdx`.
|
|
61
|
+
|
|
62
|
+
## Core patterns
|
|
63
|
+
|
|
64
|
+
### Pattern 1 — query the store through TelescopeService
|
|
65
|
+
|
|
66
|
+
Every set field of `EntryQuery` is an AND predicate; results are always
|
|
67
|
+
newest-first.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { TelescopeService } from '@adonis-agora/telescope'
|
|
71
|
+
|
|
72
|
+
const telescope = await app.container.make(TelescopeService)
|
|
73
|
+
await telescope.list({ type: 'query', search: 'users', limit: 50 })
|
|
74
|
+
await telescope.list({ tag: 'status:500', after: oneHourAgo })
|
|
75
|
+
await telescope.list({ familyHash: 'billing:invoice-paid' })
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Source: `docs/concepts/storage.mdx` (Querying).
|
|
79
|
+
|
|
80
|
+
### Pattern 2 — redaction and per-type bounds
|
|
81
|
+
|
|
82
|
+
Redaction is **on by default** at the one boundary every watcher records through —
|
|
83
|
+
the store's `record()` — so no watcher can bypass it. Sensitive keys
|
|
84
|
+
(`authorization`, `cookie`, `password`, `token`, `secret`, …) are masked with
|
|
85
|
+
`[REDACTED]` case-insensitively at any depth; the clone is also memory-bounded and
|
|
86
|
+
cycle-safe.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// config/telescope.ts
|
|
90
|
+
export default defineConfig({
|
|
91
|
+
redact: {
|
|
92
|
+
enabled: true,
|
|
93
|
+
keys: ['ssn', 'credit_card'], // merged with the built-in set
|
|
94
|
+
// perType raises only the NUMERIC bounds for one type — masking stays uniform:
|
|
95
|
+
perType: { exception: { maxContentBytes: 64_000 } },
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The same `redact()` function is exported to scrub a value before building custom
|
|
101
|
+
content yourself.
|
|
102
|
+
|
|
103
|
+
Source: `docs/reference/configuration.mdx` (redact callout),
|
|
104
|
+
`docs/recipes/tags-and-redaction.mdx` (Built-in redaction).
|
|
105
|
+
|
|
106
|
+
### Pattern 3 — tail-sample noisy types without losing errors
|
|
107
|
+
|
|
108
|
+
`sampling` runs on the write path. A bare number is a uniform keep-rate; per-type
|
|
109
|
+
rules keep everything that looks like an error or is slow, no matter the rate.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
export default defineConfig({
|
|
113
|
+
sampling: {
|
|
114
|
+
default: 1, // keep everything by default
|
|
115
|
+
cache: 0.1, // a tenth of cache entries
|
|
116
|
+
request: { rate: 0.25, keepErrors: true, keepSlowMs: 500 }, // but all errors/slow
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Sampled-out entries are never persisted, and the dashboard's retention indicator
|
|
122
|
+
shows which types record below 100% so counts aren't misread.
|
|
123
|
+
|
|
124
|
+
Source: `docs/recipes/tags-and-redaction.mdx` (Sampling),
|
|
125
|
+
`docs/packages/ui.mdx` (Retention endpoint).
|
|
126
|
+
|
|
127
|
+
### Pattern 4 — pause capture around bulk work
|
|
128
|
+
|
|
129
|
+
`setTelescopePaused(true)` flips the same flag the overload guard uses; every
|
|
130
|
+
ingestion point honours it immediately.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { setTelescopePaused } from '@adonis-agora/telescope'
|
|
134
|
+
|
|
135
|
+
setTelescopePaused(true)
|
|
136
|
+
try {
|
|
137
|
+
await importEverything()
|
|
138
|
+
} finally {
|
|
139
|
+
setTelescopePaused(false) // always restore: process-global, nothing else flips it back
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Source: `docs/concepts/protection.mdx` (Pausing on purpose).
|
|
144
|
+
|
|
145
|
+
### Pattern 5 — a custom store
|
|
146
|
+
|
|
147
|
+
Implement the six-method contract and pass an instance as `store`. Two
|
|
148
|
+
responsibilities are easy to miss: `record` must resolve `traceId`/`origin`, and
|
|
149
|
+
`search` needs serialized content text.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import {
|
|
153
|
+
currentTraceId, isBatchOrigin,
|
|
154
|
+
type BatchOrigin, type Entry, type EntryQuery,
|
|
155
|
+
type RecordInput, type TelescopeStore,
|
|
156
|
+
} from '@adonis-agora/telescope'
|
|
157
|
+
import { randomUUID } from 'node:crypto'
|
|
158
|
+
|
|
159
|
+
export class MongoTelescopeStore implements TelescopeStore {
|
|
160
|
+
async record<T>(input: RecordInput<T>): Promise<Entry<T>> {
|
|
161
|
+
const traceId = input.traceId !== undefined ? input.traceId : currentTraceId()
|
|
162
|
+
const origin: BatchOrigin = isBatchOrigin(input.origin) ? input.origin : 'manual'
|
|
163
|
+
const entry: Entry<T> = {
|
|
164
|
+
id: randomUUID(), type: input.type, familyHash: input.familyHash ?? null,
|
|
165
|
+
content: input.content, tags: input.tags ?? [], sequence: this.sequence++,
|
|
166
|
+
durationMs: input.durationMs ?? null, origin, traceId, createdAt: new Date(),
|
|
167
|
+
}
|
|
168
|
+
await this.col.insertOne({ ...entry, createdAtMs: entry.createdAt.getTime(),
|
|
169
|
+
contentText: JSON.stringify(entry.content) }) // search target
|
|
170
|
+
return entry
|
|
171
|
+
}
|
|
172
|
+
/* get / list / count / prune(olderThan, keepLast?) / clear */
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// config/telescope.ts
|
|
176
|
+
export default defineConfig({ store: new MongoTelescopeStore(col) })
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Source: `docs/recipes/custom-storage.mdx`.
|
|
180
|
+
|
|
181
|
+
## Common mistakes
|
|
182
|
+
|
|
183
|
+
### HIGH Shipping the memory driver to production
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// Wrong — per-process ring buffer, lost on every restart/deploy; in a cluster each
|
|
187
|
+
// worker shows a different slice of reality.
|
|
188
|
+
defineConfig({ store: 'memory', stores: { memory: storage.memory({ limit: 1000 }) } })
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
// Correct — durable, cross-query storage via the built-in Lucid driver.
|
|
193
|
+
defineConfig({ store: 'lucid', stores: { lucid: storage.lucid() } })
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Mechanism: eviction past the cap drops the oldest entries and nothing survives a
|
|
197
|
+
restart, so "nothing here" after a deploy is indistinguishable from "nothing
|
|
198
|
+
happened".
|
|
199
|
+
Source: `docs/concepts/storage.mdx` (memory driver warn).
|
|
200
|
+
|
|
201
|
+
### HIGH Switching to lucid without arming retention
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
// Wrong — a persistent SQL table with no time-based bound grows forever.
|
|
205
|
+
defineConfig({ store: 'lucid', stores: { lucid: storage.lucid() } })
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
// Correct — supply a prune block; that is what arms the background pruner.
|
|
210
|
+
defineConfig({
|
|
211
|
+
store: 'lucid',
|
|
212
|
+
stores: { lucid: storage.lucid() },
|
|
213
|
+
prune: { after: '24h', keepLast: 50_000 },
|
|
214
|
+
})
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Mechanism: no store auto-prunes by age (the memory driver evicts by count, the
|
|
218
|
+
lucid store never auto-evicts because that would add a query per write); the
|
|
219
|
+
pruner exists only when a `prune` block is supplied.
|
|
220
|
+
Source: `docs/concepts/protection.mdx` (info callout), `docs/packages/storages.mdx` (warn).
|
|
221
|
+
|
|
222
|
+
### HIGH Custom store skipping traceId/origin resolution
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
// Wrong — traceId left undefined and origin unvalidated:
|
|
226
|
+
traceId: input.traceId, origin: input.origin as BatchOrigin,
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
// Correct — explicit null respected, omitted reads context, junk falls back:
|
|
231
|
+
const traceId = input.traceId !== undefined ? input.traceId : currentTraceId()
|
|
232
|
+
const origin: BatchOrigin = isBatchOrigin(input.origin) ? input.origin : 'manual'
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Mechanism: skipping this silently breaks trace correlation for everything
|
|
236
|
+
downstream (`byTrace`, waterfalls, N+1 detection) while every write still succeeds.
|
|
237
|
+
Source: `docs/recipes/custom-storage.mdx` (load-bearing lines).
|
|
238
|
+
|
|
239
|
+
### MEDIUM Importing services/db inside a provider's boot()
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
// Wrong — the facade default export is undefined until app.booted(), which runs
|
|
243
|
+
// AFTER provider boot.
|
|
244
|
+
import db from '@adonisjs/lucid/services/db'
|
|
245
|
+
const store = new LucidTelescopeStore(db)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
// Correct — resolve the binding from the container during boot.
|
|
250
|
+
const db = await app.container.make('lucid.db')
|
|
251
|
+
const store = new LucidTelescopeStore(db, { tableName: 'telescope_entries' })
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Mechanism: provider boot precedes `app.booted()`; resolving lazily through the
|
|
255
|
+
container gets the live binding instead of `undefined`.
|
|
256
|
+
Source: `docs/packages/storages.mdx` (Programmatic API note).
|
|
257
|
+
|
|
258
|
+
### MEDIUM Treating sampling as a security control
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
// Wrong — assuming dropping 90% of queries hides the secrets in the other 10%.
|
|
262
|
+
sampling: { query: 0.1 }
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
// Correct — scrub at the store boundary (always on); sample only for retention cost.
|
|
267
|
+
redact: { keys: ['ssn'] },
|
|
268
|
+
sampling: { query: { rate: 0.1, keepErrors: true } },
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Mechanism: sampling decides what gets *stored*, not what gets *scrubbed*; redaction
|
|
272
|
+
runs on every recorded entry regardless of rate.
|
|
273
|
+
Source: `docs/packages/advanced.mdx` (sampling warn callout).
|
|
274
|
+
|
|
275
|
+
### MEDIUM Misreading prune keepLast semantics
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
// Wrong — assuming keepLast caps the WHOLE table at N rows.
|
|
279
|
+
await store.prune(cutoff, 500) // expected: table trimmed to newest 500 overall
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
// Correct — keepLast retains the newest N of the matched-and-doomed set:
|
|
284
|
+
const deleted = await store.prune(cutoff, 500) // deletes older-than-cutoff EXCEPT its newest 500
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Mechanism: the doomed set is computed first (everything older than cutoff), then
|
|
288
|
+
its newest `keepLast` survive; total row count can exceed `keepLast`.
|
|
289
|
+
Source: `docs/concepts/storage.mdx` (Retention and pruning).
|
|
290
|
+
|
|
291
|
+
See also: `telescope-watchers/SKILL.md` — the capture side these bounds protect;
|
|
292
|
+
`telescope-alerts-ai/SKILL.md` — why sampled-out exceptions never page.
|