Structured Logging

@Wonyuยท February 25, 2025 ยท 8 min read

๐Ÿ“Œ Spring Logging ๊ฐœ์š”

โœ… Log๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋™์ž‘์„ ์ถ”์ ํ•˜๊ณ  ๋ฌธ์ œ๋ฅผ ์ง„๋‹จํ•˜๋Š” ์ค‘์š”ํ•œ ๊ธฐ๋Šฅ
โœ… Spring์€ ๋‹ค์–‘ํ•œ ๋กœ๊น… ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์ง€์›ํ•˜๋ฉฐ, ๊ธฐ๋ณธ์ ์œผ๋กœ SLF4J๋ฅผ ๊ถŒ์žฅ

๐Ÿ“Œ SLF4J(Simple Logging Facade For Java)

โœ… SLF4J๋Š” ๋‹ค์–‘ํ•œ ๊ตฌํ˜„์ฒด(Logback, Log4j ๋“ฑ)๊ณผ ์—ฐ๋™ ๊ฐ€๋Šฅํ•œ ์ถ”์ƒํ™” ๊ณ„์ธต ์ œ๊ณต

์ฃผ์š” ํŠน์ง•

๐Ÿ”น ๋กœ๊น… ๊ตฌํ˜„์ฒด์— ๋Œ€ํ•œ ์˜์กด์„ฑ ์ œ๊ฑฐ

  • ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ œ๊ณตํ•ด ์‹ค์ œ ๋กœ๊ทธ ์ฒ˜๋ฆฌ ๊ตฌํ˜„์„ ์œ„์ž„
  • ๋Ÿฐํƒ€์ž„์— ์›ํ•˜๋Š” ๋กœ๊น… ํ”„๋ ˆ์ž„์›Œํฌ๋กœ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ

๐Ÿ”น ์œ ์—ฐํ•œ ๋กœ๊น… ํ”„๋ ˆ์ž„์›Œํฌ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ

  • ์„ค์ •๋งŒ ๋ณ€๊ฒฝํ•˜๋ฉด SLF4J API๋งŒ ์‚ฌ์šฉํ•ด ๊ตฌํ˜„์ฒด ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ

๐Ÿ”น ๋ฐ”์ธ๋”ฉ ๊ฐœ๋…์„ ์‚ฌ์šฉํ•ด ๋กœ๊น… ํ”„๋ ˆ์ž„์›Œํฌ ์—ฐ๊ฒฐ

  • ๋กœ๊น… ๊ตฌํ˜„์ฒด์˜ ๋ฐ”์ธ๋”ฉ์„ ์ถ”๊ฐ€ํ•ด ๋กœ๊ทธ ์ถœ๋ ฅ

๊ตฌ์กฐ

๐Ÿ”น SLF4J API

  • ๋กœ๊น…์„ ์œ„ํ•œ ๊ณตํ†ต ์ธํ„ฐํŽ˜์ด์Šค

๐Ÿ”น SLF4J ๋ฐ”์ธ๋”ฉ

  • SLF4J์™€ ์‹ค์ œ ๋กœ๊น… ๊ตฌํ˜„์ฒด๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ์—ญํ• 

๐Ÿ”น ๋กœ๊น… ๊ตฌํ˜„์ฒด

  • ์‹ค์ œ ๋กœ๊ทธ๋ฅผ ์ถœ๋ ฅํ•˜๋Š” ์—”์ง„

์ฃผ์š” ์ธํ„ฐํŽ˜์ด์Šค

Logger.java

package org.slf4j;

public interface Logger {
    String getName();

    boolean isTraceEnabled();
    void trace(String msg);
    void trace(String format, Object arg);
    void trace(String format, Object arg1, Object arg2);
    void trace(String format, Object... arguments);
    void trace(String msg, Throwable t);

    boolean isDebugEnabled();
    void debug(String msg);
    ...

    boolean isInfoEnabled();
    void info(String msg);
    ...

    boolean isWarnEnabled();
    void warn(String msg);
    ...

    boolean isErrorEnabled();
    void error(String msg);
    ...
}

์œ„ ์ธํ„ฐํŽ˜์ด์Šค๋Š” ๋กœ๊น…์„ ์œ„ํ•œ ๊ธฐ๋ณธ ๋ฉ”์„œ๋“œ๋“ค์„ ์ •์˜ํ•˜๋ฉฐ, ๋ชจ๋“  ๊ตฌํ˜„์ฒด๋Š” ์œ„ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค.
ํŒฉํ† ๋ฆฌ ํŒจํ„ด์œผ๋กœ ์‹ค์ œ Logger ๊ตฌํ˜„์ฒด๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค.


๐Ÿ“Œ Log Level

