Logging

참고지식

스택과 힙

  • 스택은 함수가 종료되면 삭제됨
  • 힙은 반영구(instance)영역과 영구(static)영역으로 나눠짐
  • insatnce에는 객체인스턴스가 저장됨, 가비지 콜렉션이 알아서 삭제
    • 파이썬은 레퍼런스 카운트로 삭제, 자바는 가리키는 것이 없으면 알아서 삭제
  • static에는 클래스와 리터럴(immutable)이 저장됨, 삭제불가, 변경불가
    • “Hello” + “world” 사용 시 Hello와 World를 각각 힙에 저장되고 영구 삭제 불가 => “hello {}”, “World” 사용하는 것이 좋음
    • 메모리 때문에 + 연산자는 사용안하는 것이 좋음
    • 자바의 StringBuilder는 instance영역에 문자열을 저장함

Linux 프로그램 설치 방법

  • 바이너리 다운로드 받아서 설치
    • JDK 기반 프로그램 사용 시 java를 두번 깔 필요 없음
    • 경로 설정이 복잡
  • Docker나 Kubernetes에서 컨테이너로 설치
    • 동일한 기반 이미지 사용할 수 있어야함
  • apt나 yum 같은 패키지 관리 도구 이용
    • GPG키 설치 필요
    • apt upgrade 시 문제 발생 가능 -> 버전 고정 필요

Logging

Log

  • 개발자에게 필요한 정보를 전달하기 위해서 작성하는 문자열
  • 필요한 정보를 직접 로깅할 수 도 있고 애플리케이션이 예상하지 않은 동작을 했을 때 error 정보를 전달해주기도 합니다.
  • 프로그래밍 언어에서는 로그를 전달하는 작업을 직접 수행해야 하고 프레임워크에서는 기본적인 로그를 프레임워크가 전달합니다.
  • Log를 출력할 때 콘솔에 출력하는 메서드나 함수 사용을 금기시함
    로그 관리 기능이 없고 로그는 복잡하고 많은 양의 정보를 출력하는 경우가 많은데 콘솔에 출력하는 함수나 메서드는 이러한 기능이 없음

Spring Boot에서의 로깅

  • Spring Boot에서는 spring-boot-starter-web 의존성을 추가하면 자동으로 SLF4J를 통해서 logback 이나 log4j 와 같은 로깅 프레임워크를 사용할 수 있습니다.
  • lombok의 의존성을 추가하면 Logger 객체를 직접 생성하지 않고 @Slf4j 라는 어노테이션 만으로 로깅을 할 수 있습니다.
  • 로그를 출력할 때는 Logger 객체를 이용해서 레벨에 해당하는 메서드를 호출하고 매개변수로 문자열을 넘겨주는데 포맷을 이용해서 문자열을 출력할 때는 +를 이용하지 않고 { }를 이용해서 비워두고 뒤에 매개변수로 데이터를 넘겨서 출력합니다
  • Controller 클래스를 추가해서 로깅
import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class FrontController {

   @GetMapping("/health")
   public String healthCheck() {
       return "OK";
   }

   @GetMapping("/")
   public String index(){
       log.info("lot test");
       return "My Web";
   }
}

로그에 포함되는 내용

  • Timestamp
  • Level(디버깅, 모니터링 용도)
  • 요청경로(리터럴도 포함: 트래픽이나 UI 때문), 메서드

SLF4J

  • SLF4J(Simple Logging Facade for Java)는 파사드 패턴이 적용되어 log4j, logback 같은 로깅에 대한 추상 레이어를 제공하는 로깅 라이브러리
  • 추상화되어 있기 때문에 개발자가 로깅 프레임워크를 지정해주어야 합니다.
  • log4j 라는 로깅 라이브러리를 많이 사용했는데 log4j가 치명적인 보안 이슈가 발생하면서 이를 제거해야 했는데 이 때 제거를 하게되면 다른 코드에 어떤 문제가 발생할 지 알 수 없고 이로 인해 코드의 수정이 너무 많이 발생하기 때문에 추상화 계층을 두고 편리하게 수정을 하기 위해서 사용합니다.
  • 파사드패턴
