Skip to content

Commit 4e2d58c

Browse files
Merge pull request #2219 from AletheiaFact/refactor/improve-logging-infrastructure
Improve server logging infrastructure
2 parents 6c99ad3 + cd86bd0 commit 4e2d58c

15 files changed

+355
-120
lines changed

server/chat-bot/chat-bot.service.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Injectable, Scope } from "@nestjs/common";
1+
import { Injectable, Scope, Logger } from "@nestjs/common";
22
import { HttpService } from "@nestjs/axios";
33
import { AxiosResponse } from "axios";
44
import { catchError, map } from "rxjs/operators";
@@ -25,20 +25,22 @@ interface ChatBotContext {
2525
}
2626

2727
function M2MUser(clientId): M2M {
28-
return {
29-
isM2M: true,
30-
clientId,
31-
subject: "chatbot-service",
32-
scopes: ["read", "write"],
33-
role: {
34-
main: Roles.Integration,
35-
},
36-
namespace: "main",
37-
};
28+
return {
29+
isM2M: true,
30+
clientId,
31+
subject: "chatbot-service",
32+
scopes: ["read", "write"],
33+
role: {
34+
main: Roles.Integration,
35+
},
36+
namespace: "main",
37+
};
3838
}
3939

4040
@Injectable({ scope: Scope.REQUEST })
4141
export class ChatbotService {
42+
private readonly logger = new Logger(ChatbotService.name);
43+
4244
constructor(
4345
private configService: ConfigService,
4446
private readonly httpService: HttpService,
@@ -114,7 +116,7 @@ export class ChatbotService {
114116
...chatbotState.machine.context,
115117
sourceChannel: channel,
116118
},
117-
M2MUser(chatbotState._id)
119+
M2MUser(chatbotState._id)
118120
);
119121

120122
chatBotMachineService.start(chatbotState.machine.value);
@@ -221,7 +223,7 @@ export class ChatbotService {
221223
);
222224
break;
223225
default:
224-
console.warn(`Unhandled state: ${currentState}`);
226+
this.logger.warn(`Unhandled state: ${currentState}`);
225227
}
226228
}
227229