โœ… ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋กœ๊น…ํ•  ๋ฉ”์‹œ์ง€์˜ ์ค‘์š”๋„๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์„ค์ •
โœ… ํ•„์š”ํ•˜์ง€ ์•Š์€ ๋กœ๊ทธ๋ฅผ ์ค„์ด๊ณ , ์ค‘์š”ํ•œ ์ •๋ณด๋ฅผ ์‰ฝ๊ฒŒ ํ™•์ธ ๊ฐ€๋Šฅ

๋กœ๊ทธ ๋ ˆ๋ฒจ ์„ค๋ช… ์‚ฌ์šฉ ์˜ˆ์‹œ
TRACE ๊ฐ€์žฅ ์ƒ์„ธํ•œ ๋กœ๊ทธ, ๋””๋ฒ„๊น…์šฉ ๋‚ด๋ถ€ ํ๋ฆ„ ์ถ”์  (์ž˜ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Œ)
DEBUG ๋””๋ฒ„๊น…์„ ์œ„ํ•œ ์ƒ์„ธ ๋กœ๊ทธ ๊ฐœ๋ฐœ ์‹œ ์ฝ”๋“œ ํ๋ฆ„ ํ™•์ธ
INFO ์ผ๋ฐ˜์ ์ธ ์‹คํ–‰ ์ •๋ณด ์„œ๋น„์Šค ์‹คํ–‰, ์ฃผ์š” ์ด๋ฒคํŠธ ๋กœ๊น…
WARN ์ž ์žฌ์ ์ธ ๋ฌธ์ œ ๊ฒฝ๊ณ  ์„ฑ๋Šฅ ์ €ํ•˜, ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋™์ž‘
ERROR ์‹ฌ๊ฐํ•œ ์˜ค๋ฅ˜ ๋กœ๊ทธ ์˜ˆ์™ธ ๋ฐœ์ƒ, ์‹œ์Šคํ…œ ์žฅ์• 

๐Ÿ”น Spring Boot์˜ ๊ธฐ๋ณธ ๋กœ๊ทธ ๋ ˆ๋ฒจ์€ INFO
๐Ÿ”น application.properties์—์„œ ๋กœ๊ทธ ๋ ˆ๋ฒจ ์„ค์ • ๊ฐ€๋Šฅ

logging.level.root=DEBUG

๐Ÿ”น ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” INFO, ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” DEBUG ๊ถŒ์žฅ


๐Ÿ“Œ SLF4J์˜ ๊ตฌํ˜„์ฒด

Logback

โœ… Spring Boot์˜ ๊ธฐ๋ณธ ๋กœ๊น… ๊ตฌํ˜„์ฒด

๐Ÿ”น ํŠน์ง•

  • SLF4J์˜ ๊ณต์‹ ๊ตฌํ˜„์ฒด
  • XML ๋ฐ Groovy ๊ธฐ๋ฐ˜ ์„ค์ • ๊ฐ€๋Šฅ
  • RollingFileAppender๋ฅผ ํ†ตํ•œ ๋กœ๊ทธ ๋กค๋ง ์ง€์›

Log4j2

โœ… Log4j2๋Š” Log4j์˜ ๊ฐœ์„ ๋œ ๋ฒ„์ „์œผ๋กœ, ์„ฑ๋Šฅ๊ณผ ํ™•์žฅ์„ฑ์ด ๋›ฐ์–ด๋‚œ ๋กœ๊น… ํ”„๋ ˆ์ž„์›Œํฌ
โœ… Logback๋ณด๋‹ค ์„ฑ๋Šฅ์ด ๋” ์ข‹๊ณ  ๋น„๋™๊ธฐ ๋กœ๊น… ๊ฐ€๋Šฅ

๐Ÿ”น ํŠน์ง•

  • ๋น„๋™๊ธฐ ๋กœ๊น… ์ง€์›
  • JSON, YAML, XML ์„ค์ • ํŒŒ์ผ ์ง€์›
  • Garbage-Free(GC-Free) ๋กœ๊น…์œผ๋กœ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ ์ตœ์ ํ™”

๐Ÿ”น Spring Boot์—์„œ Log4j2 ์‚ฌ์šฉ
๊ธฐ์กด Logback์„ ์ œ์™ธ์‹œํ‚ค๊ณ , Log4j2๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค.

dependencies {
	implementation "org.springframework.boot:spring-boot-starter-log4j2"
	modules {
		module("org.springframework.boot:spring-boot-starter-logging") {
			replacedBy("org.springframework.boot:spring-boot-starter-log4j2", "Use Log4j2 instead of Logback")
		}
	}
}

๐Ÿ“Œ MDC(Mapped Diagnostic Context)

