Skip to content

Commit ffb8d93

Browse files
author
Dylan Dietz
committed
feat: add caller location logging for SQL queries with Rails-style indicator
1 parent 29b5b63 commit ffb8d93

4 files changed

Lines changed: 96 additions & 7 deletions

File tree

src/logger.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
createTruncateForLog,
1414
getQueryType,
1515
formatParams,
16+
getCallerLocation,
1617
} from './utils.js';
1718
import { createSqlFormatter } from './sqlFormatter.js';
1819

@@ -123,8 +124,8 @@ export class EnhancedLogger {
123124

124125
// Handle SQL query logs - Winston spreads data directly on info, not in message
125126
if (level === 'query' && 'query' in info && 'type' in info) {
126-
const queryInfo = info as unknown as { type: string; query: string; params?: string; duration: string; timestamp: string };
127-
const { query, params = '', duration, type } = queryInfo;
127+
const queryInfo = info as unknown as { type: string; query: string; params?: string; duration: string; timestamp: string; caller?: string };
128+
const { query, params = '', duration, type, caller } = queryInfo;
128129

129130
// Determine query type for color coding
130131
const queryType = type.toUpperCase();
@@ -212,7 +213,7 @@ export class EnhancedLogger {
212213
if (this.config.enableColors) {
213214
switch (queryType) {
214215
case 'SELECT':
215-
logLine = chalk.blue(logLine);
216+
logLine = chalk.cyan(logLine); // Light blue for all SELECT queries
216217
break;
217218
case 'UPDATE':
218219
logLine = chalk.yellow(logLine);
@@ -234,6 +235,12 @@ export class EnhancedLogger {
234235
}
235236
}
236237

238+
// Add caller location if available (Rails-style ↳ indicator)
239+
if (caller) {
240+
const callerLine = this.config.enableColors ? chalk.dim(` ${caller}`) : ` ${caller}`;
241+
logLine += `\n${callerLine}`;
242+
}
243+
237244
return logLine;
238245
}
239246

@@ -406,6 +413,9 @@ export class EnhancedLogger {
406413
const queryType = getQueryType(e.query);
407414
const formattedParams = formatParams(e.params);
408415
const duration = Number(e.duration);
416+
417+
// Capture caller location
418+
const caller = getCallerLocation();
409419

410420
if (duration > this.config.slowQueryThreshold) {
411421
// Truncate both query and params for slow queries
@@ -414,6 +424,9 @@ export class EnhancedLogger {
414424

415425
// Rails-style slow query logging
416426
this.warn(` ${queryType} (${duration}ms) ${truncatedQuery} ${truncatedParams}`);
427+
if (caller) {
428+
this.warn(` ${caller}`);
429+
}
417430
this.warn(` Slow query detected`);
418431
} else {
419432
// Use logger.query() with proper QueryLogData format for Rails-style formatting
@@ -422,6 +435,7 @@ export class EnhancedLogger {
422435
query: e.query,
423436
params: formattedParams,
424437
duration: `${duration}ms`,
438+
caller, // Add caller location
425439
});
426440
}
427441
});

src/sqlFormatter.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@ export const createSqlFormatter = (config: LoggerConfig) => {
88
const truncateForLog = createTruncateForLog(config);
99
const enableColors = config.enableColors ?? true;
1010
const maxStringLength = config.maxStringLength ?? 100;
11-
12-
// For chalk v4: Just use the default instance when colors enabled
13-
// When disabled, we'll just pass through without coloring
14-
const chalkInstance = chalk;
1511

1612
const formatSqlQuery = (query: string, params: string): string => {
1713
// If custom formatter is provided, use it

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export interface QueryLogData {
7474
query: string;
7575
params: string;
7676
duration: string;
77+
/** Optional caller location (Rails-style ↳ indicator) */
78+
caller?: string;
7779
}
7880

7981
export interface RequestLogData {

src/utils.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,80 @@ export function formatParams(params: unknown): string {
253253
return String(params);
254254
}
255255
}
256+
257+
/**
258+
* Get caller location from stack trace (Rails-style ↳ indicator)
259+
* Returns formatted location like "↳ app/controllers/users.ts:42:in `getUser`"
260+
*/
261+
export function getCallerLocation(): string {
262+
const oldLimit = Error.stackTraceLimit;
263+
Error.stackTraceLimit = 50; // Capture enough frames
264+
265+
const stack = new Error().stack || '';
266+
Error.stackTraceLimit = oldLimit;
267+
268+
const lines = stack.split('\n');
269+
270+
// Start from frame 1 (skip "Error" line)
271+
for (let i = 1; i < lines.length; i++) {
272+
const line = lines[i];
273+
274+
if (!line) continue;
275+
276+
// Skip internal frames we want to ignore
277+
if (
278+
line.includes('node_modules') ||
279+
line.includes('node:internal') ||
280+
line.includes('node:diagnostics_channel') ||
281+
line.includes('node:async_hooks') ||
282+
line.includes('/dist/') || // Our compiled library code
283+
line.includes('logger.cjs') ||
284+
line.includes('logger.js') ||
285+
line.includes('utils.cjs') ||
286+
line.includes('utils.js') ||
287+
line.includes('sqlFormatter') ||
288+
line.includes('EnhancedLogger') ||
289+
line.includes('setupPrismaLogging') ||
290+
line.includes('getCallerLocation') ||
291+
line.includes('winston') ||
292+
line.includes('prisma/client') ||
293+
line.includes('prisma-client') ||
294+
line.includes('@prisma') ||
295+
line.includes('TracingChannel') ||
296+
line.includes('Module._compile') ||
297+
line.includes('Module.load') ||
298+
line.includes('Function._load') ||
299+
line.includes('_triggerQuery') || // Mock Prisma trigger
300+
line.includes('.$on(') || // Prisma event registration
301+
line.includes('PrismaClient') // Prisma client internals
302+
) {
303+
continue;
304+
}
305+
306+
// Try to extract file location
307+
// Format: " at functionName (file:line:column)" or " at file:line:column"
308+
const match = line.match(/at\s+(?:([^\s]+)\s+\()?([^)]+):(\d+):(\d+)\)?/);
309+
310+
if (match) {
311+
const functionName = match[1] || 'anonymous';
312+
let filePath = match[2] || '';
313+
const lineNum = match[3];
314+
315+
if (!filePath) continue;
316+
317+
// Clean up file path - remove file:// protocol
318+
filePath = filePath.replace('file://', '');
319+
320+
// Try to make it relative to current working directory
321+
const cwd = process.cwd();
322+
if (filePath.startsWith(cwd)) {
323+
filePath = filePath.substring(cwd.length + 1);
324+
}
325+
326+
// Format like Rails: ↳ path/to/file.ts:line:in `function`
327+
return `↳ ${filePath}:${lineNum}:in \`${functionName}\``;
328+
}
329+
}
330+
331+
return ''; // No suitable caller found
332+
}

0 commit comments

Comments
 (0)