Skip to content

Commit 5931c0f

Browse files
committed
fix: add comprehensive database/cache logging and update Grafana dashboard
1 parent e8bc1bd commit 5931c0f

File tree

6 files changed

+65
-5
lines changed

6 files changed

+65
-5
lines changed

src/lib/logger.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -376,12 +376,11 @@ class Logger {
376376
userId: userId || "anonymous",
377377
};
378378

379-
// Log slow queries at info level (visible in production)
379+
// Log all queries at info level for production visibility
380380
if (duration > 1000) {
381381
this.warn(`[DB] Slow query: ${operation} ${table} took ${duration}ms`, meta);
382382
} else {
383-
// Fast queries only logged in debug (dev-only)
384-
this.debug(`[DB] ${operation} ${table}`, meta);
383+
this.info(`[DB] ${operation} ${table}`, meta);
385384
}
386385
}
387386

@@ -464,8 +463,8 @@ class Logger {
464463
meta.keyCount = keyCount;
465464
}
466465

467-
// Log at debug level
468-
this.debug(`[CACHE] ${operation}${hit !== undefined ? (hit ? " HIT" : " MISS") : ""}`, meta);
466+
// Log at info level for production visibility
467+
this.info(`[CACHE] ${operation}${hit !== undefined ? (hit ? " HIT" : " MISS") : ""}`, meta);
469468
}
470469

