Seize the day

SEARCH RESAULT : 글 검색 결과 - 전체 글 (총 474개)

POST : Backend study

Mongo 서버에 ping 해보기

mongo 메뉴얼에 따르면, "ping 명령은 서버가 명령에 응답하는지 테스트하는 데 사용되는 무작동 명령입니다. 이 명령은 서버가 쓰기 잠금 상태인 경우에도 즉시 반환됩니다".

db서버 연결 타임아웃은 1초로 한다. 같은 vm에서 실행되기때문에 기본값인 30초는 너무 길다. 
https://www.mongodb.com/community/forums/t/connect-timeout-and-execution-timeout-in-nodejs-driver/2129/2 
코멘트를 참고했다.

구현은 hello API를 이용한다.  hello API가 하는 것은 Mongo db의 상태와 Nest.js 서버의 정상적인 응답 여부이다.  앱에서 호출하지는 않고 문제 생겼을 때 브라우저에서 확인용으로 호출해 본다. 구현은 간단해서 자세한 설명은 생략한다.  

import { handleResult } from "src/verify_token";
import { MongoClient } from "mongodb";

export async function getHello(res: Response) {
    let mongoStatus;
    try {
      const config = {
        serverSelectionTimeoutMS: 1000, //connection timeout
        socketTimeoutMS: 1000,
      }
      const client = await MongoClient.connect("mongodb://localhost", config);
      const result = await client.db("admin").command({ ping: 1 });
      await client.close();
      mongoStatus = result && result.ok && result.ok == 1? "ping OK" : "ping failed";
    } catch(error) {
      mongoStatus = "Down (connection failed)";
    }

    handleResult(res, {
      server: "API Server(version: 2)",
      mongoStatus,
      timestamp: new Date().toLocaleString()
    })
  }
top

posted at

2024. 5. 4. 20:29


POST : Backend study

Nest.js 서버에 ThrottlerGuard, AbusingGuard 적용하기

공식 문서 https://docs.nestjs.com/security/rate-limiting 를 참조하고, 
https://velog.io/@junguksim/NestJS-%EB%85%B8%ED%8A%B8-2-Guards 도 참고했다. 
https://blog-ko.superb-ai.com/nestjs-interceptor-and-lifecycle/  life-cycle을 참고했다.

특정 시간 ttl (단위 밀리세컨드), 안에 최대 limit 개수 이상의 요청은 Too many request 에러 (Http 429 에러) 응답하기

Guard 적용

@Module({
  imports: [ThrottlerModule.forRoot([
    // 6초동안 최대 요청 개수 10으로 제한
    {
      ttl: 6000,
      limit: 10,
    },
  ])],
  controllers: [AppController],
  providers: [AppService, Logger, {
    provide: APP_GUARD,
    useClass: ThrottlerGuard,
  }],
})

요청이 limit를 초과하면  class ThrottlerException extends HttpException 를 던진다. 

 

그런데 만약 limit를 초과하면 그 이후 1시간이나 24시간 동안 요청을 거부하고 싶다면 어떻게 하지??

https://github.com/nestjs/throttler/issues/1660  blockDuration 을 추가하는 이슈가 오픈되어 있다는 것은 아직 ThrottleGuard에서는 이 기능이 불가능하다는 뜻. 

요청의 IP 단위로 차단 여부를 별도로 저장하는 코드를 작성했다. https://github.com/nestjs/throttler/blob/master/src/throttler.service.ts  참고했다.

interface AbusingInfo {
    expireAt: number,
    uid?: string,
}

const abusingMap: Record<string, AbusingInfo> = {};

export function isRequestBlocked(key: string): boolean {
    const info = abusingMap[key]
    if (info) {
        const isBlocked =  Date.now() < info.expireAt;
        if (!isBlocked) {
            delete abusingMap[key];
        }
        return isBlocked;
    }
    return false;
}

export function getAbusingInfo(key: string): AbusingInfo | undefined {
  return abusingMap[key];
}

// 기본 1시간 동안 요청을 차단한다.
export function addAbusingRequest(key: string, durationSeconds: number = 3600) {    
    abusingMap[key] = {expireAt: Date.now() + durationSeconds * 1000};
}

export function removeAbusingRequest(key: string) {
    delete abusingMap[key];
}

addAbusingRequest로 차단할 요청의 IP와 얼마동안 차단할지 초단위로 지정한다. 

 

AbusingGuard 구현

그리고 간단하게 특정 IP의 요청을 blockDuration 동안 차단하는  AbusingGuard 를 만들었다.  LoggerMiddleWare에서 하려고도 했으나 거기서 예외를 던지면 제대로 처리되지 않는다. 

