Skip to content

Commit 1a4bf56

Browse files
committed
feat(db): add query timeout with config override and request context logging
1 parent afc6a08 commit 1a4bf56

6 files changed

Lines changed: 73 additions & 13 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ FRONTEND_URL=http://localhost:5173
55

66
# Docker Postgres defaults
77
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/accesslayer
8+
DB_QUERY_TIMEOUT_MS=5000
89

910
GOOGLE_CLIENT_ID=
1011
GOOGLE_CLIENT_SECRET=

src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ import { requestIdMiddleware } from './middlewares/request-id.middleware';
1313
import { responseTimingMiddleware } from './middlewares/response-timing.middleware';
1414
import { apiVersionMiddleware } from './middlewares/api-version.middleware';
1515
import { requestLoggerMiddleware } from './middlewares/request-logger.middleware';
16+
import { requestContextMiddleware } from './middlewares/request-context.middleware';
1617
import { envConfig } from './config';
1718

1819
const app: Express = express();
1920

2021
// Middleware setup
2122
app.set('trust proxy', 1);
2223
app.use(responseTimingMiddleware);
24+
app.use(requestContextMiddleware);
2325
app.use(apiVersionMiddleware);
2426
app.use(requestIdMiddleware);
2527
app.use(corsMiddleware());

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const envSchema = z.object({
4949
API_VERSION: z.string().default('1.0.0'),
5050
ENABLE_API_VERSION_HEADER: z.coerce.boolean().default(true),
5151
ENABLE_REQUEST_LOGGING: z.coerce.boolean().default(true),
52+
DB_QUERY_TIMEOUT_MS: z.coerce.number().default(5000),
5253
});
5354

5455
export const envConfig = envSchema.parse(process.env);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Request, Response, NextFunction } from 'express';
2+
import { requestContextStorage } from '../utils/als.utils';
3+
4+
export const requestContextMiddleware = (
5+
req: Request,
6+
_res: Response,
7+
next: NextFunction
8+
): void => {
9+
const context = {
10+
path: req.originalUrl || req.url,
11+
method: req.method,
12+
requestId: req.requestId as string | undefined,
13+
};
14+
15+
requestContextStorage.run(context, () => {
16+
next();
17+
});
18+
};

src/utils/als.utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { AsyncLocalStorage } from 'async_hooks';
2+
3+
export interface RequestContext {
4+
path: string;
5+
method: string;
6+
requestId?: string;
7+
}
8+
9+
export const requestContextStorage = new AsyncLocalStorage<RequestContext>();

src/utils/prisma.utils.ts

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,52 @@
11
import { PrismaClient } from '@prisma/client';
22
import { envConfig } from '../config';
3+
import { requestContextStorage } from './als.utils';
4+
import { logger } from './logger.utils';
35

46
// Use global variable to prevent multiple instances in development
5-
// See: https://www.prisma.io/docs/guides/performance-and-optimization/connection-management
6-
77
declare global {
8-
var prisma: PrismaClient | undefined;
8+
var prisma: any | undefined;
99
}
1010

11-
// Export a singleton PrismaClient instance
12-
export const prisma =
13-
global.prisma ||
14-
new PrismaClient({
15-
log:
16-
envConfig.MODE === 'development'
17-
? ['query', 'error', 'warn']
18-
: ['error'],
19-
datasourceUrl: envConfig.DATABASE_URL,
20-
});
11+
const basePrisma = new PrismaClient({
12+
log:
13+
envConfig.MODE === 'development'
14+
? ['query', 'error', 'warn']
15+
: ['error'],
16+
datasourceUrl: envConfig.DATABASE_URL,
17+
});
18+
19+
// Extend Prisma with query timeout
20+
export const prisma = basePrisma.$extends({
21+
query: {
22+
$allOperations({ operation, model, args, query }) {
23+
const timeoutMs = envConfig.DB_QUERY_TIMEOUT_MS;
24+
const context = requestContextStorage.getStore();
25+
26+
let timeoutId: NodeJS.Timeout;
27+
const timeoutPromise = new Promise((_, reject) => {
28+
timeoutId = setTimeout(() => {
29+
const logContext = {
30+
type: 'database_timeout',
31+
operation,
32+
model,
33+
timeoutMs,
34+
path: context?.path,
35+
method: context?.method,
36+
requestId: context?.requestId,
37+
};
38+
logger.error(logContext, `Database query timed out after ${timeoutMs}ms`);
39+
reject(new Error(`Database query timed out after ${timeoutMs}ms`));
40+
}, timeoutMs);
41+
});
42+
43+
return Promise.race([
44+
query(args).finally(() => clearTimeout(timeoutId)),
45+
timeoutPromise,
46+
]);
47+
},
48+
},
49+
});
2150

2251
// Prevent multiple instances in development environment
2352
if (envConfig.MODE !== 'production') {

0 commit comments

Comments
 (0)