Skip to content

Commit e4399e8

Browse files
Merge pull request #559 from 42core-team/548-add-logging-to-api
feat: Implement structured logging with NestJS Pino and a global exce…
2 parents ea57855 + 73eb34c commit e4399e8

17 files changed

+1237
-719
lines changed

api/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,14 @@
4747
"cookie-parser": "^1.4.7",
4848
"crypto-js": "^4.2.0",
4949
"jiti": "^2.6.1",
50+
"nestjs-pino": "^4.6.0",
5051
"passport": "^0.7.0",
5152
"passport-github2": "^0.1.12",
5253
"passport-jwt": "^4.0.1",
5354
"passport-oauth2": "^1.8.0",
5455
"pg": "^8.20.0",
56+
"pino": "^10.3.1",
57+
"pino-http": "^11.0.0",
5558
"reflect-metadata": "^0.2.2",
5659
"rxjs": "^7.8.2",
5760
"tournament-pairings": "^2.0.1",
@@ -68,15 +71,16 @@
6871
"@types/cookie-parser": "^1.4.10",
6972
"@types/express": "^5.0.6",
7073
"@types/jest": "^30.0.0",
71-
"@types/node": "^25.4.0",
74+
"@types/node": "^25.5.0",
7275
"@types/passport-local": "^1.0.38",
7376
"@types/supertest": "^7.2.0",
7477
"dotenv": "^17.3.1",
7578
"eslint": "^10.0.3",
7679
"eslint-config-prettier": "^10.1.8",
7780
"eslint-plugin-prettier": "^5.5.5",
7881
"globals": "^17.4.0",
79-
"jest": "^30.2.0",
82+
"jest": "^30.3.0",
83+
"pino-pretty": "^13.1.3",
8084
"prettier": "^3.8.1",
8185
"source-map-support": "^0.5.21",
8286
"supertest": "^7.2.2",

api/pnpm-lock.yaml

Lines changed: 938 additions & 684 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/src/app.module.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { DatabaseConfig } from "./DatabaseConfig";
1212
import { GithubApiModule } from "./github-api/github-api.module";
1313
import { ScheduleModule } from "@nestjs/schedule";
1414
import { StatsModule } from "./stats/stats.module";
15+
import { LoggerModule } from "nestjs-pino";
16+
import { Request } from "express";
1517

