본문 바로가기
WIKI 기술 지식 베이스

Java 로그 수집

원문 보기 위키 갱신

Datadog로 로그를 보내려면 파일에 로그를 기록하고 Datadog Agent로 그 파일을 tail하세요.

일반적인 Java 로그의 스택 트레이스는 여러 줄로 나뉘어 있어서 원래 로그 이벤트와 연관 짓기 어려워요. 예를 들어:

//4 events generated when only one is expected!
Exception in thread "main" java.lang.NullPointerException
        at com.example.myproject.Book.getTitle(Book.java:16)
        at com.example.myproject.Author.getBookTitles(Author.java:25)
        at com.example.myproject.Bootstrap.main(Bootstrap.java:14)

이 문제를 해결하려면 로깅 라이브러리가 JSON 형식으로 로그를 생성하게 구성하세요. JSON으로 로깅하면:

  • 스택 트레이스가 로그 이벤트에 제대로 감싸지도록 보장해요.
  • 모든 로그 이벤트 속성(심각도, 로거 이름, 스레드 이름 등)이 제대로 추출되도록 보장해요.
  • 어떤 로그 이벤트에든 붙일 수 있는 Mapped Diagnostic Context (MDC) 속성에 접근할 수 있어요.
  • 사용자 정의 파싱 규칙이 필요 없어요.

다음 지침은 Log4j, Log4j 2, Logback 로깅 라이브러리의 설정 예시를 보여줘요.

출처: 문서

본문

로거 구성하기

JSON 형식

{% tab title="Log4j" %} Log4j의 경우 SLF4J 모듈 log4j-over-slf4j를 Logback과 함께 사용해 JSON 형식으로 로그하세요. log4j-over-slf4j는 애플리케이션의 Log4j를 깨끗하게 대체하므로 코드를 전혀 바꿀 필요가 없어요.

  1. pom.xml 파일에서 log4j.jar 의존성을 log4j-over-slf4j.jar 의존성으로 바꾸고 Logback 의존성을 추가하세요. 예를 들어:

    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>log4j-over-slf4j</artifactId>
      <version>1.7.32</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.9</version>
    </dependency>
    <dependency>
      <groupId>net.logstash.logback</groupId>
      <artifactId>logstash-logback-encoder</artifactId>
      <version>6.6</version>
    </dependency>
    
  2. logback.xml에서 JSON 레이아웃을 사용해 appender를 구성하세요. 파일과 콘솔에 대한 예시 구성은 다음과 같아요.

파일의 경우:

<configuration>
  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>logs/app.log</file>
    <encoder class="net.logstash.logback.encoder.LogstashEncoder" />
  </appender>

  <root level="INFO">
    <appender-ref ref="FILE"/>
  </root>
</configuration>

콘솔의 경우:

<configuration>
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
      <encoder class="ch.qos.logback.classic.encoder.JsonEncoder"/>
  </appender>

  <root>
    <level value="DEBUG"/>
      <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

{% /tab %}

{% tab title="Log4j 2" %} Log4j 2에는 JSON 레이아웃이 포함돼 있어요.

  1. log4j2.xml에서 JSON 레이아웃을 사용해 appender를 구성하세요. 파일과 콘솔 appender의 예시 구성을 참고하세요. Log4j 플러그인에 대한 자세한 설명은 Log4j Plugin reference를 참고하세요.

{% collapsible-section %}

파일 appender

log4j2.xml 파일에서:

<?xml version="1.0" encoding="UTF-8"?>
  <Configuration>
    <Appenders>
      <File name="FILE" fileName="logs/app.log" >
        <JsonTemplateLayout eventTemplateUri="classpath:MyLayout.json"/>
      </File>
    </Appenders>
    <Loggers>
      <Root level="INFO">
        <AppenderRef ref="FILE"/>
      </Root>
    </Loggers>
  </Configuration>

{% /collapsible-section %}

{% collapsible-section %}

콘솔 appender

log4j2.xml 파일에서:

  <?xml version="1.0" encoding="UTF-8"?>
  <Configuration>
    <Appenders>
      <Console name="console" target="SYSTEM_OUT">
        <JsonTemplateLayout eventTemplateUri="classpath:MyLayout.json"/>
      </Console>
    </Appenders>
    <Loggers>
      <Root level="INFO">
        <AppenderRef ref="console"/>
      </Root>
    </Loggers>
  </Configuration>

{% /collapsible-section %}

Java 프로젝트의 src/main/resources 디렉터리에 JSON 레이아웃 템플릿 파일(MyLayout.json 등)을 추가하세요. 예를 들어:

{
   "timestamp":{
      "$resolver":"timestamp",
      "pattern":{
         "format":"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
         "timeZone":"UTC"
      }
   },
   "status":{
      "$resolver":"level",
      "field":"name"
   },
   "thread_name":{
      "$resolver":"thread",
      "field":"name"
   },
   "logger_name":{
      "$resolver":"logger",
      "field":"name"
   },
   "message":{
      "$resolver":"message",
      "stringified":true
   },
   "exception_class":{
      "$resolver":"exception",
      "field":"className"
   },
   "exception_message":{
      "$resolver":"exception",
      "field":"message"
   },
   "stack_trace":{
      "$resolver":"exception",
      "field":"stackTrace",
      "stackTrace":{
         "stringified":true
      }
   },
   "host":"${hostName}",
   "service":"${env:DD_SERVICE}",
   "version":"${env:DD_VERSION}",
   "dd.trace_id":{
      "$resolver":"mdc",
      "key":"dd.trace_id"
   },
   "dd.span_id":{
      "$resolver":"mdc",
      "key":"dd.span_id"
   }
}

pom.xml에 JSON 레이아웃 의존성을 추가하세요. 예를 들어:

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.17.1</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.13.0</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.13.0</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.13.0</version>
</dependency>

{% /tab %}

{% tab title="Logback" %} Logback에서 JSON 형식 로그에 logstash-logback-encoder를 사용하세요.

  1. logback.xml에서 JSON 레이아웃을 사용해 파일 appender를 구성하세요. 예를 들어:

    <configuration>
      <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>logs/app.log</file>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder" />
      </appender>
    
      <root level="INFO">
        <appender-ref ref="FILE"/>
      </root>
    </configuration>
    
  2. pom.xml 파일에 Logstash 인코더 의존성을 추가하세요. 예를 들어:

    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.9</version>
    </dependency>
    <dependency>
      <groupId>net.logstash.logback</groupId>
      <artifactId>logstash-logback-encoder</artifactId>
      <version>6.6</version>
    </dependency>
    

{% /tab %}

{% tab title="Tinylog" %} 공식 Tinylog 문서를 기반으로 JSON writer 구성을 만드세요.

tinylog.properties 파일에서 다음 형식을 사용하세요:

writer                     = json
writer.file                = log.json
writer.format              = LDJSON
writer.level               = info
writer.field.level         = level
writer.field.source        = {class}.{method}()
writer.field.message       = {message}
writer.field.dd.trace_id   = {context: dd.trace_id}
writer.field.dd.span_id    = {context: dd.span_id}
writer.field.dd.service    = {context: dd.service}
writer.field.dd.version    = {context: dd.version}
writer.field.dd.env        = {context: dd.env}

{% /tab %}

원시 형식

{% tab title="Log4j" %} log4j.xml에서 파일 appender를 구성하세요. 예를 들어:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration>

  <appender name="FILE" class="org.apache.log4j.FileAppender">
    <param name="File" value="logs/app.log"/>
    <param name="Append" value="true"/>

    <layout class="org.apache.log4j.PatternLayout">
      <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5p %C:%L - %X{dd.trace_id} %X{dd.span_id} - %m%n"/>
    </layout>
  </appender>

  <root>
    <priority value="INFO"/>
    <appender-ref ref="FILE"/>
  </root>

</log4j:configuration>

{% /tab %}

{% tab title="Log4j 2" %} log4j2.xml에서 파일 appender를 구성하세요. 예를 들어:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
  <Appenders>
    <File name="FILE" fileName="logs/app.log">
      <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %C:%L - %X{dd.trace_id} %X{dd.span_id} - %m%n"/>
    </File>
  </Appenders>

  <Loggers>
    <Root level="INFO">
      <AppenderRef ref="FILE"/>
    </Root>
  </Loggers>
</Configuration>

{% /tab %}

{% tab title="Logback" %} logback.xml에서 파일 appender를 구성하세요. 예를 들어:

<configuration>
  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>${dd.test.logfile}</file>
    <append>false</append>
    <immediateFlush>true</immediateFlush>

    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5p %C:%L - %X{dd.trace_id} %X{dd.span_id} - %m%n</pattern>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="FILE"/>
  </root>
</configuration>

{% /tab %}

{% tab title="Tinylog" %} 공식 Tinylog 문서를 기반으로 파일로 출력하는 writer 구성을 만드세요.

tinylog.properties 파일에서 다음 형식을 사용하세요:

writer          = file
writer.level    = debug
writer.format   = {level} - {message} - "dd.trace_id":{context: dd.trace_id} - "dd.span_id":{context: dd.span_id}
writer.file     = log.txt

{% /tab %}

로그에 트레이스 ID 주입하기

