@geekmidas/telescope 0.0.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 +521 -0
- package/dist/Telescope-B3Wd82yk.cjs +602 -0
- package/dist/Telescope-B3Wd82yk.cjs.map +1 -0
- package/dist/Telescope-C5dyDYYB.d.cts +133 -0
- package/dist/Telescope-D-uoZB6b.mjs +596 -0
- package/dist/Telescope-D-uoZB6b.mjs.map +1 -0
- package/dist/Telescope-DyIWgh9-.d.mts +133 -0
- package/dist/Telescope.cjs +3 -0
- package/dist/Telescope.d.cts +3 -0
- package/dist/Telescope.d.mts +3 -0
- package/dist/Telescope.mjs +3 -0
- package/dist/chunk-CUT6urMc.cjs +30 -0
- package/dist/index.cjs +5 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +4 -0
- package/dist/logger/console.cjs +161 -0
- package/dist/logger/console.cjs.map +1 -0
- package/dist/logger/console.d.cts +109 -0
- package/dist/logger/console.d.mts +109 -0
- package/dist/logger/console.mjs +159 -0
- package/dist/logger/console.mjs.map +1 -0
- package/dist/logger/pino.cjs +118 -0
- package/dist/logger/pino.cjs.map +1 -0
- package/dist/logger/pino.d.cts +89 -0
- package/dist/logger/pino.d.mts +89 -0
- package/dist/logger/pino.mjs +116 -0
- package/dist/logger/pino.mjs.map +1 -0
- package/dist/memory-9-B9WACq.cjs +110 -0
- package/dist/memory-9-B9WACq.cjs.map +1 -0
- package/dist/memory-Cm0eevCS.d.mts +38 -0
- package/dist/memory-DiP1a-pp.d.cts +38 -0
- package/dist/memory-SdN5vtG9.mjs +104 -0
- package/dist/memory-SdN5vtG9.mjs.map +1 -0
- package/dist/server/hono.cjs +180 -0
- package/dist/server/hono.cjs.map +1 -0
- package/dist/server/hono.d.cts +26 -0
- package/dist/server/hono.d.mts +26 -0
- package/dist/server/hono.mjs +176 -0
- package/dist/server/hono.mjs.map +1 -0
- package/dist/storage/kysely.cjs +336 -0
- package/dist/storage/kysely.cjs.map +1 -0
- package/dist/storage/kysely.d.cts +161 -0
- package/dist/storage/kysely.d.mts +161 -0
- package/dist/storage/kysely.mjs +334 -0
- package/dist/storage/kysely.mjs.map +1 -0
- package/dist/storage/memory.cjs +3 -0
- package/dist/storage/memory.d.cts +3 -0
- package/dist/storage/memory.d.mts +3 -0
- package/dist/storage/memory.mjs +3 -0
- package/dist/types-BGDhFv4R.d.cts +170 -0
- package/dist/types-CZbzz8kx.d.mts +170 -0
- package/dist/types.cjs +0 -0
- package/dist/types.d.cts +2 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +0 -0
- package/dist/ui-assets-D6-8TAr_.mjs +30 -0
- package/dist/ui-assets-D6-8TAr_.mjs.map +1 -0
- package/dist/ui-assets-ulevVble.cjs +48 -0
- package/dist/ui-assets-ulevVble.cjs.map +1 -0
- package/dist/ui-assets.cjs +5 -0
- package/dist/ui-assets.d.cts +12 -0
- package/dist/ui-assets.d.mts +12 -0
- package/dist/ui-assets.mjs +3 -0
- package/package.json +83 -0
- package/scripts/embed-ui.ts +90 -0
- package/src/Telescope.ts +714 -0
- package/src/__tests__/Telescope.spec.ts +356 -0
- package/src/index.ts +23 -0
- package/src/logger/__tests__/console.spec.ts +266 -0
- package/src/logger/__tests__/pino.spec.ts +217 -0
- package/src/logger/console.ts +230 -0
- package/src/logger/pino.ts +191 -0
- package/src/server/__tests__/hono.spec.ts +340 -0
- package/src/server/hono.ts +247 -0
- package/src/storage/__tests__/kysely.spec.ts +715 -0
- package/src/storage/__tests__/memory.spec.ts +411 -0
- package/src/storage/kysely.ts +572 -0
- package/src/storage/memory.ts +168 -0
- package/src/types.ts +188 -0
- package/src/ui-assets.ts +40 -0
- package/ui/index.html +12 -0
- package/ui/node_modules/.bin/browserslist +21 -0
- package/ui/node_modules/.bin/jiti +21 -0
- package/ui/node_modules/.bin/terser +21 -0
- package/ui/node_modules/.bin/tsc +21 -0
- package/ui/node_modules/.bin/tsserver +21 -0
- package/ui/node_modules/.bin/tsx +21 -0
- package/ui/node_modules/.bin/vite +21 -0
- package/ui/package.json +24 -0
- package/ui/src/App.tsx +342 -0
- package/ui/src/api.ts +75 -0
- package/ui/src/components/ExceptionDetail.tsx +100 -0
- package/ui/src/components/LogDetail.tsx +91 -0
- package/ui/src/components/RequestDetail.tsx +143 -0
- package/ui/src/main.tsx +10 -0
- package/ui/src/styles.css +10 -0
- package/ui/src/types.ts +63 -0
- package/ui/src/vite-env.d.ts +1 -0
- package/ui/src/vite-plugin-gkm-config.ts +54 -0
- package/ui/tsconfig.json +20 -0
- package/ui/tsconfig.tsbuildinfo +14 -0
- package/ui/vite.config.ts +13 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import type { Kysely } from 'kysely';
|
|
2
|
+
import type {
|
|
3
|
+
ExceptionEntry,
|
|
4
|
+
LogEntry,
|
|
5
|
+
QueryOptions,
|
|
6
|
+
RequestEntry,
|
|
7
|
+
TelescopeStats,
|
|
8
|
+
TelescopeStorage,
|
|
9
|
+
} from '../types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Database table interface for telescope requests.
|
|
13
|
+
* Use this to define your telescope_requests table in your Kysely database schema.
|
|
14
|
+
*/
|
|
15
|
+
export interface TelescopeRequestTable {
|
|
16
|
+
id: string;
|
|
17
|
+
method: string;
|
|
18
|
+
path: string;
|
|
19
|
+
url: string;
|
|
20
|
+
headers: unknown;
|
|
21
|
+
body: unknown | null;
|
|
22
|
+
query: unknown | null;
|
|
23
|
+
status: number;
|
|
24
|
+
response_headers: unknown;
|
|
25
|
+
response_body: unknown | null;
|
|
26
|
+
duration: number;
|
|
27
|
+
timestamp: Date;
|
|
28
|
+
ip: string | null;
|
|
29
|
+
user_id: string | null;
|
|
30
|
+
tags: unknown | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Database table interface for telescope exceptions.
|
|
35
|
+
*/
|
|
36
|
+
export interface TelescopeExceptionTable {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
message: string;
|
|
40
|
+
stack: unknown;
|
|
41
|
+
source: unknown | null;
|
|
42
|
+
request_id: string | null;
|
|
43
|
+
timestamp: Date;
|
|
44
|
+
handled: boolean;
|
|
45
|
+
tags: unknown | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Database table interface for telescope logs.
|
|
50
|
+
*/
|
|
51
|
+
export interface TelescopeLogTable {
|
|
52
|
+
id: string;
|
|
53
|
+
level: string;
|
|
54
|
+
message: string;
|
|
55
|
+
context: unknown | null;
|
|
56
|
+
request_id: string | null;
|
|
57
|
+
timestamp: Date;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Combined database interface for all telescope tables.
|
|
62
|
+
* Use this to extend your database schema.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* import type { TelescopeTables } from '@geekmidas/telescope/storage/kysely';
|
|
67
|
+
*
|
|
68
|
+
* interface Database extends TelescopeTables {
|
|
69
|
+
* users: UserTable;
|
|
70
|
+
* // ... other tables
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export interface TelescopeTables {
|
|
75
|
+
telescope_requests: TelescopeRequestTable;
|
|
76
|
+
telescope_exceptions: TelescopeExceptionTable;
|
|
77
|
+
telescope_logs: TelescopeLogTable;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Configuration for KyselyStorage.
|
|
82
|
+
*/
|
|
83
|
+
export interface KyselyStorageConfig<DB> {
|
|
84
|
+
/** Kysely database instance */
|
|
85
|
+
db: Kysely<DB>;
|
|
86
|
+
/**
|
|
87
|
+
* Table name prefix (default: 'telescope').
|
|
88
|
+
* Tables will be named: {prefix}_requests, {prefix}_exceptions, {prefix}_logs
|
|
89
|
+
*/
|
|
90
|
+
tablePrefix?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Kysely-based storage implementation for Telescope.
|
|
95
|
+
* Stores telescope data in PostgreSQL, MySQL, or SQLite using Kysely.
|
|
96
|
+
*
|
|
97
|
+
* @template DB - Your Kysely database schema (must include TelescopeTables)
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* import { Kysely, PostgresDialect } from 'kysely';
|
|
102
|
+
* import { KyselyStorage, type TelescopeTables } from '@geekmidas/telescope/storage/kysely';
|
|
103
|
+
*
|
|
104
|
+
* interface Database extends TelescopeTables {
|
|
105
|
+
* users: UserTable;
|
|
106
|
+
* }
|
|
107
|
+
*
|
|
108
|
+
* const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool }) });
|
|
109
|
+
* const storage = new KyselyStorage({ db });
|
|
110
|
+
*
|
|
111
|
+
* const telescope = new Telescope({ storage });
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export class KyselyStorage<DB> implements TelescopeStorage {
|
|
115
|
+
private readonly db: Kysely<DB>;
|
|
116
|
+
private readonly requestsTable: string;
|
|
117
|
+
private readonly exceptionsTable: string;
|
|
118
|
+
private readonly logsTable: string;
|
|
119
|
+
|
|
120
|
+
constructor(config: KyselyStorageConfig<DB>) {
|
|
121
|
+
this.db = config.db;
|
|
122
|
+
const prefix = config.tablePrefix ?? 'telescope';
|
|
123
|
+
this.requestsTable = `${prefix}_requests`;
|
|
124
|
+
this.exceptionsTable = `${prefix}_exceptions`;
|
|
125
|
+
this.logsTable = `${prefix}_logs`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================
|
|
129
|
+
// Requests
|
|
130
|
+
// ============================================
|
|
131
|
+
|
|
132
|
+
async saveRequest(entry: RequestEntry): Promise<void> {
|
|
133
|
+
const row = this.requestToRow(entry);
|
|
134
|
+
await (this.db as any).insertInto(this.requestsTable).values(row).execute();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async saveRequests(entries: RequestEntry[]): Promise<void> {
|
|
138
|
+
if (entries.length === 0) return;
|
|
139
|
+
|
|
140
|
+
const rows = entries.map((e) => this.requestToRow(e));
|
|
141
|
+
await (this.db as any)
|
|
142
|
+
.insertInto(this.requestsTable)
|
|
143
|
+
.values(rows)
|
|
144
|
+
.execute();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async getRequests(options?: QueryOptions): Promise<RequestEntry[]> {
|
|
148
|
+
let query = (this.db as any)
|
|
149
|
+
.selectFrom(this.requestsTable)
|
|
150
|
+
.selectAll()
|
|
151
|
+
.orderBy('timestamp', 'desc');
|
|
152
|
+
|
|
153
|
+
query = this.applyQueryOptions(query, options);
|
|
154
|
+
|
|
155
|
+
const rows = await query.execute();
|
|
156
|
+
return rows.map((row: TelescopeRequestTable) => this.rowToRequest(row));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async getRequest(id: string): Promise<RequestEntry | null> {
|
|
160
|
+
const row = await (this.db as any)
|
|
161
|
+
.selectFrom(this.requestsTable)
|
|
162
|
+
.selectAll()
|
|
163
|
+
.where('id', '=', id)
|
|
164
|
+
.executeTakeFirst();
|
|
165
|
+
|
|
166
|
+
return row ? this.rowToRequest(row) : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ============================================
|
|
170
|
+
// Exceptions
|
|
171
|
+
// ============================================
|
|
172
|
+
|
|
173
|
+
async saveException(entry: ExceptionEntry): Promise<void> {
|
|
174
|
+
const row = this.exceptionToRow(entry);
|
|
175
|
+
await (this.db as any)
|
|
176
|
+
.insertInto(this.exceptionsTable)
|
|
177
|
+
.values(row)
|
|
178
|
+
.execute();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async saveExceptions(entries: ExceptionEntry[]): Promise<void> {
|
|
182
|
+
if (entries.length === 0) return;
|
|
183
|
+
|
|
184
|
+
const rows = entries.map((e) => this.exceptionToRow(e));
|
|
185
|
+
await (this.db as any)
|
|
186
|
+
.insertInto(this.exceptionsTable)
|
|
187
|
+
.values(rows)
|
|
188
|
+
.execute();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async getExceptions(options?: QueryOptions): Promise<ExceptionEntry[]> {
|
|
192
|
+
let query = (this.db as any)
|
|
193
|
+
.selectFrom(this.exceptionsTable)
|
|
194
|
+
.selectAll()
|
|
195
|
+
.orderBy('timestamp', 'desc');
|
|
196
|
+
|
|
197
|
+
query = this.applyQueryOptions(query, options);
|
|
198
|
+
|
|
199
|
+
const rows = await query.execute();
|
|
200
|
+
return rows.map((row: TelescopeExceptionTable) => this.rowToException(row));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async getException(id: string): Promise<ExceptionEntry | null> {
|
|
204
|
+
const row = await (this.db as any)
|
|
205
|
+
.selectFrom(this.exceptionsTable)
|
|
206
|
+
.selectAll()
|
|
207
|
+
.where('id', '=', id)
|
|
208
|
+
.executeTakeFirst();
|
|
209
|
+
|
|
210
|
+
return row ? this.rowToException(row) : null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ============================================
|
|
214
|
+
// Logs
|
|
215
|
+
// ============================================
|
|
216
|
+
|
|
217
|
+
async saveLog(entry: LogEntry): Promise<void> {
|
|
218
|
+
const row = this.logToRow(entry);
|
|
219
|
+
await (this.db as any).insertInto(this.logsTable).values(row).execute();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async saveLogs(entries: LogEntry[]): Promise<void> {
|
|
223
|
+
if (entries.length === 0) return;
|
|
224
|
+
|
|
225
|
+
const rows = entries.map((e) => this.logToRow(e));
|
|
226
|
+
await (this.db as any).insertInto(this.logsTable).values(rows).execute();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async getLogs(options?: QueryOptions): Promise<LogEntry[]> {
|
|
230
|
+
let query = (this.db as any)
|
|
231
|
+
.selectFrom(this.logsTable)
|
|
232
|
+
.selectAll()
|
|
233
|
+
.orderBy('timestamp', 'desc');
|
|
234
|
+
|
|
235
|
+
query = this.applyQueryOptions(query, options);
|
|
236
|
+
|
|
237
|
+
const rows = await query.execute();
|
|
238
|
+
return rows.map((row: TelescopeLogTable) => this.rowToLog(row));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ============================================
|
|
242
|
+
// Cleanup
|
|
243
|
+
// ============================================
|
|
244
|
+
|
|
245
|
+
async prune(olderThan: Date): Promise<number> {
|
|
246
|
+
const results = await Promise.all([
|
|
247
|
+
(this.db as any)
|
|
248
|
+
.deleteFrom(this.requestsTable)
|
|
249
|
+
.where('timestamp', '<', olderThan)
|
|
250
|
+
.executeTakeFirst(),
|
|
251
|
+
(this.db as any)
|
|
252
|
+
.deleteFrom(this.exceptionsTable)
|
|
253
|
+
.where('timestamp', '<', olderThan)
|
|
254
|
+
.executeTakeFirst(),
|
|
255
|
+
(this.db as any)
|
|
256
|
+
.deleteFrom(this.logsTable)
|
|
257
|
+
.where('timestamp', '<', olderThan)
|
|
258
|
+
.executeTakeFirst(),
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
return results.reduce(
|
|
262
|
+
(sum, result) => sum + Number(result.numDeletedRows ?? 0),
|
|
263
|
+
0,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ============================================
|
|
268
|
+
// Stats
|
|
269
|
+
// ============================================
|
|
270
|
+
|
|
271
|
+
async getStats(): Promise<TelescopeStats> {
|
|
272
|
+
const [requestsResult, exceptionsResult, logsResult] = await Promise.all([
|
|
273
|
+
(this.db as any)
|
|
274
|
+
.selectFrom(this.requestsTable)
|
|
275
|
+
.select((eb: any) => [
|
|
276
|
+
eb.fn.count('id').as('count'),
|
|
277
|
+
eb.fn.min('timestamp').as('oldest'),
|
|
278
|
+
eb.fn.max('timestamp').as('newest'),
|
|
279
|
+
])
|
|
280
|
+
.executeTakeFirst(),
|
|
281
|
+
(this.db as any)
|
|
282
|
+
.selectFrom(this.exceptionsTable)
|
|
283
|
+
.select((eb: any) => [
|
|
284
|
+
eb.fn.count('id').as('count'),
|
|
285
|
+
eb.fn.min('timestamp').as('oldest'),
|
|
286
|
+
eb.fn.max('timestamp').as('newest'),
|
|
287
|
+
])
|
|
288
|
+
.executeTakeFirst(),
|
|
289
|
+
(this.db as any)
|
|
290
|
+
.selectFrom(this.logsTable)
|
|
291
|
+
.select((eb: any) => [
|
|
292
|
+
eb.fn.count('id').as('count'),
|
|
293
|
+
eb.fn.min('timestamp').as('oldest'),
|
|
294
|
+
eb.fn.max('timestamp').as('newest'),
|
|
295
|
+
])
|
|
296
|
+
.executeTakeFirst(),
|
|
297
|
+
]);
|
|
298
|
+
|
|
299
|
+
const allDates = [
|
|
300
|
+
requestsResult?.oldest,
|
|
301
|
+
requestsResult?.newest,
|
|
302
|
+
exceptionsResult?.oldest,
|
|
303
|
+
exceptionsResult?.newest,
|
|
304
|
+
logsResult?.oldest,
|
|
305
|
+
logsResult?.newest,
|
|
306
|
+
]
|
|
307
|
+
.filter((d): d is Date => d != null)
|
|
308
|
+
.sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
requests: Number(requestsResult?.count ?? 0),
|
|
312
|
+
exceptions: Number(exceptionsResult?.count ?? 0),
|
|
313
|
+
logs: Number(logsResult?.count ?? 0),
|
|
314
|
+
oldestEntry: allDates[0] ? new Date(allDates[0]) : undefined,
|
|
315
|
+
newestEntry:
|
|
316
|
+
allDates.length > 0
|
|
317
|
+
? new Date(allDates[allDates.length - 1])
|
|
318
|
+
: undefined,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ============================================
|
|
323
|
+
// Private Helpers
|
|
324
|
+
// ============================================
|
|
325
|
+
|
|
326
|
+
private applyQueryOptions(query: any, options?: QueryOptions): any {
|
|
327
|
+
if (!options) {
|
|
328
|
+
return query.limit(50);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (options.after) {
|
|
332
|
+
query = query.where('timestamp', '>=', options.after);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (options.before) {
|
|
336
|
+
query = query.where('timestamp', '<=', options.before);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (options.search) {
|
|
340
|
+
// Search in relevant text fields - using ILIKE for case-insensitive
|
|
341
|
+
// This is a simple implementation; for production you'd want full-text search
|
|
342
|
+
query = query.where((eb: any) =>
|
|
343
|
+
eb.or([
|
|
344
|
+
eb('message', 'ilike', `%${options.search}%`),
|
|
345
|
+
eb('path', 'ilike', `%${options.search}%`),
|
|
346
|
+
eb('url', 'ilike', `%${options.search}%`),
|
|
347
|
+
]),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Tags filter would require array contains operation
|
|
352
|
+
// which is database-specific (PostgreSQL: @>, etc.)
|
|
353
|
+
|
|
354
|
+
const limit = options.limit ?? 50;
|
|
355
|
+
const offset = options.offset ?? 0;
|
|
356
|
+
|
|
357
|
+
return query.limit(limit).offset(offset);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private requestToRow(entry: RequestEntry): TelescopeRequestTable {
|
|
361
|
+
return {
|
|
362
|
+
id: entry.id,
|
|
363
|
+
method: entry.method,
|
|
364
|
+
path: entry.path,
|
|
365
|
+
url: entry.url,
|
|
366
|
+
headers: entry.headers,
|
|
367
|
+
body: entry.body ?? null,
|
|
368
|
+
query: entry.query ?? null,
|
|
369
|
+
status: entry.status,
|
|
370
|
+
response_headers: entry.responseHeaders,
|
|
371
|
+
response_body: entry.responseBody ?? null,
|
|
372
|
+
duration: entry.duration,
|
|
373
|
+
timestamp: entry.timestamp,
|
|
374
|
+
ip: entry.ip ?? null,
|
|
375
|
+
user_id: entry.userId ?? null,
|
|
376
|
+
tags: entry.tags ?? null,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private rowToRequest(row: TelescopeRequestTable): RequestEntry {
|
|
381
|
+
return {
|
|
382
|
+
id: row.id,
|
|
383
|
+
method: row.method,
|
|
384
|
+
path: row.path,
|
|
385
|
+
url: row.url,
|
|
386
|
+
headers: this.parseJson(row.headers) as Record<string, string>,
|
|
387
|
+
body: row.body ? this.parseJson(row.body) : undefined,
|
|
388
|
+
query: row.query
|
|
389
|
+
? (this.parseJson(row.query) as Record<string, string>)
|
|
390
|
+
: undefined,
|
|
391
|
+
status: row.status,
|
|
392
|
+
responseHeaders: this.parseJson(row.response_headers) as Record<
|
|
393
|
+
string,
|
|
394
|
+
string
|
|
395
|
+
>,
|
|
396
|
+
responseBody: row.response_body
|
|
397
|
+
? this.parseJson(row.response_body)
|
|
398
|
+
: undefined,
|
|
399
|
+
duration: row.duration,
|
|
400
|
+
timestamp: new Date(row.timestamp),
|
|
401
|
+
ip: row.ip ?? undefined,
|
|
402
|
+
userId: row.user_id ?? undefined,
|
|
403
|
+
tags: row.tags ? (this.parseJson(row.tags) as string[]) : undefined,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private exceptionToRow(entry: ExceptionEntry): TelescopeExceptionTable {
|
|
408
|
+
return {
|
|
409
|
+
id: entry.id,
|
|
410
|
+
name: entry.name,
|
|
411
|
+
message: entry.message,
|
|
412
|
+
stack: entry.stack,
|
|
413
|
+
source: entry.source ?? null,
|
|
414
|
+
request_id: entry.requestId ?? null,
|
|
415
|
+
timestamp: entry.timestamp,
|
|
416
|
+
handled: entry.handled,
|
|
417
|
+
tags: entry.tags ?? null,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private rowToException(row: TelescopeExceptionTable): ExceptionEntry {
|
|
422
|
+
return {
|
|
423
|
+
id: row.id,
|
|
424
|
+
name: row.name,
|
|
425
|
+
message: row.message,
|
|
426
|
+
stack: this.parseJson(row.stack) as ExceptionEntry['stack'],
|
|
427
|
+
source: row.source
|
|
428
|
+
? (this.parseJson(row.source) as ExceptionEntry['source'])
|
|
429
|
+
: undefined,
|
|
430
|
+
requestId: row.request_id ?? undefined,
|
|
431
|
+
timestamp: new Date(row.timestamp),
|
|
432
|
+
handled: row.handled,
|
|
433
|
+
tags: row.tags ? (this.parseJson(row.tags) as string[]) : undefined,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private logToRow(entry: LogEntry): TelescopeLogTable {
|
|
438
|
+
return {
|
|
439
|
+
id: entry.id,
|
|
440
|
+
level: entry.level,
|
|
441
|
+
message: entry.message,
|
|
442
|
+
context: entry.context ?? null,
|
|
443
|
+
request_id: entry.requestId ?? null,
|
|
444
|
+
timestamp: entry.timestamp,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private rowToLog(row: TelescopeLogTable): LogEntry {
|
|
449
|
+
return {
|
|
450
|
+
id: row.id,
|
|
451
|
+
level: row.level as LogEntry['level'],
|
|
452
|
+
message: row.message,
|
|
453
|
+
context: row.context
|
|
454
|
+
? (this.parseJson(row.context) as Record<string, unknown>)
|
|
455
|
+
: undefined,
|
|
456
|
+
requestId: row.request_id ?? undefined,
|
|
457
|
+
timestamp: new Date(row.timestamp),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Parse a JSON value that may already be parsed (e.g., from jsonb columns).
|
|
463
|
+
*/
|
|
464
|
+
private parseJson(value: unknown): unknown {
|
|
465
|
+
if (typeof value === 'object' && value !== null) {
|
|
466
|
+
return value;
|
|
467
|
+
}
|
|
468
|
+
if (typeof value === 'string') {
|
|
469
|
+
try {
|
|
470
|
+
return JSON.parse(value);
|
|
471
|
+
} catch {
|
|
472
|
+
return value;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return value;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* SQL migration to create telescope tables.
|
|
481
|
+
* Use this to set up the required tables in your database.
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
* ```typescript
|
|
485
|
+
* import { getTelescopeMigration } from '@geekmidas/telescope/storage/kysely';
|
|
486
|
+
*
|
|
487
|
+
* // In your migration file
|
|
488
|
+
* export async function up(db: Kysely<any>): Promise<void> {
|
|
489
|
+
* const migration = getTelescopeMigration();
|
|
490
|
+
* await db.schema.executeRaw(migration.up).execute();
|
|
491
|
+
* }
|
|
492
|
+
*
|
|
493
|
+
* export async function down(db: Kysely<any>): Promise<void> {
|
|
494
|
+
* const migration = getTelescopeMigration();
|
|
495
|
+
* await db.schema.executeRaw(migration.down).execute();
|
|
496
|
+
* }
|
|
497
|
+
* ```
|
|
498
|
+
*/
|
|
499
|
+
export function getTelescopeMigration(tablePrefix = 'telescope'): {
|
|
500
|
+
up: string;
|
|
501
|
+
down: string;
|
|
502
|
+
} {
|
|
503
|
+
return {
|
|
504
|
+
up: `
|
|
505
|
+
-- Telescope requests table
|
|
506
|
+
CREATE TABLE IF NOT EXISTS ${tablePrefix}_requests (
|
|
507
|
+
id VARCHAR(21) PRIMARY KEY,
|
|
508
|
+
method VARCHAR(10) NOT NULL,
|
|
509
|
+
path TEXT NOT NULL,
|
|
510
|
+
url TEXT NOT NULL,
|
|
511
|
+
headers JSONB NOT NULL,
|
|
512
|
+
body JSONB,
|
|
513
|
+
query JSONB,
|
|
514
|
+
status INTEGER NOT NULL,
|
|
515
|
+
response_headers JSONB NOT NULL,
|
|
516
|
+
response_body JSONB,
|
|
517
|
+
duration DOUBLE PRECISION NOT NULL,
|
|
518
|
+
timestamp TIMESTAMPTZ NOT NULL,
|
|
519
|
+
ip VARCHAR(45),
|
|
520
|
+
user_id VARCHAR(255),
|
|
521
|
+
tags JSONB
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_requests_timestamp
|
|
525
|
+
ON ${tablePrefix}_requests (timestamp DESC);
|
|
526
|
+
CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_requests_path
|
|
527
|
+
ON ${tablePrefix}_requests (path);
|
|
528
|
+
CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_requests_status
|
|
529
|
+
ON ${tablePrefix}_requests (status);
|
|
530
|
+
|
|
531
|
+
-- Telescope exceptions table
|
|
532
|
+
CREATE TABLE IF NOT EXISTS ${tablePrefix}_exceptions (
|
|
533
|
+
id VARCHAR(21) PRIMARY KEY,
|
|
534
|
+
name VARCHAR(255) NOT NULL,
|
|
535
|
+
message TEXT NOT NULL,
|
|
536
|
+
stack JSONB NOT NULL,
|
|
537
|
+
source JSONB,
|
|
538
|
+
request_id VARCHAR(21),
|
|
539
|
+
timestamp TIMESTAMPTZ NOT NULL,
|
|
540
|
+
handled BOOLEAN NOT NULL DEFAULT FALSE,
|
|
541
|
+
tags JSONB
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_exceptions_timestamp
|
|
545
|
+
ON ${tablePrefix}_exceptions (timestamp DESC);
|
|
546
|
+
CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_exceptions_request_id
|
|
547
|
+
ON ${tablePrefix}_exceptions (request_id);
|
|
548
|
+
|
|
549
|
+
-- Telescope logs table
|
|
550
|
+
CREATE TABLE IF NOT EXISTS ${tablePrefix}_logs (
|
|
551
|
+
id VARCHAR(21) PRIMARY KEY,
|
|
552
|
+
level VARCHAR(10) NOT NULL,
|
|
553
|
+
message TEXT NOT NULL,
|
|
554
|
+
context JSONB,
|
|
555
|
+
request_id VARCHAR(21),
|
|
556
|
+
timestamp TIMESTAMPTZ NOT NULL
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_logs_timestamp
|
|
560
|
+
ON ${tablePrefix}_logs (timestamp DESC);
|
|
561
|
+
CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_logs_level
|
|
562
|
+
ON ${tablePrefix}_logs (level);
|
|
563
|
+
CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_logs_request_id
|
|
564
|
+
ON ${tablePrefix}_logs (request_id);
|
|
565
|
+
`,
|
|
566
|
+
down: `
|
|
567
|
+
DROP TABLE IF EXISTS ${tablePrefix}_logs;
|
|
568
|
+
DROP TABLE IF EXISTS ${tablePrefix}_exceptions;
|
|
569
|
+
DROP TABLE IF EXISTS ${tablePrefix}_requests;
|
|
570
|
+
`,
|
|
571
|
+
};
|
|
572
|
+
}
|