โœ… ๊ฐ ์š”์ฒญ๋งˆ๋‹ค ๊ด€๋ จ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์œ ์ง€ํ•˜๊ณ  ๋กœ๊ทธ์— ์ž๋™์œผ๋กœ ํฌํ•จํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” Key-Value ์ €์žฅ์†Œ
โœ… ๋‚ด๋ถ€์ ์œผ๋กœ ThreadLocal์„ ์‚ฌ์šฉํ•ด ๊ฐ ์“ฐ๋ ˆ๋“œ๋งˆ๋‹ค ์ €์žฅ์†Œ๋ฅผ ์œ ์ง€

ํŠน์ง•

๐Ÿ”น ์š”์ฒญ ๋‹จ์œ„ ๋กœ๊ทธ ์ถ”์  ๊ฐ€๋Šฅ

  • ๊ฐ™์€ ์š”์ฒญ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ชจ๋“  ๋กœ๊ทธ์— ๊ณตํ†ต๋œ ์ •๋ณด๋ฅผ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ

๐Ÿ”น key-value ๊ธฐ๋ฐ˜ ์ €์žฅ
๐Ÿ”น ๋กœ๊ทธ ์ž๋™ ์ถœ๋ ฅ

  • MDC์— ์ €์žฅ๋œ ๊ฐ’๋“ค์„ ๋กœ๊น… ๊ตฌํ˜„์ฒด ์„ค์ •์„ ํ†ตํ•ด ์ž๋™์œผ๋กœ ๋กœ๊ทธ์— ํฌํ•จ ๊ฐ€๋Šฅ

๐Ÿ“Œ Structured Log

โœ… ๋‹จ์ˆœํ•œ ๋ฌธ์ž์—ด ํ˜•ํƒœ๊ฐ€ ์•„๋‹Œ JSON์ด๋‚˜ key-value ํ˜•์‹์œผ๋กœ ๊ธฐ๋กํ•˜๋Š” ๋กœ๊น… ๊ธฐ๋ฒ•
โœ… ๋กœ๊ทธ ๋ถ„์„, ๊ฒ€์ƒ‰, ํ•„ํ„ฐ๋ง์— ์ตœ์ ํ™”๋จ (ELK, Grafana์™€ ์—ฐ๋™ ๊ฐ€๋Šฅ)

์žฅ์ 

๐Ÿ”น ๊ฒ€์ƒ‰ ๋ฐ ํ•„ํ„ฐ๋ง ์šฉ์ด
๐Ÿ”น ๋กœ๊ทธ ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™” ๊ฐ€๋Šฅ

  • ELK(Elasticsearch, Logstash, Kibana), Grafana ๋“ฑ์˜ ๋กœ๊ทธ ๋ถ„์„ ๋„๊ตฌ์™€ ์‰ฝ๊ฒŒ ์—ฐ๋™ ๊ฐ€๋Šฅ
  • ๋กœ๊ทธ ๋ฐ์ดํ„ฐ๋ฅผ ์ฐจํŠธ, ๊ทธ๋ž˜ํ”„ ํ˜•ํƒœ๋กœ ๋ถ„์„ ๊ฐ€๋Šฅ

๐Ÿ”น JSON ํฌ๋งท์„ ์‚ฌ์šฉํ•œ ๋จธ์‹ ๋Ÿฌ๋‹ ๋ถ„์„ ๊ฐ€๋Šฅ

  • ๋น…๋ฐ์ดํ„ฐ ๋ถ„์„์ด๋‚˜ ๋จธ์‹ ๋Ÿฌ๋‹ ๋ชจ๋ธ ํ•™์Šต์— ํ™œ์šฉ ๊ฐ€๋Šฅ

๐Ÿ”น ๊ฒ€์ƒ‰ ๋ฐ ํ•„ํ„ฐ๋ง ์šฉ์ด

  • MSA์—์„œ ๊ฐ ์„œ๋น„์Šค์™€ ๋กœ๊ทธ๋ฅผ ๊ณตํ†ต๋œ JSON ํฌ๋งท์œผ๋กœ ํ†ตํ•ฉ ๊ฐ€๋Šฅ

๐Ÿ”น ๊ฒ€์ƒ‰ ๋ฐ ํ•„ํ„ฐ๋ง ์šฉ์ด

Format

โœ… ๊ธฐ๋ณธ์ ์œผ๋กœ ์ถœ๋ ฅ๋˜๋Š” JSON ๋กœ๊ทธ๋Š”, ์„ค์ •๋œ ํฌ๋งท์— ๋งž๊ฒŒ MDC ๊ฐ’์„ ํ™œ์šฉํ•˜์—ฌ ์ž๋™์œผ๋กœ ์ƒ์„ฑ