이 애플리케이션에서 APM이 활성화되어 있다면 트레이스 ID 주입을 활성화해 로그와 트레이스를 연관 지을 수 있어요. Java 로그와 트레이스 연결을 참고하세요.

Datadog CorrelationIdentifier API로 트레이스 ID를 수동 주입한다면 빌드에 dd-trace-api 의존성을 추가해야 해요. 의존성 스니펫과 코드 예시는 Java 로그와 트레이스 연결을 참고하세요.

로그와 트레이스를 연관 짓지 않는다면 앞선 구성 예시의 로그 패턴에서 MDC 플레이스홀더(%X{dd.trace_id} %X{dd.span_id})를 제거하세요.

예를 들어 Log4j 2를 사용하지만 로그와 트레이스를 연관 짓지 않는다면 예시 로그 레이아웃 템플릿 MyLayout.json에서 다음 블록을 제거하세요:


"dd.trace_id":{
   "$resolver":"mdc",
   "key":"dd.trace_id"
},
"dd.span_id":{
   "$resolver":"mdc",
   "key":"dd.span_id"
}

Datadog Agent 구성하기

log collection이 활성화되면 custom log collection을 설정해 로그 파일을 tail하고 Datadog로 보내세요.

  1. conf.d/ Agent 구성 디렉터리에 java.d/ 폴더를 만드세요.

  2. java.d/에 다음 내용으로 conf.yaml 파일을 만드세요:

    #Log section
    logs:
    
      - type: file
        path: "<path_to_your_java_log>.log"
        service: <service_name>
        source: java
        sourcecategory: sourcecode
        # For multiline logs, if they start by the date with the format yyyy-mm-dd uncomment the following processing rule
        #log_processing_rules:
        #  - type: multi_line
        #    name: new_log_start_with_date
        #    pattern: \d{4}\-(0?[1-9]|1[012])\-(0?[1-9]|[12][0-9]|3[01])
    
  3. Agent를 재시작하세요.

  4. Agent의 status 하위 명령을 실행하고 Checks 섹션에서 java를 찾아 로그가 Datadog로 성공적으로 제출됐는지 확인하세요.

로그가 JSON 형식이라면 Datadog는 로그 속성을 추출하기 위해 로그 메시지를 자동으로 파싱해요. Log Explorer를 사용해 로그를 보고 문제를 해결하세요.

Agent로 로그 직접 스트리밍하기

애플리케이션이 접근할 수 없거나 파일에 로그할 수 없는 머신에서 실행되는 예외적인 경우에는 Datadog 또는 Datadog Agent로 로그를 직접 스트리밍할 수 있어요. 이는 애플리케이션이 연결 문제를 처리해야 하므로 권장되는 설정은 아니에요.

Datadog로 로그를 직접 스트리밍하려면:

  1. 코드에 Logback 로깅 라이브러리를 추가하거나, 현재 로거를 Logback으로 브리지하세요.
  2. Logback을 구성해 Datadog로 로그를 보내세요.

Java 로깅 라이브러리에서 Logback으로 브리지하기

이미 Logback을 사용 중이 아니라면 대부분의 일반적인 로깅 라이브러리를 Logback으로 브리지할 수 있어요.

{% tab title="Log4j" %} SLF4J 모듈 log4j-over-slf4j를 Logback과 함께 사용해 로그를 다른 서버로 보내세요. log4j-over-slf4j는 애플리케이션의 Log4j를 깨끗하게 대체하므로 코드를 전혀 바꿀 필요가 없어요.

  1. pom.xml 파일에서 log4j.jar 의존성을 log4j-over-slf4j.jar 의존성으로 바꾸고 Logback 의존성을 추가하세요. 예를 들어:
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>log4j-over-slf4j</artifactId>
      <version>1.7.32</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.9</version>
    </dependency>
    <dependency>
      <groupId>net.logstash.logback</groupId>
      <artifactId>logstash-logback-encoder</artifactId>
      <version>6.6</version>
    </dependency>
    
  2. Logback을 구성하세요.

참고: 이 변경의 결과로 Log4j 구성 파일은 더 이상 읽히지 않아요. Log4j translator로 log4j.properties 파일을 logback.xml로 마이그레이션하세요. {% /tab %}

{% tab title="Log4j 2" %} Log4j 2는 원격 호스트로 로깅할 수 있지만 로그에 API 키를 접두사로 붙이는 기능은 제공하지 않아요. 따라서 SLF4J 모듈 log4j-over-slf4j와 Logback을 사용하세요. log4j-to-slf4j.jar는 애플리케이션의 Log4j 2를 깨끗하게 대체하므로 코드를 전혀 바꿀 필요가 없어요. 사용하려면:

  1. pom.xml 파일에서 log4j.jar 의존성을 log4j-over-slf4j.jar 의존성으로 바꾸고 Logback 의존성을 추가하세요. 예를 들어:
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-to-slf4j</artifactId>
        <version>2.17.1</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.9</version>
    </dependency>
    <dependency>
        <groupId>net.logstash.logback</groupId>
        <artifactId>logstash-logback-encoder</artifactId>
        <version>6.6</version>
    </dependency>
    
  2. Logback을 구성하세요.

