로그 마이크로서비스

마이크로서비스 아키텍처는 마이크로서비스들이 각자의 로그를 발생하는 구조이기에, 서로 독립적인 위치에 로그를 저장하면 모든 로그를 수집하는 데 많은 비용이 듭니다. 이러한 로그를 한곳에 저장하는 방법은 여러 가지가 있습니다. 각자 남긴 로그를 logstash나 fluentd 같은 로그 수집기를 이용해 한곳에 모으거나 모든 마이크로서비스를 같은 저장소에 저장하면 됩니다. 하지만 로그를 저장하는 저장소를 변경하거나 로그 형식을 일괄적으로 변경할 때는 모든 마이크로서비스를 변경해야 한다는 문제가 있습니다.

마이크로서비스 로그 저장 1

마이크로서비스 로그 저장 1

이러한 문제는 로그를 관리하는 마이크로서비스를 만들고, 모든 마이크로서비스는 로그 관리 마이크로서비스에 로그를 전달하도록 하면 해결할 수 있어 좀 더 유연하게 관리할 수 있습니다.

마이크로서비스 로그 저장 2

마이크로서비스 로그 저장 2

우리가 만든 마이크로서비스 아키텍처에 로그를 관리하는 마이크로서비스를 추가하겠습니다. 로그 입력 기능만 있습니다.

'use strict';

const cluster = require('cluster');

class logs extends require('./server.js') {
    constructor() {
        super("logs"  // ➊ POST/logs 한 가지 기능만 가지도록 함
            , process.argv[2] ? Number(process.argv[2]) : 9040
            , ["POST/logs"]
        );

        this.connectToDistributor("127.0.0.1", 9000, (data) => {
            console.log("Distributor Notification", data);
        });
    }

    onRead(socket, data) {  // ➋ 로그가 입력되면 화면에 출력
        const sz = new Date().toLocaleString() + '\\t' + socket.remoteAddress + '\\t' +
                   socket.remotePort + '\\t' + JSON.stringify(data) + '\\n';
        console.log(sz);
    }
}

if (cluster.isMaster) {
    cluster.fork();

    cluster.on('exit', (worker, code, signal) => {
        console.log(`worker ${worker.process.pid} died`);
        cluster.fork();
    });
} else {
    new logs();
}

이전에 만든 마이크로서비스와 동일한 패턴으로 로그 관리 마이크로서비스를 만듭니다. 이름은 logs로 설정하고, 기본 포트 정보는 9040으로 지정하며, 기능은 로그 입력만 구현합니다(➊). API가 호출되면 화면에는 시간과 접속한 마이크로서비스의 주소 정보, 입력한 로그를 출력합니다(➋).

로그를 처리할 마이크로서비스가 준비되었습니다. 이제 모든 마이크로서비스의 부모 클래스를 수정해 로그 관리 마이크로서비스에 API 요청 로그를 남기도록 합니다.

class tcpServer {
    constructor(name, port, urls) {
        this.logTcpClient = null; // ➊ 그 관리 마이크로서비스 연결 클라이언트

......

        socket.on('data', (data) => {
            var key = socket.remoteAddress + ":" + socket.remotePort;
            var sz = this.merge[key] ? this.merge[key] + data.toString() :
                data.toString();
            var arr = sz.split('¶');
            for (var n in arr) {
                if (sz.charAt(sz.length - 1) != '¶' && n == arr.length - 1) {
                    this.merge[key] = arr[n];
                    break;
                } else if (arr[n] == "") {
                    break;
                } else {
                    this.writeLog(arr[n]); // ➋ equest 로그
                    this.onRead(socket, JSON.parse(arr[n]));
                }
            }
        });
    });

......

    connectToDistributor(host, port, onNoti) {

......

        this.clientDistributor = new tcpClient(
            host
            , port
            , (options) => {
                isConnectedDistributor = true;
                this.clientDistributor.write(packet);
            }
            , (options, data) => {

           // ➌ 그 관리 마이크로서비스 연결
            if (this.logTcpClient == null && this.context.name != 'logs') {
                for (var n in data.params) {
                    const ms = data.params[n];
                    if (ms.name == 'logs') {
                        this.connectToLog(ms.host, ms.port);
                        break;
                    }
                }
            }
            onNoti(data);
            }               // Distributor 데이터 수신 이벤트
            // Distributor 접속 종료 이벤트
            , (options) => { isConnectedDistributor = false; }
            // Distributor 통신 에러 이벤트
            , (options) => { isConnectedDistributor = false; }
        );
        
        ......
    }
    
    connectToLog(host, port) { // ➍ 그 관리 마이크로서비스 연결
        this.logTcpClient = new tcpClient(
            host
            , port
            , (options) => {}
            , (options) => { this.logTcpClient = null; }
            , (options) => { this.logTcpClient = null; }
        );
        this.logTcpClient.connect();
    }
    
    writeLog(log) { // ➎ 그 패킷 전달
        if (this.logTcpClient) {
            const packet = {
                uri: "/logs",
                method: "POST",
                key: 0,
                params: log
            };
            this.logTcpClient.write(packet);
        } else {
            console.log(log);
        }
    }
}

module.exports = tcpServer;

로그 관리 마이크로서비스 연결용 tcpClient 변수를 선언합니다(➊). API가 호출되면 자식 프로세스에 전달하기 전에 먼저 로그 관리 마이크로서비스로 로그를 전달합니다. 이때 아직 로그 관리 마이크로서비스가 준비되지 않았으면 화면에 로그를 출력합니다(➋, ➎). Distributor에서 로그 관리 마이크로서비스가 접속했다는 정보를 받으면 접속 정보를 이용해 로그 관리 마이크로서비스로 접속을 시도합니다(➌, ➍).

개별 명령 프롬프트 창에서 Distributor, 게이트웨이, 로그 관리 마이크로서비스, 상품 관리 마이크로서비스를 각각 실행해 로그가 정상적으로 처리되는지 확인합니다.

> node distributor.js
> node microservice_goods.js
> node gate.js
> node microservice_logs.js

웹 브라우저에서 http://127.0.0.1:8000/goods를 입력해 상품 조회 API를 호출합니다. 로그 관리 마이크로서비스에 다음과 같이 로그가 출력되는 것을 확인할 수 있습니다.

......
2017-10-3 02:35:08 ::ffff:127.0.0.1 62881 { uri: '/logs',
  method: 'POST',
  key: 0,
  params: '{"uri":"/goods","method":"GET","params":{"key":5}}' }

로그 저장

로그를 분석하려면 분석하기 쉬운 형태로 저장해야 합니다. 일반적으로 많이 사용하는 파일 저장 방식과 빅데이터 솔루션 연동 방법을 알아보겠습니다.

fs 모듈을 이용한 파일 로그 만들기

Node.js에서는 파일 처리와 관련된 기능을 fs 모듈로 제공합니다. fs 모듈에서 많은 파일 처리 기능을 제공하지만, 쓰기용 스트림 생성 함수(createWriteStream)와 쓰기용 스트림을 이용한 파일 저장 기능(write)만 활용해 로그 파일을 저장하겠습니다.