1618
@Module({
1719
imports: [
@@ -20,6 +22,44 @@ import { StatsModule } from "./stats/stats.module";
2022
isGlobal: true,
2123
envFilePath: ".env",
2224
}),
25+
LoggerModule.forRoot({
26+
pinoHttp: {
27+
autoLogging: true,
28+
transport:
29+
process.env.NODE_ENV !== "production"
30+
? {
31+
target: "pino-pretty",
32+
options: {
33+
singleLine: true,
34+
colorize: true,
35+
ignore: "pid,hostname",
36+
messageFormat: "[{context}] {msg}",
37+
},
38+
}
39+
: undefined,
40+
customProps: (req: Request) => {
41+
const customReq = req as Request & { user?: { id: string } };
42+
const user = customReq.user;
43+
return {
44+
userId: user?.id,
45+
};
46+
},
47+
serializers: {
48+
req: (req) => {
49+
return {
50+
id: req.id,
51+
method: req.method,
52+
url: req.url,
53+
};
54+
},
55+
res: (res) => {
56+
return {
57+
statusCode: res.statusCode,
58+
};
59+
},
60+
},
61+
},
62+
}),
2363
TypeOrmModule.forRootAsync({
2464
imports: [ConfigModule],
2565
useFactory: (config: ConfigService) => {

api/src/auth/auth.controller.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Req,
77
Res,
88
UseGuards,
9+
Logger,
910
} from "@nestjs/common";
1011
import { UserEntity } from "../user/entities/user.entity";
1112
import { AuthGuard } from "@nestjs/passport";
@@ -21,6 +22,8 @@ import { SocialPlatform } from "../user/entities/social-account.entity";
2122

2223
@Controller("auth")
2324
export class AuthController {
25+
private readonly logger = new Logger(AuthController.name);
26+
2427
constructor(
2528
private readonly auth: AuthService,
2629
private readonly configService: ConfigService,
@@ -32,6 +35,8 @@ export class AuthController {
3235
@UseGuards(AuthGuard("github"))
3336
githubCallback(@Req() req: Request, @Res() res: Response) {
3437
const user = req.user as UserEntity;
38+
this.logger.log({ action: "github_login", userId: user.id });
39+
3540
const token = this.auth.signToken(user);
3641
const redirectUrl = this.configService.getOrThrow<string>(
3742
"OAUTH_SUCCESS_REDIRECT_URL",
@@ -105,19 +110,20 @@ export class AuthController {
105110
username: request.user.fortyTwoAccount.username,
106111
});
107112

113+
this.logger.log({ action: "fortytwo_link", userId });
114+
108115
const redirectUrl = this.configService.getOrThrow<string>(
109116
"OAUTH_42_SUCCESS_REDIRECT_URL",
110117
);
111118

112119
return res.redirect(redirectUrl);
113120
} catch (e) {
114-
// Use a more detailed log, and preserve specific error messages for BadRequestException
115-
console.error("Error in FortyTwo callback:", e);
116121
if (e instanceof BadRequestException) {
117122
throw e;
118123
}
124+
119125
throw new BadRequestException(
120-
e && typeof e.message === "string"
126+
e instanceof Error
121127
? `Invalid state parameter: ${e.message}`
122128
: "Invalid state parameter.",
123129
);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {
2+
ExceptionFilter,
3+
Catch,
4+
ArgumentsHost,
5+
HttpException,
6+
HttpStatus,
7+
Logger,
8+
} from "@nestjs/common";
9+
import { HttpAdapterHost } from "@nestjs/core";
10+
11+
@Catch()
12+
export class AllExceptionsFilter implements ExceptionFilter {
13+
private readonly logger = new Logger(AllExceptionsFilter.name);
14+
15+
constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
16+
17+
catch(exception: unknown, host: ArgumentsHost): void {
18+
if (host.getType() !== "http") {
19+
return;
20+
}
21+
22+
const { httpAdapter } = this.httpAdapterHost;
23+
const ctx = host.switchToHttp();
24+
25+
const httpStatus =
26+
exception instanceof HttpException
27+
? exception.getStatus()
28+
: HttpStatus.INTERNAL_SERVER_ERROR;
29+
30+
const responseBody = {
31+
statusCode: httpStatus,
32+
timestamp: new Date().toISOString(),
33+
path: httpAdapter.getRequestUrl(ctx.getRequest()),
34+
message:
35+
exception instanceof HttpException
36+
? exception.message
37+
: "Internal server error",
38+
};
39+
40+
if (httpStatus >= 500) {
41+
if (exception instanceof Error) {
42+
this.logger.error(exception.message, exception.stack);
43+
} else {
44+
this.logger.error(`Unhandled Exception: ${exception}`);
45+
}
46+
} else {
47+
// 4xx errors
48+
if (exception instanceof Error) {
49+
this.logger.warn(`HTTP Exception: ${exception.message}`);
50+
} else {
51+
this.logger.warn(`HTTP Exception: ${exception}`);
52+
}
53+
}
54+
55+
httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
56+
}
57+
}

api/src/common/TypeormExceptionFilter.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,28 @@ import {
44
Catch,
55
ArgumentsHost,
66
HttpStatus,
7+
Logger,
78
} from "@nestjs/common";
8-
import { Response } from "express";
9+
import { Response, Request } from "express";
910

1011
@Catch(EntityNotFoundError)
1112
export class TypeormExceptionFilter implements ExceptionFilter {
1213
catch(exception: EntityNotFoundError, host: ArgumentsHost) {
1314
const ctx = host.switchToHttp();
1415
const response = ctx.getResponse<Response>();
16+
const request = ctx.getRequest<Request>();
17+
18+
// Log the error
19+
Logger.error(
20+
`Entity not found for request ${request.method} ${request.url}: ${exception.message}`,
21+
exception.stack,
22+
"TypeormExceptionFilter",
23+
);
1524

1625
response.status(HttpStatus.NOT_FOUND).json({
1726
statusCode: HttpStatus.NOT_FOUND,
1827
timestamp: new Date().toISOString(),
1928
message: "Entity not found",
20-
error: exception.message,
2129
});
2230
}
2331
}

api/src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ export const enum LockKeys {
22
AUTO_LOCK_EVENTS = 12345,
33
CREATE_TEAM_REPOS = 12346,
44
PROCESS_QUEUE_MATCHES = 12347,
5-
}
5+
}

api/src/event/dtos/createEventDto.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,4 @@ export class CreateEventDto {
8888
@ApiProperty()
8989
@IsBoolean()
9090
isPrivate: boolean;
91-
9291
}

api/src/event/dtos/updateEventSettingsDto.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,3 @@ export class UpdateEventSettingsDto {
102102
@IsString()
103103
serverConfig?: string;
104104
}
105-

api/src/event/event.controller.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Put,
1111
UnauthorizedException,
1212
UseGuards,
13+
Logger,
1314
} from "@nestjs/common";
1415
import { EventService } from "./event.service";
1516
import { TeamService } from "../team/team.service";
@@ -24,6 +25,8 @@ import { UserId } from "../guards/UserGuard";
2425

2526
@Controller("event")
2627
export class EventController {
28+
private readonly logger = new Logger(EventController.name);
29+
2730
constructor(
2831
private readonly eventService: EventService,
2932
private readonly teamService: TeamService,
@@ -91,6 +94,12 @@ export class EventController {
9194
"You are not authorized to create events.",
9295
);
9396

97+
this.logger.log({
98+
action: "attempt_create_event",
99+
userId,
100+
eventName: createEventDto.name,
101+
});
102+
94103
return this.eventService.createEvent(
95104
userId,
96105
createEventDto.name,
@@ -165,6 +174,8 @@ export class EventController {
165174
throw new BadRequestException("Event has not started yet.");
166175
}
167176

177+
this.logger.log({ action: "attempt_join_event", userId, eventId });
178+
168179
return this.userService.joinEvent(userId, eventId);
169180
}
170181

@@ -179,6 +190,8 @@ export class EventController {
179190
"You are not authorized to lock this event.",
180191
);
181192

193+
this.logger.log({ action: "attempt_lock_event", userId, eventId });
194+
182195
return this.eventService.lockEvent(eventId);
183196
}
184197

@@ -193,6 +206,8 @@ export class EventController {
193206
"You are not authorized to unlock teams for this event.",
194207
);
195208

209+
this.logger.log({ action: "attempt_unlock_event", userId, eventId });
210+
196211
return this.eventService.unlockEvent(eventId);
197212
}
198213

@@ -208,6 +223,13 @@ export class EventController {
208223
"You are not authorized to lock teams for this event.",
209224
);
210225

226+
this.logger.log({
227+
action: "attempt_set_lock_teams_date",
228+
userId,
229+
eventId,
230+
repoLockDate: body.repoLockDate,
231+
});
232+
211233
if (!body.repoLockDate)
212234
return this.eventService.setTeamsLockedDate(eventId, null);
213235
return this.eventService.setTeamsLockedDate(
@@ -228,6 +250,12 @@ export class EventController {
228250
"You are not authorized to update settings for this event.",
229251
);
230252

253+
this.logger.log({
254+
action: "attempt_update_event_settings",
255+
userId,
256+
eventId,
257+
});
258+
231259
return this.eventService.updateEventSettings(eventId, body);
232260
}
233261

@@ -253,6 +281,13 @@ export class EventController {
253281
if (!(await this.eventService.isEventAdmin(eventId, userId))) {
254282
throw new UnauthorizedException("You are not an admin of this event");
255283
}
284+
285+
this.logger.log({
286+
action: "attempt_add_event_admin",
287+
userId,
288+
eventId,
289+
newAdminId,
290+
});
256291
return this.eventService.addEventAdmin(eventId, newAdminId);
257292
}
258293

@@ -266,6 +301,13 @@ export class EventController {
266301
if (!(await this.eventService.isEventAdmin(eventId, userId))) {
267302
throw new UnauthorizedException("You are not an admin of this event");
268303
}
304+
305+
this.logger.log({
306+
action: "attempt_remove_event_admin",
307+
userId,
308+
eventId,
309+
removedAdminId: adminIdToRemove,
310+
});
269311
return this.eventService.removeEventAdmin(eventId, adminIdToRemove);
270312
}
271313

@@ -285,6 +327,13 @@ export class EventController {
285327
if (!(await this.eventService.isEventAdmin(eventId, userId))) {
286328
throw new UnauthorizedException("You are not an admin of this event");
287329
}
330+
331+
this.logger.log({
332+
action: "attempt_create_starter_template",
333+
userId,
334+
eventId,
335+
templateName: body.name,
336+
});
288337
return this.eventService.createStarterTemplate(
289338
eventId,
290339
body.name,
@@ -304,6 +353,13 @@ export class EventController {
304353
if (!(await this.eventService.isEventAdmin(eventId, userId))) {
305354
throw new UnauthorizedException("You are not an admin of this event");
306355
}
356+
357+
this.logger.log({
358+
action: "attempt_update_starter_template",
359+
userId,
360+
eventId,
361+
templateId,
362+
});
307363
return this.eventService.updateStarterTemplate(eventId, templateId, body);
308364
}
309365

@@ -317,6 +373,13 @@ export class EventController {
317373
if (!(await this.eventService.isEventAdmin(eventId, userId))) {
318374
throw new UnauthorizedException("You are not an admin of this event");
319375
}
376+
377+
this.logger.log({
378+
action: "attempt_delete_starter_template",
379+
userId,
380+
eventId,
381+
templateId,
382+
});
320383
return this.eventService.deleteStarterTemplate(eventId, templateId);
321384
}
322385
}

0 commit comments

Comments
 (0)