티스토리 뷰

개요

  • Graylog는 여러 곳에 분산된 마이크로서비스들의 로그를 중앙에서 수집하고 관리할 수 있게 해주는 오픈 소스 통합 중앙 로그 관제 플랫폼이다. Key-Value 기반의 독자적인 GELF 로그 포맷을 지원하며 v4.0.0 버전부터는 로그 저장소로 Elasticsearch 7.10을 지원한다. 이번 글에서는 Spring Boot 기반 프로젝트에서 GELF UDP 로그를 전송하는 방법을 설명하고자 한다.

구조화된 로그가 필요한 이유

  • AI의 등장과 함께 이제 애플리케이션 로그는 사람이 보는 정보가 아니라 기계가 분석하는 정보가 되었다. 기계가 로그를 분석하려면 기존의 전통적인 라인 단위의 텍스트 로그로는 부족하다. 잘 설계된 구조화된 로그를 전송하면 기계에 의해 분석되어 유의미한 데이터로 가공될 수 있다. 구조화된 GELF 로그 포맷은 그러한 목적으로 탄생하였다. [관련 링크]

앞서 읽으면 좋은 글

build.gradle.kts

  • 프로젝트 루트의 build.gradle.kts에 아래 내용을 추가한다.
dependencies {
    implementation("io.github.microutils:kotlin-logging-jvm:3.0.3")
    implementation("de.siegmar:logback-gelf:4.0.2")
}
  • logback-gelf 아티팩트는 다양한 로깅 라이브러리에서 GELF(TCP/UDP) 로깅을 가능하게 해준다. 본 예제는 Spring Boot 기반이므로 기본 로깅 라이브러리인 Logback을 기준으로 설명한다.
  • 원래 오랫동안 biz.paluch.logging:logstash-gelf 라이브러리를 사용했었는데, Graylog 인스턴스의 IP 주소가 변경되었을 경우 애플리케이션 재시작 전까지UDP 로그가 유실되는 이슈가 있어 de.siegmar:logback-gelf 라이브러리로 변경했다.

로깅 작성 예

import mu.NamedKLogging

class Foo {

    companion object : NamedKLogging("GELF_UDP")

    fun doSomething(): {

        logger.info("DO_SOMETHING_FINISHED")
    }
}
  • GELF 로깅을 전송할 클래스에서는 위와 같이 NamedKLogging("GELF_UDP")으로 이름을 직접적으로 명시하여 logger 오브젝트를 생성한다. 위 예제에서는 일반 로거와의 쉬운 구분을 위해 로거의 이름으로 GELF_UDP를 명시해주었는데 명명은 자유롭게 해도 무방하다. (일반적으로 로거의 이름은 클래스를 포함한 패키지명으로 명명하는 것이 관행이다.)
  • logger.info({message})가 실행되는 즉시 GELF 형식의 로그가 Graylog 서버로 전송된다. 아규먼트로 전달된 message 값은 GELF 포맷의 short_message, full_message 필드에 담겨 전송된다. 나머지 필수 필드는 logback-gelf 라이브러리에 의해 자동 생성되어 전송된다. 실제 전송되는 필드 정보는 아래와 같다.
{
  "LoggerName": "GELF",
  "Severity": "INFO",
  "SourceClassName": "com.jsonobject.example.Foo",
  "SourceLineNumber": 6,
  "SourceMethodName": "doSomething",
  "SourceSimpleClassName": "Foo",
  "Thread": "main",
  "Time": "2017-09-06 17:44:33,0919",
  "facility": "logstash-gelf",
  "message": "DO_SOMETHING_FINISHED",
  "full_message": "DO_SOMETHING_FINISHED",
  "level": 6,
  "source": "jsonobejct",
  "timestamp": "2017-09-06T08:44:33.919Z"
}
  • GELF 메시지의 원본은 위와 같은 JSON 문자열이다. 이 문자열은 어펜더에 의해 전송 전 GZIP으로 압축되는데 UDP의 경우 압축된 총 크기가 8,192바이트를 초과할 경우 분할 전송된다. 이 경우 문제는 메시지 인풋이 실행 중인 Graylog 서버가 로드 밸런싱되고 있을 경우 분할 전송된 패킷 조차 로드 밸런싱되어 메시지가 유실된다. 따라서 압축된 로그 메시지의 크기가 8,192바이트를 초과하지 않도록 설계하는 것이 바람직하다. 애플리케이션에서 발생하는 대부분의 로그는 이 크기 안에서 소화가 가능하다.