interface Slf4j {
    public void info(String str)
}

class Log4j implements Slf4j {
    public void info(String str){
        print("Log4j");
    }
}

class LogBack implements Slf4j {
    public void info(String str) {
        print("Log Back");
    }
}

Slf4j log = new Log4j();
log.info();
  • Log4j에 문제가 생기면 new Log4j()만 new LogBack()으로 고치면 됨

Log Level

  • FATAL: 심각한 에러
  • ERROR: 시스템이 정상적인 기능을 못할 때 찍히는 로그
  • WARN: 에러는 아니지만 주의할 필요가 있는 경우
  • INFO: 운영에 참고할 만한 사항 또는 중요 정보를 나타낼 때 사용
  • DEBUG: 개발 단계에서 사용하는 정보
  • TRACE: 모든 레벨에 대한 로깅
  • ALL

로그 출력 설정

  • application.yaml(properties)를 이용하는 방법이 있고 별도의 설정 파일(logback-spring.xml) 파일을 이용할 수 있습니다.

  • xml 설정

    • appender 와 logger로 구성
    • appender 에 로그를 어디에 출력할 지 설정하고 logger는 실제 출력을 위한 것
    • 콘솔, 파일, 데이터베이스, LogStash 등에 출력하는 것이 가능
  • logback-spring.xml 작성

logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
   <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
       <layout class="ch.qos.logback.classic.PatternLayout">
           <Pattern>[%d{yyyy-MM-dd HH:mm:ss}:%-3relative] %-5level ${PID:-} --- [%15.15thread] %-40.40logger{36} : %msg%n</Pattern>
       </layout>
   </appender>

   <root level="debug">
       <appender-ref ref="STDOUT" />
   </root>
</configuration>

다시 실행시켜서 확인하면 표준 로그와 거의 유사하게 출력됨

로그를 파일로 출력

고려할 점

  • 로그는 분석 대상이 될 수 있습니다.
  • 로그는 콘솔에 출력하는 것도 중요하지만 파일이나 데이터베이스에 저장이 되서 나중에 분석을 할 수 있어야 합니다.
  • 애플리케이션에 직접 로그를 로컬에 저장하는 것은 서버가 1대 인 경우는 별 문제가 없지만 여러 대인 경우는 문제가 발생합니다.
  • 애플리케이션 별로 로그를 별도의 파일에 저장하면 나중에 이 파일들을 하나로 만들어야 하는 번거로움이 있습니다.
  • 하나의 애플리케이션을 여러 개의 노드에 배포하는 환경에서는 각 노드에 배포된 애플리케이션의 로그를 모아서 한 번에 저장해 주는 시스템이 필요
  • 카프카 와 같은 시스템을 사용해서 로그를 기록하는 것을 고려
  • 각 애플리케이션은 로그가 발생하면 카프카에게 전달을 하고 카프카는 이를 로그를 기록하는 애플리케이션에게 전달하는 방식을 취할 수 있습니다.

애플리케이션의 로그를 카프카에 전송하고 이를 구독하는 애플리케이션에서 로그를 받아서 파일에 저장한 후 S3에 전송

1)카프카 설정

  • 카프카 설치
    • EC2 인스턴스를 생성

    • Kafka는 9092 와 2181 번 포트를 사용하므로 인바운드 규칙에서 포트 개발

    • Kafka 나 ELK Stack 등은 Java로 만들어진 애플리케이션이라서 Docker 나 Kubernetes를 이용하지 않고 설치할 때는 JVM(JRE)이 설치가 되어 있어야 합니다
      sudo apt install -y openjdk-17-jdk

    • 바이너리 파일 다운로드
      wget https://archive.apache.org/dist/kafka/3.6.0/kafka_2.13-3.6.0.tgz

    • 압축 해제
      tar xvf kafka_2.13-3.6.0.tgz

    • 애플리케이션 사용을 편리하게 하기 위한 작업
      sudo mv kafka_2.13-3.6.0 /opt/kafka

      환경 변수 추가 - nano ~/.bashrc

      export KAFKA_HOME-/opt/kafka
      export PATH=$PATH:$KAFKA_HOME/bin

      적용
      source ~/.bashrc

  • 카프카 환경 설정
    • opt/kafka/config/server.properties 파일을 수정: 아래 내용을 추가하는데 IP 주소는 자신의 EC2 퍼블릭 IP