import {
    Injectable,
    CanActivate,
    ExecutionContext,
    HttpException,
    HttpStatus,
  } from "@nestjs/common";
  
@Injectable()
export class AbusingGuard implements CanActivate {
  canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();

    if (isRequestBlocked(request.ip)) {
        throw new HttpException("차단되었습니다. (You have been blocked.)", HttpStatus.FORBIDDEN);
    }
    return true;
  }
}

Guard 추가

providers: [AppService, Logger, {
    provide: APP_GUARD,
    useClass: AbusingGuard,
  }, {
    provide: APP_GUARD,
    useClass: ThrottlerGuard,
  }],

 

ThrottlerGuard에서 거부될 때, 1시간 동안 차단하기

HttpExceptionFilter 에서  ThrotterException 체크

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {

  catch(exception: HttpException, host: ArgumentsHost): void {
...

    if (exception instanceof ThrottlerException) {        
        this.logger.warn("ThrottlerException 발생 1시간동안 차단")
        addAbusingRequest(request.ip);
    }

특정 API에서 이상 탐지시 1시간 동안 요청 차단하기.  

@Get(v1 + "hello")
  hello(@Req() req: Request, @Res() res: Response) {
    addAbusingRequest(req.ip);
    getHello(res);
  }
top

posted at

2024. 5. 2. 21:25


POST : Backend study

Nest.js 예외처리 ExceptionFilter 적용하기.

https://velog.io/@anjinwoong/Nest.js-Exception-ExceptionFilter 를 참고했고 자세한 구현 설명은 생락하고 코드 위주로..

ErrorCodeException

입력 파라미터 체크 실패이거나, 로직상 정상적인 응답이지만 API상의 에러코드를 응답해야 하는 경우를 위해서 ErrorCodeException을 정의했다. 예외가 발생하면 file logging과 discord notify를 같이 적용한다. 

export class ErrorCodeException extends Error {
    errorCode: number
    
    constructor(message: string, errorCode: number) {
        super(message)
        this.errorCode = errorCode
    }
}

@Catch(ErrorCodeException)
export class ErrorCodeExceptionFilter implements ExceptionFilter {

  private readonly logger = winstonLogger;

  catch(exception: ErrorCodeException, host: ArgumentsHost): void {
    const ctx = host.switchToHttp();
    const request = ctx.getRequest<Request>();
    const response = ctx.getResponse<Response>();
    const errorCode = exception.errorCode;
    const timestamp = new Date().toLocaleString();

    const content = {
      code: errorCode,
      message: exception.message,
      path: request.url,
      timestamp: timestamp,
    };

    response.status(200).send(content);

    if (IS_PRODUCTION) {
      axios.post(DISCORD_WEBHOOK_URL, {
        content: JSON.stringify(content),
      });
    }

    // write log file
    this.logger.error(`errorCode: ${errorCode} message: ${exception.message} path: ${request.url} ip: ${request.ip}`)
  }
}

 

HttpException이 발생하면 경고 log를 작성하고 역시 discord로 알린다. 

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {

  private readonly logger = winstonLogger;

  catch(exception: HttpException, host: ArgumentsHost): void {
    const ctx = host.switchToHttp();
    const request = ctx.getRequest<Request>();
    const response = ctx.getResponse<Response>();
    const statusCode = exception.getStatus();
    const timestamp = new Date().toLocaleString();

    const content = {
      statusCode: statusCode,
      message: exception.message,
      path: request.url,
      timestamp: timestamp,
    };

    response.status(statusCode).send(content);

    if (IS_PRODUCTION) {
      axios.post(DISCORD_WEBHOOK_URL, {
        content: JSON.stringify(content),
      });
    }

    // write log file
    this.logger.warn(`statusCode: ${statusCode} message: ${exception.message} path: ${request.url} ip: ${request.ip}`)
  }
}

 

그외 new Error("fatal error")나 알 수 없는 오류가 발생할 경우 (발생하지 말아야 할 오류이다.)

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {

  catch(exception: unknown, host: ArgumentsHost) {    
    super.catch(exception, host);
    
    if (IS_PRODUCTION) {
        const ctx = host.switchToHttp();
        const request = ctx.getRequest<Request>();    
        const message = exception?.toString();
        const timestamp = new Date().toLocaleString();
        
        const logMessage = `${timestamp} message: ${message} path: ${request.url} ip: ${request.ip}`;
        const stack =  super.isExceptionObject(exception)? exception.stack : null;
    
        axios.post(DISCORD_WEBHOOK_URL, {
            content: `${logMessage} ${stack}`,
        });        
    }    
  }
}

super.catch(exception, host); 에서 file로 log를 저장하므로 , discord로 알리는 코드만 구현한다.

 

ExceptionFilter 적용

ap.controller.ts

@Controller()
@UseFilters(ErrorCodeExceptionFilter)
export class AppController {
...

main.ts

const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new AllExceptionsFilter(httpAdapter), new HttpExceptionFilter());

HttpException은 HttpExceptionFilter에서 처리하고 그 외는  AllExceptionFilter 에서 처리하고 싶은데, HttpExceptionFilter를 모듈에 설치하면 제대로 동작하지 않는다. useGlobalFilters에 두 개다 추가해야하고, 심지어 추가하는 순서도 중요하다. HttpExceptionFilter가 앞에 있으면 제대로 동작하지 않는다. 

@Controller() @UseFilters(ErrorCodeExceptionFilter) 내보내기 클래스 AppController { ...
 
top

posted at

2024. 5. 2. 00:35


POST : Backend study

Nest.js file logging을 해 보자. (winston)

file로 로그 파일을 작성해야  나중에 어떤 문제가 발생했을 때 원인파악을 할 수 있다. 때문에 file로 로그를 저장하되, 오래된 파일은 자동으로 삭제되도록하여 스토리지가 가득차는 일이 생기지 않도록 하자. 여러검색을 해 보니 winston이 가장 좋은 것 같다. 

https://velog.io/@aryang/NestJS-winston%EC%9C%BC%EB%A1%9C-%EB%A1%9C%EA%B7%B8-%EA%B4%80%EB%A6%AC  이쪽 블로그를 참조해서 조금 수정했다.

logger.utils.ts

import { utilities, WinstonModule } from 'nest-winston';
//import as winstonDaily from 'winston-daily-rotate-file';
const winstonDaily = require('winston-daily-rotate-file')
import * as winston from 'winston';
import { IS_DEVELOPMENT } from './app.const';

const PROJECT_ID = "LT";
const logDir = __dirname + '/../logs'; // log 파일을 관리할 폴더

const dailyOptions = (level: string) => {
  return {
    level,
    datePattern: 'YYYY-MM-DD',
    dirname: logDir + `/${level}`,
    filename: `%DATE%.${level}.log`,
    maxFiles: 30, //30일치 로그파일 저장
    zippedArchive: true, // 로그가 쌓이면 압축하여 관리
    format: winston.format.combine(
      winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), 
      utilities.format.nestLike(PROJECT_ID, {prettyPrint: true}),      
    ),
  };
};

const consoleOptions = {
  level: 'debug', //console은 debug 이상은 다 출력한다
  format: winston.format.combine(
    winston.format.colorize({ all: true }),
    winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    utilities.format.nestLike(PROJECT_ID, {prettyPrint: true}),
  ),
};

const createTransports = () => {
  const transprots : winston.transport[]= [];
  if (IS_DEVELOPMENT) {
    // Console에 출력은 개발모드일때만 한다.
    transprots.push(new winston.transports.Console(consoleOptions));
  }
  transprots.push(new winstonDaily(dailyOptions('info')));
  transprots.push(new winstonDaily(dailyOptions('warn')));
  transprots.push(new winstonDaily(dailyOptions('error')));
  return transprots;
};


export const winstonLogger = WinstonModule.createLogger({
  transports: createTransports(),
});

 파일로 저장과 console로 출력되는 내용은 같게 한다.  콘솔로 출력은 debug 이상부터 출력하지만, 파일로는 info 이상부터 쓴다. 

 

logger.middleware.ts

import {
    Inject,
    Injectable,
    Logger,
    NestMiddleware,
  } from '@nestjs/common';
  
import { Request, Response, NextFunction } from 'express';
  
  @Injectable()
  export class LoggerMiddleware implements NestMiddleware {
    constructor(@Inject(Logger) private readonly logger: Logger) {}
  
    use(req: Request, res: Response, next: NextFunction) {
      // 요청 객체로부터 ip, http method, url, user agent를 받아온 후
      const { ip, method, originalUrl } = req;
      //const userAgent = req.get('user-agent'); //express
      const userAgent = req.headers['user-agent']; // fastify

      // 응답이 끝나는 이벤트가 발생하면 로그를 찍는다.
      res.on('finish', () => {
        const { statusCode } = res;
        this.logger.log(
          `${method} ${originalUrl} ${statusCode} ${ip} ${userAgent}`,
        );
      });
  
      next();
    }
  }

이것은 Appache의 로그 파일 처럼 모든 Http 요청과 응답코드를 기록한다. 

 

전역 logger 교체 

import { winstonLogger } from './logger.utils';

...
const app = await NestFactory.create<NestFastifyApplication>(
    AppModule, 
    new FastifyAdapter(),
    {logger: winstonLogger,}
  );

 

LoggerMiddleWare 적용

import { MiddlewareConsumer, Module, Logger } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { LoggerMiddleware } from "./logger.middleware";

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService, Logger],
})
export class AppModule{
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('*');
  }
}

