Structured Logging in Spring Boot
1. 개요
로그 기록은 모든 소프트웨어 애플리케이션의 필수 기능입니다. 애플리케이션의 런타임 동안 발생하는 오류, 경고 및 기타 이벤트를 기록하여 애플리케이션의 동작을 추적하는 데 도움이 됩니다.
기본적으로 Spring Boot 애플리케이션은 비구조화되고 인간이 읽을 수 있는 로그를 생성합니다. 이러한 로그는 개발자에게 유용하지만, 로그 집계 도구에 의해 쉽게 파싱되거나 분석되지 않습니다. 구조화된 로깅은 이러한 한계를 해결합니다.
이 튜토리얼에서는 Spring Boot 버전 3.4.0에서 도입된 기능을 활용하여 구조화된 로깅을 구현하는 방법을 배웁니다.
2. Maven 의존성
우선, pom.xml에 spring-boot-starter 를 추가하여 Spring Boot 프로젝트를 부트스트랩하겠습니다:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
위의 의존성은 일반적인 Spring Boot 애플리케이션에서 자동 구성 및 로깅을 지원합니다.
3. Spring Boot 기본 로그
여기 Spring Boot의 기본 로그 예시가 있습니다:
INFO 22059 --- [ main] c.b.s.StructuredLoggingApp : No active profile set, falling back to 1 default profile: "default"
INFO 22059 --- [ main] c.b.s.StructuredLoggingApp : Started StructuredLoggingApp in 2.349 seconds (process running for 3.259)
이 로그는 유익하지만, Elasticsearch와 같은 도구에 의해 쉽게 수집되거나 메트릭 분석을 위해 분석할 수 없습니다. JSON과 같은 구조화된 로깅 형식이 이러한 문제를 해결하여 로그 내용의 표준화를 가능하게 합니다.
4. 구성
Spring Boot 버전 3.4.0부터 구조화된 로깅은 내장되어 있으며 Elastic Common Schema (ECS), Graylog Extended Log Format (GELF), Logstash JSON 형식과 같은 형식을 지원합니다.
구조화된 로깅을 application.properties 파일에서 직접 구성할 수 있습니다.
4.1. Elastic Common Schema
Elastic Common Schema (ECS)는 표준화된 JSON 기반 로그 형식으로, Elasticsearch 및 Kibana를 매끄럽게 통합합니다.
우리의 애플리케이션에서 ECS를 구성하기 위해, application.properties 파일에 다음 속성을 추가합시다:
logging.structured.format.console=ecs
하나의 예시 출력은 다음과 같습니다:
{
"@timestamp": "2024-12-19T01:17:47.195098997Z",
"log.level": "INFO",
"process.pid": 16623,
"process.thread.name": "main",
"log.logger": "com.baeldung.springstructuredlogging.StructuredLoggingApp",
"message": "Started StructuredLoggingApp in 3.15 seconds (process running for 4.526)",
"ecs.version": "8.11"
}
출력은 Elasticsearch와 Kibana에서 쉽게 파싱할 수 있는 키-값 쌍으로 구성되어 있습니다.
또한, ECS 로그를 개선하기 위해 서비스 이름, 환경 및 노드 이름과 같은 필드를 추가하여 가시성을 향상시킬 수 있습니다:
logging.structured.ecs.service.name=MyService
logging.structured.ecs.service.version=1
logging.structured.ecs.service.environment=Production
logging.structured.ecs.service.node-name=Primary
새로운 출력은 다음과 같습니다:
{
"@timestamp": "2024-12-19T01:25:15.123108416Z",
"log.level": "INFO",
"process.pid": 18763,
"process.thread.name": "main",
"service.name": "BaeldungService",
"service.version": "1",
"service.environment": "Production",
"service.node.name": "Primary",
"log.logger": "com.baeldung.springstructuredlogging.StructuredLoggingApp",
"message": "Started StructuredLoggingApp in 3.378 seconds (process running for 4.376)",
"ecs.version": "8.11"
}
출력에는 application.properties 파일에서 정의한 서비스 정보가 포함되어 있습니다.
4.2. Graylog Extended Log Format
Graylog Extended Log Format (GELF)는 또 다른 JSON 기반의 지원되는 구조화 로그 형식입니다. 우리의 application.properties 파일에서 이를 활성화해 봅시다:
logging.structured.format.console=gelf
GELF 형식은 ECS 형식과 유사하지만 속성 이름이 다릅니다:
{
"version": "1.1",
"short_message": "Started StructuredLoggingApp in 2.77 seconds (process running for 3.89)",
"timestamp": 1734572549.172,
"level": 6,
"_level_name": "INFO",
"_process_pid": 23929,
"_process_thread_name": "main",
"_log_logger": "com.baeldung.springstructuredlogging.StructuredLoggingApp"
}
ECS 구성과 마찬가지로, application.properties 파일에서 호스트 및 서비스 버전을 정의하여 출력을 향상시킬 수 있습니다:
logging.structured.gelf.host=MyService
logging.structured.gelf.service.version=1
이렇게 함으로써 로그에 호스트 및 서비스의 키-값 쌍이 추가됩니다.
4.3. Logstash 형식
Logstash 형식도 기본적으로 지원되며, 로그를 이 형식으로 구조화하기 위해 application.properties에 지정할 수 있습니다:
logging.structured.format.file=logstash
샘플 구조화 로그는 다음과 같습니다:
{
"@timestamp": "2024-12-19T02:49:33.017851728+01:00",
"@version": "1",
"message": "Started StructuredLoggingApp in 2.749 seconds (process running for 3.605)",
"logger_name": "com.baeldung.springstructuredlogging.StructuredLoggingApp",
"thread_name": "main",
"level": "INFO",
"level_value": 20000
}
위 형식은 Logstash 형식을 지원하는 로그 집계 도구를 통해 쉽게 분석할 수 있습니다.
4.4. 추가 정보
구조화된 로그에 Mapped Diagnostic Context (MDC) 클래스를 사용하여 더 많은 정보를 추가할 수 있습니다. 예를 들어, 로그에 userId를 추가하여 userId별로 로그를 필터링할 수 있습니다:
private static final Logger LOGGER = LoggerFactory.getLogger(CustomLog.class);
public void additionalDetailsWithMdc() {
MDC.put("userId", "1");
MDC.put("userName", "Baeldung");
LOGGER.info("Hello structured logging!");
MDC.remove("userId");
MDC.remove("userName");
}
위 코드에서 각 항목을 제거하여 MDC 컨텍스트를 정리하여 메모리 누수를 방지합니다.
사용자의 세부 정보로 로그 출력을 봅시다:
{
"@timestamp": "2024-12-19T07:52:30.556819106+01:00",
"@version": "1",
"message": "Hello structured logging!",
"logger_name": "com.baeldung.springstructuredlogging.CustomLog",
"thread_name": "main",
"level": "INFO",
"level_value": 20000,
"userId": "1",
"userName": "Baeldung"
}
여기에서 로그 메시지에 추가 정보가 포함되며, userId에 따라 로그를 쉽게 필터링할 수 있습니다. MDC 클래스를 사용하여 로그에 더 많은 속성을 추가할 수 있습니다.
또한 유사한 목적을 달성하기 위해 유창한 로깅 API를 사용할 수 있습니다:
public void additionalDetailsUsingFluentApi() {
LOGGER.atInfo()
.setMessage("Hello Structure logging!")
.addKeyValue("userId", "1")
.addKeyValue("userName", "Baeldung")
.log();
}
이 접근 방식은 더 간결하며, 컨텍스트 청소를 자동으로 처리하여 오류의 가능성을 줄입니다.
4.5. 사용자 정의 로그 형식
또한 자체 사용자 정의 구조 로그 형식을 정의하여 application.properties에서 사용할 수 있습니다. 이는 지원되는 로그 형식이 사용 사례에 적합하지 않을 때 유용할 수 있습니다.
우선, StructuredLogFormatter 인터페이스를 구현하고 그 format() 메서드를 오버라이드해야 합니다:
class MyStructuredLoggingFormatter implements StructuredLogFormatter<ILoggingEvent> {
@Override
public String format(ILoggingEvent event) {
return "time=" + event.getTimeStamp() + " level=" + event.getLevel() + " message=" + event.getMessage() + "\n";
}
}
여기에서 우리의 사용자 정의 포맷은 표준 JSON이 아닌 텍스트 형식입니다. 이는 JSON, XML 등 임의의 형식으로 로그를 구조화할 수 있는 유연성을 제공합니다.
다음으로, application.properties에서 우리의 사용자 정의 구성을 정의해 봅시다:
logging.structured.format.console=com.baeldung.springstructuredlogging.MyStructuredLoggingFormatter
여기서 우리는 MyStructuredLoggingFormatter의 전체적인 클래스를 정의합니다.
로그 출력은 다음과 같습니다:
time=1734598194538 level=INFO message=Hello structured logging!
출력은 로그 세부 정보를 나타내는 키와 값 쌍을 포함하며 텍스트 형식입니다.
사용자 정의 형식은 지원되는 형식이 우리의 필요에 맞지 않을 때 유리할 수 있습니다.
또한, JSONWriter를 사용하여 사용자 정의 JSON 형식을 만들 수 있습니다:
private final JsonWriter<ILoggingEvent> writer = JsonWriter.<ILoggingEvent>of((members) -> {
members.add("time", ILoggingEvent::getInstant);
members.add("level", ILoggingEvent::getLevel);
members.add("thread", ILoggingEvent::getThreadName);
members.add("message", ILoggingEvent::getFormattedMessage);
members.add("application").usingMembers((application) -> {
application.add("name", "StructuredLoggingDemo");
application.add("version", "1.0.0-SNAPSHOT");
});
members.add("node").usingMembers((node) -> {
node.add("hostname", "node-1");
node.add("ip", "10.0.0.7");
});
}).withNewLineAtEnd();
다음으로 format() 메서드에 writer() 메서드를 통합해봅시다:
@Override
public String format(ILoggingEvent event) {
return this.writer.writeToString(event);
}
출력된 로그는 JSON 형식으로 구조화됩니다:
{
"time": "2024-12-19T08:55:13.284101533Z",
"level": "INFO",
"thread": "main",
"message": "No active profile set, falling back to 1 default profile: \"default\"",
"application": {
"name": "StructuredLoggingDemo",
"version": "1.0.0-SNAPSHOT"
},
"node": {
"hostname": "node-1",
"ip": "10.0.0.7"
}
}
사용 사례에 따라 사용자 정의 형식을 작성하면 Spring Boot에서 기본적으로 지원되지 않는 로그 집계 작업을 보다 유연하게 수행할 수 있습니다.
4.6. 파일로 로깅
앞서 언급한 예제는 로그를 콘솔에 직접 기록합니다. 그러나 인간 로그 형식을 콘솔에 유지하면서 구조화된 로그를 파일에 기록할 수 있도록 구성을 수정할 수 있습니다:
logging.structured.format.file=ecs
logging.file.name=log.json
여기서는 콘솔 속성 대신 파일 속성을 사용합니다. 이렇게 하면 구조화된 로그 정보가 포함된 log.json 파일이 프로젝트 루트 디렉터리에 생성됩니다.
5. 결론
이 기사에서는 application.properties 구성을 사용하여 구조화된 로깅을 위해 애플리케이션을 구성하는 방법을 배웠습니다. 또한 지원되는 다양한 구조화된 로그 형식의 예를 보았습니다.
마지막으로, 사용자 정의 형식을 작성하고 이를 구성하여 사용하는 방법도 살펴보았습니다.
항상 그렇듯이 예제에 대한 전체 소스 코드는 GitHub에서 확인할 수 있습니다.