API 로그를 어떻게 Filebeat 가 긁어갈 수 있는가

Jae Min·2023년 12월 22일
0

ELK STACK

목록 보기
2/5
post-thumbnail

회사에서 ELK stack 에서 로그파일을 긁어오기 위해서는 filebeat 를 사용한다.
filebeat 가 에러파일을 수집할 수 있다고 여기서 언급했었다.

그렇다면 실제로 어떻게 로그를 쌓고, 어떻게 연결이 되어서 긁어가는지 알아보자.

우리는 어디서든 에러가 발생하면 ExceptionFilter 를 통해서 에러를 캐치한다. 이를 실제로 구현한 코드는 아래와 같다.

// all-exception.filter.ts
import { LoggerService } from '@common/utility/logger.service';
import { ResponseObj } from '@core/class/response/response-obj';
import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpAdapterHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';

@Catch()
export class AllExceptionFilter implements ExceptionFilter {
  constructor(
    private readonly httpAdapterHost: HttpAdapterHost,
    private readonly logger: LoggerService,
  ) {}

 
  catch(exception: any, host: ArgumentsHost) {
    const { httpAdapter } = this.httpAdapterHost;
    const ctx = host.switchToHttp();

    const hostArgs = host.getArgs();
    const req = host.getArgByIndex(0);
    const incommingMessage = hostArgs[0];
    const serverIp = req.connection.localAddress.split(':')[3];
    // host port 
    const serverPort = req.connection.localPort;
    const hostUrl = `${req.headers.host}::${serverPort}`;

    const clientIp =
      incommingMessage.headers['x-forwarded-for']?.split(',')[0] ||
      req.connection.remoteAddress.split(':')[3];

    const { url, method } = incommingMessage;
    const appName = url.split('/')[2];
    let httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
    const errorCode = Number(exception.errorCode)
      ? exception.errorCode
      : Number(exception.message)
      ? exception.message
      : '0000';

    exception.url = url;
    exception.method = method;
    exception.appName = appName;
    exception.serverIp = serverIp;
    exception.clientIp = clientIp;
    exception.body = JSON.stringify(req.body);
    exception.hostUrl = hostUrl;

    httpStatus = exception.status
      ? exception.status
      : exception.statusCode
      ? exception.statusCode
      : httpStatus;
    
    switch(errorCode){
      case ...:
        this.logger.info(exception);
        this.logger.warn(exception);
        this.logger.error(exception):
    }

    let exceptionInfo: {
      statusCode?: number;
      error?: string;
      message?: string;
    } = {
      statusCode: exception?.status,
      message: exception?.message,
      error: exception?.message,
    };
   /**
    exceptionInfo 에 어떻게 값을 할당하는지는 생략하겠다.
   */

    const responseObj: ResponseObj<any> = new ResponseObj<any>(
      false,
      null,
      exceptionInfo,
      httpAdapter.getRequestUrl(ctx.getRequest()),
    );

    httpAdapter.reply(ctx.getResponse(), responseObj, httpStatus);
  }
}
// logger.service.ts
import { LoggerService, LogLevel } from '@nestjs/common';
import * as winston from 'winston';
import * as winstonDaily from 'winston-daily-rotate-file';

const logDir = `${process.cwd()}/logs`;
const { errors, combine, timestamp, printf, colorize, label } = winston.format;

export class LoggerService implements LoggerService {
  private logger: winston.Logger;

  constructor(service: string) {
    this.logger = winston.createLogger({
      exitOnError: true,
      format: combine(
        timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSSZ' }),
        errors({ stack: true }),
        label({ label: service }),
        printf((msg) => {
          return `#${msg.timestamp}#${msg.level}#${msg.message.appName}#${msg.message.method}#${msg.message.hostUrl}#${msg.message.url}#${msg.message.error}#${msg.message.body}#${msg.message.clientIp}#${msg.message.serverIp}#Exception ${msg.message.stackTrace}`;
        }),
      ),
      transports: [new winstonDaily(this.dailyOptions('info', service))],
      // handleExceptions: true,
    });

  }
  public log(message: any, ...optionalParams: any[]) {
    throw new Error('Method not implemented.');
  }
  public setLogLevels?(levels: LogLevel[]) {
    throw new Error('Method not implemented.');
  }

  console(): object {
    return this.logger;
  }

  public error(message: string, meta?) {
    this.logger.error(message, meta);
  }

  public warn(message: string) {
    this.logger.warn(message);
  }

  public info(message: string) {
    this.logger.info(message);
  }

  public debug(message: string) {
    this.logger.debug(message);
  }

  private dailyOptions(level: string, service: string) {
    return {
      level,
      datePattern: 'YYYY-MM-DD',
      dirname: logDir + `/${service}`,
      filename: `%DATE%.log`,
      maxFiles: '14d',
      maxSize: '256m',
      zippedArchive: false,
    };
  }
}

생략한 부분이 많긴 하지만, 중요하게 볼 부분은 아래 부분이다.

switch(errorCode){
  case ...:
    this.logger.info(exception);
    this.logger.warn(exception);
    this.logger.error(exception):
}

LoggerService 를 주입받아서 사용한 this.logger. 를 통해서
#${msg.timestamp}#${msg.level}#${msg.message.appName}#${msg.message.method}#${msg.message.hostUrl}#${msg.message.url}#${msg.message.error}#${msg.message.body}#${msg.message.clientIp}#${msg.message.serverIp}#Exception ${msg.message.stackTrace} 형태로 ${process.cwd()}/logs 파일에 저장할 수 있다.

이렇게 로컬 파일에 저장을 하게 되면 우리는 도커 이미지를 통해 배포를 하고 서비스를 운영하기 때문에 실제로는 ec2 instance 에 로그파일이 쌓이는 것이 아니고, 도커 이미지 안에 위치하게 된다.
그렇기 때문에, 도커 volume 을 통해서 파일을 공유해야 하는데, 아래와 같이 공유할 수 있다.

version: '3.7'
services:
  gateway:
    image: gateway
    container_name: gateway
    restart: always
    ports:
      - 3000:3000
      - 3051:3051
    working_dir: /app
    volumes:
      - /var/elk-log:/app/logs
    entrypoint:
      - node
      - ./dist/apps/gateway/main.js

도커 이미지 안에 위치한 /app/logs 이 부분을 실제 ec2 instance 의 /var/elk-log 로 이동시켜줄 수 있다. 이를 volumne mounting 이라고 한다.

이렇게 공유된 로그 파일은 /var/elk-log 에서 확인할 수 있다.

이렇게 공유가 되면 filebeat 에서는 input -> paths 를 통해서 파일을 긁어올 수 있게 된다.

filebeat.inputs:

# filestream is an input for collecting log messages from files.
- type: log

  # Unique ID among all inputs, an ID is required.
  #id: my-filestream-id

  # Change to true to enable this input configuration.
  enabled: true

  # Paths that should be crawled and fetched. Glob based paths.
  paths:
    - /var/elk-log/*/*.log

다음에는 이렇게 긁어온 파일을 받은 logstash 에서 어떻게 처리하는지 확인해보자.


REF

사진: UnsplashAnis Rahman

profile
자유로워지고 싶다면 기록하라.

0개의 댓글