@dudousxd/nestjs-telescope-mikro-orm 1.0.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 +110 -0
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/mikro-orm-query.watcher.d.ts +43 -0
- package/dist/mikro-orm-query.watcher.d.ts.map +1 -0
- package/dist/mikro-orm-query.watcher.js +62 -0
- package/dist/mikro-orm-query.watcher.js.map +1 -0
- package/dist/mikro-orm-storage.provider.d.ts +51 -0
- package/dist/mikro-orm-storage.provider.d.ts.map +1 -0
- package/dist/mikro-orm-storage.provider.js +348 -0
- package/dist/mikro-orm-storage.provider.js.map +1 -0
- package/dist/n-plus-one.d.ts +9 -0
- package/dist/n-plus-one.d.ts.map +1 -0
- package/dist/n-plus-one.js +28 -0
- package/dist/n-plus-one.js.map +1 -0
- package/dist/query-family-hash.d.ts +3 -0
- package/dist/query-family-hash.d.ts.map +1 -0
- package/dist/query-family-hash.js +26 -0
- package/dist/query-family-hash.js.map +1 -0
- package/dist/telescope-entry.entity.d.ts +19 -0
- package/dist/telescope-entry.entity.d.ts.map +1 -0
- package/dist/telescope-entry.entity.js +37 -0
- package/dist/telescope-entry.entity.js.map +1 -0
- package/dist/telescope-mikro-orm.logger.d.ts +28 -0
- package/dist/telescope-mikro-orm.logger.d.ts.map +1 -0
- package/dist/telescope-mikro-orm.logger.js +49 -0
- package/dist/telescope-mikro-orm.logger.js.map +1 -0
- package/dist/telescope-rollup.entity.d.ts +12 -0
- package/dist/telescope-rollup.entity.d.ts.map +1 -0
- package/dist/telescope-rollup.entity.js +37 -0
- package/dist/telescope-rollup.entity.js.map +1 -0
- package/package.json +70 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# @dudousxd/nestjs-telescope-mikro-orm
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`1dd4db0`](https://github.com/DavideCarvalho/nestjs-telescope/commit/1dd4db0f3cd46d04b35ac112343cfb424c2d3190) - Estimate latency percentiles (p50/p95/p99 in `/stats`) from a pre-aggregated
|
|
8
|
+
latency histogram instead of scanning every raw entry in the window. Rollups now
|
|
9
|
+
carry a fixed-size `histogram` (counts per `LATENCY_BOUNDARIES_MS` cell, plus an
|
|
10
|
+
overflow cell) aggregated at flush time from `durationMs`. When the store is a
|
|
11
|
+
`RollupStore`, `StatsService` queries the histogram rollups for the type+window,
|
|
12
|
+
merges them element-wise, and calls `estimatePercentileFromHistogram`
|
|
13
|
+
(bucket-resolution nearest-rank, O(buckets)); count/max/slow and the
|
|
14
|
+
family/cache/status/exception breakdowns still derive from the raw scan, and
|
|
15
|
+
stores without rollup support keep the exact raw-scan percentile path as a
|
|
16
|
+
fallback.
|
|
17
|
+
|
|
18
|
+
The histogram is persisted and merged additively by every `RollupStore`
|
|
19
|
+
implementation (in-memory, core SQLite, MikroORM, Redis). Legacy rollup
|
|
20
|
+
rows/hashes without a histogram are self-healed (new column/field) and read back
|
|
21
|
+
as an all-zeros array of the canonical length — never NaN, never a crash.
|
|
22
|
+
|
|
23
|
+
- [`00e0539`](https://github.com/DavideCarvalho/nestjs-telescope/commit/00e05393b00047258622be093d0006af1999d300) - `MikroOrmStorageProvider` now implements the `RollupStore` SPI, so production
|
|
24
|
+
MySQL deployments get rollup-backed timeseries automatically (the Recorder's
|
|
25
|
+
ingest hook and the timeseries service both detect the provider via
|
|
26
|
+
`isRollupStore`). Adds a `TelescopeRollup` entity mapping the `telescope_rollups`
|
|
27
|
+
table (composite PK `(metric, bucket_start)` with additive `count`/`sum` and a
|
|
28
|
+
running `max`), registered in the provider's owned ORM so `schema.update({ safe:
|
|
29
|
+
true })` self-heals it additively at boot — no migration. `recordRollups`
|
|
30
|
+
accumulates per `(metric, bucketStart)` inside a transaction (read-modify-write,
|
|
31
|
+
portable across MySQL/SQLite); `queryRollups` filters by metric set and an
|
|
32
|
+
inclusive bucket range; `clear()` now also empties the rollups table.
|
|
33
|
+
|
|
34
|
+
- [`e14ac60`](https://github.com/DavideCarvalho/nestjs-telescope/commit/e14ac603551372fc3767c63a349c509582b5e6ab) - Add `MikroOrmStorageProvider`: persist Telescope entries to MySQL/SQLite through
|
|
35
|
+
MikroORM with zero migration and zero manual table setup.
|
|
36
|
+
|
|
37
|
+
The provider owns a **dedicated MikroORM scoped to a single `TelescopeEntry`** and
|
|
38
|
+
**self-heals its schema at boot** (`schema.ensureDatabase()` +
|
|
39
|
+
`schema.update({ safe: true })` — additive only: creates the table and adds
|
|
40
|
+
missing columns, never drops). The scoped connection means the schema diff is
|
|
41
|
+
structurally incapable of touching the host's other tables, and it avoids a
|
|
42
|
+
self-capture loop with the query watcher's `loggerFactory`. Pass your existing
|
|
43
|
+
`MikroORM` (its connection config is borrowed) or explicit connection options to
|
|
44
|
+
run Telescope on a separate database. `ensureSchema: false` opts out.
|
|
45
|
+
|
|
46
|
+
Core gains an optional `StorageProvider.init()` boot hook (awaited by
|
|
47
|
+
`TelescopeModule` at startup) and now always calls `close()` after the final
|
|
48
|
+
flush at shutdown — providers that borrow a host-owned resource must no-op
|
|
49
|
+
`close()`. Features newest-first keyset pagination via an `$or` `(createdAt, id)`
|
|
50
|
+
position and cross-driver tag filtering against a space-padded `tagsText` column.
|
|
51
|
+
|
|
52
|
+
- [`d507547`](https://github.com/DavideCarvalho/nestjs-telescope/commit/d507547df3c13e76e90b8a97c4e3e1d8aef25bd1) - Add OpenTelemetry trace linking. The new `@dudousxd/nestjs-telescope-otel`
|
|
53
|
+
package implements core's `TraceContextProvider` SPI by reading the active
|
|
54
|
+
OpenTelemetry span via `@opentelemetry/api` (`OtelTraceContextProvider`,
|
|
55
|
+
optional peer, read-only — never creates or exports spans, degrades to `null`
|
|
56
|
+
when no span/API is present). Core gains the `TraceContextProvider` SPI,
|
|
57
|
+
`traceId`/`spanId` on each `Entry`, and a `meta.traceLink` template so the UI
|
|
58
|
+
can deep-link entries to a trace backend; the MikroORM storage provider adds the
|
|
59
|
+
corresponding trace columns so the ids survive persistence. The Redis storage
|
|
60
|
+
provider now reconstructs `traceId`/`spanId` when reading entries back, and the
|
|
61
|
+
dashboard shows a Trace row in the entry detail with a clickable deep-link when
|
|
62
|
+
`traceLink` is configured.
|
|
63
|
+
|
|
64
|
+
### Patch Changes
|
|
65
|
+
|
|
66
|
+
- [`090cd1f`](https://github.com/DavideCarvalho/nestjs-telescope/commit/090cd1ff871dbe46c1c877a26f90496550b5304c) - Dashboard insights, charts, and trace grouping. Core gains a `StatsService` +
|
|
67
|
+
`GET /stats` endpoint with per-type analytics (latency p50/p95/p99, query-family
|
|
68
|
+
breakdown, cache hit/miss ratio, request status breakdown) and an `EntryQuery.traceId`
|
|
69
|
+
filter across all storages. The schedule watcher now tags scheduled runs with a stable
|
|
70
|
+
`schedule` tag. The dashboard adds a per-type insights header with Recharts cards, cache
|
|
71
|
+
hit/miss badges, a working Schedule view (job entries tagged `schedule`), and a
|
|
72
|
+
`#/traces/:traceId` page that groups every entry sharing an OpenTelemetry trace.
|
|
73
|
+
|
|
74
|
+
- [`6a4d8d5`](https://github.com/DavideCarvalho/nestjs-telescope/commit/6a4d8d56321d3840fe64e646130ccfdafcfb1bdd) - Batch pulse two-pass hydration into ONE query. `EntryQuery` gains an optional
|
|
75
|
+
`ids?: string[]` filter: when set, only entries whose `id` is in the set are
|
|
76
|
+
returned (ANDed with every other filter; an empty array returns no rows). Each
|
|
77
|
+
storage applies it natively — SQLite via `id IN (...)`, MikroORM via
|
|
78
|
+
`id: { $in }`, in-memory via a membership `Set`, and Redis via a single `MGET`
|
|
79
|
+
of the entry keys (the big win — it skips the index scan entirely). The pulse
|
|
80
|
+
service now hydrates the displayed rows (top-N slowest + exception reps + N+1
|
|
81
|
+
reps) with one batched `get({ ids })` instead of N per-id `find()` round-trips,
|
|
82
|
+
cutting pulse latency against a remote store. Pulse output shape is unchanged.
|
|
83
|
+
|
|
84
|
+
- [`c4222b1`](https://github.com/DavideCarvalho/nestjs-telescope/commit/c4222b16ef4c0fc9c61694eb67033f03369ff24e) - Speed up the `/pulse` and `/timeseries` analytics endpoints by aggregating over
|
|
85
|
+
a content-less projection instead of reading every entry's heavy `content` JSON
|
|
86
|
+
blob.
|
|
87
|
+
|
|
88
|
+
`EntryQuery` gains an `omitContent?: boolean` flag. When set, `get()` returns
|
|
89
|
+
entries with `content: null` and the provider skips reading/parsing the content
|
|
90
|
+
column where its driver allows:
|
|
91
|
+
|
|
92
|
+
- **mikro-orm**: projects every persisted column EXCEPT `content` via MikroORM's
|
|
93
|
+
`fields` option, so the blob never leaves the database (the main win on MySQL).
|
|
94
|
+
- **sqlite**: SELECTs the content-less column list and skips JSON parsing.
|
|
95
|
+
- **in-memory**: returns shallow copies with `content` nulled.
|
|
96
|
+
- **redis**: stores the whole entry as one blob, so it must read it anyway —
|
|
97
|
+
`omitContent` nulls `content` after parse (correct, but no projection benefit).
|
|
98
|
+
|
|
99
|
+
`timeseries` now scans content-less (bucketing only needs `createdAt`/`type`).
|
|
100
|
+
`pulse` runs two passes: pass 1 aggregates the whole window content-less
|
|
101
|
+
(counts, slowest candidates, exception families, N+1 hotspots); pass 2 hydrates
|
|
102
|
+
`content` for ONLY the few displayed rows (top-N slowest, one representative per
|
|
103
|
+
reported exception family, one per reported N+1 family) via targeted `find`s.
|
|
104
|
+
The pulse output shape is unchanged.
|
|
105
|
+
|
|
106
|
+
At ~10k entries with large content blobs this drops pulse and timeseries from
|
|
107
|
+
several seconds to a fraction of a second.
|
|
108
|
+
|
|
109
|
+
- Updated dependencies [[`73b50ad`](https://github.com/DavideCarvalho/nestjs-telescope/commit/73b50ad00193127271fdec36ad080d2858045922), [`9126bb0`](https://github.com/DavideCarvalho/nestjs-telescope/commit/9126bb04777cdaec6af3b0a1c5fe6f91d055ce82), [`1f00e62`](https://github.com/DavideCarvalho/nestjs-telescope/commit/1f00e62c8e60482b64251813680a5f866ef1619a), [`090cd1f`](https://github.com/DavideCarvalho/nestjs-telescope/commit/090cd1ff871dbe46c1c877a26f90496550b5304c), [`b7326b3`](https://github.com/DavideCarvalho/nestjs-telescope/commit/b7326b33d8d55b5f1ac5de4256f5e1980278699e), [`bfc0e26`](https://github.com/DavideCarvalho/nestjs-telescope/commit/bfc0e268388b5563d05c24e4de6ff99c74d1201a), [`7797a2a`](https://github.com/DavideCarvalho/nestjs-telescope/commit/7797a2a1554aff49bb59f5ca1b204974a7e04a41), [`6817fe6`](https://github.com/DavideCarvalho/nestjs-telescope/commit/6817fe62775b1ff847fdb1038d3298e7709569e0), [`20ceb87`](https://github.com/DavideCarvalho/nestjs-telescope/commit/20ceb878cd4495dfbc7a3c71d882ae216a633757), [`1dd4db0`](https://github.com/DavideCarvalho/nestjs-telescope/commit/1dd4db0f3cd46d04b35ac112343cfb424c2d3190), [`a9f517c`](https://github.com/DavideCarvalho/nestjs-telescope/commit/a9f517c076461aef55cfb90d072ec38427ace91b), [`9d8eb65`](https://github.com/DavideCarvalho/nestjs-telescope/commit/9d8eb6562a7584801d5aa8b74491091f0fade5f9), [`418f1f0`](https://github.com/DavideCarvalho/nestjs-telescope/commit/418f1f0421948b40b25f845441a716fa4c6655c2), [`e76980d`](https://github.com/DavideCarvalho/nestjs-telescope/commit/e76980ddd7ee740f1b337a422a98ca98d97a007e), [`80c8f97`](https://github.com/DavideCarvalho/nestjs-telescope/commit/80c8f9769c8ab9ee724635086740910bc4d44ea3), [`e14ac60`](https://github.com/DavideCarvalho/nestjs-telescope/commit/e14ac603551372fc3767c63a349c509582b5e6ab), [`6fa0946`](https://github.com/DavideCarvalho/nestjs-telescope/commit/6fa0946f5543868704864af2e32793eb448ac827), [`d507547`](https://github.com/DavideCarvalho/nestjs-telescope/commit/d507547df3c13e76e90b8a97c4e3e1d8aef25bd1), [`affd07e`](https://github.com/DavideCarvalho/nestjs-telescope/commit/affd07e4cb9ee85cfabaabed424833e8c638d04a), [`6a4d8d5`](https://github.com/DavideCarvalho/nestjs-telescope/commit/6a4d8d56321d3840fe64e646130ccfdafcfb1bdd), [`c1f1ec9`](https://github.com/DavideCarvalho/nestjs-telescope/commit/c1f1ec903d470d6b884924e2713de305b61b7481), [`cad6dae`](https://github.com/DavideCarvalho/nestjs-telescope/commit/cad6dae0dba4f22e476d78c23ce2f74f7f6848e4), [`c8596c8`](https://github.com/DavideCarvalho/nestjs-telescope/commit/c8596c85712880cb235e8cce059a1d93d339e9bd), [`c4222b1`](https://github.com/DavideCarvalho/nestjs-telescope/commit/c4222b16ef4c0fc9c61694eb67033f03369ff24e), [`de29d2f`](https://github.com/DavideCarvalho/nestjs-telescope/commit/de29d2f519b1f25bc702c9dcc737d99b4751c8c9), [`10a3bc2`](https://github.com/DavideCarvalho/nestjs-telescope/commit/10a3bc224f6e0b1a237e1e7631acad70493b4c12), [`abde392`](https://github.com/DavideCarvalho/nestjs-telescope/commit/abde39264effb31b0524cc4fa89a335276c8dccb), [`8ff32a2`](https://github.com/DavideCarvalho/nestjs-telescope/commit/8ff32a2cc95775224eea3377460d91674dfda47f), [`5f2eddd`](https://github.com/DavideCarvalho/nestjs-telescope/commit/5f2eddd0ed5d72bf1de323b45870d7ddcaf64349), [`b14a201`](https://github.com/DavideCarvalho/nestjs-telescope/commit/b14a20175eae3d3017e8cbc068d367a03f634175), [`a90ef56`](https://github.com/DavideCarvalho/nestjs-telescope/commit/a90ef569ba12484bece07d8de2045e13ff2ff528), [`593bcc8`](https://github.com/DavideCarvalho/nestjs-telescope/commit/593bcc85ad6558040c62ba66bd1e5e0cbe5a6ac7), [`7b6636b`](https://github.com/DavideCarvalho/nestjs-telescope/commit/7b6636b54438427cd53ea0cbedd186b77d807169), [`e64f35a`](https://github.com/DavideCarvalho/nestjs-telescope/commit/e64f35a1bb7cae15b2ef24404888463d04f81eef), [`4892ef6`](https://github.com/DavideCarvalho/nestjs-telescope/commit/4892ef61e45e5486d34c7ec82764e6767fe8233d)]:
|
|
110
|
+
- @dudousxd/nestjs-telescope@1.0.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Davi Carvalho
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# @dudousxd/nestjs-telescope-mikro-orm
|
|
2
|
+
|
|
3
|
+
> MikroORM query watcher for
|
|
4
|
+
> [@dudousxd/nestjs-telescope](https://github.com/DavideCarvalho/nestjs-telescope).
|
|
5
|
+
> Captures every executed SQL query, correlates it to the request that triggered
|
|
6
|
+
> it, tags slow queries, and exposes a pure N+1 detector. Also ships a
|
|
7
|
+
> `MikroOrmStorageProvider` so you can persist Telescope entries in your own
|
|
8
|
+
> MySQL/SQLite instead of Redis.
|
|
9
|
+
|
|
10
|
+
**Status:** early development (`0.0.0`). Requires MikroORM v7+.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @dudousxd/nestjs-telescope @dudousxd/nestjs-telescope-mikro-orm
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## How query capture works
|
|
19
|
+
|
|
20
|
+
MikroORM v7 stores its logger in a private field set once at `Configuration`
|
|
21
|
+
construction time. Runtime logger replacement is not possible after the ORM is
|
|
22
|
+
built. Because of this, **the host must wire `loggerFactory` before constructing
|
|
23
|
+
the ORM** — that is the real, working integration path.
|
|
24
|
+
|
|
25
|
+
`MikroOrmQueryWatcher` is still registered in `watchers` (it marks the query
|
|
26
|
+
entry type and logs a clear warning if `loggerFactory` is missing), but it does
|
|
27
|
+
not capture queries on its own. The `loggerFactory` is the capture mechanism.
|
|
28
|
+
|
|
29
|
+
## Setup
|
|
30
|
+
|
|
31
|
+
Use `MikroOrmModule.forRootAsync` so `TelescopeService` can be injected before
|
|
32
|
+
MikroORM is constructed:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { TelescopeModule, TelescopeService } from '@dudousxd/nestjs-telescope';
|
|
36
|
+
import {
|
|
37
|
+
MikroOrmQueryWatcher,
|
|
38
|
+
telescopeMikroOrmLogger,
|
|
39
|
+
} from '@dudousxd/nestjs-telescope-mikro-orm';
|
|
40
|
+
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
|
41
|
+
|
|
42
|
+
@Module({
|
|
43
|
+
imports: [
|
|
44
|
+
TelescopeModule.forRoot({
|
|
45
|
+
authorizer: () => true, // restrict in production
|
|
46
|
+
watchers: [new MikroOrmQueryWatcher()],
|
|
47
|
+
}),
|
|
48
|
+
|
|
49
|
+
// forRootAsync lets us inject TelescopeService, which isn't available
|
|
50
|
+
// at module-definition time.
|
|
51
|
+
MikroOrmModule.forRootAsync({
|
|
52
|
+
inject: [TelescopeService],
|
|
53
|
+
useFactory: (telescope: TelescopeService) => ({
|
|
54
|
+
// ... your driver, dbName, entities, etc.
|
|
55
|
+
debug: ['query'],
|
|
56
|
+
loggerFactory: telescopeMikroOrmLogger(
|
|
57
|
+
(input) => telescope.record(input),
|
|
58
|
+
{ slowMs: 100 }, // queries >= 100 ms get a 'slow' tag
|
|
59
|
+
),
|
|
60
|
+
}),
|
|
61
|
+
}),
|
|
62
|
+
],
|
|
63
|
+
})
|
|
64
|
+
export class AppModule {}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Queries recorded via `telescopeMikroOrmLogger` automatically inherit the active
|
|
68
|
+
ALS batch opened by the request middleware, so each query entry is correlated to
|
|
69
|
+
the request that triggered it.
|
|
70
|
+
|
|
71
|
+
## N+1 detection
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { detectNPlusOne } from '@dudousxd/nestjs-telescope-mikro-orm';
|
|
75
|
+
|
|
76
|
+
// Pass the query entries for a single request batch and a repeat threshold.
|
|
77
|
+
const insights = detectNPlusOne(batchEntries, 5);
|
|
78
|
+
// insights: Array<{ familyHash: string; count: number; sql: string }>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`detectNPlusOne` is a pure function — no I/O, no side effects. Feed it the
|
|
82
|
+
entries for one batch (filter by `batchId`) and it returns every query template
|
|
83
|
+
that ran at least `threshold` times.
|
|
84
|
+
|
|
85
|
+
## MySQL / SQLite storage
|
|
86
|
+
|
|
87
|
+
`MikroOrmStorageProvider` persists Telescope entries to your application's own
|
|
88
|
+
database through MikroORM, so you can use your existing MySQL (or SQLite) for
|
|
89
|
+
Telescope instead of standing up Redis. **No migration and no manual table
|
|
90
|
+
setup** — the provider owns a dedicated MikroORM scoped to a single
|
|
91
|
+
`TelescopeEntry` and self-heals its schema at boot. Just hand it your existing
|
|
92
|
+
`MikroORM`:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { TelescopeModule } from '@dudousxd/nestjs-telescope';
|
|
96
|
+
import { MikroOrmStorageProvider } from '@dudousxd/nestjs-telescope-mikro-orm';
|
|
97
|
+
import { MikroORM } from '@mikro-orm/core';
|
|
98
|
+
|
|
99
|
+
@Module({
|
|
100
|
+
imports: [
|
|
101
|
+
// your normal MikroOrmModule.forRoot({ ... }) — nothing to add here
|
|
102
|
+
TelescopeModule.forRootAsync({
|
|
103
|
+
inject: [MikroORM],
|
|
104
|
+
useFactory: (orm: MikroORM) => ({
|
|
105
|
+
// borrows the host's connection config; opens its own scoped connection
|
|
106
|
+
storage: new MikroOrmStorageProvider(orm),
|
|
107
|
+
}),
|
|
108
|
+
}),
|
|
109
|
+
],
|
|
110
|
+
})
|
|
111
|
+
export class AppModule {}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Want Telescope on a **separate database** (recommended for high write volume)?
|
|
115
|
+
Pass explicit MikroORM connection options instead of the host `MikroORM`:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { MySqlDriver } from '@mikro-orm/mysql';
|
|
119
|
+
|
|
120
|
+
new MikroOrmStorageProvider({
|
|
121
|
+
driver: MySqlDriver,
|
|
122
|
+
clientUrl: process.env.TELESCOPE_DATABASE_URL,
|
|
123
|
+
// ensureSchema: false, // opt out of boot-time schema sync (e.g. you manage it via migration)
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
How it works:
|
|
128
|
+
|
|
129
|
+
- **Dedicated, scoped connection.** The provider opens its OWN MikroORM that
|
|
130
|
+
knows *only* `TelescopeEntry`. Two reasons this matters: (1) the schema diff
|
|
131
|
+
can only ever touch `telescope_entries` — it is structurally incapable of
|
|
132
|
+
altering your other tables; (2) it avoids a self-capture loop — if storage
|
|
133
|
+
writes ran on the host connection (which has the query watcher's
|
|
134
|
+
`loggerFactory` wired), every `INSERT INTO telescope_entries` would be captured
|
|
135
|
+
as a query entry, recorded, flushed, and re-captured.
|
|
136
|
+
- **Self-healing schema at boot.** On `init()` the provider runs
|
|
137
|
+
`schema.ensureDatabase()` + `schema.update({ safe: true })` — additive only:
|
|
138
|
+
it creates `telescope_entries` if missing and adds any missing columns, and
|
|
139
|
+
**never drops** a table or column. Set `ensureSchema: false` to disable (e.g.
|
|
140
|
+
when you provision the table via your own migration).
|
|
141
|
+
- **Lifecycle.** `init()` is awaited by `TelescopeModule` at startup; `close()`
|
|
142
|
+
closes the owned connection at shutdown. You don't call either yourself.
|
|
143
|
+
- **Keyset pagination** is newest-first (`createdAt DESC, id DESC`); the cursor
|
|
144
|
+
encodes a `(createdAt, id)` position via `$or`, so paging resumes correctly
|
|
145
|
+
even after older entries are pruned.
|
|
146
|
+
- **Tag filtering** runs against a space-padded `tagsText` column with
|
|
147
|
+
`LIKE '% <tag> %'` (JSON-array predicates are not portable across MySQL and
|
|
148
|
+
SQLite). The `tags` JSON column remains the source of truth for retrieval.
|
|
149
|
+
|
|
150
|
+
## Exports
|
|
151
|
+
|
|
152
|
+
| Export | Description |
|
|
153
|
+
|--------|-------------|
|
|
154
|
+
| `MikroOrmQueryWatcher` | Watcher marker — add to `TelescopeModule.forRoot({ watchers })` |
|
|
155
|
+
| `telescopeMikroOrmLogger(record, opts?)` | `loggerFactory`-compatible factory; `opts.slowMs` defaults to `100` |
|
|
156
|
+
| `TelescopeMikroOrmLogger` | `DefaultLogger` subclass (advanced: extend or instantiate directly) |
|
|
157
|
+
| `MikroOrmStorageProvider` | `StorageProvider` persisting entries to MySQL/SQLite; owns a dedicated scoped connection and self-heals its schema at boot |
|
|
158
|
+
| `TelescopeEntry` | `EntitySchema` for table `telescope_entries` (owned internally by the provider; exported for advanced use — you don't register it) |
|
|
159
|
+
| `detectNPlusOne(entries, threshold)` | Pure N+1 detector |
|
|
160
|
+
| `queryFamilyHash(sql)` | SQL template normalizer + FNV-1a hash (used internally) |
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT © Davi Carvalho
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { queryFamilyHash } from '@dudousxd/nestjs-telescope';
|
|
2
|
+
export { detectNPlusOne, type NPlusOneInsight } from '@dudousxd/nestjs-telescope';
|
|
3
|
+
export * from './telescope-mikro-orm.logger.js';
|
|
4
|
+
export * from './mikro-orm-query.watcher.js';
|
|
5
|
+
export * from './telescope-entry.entity.js';
|
|
6
|
+
export * from './mikro-orm-storage.provider.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClF,cAAc,iCAAiC,CAAC;AAChD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iCAAiC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { queryFamilyHash } from '@dudousxd/nestjs-telescope';
|
|
2
|
+
export { detectNPlusOne } from '@dudousxd/nestjs-telescope';
|
|
3
|
+
export * from './telescope-mikro-orm.logger.js';
|
|
4
|
+
export * from './mikro-orm-query.watcher.js';
|
|
5
|
+
export * from './telescope-entry.entity.js';
|
|
6
|
+
export * from './mikro-orm-storage.provider.js';
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAwB,MAAM,4BAA4B,CAAC;AAClF,cAAc,iCAAiC,CAAC;AAChD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iCAAiC,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Watcher, WatcherContext } from '@dudousxd/nestjs-telescope';
|
|
2
|
+
export interface MikroOrmQueryWatcherOptions {
|
|
3
|
+
slowMs?: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Captures MikroORM queries and correlates them to the active request batch.
|
|
7
|
+
*
|
|
8
|
+
* ## Integration path: host-wired loggerFactory (v7.x)
|
|
9
|
+
*
|
|
10
|
+
* MikroORM v7 caches the logger instance at `Configuration` constructor time
|
|
11
|
+
* (stored in a private `#logger` field). Runtime `config.set('loggerFactory')`
|
|
12
|
+
* does NOT update the cached logger, so zero-config runtime-wrap is not
|
|
13
|
+
* possible on v7.
|
|
14
|
+
*
|
|
15
|
+
* Instead, `register()` logs a clear warning and exposes `record` via
|
|
16
|
+
* `ctx.record`. The host must wire the logger manually:
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { telescopeMikroOrmLogger } from '@dudousxd/nestjs-telescope-mikro-orm';
|
|
20
|
+
*
|
|
21
|
+
* // In your MikroORM config, before the ORM is constructed:
|
|
22
|
+
* MikroOrmModule.forRootAsync({
|
|
23
|
+
* inject: [TelescopeService],
|
|
24
|
+
* useFactory: (telescope: TelescopeService) => ({
|
|
25
|
+
* debug: ['query'],
|
|
26
|
+
* loggerFactory: telescopeMikroOrmLogger((input) => telescope.record(input)),
|
|
27
|
+
* // ...rest of config
|
|
28
|
+
* }),
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* Queries recorded via `telescopeMikroOrmLogger` automatically inherit the
|
|
33
|
+
* active ALS batch (set by the request middleware's `enterWith`), so they
|
|
34
|
+
* correlate to their request.
|
|
35
|
+
*/
|
|
36
|
+
export declare class MikroOrmQueryWatcher implements Watcher {
|
|
37
|
+
private readonly options;
|
|
38
|
+
readonly type: "query";
|
|
39
|
+
private readonly logger;
|
|
40
|
+
constructor(options?: MikroOrmQueryWatcherOptions);
|
|
41
|
+
register(_ctx: WatcherContext): void;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=mikro-orm-query.watcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mikro-orm-query.watcher.d.ts","sourceRoot":"","sources":["../src/mikro-orm-query.watcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAK1E,MAAM,WAAW,2BAA2B;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,qBAAa,oBAAqB,YAAW,OAAO;IAItC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,QAAQ,CAAC,IAAI,UAAmB;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyC;gBAEnC,OAAO,GAAE,2BAAgC;IAEtE,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;CAqBrC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { EntryType } from '@dudousxd/nestjs-telescope';
|
|
2
|
+
// packages/mikro-orm/src/mikro-orm-query.watcher.ts
|
|
3
|
+
import { Logger } from '@nestjs/common';
|
|
4
|
+
/**
|
|
5
|
+
* Captures MikroORM queries and correlates them to the active request batch.
|
|
6
|
+
*
|
|
7
|
+
* ## Integration path: host-wired loggerFactory (v7.x)
|
|
8
|
+
*
|
|
9
|
+
* MikroORM v7 caches the logger instance at `Configuration` constructor time
|
|
10
|
+
* (stored in a private `#logger` field). Runtime `config.set('loggerFactory')`
|
|
11
|
+
* does NOT update the cached logger, so zero-config runtime-wrap is not
|
|
12
|
+
* possible on v7.
|
|
13
|
+
*
|
|
14
|
+
* Instead, `register()` logs a clear warning and exposes `record` via
|
|
15
|
+
* `ctx.record`. The host must wire the logger manually:
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { telescopeMikroOrmLogger } from '@dudousxd/nestjs-telescope-mikro-orm';
|
|
19
|
+
*
|
|
20
|
+
* // In your MikroORM config, before the ORM is constructed:
|
|
21
|
+
* MikroOrmModule.forRootAsync({
|
|
22
|
+
* inject: [TelescopeService],
|
|
23
|
+
* useFactory: (telescope: TelescopeService) => ({
|
|
24
|
+
* debug: ['query'],
|
|
25
|
+
* loggerFactory: telescopeMikroOrmLogger((input) => telescope.record(input)),
|
|
26
|
+
* // ...rest of config
|
|
27
|
+
* }),
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Queries recorded via `telescopeMikroOrmLogger` automatically inherit the
|
|
32
|
+
* active ALS batch (set by the request middleware's `enterWith`), so they
|
|
33
|
+
* correlate to their request.
|
|
34
|
+
*/
|
|
35
|
+
export class MikroOrmQueryWatcher {
|
|
36
|
+
options;
|
|
37
|
+
type = EntryType.Query;
|
|
38
|
+
logger = new Logger(MikroOrmQueryWatcher.name);
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
this.options = options;
|
|
41
|
+
}
|
|
42
|
+
register(_ctx) {
|
|
43
|
+
this.logger.warn([
|
|
44
|
+
'MikroOrmQueryWatcher: MikroORM v7 caches its logger at construction time,',
|
|
45
|
+
'so runtime logger replacement is not possible.',
|
|
46
|
+
'To capture queries, add loggerFactory + debug to your MikroORM config:',
|
|
47
|
+
'',
|
|
48
|
+
" import { telescopeMikroOrmLogger } from '@dudousxd/nestjs-telescope-mikro-orm';",
|
|
49
|
+
'',
|
|
50
|
+
' MikroOrmModule.forRootAsync({',
|
|
51
|
+
' inject: [TelescopeService],',
|
|
52
|
+
' useFactory: (telescope: TelescopeService) => ({',
|
|
53
|
+
" debug: ['query'],",
|
|
54
|
+
' loggerFactory: telescopeMikroOrmLogger((input) => telescope.record(input)),',
|
|
55
|
+
' }),',
|
|
56
|
+
' });',
|
|
57
|
+
].join('\n'));
|
|
58
|
+
// slowMs option is available for consumers who use telescopeMikroOrmLogger directly.
|
|
59
|
+
void this.options.slowMs;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=mikro-orm-query.watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mikro-orm-query.watcher.js","sourceRoot":"","sources":["../src/mikro-orm-query.watcher.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,oDAAoD;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAMxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,OAAO,oBAAoB;IAIF;IAHpB,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC;IACf,MAAM,GAAG,IAAI,MAAM,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC;IAEhE,YAA6B,UAAuC,EAAE;QAAzC,YAAO,GAAP,OAAO,CAAkC;IAAG,CAAC;IAE1E,QAAQ,CAAC,IAAoB;QAC3B,IAAI,CAAC,MAAM,CAAC,IAAI,CACd;YACE,2EAA2E;YAC3E,gDAAgD;YAChD,wEAAwE;YACxE,EAAE;YACF,mFAAmF;YACnF,EAAE;YACF,iCAAiC;YACjC,iCAAiC;YACjC,qDAAqD;YACrD,yBAAyB;YACzB,mFAAmF;YACnF,SAAS;YACT,OAAO;SACR,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;QACF,qFAAqF;QACrF,KAAK,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;IAC3B,CAAC;CACF"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Entry, EntryQuery, EntryWithBatch, Page, RollupBucket, RollupDelta, RollupStore, StorageProvider, TagCount } from '@dudousxd/nestjs-telescope';
|
|
2
|
+
import type { Options } from '@mikro-orm/core';
|
|
3
|
+
import { MikroORM } from '@mikro-orm/core';
|
|
4
|
+
/**
|
|
5
|
+
* Options for the dedicated, single-entity ORM the provider owns. These are
|
|
6
|
+
* standard MikroORM `Options` (connection config) plus an optional
|
|
7
|
+
* `ensureSchema` flag. Any `entities` provided here is overridden — the provider
|
|
8
|
+
* always forces `entities: [TelescopeEntry, TelescopeRollup]`.
|
|
9
|
+
*/
|
|
10
|
+
export type MikroOrmStorageProviderOptions = Options & {
|
|
11
|
+
ensureSchema?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export declare class MikroOrmStorageProvider implements StorageProvider, RollupStore {
|
|
14
|
+
private readonly source;
|
|
15
|
+
private readonly ensureSchema;
|
|
16
|
+
private orm;
|
|
17
|
+
constructor(source: MikroORM | MikroOrmStorageProviderOptions, opts?: {
|
|
18
|
+
ensureSchema?: boolean;
|
|
19
|
+
});
|
|
20
|
+
private buildOrmOptions;
|
|
21
|
+
init(): Promise<void>;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
private forkEm;
|
|
24
|
+
store(entries: Entry[]): Promise<void>;
|
|
25
|
+
update(id: string, patch: Partial<Entry>): Promise<void>;
|
|
26
|
+
find(id: string): Promise<EntryWithBatch | null>;
|
|
27
|
+
get(query: EntryQuery): Promise<Page<Entry>>;
|
|
28
|
+
batch(batchId: string): Promise<Entry[]>;
|
|
29
|
+
tags(prefix?: string): Promise<TagCount[]>;
|
|
30
|
+
prune(olderThan: Date, keepLast?: number): Promise<number>;
|
|
31
|
+
clear(): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Additively merge deltas into `telescope_rollups`, matching the sqlite
|
|
34
|
+
* provider's `on conflict do update set count = count + excluded.count, ...`
|
|
35
|
+
* semantics: accumulate count/sum and keep a running max per
|
|
36
|
+
* (metric, bucketStart).
|
|
37
|
+
*
|
|
38
|
+
* Implemented as a read-modify-write inside ONE transaction rather than a
|
|
39
|
+
* native upsert. MikroORM's `em.upsert` / `qb.onConflict().merge()` REPLACE
|
|
40
|
+
* conflicting columns; neither expresses a portable `count = count +
|
|
41
|
+
* excluded.count` self-referential increment across MySQL and SQLite without
|
|
42
|
+
* driver-specific raw SQL. The transactional read-modify-write is portable,
|
|
43
|
+
* cast-free, and — because the whole batch commits atomically with row locks
|
|
44
|
+
* on the touched PKs — safe to call repeatedly and concurrently: concurrent
|
|
45
|
+
* writers serialize on the same (metric, bucketStart) rows, so no increment is
|
|
46
|
+
* lost. New buckets are inserted; existing ones accumulate.
|
|
47
|
+
*/
|
|
48
|
+
recordRollups(deltas: RollupDelta[]): Promise<void>;
|
|
49
|
+
queryRollups(metrics: string[], fromBucket: number, toBucket: number): Promise<RollupBucket[]>;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=mikro-orm-storage.provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mikro-orm-storage.provider.d.ts","sourceRoot":"","sources":["../src/mikro-orm-storage.provider.ts"],"names":[],"mappings":"AAmDA,OAAO,KAAK,EACV,KAAK,EACL,UAAU,EACV,cAAc,EACd,IAAI,EACJ,YAAY,EACZ,WAAW,EACX,WAAW,EACX,eAAe,EACf,QAAQ,EACT,MAAM,4BAA4B,CAAC;AAQpC,OAAO,KAAK,EAA8B,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC3E,OAAO,EAAE,QAAQ,EAAO,MAAM,iBAAiB,CAAC;AAuChD;;;;;GAKG;AACH,MAAM,MAAM,8BAA8B,GAAG,OAAO,GAAG;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AA8DlF,qBAAa,uBAAwB,YAAW,eAAe,EAAE,WAAW;IAC1E,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4C;IACnE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,GAAG,CAAyB;gBAGlC,MAAM,EAAE,QAAQ,GAAG,8BAA8B,EACjD,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE;IAYnC,OAAO,CAAC,eAAe;IAoBjB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAYrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B,OAAO,CAAC,MAAM;IAOR,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAStC,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAcxD,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAQhD,GAAG,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IA+D5C,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAMxC,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAkB1C,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAgB1D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ5B;;;;;;;;;;;;;;;OAeG;IACG,aAAa,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAgCnD,YAAY,CAChB,OAAO,EAAE,MAAM,EAAE,EACjB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,EAAE,CAAC;CAgB3B"}
|