Logger를 Inject할 수 있도록 providers에 추가한다.

 

Test

@Controller()
export class AppController {
  constructor(@Inject(Logger) private readonly logger: Logger) {}

  @Get(v1 + "hello")
  hello(@Res() res: Response) {

    this.logger.log("INFO log");
    this.logger.debug("DEBUG log");
    this.logger.warn("WARN log");
    this.logger.error("ERROR log");

    getHello(res);
  }

Console출력은..

[LT] info	2024-05-01 23:56:28 INFO log
[LT] debug	2024-05-01 23:56:28 DEBUG log
[LT] warn	2024-05-01 23:56:28 WARN log
[LT] error	2024-05-01 23:56:28 ERROR log - { stack: [ null ] }
[LT] info	2024-05-01 23:56:28 GET /lt/v1/hello 200 127.0.0.1 PostmanRuntime/7.37.3

로그 파일에는 debug는 제외하고 동일하게 저장되었다.

top

posted at

2024. 5. 1. 23:57


POST : Backend study

Nest.js 서버 안전하게 종료하기

1. SIGTERM으로 안전하게 종료해야 한다. 

참고  https://linuxhandbook.com/sigterm-vs-sigkill/ 
참고2 https://dev.to/hienngm/graceful-shutdown-in-nestjs-ensuring-smooth-application-termination-4e5n  

kill_server.sh를 수정..  -15 파라미터로 변경

pid=$(<nest_server.pid)
echo "stop nest_server (pid is $pid)"

# kill with SIGTERM
kill -15 $pid

# port 3000을 사용하는 pid에 SIGTERM을 보낸다.
# kill -15 $(lsof -i tcp:3000 | awk '{if (NR > 1) print $2}')

rm nest_server.pid

 

2. SIGTERM 받을 때, 종료 처리

Nest.js의 라이프 사이클 참고  https://docs.nestjs.com/fundamentals/lifecycle-events   onApplicationShutdown 에서 뭔가를 하면 좋을 듯..

main.ts

app.enableShutdownHooks([ShutdownSignal.SIGTERM]);

 

app.service.ts 

import { Injectable, OnApplicationShutdown } from "@nestjs/common";
import axios from 'axios';

import { mongoClient } from "./mongo_db";
import { DISCORD_WEBHOOK_URL, IS_PRODUCTION } from "./app.const";

@Injectable()
export class AppService implements OnApplicationShutdown {

