Skip to content

Commit 7aa34e4

Browse files
authored
Merge pull request #299 from codex-team/chore/logs-and-optimization
Update logs
2 parents b19cb3a + 3d71b5c commit 7aa34e4

15 files changed

Lines changed: 143 additions & 25 deletions

File tree

app-config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ logging:
2828
appServer: info
2929
database: info
3030
s3Storage: info
31+
middlewares: info
32+
policies: info
3133

3234
s3:
3335
accessKeyId: 'secret'

eslint.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ export default [
7777
'n/no-unsupported-features/es-builtins': ['error', {
7878
version: '>=22.1.0',
7979
}],
80+
'n/no-unsupported-features/node-builtins': ['error', {
81+
version: '>=22.1.0',
82+
}],
8083
'@typescript-eslint/naming-convention': ['error', {
8184
selector: 'property',
8285
format: ['camelCase', 'PascalCase'],

src/infrastructure/config/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ export const LoggingConfig = z.object({
9191
appServer: LoggingLevel,
9292
database: LoggingLevel,
9393
s3Storage: LoggingLevel,
94+
middlewares: LoggingLevel,
95+
policies: LoggingLevel,
9496
});
9597

9698
export type LoggingConfig = z.infer<typeof LoggingConfig>;
@@ -161,6 +163,8 @@ const defaultConfig: AppConfig = {
161163
appServer: 'info',
162164
database: 'info',
163165
s3Storage: 'info',
166+
middlewares: 'info',
167+
policies: 'info',
164168
},
165169
database: {
166170
dsn: 'postgres://user:pass@postgres/codex-notes',

src/infrastructure/logging/index.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { pino } from 'pino';
22
import * as process from 'process';
33
import type { LoggingConfig } from '../config/index.js';
44
import appConfig from '../config/index.js';
5+
import { getCurrentReqId } from './reqId.context.js';
56

67
const loggerConfig = process.env['NODE_ENV'] === 'production'
78
? {}
@@ -16,11 +17,19 @@ const loggerConfig = process.env['NODE_ENV'] === 'production'
1617

1718
const rootLogger = pino(loggerConfig);
1819

20+
const loggerCache = new Map<keyof LoggingConfig, pino.Logger>();
21+
1922
/**
2023
* Creates child logger and returns it.
2124
* @param moduleName - name of the module that is logging
2225
*/
2326
export function getLogger(moduleName: keyof LoggingConfig): pino.Logger {
27+
const cachedLogger = loggerCache.get(moduleName);
28+
29+
if (cachedLogger) {
30+
return cachedLogger;
31+
}
32+
2433
const childLogger = rootLogger.child({
2534
module: moduleName,
2635
});
@@ -33,9 +42,29 @@ export function getLogger(moduleName: keyof LoggingConfig): pino.Logger {
3342

3443
childLogger.level = logLevel;
3544

45+
loggerCache.set(moduleName, childLogger);
46+
3647
return childLogger;
3748
}
3849

50+
/**
51+
* Creates a request-scoped logger that includes the request ID
52+
* @param moduleName - name of the module that is logging
53+
* @returns Logger instance with request ID context
54+
*/
55+
export function getRequestLogger(moduleName: keyof LoggingConfig): pino.Logger {
56+
const baseLogger = getLogger(moduleName);
57+
const reqId = getCurrentReqId();
58+
59+
if (reqId != null && reqId !== '') {
60+
return baseLogger.child({
61+
reqId,
62+
});
63+
}
64+
65+
return baseLogger;
66+
}
67+
3968
const logger = getLogger('global');
4069

4170
export default logger;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { AsyncLocalStorage } from 'node:async_hooks';
2+
3+
/**
4+
* Context for storing reqId within asynchronous operations
5+
*/
6+
interface RequestContext {
7+
reqId?: string;
8+
}
9+
10+
/**
11+
* AsyncLocalStorage for storing request context
12+
* Allows tying database logs to the corresponding request
13+
*/
14+
export const requestContextStorage = new AsyncLocalStorage<RequestContext>();
15+
16+
/**
17+
* Gets the current reqId from the execution context
18+
* @returns reqId or undefined if context is not set
19+
*/
20+
export function getCurrentReqId(): string | undefined {
21+
const context = requestContextStorage.getStore();
22+
23+
return context?.reqId;
24+
}

src/presentation/http/http-api.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { fastifyOauth2 } from '@fastify/oauth2';
99
import fastifySwagger from '@fastify/swagger';
1010
import fastifySwaggerUI from '@fastify/swagger-ui';
1111
import addUserIdResolver from '@presentation/http/middlewares/common/userIdResolver.js';
12+
import addReqIdContext from '@presentation/http/middlewares/common/reqIdContext.js';
1213
import cookie from '@fastify/cookie';
1314
import { notFound, forbidden, unauthorized, notAcceptable, domainError } from './decorators/index.js';
1415
import NoteRouter from '@presentation/http/router/note.js';
@@ -324,7 +325,8 @@ export default class HttpApi implements Api {
324325
throw new Error('Server is not initialized');
325326
}
326327

327-
addUserIdResolver(this.server, domainServices.authService, appServerLogger);
328+
addReqIdContext(this.server);
329+
addUserIdResolver(this.server, domainServices.authService);
328330
}
329331

330332
/**
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { FastifyInstance } from 'fastify';
2+
import { requestContextStorage } from '@infrastructure/logging/reqId.context.js';
3+
4+
/**
5+
* Adds middleware to set reqId context at the beginning of each request
6+
* @param server - fastify instance
7+
*/
8+
export default function addReqIdContext(server: FastifyInstance): void {
9+
/**
10+
* Sets reqId context for all asynchronous operations within the request
11+
* Uses 'onRequest' hook so context is available in all subsequent hooks and handlers
12+
*/
13+
server.addHook('onRequest', (request, _reply, done) => {
14+
requestContextStorage.run({ reqId: request.id }, () => {
15+
done();
16+
});
17+
});
18+
}

src/presentation/http/middlewares/common/userIdResolver.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import type { FastifyInstance } from 'fastify';
22
import type AuthService from '@domain/service/auth.js';
3-
import type Logger from '@infrastructure/logging/index.js';
43
import { notEmpty } from '@infrastructure/utils/empty.js';
4+
import { getRequestLogger } from '@infrastructure/logging/index.js';
55

66
/**
77
* Add middleware for resolve userId from Access Token and add it to request
88
* @param server - fastify instance
99
* @param authService - auth domain service
10-
* @param logger - logger instance
1110
*/
12-
export default function addUserIdResolver(server: FastifyInstance, authService: AuthService, logger: typeof Logger): void {
11+
export default function addUserIdResolver(server: FastifyInstance, authService: AuthService): void {
1312
/**
1413
* Default userId value — null
1514
*/
@@ -19,13 +18,15 @@ export default function addUserIdResolver(server: FastifyInstance, authService:
1918
* Resolve userId from Access Token on each request
2019
*/
2120
server.addHook('preHandler', (request, _reply, done) => {
21+
const logger = getRequestLogger('middlewares').child({ middleware: 'userIdResolver' });
2222
const authorizationHeader = request.headers.authorization;
2323

2424
if (notEmpty(authorizationHeader)) {
2525
const token = authorizationHeader.replace('Bearer ', '');
2626

2727
try {
2828
request.userId = authService.verifyAccessToken(token)['id'];
29+
logger.debug('User ID resolved from Access Token');
2930
} catch (error) {
3031
logger.error('Invalid Access Token');
3132
logger.error(error);

src/presentation/http/middlewares/note/useNoteResolver.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type NoteService from '@domain/service/note.js';
33
import { notEmpty } from '@infrastructure/utils/empty.js';
44
import { StatusCodes } from 'http-status-codes';
55
import hasProperty from '@infrastructure/utils/hasProperty.js';
6-
import { getLogger } from '@infrastructure/logging/index.js';
6+
import { getRequestLogger } from '@infrastructure/logging/index.js';
77
import type { Note, NotePublicId } from '@domain/entities/note.js';
88

99
/**
@@ -18,11 +18,6 @@ export default function useNoteResolver(noteService: NoteService): {
1818
*/
1919
noteResolver: preHandlerHookHandler;
2020
} {
21-
/**
22-
* Get logger instance
23-
*/
24-
const logger = getLogger('appServer');
25-
2621
/**
2722
* Search for Note by public id in passed payload and resolves a note by it
2823
* @param requestData - fastify request data. Can be query, params or body
@@ -40,6 +35,7 @@ export default function useNoteResolver(noteService: NoteService): {
4035

4136
return {
4237
noteResolver: async function noteIdResolver(request, reply) {
38+
const logger = getRequestLogger('middlewares').child({ middleware: 'noteResolver' });
4339
let note: Note | undefined;
4440

4541
let statusCode = StatusCodes.NOT_ACCEPTABLE;
@@ -59,6 +55,7 @@ export default function useNoteResolver(noteService: NoteService): {
5955

6056
if (note) {
6157
request.note = note;
58+
logger.debug('Note resolved by public ID');
6259
} else {
6360
throw new Error('Note not found');
6461
}

src/presentation/http/middlewares/noteSettings/useMemberRoleResolver.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { preHandlerHookHandler } from 'fastify';
2-
import { getLogger } from '@infrastructure/logging/index.js';
2+
import { getRequestLogger } from '@infrastructure/logging/index.js';
33
import type NoteSettingsService from '@domain/service/noteSettings.js';
44
import type { MemberRole } from '@domain/entities/team.js';
55
import { isEmpty } from '@infrastructure/utils/empty.js';
@@ -16,13 +16,9 @@ export default function useMemberRoleResolver(noteSettingsService: NoteSettingsS
1616
*/
1717
memberRoleResolver: preHandlerHookHandler;
1818
} {
19-
/**
20-
* Get logger instance
21-
*/
22-
const logger = getLogger('appServer');
23-
2419
return {
2520
memberRoleResolver: async function memberRoleResolver(request, reply) {
21+
const logger = getRequestLogger('middlewares').child({ middleware: 'memberRoleResolver' });
2622
/** If MemberRole equals null, it means that user is not in the team or is not authenticated */
2723
let memberRole: MemberRole | undefined;
2824

@@ -40,9 +36,14 @@ export default function useMemberRoleResolver(noteSettingsService: NoteSettingsS
4036

4137
if (memberRole !== undefined) {
4238
request.memberRole = memberRole;
39+
logger.debug('Member role resolved');
4340
}
4441
} catch (error) {
45-
logger.error('Can not resolve Member role by note [id = ${request.note.id}] and user [id = ${request.userId}]');
42+
if (request.note != null && request.userId != null) {
43+
logger.error(`Cannot resolve Member role by note [id = ${request.note.id}] and user [id = ${request.userId}]`);
44+
} else {
45+
logger.error('Cannot resolve Member role - note or user ID not available');
46+
}
4647
logger.error(error);
4748

4849
await reply.notAcceptable('Team member not found');

0 commit comments

Comments
 (0)