@geekmidas/telescope 9.0.2 → 10.0.0-alpha.1
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/README.md +14 -4
- package/dist/{http-D5kn3_Mt.d.mts → http-BiNtg85L.d.mts} +3 -3
- package/dist/{http-D5kn3_Mt.d.mts.map → http-BiNtg85L.d.mts.map} +1 -1
- package/dist/instrumentation/http.d.mts +1 -1
- package/dist/instrumentation/index.cjs +2 -2
- package/dist/instrumentation/index.d.cts +2 -2
- package/dist/instrumentation/index.d.mts +3 -3
- package/dist/instrumentation/index.mjs +2 -2
- package/dist/instrumentation/middleware.cjs +1 -1
- package/dist/instrumentation/middleware.d.cts +1 -1
- package/dist/instrumentation/middleware.d.mts +1 -1
- package/dist/instrumentation/middleware.mjs +1 -1
- package/dist/instrumentation/otel.cjs +1 -1
- package/dist/instrumentation/otel.d.cts +1 -1
- package/dist/instrumentation/otel.d.mts +1 -1
- package/dist/instrumentation/otel.mjs +1 -1
- package/dist/logger/console.d.cts.map +1 -1
- package/dist/logger/pino.d.mts +2 -2
- package/dist/{middleware-DqPQpcup.cjs → middleware-Bcq6XXGG.cjs} +2 -2
- package/dist/middleware-Bcq6XXGG.cjs.map +1 -0
- package/dist/{middleware-4-GrQlOT.mjs → middleware-D1XEHvyy.mjs} +2 -2
- package/dist/middleware-D1XEHvyy.mjs.map +1 -0
- package/dist/{middleware-CA84nsNN.d.cts → middleware-PeyS-9Ny.d.cts} +2 -2
- package/dist/{middleware-CA84nsNN.d.cts.map → middleware-PeyS-9Ny.d.cts.map} +1 -1
- package/dist/{middleware-DAceVpfw.d.mts → middleware-oHvo_ZgU.d.mts} +2 -2
- package/dist/{middleware-DAceVpfw.d.mts.map → middleware-oHvo_ZgU.d.mts.map} +1 -1
- package/dist/{otel-40udNP5g.mjs → otel-B9bNoeZ2.mjs} +3 -3
- package/dist/otel-B9bNoeZ2.mjs.map +1 -0
- package/dist/{otel-SBVoVEHr.d.cts → otel-Bk89b1lX.d.cts} +3 -3
- package/dist/{otel-1bJPb2Z7.d.mts.map → otel-Bk89b1lX.d.cts.map} +1 -1
- package/dist/{otel-COrpz_GZ.cjs → otel-BzjOtVxZ.cjs} +3 -3
- package/dist/otel-BzjOtVxZ.cjs.map +1 -0
- package/dist/{otel-1bJPb2Z7.d.mts → otel-lPMsM26e.d.mts} +3 -3
- package/dist/{otel-SBVoVEHr.d.cts.map → otel-lPMsM26e.d.mts.map} +1 -1
- package/dist/storage/kysely.cjs +48 -38
- package/dist/storage/kysely.cjs.map +1 -1
- package/dist/storage/kysely.d.cts +29 -9
- package/dist/storage/kysely.d.cts.map +1 -1
- package/dist/storage/kysely.d.mts +29 -9
- package/dist/storage/kysely.d.mts.map +1 -1
- package/dist/storage/kysely.mjs +48 -38
- package/dist/storage/kysely.mjs.map +1 -1
- package/dist/tracing-PnECEbEu.d.mts.map +1 -1
- package/package.json +6 -3
- package/CHANGELOG.md +0 -72
- package/dist/middleware-4-GrQlOT.mjs.map +0 -1
- package/dist/middleware-DqPQpcup.cjs.map +0 -1
- package/dist/otel-40udNP5g.mjs.map +0 -1
- package/dist/otel-COrpz_GZ.cjs.map +0 -1
- package/src/Telescope.ts +0 -561
- package/src/__tests__/Telescope.spec.ts +0 -438
- package/src/__tests__/pino-transport.spec.ts +0 -115
- package/src/__tests__/redact.spec.ts +0 -336
- package/src/adapters/__tests__/lambda.spec.ts +0 -318
- package/src/adapters/__tests__/middy.spec.ts +0 -741
- package/src/adapters/index.ts +0 -22
- package/src/adapters/lambda.ts +0 -432
- package/src/adapters/middy.ts +0 -402
- package/src/adapters/types.ts +0 -181
- package/src/core.ts +0 -51
- package/src/index.ts +0 -34
- package/src/instrumentation/__tests__/core.spec.ts +0 -248
- package/src/instrumentation/__tests__/hono.spec.ts +0 -348
- package/src/instrumentation/__tests__/middleware.spec.ts +0 -528
- package/src/instrumentation/__tests__/otel.spec.ts +0 -291
- package/src/instrumentation/__tests__/tracing.spec.ts +0 -208
- package/src/instrumentation/core.ts +0 -228
- package/src/instrumentation/hono.ts +0 -270
- package/src/instrumentation/http.ts +0 -306
- package/src/instrumentation/index.ts +0 -90
- package/src/instrumentation/middleware.ts +0 -297
- package/src/instrumentation/otel.ts +0 -271
- package/src/instrumentation/setup.ts +0 -246
- package/src/instrumentation/tracing.ts +0 -145
- package/src/logger/__tests__/console.spec.ts +0 -366
- package/src/logger/__tests__/pino.spec.ts +0 -217
- package/src/logger/console.ts +0 -230
- package/src/logger/pino.ts +0 -201
- package/src/metrics/MetricsAggregator.ts +0 -555
- package/src/metrics/__tests__/MetricsAggregator.spec.ts +0 -298
- package/src/metrics/index.ts +0 -2
- package/src/metrics/storage/InMemoryMetricsStorage.ts +0 -380
- package/src/metrics/storage/MetricsStorage.ts +0 -74
- package/src/metrics/storage/index.ts +0 -3
- package/src/metrics/storage/types.ts +0 -243
- package/src/otlp/__tests__/receiver.spec.ts +0 -367
- package/src/otlp/__tests__/transformer.spec.ts +0 -455
- package/src/otlp/hono.ts +0 -120
- package/src/otlp/index.ts +0 -22
- package/src/otlp/receiver.ts +0 -162
- package/src/otlp/transformer.ts +0 -367
- package/src/otlp/types.ts +0 -364
- package/src/redact.ts +0 -116
- package/src/server/__tests__/hono.spec.ts +0 -814
- package/src/server/hono.ts +0 -442
- package/src/storage/__tests__/kysely.spec.ts +0 -715
- package/src/storage/__tests__/memory.spec.ts +0 -694
- package/src/storage/kysely.ts +0 -599
- package/src/storage/memory.ts +0 -204
- package/src/types.ts +0 -447
- package/src/ui-assets.ts +0 -40
- package/tsconfig.json +0 -9
- package/ui/CHANGELOG.md +0 -7
- package/ui/index.html +0 -12
- package/ui/node_modules/.bin/tsc +0 -21
- package/ui/node_modules/.bin/tsserver +0 -21
- package/ui/node_modules/.bin/vite +0 -21
- package/ui/package.json +0 -24
- package/ui/src/App.tsx +0 -595
- package/ui/src/api.ts +0 -88
- package/ui/src/components/ExceptionDetail.tsx +0 -100
- package/ui/src/components/FilterBar.tsx +0 -162
- package/ui/src/components/LogDetail.tsx +0 -91
- package/ui/src/components/RequestDetail.tsx +0 -143
- package/ui/src/main.tsx +0 -10
- package/ui/src/styles.css +0 -11
- package/ui/src/types.ts +0 -63
- package/ui/src/vite-env.d.ts +0 -1
- package/ui/src/vite-plugin-gkm-config.ts +0 -54
- package/ui/tsconfig.json +0 -20
- package/ui/tsconfig.tsbuildinfo +0 -1
- package/ui/vite.config.ts +0 -13
package/dist/storage/kysely.cjs
CHANGED
|
@@ -23,15 +23,20 @@
|
|
|
23
23
|
*/
|
|
24
24
|
var KyselyStorage = class {
|
|
25
25
|
db;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Unqualified, and deliberately.
|
|
28
|
+
*
|
|
29
|
+
* These were `telescope_requests`, `telescope_exceptions` and
|
|
30
|
+
* `telescope_logs` — a prefix, which is what you reach for when the tables
|
|
31
|
+
* have to share a schema with an application's own. They do not: a telescope
|
|
32
|
+
* derived from a database is a schema tenant, so the names are just the
|
|
33
|
+
* names, and `DROP SCHEMA telescope CASCADE` is the whole cleanup.
|
|
34
|
+
*/
|
|
35
|
+
requestsTable = "requests";
|
|
36
|
+
exceptionsTable = "exceptions";
|
|
37
|
+
logsTable = "logs";
|
|
29
38
|
constructor(config) {
|
|
30
|
-
this.db = config.db;
|
|
31
|
-
const prefix = config.tablePrefix ?? "telescope";
|
|
32
|
-
this.requestsTable = `${prefix}_requests`;
|
|
33
|
-
this.exceptionsTable = `${prefix}_exceptions`;
|
|
34
|
-
this.logsTable = `${prefix}_logs`;
|
|
39
|
+
this.db = config.schema ? config.db.withSchema(config.schema) : config.db;
|
|
35
40
|
}
|
|
36
41
|
async saveRequest(entry) {
|
|
37
42
|
const row = this.requestToRow(entry);
|
|
@@ -254,8 +259,11 @@ var KyselyStorage = class {
|
|
|
254
259
|
}
|
|
255
260
|
};
|
|
256
261
|
/**
|
|
257
|
-
* SQL migration to create telescope tables.
|
|
258
|
-
*
|
|
262
|
+
* SQL migration to create telescope's tables.
|
|
263
|
+
*
|
|
264
|
+
* Pass a schema to have them created in one, and to have `down` drop the whole
|
|
265
|
+
* schema rather than three tables. Pass nothing where the connection's
|
|
266
|
+
* `search_path` already places them.
|
|
259
267
|
*
|
|
260
268
|
* @example
|
|
261
269
|
* ```typescript
|
|
@@ -273,11 +281,13 @@ var KyselyStorage = class {
|
|
|
273
281
|
* }
|
|
274
282
|
* ```
|
|
275
283
|
*/
|
|
276
|
-
function getTelescopeMigration(
|
|
284
|
+
function getTelescopeMigration(schema) {
|
|
285
|
+
const q = (name) => schema ? `${schema}.${name}` : name;
|
|
286
|
+
const idx = (name) => schema ? `${schema}_${name}` : name;
|
|
277
287
|
return {
|
|
278
|
-
up: `
|
|
279
|
-
-- Telescope requests
|
|
280
|
-
CREATE TABLE IF NOT EXISTS ${
|
|
288
|
+
up: `${schema ? `CREATE SCHEMA IF NOT EXISTS ${schema};\n` : ""}
|
|
289
|
+
-- Telescope requests
|
|
290
|
+
CREATE TABLE IF NOT EXISTS ${q("requests")} (
|
|
281
291
|
id VARCHAR(21) PRIMARY KEY,
|
|
282
292
|
method VARCHAR(10) NOT NULL,
|
|
283
293
|
path TEXT NOT NULL,
|
|
@@ -295,15 +305,15 @@ CREATE TABLE IF NOT EXISTS ${tablePrefix}_requests (
|
|
|
295
305
|
tags JSONB
|
|
296
306
|
);
|
|
297
307
|
|
|
298
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
299
|
-
ON ${
|
|
300
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
301
|
-
ON ${
|
|
302
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
303
|
-
ON ${
|
|
308
|
+
CREATE INDEX IF NOT EXISTS idx_${idx("requests")}_timestamp
|
|
309
|
+
ON ${q("requests")} (timestamp DESC);
|
|
310
|
+
CREATE INDEX IF NOT EXISTS idx_${idx("requests")}_path
|
|
311
|
+
ON ${q("requests")} (path);
|
|
312
|
+
CREATE INDEX IF NOT EXISTS idx_${idx("requests")}_status
|
|
313
|
+
ON ${q("requests")} (status);
|
|
304
314
|
|
|
305
|
-
-- Telescope exceptions
|
|
306
|
-
CREATE TABLE IF NOT EXISTS ${
|
|
315
|
+
-- Telescope exceptions
|
|
316
|
+
CREATE TABLE IF NOT EXISTS ${q("exceptions")} (
|
|
307
317
|
id VARCHAR(21) PRIMARY KEY,
|
|
308
318
|
name VARCHAR(255) NOT NULL,
|
|
309
319
|
message TEXT NOT NULL,
|
|
@@ -315,13 +325,13 @@ CREATE TABLE IF NOT EXISTS ${tablePrefix}_exceptions (
|
|
|
315
325
|
tags JSONB
|
|
316
326
|
);
|
|
317
327
|
|
|
318
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
319
|
-
ON ${
|
|
320
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
321
|
-
ON ${
|
|
328
|
+
CREATE INDEX IF NOT EXISTS idx_${idx("exceptions")}_timestamp
|
|
329
|
+
ON ${q("exceptions")} (timestamp DESC);
|
|
330
|
+
CREATE INDEX IF NOT EXISTS idx_${idx("exceptions")}_request_id
|
|
331
|
+
ON ${q("exceptions")} (request_id);
|
|
322
332
|
|
|
323
|
-
-- Telescope logs
|
|
324
|
-
CREATE TABLE IF NOT EXISTS ${
|
|
333
|
+
-- Telescope logs
|
|
334
|
+
CREATE TABLE IF NOT EXISTS ${q("logs")} (
|
|
325
335
|
id VARCHAR(21) PRIMARY KEY,
|
|
326
336
|
level VARCHAR(10) NOT NULL,
|
|
327
337
|
message TEXT NOT NULL,
|
|
@@ -330,17 +340,17 @@ CREATE TABLE IF NOT EXISTS ${tablePrefix}_logs (
|
|
|
330
340
|
timestamp TIMESTAMPTZ NOT NULL
|
|
331
341
|
);
|
|
332
342
|
|
|
333
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
334
|
-
ON ${
|
|
335
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
336
|
-
ON ${
|
|
337
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
338
|
-
ON ${
|
|
343
|
+
CREATE INDEX IF NOT EXISTS idx_${idx("logs")}_timestamp
|
|
344
|
+
ON ${q("logs")} (timestamp DESC);
|
|
345
|
+
CREATE INDEX IF NOT EXISTS idx_${idx("logs")}_level
|
|
346
|
+
ON ${q("logs")} (level);
|
|
347
|
+
CREATE INDEX IF NOT EXISTS idx_${idx("logs")}_request_id
|
|
348
|
+
ON ${q("logs")} (request_id);
|
|
339
349
|
`,
|
|
340
|
-
down: `
|
|
341
|
-
DROP TABLE IF EXISTS
|
|
342
|
-
DROP TABLE IF EXISTS
|
|
343
|
-
DROP TABLE IF EXISTS
|
|
350
|
+
down: schema ? `DROP SCHEMA IF EXISTS ${schema} CASCADE;\n` : `
|
|
351
|
+
DROP TABLE IF EXISTS logs;
|
|
352
|
+
DROP TABLE IF EXISTS exceptions;
|
|
353
|
+
DROP TABLE IF EXISTS requests;
|
|
344
354
|
`
|
|
345
355
|
};
|
|
346
356
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kysely.cjs","names":["config: KyselyStorageConfig<DB>","entry: RequestEntry","entries: RequestEntry[]","options?: QueryOptions","row: TelescopeRequestTable","id: string","entry: ExceptionEntry","entries: ExceptionEntry[]","row: TelescopeExceptionTable","entry: LogEntry","entries: LogEntry[]","row: TelescopeLogTable","olderThan: Date","eb: any","query: any","value: unknown"],"sources":["../../src/storage/kysely.ts"],"sourcesContent":["import type { Kysely } from 'kysely';\nimport type {\n\tExceptionEntry,\n\tLogEntry,\n\tQueryOptions,\n\tRequestEntry,\n\tTelescopeStats,\n\tTelescopeStorage,\n} from '../types';\n\n/**\n * Database table interface for telescope requests.\n * Use this to define your telescope_requests table in your Kysely database schema.\n */\nexport interface TelescopeRequestTable {\n\tid: string;\n\tmethod: string;\n\tpath: string;\n\turl: string;\n\theaders: unknown;\n\tbody: unknown | null;\n\tquery: unknown | null;\n\tstatus: number;\n\tresponse_headers: unknown;\n\tresponse_body: unknown | null;\n\tduration: number;\n\ttimestamp: Date;\n\tip: string | null;\n\tuser_id: string | null;\n\ttags: unknown | null;\n}\n\n/**\n * Database table interface for telescope exceptions.\n */\nexport interface TelescopeExceptionTable {\n\tid: string;\n\tname: string;\n\tmessage: string;\n\tstack: unknown;\n\tsource: unknown | null;\n\trequest_id: string | null;\n\ttimestamp: Date;\n\thandled: boolean;\n\ttags: unknown | null;\n}\n\n/**\n * Database table interface for telescope logs.\n */\nexport interface TelescopeLogTable {\n\tid: string;\n\tlevel: string;\n\tmessage: string;\n\tcontext: unknown | null;\n\trequest_id: string | null;\n\ttimestamp: Date;\n}\n\n/**\n * Combined database interface for all telescope tables.\n * Use this to extend your database schema.\n *\n * @example\n * ```typescript\n * import type { TelescopeTables } from '@geekmidas/telescope/storage/kysely';\n *\n * interface Database extends TelescopeTables {\n * users: UserTable;\n * // ... other tables\n * }\n * ```\n */\nexport interface TelescopeTables {\n\ttelescope_requests: TelescopeRequestTable;\n\ttelescope_exceptions: TelescopeExceptionTable;\n\ttelescope_logs: TelescopeLogTable;\n}\n\n/**\n * Configuration for KyselyStorage.\n */\nexport interface KyselyStorageConfig<DB> {\n\t/** Kysely database instance */\n\tdb: Kysely<DB>;\n\t/**\n\t * Table name prefix (default: 'telescope').\n\t * Tables will be named: {prefix}_requests, {prefix}_exceptions, {prefix}_logs\n\t */\n\ttablePrefix?: string;\n}\n\n/**\n * Kysely-based storage implementation for Telescope.\n * Stores telescope data in PostgreSQL, MySQL, or SQLite using Kysely.\n *\n * @template DB - Your Kysely database schema (must include TelescopeTables)\n *\n * @example\n * ```typescript\n * import { Kysely, PostgresDialect } from 'kysely';\n * import { KyselyStorage, type TelescopeTables } from '@geekmidas/telescope/storage/kysely';\n *\n * interface Database extends TelescopeTables {\n * users: UserTable;\n * }\n *\n * const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool }) });\n * const storage = new KyselyStorage({ db });\n *\n * const telescope = new Telescope({ storage });\n * ```\n */\nexport class KyselyStorage<DB> implements TelescopeStorage {\n\tprivate readonly db: Kysely<DB>;\n\tprivate readonly requestsTable: string;\n\tprivate readonly exceptionsTable: string;\n\tprivate readonly logsTable: string;\n\n\tconstructor(config: KyselyStorageConfig<DB>) {\n\t\tthis.db = config.db;\n\t\tconst prefix = config.tablePrefix ?? 'telescope';\n\t\tthis.requestsTable = `${prefix}_requests`;\n\t\tthis.exceptionsTable = `${prefix}_exceptions`;\n\t\tthis.logsTable = `${prefix}_logs`;\n\t}\n\n\t// ============================================\n\t// Requests\n\t// ============================================\n\n\tasync saveRequest(entry: RequestEntry): Promise<void> {\n\t\tconst row = this.requestToRow(entry);\n\t\tawait (this.db as any).insertInto(this.requestsTable).values(row).execute();\n\t}\n\n\tasync saveRequests(entries: RequestEntry[]): Promise<void> {\n\t\tif (entries.length === 0) return;\n\n\t\tconst rows = entries.map((e) => this.requestToRow(e));\n\t\tawait (this.db as any)\n\t\t\t.insertInto(this.requestsTable)\n\t\t\t.values(rows)\n\t\t\t.execute();\n\t}\n\n\tasync getRequests(options?: QueryOptions): Promise<RequestEntry[]> {\n\t\tlet query = (this.db as any)\n\t\t\t.selectFrom(this.requestsTable)\n\t\t\t.selectAll()\n\t\t\t.orderBy('timestamp', 'desc');\n\n\t\t// Apply request-specific filters\n\t\tif (options?.method) {\n\t\t\tquery = query.where('method', '=', options.method);\n\t\t}\n\n\t\tif (options?.status) {\n\t\t\tconst statusFilter = options.status;\n\t\t\tif (statusFilter.endsWith('xx')) {\n\t\t\t\t// Handle status ranges like \"2xx\", \"4xx\", \"5xx\"\n\t\t\t\tconst firstChar = statusFilter[0];\n\t\t\t\tif (firstChar) {\n\t\t\t\t\tconst category = parseInt(firstChar, 10);\n\t\t\t\t\tconst minStatus = category * 100;\n\t\t\t\t\tconst maxStatus = minStatus + 99;\n\t\t\t\t\tquery = query\n\t\t\t\t\t\t.where('status', '>=', minStatus)\n\t\t\t\t\t\t.where('status', '<=', maxStatus);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Handle exact status codes\n\t\t\t\tquery = query.where('status', '=', parseInt(statusFilter, 10));\n\t\t\t}\n\t\t}\n\n\t\tquery = this.applyQueryOptions(query, options);\n\n\t\tconst rows = await query.execute();\n\t\treturn rows.map((row: TelescopeRequestTable) => this.rowToRequest(row));\n\t}\n\n\tasync getRequest(id: string): Promise<RequestEntry | null> {\n\t\tconst row = await (this.db as any)\n\t\t\t.selectFrom(this.requestsTable)\n\t\t\t.selectAll()\n\t\t\t.where('id', '=', id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToRequest(row) : null;\n\t}\n\n\t// ============================================\n\t// Exceptions\n\t// ============================================\n\n\tasync saveException(entry: ExceptionEntry): Promise<void> {\n\t\tconst row = this.exceptionToRow(entry);\n\t\tawait (this.db as any)\n\t\t\t.insertInto(this.exceptionsTable)\n\t\t\t.values(row)\n\t\t\t.execute();\n\t}\n\n\tasync saveExceptions(entries: ExceptionEntry[]): Promise<void> {\n\t\tif (entries.length === 0) return;\n\n\t\tconst rows = entries.map((e) => this.exceptionToRow(e));\n\t\tawait (this.db as any)\n\t\t\t.insertInto(this.exceptionsTable)\n\t\t\t.values(rows)\n\t\t\t.execute();\n\t}\n\n\tasync getExceptions(options?: QueryOptions): Promise<ExceptionEntry[]> {\n\t\tlet query = (this.db as any)\n\t\t\t.selectFrom(this.exceptionsTable)\n\t\t\t.selectAll()\n\t\t\t.orderBy('timestamp', 'desc');\n\n\t\tquery = this.applyQueryOptions(query, options);\n\n\t\tconst rows = await query.execute();\n\t\treturn rows.map((row: TelescopeExceptionTable) => this.rowToException(row));\n\t}\n\n\tasync getException(id: string): Promise<ExceptionEntry | null> {\n\t\tconst row = await (this.db as any)\n\t\t\t.selectFrom(this.exceptionsTable)\n\t\t\t.selectAll()\n\t\t\t.where('id', '=', id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToException(row) : null;\n\t}\n\n\t// ============================================\n\t// Logs\n\t// ============================================\n\n\tasync saveLog(entry: LogEntry): Promise<void> {\n\t\tconst row = this.logToRow(entry);\n\t\tawait (this.db as any).insertInto(this.logsTable).values(row).execute();\n\t}\n\n\tasync saveLogs(entries: LogEntry[]): Promise<void> {\n\t\tif (entries.length === 0) return;\n\n\t\tconst rows = entries.map((e) => this.logToRow(e));\n\t\tawait (this.db as any).insertInto(this.logsTable).values(rows).execute();\n\t}\n\n\tasync getLogs(options?: QueryOptions): Promise<LogEntry[]> {\n\t\tlet query = (this.db as any)\n\t\t\t.selectFrom(this.logsTable)\n\t\t\t.selectAll()\n\t\t\t.orderBy('timestamp', 'desc');\n\n\t\t// Apply log-specific filters\n\t\tif (options?.level) {\n\t\t\tquery = query.where('level', '=', options.level);\n\t\t}\n\n\t\tquery = this.applyQueryOptions(query, options);\n\n\t\tconst rows = await query.execute();\n\t\treturn rows.map((row: TelescopeLogTable) => this.rowToLog(row));\n\t}\n\n\t// ============================================\n\t// Cleanup\n\t// ============================================\n\n\tasync prune(olderThan: Date): Promise<number> {\n\t\tconst results = await Promise.all([\n\t\t\t(this.db as any)\n\t\t\t\t.deleteFrom(this.requestsTable)\n\t\t\t\t.where('timestamp', '<', olderThan)\n\t\t\t\t.executeTakeFirst(),\n\t\t\t(this.db as any)\n\t\t\t\t.deleteFrom(this.exceptionsTable)\n\t\t\t\t.where('timestamp', '<', olderThan)\n\t\t\t\t.executeTakeFirst(),\n\t\t\t(this.db as any)\n\t\t\t\t.deleteFrom(this.logsTable)\n\t\t\t\t.where('timestamp', '<', olderThan)\n\t\t\t\t.executeTakeFirst(),\n\t\t]);\n\n\t\treturn results.reduce(\n\t\t\t(sum, result) => sum + Number(result.numDeletedRows ?? 0),\n\t\t\t0,\n\t\t);\n\t}\n\n\t// ============================================\n\t// Stats\n\t// ============================================\n\n\tasync getStats(): Promise<TelescopeStats> {\n\t\tconst [requestsResult, exceptionsResult, logsResult] = await Promise.all([\n\t\t\t(this.db as any)\n\t\t\t\t.selectFrom(this.requestsTable)\n\t\t\t\t.select((eb: any) => [\n\t\t\t\t\teb.fn.count('id').as('count'),\n\t\t\t\t\teb.fn.min('timestamp').as('oldest'),\n\t\t\t\t\teb.fn.max('timestamp').as('newest'),\n\t\t\t\t])\n\t\t\t\t.executeTakeFirst(),\n\t\t\t(this.db as any)\n\t\t\t\t.selectFrom(this.exceptionsTable)\n\t\t\t\t.select((eb: any) => [\n\t\t\t\t\teb.fn.count('id').as('count'),\n\t\t\t\t\teb.fn.min('timestamp').as('oldest'),\n\t\t\t\t\teb.fn.max('timestamp').as('newest'),\n\t\t\t\t])\n\t\t\t\t.executeTakeFirst(),\n\t\t\t(this.db as any)\n\t\t\t\t.selectFrom(this.logsTable)\n\t\t\t\t.select((eb: any) => [\n\t\t\t\t\teb.fn.count('id').as('count'),\n\t\t\t\t\teb.fn.min('timestamp').as('oldest'),\n\t\t\t\t\teb.fn.max('timestamp').as('newest'),\n\t\t\t\t])\n\t\t\t\t.executeTakeFirst(),\n\t\t]);\n\n\t\tconst allDates = [\n\t\t\trequestsResult?.oldest,\n\t\t\trequestsResult?.newest,\n\t\t\texceptionsResult?.oldest,\n\t\t\texceptionsResult?.newest,\n\t\t\tlogsResult?.oldest,\n\t\t\tlogsResult?.newest,\n\t\t]\n\t\t\t.filter((d): d is Date => d != null)\n\t\t\t.sort((a, b) => new Date(a).getTime() - new Date(b).getTime());\n\n\t\tconst newestDate = allDates[allDates.length - 1];\n\t\treturn {\n\t\t\trequests: Number(requestsResult?.count ?? 0),\n\t\t\texceptions: Number(exceptionsResult?.count ?? 0),\n\t\t\tlogs: Number(logsResult?.count ?? 0),\n\t\t\toldestEntry: allDates[0] ? new Date(allDates[0]) : undefined,\n\t\t\tnewestEntry: newestDate ? new Date(newestDate) : undefined,\n\t\t};\n\t}\n\n\t// ============================================\n\t// Private Helpers\n\t// ============================================\n\n\tprivate applyQueryOptions(query: any, options?: QueryOptions): any {\n\t\tif (!options) {\n\t\t\treturn query.limit(50);\n\t\t}\n\n\t\tif (options.after) {\n\t\t\tquery = query.where('timestamp', '>=', options.after);\n\t\t}\n\n\t\tif (options.before) {\n\t\t\tquery = query.where('timestamp', '<=', options.before);\n\t\t}\n\n\t\tif (options.search) {\n\t\t\t// Search in relevant text fields - using ILIKE for case-insensitive\n\t\t\t// This is a simple implementation; for production you'd want full-text search\n\t\t\tquery = query.where((eb: any) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb('message', 'ilike', `%${options.search}%`),\n\t\t\t\t\teb('path', 'ilike', `%${options.search}%`),\n\t\t\t\t\teb('url', 'ilike', `%${options.search}%`),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\t// Tags filter would require array contains operation\n\t\t// which is database-specific (PostgreSQL: @>, etc.)\n\n\t\tconst limit = options.limit ?? 50;\n\t\tconst offset = options.offset ?? 0;\n\n\t\treturn query.limit(limit).offset(offset);\n\t}\n\n\tprivate requestToRow(entry: RequestEntry): TelescopeRequestTable {\n\t\treturn {\n\t\t\tid: entry.id,\n\t\t\tmethod: entry.method,\n\t\t\tpath: entry.path,\n\t\t\turl: entry.url,\n\t\t\theaders: entry.headers,\n\t\t\tbody: entry.body ?? null,\n\t\t\tquery: entry.query ?? null,\n\t\t\tstatus: entry.status,\n\t\t\tresponse_headers: entry.responseHeaders,\n\t\t\tresponse_body: entry.responseBody ?? null,\n\t\t\tduration: entry.duration,\n\t\t\ttimestamp: entry.timestamp,\n\t\t\tip: entry.ip ?? null,\n\t\t\tuser_id: entry.userId ?? null,\n\t\t\ttags: entry.tags ?? null,\n\t\t};\n\t}\n\n\tprivate rowToRequest(row: TelescopeRequestTable): RequestEntry {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tmethod: row.method,\n\t\t\tpath: row.path,\n\t\t\turl: row.url,\n\t\t\theaders: this.parseJson(row.headers) as Record<string, string>,\n\t\t\tbody: row.body ? this.parseJson(row.body) : undefined,\n\t\t\tquery: row.query\n\t\t\t\t? (this.parseJson(row.query) as Record<string, string>)\n\t\t\t\t: undefined,\n\t\t\tstatus: row.status,\n\t\t\tresponseHeaders: this.parseJson(row.response_headers) as Record<\n\t\t\t\tstring,\n\t\t\t\tstring\n\t\t\t>,\n\t\t\tresponseBody: row.response_body\n\t\t\t\t? this.parseJson(row.response_body)\n\t\t\t\t: undefined,\n\t\t\tduration: row.duration,\n\t\t\ttimestamp: new Date(row.timestamp),\n\t\t\tip: row.ip ?? undefined,\n\t\t\tuserId: row.user_id ?? undefined,\n\t\t\ttags: row.tags ? (this.parseJson(row.tags) as string[]) : undefined,\n\t\t};\n\t}\n\n\tprivate exceptionToRow(entry: ExceptionEntry): TelescopeExceptionTable {\n\t\treturn {\n\t\t\tid: entry.id,\n\t\t\tname: entry.name,\n\t\t\tmessage: entry.message,\n\t\t\tstack: entry.stack,\n\t\t\tsource: entry.source ?? null,\n\t\t\trequest_id: entry.requestId ?? null,\n\t\t\ttimestamp: entry.timestamp,\n\t\t\thandled: entry.handled,\n\t\t\ttags: entry.tags ?? null,\n\t\t};\n\t}\n\n\tprivate rowToException(row: TelescopeExceptionTable): ExceptionEntry {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tname: row.name,\n\t\t\tmessage: row.message,\n\t\t\tstack: this.parseJson(row.stack) as ExceptionEntry['stack'],\n\t\t\tsource: row.source\n\t\t\t\t? (this.parseJson(row.source) as ExceptionEntry['source'])\n\t\t\t\t: undefined,\n\t\t\trequestId: row.request_id ?? undefined,\n\t\t\ttimestamp: new Date(row.timestamp),\n\t\t\thandled: row.handled,\n\t\t\ttags: row.tags ? (this.parseJson(row.tags) as string[]) : undefined,\n\t\t};\n\t}\n\n\tprivate logToRow(entry: LogEntry): TelescopeLogTable {\n\t\treturn {\n\t\t\tid: entry.id,\n\t\t\tlevel: entry.level,\n\t\t\tmessage: entry.message,\n\t\t\tcontext: entry.context ?? null,\n\t\t\trequest_id: entry.requestId ?? null,\n\t\t\ttimestamp: entry.timestamp,\n\t\t};\n\t}\n\n\tprivate rowToLog(row: TelescopeLogTable): LogEntry {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tlevel: row.level as LogEntry['level'],\n\t\t\tmessage: row.message,\n\t\t\tcontext: row.context\n\t\t\t\t? (this.parseJson(row.context) as Record<string, unknown>)\n\t\t\t\t: undefined,\n\t\t\trequestId: row.request_id ?? undefined,\n\t\t\ttimestamp: new Date(row.timestamp),\n\t\t};\n\t}\n\n\t/**\n\t * Parse a JSON value that may already be parsed (e.g., from jsonb columns).\n\t */\n\tprivate parseJson(value: unknown): unknown {\n\t\tif (typeof value === 'object' && value !== null) {\n\t\t\treturn value;\n\t\t}\n\t\tif (typeof value === 'string') {\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(value);\n\t\t\t} catch {\n\t\t\t\treturn value;\n\t\t\t}\n\t\t}\n\t\treturn value;\n\t}\n}\n\n/**\n * SQL migration to create telescope tables.\n * Use this to set up the required tables in your database.\n *\n * @example\n * ```typescript\n * import { getTelescopeMigration } from '@geekmidas/telescope/storage/kysely';\n *\n * // In your migration file\n * export async function up(db: Kysely<any>): Promise<void> {\n * const migration = getTelescopeMigration();\n * await db.schema.executeRaw(migration.up).execute();\n * }\n *\n * export async function down(db: Kysely<any>): Promise<void> {\n * const migration = getTelescopeMigration();\n * await db.schema.executeRaw(migration.down).execute();\n * }\n * ```\n */\nexport function getTelescopeMigration(tablePrefix = 'telescope'): {\n\tup: string;\n\tdown: string;\n} {\n\treturn {\n\t\tup: `\n-- Telescope requests table\nCREATE TABLE IF NOT EXISTS ${tablePrefix}_requests (\n id VARCHAR(21) PRIMARY KEY,\n method VARCHAR(10) NOT NULL,\n path TEXT NOT NULL,\n url TEXT NOT NULL,\n headers JSONB NOT NULL,\n body JSONB,\n query JSONB,\n status INTEGER NOT NULL,\n response_headers JSONB NOT NULL,\n response_body JSONB,\n duration DOUBLE PRECISION NOT NULL,\n timestamp TIMESTAMPTZ NOT NULL,\n ip VARCHAR(45),\n user_id VARCHAR(255),\n tags JSONB\n);\n\nCREATE INDEX IF NOT EXISTS idx_${tablePrefix}_requests_timestamp\n ON ${tablePrefix}_requests (timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_${tablePrefix}_requests_path\n ON ${tablePrefix}_requests (path);\nCREATE INDEX IF NOT EXISTS idx_${tablePrefix}_requests_status\n ON ${tablePrefix}_requests (status);\n\n-- Telescope exceptions table\nCREATE TABLE IF NOT EXISTS ${tablePrefix}_exceptions (\n id VARCHAR(21) PRIMARY KEY,\n name VARCHAR(255) NOT NULL,\n message TEXT NOT NULL,\n stack JSONB NOT NULL,\n source JSONB,\n request_id VARCHAR(21),\n timestamp TIMESTAMPTZ NOT NULL,\n handled BOOLEAN NOT NULL DEFAULT FALSE,\n tags JSONB\n);\n\nCREATE INDEX IF NOT EXISTS idx_${tablePrefix}_exceptions_timestamp\n ON ${tablePrefix}_exceptions (timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_${tablePrefix}_exceptions_request_id\n ON ${tablePrefix}_exceptions (request_id);\n\n-- Telescope logs table\nCREATE TABLE IF NOT EXISTS ${tablePrefix}_logs (\n id VARCHAR(21) PRIMARY KEY,\n level VARCHAR(10) NOT NULL,\n message TEXT NOT NULL,\n context JSONB,\n request_id VARCHAR(21),\n timestamp TIMESTAMPTZ NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_${tablePrefix}_logs_timestamp\n ON ${tablePrefix}_logs (timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_${tablePrefix}_logs_level\n ON ${tablePrefix}_logs (level);\nCREATE INDEX IF NOT EXISTS idx_${tablePrefix}_logs_request_id\n ON ${tablePrefix}_logs (request_id);\n`,\n\t\tdown: `\nDROP TABLE IF EXISTS ${tablePrefix}_logs;\nDROP TABLE IF EXISTS ${tablePrefix}_exceptions;\nDROP TABLE IF EXISTS ${tablePrefix}_requests;\n`,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAiHA,IAAa,gBAAb,MAA2D;CAC1D,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CAEjB,YAAYA,QAAiC;AAC5C,OAAK,KAAK,OAAO;EACjB,MAAM,SAAS,OAAO,eAAe;AACrC,OAAK,iBAAiB,EAAE,OAAO;AAC/B,OAAK,mBAAmB,EAAE,OAAO;AACjC,OAAK,aAAa,EAAE,OAAO;CAC3B;CAMD,MAAM,YAAYC,OAAoC;EACrD,MAAM,MAAM,KAAK,aAAa,MAAM;AACpC,QAAM,AAAC,KAAK,GAAW,WAAW,KAAK,cAAc,CAAC,OAAO,IAAI,CAAC,SAAS;CAC3E;CAED,MAAM,aAAaC,SAAwC;AAC1D,MAAI,QAAQ,WAAW,EAAG;EAE1B,MAAM,OAAO,QAAQ,IAAI,CAAC,MAAM,KAAK,aAAa,EAAE,CAAC;AACrD,QAAM,AAAC,KAAK,GACV,WAAW,KAAK,cAAc,CAC9B,OAAO,KAAK,CACZ,SAAS;CACX;CAED,MAAM,YAAYC,SAAiD;EAClE,IAAI,QAAQ,AAAC,KAAK,GAChB,WAAW,KAAK,cAAc,CAC9B,WAAW,CACX,QAAQ,aAAa,OAAO;AAG9B,MAAI,SAAS,OACZ,SAAQ,MAAM,MAAM,UAAU,KAAK,QAAQ,OAAO;AAGnD,MAAI,SAAS,QAAQ;GACpB,MAAM,eAAe,QAAQ;AAC7B,OAAI,aAAa,SAAS,KAAK,EAAE;IAEhC,MAAM,YAAY,aAAa;AAC/B,QAAI,WAAW;KACd,MAAM,WAAW,SAAS,WAAW,GAAG;KACxC,MAAM,YAAY,WAAW;KAC7B,MAAM,YAAY,YAAY;AAC9B,aAAQ,MACN,MAAM,UAAU,MAAM,UAAU,CAChC,MAAM,UAAU,MAAM,UAAU;IAClC;GACD,MAEA,SAAQ,MAAM,MAAM,UAAU,KAAK,SAAS,cAAc,GAAG,CAAC;EAE/D;AAED,UAAQ,KAAK,kBAAkB,OAAO,QAAQ;EAE9C,MAAM,OAAO,MAAM,MAAM,SAAS;AAClC,SAAO,KAAK,IAAI,CAACC,QAA+B,KAAK,aAAa,IAAI,CAAC;CACvE;CAED,MAAM,WAAWC,IAA0C;EAC1D,MAAM,MAAM,MAAM,AAAC,KAAK,GACtB,WAAW,KAAK,cAAc,CAC9B,WAAW,CACX,MAAM,MAAM,KAAK,GAAG,CACpB,kBAAkB;AAEpB,SAAO,MAAM,KAAK,aAAa,IAAI,GAAG;CACtC;CAMD,MAAM,cAAcC,OAAsC;EACzD,MAAM,MAAM,KAAK,eAAe,MAAM;AACtC,QAAM,AAAC,KAAK,GACV,WAAW,KAAK,gBAAgB,CAChC,OAAO,IAAI,CACX,SAAS;CACX;CAED,MAAM,eAAeC,SAA0C;AAC9D,MAAI,QAAQ,WAAW,EAAG;EAE1B,MAAM,OAAO,QAAQ,IAAI,CAAC,MAAM,KAAK,eAAe,EAAE,CAAC;AACvD,QAAM,AAAC,KAAK,GACV,WAAW,KAAK,gBAAgB,CAChC,OAAO,KAAK,CACZ,SAAS;CACX;CAED,MAAM,cAAcJ,SAAmD;EACtE,IAAI,QAAQ,AAAC,KAAK,GAChB,WAAW,KAAK,gBAAgB,CAChC,WAAW,CACX,QAAQ,aAAa,OAAO;AAE9B,UAAQ,KAAK,kBAAkB,OAAO,QAAQ;EAE9C,MAAM,OAAO,MAAM,MAAM,SAAS;AAClC,SAAO,KAAK,IAAI,CAACK,QAAiC,KAAK,eAAe,IAAI,CAAC;CAC3E;CAED,MAAM,aAAaH,IAA4C;EAC9D,MAAM,MAAM,MAAM,AAAC,KAAK,GACtB,WAAW,KAAK,gBAAgB,CAChC,WAAW,CACX,MAAM,MAAM,KAAK,GAAG,CACpB,kBAAkB;AAEpB,SAAO,MAAM,KAAK,eAAe,IAAI,GAAG;CACxC;CAMD,MAAM,QAAQI,OAAgC;EAC7C,MAAM,MAAM,KAAK,SAAS,MAAM;AAChC,QAAM,AAAC,KAAK,GAAW,WAAW,KAAK,UAAU,CAAC,OAAO,IAAI,CAAC,SAAS;CACvE;CAED,MAAM,SAASC,SAAoC;AAClD,MAAI,QAAQ,WAAW,EAAG;EAE1B,MAAM,OAAO,QAAQ,IAAI,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;AACjD,QAAM,AAAC,KAAK,GAAW,WAAW,KAAK,UAAU,CAAC,OAAO,KAAK,CAAC,SAAS;CACxE;CAED,MAAM,QAAQP,SAA6C;EAC1D,IAAI,QAAQ,AAAC,KAAK,GAChB,WAAW,KAAK,UAAU,CAC1B,WAAW,CACX,QAAQ,aAAa,OAAO;AAG9B,MAAI,SAAS,MACZ,SAAQ,MAAM,MAAM,SAAS,KAAK,QAAQ,MAAM;AAGjD,UAAQ,KAAK,kBAAkB,OAAO,QAAQ;EAE9C,MAAM,OAAO,MAAM,MAAM,SAAS;AAClC,SAAO,KAAK,IAAI,CAACQ,QAA2B,KAAK,SAAS,IAAI,CAAC;CAC/D;CAMD,MAAM,MAAMC,WAAkC;EAC7C,MAAM,UAAU,MAAM,QAAQ,IAAI;GACjC,AAAC,KAAK,GACJ,WAAW,KAAK,cAAc,CAC9B,MAAM,aAAa,KAAK,UAAU,CAClC,kBAAkB;GACpB,AAAC,KAAK,GACJ,WAAW,KAAK,gBAAgB,CAChC,MAAM,aAAa,KAAK,UAAU,CAClC,kBAAkB;GACpB,AAAC,KAAK,GACJ,WAAW,KAAK,UAAU,CAC1B,MAAM,aAAa,KAAK,UAAU,CAClC,kBAAkB;EACpB,EAAC;AAEF,SAAO,QAAQ,OACd,CAAC,KAAK,WAAW,MAAM,OAAO,OAAO,kBAAkB,EAAE,EACzD,EACA;CACD;CAMD,MAAM,WAAoC;EACzC,MAAM,CAAC,gBAAgB,kBAAkB,WAAW,GAAG,MAAM,QAAQ,IAAI;GACxE,AAAC,KAAK,GACJ,WAAW,KAAK,cAAc,CAC9B,OAAO,CAACC,OAAY;IACpB,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ;IAC7B,GAAG,GAAG,IAAI,YAAY,CAAC,GAAG,SAAS;IACnC,GAAG,GAAG,IAAI,YAAY,CAAC,GAAG,SAAS;GACnC,EAAC,CACD,kBAAkB;GACpB,AAAC,KAAK,GACJ,WAAW,KAAK,gBAAgB,CAChC,OAAO,CAACA,OAAY;IACpB,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ;IAC7B,GAAG,GAAG,IAAI,YAAY,CAAC,GAAG,SAAS;IACnC,GAAG,GAAG,IAAI,YAAY,CAAC,GAAG,SAAS;GACnC,EAAC,CACD,kBAAkB;GACpB,AAAC,KAAK,GACJ,WAAW,KAAK,UAAU,CAC1B,OAAO,CAACA,OAAY;IACpB,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ;IAC7B,GAAG,GAAG,IAAI,YAAY,CAAC,GAAG,SAAS;IACnC,GAAG,GAAG,IAAI,YAAY,CAAC,GAAG,SAAS;GACnC,EAAC,CACD,kBAAkB;EACpB,EAAC;EAEF,MAAM,WAAW;GAChB,gBAAgB;GAChB,gBAAgB;GAChB,kBAAkB;GAClB,kBAAkB;GAClB,YAAY;GACZ,YAAY;EACZ,EACC,OAAO,CAAC,MAAiB,KAAK,KAAK,CACnC,KAAK,CAAC,GAAG,MAAM,IAAI,KAAK,GAAG,SAAS,GAAG,IAAI,KAAK,GAAG,SAAS,CAAC;EAE/D,MAAM,aAAa,SAAS,SAAS,SAAS;AAC9C,SAAO;GACN,UAAU,OAAO,gBAAgB,SAAS,EAAE;GAC5C,YAAY,OAAO,kBAAkB,SAAS,EAAE;GAChD,MAAM,OAAO,YAAY,SAAS,EAAE;GACpC,aAAa,SAAS,KAAK,IAAI,KAAK,SAAS;GAC7C,aAAa,aAAa,IAAI,KAAK;EACnC;CACD;CAMD,AAAQ,kBAAkBC,OAAYX,SAA6B;AAClE,OAAK,QACJ,QAAO,MAAM,MAAM,GAAG;AAGvB,MAAI,QAAQ,MACX,SAAQ,MAAM,MAAM,aAAa,MAAM,QAAQ,MAAM;AAGtD,MAAI,QAAQ,OACX,SAAQ,MAAM,MAAM,aAAa,MAAM,QAAQ,OAAO;AAGvD,MAAI,QAAQ,OAGX,SAAQ,MAAM,MAAM,CAACU,OACpB,GAAG,GAAG;GACL,GAAG,WAAW,UAAU,GAAG,QAAQ,OAAO,GAAG;GAC7C,GAAG,QAAQ,UAAU,GAAG,QAAQ,OAAO,GAAG;GAC1C,GAAG,OAAO,UAAU,GAAG,QAAQ,OAAO,GAAG;EACzC,EAAC,CACF;EAMF,MAAM,QAAQ,QAAQ,SAAS;EAC/B,MAAM,SAAS,QAAQ,UAAU;AAEjC,SAAO,MAAM,MAAM,MAAM,CAAC,OAAO,OAAO;CACxC;CAED,AAAQ,aAAaZ,OAA4C;AAChE,SAAO;GACN,IAAI,MAAM;GACV,QAAQ,MAAM;GACd,MAAM,MAAM;GACZ,KAAK,MAAM;GACX,SAAS,MAAM;GACf,MAAM,MAAM,QAAQ;GACpB,OAAO,MAAM,SAAS;GACtB,QAAQ,MAAM;GACd,kBAAkB,MAAM;GACxB,eAAe,MAAM,gBAAgB;GACrC,UAAU,MAAM;GAChB,WAAW,MAAM;GACjB,IAAI,MAAM,MAAM;GAChB,SAAS,MAAM,UAAU;GACzB,MAAM,MAAM,QAAQ;EACpB;CACD;CAED,AAAQ,aAAaG,KAA0C;AAC9D,SAAO;GACN,IAAI,IAAI;GACR,QAAQ,IAAI;GACZ,MAAM,IAAI;GACV,KAAK,IAAI;GACT,SAAS,KAAK,UAAU,IAAI,QAAQ;GACpC,MAAM,IAAI,OAAO,KAAK,UAAU,IAAI,KAAK;GACzC,OAAO,IAAI,QACP,KAAK,UAAU,IAAI,MAAM;GAE7B,QAAQ,IAAI;GACZ,iBAAiB,KAAK,UAAU,IAAI,iBAAiB;GAIrD,cAAc,IAAI,gBACf,KAAK,UAAU,IAAI,cAAc;GAEpC,UAAU,IAAI;GACd,WAAW,IAAI,KAAK,IAAI;GACxB,IAAI,IAAI;GACR,QAAQ,IAAI;GACZ,MAAM,IAAI,OAAQ,KAAK,UAAU,IAAI,KAAK;EAC1C;CACD;CAED,AAAQ,eAAeE,OAAgD;AACtE,SAAO;GACN,IAAI,MAAM;GACV,MAAM,MAAM;GACZ,SAAS,MAAM;GACf,OAAO,MAAM;GACb,QAAQ,MAAM,UAAU;GACxB,YAAY,MAAM,aAAa;GAC/B,WAAW,MAAM;GACjB,SAAS,MAAM;GACf,MAAM,MAAM,QAAQ;EACpB;CACD;CAED,AAAQ,eAAeE,KAA8C;AACpE,SAAO;GACN,IAAI,IAAI;GACR,MAAM,IAAI;GACV,SAAS,IAAI;GACb,OAAO,KAAK,UAAU,IAAI,MAAM;GAChC,QAAQ,IAAI,SACR,KAAK,UAAU,IAAI,OAAO;GAE9B,WAAW,IAAI;GACf,WAAW,IAAI,KAAK,IAAI;GACxB,SAAS,IAAI;GACb,MAAM,IAAI,OAAQ,KAAK,UAAU,IAAI,KAAK;EAC1C;CACD;CAED,AAAQ,SAASC,OAAoC;AACpD,SAAO;GACN,IAAI,MAAM;GACV,OAAO,MAAM;GACb,SAAS,MAAM;GACf,SAAS,MAAM,WAAW;GAC1B,YAAY,MAAM,aAAa;GAC/B,WAAW,MAAM;EACjB;CACD;CAED,AAAQ,SAASE,KAAkC;AAClD,SAAO;GACN,IAAI,IAAI;GACR,OAAO,IAAI;GACX,SAAS,IAAI;GACb,SAAS,IAAI,UACT,KAAK,UAAU,IAAI,QAAQ;GAE/B,WAAW,IAAI;GACf,WAAW,IAAI,KAAK,IAAI;EACxB;CACD;;;;CAKD,AAAQ,UAAUI,OAAyB;AAC1C,aAAW,UAAU,YAAY,UAAU,KAC1C,QAAO;AAER,aAAW,UAAU,SACpB,KAAI;AACH,UAAO,KAAK,MAAM,MAAM;EACxB,QAAO;AACP,UAAO;EACP;AAEF,SAAO;CACP;AACD;;;;;;;;;;;;;;;;;;;;;AAsBD,SAAgB,sBAAsB,cAAc,aAGlD;AACD,QAAO;EACN,KAAK;;6BAEsB,YAAY;;;;;;;;;;;;;;;;;;iCAkBR,YAAY;OACtC,YAAY;iCACc,YAAY;OACtC,YAAY;iCACc,YAAY;OACtC,YAAY;;;6BAGU,YAAY;;;;;;;;;;;;iCAYR,YAAY;OACtC,YAAY;iCACc,YAAY;OACtC,YAAY;;;6BAGU,YAAY;;;;;;;;;iCASR,YAAY;OACtC,YAAY;iCACc,YAAY;OACtC,YAAY;iCACc,YAAY;OACtC,YAAY;;EAEjB,OAAO;uBACc,YAAY;uBACZ,YAAY;uBACZ,YAAY;;CAEjC;AACD"}
|
|
1
|
+
{"version":3,"file":"kysely.cjs","names":["config: KyselyStorageConfig<DB>","entry: RequestEntry","entries: RequestEntry[]","options?: QueryOptions","row: TelescopeRequestTable","id: string","entry: ExceptionEntry","entries: ExceptionEntry[]","row: TelescopeExceptionTable","entry: LogEntry","entries: LogEntry[]","row: TelescopeLogTable","olderThan: Date","eb: any","query: any","value: unknown","schema?: string","name: string"],"sources":["../../src/storage/kysely.ts"],"sourcesContent":["import type { Kysely } from 'kysely';\nimport type {\n\tExceptionEntry,\n\tLogEntry,\n\tQueryOptions,\n\tRequestEntry,\n\tTelescopeStats,\n\tTelescopeStorage,\n} from '../types';\n\n/**\n * Database table interface for telescope requests.\n * Use this to define your telescope_requests table in your Kysely database schema.\n */\nexport interface TelescopeRequestTable {\n\tid: string;\n\tmethod: string;\n\tpath: string;\n\turl: string;\n\theaders: unknown;\n\tbody: unknown | null;\n\tquery: unknown | null;\n\tstatus: number;\n\tresponse_headers: unknown;\n\tresponse_body: unknown | null;\n\tduration: number;\n\ttimestamp: Date;\n\tip: string | null;\n\tuser_id: string | null;\n\ttags: unknown | null;\n}\n\n/**\n * Database table interface for telescope exceptions.\n */\nexport interface TelescopeExceptionTable {\n\tid: string;\n\tname: string;\n\tmessage: string;\n\tstack: unknown;\n\tsource: unknown | null;\n\trequest_id: string | null;\n\ttimestamp: Date;\n\thandled: boolean;\n\ttags: unknown | null;\n}\n\n/**\n * Database table interface for telescope logs.\n */\nexport interface TelescopeLogTable {\n\tid: string;\n\tlevel: string;\n\tmessage: string;\n\tcontext: unknown | null;\n\trequest_id: string | null;\n\ttimestamp: Date;\n}\n\n/**\n * Combined database interface for all telescope tables.\n * Use this to extend your database schema.\n *\n * @example\n * ```typescript\n * import type { TelescopeTables } from '@geekmidas/telescope/storage/kysely';\n *\n * interface Database extends TelescopeTables {\n * users: UserTable;\n * // ... other tables\n * }\n * ```\n */\nexport interface TelescopeTables {\n\trequests: TelescopeRequestTable;\n\texceptions: TelescopeExceptionTable;\n\tlogs: TelescopeLogTable;\n}\n\n/**\n * Configuration for KyselyStorage.\n */\nexport interface KyselyStorageConfig<DB> {\n\t/** Kysely database instance */\n\tdb: Kysely<DB>;\n\t/**\n\t * The schema holding `requests`, `exceptions` and `logs`.\n\t *\n\t * Normally omitted. A telescope derived from a database gets a schema of its\n\t * own and a role whose `search_path` is pinned to it, so an unqualified name\n\t * already resolves there — which is the same rule every other tenant here\n\t * follows, and the reason a connection string never has to remember where\n\t * its tables live.\n\t *\n\t * Name one only when the connection is *not* pinned: a shared pool, a\n\t * migration run as an owner whose `search_path` finds `public` first.\n\t */\n\tschema?: string;\n}\n\n/**\n * Kysely-based storage implementation for Telescope.\n * Stores telescope data in PostgreSQL, MySQL, or SQLite using Kysely.\n *\n * @template DB - Your Kysely database schema (must include TelescopeTables)\n *\n * @example\n * ```typescript\n * import { Kysely, PostgresDialect } from 'kysely';\n * import { KyselyStorage, type TelescopeTables } from '@geekmidas/telescope/storage/kysely';\n *\n * interface Database extends TelescopeTables {\n * users: UserTable;\n * }\n *\n * const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool }) });\n * const storage = new KyselyStorage({ db });\n *\n * const telescope = new Telescope({ storage });\n * ```\n */\nexport class KyselyStorage<DB> implements TelescopeStorage {\n\tprivate readonly db: Kysely<DB>;\n\n\t/**\n\t * Unqualified, and deliberately.\n\t *\n\t * These were `telescope_requests`, `telescope_exceptions` and\n\t * `telescope_logs` — a prefix, which is what you reach for when the tables\n\t * have to share a schema with an application's own. They do not: a telescope\n\t * derived from a database is a schema tenant, so the names are just the\n\t * names, and `DROP SCHEMA telescope CASCADE` is the whole cleanup.\n\t */\n\tprivate readonly requestsTable = 'requests';\n\tprivate readonly exceptionsTable = 'exceptions';\n\tprivate readonly logsTable = 'logs';\n\n\tconstructor(config: KyselyStorageConfig<DB>) {\n\t\t// `withSchema` only where the connection is not already pinned to one.\n\t\tthis.db = config.schema\n\t\t\t? (config.db.withSchema(config.schema) as Kysely<DB>)\n\t\t\t: config.db;\n\t}\n\n\t// ============================================\n\t// Requests\n\t// ============================================\n\n\tasync saveRequest(entry: RequestEntry): Promise<void> {\n\t\tconst row = this.requestToRow(entry);\n\t\tawait (this.db as any).insertInto(this.requestsTable).values(row).execute();\n\t}\n\n\tasync saveRequests(entries: RequestEntry[]): Promise<void> {\n\t\tif (entries.length === 0) return;\n\n\t\tconst rows = entries.map((e) => this.requestToRow(e));\n\t\tawait (this.db as any)\n\t\t\t.insertInto(this.requestsTable)\n\t\t\t.values(rows)\n\t\t\t.execute();\n\t}\n\n\tasync getRequests(options?: QueryOptions): Promise<RequestEntry[]> {\n\t\tlet query = (this.db as any)\n\t\t\t.selectFrom(this.requestsTable)\n\t\t\t.selectAll()\n\t\t\t.orderBy('timestamp', 'desc');\n\n\t\t// Apply request-specific filters\n\t\tif (options?.method) {\n\t\t\tquery = query.where('method', '=', options.method);\n\t\t}\n\n\t\tif (options?.status) {\n\t\t\tconst statusFilter = options.status;\n\t\t\tif (statusFilter.endsWith('xx')) {\n\t\t\t\t// Handle status ranges like \"2xx\", \"4xx\", \"5xx\"\n\t\t\t\tconst firstChar = statusFilter[0];\n\t\t\t\tif (firstChar) {\n\t\t\t\t\tconst category = parseInt(firstChar, 10);\n\t\t\t\t\tconst minStatus = category * 100;\n\t\t\t\t\tconst maxStatus = minStatus + 99;\n\t\t\t\t\tquery = query\n\t\t\t\t\t\t.where('status', '>=', minStatus)\n\t\t\t\t\t\t.where('status', '<=', maxStatus);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Handle exact status codes\n\t\t\t\tquery = query.where('status', '=', parseInt(statusFilter, 10));\n\t\t\t}\n\t\t}\n\n\t\tquery = this.applyQueryOptions(query, options);\n\n\t\tconst rows = await query.execute();\n\t\treturn rows.map((row: TelescopeRequestTable) => this.rowToRequest(row));\n\t}\n\n\tasync getRequest(id: string): Promise<RequestEntry | null> {\n\t\tconst row = await (this.db as any)\n\t\t\t.selectFrom(this.requestsTable)\n\t\t\t.selectAll()\n\t\t\t.where('id', '=', id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToRequest(row) : null;\n\t}\n\n\t// ============================================\n\t// Exceptions\n\t// ============================================\n\n\tasync saveException(entry: ExceptionEntry): Promise<void> {\n\t\tconst row = this.exceptionToRow(entry);\n\t\tawait (this.db as any)\n\t\t\t.insertInto(this.exceptionsTable)\n\t\t\t.values(row)\n\t\t\t.execute();\n\t}\n\n\tasync saveExceptions(entries: ExceptionEntry[]): Promise<void> {\n\t\tif (entries.length === 0) return;\n\n\t\tconst rows = entries.map((e) => this.exceptionToRow(e));\n\t\tawait (this.db as any)\n\t\t\t.insertInto(this.exceptionsTable)\n\t\t\t.values(rows)\n\t\t\t.execute();\n\t}\n\n\tasync getExceptions(options?: QueryOptions): Promise<ExceptionEntry[]> {\n\t\tlet query = (this.db as any)\n\t\t\t.selectFrom(this.exceptionsTable)\n\t\t\t.selectAll()\n\t\t\t.orderBy('timestamp', 'desc');\n\n\t\tquery = this.applyQueryOptions(query, options);\n\n\t\tconst rows = await query.execute();\n\t\treturn rows.map((row: TelescopeExceptionTable) => this.rowToException(row));\n\t}\n\n\tasync getException(id: string): Promise<ExceptionEntry | null> {\n\t\tconst row = await (this.db as any)\n\t\t\t.selectFrom(this.exceptionsTable)\n\t\t\t.selectAll()\n\t\t\t.where('id', '=', id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToException(row) : null;\n\t}\n\n\t// ============================================\n\t// Logs\n\t// ============================================\n\n\tasync saveLog(entry: LogEntry): Promise<void> {\n\t\tconst row = this.logToRow(entry);\n\t\tawait (this.db as any).insertInto(this.logsTable).values(row).execute();\n\t}\n\n\tasync saveLogs(entries: LogEntry[]): Promise<void> {\n\t\tif (entries.length === 0) return;\n\n\t\tconst rows = entries.map((e) => this.logToRow(e));\n\t\tawait (this.db as any).insertInto(this.logsTable).values(rows).execute();\n\t}\n\n\tasync getLogs(options?: QueryOptions): Promise<LogEntry[]> {\n\t\tlet query = (this.db as any)\n\t\t\t.selectFrom(this.logsTable)\n\t\t\t.selectAll()\n\t\t\t.orderBy('timestamp', 'desc');\n\n\t\t// Apply log-specific filters\n\t\tif (options?.level) {\n\t\t\tquery = query.where('level', '=', options.level);\n\t\t}\n\n\t\tquery = this.applyQueryOptions(query, options);\n\n\t\tconst rows = await query.execute();\n\t\treturn rows.map((row: TelescopeLogTable) => this.rowToLog(row));\n\t}\n\n\t// ============================================\n\t// Cleanup\n\t// ============================================\n\n\tasync prune(olderThan: Date): Promise<number> {\n\t\tconst results = await Promise.all([\n\t\t\t(this.db as any)\n\t\t\t\t.deleteFrom(this.requestsTable)\n\t\t\t\t.where('timestamp', '<', olderThan)\n\t\t\t\t.executeTakeFirst(),\n\t\t\t(this.db as any)\n\t\t\t\t.deleteFrom(this.exceptionsTable)\n\t\t\t\t.where('timestamp', '<', olderThan)\n\t\t\t\t.executeTakeFirst(),\n\t\t\t(this.db as any)\n\t\t\t\t.deleteFrom(this.logsTable)\n\t\t\t\t.where('timestamp', '<', olderThan)\n\t\t\t\t.executeTakeFirst(),\n\t\t]);\n\n\t\treturn results.reduce(\n\t\t\t(sum, result) => sum + Number(result.numDeletedRows ?? 0),\n\t\t\t0,\n\t\t);\n\t}\n\n\t// ============================================\n\t// Stats\n\t// ============================================\n\n\tasync getStats(): Promise<TelescopeStats> {\n\t\tconst [requestsResult, exceptionsResult, logsResult] = await Promise.all([\n\t\t\t(this.db as any)\n\t\t\t\t.selectFrom(this.requestsTable)\n\t\t\t\t.select((eb: any) => [\n\t\t\t\t\teb.fn.count('id').as('count'),\n\t\t\t\t\teb.fn.min('timestamp').as('oldest'),\n\t\t\t\t\teb.fn.max('timestamp').as('newest'),\n\t\t\t\t])\n\t\t\t\t.executeTakeFirst(),\n\t\t\t(this.db as any)\n\t\t\t\t.selectFrom(this.exceptionsTable)\n\t\t\t\t.select((eb: any) => [\n\t\t\t\t\teb.fn.count('id').as('count'),\n\t\t\t\t\teb.fn.min('timestamp').as('oldest'),\n\t\t\t\t\teb.fn.max('timestamp').as('newest'),\n\t\t\t\t])\n\t\t\t\t.executeTakeFirst(),\n\t\t\t(this.db as any)\n\t\t\t\t.selectFrom(this.logsTable)\n\t\t\t\t.select((eb: any) => [\n\t\t\t\t\teb.fn.count('id').as('count'),\n\t\t\t\t\teb.fn.min('timestamp').as('oldest'),\n\t\t\t\t\teb.fn.max('timestamp').as('newest'),\n\t\t\t\t])\n\t\t\t\t.executeTakeFirst(),\n\t\t]);\n\n\t\tconst allDates = [\n\t\t\trequestsResult?.oldest,\n\t\t\trequestsResult?.newest,\n\t\t\texceptionsResult?.oldest,\n\t\t\texceptionsResult?.newest,\n\t\t\tlogsResult?.oldest,\n\t\t\tlogsResult?.newest,\n\t\t]\n\t\t\t.filter((d): d is Date => d != null)\n\t\t\t.sort((a, b) => new Date(a).getTime() - new Date(b).getTime());\n\n\t\tconst newestDate = allDates[allDates.length - 1];\n\t\treturn {\n\t\t\trequests: Number(requestsResult?.count ?? 0),\n\t\t\texceptions: Number(exceptionsResult?.count ?? 0),\n\t\t\tlogs: Number(logsResult?.count ?? 0),\n\t\t\toldestEntry: allDates[0] ? new Date(allDates[0]) : undefined,\n\t\t\tnewestEntry: newestDate ? new Date(newestDate) : undefined,\n\t\t};\n\t}\n\n\t// ============================================\n\t// Private Helpers\n\t// ============================================\n\n\tprivate applyQueryOptions(query: any, options?: QueryOptions): any {\n\t\tif (!options) {\n\t\t\treturn query.limit(50);\n\t\t}\n\n\t\tif (options.after) {\n\t\t\tquery = query.where('timestamp', '>=', options.after);\n\t\t}\n\n\t\tif (options.before) {\n\t\t\tquery = query.where('timestamp', '<=', options.before);\n\t\t}\n\n\t\tif (options.search) {\n\t\t\t// Search in relevant text fields - using ILIKE for case-insensitive\n\t\t\t// This is a simple implementation; for production you'd want full-text search\n\t\t\tquery = query.where((eb: any) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb('message', 'ilike', `%${options.search}%`),\n\t\t\t\t\teb('path', 'ilike', `%${options.search}%`),\n\t\t\t\t\teb('url', 'ilike', `%${options.search}%`),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\t// Tags filter would require array contains operation\n\t\t// which is database-specific (PostgreSQL: @>, etc.)\n\n\t\tconst limit = options.limit ?? 50;\n\t\tconst offset = options.offset ?? 0;\n\n\t\treturn query.limit(limit).offset(offset);\n\t}\n\n\tprivate requestToRow(entry: RequestEntry): TelescopeRequestTable {\n\t\treturn {\n\t\t\tid: entry.id,\n\t\t\tmethod: entry.method,\n\t\t\tpath: entry.path,\n\t\t\turl: entry.url,\n\t\t\theaders: entry.headers,\n\t\t\tbody: entry.body ?? null,\n\t\t\tquery: entry.query ?? null,\n\t\t\tstatus: entry.status,\n\t\t\tresponse_headers: entry.responseHeaders,\n\t\t\tresponse_body: entry.responseBody ?? null,\n\t\t\tduration: entry.duration,\n\t\t\ttimestamp: entry.timestamp,\n\t\t\tip: entry.ip ?? null,\n\t\t\tuser_id: entry.userId ?? null,\n\t\t\ttags: entry.tags ?? null,\n\t\t};\n\t}\n\n\tprivate rowToRequest(row: TelescopeRequestTable): RequestEntry {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tmethod: row.method,\n\t\t\tpath: row.path,\n\t\t\turl: row.url,\n\t\t\theaders: this.parseJson(row.headers) as Record<string, string>,\n\t\t\tbody: row.body ? this.parseJson(row.body) : undefined,\n\t\t\tquery: row.query\n\t\t\t\t? (this.parseJson(row.query) as Record<string, string>)\n\t\t\t\t: undefined,\n\t\t\tstatus: row.status,\n\t\t\tresponseHeaders: this.parseJson(row.response_headers) as Record<\n\t\t\t\tstring,\n\t\t\t\tstring\n\t\t\t>,\n\t\t\tresponseBody: row.response_body\n\t\t\t\t? this.parseJson(row.response_body)\n\t\t\t\t: undefined,\n\t\t\tduration: row.duration,\n\t\t\ttimestamp: new Date(row.timestamp),\n\t\t\tip: row.ip ?? undefined,\n\t\t\tuserId: row.user_id ?? undefined,\n\t\t\ttags: row.tags ? (this.parseJson(row.tags) as string[]) : undefined,\n\t\t};\n\t}\n\n\tprivate exceptionToRow(entry: ExceptionEntry): TelescopeExceptionTable {\n\t\treturn {\n\t\t\tid: entry.id,\n\t\t\tname: entry.name,\n\t\t\tmessage: entry.message,\n\t\t\tstack: entry.stack,\n\t\t\tsource: entry.source ?? null,\n\t\t\trequest_id: entry.requestId ?? null,\n\t\t\ttimestamp: entry.timestamp,\n\t\t\thandled: entry.handled,\n\t\t\ttags: entry.tags ?? null,\n\t\t};\n\t}\n\n\tprivate rowToException(row: TelescopeExceptionTable): ExceptionEntry {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tname: row.name,\n\t\t\tmessage: row.message,\n\t\t\tstack: this.parseJson(row.stack) as ExceptionEntry['stack'],\n\t\t\tsource: row.source\n\t\t\t\t? (this.parseJson(row.source) as ExceptionEntry['source'])\n\t\t\t\t: undefined,\n\t\t\trequestId: row.request_id ?? undefined,\n\t\t\ttimestamp: new Date(row.timestamp),\n\t\t\thandled: row.handled,\n\t\t\ttags: row.tags ? (this.parseJson(row.tags) as string[]) : undefined,\n\t\t};\n\t}\n\n\tprivate logToRow(entry: LogEntry): TelescopeLogTable {\n\t\treturn {\n\t\t\tid: entry.id,\n\t\t\tlevel: entry.level,\n\t\t\tmessage: entry.message,\n\t\t\tcontext: entry.context ?? null,\n\t\t\trequest_id: entry.requestId ?? null,\n\t\t\ttimestamp: entry.timestamp,\n\t\t};\n\t}\n\n\tprivate rowToLog(row: TelescopeLogTable): LogEntry {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tlevel: row.level as LogEntry['level'],\n\t\t\tmessage: row.message,\n\t\t\tcontext: row.context\n\t\t\t\t? (this.parseJson(row.context) as Record<string, unknown>)\n\t\t\t\t: undefined,\n\t\t\trequestId: row.request_id ?? undefined,\n\t\t\ttimestamp: new Date(row.timestamp),\n\t\t};\n\t}\n\n\t/**\n\t * Parse a JSON value that may already be parsed (e.g., from jsonb columns).\n\t */\n\tprivate parseJson(value: unknown): unknown {\n\t\tif (typeof value === 'object' && value !== null) {\n\t\t\treturn value;\n\t\t}\n\t\tif (typeof value === 'string') {\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(value);\n\t\t\t} catch {\n\t\t\t\treturn value;\n\t\t\t}\n\t\t}\n\t\treturn value;\n\t}\n}\n\n/**\n * SQL migration to create telescope's tables.\n *\n * Pass a schema to have them created in one, and to have `down` drop the whole\n * schema rather than three tables. Pass nothing where the connection's\n * `search_path` already places them.\n *\n * @example\n * ```typescript\n * import { getTelescopeMigration } from '@geekmidas/telescope/storage/kysely';\n *\n * // In your migration file\n * export async function up(db: Kysely<any>): Promise<void> {\n * const migration = getTelescopeMigration();\n * await db.schema.executeRaw(migration.up).execute();\n * }\n *\n * export async function down(db: Kysely<any>): Promise<void> {\n * const migration = getTelescopeMigration();\n * await db.schema.executeRaw(migration.down).execute();\n * }\n * ```\n */\nexport function getTelescopeMigration(schema?: string): {\n\tup: string;\n\tdown: string;\n} {\n\t// Qualified only when a schema is named. Unqualified otherwise, so the\n\t// connection's `search_path` places them — the same rule the tables\n\t// themselves follow at query time.\n\tconst q = (name: string) => (schema ? `${schema}.${name}` : name);\n\tconst idx = (name: string) => (schema ? `${schema}_${name}` : name);\n\n\treturn {\n\t\tup: `${schema ? `CREATE SCHEMA IF NOT EXISTS ${schema};\\n` : ''}\n-- Telescope requests\nCREATE TABLE IF NOT EXISTS ${q('requests')} (\n id VARCHAR(21) PRIMARY KEY,\n method VARCHAR(10) NOT NULL,\n path TEXT NOT NULL,\n url TEXT NOT NULL,\n headers JSONB NOT NULL,\n body JSONB,\n query JSONB,\n status INTEGER NOT NULL,\n response_headers JSONB NOT NULL,\n response_body JSONB,\n duration DOUBLE PRECISION NOT NULL,\n timestamp TIMESTAMPTZ NOT NULL,\n ip VARCHAR(45),\n user_id VARCHAR(255),\n tags JSONB\n);\n\nCREATE INDEX IF NOT EXISTS idx_${idx('requests')}_timestamp\n ON ${q('requests')} (timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_${idx('requests')}_path\n ON ${q('requests')} (path);\nCREATE INDEX IF NOT EXISTS idx_${idx('requests')}_status\n ON ${q('requests')} (status);\n\n-- Telescope exceptions\nCREATE TABLE IF NOT EXISTS ${q('exceptions')} (\n id VARCHAR(21) PRIMARY KEY,\n name VARCHAR(255) NOT NULL,\n message TEXT NOT NULL,\n stack JSONB NOT NULL,\n source JSONB,\n request_id VARCHAR(21),\n timestamp TIMESTAMPTZ NOT NULL,\n handled BOOLEAN NOT NULL DEFAULT FALSE,\n tags JSONB\n);\n\nCREATE INDEX IF NOT EXISTS idx_${idx('exceptions')}_timestamp\n ON ${q('exceptions')} (timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_${idx('exceptions')}_request_id\n ON ${q('exceptions')} (request_id);\n\n-- Telescope logs\nCREATE TABLE IF NOT EXISTS ${q('logs')} (\n id VARCHAR(21) PRIMARY KEY,\n level VARCHAR(10) NOT NULL,\n message TEXT NOT NULL,\n context JSONB,\n request_id VARCHAR(21),\n timestamp TIMESTAMPTZ NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_${idx('logs')}_timestamp\n ON ${q('logs')} (timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_${idx('logs')}_level\n ON ${q('logs')} (level);\nCREATE INDEX IF NOT EXISTS idx_${idx('logs')}_request_id\n ON ${q('logs')} (request_id);\n`,\n\t\t// A schema tenant drops whole; only the unqualified case drops tables.\n\t\tdown: schema\n\t\t\t? `DROP SCHEMA IF EXISTS ${schema} CASCADE;\\n`\n\t\t\t: `\nDROP TABLE IF EXISTS logs;\nDROP TABLE IF EXISTS exceptions;\nDROP TABLE IF EXISTS requests;\n`,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -64,9 +64,9 @@ interface TelescopeLogTable {
|
|
|
64
64
|
* ```
|
|
65
65
|
*/
|
|
66
66
|
interface TelescopeTables {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
requests: TelescopeRequestTable;
|
|
68
|
+
exceptions: TelescopeExceptionTable;
|
|
69
|
+
logs: TelescopeLogTable;
|
|
70
70
|
}
|
|
71
71
|
/**
|
|
72
72
|
* Configuration for KyselyStorage.
|
|
@@ -75,10 +75,18 @@ interface KyselyStorageConfig<DB> {
|
|
|
75
75
|
/** Kysely database instance */
|
|
76
76
|
db: Kysely<DB>;
|
|
77
77
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
78
|
+
* The schema holding `requests`, `exceptions` and `logs`.
|
|
79
|
+
*
|
|
80
|
+
* Normally omitted. A telescope derived from a database gets a schema of its
|
|
81
|
+
* own and a role whose `search_path` is pinned to it, so an unqualified name
|
|
82
|
+
* already resolves there — which is the same rule every other tenant here
|
|
83
|
+
* follows, and the reason a connection string never has to remember where
|
|
84
|
+
* its tables live.
|
|
85
|
+
*
|
|
86
|
+
* Name one only when the connection is *not* pinned: a shared pool, a
|
|
87
|
+
* migration run as an owner whose `search_path` finds `public` first.
|
|
80
88
|
*/
|
|
81
|
-
|
|
89
|
+
schema?: string;
|
|
82
90
|
}
|
|
83
91
|
/**
|
|
84
92
|
* Kysely-based storage implementation for Telescope.
|
|
@@ -103,6 +111,15 @@ interface KyselyStorageConfig<DB> {
|
|
|
103
111
|
*/
|
|
104
112
|
declare class KyselyStorage<DB> implements TelescopeStorage {
|
|
105
113
|
private readonly db;
|
|
114
|
+
/**
|
|
115
|
+
* Unqualified, and deliberately.
|
|
116
|
+
*
|
|
117
|
+
* These were `telescope_requests`, `telescope_exceptions` and
|
|
118
|
+
* `telescope_logs` — a prefix, which is what you reach for when the tables
|
|
119
|
+
* have to share a schema with an application's own. They do not: a telescope
|
|
120
|
+
* derived from a database is a schema tenant, so the names are just the
|
|
121
|
+
* names, and `DROP SCHEMA telescope CASCADE` is the whole cleanup.
|
|
122
|
+
*/
|
|
106
123
|
private readonly requestsTable;
|
|
107
124
|
private readonly exceptionsTable;
|
|
108
125
|
private readonly logsTable;
|
|
@@ -133,8 +150,11 @@ declare class KyselyStorage<DB> implements TelescopeStorage {
|
|
|
133
150
|
private parseJson;
|
|
134
151
|
}
|
|
135
152
|
/**
|
|
136
|
-
* SQL migration to create telescope tables.
|
|
137
|
-
*
|
|
153
|
+
* SQL migration to create telescope's tables.
|
|
154
|
+
*
|
|
155
|
+
* Pass a schema to have them created in one, and to have `down` drop the whole
|
|
156
|
+
* schema rather than three tables. Pass nothing where the connection's
|
|
157
|
+
* `search_path` already places them.
|
|
138
158
|
*
|
|
139
159
|
* @example
|
|
140
160
|
* ```typescript
|
|
@@ -152,7 +172,7 @@ declare class KyselyStorage<DB> implements TelescopeStorage {
|
|
|
152
172
|
* }
|
|
153
173
|
* ```
|
|
154
174
|
*/
|
|
155
|
-
declare function getTelescopeMigration(
|
|
175
|
+
declare function getTelescopeMigration(schema?: string): {
|
|
156
176
|
up: string;
|
|
157
177
|
down: string;
|
|
158
178
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kysely.d.cts","names":[],"sources":["../../src/storage/kysely.ts"],"sourcesContent":[],"mappings":";;;;;;;AAcA;AAqBA;AAeiB,UApCA,qBAAA,
|
|
1
|
+
{"version":3,"file":"kysely.d.cts","names":[],"sources":["../../src/storage/kysely.ts"],"sourcesContent":[],"mappings":";;;;;;;AAcA;AAqBA;AAeiB,UApCA,qBAAA,CA0CD;EAiBC,EAAA,EAAA,MAAA;EAAe,MAAA,EAAA,MAAA;EAAA,IACrB,EAAA,MAAA;EAAqB,GACnB,EAAA,MAAA;EAAuB,OAC7B,EAAA,OAAA;EAAiB,IAAA,EAAA,OAAA,GAAA,IAAA;EAMP,KAAA,EAAA,OAAA,GAAA,IAAA;EAAmB,MAAA,EAAA,MAAA;EAAA,gBAExB,EAAA,OAAA;EAAE,aAAT,EAAA,OAAA,GAAA,IAAA;EAAM,QAAA,EAAA,MAAA;EAqCE,SAAA,EA/FD,IA+FC;EAAa,EAAA,EAAA,MAAA,GAAA,IAAA;EAAA,OAgBe,EAAA,MAAA,GAAA,IAAA;EAAE,IAAtB,EAAA,OAAA,GAAA,IAAA;;;;;AA0BQ,UAhIZ,uBAAA,CAgIY;EAAY,EAAA,EAAW,MAAA;EAAY,IAApB,EAAA,MAAA;EAAO,OAoCZ,EAAA,MAAA;EAAY,KAApB,EAAA,OAAA;EAAO,MAcV,EAAA,OAAA,GAAA,IAAA;EAAc,UAAG,EAAA,MAAA,GAAA,IAAA;EAAO,SAQrB,EAnLnB,IAmLmB;EAAc,OAAK,EAAA,OAAA;EAAO,IAU1B,EAAA,OAAA,GAAA,IAAA;;;;;AA0BT,UA/ML,iBAAA,CA+MK;EAAQ,EAAA,EAAG,MAAA;EAAO,KAKf,EAAA,MAAA;EAAQ,OAAK,EAAA,MAAA;EAAO,OAOpB,EAAA,OAAA,GAAA,IAAA;EAAY,UAAW,EAAA,MAAA,GAAA,IAAA;EAAQ,SAAhB,EArN5B,IAqN4B;;;;;;AApJkB;AAwa1D;;;;;;;;;UAxdiB,eAAA;YACN;cACE;QACN;;;;;UAMU;;MAEZ,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAqCC,6BAA6B;;;;;;;;;;;;;;sBAgBrB,oBAAoB;qBAWf,eAAe;wBAKZ,iBAAiB;wBAUjB,eAAe,QAAQ;0BAoCrB,QAAQ;uBAcX,iBAAiB;0BAQd,mBAAmB;0BAUnB,eAAe,QAAQ;4BAYrB,QAAQ;iBAcnB,WAAW;oBAKR,aAAa;oBAOb,eAAe,QAAQ;mBAqBxB,OAAO;cA0BZ,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqOX,qBAAA"}
|
|
@@ -64,9 +64,9 @@ interface TelescopeLogTable {
|
|
|
64
64
|
* ```
|
|
65
65
|
*/
|
|
66
66
|
interface TelescopeTables {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
requests: TelescopeRequestTable;
|
|
68
|
+
exceptions: TelescopeExceptionTable;
|
|
69
|
+
logs: TelescopeLogTable;
|
|
70
70
|
}
|
|
71
71
|
/**
|
|
72
72
|
* Configuration for KyselyStorage.
|
|
@@ -75,10 +75,18 @@ interface KyselyStorageConfig<DB> {
|
|
|
75
75
|
/** Kysely database instance */
|
|
76
76
|
db: Kysely<DB>;
|
|
77
77
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
78
|
+
* The schema holding `requests`, `exceptions` and `logs`.
|
|
79
|
+
*
|
|
80
|
+
* Normally omitted. A telescope derived from a database gets a schema of its
|
|
81
|
+
* own and a role whose `search_path` is pinned to it, so an unqualified name
|
|
82
|
+
* already resolves there — which is the same rule every other tenant here
|
|
83
|
+
* follows, and the reason a connection string never has to remember where
|
|
84
|
+
* its tables live.
|
|
85
|
+
*
|
|
86
|
+
* Name one only when the connection is *not* pinned: a shared pool, a
|
|
87
|
+
* migration run as an owner whose `search_path` finds `public` first.
|
|
80
88
|
*/
|
|
81
|
-
|
|
89
|
+
schema?: string;
|
|
82
90
|
}
|
|
83
91
|
/**
|
|
84
92
|
* Kysely-based storage implementation for Telescope.
|
|
@@ -103,6 +111,15 @@ interface KyselyStorageConfig<DB> {
|
|
|
103
111
|
*/
|
|
104
112
|
declare class KyselyStorage<DB> implements TelescopeStorage {
|
|
105
113
|
private readonly db;
|
|
114
|
+
/**
|
|
115
|
+
* Unqualified, and deliberately.
|
|
116
|
+
*
|
|
117
|
+
* These were `telescope_requests`, `telescope_exceptions` and
|
|
118
|
+
* `telescope_logs` — a prefix, which is what you reach for when the tables
|
|
119
|
+
* have to share a schema with an application's own. They do not: a telescope
|
|
120
|
+
* derived from a database is a schema tenant, so the names are just the
|
|
121
|
+
* names, and `DROP SCHEMA telescope CASCADE` is the whole cleanup.
|
|
122
|
+
*/
|
|
106
123
|
private readonly requestsTable;
|
|
107
124
|
private readonly exceptionsTable;
|
|
108
125
|
private readonly logsTable;
|
|
@@ -133,8 +150,11 @@ declare class KyselyStorage<DB> implements TelescopeStorage {
|
|
|
133
150
|
private parseJson;
|
|
134
151
|
}
|
|
135
152
|
/**
|
|
136
|
-
* SQL migration to create telescope tables.
|
|
137
|
-
*
|
|
153
|
+
* SQL migration to create telescope's tables.
|
|
154
|
+
*
|
|
155
|
+
* Pass a schema to have them created in one, and to have `down` drop the whole
|
|
156
|
+
* schema rather than three tables. Pass nothing where the connection's
|
|
157
|
+
* `search_path` already places them.
|
|
138
158
|
*
|
|
139
159
|
* @example
|
|
140
160
|
* ```typescript
|
|
@@ -152,7 +172,7 @@ declare class KyselyStorage<DB> implements TelescopeStorage {
|
|
|
152
172
|
* }
|
|
153
173
|
* ```
|
|
154
174
|
*/
|
|
155
|
-
declare function getTelescopeMigration(
|
|
175
|
+
declare function getTelescopeMigration(schema?: string): {
|
|
156
176
|
up: string;
|
|
157
177
|
down: string;
|
|
158
178
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kysely.d.mts","names":[],"sources":["../../src/storage/kysely.ts"],"sourcesContent":[],"mappings":";;;;;;;AAcA;AAqBA;AAeiB,UApCA,qBAAA,CA0CL;EAiBK,EAAA,EAAA,MAAA;EAAe,MAAA,EAAA,MAAA;EAAA,
|
|
1
|
+
{"version":3,"file":"kysely.d.mts","names":[],"sources":["../../src/storage/kysely.ts"],"sourcesContent":[],"mappings":";;;;;;;AAcA;AAqBA;AAeiB,UApCA,qBAAA,CA0CL;EAiBK,EAAA,EAAA,MAAA;EAAe,MAAA,EAAA,MAAA;EAAA,IACrB,EAAA,MAAA;EAAqB,GACnB,EAAA,MAAA;EAAuB,OAC7B,EAAA,OAAA;EAAiB,IAAA,EAAA,OAAA,GAAA,IAAA;EAMP,KAAA,EAAA,OAAA,GAAA,IAAA;EAAmB,MAAA,EAAA,MAAA;EAAA,gBAExB,EAAA,OAAA;EAAE,aAAT,EAAA,OAAA,GAAA,IAAA;EAAM,QAAA,EAAA,MAAA;EAqCE,SAAA,EA/FD,IA+FC;EAAa,EAAA,EAAA,MAAA,GAAA,IAAA;EAAA,OAgBe,EAAA,MAAA,GAAA,IAAA;EAAE,IAAtB,EAAA,OAAA,GAAA,IAAA;;;;;AA0BQ,UAhIZ,uBAAA,CAgIY;EAAY,EAAA,EAAW,MAAA;EAAY,IAApB,EAAA,MAAA;EAAO,OAoCZ,EAAA,MAAA;EAAY,KAApB,EAAA,OAAA;EAAO,MAcV,EAAA,OAAA,GAAA,IAAA;EAAc,UAAG,EAAA,MAAA,GAAA,IAAA;EAAO,SAQrB,EAnLnB,IAmLmB;EAAc,OAAK,EAAA,OAAA;EAAO,IAU1B,EAAA,OAAA,GAAA,IAAA;;;;;AA0BT,UA/ML,iBAAA,CA+MK;EAAQ,EAAA,EAAG,MAAA;EAAO,KAKf,EAAA,MAAA;EAAQ,OAAK,EAAA,MAAA;EAAO,OAOpB,EAAA,OAAA,GAAA,IAAA;EAAY,UAAW,EAAA,MAAA,GAAA,IAAA;EAAQ,SAAhB,EArN5B,IAqN4B;;;;;;AApJkB;AAwa1D;;;;;;;;;UAxdiB,eAAA;YACN;cACE;QACN;;;;;UAMU;;MAEZ,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAqCC,6BAA6B;;;;;;;;;;;;;;sBAgBrB,oBAAoB;qBAWf,eAAe;wBAKZ,iBAAiB;wBAUjB,eAAe,QAAQ;0BAoCrB,QAAQ;uBAcX,iBAAiB;0BAQd,mBAAmB;0BAUnB,eAAe,QAAQ;4BAYrB,QAAQ;iBAcnB,WAAW;oBAKR,aAAa;oBAOb,eAAe,QAAQ;mBAqBxB,OAAO;cA0BZ,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqOX,qBAAA"}
|
package/dist/storage/kysely.mjs
CHANGED
|
@@ -22,15 +22,20 @@
|
|
|
22
22
|
*/
|
|
23
23
|
var KyselyStorage = class {
|
|
24
24
|
db;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Unqualified, and deliberately.
|
|
27
|
+
*
|
|
28
|
+
* These were `telescope_requests`, `telescope_exceptions` and
|
|
29
|
+
* `telescope_logs` — a prefix, which is what you reach for when the tables
|
|
30
|
+
* have to share a schema with an application's own. They do not: a telescope
|
|
31
|
+
* derived from a database is a schema tenant, so the names are just the
|
|
32
|
+
* names, and `DROP SCHEMA telescope CASCADE` is the whole cleanup.
|
|
33
|
+
*/
|
|
34
|
+
requestsTable = "requests";
|
|
35
|
+
exceptionsTable = "exceptions";
|
|
36
|
+
logsTable = "logs";
|
|
28
37
|
constructor(config) {
|
|
29
|
-
this.db = config.db;
|
|
30
|
-
const prefix = config.tablePrefix ?? "telescope";
|
|
31
|
-
this.requestsTable = `${prefix}_requests`;
|
|
32
|
-
this.exceptionsTable = `${prefix}_exceptions`;
|
|
33
|
-
this.logsTable = `${prefix}_logs`;
|
|
38
|
+
this.db = config.schema ? config.db.withSchema(config.schema) : config.db;
|
|
34
39
|
}
|
|
35
40
|
async saveRequest(entry) {
|
|
36
41
|
const row = this.requestToRow(entry);
|
|
@@ -253,8 +258,11 @@ var KyselyStorage = class {
|
|
|
253
258
|
}
|
|
254
259
|
};
|
|
255
260
|
/**
|
|
256
|
-
* SQL migration to create telescope tables.
|
|
257
|
-
*
|
|
261
|
+
* SQL migration to create telescope's tables.
|
|
262
|
+
*
|
|
263
|
+
* Pass a schema to have them created in one, and to have `down` drop the whole
|
|
264
|
+
* schema rather than three tables. Pass nothing where the connection's
|
|
265
|
+
* `search_path` already places them.
|
|
258
266
|
*
|
|
259
267
|
* @example
|
|
260
268
|
* ```typescript
|
|
@@ -272,11 +280,13 @@ var KyselyStorage = class {
|
|
|
272
280
|
* }
|
|
273
281
|
* ```
|
|
274
282
|
*/
|
|
275
|
-
function getTelescopeMigration(
|
|
283
|
+
function getTelescopeMigration(schema) {
|
|
284
|
+
const q = (name) => schema ? `${schema}.${name}` : name;
|
|
285
|
+
const idx = (name) => schema ? `${schema}_${name}` : name;
|
|
276
286
|
return {
|
|
277
|
-
up: `
|
|
278
|
-
-- Telescope requests
|
|
279
|
-
CREATE TABLE IF NOT EXISTS ${
|
|
287
|
+
up: `${schema ? `CREATE SCHEMA IF NOT EXISTS ${schema};\n` : ""}
|
|
288
|
+
-- Telescope requests
|
|
289
|
+
CREATE TABLE IF NOT EXISTS ${q("requests")} (
|
|
280
290
|
id VARCHAR(21) PRIMARY KEY,
|
|
281
291
|
method VARCHAR(10) NOT NULL,
|
|
282
292
|
path TEXT NOT NULL,
|
|
@@ -294,15 +304,15 @@ CREATE TABLE IF NOT EXISTS ${tablePrefix}_requests (
|
|
|
294
304
|
tags JSONB
|
|
295
305
|
);
|
|
296
306
|
|
|
297
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
298
|
-
ON ${
|
|
299
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
300
|
-
ON ${
|
|
301
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
302
|
-
ON ${
|
|
307
|
+
CREATE INDEX IF NOT EXISTS idx_${idx("requests")}_timestamp
|
|
308
|
+
ON ${q("requests")} (timestamp DESC);
|
|
309
|
+
CREATE INDEX IF NOT EXISTS idx_${idx("requests")}_path
|
|
310
|
+
ON ${q("requests")} (path);
|
|
311
|
+
CREATE INDEX IF NOT EXISTS idx_${idx("requests")}_status
|
|
312
|
+
ON ${q("requests")} (status);
|
|
303
313
|
|
|
304
|
-
-- Telescope exceptions
|
|
305
|
-
CREATE TABLE IF NOT EXISTS ${
|
|
314
|
+
-- Telescope exceptions
|
|
315
|
+
CREATE TABLE IF NOT EXISTS ${q("exceptions")} (
|
|
306
316
|
id VARCHAR(21) PRIMARY KEY,
|
|
307
317
|
name VARCHAR(255) NOT NULL,
|
|
308
318
|
message TEXT NOT NULL,
|
|
@@ -314,13 +324,13 @@ CREATE TABLE IF NOT EXISTS ${tablePrefix}_exceptions (
|
|
|
314
324
|
tags JSONB
|
|
315
325
|
);
|
|
316
326
|
|
|
317
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
318
|
-
ON ${
|
|
319
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
320
|
-
ON ${
|
|
327
|
+
CREATE INDEX IF NOT EXISTS idx_${idx("exceptions")}_timestamp
|
|
328
|
+
ON ${q("exceptions")} (timestamp DESC);
|
|
329
|
+
CREATE INDEX IF NOT EXISTS idx_${idx("exceptions")}_request_id
|
|
330
|
+
ON ${q("exceptions")} (request_id);
|
|
321
331
|
|
|
322
|
-
-- Telescope logs
|
|
323
|
-
CREATE TABLE IF NOT EXISTS ${
|
|
332
|
+
-- Telescope logs
|
|
333
|
+
CREATE TABLE IF NOT EXISTS ${q("logs")} (
|
|
324
334
|
id VARCHAR(21) PRIMARY KEY,
|
|
325
335
|
level VARCHAR(10) NOT NULL,
|
|
326
336
|
message TEXT NOT NULL,
|
|
@@ -329,17 +339,17 @@ CREATE TABLE IF NOT EXISTS ${tablePrefix}_logs (
|
|
|
329
339
|
timestamp TIMESTAMPTZ NOT NULL
|
|
330
340
|
);
|
|
331
341
|
|
|
332
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
333
|
-
ON ${
|
|
334
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
335
|
-
ON ${
|
|
336
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
337
|
-
ON ${
|
|
342
|
+
CREATE INDEX IF NOT EXISTS idx_${idx("logs")}_timestamp
|
|
343
|
+
ON ${q("logs")} (timestamp DESC);
|
|
344
|
+
CREATE INDEX IF NOT EXISTS idx_${idx("logs")}_level
|
|
345
|
+
ON ${q("logs")} (level);
|
|
346
|
+
CREATE INDEX IF NOT EXISTS idx_${idx("logs")}_request_id
|
|
347
|
+
ON ${q("logs")} (request_id);
|
|
338
348
|
`,
|
|
339
|
-
down: `
|
|
340
|
-
DROP TABLE IF EXISTS
|
|
341
|
-
DROP TABLE IF EXISTS
|
|
342
|
-
DROP TABLE IF EXISTS
|
|
349
|
+
down: schema ? `DROP SCHEMA IF EXISTS ${schema} CASCADE;\n` : `
|
|
350
|
+
DROP TABLE IF EXISTS logs;
|
|
351
|
+
DROP TABLE IF EXISTS exceptions;
|
|
352
|
+
DROP TABLE IF EXISTS requests;
|
|
343
353
|
`
|
|
344
354
|
};
|
|
345
355
|
}
|