listerners=PLAINTEXT://:9092
advertised.listeners=PLAINTEXT://IP주소:9092

delete.topic.enable=true
auto.create.topics.enable=true
log.retention.minutes=10
  • 카프카 실행
/opt/kafka/bin/zookeeper-server-start.sh -daemon /opt/kafka/config/zookeeper.properties

jps -vm


/opt/kafka/bin/kafka-server-start.sh -daemon /opt/kafka/config/server.properties

jps -m
  • 토픽 생성 및 확인
kafka-topics.sh --create --bootstrap-server localhost:9092 --topic test

kafka-topics.sh --bootstrap-server localhost:9092 --list
  • 토픽 프로듀스 및 컨슘
$ kafka-console-producer.sh --bootstrap-server localhost:9092 --topic test
>Hello
>World
>Bye
>World
>exit
$ kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning
Hello
World
Bye
World
exit

2)로그를 기록하는 애플리케이션

  • spring web, devtools, lombok, kafka 의존성을 설정한 프로젝트 생성
  • application.yml 파일을 생성하고 카프카 설정을 추가
application.yml
spring:
  kafka:
    bootstrap-servers: 43.202.64.36:9092
    consumer:
      group-id: itstudy
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.serialization.StringDeserializer
    producer:
      key-deserializer: org.apache.kafka.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.serialization.StringDeserializer
  • 프로젝트에 카프카 환경 설정 클래스를 추가(KafkaConfiguration)
import org.apache.kafka.common.serialization.StringSerializer;

import org.apache.kafka.clients.producer.ProducerConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.ProducerFactory;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class KafkaConfiguration {
   //application.properties 나 application.yaml 파일에 있는
   //키의 값을 가져와서 설정
   @Value("${spring.kafka.bootstrap-servers}")
   private String bootstrapServers;
  
   @Bean
   public ProducerFactory<String, String> producerFactory(){
       Map<String, Object> configs = new HashMap<>();
       configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
       configs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
       configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
       return new DefaultKafkaProducerFactory(configs);
   }

@Bean
public KafkaTemplate<String, String> kafkaTemplate(){
   return new KafkaTemplate<>(producerFactory());
   } 
}
  • 로그 전송 클래스
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;


@Service
public class KafkaProducer {
   //토픽이름
   private static final String TOPIC = "log-topic";


   @Autowired
   private KafkaTemplate<String, String> kafkaTemplate;


   //토픽을 전송하는 메서드
   public void sendTopic(String timestamp){
       StringBuilder sb = new StringBuilder("time:");
       sb.append(timestamp);
       kafkaTemplate.send(TOPIC, sb.toString());
   }
}
  • Controller 클래스
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;


import java.util.GregorianCalendar;
import java.util.Calendar;
@RestController
@RequiredArgsConstructor
public class FrontController {
   private final KafkaProducer kafkaProducer;


   @GetMapping("/health")
   public String health() {
       return "OK";
   }


   @GetMapping("/")
   public String index() {
       Calendar cal = new GregorianCalendar();
       kafkaProducer.sendTopic(cal.toString());
       return "MyWeb";
   }
}
  • 실행 한 후 localhost:8080을 브라우저에서 호출

3)kafka로 부터 로그 메시지를 받아서 파일로 저장한 후 S3에 업로드하는 애플리케이션

  • 로그를 전송하는 애플리케이션 과 동일한 의존성을 가진 Spring 프로젝트 생성
  • 이전 프로젝트에서 작성했던 application.yaml 파일을 복사해서 가져옵니다.
server:
 port: 8090
spring:
 kafka:
   bootstrap-servers: 43.202.4.59:9092
   consumer:
     group-id: itstudy
     auto-offset-reset: earliest
     key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
     value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
   producer:
     key-serializer: org.apache.kafka.common.serialization.StringSerializer
     value-serializer: org.apache.kafka.common.serialization.StringSerializer
  • 카프카 환경 설정 클래스도 그대로 사용
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;




