@kysera/debug 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +663 -0
- package/dist/index.d.ts +297 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/src/format.ts +176 -0
- package/src/index.ts +52 -0
- package/src/plugin.ts +237 -0
- package/src/profiler.ts +157 -0
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug plugin for Kysely.
|
|
3
|
+
*
|
|
4
|
+
* @module @kysera/debug
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
Kysely,
|
|
9
|
+
PluginTransformQueryArgs,
|
|
10
|
+
PluginTransformResultArgs,
|
|
11
|
+
QueryResult,
|
|
12
|
+
UnknownRow,
|
|
13
|
+
KyselyPlugin,
|
|
14
|
+
RootOperationNode,
|
|
15
|
+
} from 'kysely';
|
|
16
|
+
import { DefaultQueryCompiler } from 'kysely';
|
|
17
|
+
import { consoleLogger, type KyseraLogger } from '@kysera/core';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Query metrics data.
|
|
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
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Options for debug plugin.
|
|
35
|
+
*/
|
|
36
|
+
export interface DebugOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Log query SQL.
|
|
39
|
+
* @default true
|
|
40
|
+
*/
|
|
41
|
+
logQuery?: boolean;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Log query parameters.
|
|
45
|
+
* @default false
|
|
46
|
+
*/
|
|
47
|
+
logParams?: boolean;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Duration threshold (ms) to consider a query slow.
|
|
51
|
+
* @default 100
|
|
52
|
+
*/
|
|
53
|
+
slowQueryThreshold?: number;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Callback for slow queries.
|
|
57
|
+
*/
|
|
58
|
+
onSlowQuery?: (sql: string, duration: number) => void;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Logger for debug messages.
|
|
62
|
+
* @default consoleLogger
|
|
63
|
+
*/
|
|
64
|
+
logger?: KyseraLogger;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Maximum number of metrics to keep in memory.
|
|
68
|
+
* When limit is reached, oldest metrics are removed (circular buffer).
|
|
69
|
+
* @default 1000
|
|
70
|
+
*/
|
|
71
|
+
maxMetrics?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Internal query data for tracking execution.
|
|
76
|
+
* @internal
|
|
77
|
+
*/
|
|
78
|
+
interface QueryData {
|
|
79
|
+
startTime: number;
|
|
80
|
+
sql: string;
|
|
81
|
+
params: readonly unknown[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Debug plugin implementation.
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
class DebugPlugin implements KyselyPlugin {
|
|
89
|
+
private metrics: QueryMetrics[] = [];
|
|
90
|
+
private queryData = new WeakMap<object, QueryData>();
|
|
91
|
+
private readonly maxMetrics: number;
|
|
92
|
+
private readonly logger: KyseraLogger;
|
|
93
|
+
private readonly options: Required<Pick<DebugOptions, 'logQuery' | 'logParams' | 'slowQueryThreshold'>>;
|
|
94
|
+
private readonly onSlowQuery: ((sql: string, duration: number) => void) | undefined;
|
|
95
|
+
|
|
96
|
+
constructor(options: DebugOptions = {}) {
|
|
97
|
+
this.logger = options.logger ?? consoleLogger;
|
|
98
|
+
this.maxMetrics = options.maxMetrics ?? 1000;
|
|
99
|
+
this.onSlowQuery = options.onSlowQuery;
|
|
100
|
+
this.options = {
|
|
101
|
+
logQuery: options.logQuery ?? true,
|
|
102
|
+
logParams: options.logParams ?? false,
|
|
103
|
+
slowQueryThreshold: options.slowQueryThreshold ?? 100,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
|
|
108
|
+
const startTime = performance.now();
|
|
109
|
+
|
|
110
|
+
// Compile the query to get SQL and parameters
|
|
111
|
+
const compiler = new DefaultQueryCompiler();
|
|
112
|
+
const compiled = compiler.compileQuery(args.node, args.queryId);
|
|
113
|
+
|
|
114
|
+
// Store query data for later use in transformResult
|
|
115
|
+
this.queryData.set(args.queryId, {
|
|
116
|
+
startTime,
|
|
117
|
+
sql: compiled.sql,
|
|
118
|
+
params: compiled.parameters,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return args.node;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {
|
|
125
|
+
const data = this.queryData.get(args.queryId);
|
|
126
|
+
|
|
127
|
+
if (data) {
|
|
128
|
+
const endTime = performance.now();
|
|
129
|
+
const duration = endTime - data.startTime;
|
|
130
|
+
this.queryData.delete(args.queryId);
|
|
131
|
+
|
|
132
|
+
const metric: QueryMetrics = {
|
|
133
|
+
sql: data.sql,
|
|
134
|
+
params: [...data.params],
|
|
135
|
+
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();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (this.options.logQuery) {
|
|
146
|
+
const message = this.options.logParams
|
|
147
|
+
? `[SQL] ${data.sql}\n[Params] ${JSON.stringify(data.params)}`
|
|
148
|
+
: `[SQL] ${data.sql}`;
|
|
149
|
+
this.logger.debug(message);
|
|
150
|
+
this.logger.debug(`[Duration] ${duration.toFixed(2)}ms`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check for slow query
|
|
154
|
+
if (duration > this.options.slowQueryThreshold) {
|
|
155
|
+
if (this.onSlowQuery) {
|
|
156
|
+
this.onSlowQuery(data.sql, duration);
|
|
157
|
+
} else {
|
|
158
|
+
this.logger.warn(`[SLOW QUERY] ${duration.toFixed(2)}ms: ${data.sql}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return Promise.resolve(args.result);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
getMetrics(): QueryMetrics[] {
|
|
167
|
+
return [...this.metrics];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
clearMetrics(): void {
|
|
171
|
+
this.metrics = [];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Database with debug capabilities.
|
|
177
|
+
*/
|
|
178
|
+
export interface DebugDatabase<DB> extends Kysely<DB> {
|
|
179
|
+
/** Get all collected query metrics */
|
|
180
|
+
getMetrics(): QueryMetrics[];
|
|
181
|
+
/** Clear all collected metrics */
|
|
182
|
+
clearMetrics(): void;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Wrap a Kysely database with debug capabilities.
|
|
187
|
+
*
|
|
188
|
+
* Adds query logging, metrics collection, and slow query detection.
|
|
189
|
+
*
|
|
190
|
+
* @param db - Kysely database instance
|
|
191
|
+
* @param options - Debug options
|
|
192
|
+
* @returns Database with debug capabilities
|
|
193
|
+
*
|
|
194
|
+
* @example Basic usage
|
|
195
|
+
* ```typescript
|
|
196
|
+
* import { withDebug } from '@kysera/debug';
|
|
197
|
+
*
|
|
198
|
+
* const debugDb = withDebug(db);
|
|
199
|
+
*
|
|
200
|
+
* // Queries are now logged and timed
|
|
201
|
+
* await debugDb.selectFrom('users').selectAll().execute();
|
|
202
|
+
*
|
|
203
|
+
* // Get collected metrics
|
|
204
|
+
* const metrics = debugDb.getMetrics();
|
|
205
|
+
* console.log(`Total queries: ${metrics.length}`);
|
|
206
|
+
* ```
|
|
207
|
+
*
|
|
208
|
+
* @example With custom options
|
|
209
|
+
* ```typescript
|
|
210
|
+
* import { withDebug } from '@kysera/debug';
|
|
211
|
+
*
|
|
212
|
+
* const debugDb = withDebug(db, {
|
|
213
|
+
* logQuery: true,
|
|
214
|
+
* logParams: true,
|
|
215
|
+
* slowQueryThreshold: 50,
|
|
216
|
+
* maxMetrics: 500,
|
|
217
|
+
* onSlowQuery: (sql, duration) => {
|
|
218
|
+
* alertService.notify(`Slow query: ${duration}ms`);
|
|
219
|
+
* },
|
|
220
|
+
* });
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
export function withDebug<DB>(
|
|
224
|
+
db: Kysely<DB>,
|
|
225
|
+
options: DebugOptions = {}
|
|
226
|
+
): DebugDatabase<DB> {
|
|
227
|
+
const plugin = new DebugPlugin(options);
|
|
228
|
+
const debugDb = db.withPlugin(plugin) as DebugDatabase<DB>;
|
|
229
|
+
|
|
230
|
+
// Attach metrics methods
|
|
231
|
+
debugDb.getMetrics = (): QueryMetrics[] => plugin.getMetrics();
|
|
232
|
+
debugDb.clearMetrics = (): void => {
|
|
233
|
+
plugin.clearMetrics();
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return debugDb;
|
|
237
|
+
}
|
package/src/profiler.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query profiler for performance analysis.
|
|
3
|
+
*
|
|
4
|
+
* @module @kysera/debug
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { QueryMetrics } from './plugin.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Query profiler summary.
|
|
11
|
+
*/
|
|
12
|
+
export interface ProfilerSummary {
|
|
13
|
+
/** Total number of recorded queries */
|
|
14
|
+
totalQueries: number;
|
|
15
|
+
/** Sum of all query durations */
|
|
16
|
+
totalDuration: number;
|
|
17
|
+
/** Average query duration */
|
|
18
|
+
averageDuration: number;
|
|
19
|
+
/** Slowest recorded query */
|
|
20
|
+
slowestQuery: QueryMetrics | null;
|
|
21
|
+
/** Fastest recorded query */
|
|
22
|
+
fastestQuery: QueryMetrics | null;
|
|
23
|
+
/** All recorded queries */
|
|
24
|
+
queries: QueryMetrics[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Options for QueryProfiler.
|
|
29
|
+
*/
|
|
30
|
+
export interface ProfilerOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Maximum number of queries to keep in memory.
|
|
33
|
+
* @default 1000
|
|
34
|
+
*/
|
|
35
|
+
maxQueries?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Query profiler for collecting and analyzing query performance.
|
|
40
|
+
*
|
|
41
|
+
* Provides detailed statistics about query execution times
|
|
42
|
+
* including average, min, max, and query counts.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* import { QueryProfiler } from '@kysera/debug';
|
|
47
|
+
*
|
|
48
|
+
* const profiler = new QueryProfiler({ maxQueries: 500 });
|
|
49
|
+
*
|
|
50
|
+
* // Record queries manually
|
|
51
|
+
* profiler.record({
|
|
52
|
+
* sql: 'SELECT * FROM users',
|
|
53
|
+
* duration: 10,
|
|
54
|
+
* timestamp: Date.now(),
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* // Get summary
|
|
58
|
+
* const summary = profiler.getSummary();
|
|
59
|
+
* console.log(`Total queries: ${summary.totalQueries}`);
|
|
60
|
+
* console.log(`Average duration: ${summary.averageDuration.toFixed(2)}ms`);
|
|
61
|
+
*
|
|
62
|
+
* // Clear recorded queries
|
|
63
|
+
* profiler.clear();
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export class QueryProfiler {
|
|
67
|
+
private queries: QueryMetrics[] = [];
|
|
68
|
+
private readonly maxQueries: number;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create a new query profiler.
|
|
72
|
+
*
|
|
73
|
+
* @param options - Profiler options
|
|
74
|
+
*/
|
|
75
|
+
constructor(options: ProfilerOptions = {}) {
|
|
76
|
+
this.maxQueries = options.maxQueries ?? 1000;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Record a query metric.
|
|
81
|
+
*
|
|
82
|
+
* @param metric - Query metrics to record
|
|
83
|
+
*/
|
|
84
|
+
record(metric: QueryMetrics): void {
|
|
85
|
+
this.queries.push(metric);
|
|
86
|
+
// Circular buffer: keep only last N queries
|
|
87
|
+
if (this.queries.length > this.maxQueries) {
|
|
88
|
+
this.queries.shift();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get profiling summary.
|
|
94
|
+
*
|
|
95
|
+
* @returns Summary of all recorded queries
|
|
96
|
+
*/
|
|
97
|
+
getSummary(): ProfilerSummary {
|
|
98
|
+
if (this.queries.length === 0) {
|
|
99
|
+
return {
|
|
100
|
+
totalQueries: 0,
|
|
101
|
+
totalDuration: 0,
|
|
102
|
+
averageDuration: 0,
|
|
103
|
+
slowestQuery: null,
|
|
104
|
+
fastestQuery: null,
|
|
105
|
+
queries: [],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const totalDuration = this.queries.reduce((sum, q) => sum + q.duration, 0);
|
|
110
|
+
const sorted = [...this.queries].sort((a, b) => b.duration - a.duration);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
totalQueries: this.queries.length,
|
|
114
|
+
totalDuration,
|
|
115
|
+
averageDuration: totalDuration / this.queries.length,
|
|
116
|
+
slowestQuery: sorted[0] ?? null,
|
|
117
|
+
fastestQuery: sorted[sorted.length - 1] ?? null,
|
|
118
|
+
queries: [...this.queries],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get the slowest N queries.
|
|
124
|
+
*
|
|
125
|
+
* @param count - Number of queries to return
|
|
126
|
+
* @returns Array of slowest queries
|
|
127
|
+
*/
|
|
128
|
+
getSlowestQueries(count: number): QueryMetrics[] {
|
|
129
|
+
return [...this.queries]
|
|
130
|
+
.sort((a, b) => b.duration - a.duration)
|
|
131
|
+
.slice(0, count);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get queries slower than a threshold.
|
|
136
|
+
*
|
|
137
|
+
* @param thresholdMs - Duration threshold in milliseconds
|
|
138
|
+
* @returns Array of slow queries
|
|
139
|
+
*/
|
|
140
|
+
getSlowQueries(thresholdMs: number): QueryMetrics[] {
|
|
141
|
+
return this.queries.filter((q) => q.duration > thresholdMs);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Clear all recorded queries.
|
|
146
|
+
*/
|
|
147
|
+
clear(): void {
|
|
148
|
+
this.queries = [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get the number of recorded queries.
|
|
153
|
+
*/
|
|
154
|
+
get count(): number {
|
|
155
|
+
return this.queries.length;
|
|
156
|
+
}
|
|
157
|
+
}
|