Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 88 additions & 2 deletions src/collaboration/TravelDocWebSocketServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,40 @@ export class TravelDocWebSocketServer {
start() {
this.server.on("upgrade", this.handleUpgrade.bind(this));
this.wss.on("connection", this.handleConnection.bind(this));
console.log(
`[ws] upgrade handler ready maxUsersPerRoom=${this.maxUsersPerRoom}`
);
}

handleUpgrade(req, socket, head) {
const requestLog = this.getRequestLogContext(req, socket);
console.log(
`[ws upgrade] received url=${requestLog.url} remote=${requestLog.remoteAddress} host=${requestLog.host}`
);
Comment on lines +24 to +26

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

운영 환경에서의 디버깅과 로그 관리를 위해 console.log 대신 winston이나 pino와 같은 구조화된 로거(Structured Logger) 사용을 권장합니다. 구조화된 로그를 사용하면 userId, travelDocId와 같은 필드를 개별 속성으로 저장할 수 있어, 나중에 로그 검색 및 분석이 훨씬 용이해집니다.


const authResult = this.authenticator.authenticateUpgrade(req);
if (!authResult.ok) {
console.warn(
`[ws upgrade] auth rejected status=${authResult.statusCode} message="${authResult.message}" url=${requestLog.url} remote=${requestLog.remoteAddress}`
);
this.rejectUpgrade(socket, authResult.statusCode, authResult.message);
return;
}

const { travelDocId, userId } = authResult.session;
console.log(
`[ws upgrade] auth accepted userId=${userId} travelDocId=${travelDocId} url=${requestLog.url}`
);

const reservationResult = this.reserveRoomSlot(travelDocId, userId);
console.log(
`[ws upgrade] room reservation result ok=${reservationResult.ok} reserved=${reservationResult.reserved ?? false} userId=${userId} travelDocId=${travelDocId} activeUsers=${this.getRoomUserCount(travelDocId)} pendingUsers=${this.getPendingRoomUserCount(travelDocId)}`
);

if (!reservationResult.ok) {
console.warn(
`[ws upgrade] room rejected maxUsersPerRoom=${this.maxUsersPerRoom} userId=${userId} travelDocId=${travelDocId}`
);
this.rejectUpgrade(
socket,
403,
Expand All @@ -37,9 +59,15 @@ export class TravelDocWebSocketServer {
if (reservationResult.reserved) {
this.releaseReservedRoomSlot(travelDocId, userId);
}
console.warn(
`[ws upgrade] secondary token rejected as already used userId=${userId} travelDocId=${travelDocId}`
);
this.rejectUpgrade(socket, 401, "Secondary token already used");
return;
}
console.log(
`[ws upgrade] secondary token consumed userId=${userId} travelDocId=${travelDocId}`
);

let reservationSettled = false;
const rollbackReservation = () => {
Expand All @@ -48,6 +76,9 @@ export class TravelDocWebSocketServer {
}
reservationSettled = true;
this.releaseReservedRoomSlot(travelDocId, userId);
console.warn(
`[ws upgrade] room reservation rolled back userId=${userId} travelDocId=${travelDocId} activeUsers=${this.getRoomUserCount(travelDocId)} pendingUsers=${this.getPendingRoomUserCount(travelDocId)}`
);
};
const finalizeReservation = () => {
if (reservationSettled) {
Expand All @@ -69,17 +100,24 @@ export class TravelDocWebSocketServer {
try {
this.wss.handleUpgrade(req, socket, head, ws => {
finalizeReservation();
console.log(
`[ws upgrade] handshake accepted userId=${userId} travelDocId=${travelDocId} url=${requestLog.url}`
);
this.wss.emit("connection", ws, req);
});
} catch {
} catch (error) {
rollbackReservation();
console.error(
`[ws upgrade] handshake failed userId=${userId} travelDocId=${travelDocId} error=${error?.message ?? error}`
);
Comment on lines +110 to +112

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

에러 로그 기록 시 error.message만 문자열에 포함하면 실제 에러의 스택 트레이스(Stack Trace) 정보를 잃게 됩니다. console.error의 두 번째 인자로 에러 객체를 직접 전달하면 더 상세한 디버깅 정보를 기록할 수 있습니다.

Suggested change
console.error(
`[ws upgrade] handshake failed userId=${userId} travelDocId=${travelDocId} error=${error?.message ?? error}`
);
console.error(
'[ws upgrade] handshake failed userId=' + userId + ' travelDocId=' + travelDocId,
error
);

this.rejectUpgrade(socket, 500, "WebSocket handshake failed");
}
}

handleConnection(ws, req) {
const session = req.session;
if (!session) {
console.error("[ws connection] rejected because session is missing");
ws.close(1011, "session missing");
return;
}
Expand All @@ -90,11 +128,23 @@ export class TravelDocWebSocketServer {
}
this.addRoomParticipant(travelDocId, userId);
this.addUserSession(userId, ws);
console.log(
`[ws connection] connected userId=${userId} travelDocId=${travelDocId} userSessions=${this.getUserSessionCount(userId)} activeUsers=${this.getRoomUserCount(travelDocId)}`
);
this.yWebSocketHandler.handleConnection(ws, req, session);

ws.on("close", () => {
ws.on("close", (code, reason) => {
this.removeUserSession(userId, ws);
this.removeRoomParticipant(travelDocId, userId);
console.log(
`[ws connection] closed userId=${userId} travelDocId=${travelDocId} code=${code} reason="${reason.toString()}" userSessions=${this.getUserSessionCount(userId)} activeUsers=${this.getRoomUserCount(travelDocId)}`
);
});

ws.on("error", error => {
console.error(
`[ws connection] error userId=${userId} travelDocId=${travelDocId} error=${error?.message ?? error}`
);
Comment on lines +145 to +147

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

위의 handleUpgrade 에러 로그와 마찬가지로, 에러 객체를 직접 전달하여 스택 트레이스를 포함한 전체 에러 컨텍스트를 유지하는 것이 좋습니다.

      console.error(
        '[ws connection] error userId=' + userId + ' travelDocId=' + travelDocId,
        error
      );

});
}

Expand Down Expand Up @@ -201,7 +251,43 @@ export class TravelDocWebSocketServer {
}
}

getRoomUserCount(travelDocId) {
return this.roomParticipants.get(travelDocId)?.size ?? 0;
}

getPendingRoomUserCount(travelDocId) {
return this.pendingRoomParticipants.get(travelDocId)?.size ?? 0;
}

getUserSessionCount(userId) {
return this.userSessions.get(userId)?.size ?? 0;
}

getRequestLogContext(req, socket) {
return {
url: this.sanitizeRequestUrl(req),
host: req.headers.host ?? "-",
remoteAddress:
req.headers["x-forwarded-for"] ?? socket.remoteAddress ?? "-"
};
}

sanitizeRequestUrl(req) {
const host = req.headers.host ?? "localhost";
try {
const url = new URL(req.url ?? "/", `http://${host}`);
if (url.searchParams.has("secondaryToken")) {
url.searchParams.set("secondaryToken", "[redacted]");
}
return `${url.pathname}${url.search}`;
} catch {
return req.url ?? "/";
}
}

rejectUpgrade(socket, statusCode, message) {
console.warn(`[ws upgrade] responding status=${statusCode} message="${message}"`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

rejectUpgrade 메서드 내부에 추가된 로그는 handleUpgrade에서 rejectUpgrade를 호출하기 직전에 이미 상세한 로그를 남기고 있어 중복됩니다. 로그 노이즈를 줄이기 위해 이 라인은 삭제하는 것을 권장합니다.


let statusText = "Internal Server Error";
if (statusCode === 400) {
statusText = "Bad Request";
Expand Down
Loading