개요 

여러 서비스들의 배치 수행시간을 컨트롤하고 배치 성공, 실패 로그를 수집해야하는 작업이 주어졌다.

해당 업무를 진행 하기 전 테스트를 위하여 진행하고 있던 웹 포트폴리오에서 테스트를 진행하고자 한다.


구성 및 설계

기존은 @shceduled를 사용하여 특정 시간에 배치를 수행하였지만 배치 수행시간을 변경하려면 소스의 수정 및 배포가 강제되었다. 이를 해결하기위해 해당 배치들을 API 형식으로 수정하고 Jenkins의 스캐줄을 사용하여 배치수행을 진행한다.

 

또한 단일 서비스가 아닌 여러 서비스의 배치 통합이 이루어져야 하기 때문에 각각의 Batch 수행에 관련된 로그를 수집하여야 한다. 이를 해결하기위해 Elasticsearch와 Logstash를 사용한다.

 

 


Elasticsearch, Logstash 설치

Docker를 사용하여 설치

# ELK가 세팅된 git 프로젝트 clone
git clone https://github.com/ksundong/docker-elk-kor.git

# logstash 데이터를 확인하기위한 volum 디렉토리 생성
mkdir logs

 

 

logstash데이터 확인을 위한 logs 볼륨 추가

docker-compose.yml

      context: logstash/
      args:
        ELK_VERSION: $ELK_VERSION
    volumes:
      - type: bind
        source: ./logstash/config/logstash.yml
        target: /usr/share/logstash/config/logstash.yml
        read_only: true
      - type: bind
        source: ./logstash/pipeline
        target: /usr/share/logstash/pipeline
        read_only: true
        # logstash input을 확인하기위한 볼륨
      - type: bind
        source: /home/k/ELK/docker-elk-kor/logs
        target: /logs
    ports:
      - "5044:5044"
      - "5000:5000/tcp"
      - "5000:5000/udp"
      - "9600:9600"
    environment:
      LS_JAVA_OPTS: "-Xmx256m -Xms256m"
    networks:
      - elk
    depends_on:
      - elasticsearch

  kibana:
    build:
      context: kibana/
      args:
        ELK_VERSION: $ELK_VERSION
    volumes:
      - type: bind
        source: ./kibana/config/kibana.yml
        target: /usr/share/kibana/config/kibana.yml
        read_only: true
    ports:
      - "5601:5601"
    networks:
      - elk
    depends_on:
      - elasticsearch

networks:
  elk:
    driver: bridge

volumes:
  elasticsearch:

 

 

 

 

logstash 파일 출력 및 기타설정

logstash/pipeline/logstash.conf

input {
# beats 미사용
       # beats {
        #        port => 5044
       # }

        tcp {
                port => 5000
        }
}

## Add your filters / logstash plugins configuration here

output {
        elasticsearch {
                hosts => "elasticsearch:9200"
                index => "logs"
                user => "username"
                password => "password"
                ecs_compatibility => disabled
        }
		# 파일 출력을 위한 설정
        file {
                path => "/logs/test.log"
        }

}

 

 

Logstash, Elasticsearch 기동

docker-compose build && docker-compose up -d

 

 

 

키바나 확인 

http://localhost:5601/app/home#/ 

 

 


Spring boot의 Logback 세팅

module-common의 build.gradle

dependencies {
    // 이미 board에 포함된 의존성임으로 추가하지 않음
    implementation project(":module-common")
    implementation project(":module-normal-board")

	// logback을 사용하여 logstash로 전달하기 위하여 필요한 의존성
    implementation 'net.logstash.logback:logstash-logback-encoder:7.4'
    implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.5.6'
}

 

Logback 세팅

module-main의 ressources에 logback.xml 생성

 

logback.xml

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>

<!-- 콘솔에 로그를 출력하는 콘솔 앱렌더 설정 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <!-- 로그 출력을 위한 레이아웃 설정 -->
    <layout class="ch.qos.logback.classic.PatternLayout">
        <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
    </layout>
</appender>

<!-- 파일에 로그를 저장하는 파일 앱렌더 설정 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/app.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 일별로 로그 파일 롤링 -->
            <fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 보관할 로그 파일의 최대 개수 -->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
        </layout>
    </appender>

	<!-- 로그스태시 세팅, 로컬의 5000포트 사용-->
    <appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <destination>127.0.0.1:5000</destination>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
        <layout class="net.logstash.logback.layout.LogstashLayout">
            <timestampPattern>yyyy-MM-dd' 'HH:mm:ss.SSS</timestampPattern>
        </layout>
    </appender>


