discord developer

디스코드 레니 봇 개발 일지_ ( 로그 편 )

yjlee06 2023. 11. 20. 02:41
반응형

디스코드 봇, 인공지능, 게임을 개발할 때 항상 느끼는 거지만

정상적으로 빌드됐는데 실행할 때 발생하는 오류가 가장 짜증 나는 거 같다.


물론 코드를 잘 짜는 것도 중요하지만..
현실은 그렇게 하기 힘들다. 그래서 사후 처리를 위해 로그를 남기기로 했다.

 

 

1. 어떤 방법으로 로그를 저장하면 좋을까?


아마존 데이터베이스를 사용하거나 타 데이터베이스 서비스를 이용하는 것도 좋지만

음악봇 특성상 로그 이벤트가 자주 발생하며 여기서 발생하는 트래픽 비용 또한 만만치 않다....
(아마존 DB를 이용한 조회된 음악 데이터 저장 서비스 했다가 30분 만에 60만 원이 나갔다) 

 

그래서 큰 비용 및 저장 비용 지출 우려가 있는 웹 클라우드 서비스를 사용하는 것보다
파일로 저장하는 걸로 결정하였다.  

 

사용 라이브러리

 

import fs from "fs"

 

 

2. 어떻게 폴더 구조 및 이름을 정하면 좋을까?

 

유저의 기록 데이터를 저장하는 소스코드를 사용한다고 볼 때
유저정보를 하나의 파일에 다 때려 넣으면

나중에 개발할 때 수백 수천 개의 파일을 확인하는 대참사가 발생할 수 있으며

만약 유저 정보를 파일, 폴더명으로 했을 때 개인정보우려가 발생할 수 있어 고민이었다.

 

그래서

유저정보를 폴더, 파일명으로 하는 대신 해당 정보를 hash로 바꾸는 과정을 거쳐
특정정보를 확인할 때 hash값을 비교하며 찾도록 하도록 하였다.

이로서 필요 없는 리소스 사용을 줄이고 개인정보 또한 지킬 수 있게 되었다!

 

폴더 구조

.
└── log
    └── [guildId]
        └── 2023(year)
            └── 11(month)
                └── 9(Date)
                    ├── [fileId]
                    ├── [fileId]
                    ├── [fileId]
                    ├── [fileId]
                    ├── [fileId]
                    ├── .
                    ├── .
                    └── .

guildId:디스코드 서버 id
fileId: [count]_[timeId]_[isError] 

count: 해당 날짜에 발생된 파일 개수
timeId: Date.getUTCHours() Date.getUTCMinutes() Date.getUTCSeconds() Date.getUTCMilliseconds()

isError: true=1 / false=0

 

사용 라이브러리

import hash from 'hash.js';
hash.sha256().update(userId).digest('hex')

userId: 디스코드 유저 id

 

3. 구체적인 소스코드 구조는 어떻게 작성해야 할까?

일단 나는 아래와 같은 방법으로 해결했다!

 

자주 사용하는 변수
1. baseURL: 해당 로그 폴더까지의 룻트
 2. userIdHash: 유저 id 해쉬값

3. guildIdHash 길드 id 해쉬값

4. Date 객체 저장

 

자주 사용하는 함수

해당 함수의 폴더위치를 읽는 방식은 다음과 같다. [ baseURL + folder ]

그 후 읽은 폴더위치의 폴더이름 리스트를 가져온 다음

targetName( userId 또는 guildId 해쉬값)을. filter() 함수를 통해 조회한다.

function FindDir({ folder, targetName, baseURL }: { folder?: string, targetName?: string | number, baseURL: string }) {
 return new Promise<string[]>((resolve: (successValue: string[]) => void, reject: (error: void) => void) => {
  if (!folder) folder = "";
   const dir = fs.readdirSync(baseURL + folder).filter((folder: string) => { return folder == targetName });

   if (dir.length == 0) reject()
    else resolve(dir);
   });
}