참고:

{% /tab %}

Logback 구성하기

Datadog는 TCP로 Datadog intake에 직접 로그를 보내는 것을 지원하지 않아요. 대신 Logback을 로컬 Datadog Agent로 구성하면 Agent가 자동 강화와 함께 HTTPS로 로그를 Datadog에 전달해요.

  1. 로컬 Datadog Agent를 설치하세요 (v6+ / v7+).

  2. datadog.yaml에서 로그 수집을 활성화하고 Agent가 HTTPS로 로그를 전달하는지 확인하세요(HTTPS는 Agent v6.19+/v7.19+ 이후의 기본 전송 방식):

    logs_enabled: true
    logs_config:
      # HTTPS is the default. Keep or set this to force HTTPS forwarding.
      force_use_http: true
      # (Optional) auto-detect multi-line patterns
      auto_multi_line_detection: true
    
  3. Agent에서 로그 수집을 활성화하세요.

    # /etc/datadog-agent/conf.d/logback.d/conf.yaml
    logs:
      - type: tcp
        port: 10518           # Port the Agent will listen on
        service: my-java-app  # Your service name (unified service tagging)
        source: java          # Or a more specific source, e.g., "logback"
    
  4. 변경 사항을 적용하려면 Agent를 재시작하세요.

  5. Logback을 구성해 Agent로 로그를 보내세요. logback.xml에서 logstash-logback-encoder TCP appender를 사용해 로그를 Agent로 전달하세요:

    <configuration>
      <appender name="DD_TCP_JSON" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <destination>localhost:10518</destination>
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
          <providers>
            <timestamp/>
            <pattern>
              <pattern>
                {
                  "message": "%message",
                  "level": "%level",
                  "logger": "%logger",
                  "service": "${DD_SERVICE:-my-java-app}",
                  "env": "${DD_ENV:-prod}",
                  "version": "${DD_VERSION:-1.0.0}",
                  "dd.trace_id": "%X{dd.trace_id}",
                  "dd.span_id": "%X{dd.span_id}"
                }
              </pattern>
            </pattern>
            <arguments/>
            <stackTrace/>
          </providers>
        </encoder>
      </appender>
    </configuration>
    

그런 다음 루트 로거에서 이를 참조하세요:

<root level="INFO">
  <appender-ref ref="DD_TCP_JSON"/>
</root>
  1. 로그 전달을 확인하세요. datadog-agent status를 실행해 TCP 리스너를 확인하고, Logs Explorer에서 서비스로 태그된 항목을 확인하세요.

더 나아가기

로그 이벤트를 컨텍스트 속성으로 강화하세요.

키 값 파서 사용하기

key value parser는 어떤 로그 이벤트에서든 인식되는 <KEY>=<VALUE> 패턴을 추출해요.

Java에서 로그 이벤트를 강화하려면 코드에서 메시지를 다시 쓰고 <KEY>=<VALUE> 시퀀스를 도입할 수 있어요.

예를 들어 다음과 같은 경우:

logger.info("Emitted 1001 messages during the last 93 seconds for customer scope prod30");

다음으로 바꿀 수 있어요:

logger.info("Emitted quantity=1001 messages during the last durationInMs=93180 ms for customer scope=prod30");

키 값 파서를 활성화하면 각 쌍이 JSON에서 추출돼요:

{
  "message": "Emitted quantity=1001 messages during the last durationInMs=93180 ms for customer scope=prod30",
  "scope": "prod30",
  "durationInMs": 93180,
  "quantity": 1001
}

따라서 scope를 필드로, durationInMs와 quantity를 로그 측정값으로 활용할 수 있어요.

MDC

로그를 강화하는 또 다른 방법은 Java의 Mapped Diagnostic Contexts (MDC)를 사용하는 거예요.

SLF4J를 사용한다면 다음 Java 코드를 사용하세요:

...
MDC.put("scope", "prod30");
logger.info("Emitted 1001 messages during the last 93 seconds");
...

이 JSON을 생성하려면:

{
  "message": "Emitted 1001 messages during the last 93 seconds",
  "scope": "prod30"
}

참고: MDC는 문자열 유형만 허용하므로 숫자 값 측정항목에는 사용하지 마세요.

더 알아보기 (Learn more)