<!-- 루트 로거 설정 -->
<root level="INFO">
    <appender-ref ref="CONSOLE" />
<!--    <appender-ref ref="FILE" />-->
</root>

<!-- 로그스태시 사용 -->
<logger name="LOGSTASH">
    <appender-ref ref="LOGSTASH"/>
</logger>

</configuration>

 

 

module-main의 @EnableAsync 설정

API를 호출 했을 때 배치가 오래 걸리린다면(예를들어 1시간 돌아가는배치) request에 대한 timeOut이 발생하기 때문에 비동기로 동작할 수 있도록 세팅

 

package com.portfolio;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication(scanBasePackages = "com.portfolio")
@EnableAsync
public class PortfolioWasApplication {
    public static void main(String[] args) {

        SpringApplication.run(PortfolioWasApplication.class, args);
    }
}

 

 

modue-normal-board의 TestController

package com.portfolio.controller;

import com.portfolio.entity.NormalBoard;
import com.portfolio.repo.NormalBoardRepo;
import com.portfolio.service.NormalBoardService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@RequestMapping("/test")
@RestController
@RequiredArgsConstructor
@Slf4j
public class TestController {
    private final NormalBoardService normalBoardService;
    @PostMapping("/jpaInsertTest")
    @Tag(name = "jpa insert Test")
    @Operation(summary = "jpa insert Test")
    public String jpaInsertTest() {

        NormalBoard board = NormalBoard.builder()
                .title("testTitle")
                .content("testContent")
                .firstRegTime(LocalDateTime.now())
                .lastUpdTime(LocalDateTime.now())
                .firstRegUser("patrache")
                .lastUpdUser("patrache")
                .build();

        log.info("controller start");
        normalBoardService.insertNormalBoard(board);
        return board.toString();
    }

    @GetMapping("/jpaSelectTest")
    @Tag(name = "jpa select Test")
    @Operation(summary = "jpa select Test")
    public List<NormalBoard> jpaSelectTest() {
        List<NormalBoard> boardList = normalBoardService.selectAllBoardList();
        return boardList;
    }
}

 

 

module-normal-board의 NormalBoardServiceImpl

해당 service는 배치가 아니지만 배치라 가정해고 작성, Async가 동작하는것을 확인하기위해 Thread.sleep으로 테스트

package com.portfolio.serviceimpl;

import com.portfolio.entity.NormalBoard;
import com.portfolio.repo.NormalBoardRepo;
import com.portfolio.service.NormalBoardService;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;

@Service
@RequiredArgsConstructor
public class NormalBoardServiceImpl implements NormalBoardService {
    private final NormalBoardRepo boardRepo;
    private Logger logstash = LoggerFactory.getLogger("LOGSTASH");

    @Override
    @Async
    public void insertNormalBoard(NormalBoard board) {
        try {
            Thread.sleep(30000);
            boardRepo.save(board);
        } catch (Exception e) {
            logstash.error("BAT0001||테스트 배치||FAIL||{}", e.toString());
            e.printStackTrace();
        }
        logstash.info("BAT0001||테스트 배치||SUCCESS||ETC");

    }

    @Override
    public List<NormalBoard> selectAllBoardList() {
        return boardRepo.findAll();
    }
}

 

Async 동작 확인

Controller start 로그 30초 이후 Logstash 로그 확인

2024-06-05 16:10:08 INFO  c.p.controller.TestController - controller start
2024-06-05 16:10:09 INFO  c.p.controller.TestController - controller start

2024-06-05 16:10:38 INFO  LOGSTASH - BAT0001||테스트 배치||SUCCESS||ETC
2024-06-05 16:10:39 INFO  LOGSTASH - BAT0001||테스트 배치||SUCCESS||ETC

 


Kibana에서 데이터 확인

http://localhost:5601/
목록 > devtool

 

GET {index}/_search 

 

 


Jenkins 설치 및 배포 

docker run  -p 8088:8080 -p 50000:50000 --restart=on-failure -v jenkins_home:/var/jenkins_home jenkins/jenkins:lts-jdk17

 

 

 

 

+ Recent posts