그리하여 만약 조회된 폴더가 있다면 resolve(해당  폴더 이름)을 하고

없다면 reject() 한다.

 

resolve 되었을 경우 넘어온 폴더 경로를 통해 다음 폴더의 위치를 확인하는 과정을 거쳐

[fileId] 파일 위치까지 도달한다.

만약 reject 되었을 경우 찾는 경로에 해당 폴더를 생성하는 과정을 반복하여 새로운 폴더 위치를 만든다.

 

해당 함수를 이용하여 mkGuildDir 그리고 mkUserDir 함수를 만든 다음 최종적으로 mkUserJosnFile함수를 이용하여 logging 시스템을 만들 수 있다!

 

 

**참고**

해당 소스코드는 음악봇 개발에 방향을 둔 소스코드이기에 사용하기에는 힘들 수 있다.
위의 내용을 토대로 "어떻게 작성했는가?"의 예시라 참고하면 된다!

 

mkGuildDir 함수

만약 baseURL위치에 log 폴더가 존재하지 않을 시 생성하는 소스코드가 추가되었다.

 private mkGuildDir(): Promise<string> { // return GuildDir
  return new Promise<string>((resolve) => {
  const dir = fs.readdirSync(__dirname).filter((folder: string) => { return folder == 'log' });

  if (dir.length == 0) fs.mkdirSync(this.baseURL)
   FindDir({ targetName: this.guildIdHash, baseURL: this.baseURL })
   .catch(() => {
    fs.mkdirSync(this.baseURL + this.guildIdHash)
   }).finally(() => {
    FindDir({ folder: this.guildIdHash, targetName: this.date.getUTCFullYear(), baseURL: this.baseURL })
    .catch(() => {
     fs.mkdirSync(this.baseURL + this.guildIdHash + "/" + this.date.getUTCFullYear());
    }).finally(() => {
     FindDir({ folder: this.guildIdHash + "/" + this.date.getUTCFullYear(), targetName: this.date.getUTCMonth(), baseURL: this.baseURL })
     .catch(() => {
      fs.mkdirSync(this.baseURL + this.guildIdHash + "/" + this.date.getUTCFullYear() + "/" + this.date.getUTCMonth());
     }).finally(() => {
     FindDir({ folder: this.guildIdHash + "/" + this.date.getUTCFullYear() + "/" + this.date.getUTCMonth(), targetName: this.date.getUTCDate(), baseURL: this.baseURL })
     .catch(() => {
      fs.mkdirSync(this.baseURL + this.guildIdHash + "/" + this.date.getUTCFullYear() + "/" + this.date.getUTCMonth() + "/" + this.date.getUTCDate());
     }).finally(() => {
      resolve(this.guildIdHash + "/" + this.date.getUTCFullYear() + "/" + this.date.getUTCMonth() + "/" + this.date.getUTCDate());
     })
    })
   })
  })
 });
 }

 

mkUserDir 함수

 private mkUserDir(): Promise<string> {  // return UserDir
  return new Promise<string>((resolve) => {
  this.mkGuildDir().then((GuildDir) => {
   FindDir({ folder: GuildDir, targetName: this.userIdHash, baseURL: this.baseURL })
   .catch(() => {
    fs.mkdirSync(this.baseURL + GuildDir + "/" + this.userIdHash)
   });
   resolve(GuildDir + "/" + this.userIdHash);
   })
  });
 }

 

mkUserJosnFile 함수

 private mkUserJsonFile(userDir: string, LogData: LogFormat, isError: boolean) {
  let count: number = 0;
  let IsError: number;

  const dir = fs.readdirSync(this.baseURL + userDir);

  if (dir.length == 0) count = 1;
  else count = dir.length + 1;

  if (isError) IsError = 1;
  else IsError = 0;
  const fileId = `${count}${this.timeId}${IsError}`;

  fs.writeFileSync(`${this.baseURL}${userDir}/${fileId}.json`, JSON.stringify(LogData));
 }