471470
cacheError(operation: string, key: string, error: Error): void {

src/routes/notes/actions.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { db, notes } from "../../db";
44
import { eq, and } from "drizzle-orm";
55
import { noteSchema, noteIdParamSchema } from "../../lib/openapi-schemas";
66
import { invalidateNoteCounts } from "../../lib/cache";
7+
import { logger } from "../../lib/logger";
78

89
const actionsRouter = new OpenAPIHono();
910

@@ -40,14 +41,17 @@ actionsRouter.openapi(starNoteRoute, async (c) => {
4041
const userId = c.get("userId");
4142
const { id: noteId } = c.req.valid("param");
4243

44+
const selectStart = Date.now();
4345
const existingNote = await db.query.notes.findFirst({
4446
where: and(eq(notes.id, noteId), eq(notes.userId, userId)),
4547
});
48+
logger.databaseQuery("select", "notes", Date.now() - selectStart, userId);
4649

4750
if (!existingNote) {
4851
throw new HTTPException(404, { message: "Note not found" });
4952
}
5053

54+
const updateStart = Date.now();
5155
const [updatedNote] = await db
5256
.update(notes)
5357
.set({
@@ -56,6 +60,7 @@ actionsRouter.openapi(starNoteRoute, async (c) => {
5660
})
5761
.where(eq(notes.id, noteId))
5862
.returning();
63+
logger.databaseQuery("update", "notes", Date.now() - updateStart, userId);
5964

6065
// Invalidate counts cache for the note's folder hierarchy
6166
await invalidateNoteCounts(userId, existingNote.folderId);
@@ -96,14 +101,17 @@ actionsRouter.openapi(restoreNoteRoute, async (c) => {
96101
const userId = c.get("userId");
97102
const { id: noteId } = c.req.valid("param");
98103

104+
const selectStart = Date.now();
99105
const existingNote = await db.query.notes.findFirst({
100106
where: and(eq(notes.id, noteId), eq(notes.userId, userId)),
101107
});
108+
logger.databaseQuery("select", "notes", Date.now() - selectStart, userId);
102109

103110
if (!existingNote) {
104111
throw new HTTPException(404, { message: "Note not found" });
105112
}
106113

114+
const updateStart = Date.now();
107115
const [restoredNote] = await db
108116
.update(notes)
109117
.set({
@@ -113,6 +121,7 @@ actionsRouter.openapi(restoreNoteRoute, async (c) => {
113121
})
114122
.where(eq(notes.id, noteId))
115123
.returning();
124+
logger.databaseQuery("update", "notes", Date.now() - updateStart, userId);
116125

117126
// Invalidate counts cache for the note's folder hierarchy
118127
await invalidateNoteCounts(userId, existingNote.folderId);
@@ -156,9 +165,11 @@ actionsRouter.openapi(hideNoteRoute, async (c) => {
156165
const userId = c.get("userId");
157166
const { id: noteId } = c.req.valid("param");
158167

168+
const selectStart = Date.now();
159169
const existingNote = await db.query.notes.findFirst({
160170
where: and(eq(notes.id, noteId), eq(notes.userId, userId)),
161171
});
172+
logger.databaseQuery("select", "notes", Date.now() - selectStart, userId);
162173

163174
if (!existingNote) {
164175
throw new HTTPException(404, { message: "Note not found" });
@@ -168,6 +179,7 @@ actionsRouter.openapi(hideNoteRoute, async (c) => {
168179
throw new HTTPException(400, { message: "Note is already hidden" });
169180
}
170181

182+
const updateStart = Date.now();
171183
const [hiddenNote] = await db
172184
.update(notes)
173185
.set({
@@ -177,6 +189,7 @@ actionsRouter.openapi(hideNoteRoute, async (c) => {
177189
})
178190
.where(eq(notes.id, noteId))
179191
.returning();
192+
logger.databaseQuery("update", "notes", Date.now() - updateStart, userId);
180193

181194
// Invalidate counts cache for the note's folder hierarchy
182195
await invalidateNoteCounts(userId, existingNote.folderId);
@@ -220,9 +233,11 @@ actionsRouter.openapi(unhideNoteRoute, async (c) => {
220233
const userId = c.get("userId");
221234
const { id: noteId } = c.req.valid("param");
222235

236+
const selectStart = Date.now();
223237
const existingNote = await db.query.notes.findFirst({
224238
where: and(eq(notes.id, noteId), eq(notes.userId, userId)),
225239
});
240+
logger.databaseQuery("select", "notes", Date.now() - selectStart, userId);
226241

227242
if (!existingNote) {
228243
throw new HTTPException(404, { message: "Note not found" });
@@ -232,6 +247,7 @@ actionsRouter.openapi(unhideNoteRoute, async (c) => {
232247
throw new HTTPException(400, { message: "Note is not hidden" });
233248
}
234249

250+
const updateStart = Date.now();
235251
const [unhiddenNote] = await db
236252
.update(notes)
237253
.set({
@@ -241,6 +257,7 @@ actionsRouter.openapi(unhideNoteRoute, async (c) => {
241257
})
242258
.where(eq(notes.id, noteId))
243259
.returning();
260+
logger.databaseQuery("update", "notes", Date.now() - updateStart, userId);
244261

245262
// Invalidate counts cache for the note's folder hierarchy
246263
await invalidateNoteCounts(userId, existingNote.folderId);

src/routes/notes/counts.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { eq, and, isNull, sql } from "drizzle-orm";
44
import { getCache, setCache } from "../../lib/cache";
55
import { CacheKeys, CacheTTL } from "../../lib/cache-keys";
66
import { z } from "@hono/zod-openapi";
7+
import { logger } from "../../lib/logger";
78

89
const countsRouter = new OpenAPIHono();
910

@@ -18,6 +19,7 @@ async function getAllDescendantFolderIds(
1819
return [];
1920
}
2021

22+
const queryStart = Date.now();
2123
// Use a recursive CTE to get all descendants in a single query
2224
const result = await db.execute<{ id: string }>(sql`
2325
WITH RECURSIVE folder_tree AS (
@@ -40,6 +42,7 @@ async function getAllDescendantFolderIds(
4042
)
4143
SELECT DISTINCT id FROM folder_tree
4244
`);
45+
logger.databaseQuery("select_recursive", "folders", Date.now() - queryStart, userId);
4346

4447
return (result as unknown as { id: string }[]).map((row) => row.id);
4548
}
@@ -58,6 +61,7 @@ async function getCountsForFolders(
5861
return { all: 0, starred: 0, archived: 0, trash: 0 };
5962
}
6063

64+
const queryStart = Date.now();
6165
// Get all counts in a single query using conditional aggregation
6266
const result = await db.execute<{
6367
all_count: string;
@@ -77,6 +81,7 @@ async function getCountsForFolders(
7781
sql`, `
7882
)})
7983
`);
84+
logger.databaseQuery("count_aggregate", "notes", Date.now() - queryStart, userId);
8085

8186
const rows = result as unknown as {
8287
all_count: string;
@@ -96,6 +101,7 @@ async function getCountsForFolders(
96101
// Cache warming function - call this after note/folder operations to keep cache fresh
97102
export async function warmNotesCountsCache(userId: string): Promise<void> {
98103
// Get total counts for all user's notes using single query
104+
const totalCountsStart = Date.now();
99105
const totalCountsResult = await db.execute<{
100106
all_count: string;
101107
starred_count: string;
@@ -110,6 +116,7 @@ export async function warmNotesCountsCache(userId: string): Promise<void> {
110116
FROM notes
111117
WHERE user_id = ${userId}
112118
`);
119+
logger.databaseQuery("count_aggregate", "notes", Date.now() - totalCountsStart, userId);
113120

114121
const totalCountsRows = totalCountsResult as unknown as {
115122
all_count: string;
@@ -120,10 +127,12 @@ export async function warmNotesCountsCache(userId: string): Promise<void> {
120127
const totalCounts = totalCountsRows[0];
121128

122129
// Get root-level folders (no parent)
130+
const foldersStart = Date.now();
123131
const rootFolders = await db.query.folders.findMany({
124132
where: and(eq(folders.userId, userId), isNull(folders.parentId)),
125133
columns: { id: true },
126134
});
135+
logger.databaseQuery("select", "folders", Date.now() - foldersStart, userId);
127136

128137
const folderCounts: Record<
129138
string,
@@ -252,10 +261,12 @@ const getNotesCountsHandler: RouteHandler<typeof getNotesCountsRoute> = async (c
252261
}
253262

254263
// Get direct children of the specified folder
264+
const childFoldersStart = Date.now();
255265
const childFolders = await db.query.folders.findMany({
256266
where: and(eq(folders.parentId, folderId), eq(folders.userId, userId)),
257267
columns: { id: true },
258268
});
269+
logger.databaseQuery("select", "folders", Date.now() - childFoldersStart, userId);
259270

260271
if (childFolders.length === 0) {
261272
// No child folders, return empty object
@@ -327,6 +338,7 @@ const getNotesCountsHandler: RouteHandler<typeof getNotesCountsRoute> = async (c
327338
}
328339

329340
// Get total counts for all user's notes using single query
341+
const totalCountsStart = Date.now();
330342
const totalCountsResult = await db.execute<{
331343
all_count: string;
332344
starred_count: string;
@@ -341,6 +353,7 @@ const getNotesCountsHandler: RouteHandler<typeof getNotesCountsRoute> = async (c
341353
FROM notes
342354
WHERE user_id = ${userId}
343355
`);
356+
logger.databaseQuery("count_aggregate", "notes", Date.now() - totalCountsStart, userId);
344357

345358
const totalCountsRows = totalCountsResult as unknown as {
346359
all_count: string;
@@ -351,10 +364,12 @@ const getNotesCountsHandler: RouteHandler<typeof getNotesCountsRoute> = async (c
351364
const totalCounts = totalCountsRows[0];
352365

353366
// Get root-level folders (no parent)
367+
const rootFoldersStart = Date.now();
354368
const rootFolders = await db.query.folders.findMany({
355369
where: and(eq(folders.userId, userId), isNull(folders.parentId)),
356370
columns: { id: true },
357371
});
372+
logger.databaseQuery("select", "folders", Date.now() - rootFoldersStart, userId);
358373

359374
const folderCounts: Record<
360375
string,

src/routes/notes/crud.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
noteIdParamSchema,
1414
} from "../../lib/openapi-schemas";
1515
import { invalidateNoteCounts, invalidateNoteCountsForMove } from "../../lib/cache";
16+
import { logger } from "../../lib/logger";
1617

1718
const crudRouter = new OpenAPIHono();
1819

@@ -91,12 +92,15 @@ const listNotesHandler: RouteHandler<typeof listNotesRoute> = async (c) => {
9192

9293
const whereClause = conditions.length > 1 ? and(...conditions) : conditions[0];
9394

95+
const countStart = Date.now();
9496
const [{ total }] = await db.select({ total: count() }).from(notes).where(whereClause);
97+
logger.databaseQuery("count", "notes", Date.now() - countStart, userId);
9598

9699
const page = query.page || 1;
97100
const limit = query.limit || 20;
98101
const offset = (page - 1) * limit;
99102

103+
const selectStart = Date.now();
100104
const userNotes = await db.query.notes.findMany({
101105
where: whereClause,
102106
orderBy: [desc(notes.updatedAt)],
@@ -122,6 +126,7 @@ const listNotesHandler: RouteHandler<typeof listNotesRoute> = async (c) => {
122126
},
123127
},
124128
});
129+
logger.databaseQuery("select", "notes", Date.now() - selectStart, userId);
125130

126131
// Add attachment counts to notes
127132
const notesWithAttachmentCount = userNotes.map((note) => ({
@@ -207,12 +212,14 @@ crudRouter.openapi(getNoteRoute, async (c) => {
207212
const userId = c.get("userId");
208213
const { id: noteId } = c.req.valid("param");
209214

215+
const selectStart = Date.now();
210216
const note = await db.query.notes.findFirst({
211217
where: and(eq(notes.id, noteId), eq(notes.userId, userId)),
212218
with: {
213219
folder: true,
214220
},
215221
});
222+
logger.databaseQuery("select", "notes", Date.now() - selectStart, userId);
216223

217224
if (!note) {
218225
throw new HTTPException(404, { message: "Note not found" });
@@ -279,13 +286,15 @@ const createNoteHandler: RouteHandler<typeof createNoteRoute> = async (c) => {
279286
salt?: string;
280287
};
281288

289+
const insertStart = Date.now();
282290
const [newNote] = await db
283291
.insert(notes)
284292
.values({
285293
...validatedData,
286294
userId,
287295
})
288296
.returning();
297+
logger.databaseQuery("insert", "notes", Date.now() - insertStart, userId);
289298

290299
// Invalidate counts cache for the user and all ancestor folders
291300
await invalidateNoteCounts(userId, validatedData.folderId ?? null);
@@ -388,14 +397,17 @@ crudRouter.openapi(updateNoteRoute, async (c) => {
388397
const data = await c.req.json();
389398
const validatedData = updateNoteSchema.parse(data);
390399

400+
const selectStart = Date.now();
391401
const existingNote = await db.query.notes.findFirst({
392402
where: and(eq(notes.id, noteId), eq(notes.userId, userId)),
393403
});
404+
logger.databaseQuery("select", "notes", Date.now() - selectStart, userId);
394405

395406
if (!existingNote) {
396407
throw new HTTPException(404, { message: "Note not found" });
397408
}
398409

410+
const updateStart = Date.now();
399411
const [updatedNote] = await db
400412
.update(notes)
401413
.set({
@@ -404,6 +416,7 @@ crudRouter.openapi(updateNoteRoute, async (c) => {
404416
})
405417
.where(eq(notes.id, noteId))
406418
.returning();
419+
logger.databaseQuery("update", "notes", Date.now() - updateStart, userId);
407420

408421
// Invalidate counts cache - check if note moved between folders
409422
const oldFolderId = existingNote.folderId;
@@ -454,14 +467,17 @@ crudRouter.openapi(deleteNoteRoute, async (c) => {
454467
const userId = c.get("userId");
455468
const { id: noteId } = c.req.valid("param");
456469

470+
const selectStart = Date.now();
457471
const existingNote = await db.query.notes.findFirst({
458472
where: and(eq(notes.id, noteId), eq(notes.userId, userId)),
459473
});
474+
logger.databaseQuery("select", "notes", Date.now() - selectStart, userId);
460475

461476
if (!existingNote) {
462477
throw new HTTPException(404, { message: "Note not found" });
463478
}
464479

480+
const updateStart = Date.now();
465481
const [deletedNote] = await db
466482
.update(notes)
467483
.set({
@@ -470,6 +486,7 @@ crudRouter.openapi(deleteNoteRoute, async (c) => {
470486
})
471487
.where(eq(notes.id, noteId))
472488
.returning();
489+
logger.databaseQuery("update", "notes", Date.now() - updateStart, userId);
473490

474491
// Invalidate counts cache for the note's folder hierarchy
475492
await invalidateNoteCounts(userId, existingNote.folderId);

src/routes/notes/trash.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { db, notes } from "../../db";
44
import { eq, and, count } from "drizzle-orm";
55
import { emptyTrashResponseSchema } from "../../lib/openapi-schemas";
66
import { deleteCachePattern } from "../../lib/cache";
7+
import { logger } from "../../lib/logger";
78

89
const trashRouter = new OpenAPIHono();
910

@@ -38,12 +39,16 @@ trashRouter.openapi(emptyTrashRoute, async (c) => {
3839
try {
3940
const userId = c.get("userId");
4041

42+
const countStart = Date.now();
4143
const [{ total }] = await db
4244
.select({ total: count() })
4345
.from(notes)
4446
.where(and(eq(notes.userId, userId), eq(notes.deleted, true)));
47+
logger.databaseQuery("count", "notes", Date.now() - countStart, userId);
4548

49+
const deleteStart = Date.now();
4650
await db.delete(notes).where(and(eq(notes.userId, userId), eq(notes.deleted, true)));
51+
logger.databaseQuery("delete", "notes", Date.now() - deleteStart, userId);
4752

4853
// Invalidate all note counts cache for this user (global and all folders)
4954
await deleteCachePattern(`notes:${userId}:*`);

0 commit comments

Comments
 (0)