커스텀 필드 전송

  • GELF는 필드명의 앞에 _가 붙는 커스텀 필드(additional field)를 지원한다. 아래와 같이 커스텀 필드를 추가할 수 있다.
MDC.put("some_field", "some_value");
MDC.put("another_field", "another_value");

log.info("SOME_METHOD_FINISHED");

MDC.clear();
  • 위 방법은 Slf4j가 제공하는 MDC를 이용한 것이다. MDC는 쓰레드 단위의 생명주기를 가지며 원하는 추가 필드를 담을 수 있다. 번거롭지만 쓰레드 풀 환경에서 엉뚱한 커스텀 필드가 남겨지는 것을 예방하기 위해 반드시 MDC.clear()로 로깅을 마무리해야 한다.
  • MDC 클래스는 ThreadLocal를 범위로 가지는 것을 항상 명심하고 있어야 한다. 즉, 현재 쓰레드에서 다른 쓰레드로 전환하는, 비동기 로직을 호출할 경우 현재 쓰레드의 MDC 정보가 전환된 쓰레드로 전달되지 않는다. 이 문제에 대한 해결책은 본 블로그의 이 글을 참고하도록 한다.

Logback 환경설정

  • 프로젝트 루트의 /src/main/resources/logback-spring.xml 파일을 아래와 같이 작성한다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>

<configuration>

    <appender name="TEXT_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <charset>UTF-8</charset>
            <Pattern>%d %-4relative [%thread] %-5level %logger{35} - %msg%n</Pattern>
        </encoder>
    </appender>

    <appender name="GELF_UDP" class="de.siegmar.logbackgelf.GelfUdpAppender">
        <graylogHost>{hostname}</graylogHost>
        <graylogPort>{port}</graylogPort>
        <maxChunkSize>8192</maxChunkSize>
        <useCompression>true</useCompression>
        <messageIdSupplier class="de.siegmar.logbackgelf.MessageIdSupplier"/>
        <encoder class="de.siegmar.logbackgelf.GelfEncoder">
            <includeRawMessage>false</includeRawMessage>
            <includeMarker>true</includeMarker>
            <includeMdcData>true</includeMdcData>
            <numbersAsString>false</numbersAsString>
            <staticField>appender=GELF_UDP</staticField>
        </encoder>
    </appender>

    <logger name="GELF_UDP" level="INFO" additivity="false">
        <appender-ref ref="GELF_UDP"/>
    </logger>

    <root>
        <level value="INFO"/>
        <appender-ref ref="TEXT_CONSOLE"/>
    </root>

</configuration>
  • graylogHost에는 Graylog 서버의 호스트명(도메인 또는 IP 주소)을 입력한다.
  • graylogPort에는 Graylog 서버에서 실행 중인 GELF UDP Input의 포트명을 입력한다.
  • 로거 이름이 GELF_UDP인 로그들은 모두 GELF_UDP 어펜더를 통해 전송되도록 작성했다.
  • 로거에 additivity="false"(기본값은 true) 옵션을 추가적으로 명시하면 해당 로그는 GELF_UDP 어펜더를 통해서만 전송되고 상위 어펜더까지 전파되지 않는다. 이를 통해 불필요한 로그의 이중 전송이나 출력을 방지할 수 있다.

애플리케이션 기동

  • Amazon 생태계에서 UDP 로깅을 하고자 할 경우, 2022-11-01 현재 기준으로 AWS NLBUDP + IPv6 패킷 전송을 지원하지 않는다. 이에 따라 로그가 유실을 예방하려면 애플리케이션 레벨에서 IPv4로만 UDP 패킷이 전송되도록 강제하는 -Djava.net.preferIPv4Stack=true 옵션을 아래와 같이 추가해야 한다.
$ java ${JAVA_OPTS} -XX:+AlwaysPreTouch -Djava.security.egd=file:/dev/./urandom -Djava.net.preferIPv4Stack=true -jar /app.jar

참고 글

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함