๐Ÿ”น ECS
ElasticSearch์™€ ํ˜ธํ™˜๋˜๋Š” ํ‘œ์ค€ ๋กœ๊ทธ ํ˜•์‹ ์„ค์ •

logging.structured.format.console=ecs
logging.structured.format.file=ecs

์˜ˆ์‹œ

{
  "@timestamp": "2025-02-25T12:00:00.123Z",
  "log.level": "INFO",
  "message": "User login successful",
  "event.category": "authentication",
  "event.action": "user_login",
  "user.id": "567",
  "user.name": "john_doe",
  "http.request.method": "POST",
  "http.response.status_code": 200
}

๐Ÿ”น GELF
Graylog์— ์ตœ์ ํ™”๋œ ๋กœ๊ทธ ํ˜•์‹ ์„ค์ •

logging.structured.format.console=gelf
logging.structured.format.file=gelf

์˜ˆ์‹œ

{
  "version": "1.1",
  "host": "server-1",
  "short_message": "User login successful",
  "full_message": "User john_doe logged in from IP 192.168.1.100",
  "timestamp": 1708886400.123,
  "level": 6,
  "_user_id": "567",
  "_ip_address": "192.168.1.100",
  "_http_method": "POST"
}

๐Ÿ”น Logstash
Logstash์—์„œ ์ˆ˜์ง‘ํ•˜๊ณ  ์ฒ˜๋ฆฌํ•˜๊ธฐ ์ตœ์ ํ™”๋œ ๋กœ๊ทธ ํ˜•์‹

logging.structured.format.console=logstash
logging.structured.format.file=logstash

์˜ˆ์‹œ

{
  "timestamp": "2025-02-25T12:00:00.123Z",
  "level": "INFO",
  "message": "User login successful",
  "service": "UserService",
  "user_id": "567",
  "ip_address": "192.168.1.100",
  "http_method": "POST"
}

๐Ÿ“Œ ๊ถ๊ธˆํ•œ ์ 

Structured Logging์€ ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ• ๊นŒ?

์•„๋ž˜๋Š” ๊ฐœ์ธ์ ์ธ ์˜๊ฒฌ์ž…๋‹ˆ๋‹ค.

๋จผ์ €, Logging์— ๋Œ€ํ•œ ์‘์ง‘๋„์™€ ํ•ต์‹ฌ ๋กœ์ง ๋ถ„๋ฆฌ๋ฅผ ์œ„ํ•ด ์•„๋ž˜์™€ ๊ฐ™์€ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.

  1. Spring Filter ์ด์šฉ
  2. Spring AOP์—์„œ ์ž๋™ ์„ค์ •

์š”์ฒญ์ด ์‹œ์ž‘๋  ๋•Œ๋ถ€ํ„ฐ ๋๋‚  ๋•Œ๊นŒ์ง€ ๊ฐ€์ ธ์•ผํ•  ๊ณตํ†ต ํ•„๋“œ๋Š” Filter์—์„œ๋ถ€ํ„ฐ ์ฒ˜๋ฆฌํ•˜๊ณ ,
ํŠน์ • Layer์˜ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ์™€ ๊ฐ™์ด ๋กœ๊น… ๋ถ€๊ฐ€ ๊ธฐ๋Šฅ์„ ๋ถ„๋ฆฌํ•  ๋•Œ๋Š” AOP๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.

user.id๋ฅผ filter๋ฅผ ํ™œ์šฉํ•ด ๊ณตํ†ต ํ•„๋“œ๋กœ ์ถ”๊ฐ€ํ•ด๋ณด์ž.

application.properties

logging.structured.format.console=logstash

JsonLogFilter.java

@Component  
public class JsonLogFilter implements Filter {  
  
    @Override  
    public void doFilter(final ServletRequest request, final ServletResponse response,  
        final FilterChain chain)  
        throws IOException, ServletException {  
  
        try {  
            String userId = "example userId";  
            MDC.put("user.id", userId);  
            chain.doFilter(request, response);  
        } finally {  
            MDC.clear();  
        }  
    }  
}

log

{
   "@timestamp":"2025-02-25T23:26:59.796577+09:00",
   "@version":"1",
   "message":"test log",
   "logger_name":"com.example.javacicdpractice.member.MemberController",
   "thread_name":"http-nio-8080-exec-1",
   "level":"INFO",
   "level_value":20000,
   "user.id":"example userId"
}

user.id๊ฐ€ ๋กœ๊ทธ์— ์ •์ƒ์ ์œผ๋กœ ์ถ”๊ฐ€๋˜์—ˆ๋‹ค.

๐Ÿ“Œ ์ฐธ๊ณ 

https://docs.spring.io/spring-boot/how-to/logging.html https://docs.spring.io/spring-boot/reference/features/logging.html#features.logging.log-levels

@Wonyu
Hello :) I'm Wonyu