회사에서 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 에서 어떻게 처리하는지 확인해보자.
사진: Unsplash의Anis Rahman