server/claim/claim.service.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { Injectable, Inject, Scope, NotFoundException } from "@nestjs/common";
1+
import {
2+
Injectable,
3+
Inject,
4+
Scope,
5+
NotFoundException,
6+
Logger,
7+
} from "@nestjs/common";
28
import { InjectModel } from "@nestjs/mongoose";
39
import { FilterQuery, Model, Types } from "mongoose";
410
import { Claim, ClaimDocument } from "../claim/schemas/claim.schema";
@@ -30,6 +36,8 @@ type ClaimMatchParameters = (
3036

3137
@Injectable({ scope: Scope.REQUEST })
3238
export class ClaimService {
39+
private readonly logger = new Logger(ClaimService.name);
40+
3341
constructor(
3442
@Inject(REQUEST) private req: BaseRequest,
3543
@InjectModel(Claim.name)
@@ -263,7 +271,7 @@ export class ClaimService {
263271
newClaim
264272
);
265273
} catch (e) {
266-
console.error(e);
274+
this.logger.error("Failed to update claim:", e);
267275
throw new NotFoundException();
268276
}
269277
}

server/copilot/copilot-chat.service.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,6 @@ export class CopilotChatService {
221221
}
222222

223223
private exceptionHandling = (e: unknown) => {
224-
console.log(e);
225224
this.logger.error(e);
226225
throw new HttpException(
227226
customMessage(
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {
2+
ExceptionFilter,
3+
Catch,
4+
ArgumentsHost,
5+
HttpException,
6+
HttpStatus,
7+
Logger,
8+
} from "@nestjs/common";
9+
import { Request, Response } from "express";
10+
import { randomUUID } from "crypto";
11+
12+
@Catch()
13+
export class AllExceptionsFilter implements ExceptionFilter {
14+
private readonly logger = new Logger(AllExceptionsFilter.name);
15+
16+
catch(exception: unknown, host: ArgumentsHost) {
17+
const ctx = host.switchToHttp();
18+
const response = ctx.getResponse<Response>();
19+
const request = ctx.getRequest<Request>();
20+
21+
const status =
22+
exception instanceof HttpException
23+
? exception.getStatus()
24+
: HttpStatus.INTERNAL_SERVER_ERROR;
25+
26+
const message =
27+
exception instanceof HttpException
28+
? exception.getResponse()
29+
: exception instanceof Error
30+
? exception.message
31+
: "Internal server error";
32+
33+
const requestId =
34+
(request as any).requestId ||
35+
request.headers["x-request-id"] ||
36+
randomUUID();
37+
38+
const errorContext = {
39+
requestId,
40+
method: request.method,
41+
url: request.url,
42+
ip: request.ip || request.socket?.remoteAddress,
43+
statusCode: status,
44+
};
45+
46+
// Handle "headers already sent" error
47+
if (
48+
exception instanceof Error &&
49+
exception.message.includes("Cannot set headers after they are sent")
50+
) {
51+
this.logger.error(
52+
`Headers already sent - ${request.method} ${request.url} | RequestId: ${requestId}`,
53+
exception.stack
54+
);
55+
return;
56+
}
57+
58+
// Log the error with context
59+
const errorMessage =
60+
typeof message === "string" ? message : JSON.stringify(message);
61+
this.logger.error(
62+
`${request.method} ${request.url} | Status: ${status} | RequestId: ${requestId} | Error: ${errorMessage}`,
63+
exception instanceof Error ? exception.stack : ""
64+
);
65+
66+
// Send error response only if headers haven't been sent
67+
if (!response.headersSent) {
68+
response.status(status).json({
69+
requestId,
70+
statusCode: status,
71+
timestamp: new Date().toISOString(),
72+
path: request.url,
73+
message:
74+
status === HttpStatus.INTERNAL_SERVER_ERROR
75+
? "Internal server error"
76+
: message,
77+
});
78+
}
79+
}
80+
}

server/group/group.service.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { Injectable } from "@nestjs/common";
1+
import { Injectable, Logger } from "@nestjs/common";
22
import { Model, Types } from "mongoose";
33
import { InjectModel } from "@nestjs/mongoose";
44
import { Group, GroupDocument } from "./schemas/group.schema";
55

66
@Injectable()
77
export class GroupService {
8+
private readonly logger = new Logger(GroupService.name);
9+
810
constructor(
911
@InjectModel(Group.name)
1012
private GroupModel: Model<GroupDocument>
@@ -51,7 +53,7 @@ export class GroupService {
5153

5254
return await new this.GroupModel(group).save();
5355
} catch (error) {
54-
console.error("Failed to create or update group:", error);
56+
this.logger.error("Failed to create or update group:", error);
5557
throw error;
5658
}
5759
}
@@ -107,7 +109,7 @@ export class GroupService {
107109
);
108110
}
109111
} catch (error) {
110-
console.error("Failed to remove content:", error);
112+
this.logger.error("Failed to remove content:", error);
111113
throw error;
112114
}
113115
}

server/main.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
77
import loadConfig from "./configLoader";
88
import * as dotenv from "dotenv";
99
import { WinstonLogger } from "./winstonLogger";
10+
import { AllExceptionsFilter } from "./filters/http-exception.filter";
1011
const cookieParser = require("cookie-parser");
1112
const mongoose = require("mongoose");
1213
dotenv.config();
@@ -23,11 +24,40 @@ async function initApp() {
2324
origin: options?.cors || "*",
2425
credentials: true,
2526
methods: "GET,HEAD,PUT,PATCH,POST,DELETE, OPTIONS",
26-
allowedHeaders: ["accept", "x-requested-with", "content-type"],
27+
allowedHeaders: [
28+
"accept",
29+
"x-requested-with",
30+
"content-type",
31+
"x-request-id",
32+
],
33+
exposedHeaders: ["x-request-id"],
2734
};
2835

2936
const logger = new WinstonLogger();
3037

38+
// Handle uncaught exceptions
39+
process.on("uncaughtException", (error: Error) => {
40+
logger.error(
41+
`Uncaught Exception: ${error.message}`,
42+
error.stack,
43+
"UncaughtException"
44+
);
45+
setTimeout(() => process.exit(1), 1000);
46+
});
47+
48+
// Handle unhandled promise rejections
49+
process.on("unhandledRejection", (reason: any) => {
50+
const errorMessage =
51+
reason instanceof Error
52+
? `${reason.message}\n${reason.stack}`
53+
: String(reason);
54+
logger.error(
55+
`Unhandled Rejection: ${errorMessage}`,
56+
"",
57+
"UnhandledRejection"
58+
);
59+
});
60+
3161
const app = await NestFactory.create<NestExpressApplication>(
3262
AppModule.register(options),
3363
{
@@ -58,6 +88,9 @@ async function initApp() {
5888
})
5989
);
6090

91+
// Global exception filter for consistent error handling and logging
92+
app.useGlobalFilters(new AllExceptionsFilter());
93+
6194
// FIXME: not working but we need to enable in the future
6295
// app.use(helmet());
6396
app.use(cookieParser());
Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,45 @@
1-
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
2-
import { Request, Response, NextFunction } from 'express';
1+
import { Injectable, Logger, NestMiddleware } from "@nestjs/common";
2+
import { Request, Response, NextFunction } from "express";
3+
import { randomUUID } from "crypto";
4+
5+
declare global {
6+
namespace Express {
7+
interface Request {
8+
requestId?: string;
9+
startTime?: number;
10+
}
11+
}
12+
}
313

414
@Injectable()
515
export class LoggerMiddleware implements NestMiddleware {
6-
private logger = new Logger('HTTP');
16+
private logger = new Logger("HTTP");
17+
18+
use(request: Request, response: Response, next: NextFunction): void {
19+
const startTime = Date.now();
20+
const { ip, method, originalUrl } = request;
21+
const userAgent = request.get("user-agent") || "";
722

8-
use(request: Request, response: Response, next: NextFunction): void {
9-
const { ip, method, originalUrl } = request;
10-
const userAgent = request.get('user-agent') || '';
23+
// Generate or use existing request ID
24+
const requestId =
25+
(request.headers["x-request-id"] as string) || randomUUID();
1126

12-
response.on('finish', () => {
13-
const { statusCode } = response;
14-
const contentLength = response.get('content-length');
27+
// Attach to request for use in exception filters and services
28+
request.requestId = requestId;
29+
request.startTime = startTime;
30+
request.headers["x-request-id"] = requestId;
31+
response.setHeader("x-request-id", requestId);
1532

16-
this.logger.log(
17-
`${method} ${originalUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`,
18-
);
19-
});
33+
response.on("finish", () => {
34+
const { statusCode } = response;
35+
const contentLength = response.get("content-length") || 0;
36+
const responseTime = Date.now() - startTime;
2037

21-
next();
22-
}
23-
}
38+
this.logger.log(
39+
`${method} ${originalUrl} ${statusCode} ${contentLength} ${responseTime}ms - ${userAgent} ${ip}`
40+
);
41+
});
2442

43+
next();
44+
}
45+
}

server/notifications/notifications.service.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { Injectable } from "@nestjs/common";
1+
import { Injectable, Logger } from "@nestjs/common";
22
import { Novu, TriggerRecipientsTypeEnum } from "@novu/node";
33
import { InjectNovu } from "./novu.provider";
44
import { createHmac } from "crypto";
55
import { ConfigService } from "@nestjs/config";
66
@Injectable()
77
export class NotificationService {
8+
private readonly logger = new Logger(NotificationService.name);
9+
810
constructor(
911
@InjectNovu()
1012
private readonly novu: Novu,
@@ -90,7 +92,7 @@ export class NotificationService {
9092
const result = await this.novu.topics.get(key);
9193
return result.data;
9294
} catch (e) {
93-
console.log(e);
95+
this.logger.error("Failed to get topic:", e);
9496
return false;
9597
}
9698
}

server/review-task/review-task.service.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { ForbiddenException, Inject, Injectable, Scope } from "@nestjs/common";
1+
import {
2+
ForbiddenException,
3+
Inject,
4+
Injectable,
5+
Scope,
6+
Logger,
7+
} from "@nestjs/common";
28
import { Model, Types } from "mongoose";
39
import { ReviewTask, ReviewTaskDocument } from "./schemas/review-task.schema";
410
import { InjectModel } from "@nestjs/mongoose";
@@ -59,6 +65,7 @@ export interface IReviewTask {
5965

6066
@Injectable({ scope: Scope.REQUEST })
6167
export class ReviewTaskService {
68+
private readonly logger = new Logger(ReviewTaskService.name);
6269
fieldMap: { assigned: string; crossChecked: string; reviewed: string };
6370
constructor(
6471
@Inject(REQUEST) private req: BaseRequest,
@@ -752,7 +759,7 @@ export class ReviewTaskService {
752759

753760
return 0;
754761
} catch (error) {
755-
console.error("Error in countReviewTasksNotDeleted:", error);
762+
this.logger.error("Error in countReviewTasksNotDeleted:", error);
756763
throw error;
757764
}
758765
}

0 commit comments

Comments
 (0)