@kysera/debug 0.7.3 → 0.7.4
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 +177 -173
- package/dist/index.d.ts +99 -21
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/circular-buffer.ts +122 -0
- package/src/format.ts +49 -28
- package/src/index.ts +6 -17
- package/src/plugin.ts +62 -77
- package/src/profiler.ts +42 -33
package/src/plugin.ts
CHANGED
|
@@ -11,24 +11,14 @@ import type {
|
|
|
11
11
|
QueryResult,
|
|
12
12
|
UnknownRow,
|
|
13
13
|
KyselyPlugin,
|
|
14
|
-
RootOperationNode
|
|
15
|
-
} from 'kysely'
|
|
16
|
-
import { DefaultQueryCompiler } from 'kysely'
|
|
17
|
-
import { consoleLogger, type KyseraLogger } from '@kysera/core'
|
|
14
|
+
RootOperationNode
|
|
15
|
+
} from 'kysely'
|
|
16
|
+
import { DefaultQueryCompiler } from 'kysely'
|
|
17
|
+
import { consoleLogger, type KyseraLogger, type QueryMetrics } from '@kysera/core'
|
|
18
|
+
import { CircularBuffer } from './circular-buffer.js'
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*/
|
|
22
|
-
export interface QueryMetrics {
|
|
23
|
-
/** SQL query string */
|
|
24
|
-
sql: string;
|
|
25
|
-
/** Query parameters */
|
|
26
|
-
params?: unknown[];
|
|
27
|
-
/** Query execution duration in milliseconds */
|
|
28
|
-
duration: number;
|
|
29
|
-
/** Timestamp when query was executed */
|
|
30
|
-
timestamp: number;
|
|
31
|
-
}
|
|
20
|
+
// Re-export QueryMetrics for backwards compatibility
|
|
21
|
+
export type { QueryMetrics }
|
|
32
22
|
|
|
33
23
|
/**
|
|
34
24
|
* Options for debug plugin.
|
|
@@ -38,37 +28,37 @@ export interface DebugOptions {
|
|
|
38
28
|
* Log query SQL.
|
|
39
29
|
* @default true
|
|
40
30
|
*/
|
|
41
|
-
logQuery?: boolean
|
|
31
|
+
logQuery?: boolean
|
|
42
32
|
|
|
43
33
|
/**
|
|
44
34
|
* Log query parameters.
|
|
45
35
|
* @default false
|
|
46
36
|
*/
|
|
47
|
-
logParams?: boolean
|
|
37
|
+
logParams?: boolean
|
|
48
38
|
|
|
49
39
|
/**
|
|
50
40
|
* Duration threshold (ms) to consider a query slow.
|
|
51
41
|
* @default 100
|
|
52
42
|
*/
|
|
53
|
-
slowQueryThreshold?: number
|
|
43
|
+
slowQueryThreshold?: number
|
|
54
44
|
|
|
55
45
|
/**
|
|
56
46
|
* Callback for slow queries.
|
|
57
47
|
*/
|
|
58
|
-
onSlowQuery?: (sql: string, duration: number) => void
|
|
48
|
+
onSlowQuery?: (sql: string, duration: number) => void
|
|
59
49
|
|
|
60
50
|
/**
|
|
61
51
|
* Logger for debug messages.
|
|
62
52
|
* @default consoleLogger
|
|
63
53
|
*/
|
|
64
|
-
logger?: KyseraLogger
|
|
54
|
+
logger?: KyseraLogger
|
|
65
55
|
|
|
66
56
|
/**
|
|
67
57
|
* Maximum number of metrics to keep in memory.
|
|
68
58
|
* When limit is reached, oldest metrics are removed (circular buffer).
|
|
69
59
|
* @default 1000
|
|
70
60
|
*/
|
|
71
|
-
maxMetrics?: number
|
|
61
|
+
maxMetrics?: number
|
|
72
62
|
}
|
|
73
63
|
|
|
74
64
|
/**
|
|
@@ -76,9 +66,9 @@ export interface DebugOptions {
|
|
|
76
66
|
* @internal
|
|
77
67
|
*/
|
|
78
68
|
interface QueryData {
|
|
79
|
-
startTime: number
|
|
80
|
-
sql: string
|
|
81
|
-
params: readonly unknown[]
|
|
69
|
+
startTime: number
|
|
70
|
+
sql: string
|
|
71
|
+
params: readonly unknown[]
|
|
82
72
|
}
|
|
83
73
|
|
|
84
74
|
/**
|
|
@@ -86,89 +76,87 @@ interface QueryData {
|
|
|
86
76
|
* @internal
|
|
87
77
|
*/
|
|
88
78
|
class DebugPlugin implements KyselyPlugin {
|
|
89
|
-
private
|
|
90
|
-
private queryData = new WeakMap<object, QueryData>()
|
|
91
|
-
private readonly
|
|
92
|
-
private readonly
|
|
93
|
-
|
|
94
|
-
|
|
79
|
+
private readonly metricsBuffer: CircularBuffer<QueryMetrics>
|
|
80
|
+
private queryData = new WeakMap<object, QueryData>()
|
|
81
|
+
private readonly logger: KyseraLogger
|
|
82
|
+
private readonly options: Required<
|
|
83
|
+
Pick<DebugOptions, 'logQuery' | 'logParams' | 'slowQueryThreshold'>
|
|
84
|
+
>
|
|
85
|
+
private readonly onSlowQuery: ((sql: string, duration: number) => void) | undefined
|
|
95
86
|
|
|
96
87
|
constructor(options: DebugOptions = {}) {
|
|
97
|
-
this.logger = options.logger ?? consoleLogger
|
|
98
|
-
this.
|
|
99
|
-
this.onSlowQuery = options.onSlowQuery
|
|
88
|
+
this.logger = options.logger ?? consoleLogger
|
|
89
|
+
this.metricsBuffer = new CircularBuffer<QueryMetrics>(options.maxMetrics ?? 1000)
|
|
90
|
+
this.onSlowQuery = options.onSlowQuery
|
|
100
91
|
this.options = {
|
|
101
92
|
logQuery: options.logQuery ?? true,
|
|
102
93
|
logParams: options.logParams ?? false,
|
|
103
|
-
slowQueryThreshold: options.slowQueryThreshold ?? 100
|
|
104
|
-
}
|
|
94
|
+
slowQueryThreshold: options.slowQueryThreshold ?? 100
|
|
95
|
+
}
|
|
105
96
|
}
|
|
106
97
|
|
|
107
98
|
transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
|
|
108
|
-
const startTime = performance.now()
|
|
99
|
+
const startTime = performance.now()
|
|
109
100
|
|
|
110
101
|
// Compile the query to get SQL and parameters
|
|
111
|
-
const compiler = new DefaultQueryCompiler()
|
|
112
|
-
const compiled = compiler.compileQuery(args.node, args.queryId)
|
|
102
|
+
const compiler = new DefaultQueryCompiler()
|
|
103
|
+
const compiled = compiler.compileQuery(args.node, args.queryId)
|
|
113
104
|
|
|
114
105
|
// Store query data for later use in transformResult
|
|
115
106
|
this.queryData.set(args.queryId, {
|
|
116
107
|
startTime,
|
|
117
108
|
sql: compiled.sql,
|
|
118
|
-
params: compiled.parameters
|
|
119
|
-
})
|
|
109
|
+
params: compiled.parameters
|
|
110
|
+
})
|
|
120
111
|
|
|
121
|
-
return args.node
|
|
112
|
+
return args.node
|
|
122
113
|
}
|
|
123
114
|
|
|
124
115
|
transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {
|
|
125
|
-
const data = this.queryData.get(args.queryId)
|
|
116
|
+
const data = this.queryData.get(args.queryId)
|
|
126
117
|
|
|
127
118
|
if (data) {
|
|
128
|
-
const endTime = performance.now()
|
|
129
|
-
const duration = endTime - data.startTime
|
|
130
|
-
this.queryData.delete(args.queryId)
|
|
119
|
+
const endTime = performance.now()
|
|
120
|
+
const duration = endTime - data.startTime
|
|
121
|
+
this.queryData.delete(args.queryId)
|
|
131
122
|
|
|
132
123
|
const metric: QueryMetrics = {
|
|
133
124
|
sql: data.sql,
|
|
134
125
|
params: [...data.params],
|
|
135
126
|
duration,
|
|
136
|
-
timestamp: Date.now()
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
// Circular buffer: keep only last N metrics
|
|
140
|
-
this.metrics.push(metric);
|
|
141
|
-
if (this.metrics.length > this.maxMetrics) {
|
|
142
|
-
this.metrics.shift();
|
|
127
|
+
timestamp: Date.now()
|
|
143
128
|
}
|
|
144
129
|
|
|
130
|
+
// Add to circular buffer (O(1) operation)
|
|
131
|
+
this.metricsBuffer.add(metric)
|
|
132
|
+
|
|
145
133
|
if (this.options.logQuery) {
|
|
146
134
|
const message = this.options.logParams
|
|
147
|
-
?
|
|
148
|
-
:
|
|
149
|
-
this.logger.debug(message)
|
|
150
|
-
this.logger.debug(
|
|
135
|
+
? '[SQL] ' + data.sql + '\n[Params] ' + JSON.stringify(data.params)
|
|
136
|
+
: '[SQL] ' + data.sql
|
|
137
|
+
this.logger.debug(message)
|
|
138
|
+
this.logger.debug('[Duration] ' + duration.toFixed(2) + 'ms')
|
|
151
139
|
}
|
|
152
140
|
|
|
153
141
|
// Check for slow query
|
|
154
142
|
if (duration > this.options.slowQueryThreshold) {
|
|
155
143
|
if (this.onSlowQuery) {
|
|
156
|
-
this.onSlowQuery(data.sql, duration)
|
|
144
|
+
this.onSlowQuery(data.sql, duration)
|
|
157
145
|
} else {
|
|
158
|
-
this.logger.warn(
|
|
146
|
+
this.logger.warn('[SLOW QUERY] ' + duration.toFixed(2) + 'ms: ' + data.sql)
|
|
159
147
|
}
|
|
160
148
|
}
|
|
161
149
|
}
|
|
162
150
|
|
|
163
|
-
return Promise.resolve(args.result)
|
|
151
|
+
return Promise.resolve(args.result)
|
|
164
152
|
}
|
|
165
153
|
|
|
166
154
|
getMetrics(): QueryMetrics[] {
|
|
167
|
-
return
|
|
155
|
+
return this.metricsBuffer.getOrdered()
|
|
168
156
|
}
|
|
169
157
|
|
|
170
158
|
clearMetrics(): void {
|
|
171
|
-
this.
|
|
159
|
+
this.metricsBuffer.clear()
|
|
172
160
|
}
|
|
173
161
|
}
|
|
174
162
|
|
|
@@ -177,9 +165,9 @@ class DebugPlugin implements KyselyPlugin {
|
|
|
177
165
|
*/
|
|
178
166
|
export interface DebugDatabase<DB> extends Kysely<DB> {
|
|
179
167
|
/** Get all collected query metrics */
|
|
180
|
-
getMetrics(): QueryMetrics[]
|
|
168
|
+
getMetrics(): QueryMetrics[]
|
|
181
169
|
/** Clear all collected metrics */
|
|
182
|
-
clearMetrics(): void
|
|
170
|
+
clearMetrics(): void
|
|
183
171
|
}
|
|
184
172
|
|
|
185
173
|
/**
|
|
@@ -202,7 +190,7 @@ export interface DebugDatabase<DB> extends Kysely<DB> {
|
|
|
202
190
|
*
|
|
203
191
|
* // Get collected metrics
|
|
204
192
|
* const metrics = debugDb.getMetrics();
|
|
205
|
-
* console.log(
|
|
193
|
+
* console.log('Total queries: ' + metrics.length);
|
|
206
194
|
* ```
|
|
207
195
|
*
|
|
208
196
|
* @example With custom options
|
|
@@ -215,23 +203,20 @@ export interface DebugDatabase<DB> extends Kysely<DB> {
|
|
|
215
203
|
* slowQueryThreshold: 50,
|
|
216
204
|
* maxMetrics: 500,
|
|
217
205
|
* onSlowQuery: (sql, duration) => {
|
|
218
|
-
* alertService.notify(
|
|
206
|
+
* alertService.notify('Slow query: ' + duration + 'ms');
|
|
219
207
|
* },
|
|
220
208
|
* });
|
|
221
209
|
* ```
|
|
222
210
|
*/
|
|
223
|
-
export function withDebug<DB>(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
): DebugDatabase<DB> {
|
|
227
|
-
const plugin = new DebugPlugin(options);
|
|
228
|
-
const debugDb = db.withPlugin(plugin) as DebugDatabase<DB>;
|
|
211
|
+
export function withDebug<DB>(db: Kysely<DB>, options: DebugOptions = {}): DebugDatabase<DB> {
|
|
212
|
+
const plugin = new DebugPlugin(options)
|
|
213
|
+
const debugDb = db.withPlugin(plugin) as DebugDatabase<DB>
|
|
229
214
|
|
|
230
215
|
// Attach metrics methods
|
|
231
|
-
debugDb.getMetrics = (): QueryMetrics[] => plugin.getMetrics()
|
|
216
|
+
debugDb.getMetrics = (): QueryMetrics[] => plugin.getMetrics()
|
|
232
217
|
debugDb.clearMetrics = (): void => {
|
|
233
|
-
plugin.clearMetrics()
|
|
234
|
-
}
|
|
218
|
+
plugin.clearMetrics()
|
|
219
|
+
}
|
|
235
220
|
|
|
236
|
-
return debugDb
|
|
221
|
+
return debugDb
|
|
237
222
|
}
|
package/src/profiler.ts
CHANGED
|
@@ -4,24 +4,25 @@
|
|
|
4
4
|
* @module @kysera/debug
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { QueryMetrics } from '
|
|
7
|
+
import type { QueryMetrics } from '@kysera/core'
|
|
8
|
+
import { CircularBuffer } from './circular-buffer.js'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Query profiler summary.
|
|
11
12
|
*/
|
|
12
13
|
export interface ProfilerSummary {
|
|
13
14
|
/** Total number of recorded queries */
|
|
14
|
-
totalQueries: number
|
|
15
|
+
totalQueries: number
|
|
15
16
|
/** Sum of all query durations */
|
|
16
|
-
totalDuration: number
|
|
17
|
+
totalDuration: number
|
|
17
18
|
/** Average query duration */
|
|
18
|
-
averageDuration: number
|
|
19
|
+
averageDuration: number
|
|
19
20
|
/** Slowest recorded query */
|
|
20
|
-
slowestQuery: QueryMetrics | null
|
|
21
|
+
slowestQuery: QueryMetrics | null
|
|
21
22
|
/** Fastest recorded query */
|
|
22
|
-
fastestQuery: QueryMetrics | null
|
|
23
|
+
fastestQuery: QueryMetrics | null
|
|
23
24
|
/** All recorded queries */
|
|
24
|
-
queries: QueryMetrics[]
|
|
25
|
+
queries: QueryMetrics[]
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -32,7 +33,7 @@ export interface ProfilerOptions {
|
|
|
32
33
|
* Maximum number of queries to keep in memory.
|
|
33
34
|
* @default 1000
|
|
34
35
|
*/
|
|
35
|
-
maxQueries?: number
|
|
36
|
+
maxQueries?: number
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/**
|
|
@@ -56,16 +57,15 @@ export interface ProfilerOptions {
|
|
|
56
57
|
*
|
|
57
58
|
* // Get summary
|
|
58
59
|
* const summary = profiler.getSummary();
|
|
59
|
-
* console.log(
|
|
60
|
-
* console.log(
|
|
60
|
+
* console.log('Total queries: ' + summary.totalQueries);
|
|
61
|
+
* console.log('Average duration: ' + summary.averageDuration.toFixed(2) + 'ms');
|
|
61
62
|
*
|
|
62
63
|
* // Clear recorded queries
|
|
63
64
|
* profiler.clear();
|
|
64
65
|
* ```
|
|
65
66
|
*/
|
|
66
67
|
export class QueryProfiler {
|
|
67
|
-
private
|
|
68
|
-
private readonly maxQueries: number;
|
|
68
|
+
private readonly queriesBuffer: CircularBuffer<QueryMetrics>
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* Create a new query profiler.
|
|
@@ -73,20 +73,19 @@ export class QueryProfiler {
|
|
|
73
73
|
* @param options - Profiler options
|
|
74
74
|
*/
|
|
75
75
|
constructor(options: ProfilerOptions = {}) {
|
|
76
|
-
this.
|
|
76
|
+
this.queriesBuffer = new CircularBuffer<QueryMetrics>(options.maxQueries ?? 1000)
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
80
|
* Record a query metric.
|
|
81
81
|
*
|
|
82
|
+
* Uses O(1) circular buffer to maintain bounded memory usage.
|
|
83
|
+
* When the buffer is full, oldest entries are overwritten.
|
|
84
|
+
*
|
|
82
85
|
* @param metric - Query metrics to record
|
|
83
86
|
*/
|
|
84
87
|
record(metric: QueryMetrics): void {
|
|
85
|
-
this.
|
|
86
|
-
// Circular buffer: keep only last N queries
|
|
87
|
-
if (this.queries.length > this.maxQueries) {
|
|
88
|
-
this.queries.shift();
|
|
89
|
-
}
|
|
88
|
+
this.queriesBuffer.add(metric)
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
/**
|
|
@@ -95,28 +94,30 @@ export class QueryProfiler {
|
|
|
95
94
|
* @returns Summary of all recorded queries
|
|
96
95
|
*/
|
|
97
96
|
getSummary(): ProfilerSummary {
|
|
98
|
-
|
|
97
|
+
const orderedQueries = this.getOrderedQueries()
|
|
98
|
+
|
|
99
|
+
if (orderedQueries.length === 0) {
|
|
99
100
|
return {
|
|
100
101
|
totalQueries: 0,
|
|
101
102
|
totalDuration: 0,
|
|
102
103
|
averageDuration: 0,
|
|
103
104
|
slowestQuery: null,
|
|
104
105
|
fastestQuery: null,
|
|
105
|
-
queries: []
|
|
106
|
-
}
|
|
106
|
+
queries: []
|
|
107
|
+
}
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
const totalDuration =
|
|
110
|
-
const sorted = [...
|
|
110
|
+
const totalDuration = orderedQueries.reduce((sum, q) => sum + q.duration, 0)
|
|
111
|
+
const sorted = [...orderedQueries].sort((a, b) => b.duration - a.duration)
|
|
111
112
|
|
|
112
113
|
return {
|
|
113
|
-
totalQueries:
|
|
114
|
+
totalQueries: orderedQueries.length,
|
|
114
115
|
totalDuration,
|
|
115
|
-
averageDuration: totalDuration /
|
|
116
|
+
averageDuration: totalDuration / orderedQueries.length,
|
|
116
117
|
slowestQuery: sorted[0] ?? null,
|
|
117
118
|
fastestQuery: sorted[sorted.length - 1] ?? null,
|
|
118
|
-
queries:
|
|
119
|
-
}
|
|
119
|
+
queries: orderedQueries
|
|
120
|
+
}
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
/**
|
|
@@ -126,9 +127,7 @@ export class QueryProfiler {
|
|
|
126
127
|
* @returns Array of slowest queries
|
|
127
128
|
*/
|
|
128
129
|
getSlowestQueries(count: number): QueryMetrics[] {
|
|
129
|
-
return [...this.
|
|
130
|
-
.sort((a, b) => b.duration - a.duration)
|
|
131
|
-
.slice(0, count);
|
|
130
|
+
return [...this.getOrderedQueries()].sort((a, b) => b.duration - a.duration).slice(0, count)
|
|
132
131
|
}
|
|
133
132
|
|
|
134
133
|
/**
|
|
@@ -138,20 +137,30 @@ export class QueryProfiler {
|
|
|
138
137
|
* @returns Array of slow queries
|
|
139
138
|
*/
|
|
140
139
|
getSlowQueries(thresholdMs: number): QueryMetrics[] {
|
|
141
|
-
return this.
|
|
140
|
+
return this.getOrderedQueries().filter(q => q.duration > thresholdMs)
|
|
142
141
|
}
|
|
143
142
|
|
|
144
143
|
/**
|
|
145
144
|
* Clear all recorded queries.
|
|
146
145
|
*/
|
|
147
146
|
clear(): void {
|
|
148
|
-
this.
|
|
147
|
+
this.queriesBuffer.clear()
|
|
149
148
|
}
|
|
150
149
|
|
|
151
150
|
/**
|
|
152
151
|
* Get the number of recorded queries.
|
|
153
152
|
*/
|
|
154
153
|
get count(): number {
|
|
155
|
-
return this.
|
|
154
|
+
return this.queriesBuffer.size
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get queries in chronological order.
|
|
159
|
+
* Handles circular buffer wrap-around correctly.
|
|
160
|
+
*
|
|
161
|
+
* @returns Array of queries in chronological order
|
|
162
|
+
*/
|
|
163
|
+
private getOrderedQueries(): QueryMetrics[] {
|
|
164
|
+
return this.queriesBuffer.getOrdered()
|
|
156
165
|
}
|
|
157
166
|
}
|