    async onApplicationShutdown(signal?: string | undefined) {
        console.log(`onApplicationShutdown signal=${signal}`);
        
        console.log("mongoClient.close()")
        await mongoClient.close();

        if (IS_PRODUCTION) {
            console.log("Notify shutdown to DISCORD");
            await axios.post(DISCORD_WEBHOOK_URL, {content: "API Server shutdown"});
        }

        console.log("process exists")
    }
}

 

top

posted at

2024. 4. 29. 19:23


POST : Backend study

Nest.js 서버에 중요 서버 에러 Discord 메신저로 보내기

github에서 어떤 코드를 보다가 예전에 트위터로 중요 서버 에러를 남기는 기능에 대한 힌트를 얻었다.   요즘에는 Discord 메신저를 많이 이용하나 보다.  (DISCORD_WEBHOOK_URL 검색

빠르게 테스트를 해 보았는데 의외로 너무 잘되서 여기에 스터디 로그를 남긴다. 

1. Discord Web Hook URL 만들기.  

가입이 이제는 초대없이 되나보다.  가입부터 하고, Hook URL 만들기는 너무 간단했다.   참고 URL 

 

2. Node js 에서 에러 보내기

axios 추가

bun add axios

보내기..

    import axios from 'axios';
    
    const content = {
      code: status,
      message: exception.message,
      path: request.url,
    };

    axios.post(DISCORD_WEBHOOK_URL, {
      content: "<@123414024347770> " + JSON.stringify(content),
    });

 

3. 디스코드 User id 구하기

<@특정 사용자 ID> 로 보내면 해당 사용자가 멘션이 되는데 이 아이디를 찾는 법을 구글링했다.  멘션을 하지 않아도 앱 푸시 알림은 오기 때문에 혼자만 있는 방이라면 굳이 멘션하지 않아도 된다.

개발이 이렇게 쉬워도 되는건가 싶다. 

top

posted at

2024. 4. 29. 00:07


CONTENTS

Seize the day
BLOG main image
김대정의 앱 개발 노트와 사는 이야기
RSS 2.0Tattertools
공지
아카이브
최근 글 최근 댓글
카테고리 태그 구름사이트 링크