안녕하세요 백엔드 개발자 이수인입니다! 오늘은 discord 로 서버 error 알림을 받아보는 방법에 대해 소개해보려 합니다. 먼저 어떻게 메시지가 오는지 궁금하신 분들을 위해 사진부터 첨부하고 포스팅 시작하겠습니다.
저희 서비스는 현재 pm2로 서버를 실행하고 있습니다. 그래서 docker로 배포하신 분들에겐 큰 도움이 안 될 수 있습니다. 하지만 docker로 배포하는 것도 고려하고 있기 때문에 docker로도 알림을 보낼 수 있게 구현할 예정입니다.
에러가 발생하게 되면 서버에 접속을 해서 에러 메시지를 확인해야 하죠. 하지만 이 방식은 서버에 접속해야 하는 불편함도 있지만, 에러가 발생했을 때 바로 알 수 없다는 단점이 있습니다. 이 문제를 해결하기 위해 고민을 하던 중, 저희 팀원 중 한 명이 디스코드로 에러 메시지를 받을 수 있다고 했습니다. 그러면 알림도 오고, 메시지도 확인할 수 있어서 대응할 수 있게 되죠. 저희 팀원은 '이런 방법이 있다~' 라고만 했기 때문에 어떻게 구현해야 할지 고민할 필요가 있었죠.
discord로 에러 메시지를 보내는 방법은 생각보다 간단합니다.
생각보다 원리는 간단하죠? discord 는 그냥 메시지 받고 출력하는 용도입니다.
가장 쉽게 생각해볼 수 있는 방법입니다. 위에서 설명했던 것처럼 discord 에 에러 메시지를 보내는 방법은 정말 간단합니다. 그래서 service 함수로 처리해버릴 수 있죠. 코드로 봐볼까요?
public async sendErrorMessage(
title: string,
message: string,
error: Error,
context: DiscordWebhookContext,
) {
await this.httpService.axiosRef.post(
this.ERROR_WEBHOOK_URL,
{
content: `# **🚨 에러 로그 알림**`,
embeds: [
{
title,
description: `${message}
Error Object
\`\`\`
${this.getErrorStr(error)}
\`\`\`
`, // Description 추가
color: 16711680, // 빨간색 (16진수)
author: {
name: context,
},
footer: {
text: `시간: ${new Date().toLocaleString()}`,
},
timestamp: new Date().toISOString(),
},
],
},
{
headers: {
'Content-Type': 'application/json',
},
},
);
}
이제 에러가 발생하면 catch 해서 sendErrorMessage
함수를 호출하면 끝입니다. 어려운 방법이 아니죠.
그런데 문제가 있습니다. 이 함수 호출은 NestFactory 가 create 되어야만 호출할 수 있습니다. 만약에 create 되기 전에 호출되면 에러 메시지를 보낼 수 없습니다. ioredis 설정 실패나 env read 실패 같은 것들 말이죠. 그래서 nestjs 의 create 와 상관없이 동작할 수 있는 다른 방법을 고민해볼 필요가 있었습니다.
저희가 pm2로 서버를 돌리고 있으니 pm2 에서 해결할 수 없을까? 고민을 하고 구글링 결과 pm2-discord
라는 라이브러리를 찾을 수 있었습니다. 이 라이브러리는 nestjs 의 생성과 create와 상관없이, pm2 에서 발생하는 event 를 감지하기 때문에 제가 원했던 요구사항을 만족시켜줬습니다.
https://github.com/FranciscoG/pm2-discord
사용 방법은 정말 간단합니다. readme 를 읽어보면 알겠지만,
pm2 install pm2-discord
pm2 set pm2-discord:discord_url https://discord_url
이전에 복사해둔 웹후크 url을 설정을 하고,
위 옵션 중 필요한 것들을 true/false 로 설정하면 됩니다. 간단하죠? 에러 메시지 관련을 다루고 있으니 error
옵션을 true 로 설정하면 되겠네요.
잘 되는지 테스트가 필요합니다. 저는 대충 에러 발생시키는 용도 api를 만들어봤습니다.
@Get('test-error-log')
testErrorLog() {
console.error(
'서버 중단 없는 에러 로그 테스트입니다. 이 메시지가 디스코드에 보여야 합니다.',
);
return {
status: 'OK',
message:
'Error log has been sent, but the server is still running perfectly!',
};
}
내용은 어떻게 작성해도 상관 없지만, console.error
는 필요합니다. 이제 이 api 를 호출하면,
이렇게 알림이 오게 됩니다.
그런데 이런 메시지 형태 괜찮으신가요? 지금 예시는 한 줄이라서 크게 신경 쓰이지 않을 수 있습니다. 그러면 다른 예시를 보여드릴게요.
어떤가요? 잘 읽히나요? 일단 저는 안 읽힙니다. 심지어 위 사진은 메시지가 2개 온 겁니다. 따로 감싸져서 오는 게 아니라 그냥 텍스트가 오니까 잘 읽히지 않습니다. 그리고 언제 발생한 에러인지, 어떤 process에서 발생한 에러인지 등의 필요한 정보들을 판별하기 쉽지 않습니다.
맨 처음에 보여준 사진 기억하시나요? 기억 못 하시는 분들을 위해 다시 첨부해보면,
이렇습니다. 어떤가요? 잘 읽히지 않나요?
이렇게 여러 개가 와도 구분이 잘 됩니다. 이런 형식으로 보내려면 embed
로 감싸서 보내고, 필요한 정보들을 담아서 보내야 합니다. 이렇게 보내는 게 저희의 목표입니다.
경험이 부족한 저한테 제일 쉽지 않은 문제였습니다. 위에 옵션을 보면 알겠지만, 에러 메시지를 커스텀하는 옵션은 없습니다. 그래서 처음에는 방법이 없다고 생각하고 에러 메시지를 커스텀할 수 있는 다른 라이브러리를 찾았습니다. 그런데 아무리 찾아도 없고, 된다고 하는 것들은 안 되더라고요... 결국에 고민을 하다가 pm2-discord 코드를 내가 수정하면 안 되나? 라는 생각이 들더라고요. 그래서 어떻게 수정을 했는지 공유해보려 합니다.
원본 코드를 읽어보면 알겠지만 생각했던 것보다 코드가 간단합니다. 총 5개의 함수 discord에 메시지 보내기, buffer 관리, message queue 관리, 메시지 생성하기, 이벤트 등록하기 가 있습니다. 코드를 설명하는 글이 아니기 때문에 설명은 생략하고, 결론적으로 수정해야 할 함수는 당연하게도 discord 에 메시지 보내기, 메시지 생성하기 입니다. 원본 코드 보겠습니다.
function sendToDiscord(message) {
var description = message.description;
if (!conf.discord_url) {
return console.error("There is no Discord URL set, please set the Discord URL: 'pm2 set pm2-discord:discord_url https://[discord_url]'");
}
var payload = {
"content" : description
};
var options = {
method: 'post',
body: payload,
json: true,
url: conf.discord_url
};
request(options, function(err, res, body) {
if (err) {
return console.error(err);
}
if (res.statusCode !== 204) {
console.error("Error occured during the request to the Discord webhook");
}
});
}
function createMessage(data, eventName, altDescription) {
if (data.process.name === 'pm2-discord') {
return;
}
if (conf.process_name !== null && data.process.name !== conf.process_name) {
return;
}
var msg = altDescription || data.data;
if (typeof msg === "object") {
msg = JSON.stringify(msg);
}
messages.push({
name: data.process.name,
event: eventName,
description: stripAnsi(msg),
timestamp: Math.floor(Date.now() / 1000),
});
}
여기서 각각 수정할 내용은 아래와 같습니다.
sendToDiscord
createMessage
그렇게 어렵지 않으니 바로 코드로 보겠습니다. 수정한 부분은 주석으로 명시할게요.
function sendToDiscord(message) {
if (!discordUrl) {
return console.error(
"There is no Discord URL set, please set the Discord URL: 'pm2 set pm2-discord:discord_url https://[discord_url]'"
);
}
var payload = {
content: message.content,
embeds: [message.embed], // embed 로 감싸서 보내기
};
var options = {
method: "post",
body: payload,
json: true,
url: discordUrl,
};
request(options, function (err, res, body) {
if (err) {
return console.error(err);
}
if (res.statusCode !== 204) {
console.error("Error occured during the request to the Discord webhook");
}
});
}
// 반복되는 메시지 포멧팅으로 함수 생성
function makeEmbedFormat(content, color, msgName, msg, processName) {
return {
content: content,
embed: {
color: color,
fields: [
{
// 메시지 소제목
name: msgName,
value: `\`\`\`\n${msg}\n\`\`\``,
inline: false,
},
{
// 언제 발생한 메시지인지
name: "Time",
value: new Date().toISOString().replace("T", " ").slice(0, 19),
inline: true,
},
{
// 어떤 process 에서 발생한 건지
name: "Process",
value: processName,
inline: true,
},
],
},
};
}
function createMessage(data, eventName, altDescription) {
const processName = data.process.name;
if (processName === "pm2-discord") {
return;
}
if (conf.process_name !== null && processName !== conf.process_name) {
return;
}
var msg = altDescription || data.data;
if (typeof msg === "object") {
msg = JSON.stringify(msg);
}
// 이벤트마다 원하는 메시지 형태로 만들기
const embedFormatters = {
log: () =>
makeEmbedFormat(
"# **📜 LOG **",
5763719,
"Log Message",
msg,
processName
),
error: () =>
makeEmbedFormat(
"# **🚨 ERROR **",
15158332,
"Error Message",
msg,
processName
),
exception: () =>
makeEmbedFormat(
"# **🚨 EXCEPTION **",
15158332,
"Exception Message",
msg,
processName
),
restart: () =>
makeEmbedFormat(
"# **🔄 PROCESS RESTARTED **",
16776960,
"Restart Message",
msg,
processName
),
delete: () =>
makeEmbedFormat(
"# **❌ PROCESS DELETED **",
15158332,
"Delete Message",
msg,
processName
),
stop: () =>
makeEmbedFormat(
"# **🔴 PROCESS STOPPED **",
15158332,
"Stop Message",
msg,
processName
),
exit: () =>
makeEmbedFormat(
"# **🔴 PROCESS EXITED **",
15158332,
"Exit Message",
msg,
processName
),
start: () =>
makeEmbedFormat(
"# **🟢 PROCESS STARTED **",
5763719,
"Start Message",
msg,
processName
),
online: () =>
makeEmbedFormat(
"# **🟢 PROCESS ONLINE **",
5763719,
"Online Message",
msg,
processName
),
};
const formatter = embedFormatters[eventName];
const result = formatter();
messages.push({
name: processName,
event: eventName,
content: result.content,
embed: result.embed,
timestamp: Math.floor(Date.now() / 1000),
});
}
코드가 길 뿐이지 어렵지 않을 거라고 생각합니다.
현재 설정 값에는 discord_url
이거 하나밖에 없습니다. 그런데 채널 여러 개에서 알림을 받고 싶을 경우에는 한 개만으론 부족하죠. 그래서 package.json
에서 config
field 에 원하는 값을 추가하면 됩니다. 아래처럼 말이죠
"config": {
"discord_url": null,
"discord_error_url": null, // 추가한 설정 값
"process_name": null,
"log": true,
"error": false,
"kill": true,
"exception": true,
"restart": false,
"delete": false,
"stop": true,
"restart overlimit": true,
"exit": false,
"start": false,
"online": false,
"buffer": true,
"buffer_seconds": 1,
"queue_max": 100
}
그럼 이 코드를 실제로 적용해봐야겠죠? 원본 코드에서는 pm2-install pm2-discord
이렇게 했지만, 제가 fork 따서 개인 레포에서 수정했기 때문에 좀 다르게 해야 됩니다. pm2 install https://github.com/사용자이름/레포지토리이름
이렇게 하면 됩니다. 저는 pm2 install https://github.com/suleeesulee/pm2-discord
이렇게 했습니다. 설치 후 pm2 list
를 하면
이렇게 설치가 잘 된 것을 확인 할 수 있습니다.
현재 설정된 값들을 확인하고 싶으면 pm2 conf pm2-discord
혹은 더 자세히 보고 싶으면 pm2 describe pm2-discord
로 확인할 수 있습니다.
현재 저희 프로젝트에 적용을 하고 잘 에러를 반환하지만 개선해야 할 부분들이 많습니다.
1, 2번은 어느정도 쉽게 해결이 될 거 같은데 솔직히 3번 문제는 어떻게 해야 할지 고민을 계속하고 있습니다. 3번 문제가 해결되면 여러 프로젝트에서 활용할 수 있을 것 같고 좋은 글 소재가 될 것 같습니다.
아직 수정 중인 코드이기 때문에 조금 부족해 보일 수 있습니다. 피드백 적극 환영합니다. 꼭 좋은 코드로 완성할 테니, 이용해주시면 감사하겠습니다. 그럼 글 마치겠습니다!