import java.util.HashMap;
import java.util.Map;


@Configuration
public class KafkaConfiguration {
   //application.properties 나 application.yaml 파일에 있는
   //키의 값을 가져와서 설정
   @Value("${spring.kafka.bootstrap-servers}")
   private String bootstrapServers;


   @Bean
   public ProducerFactory<String, String> producerFactory(){
       Map<String, Object> configs = new HashMap<>();
       configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
       configs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
       configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
       return new DefaultKafkaProducerFactory(configs);
   }


   @Bean
   public KafkaTemplate<String, String> kafkaTemplate(){
       return new KafkaTemplate<>(producerFactory());
   }
}
  • 토픽으로부터 메시지를 읽어오는 클래스
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;


import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;


@Service
public class KafkaConsumer {
   @KafkaListener(topics="log-topic",groupId="itstudy")
   public void listen(String message) throws Exception {
       System.out.println(message);
       String filename = "log.txt";
       File file = new File(filename);
       FileWriter writer = new FileWriter(file, true);
      
       writer.write(message);
       writer.close();


   }
}
  • 외부에서 S3 버킷을 사용하고자 하면 버킷 사용 권한을 가진 사용자의 키가 필요합니다.
    • AWS의 IAM에 접속
    • 사용자를 생성하는데 S3FullAccess 권한을 연결합니다.
    • 사용자를 생성한 후 [보안 자격 증명]에서 Access Key를 발급받아서 로컬 컴퓨터에 저장합니다.
  • 버킷 생성
    • 버킷을 생성할 때 public access 가 가능하도록 생성
    • ACL 활성화 됨을 선택하고 아래의 퍼블릭 엑세스 차단을 해제
    • 버킷의 권한을 수정
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicListGet",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:List*",
                "s3:Get*",
                "s3:Put*",
                "s3:Delete*"
            ],
            "Resource": [
                "arn:aws:s3:::itstudylogbucket",
                "arn:aws:s3:::itstudylogbucket/*"
            ]
        }
    ]
}
  • 파일을 업로드할 프로젝트의 build.gradle의 dependencies에 AWS 의존성 라이브러리 설정
implementation 'io.awspring.cloud:spring-cloud-starter-aws:2.3.1'
  • 파일을 업로드할 프로젝트의 application.yml 파일에 S3 버킷에 대한 설정 추가
cloud:
 aws:
   credentials:
     access-key: 
     secret-key: 
   s3:
     bucket: itstudylogbucket
   region:
     static: ap-northeast-2
   stack:
     auto: false
  • 업로드할 때 마다 파일 이름을 구분해서 업로드가 되도록 해주는 파일 이름을 생성해주는 메서드를 소유한 클래스
public class CommonUtils {
   private static final String EXETENSION_SEPERATOR = ".";
   private static final String TIME_SEPERATOR = "_";


   //원본 파일을 가지고 실제 업로드되는 파일 이름을 만들어주는 메서드
   public static String fileNameCreate(String fileName){
       int extensionIndex = fileName.lastIndexOf(EXETENSION_SEPERATOR);
       String fileExtension = fileName.substring(extensionIndex);
       String uploadName = fileName.substring(0, extensionIndex);
       String now = String.valueOf(System.currentTimeMillis());


       StringBuilder sb = new StringBuilder(uploadName)
               .append(TIME_SEPERATOR)
               .append(now)
               .append(fileExtension);
       return sb.toString();


   }
}
  • 토픽으로부터 메시지를 읽어오는 클래스
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;


import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;


@Service
public class KafkaConsumer {
   @KafkaListener(topics="log-topic",groupId="itstudy")
   public void listen(String message) throws Exception {
       System.out.println(message);
       String filename = "log.txt";
       File file = new File(filename);
       FileWriter writer = new FileWriter(file, true);
      
       writer.write(message);
       writer.close();


   }
}
  • 로그 소스가 많을 경우 날짜 별로 디렉터리 만들어서 정리
  • 버킷에 똑같은 이름 방지: 이름 + 날짜시간, 이름 + UUID