반응형

[Spring Boot 실습 #10] Log4j2 적용 - level 별 appender 분리

 

1. Log4j2 활용하여 appender 분리 및 level 별로 로그 파일 분리

2. 개발환경

    2.1. 개발환경

    2.2. 라이브러리

3. 구현

    3.1. 프로젝트 구조

    3.2. 구현 - 소스코드

4. 결과

    4.1. log4j2-file.log

    4.2. log4j2-accessLog.log

    4.3. log4j2-error.log

 

 

1. log4j2 활용하여 appender 분리 및 level 별로 로그 파일 분리

 

2. 개발환경

 

2.1. 개발환경

MacOS M1 - macOS Monterey 12.0.1

IntelliJ IDEA 2021.2 (Community Edition)

 

2.2. 라이브러리

JDK 11

spring-boot-2.6.4

 

 

3. 구현

3.1. 프로젝트 구조

 

3.2. 구현 - 소스코드

 

[ pom.xml ]

  • spring-boot는 기본적으로 logback이 slf4j를 구현하도록 설정
  • log4j2를 설정하기 위해서는 logback dependency 제외 후 log4j2를 추가
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>io.home.test</groupId>
    <artifactId>spring-boot-log4j2</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-log4j2</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>11</java.version>
 
        <!-- 2021. 12. 10 Log4JShell Situation -->
        <!--<log4j2.version>2.17.1</log4j2.version>-->
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
 
            <!-- exclude logback configurations -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
 
        <!-- log4j2 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
</project>
 

 

[ log4j2-spring.xml ]

  • FileAppender
    • log4j2-file.log 파일로 분리
    • ThresholdFilter 활용하여 log level을 info, warn, error, fatal 설정 & trace, debug 무시
    • ThresholdFilter 활용하여 log level을 error, fatal 무시
  •  ErrorAppender
    • log4j2-error.log 파일로 분리
    • ThresholdFilter 활용하여 log level을 error, fatal 설정
  • AccessAppender
    • log4j2-accessLog.log 파일로 분리
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Properties>
        <Property name="LOG_MSG">%style{%d}{bright,cyan} [%highlight{%-5level}] [%style{%t}{bright,magenta}] %style{%C{1.}}{bright,blue}: %msg%n%throwable</Property>
    </Properties>
 
    <Appenders>
        <Console name="ConsoleAppender" target="SYSTEM_OUT">
            <!--<PatternLayout pattern="%style{%d}{bright,cyan} [%highlight{%-5level}] [%style{%t}{bright,magenta}] %style{%C{1.}}{bright,blue}: %msg%n%throwable" />-->
            <PatternLayout charset="UTF-8" pattern="${LOG_MSG}"/>
        </Console>
 
        <RollingFile name="AccessAppender"
                     fileName="./logs/log4j2-accessLog.log"
                     filePattern="./logs/$${date:yyyy-MM}/log4j2-accessLog-%d{-dd-MMMM-yyyy}-%i.log.gz">
            <Filters>
                <!-- accept info, warn, error, fatal and denies debug/trace -->
                <ThresholdFilter level="info"  onMatch="ACCEPT" onMismatch="DENY" />
            </Filters>
            <!--<PatternLayout>
                <pattern>%style{%d}{bright,cyan} [%highlight{%5level}] [%style{%t}{bright,magenta}] %style{%C{1.}}{bright,blue}: %m%n</pattern>
            </PatternLayout>-->
            <PatternLayout charset="UTF-8" pattern="${LOG_MSG}"/>
 
            <Policies>
                <!-- rollover on startup, daily and when the file reaches 10 MegaBytes -->
                <OnStartupTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="10 MB" />
                <TimeBasedTriggeringPolicy />
            </Policies>
        </RollingFile>
 
        <RollingFile name="FileAppender"
                     fileName="./logs/log4j2-file.log"
                     filePattern="./logs/$${date:yyyy-MM}/log4j2-file-%d{-dd-MMMM-yyyy}-%i.log.gz">
            <Filters>
                <!-- deny error, fatal -->
                <ThresholdFilter level="error"  onMatch="DENY"   onMismatch="NEUTRAL"/>
 
                <!-- accept info, warn, error, fatal and denies debug/trace -->
                <ThresholdFilter level="info"  onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
            <!--<PatternLayout>
                <pattern>%style{%d}{bright,cyan} [%highlight{%5level}] [%style{%t}{bright,magenta}] %style{%C{1.}}{bright,blue}: %m%n</pattern>
            </PatternLayout>-->
            <PatternLayout charset="UTF-8" pattern="${LOG_MSG}"/>
            <Policies>
                <!-- rollover on startup, daily and when the file reaches 10 MegaBytes -->
                <OnStartupTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="10 MB" />
                <TimeBasedTriggeringPolicy />
            </Policies>
        </RollingFile>
 
        <RollingFile name="ErrorLogFileAppender"
                     fileName="./logs/log4j2-error.log"
                     filePattern="./logs/$${date:yyyy-MM}/log4j2-error-%d{-dd-MMMM-yyyy}-%i.log.gz">
            <Filters>
                <ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY" />
            </Filters>
            <!--<PatternLayout>
                <pattern>%style{%d}{bright,cyan} [%highlight{%5level}] [%style{%t}{bright,magenta}] %style{%C{1.}}{bright,blue}: %m%n</pattern>
            </PatternLayout>-->
            <PatternLayout charset="UTF-8" pattern="${LOG_MSG}"/>
            <Policies>
                <!-- rollover on startup, daily and when the file reaches 10 MegaBytes -->
                <OnStartupTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="10 MB" />
                <TimeBasedTriggeringPolicy />
            </Policies>
        </RollingFile>
    </Appenders>
 
    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="ConsoleAppender" />
        </Root>
 
        <Logger name="org.springframework" additivity="false">
            <AppenderRef ref="ErrorLogFileAppender"/>
            <AppenderRef ref="ConsoleAppender"/>
            <AppenderRef ref="FileAppender"/>
        </Logger>
 
        <Logger name="io.home.test.base.filter" additivity="false">
            <AppenderRef ref="AccessAppender" />
            <AppenderRef ref="ConsoleAppender"/>
        </Logger>
 
        <Logger name="io.home.test" additivity="false">
            <AppenderRef ref="ErrorLogFileAppender"/>
            <AppenderRef ref="FileAppender" />
            <AppenderRef ref="ConsoleAppender"/>
        </Logger>
    </Loggers>
 
</Configuration>
 

 

[ SpringBootLogbackApplication.java ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package io.home.test;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
import java.io.File;
import java.util.HashMap;
import java.util.Map;
 
@SpringBootApplication
public class SpringBootLogbackApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(SpringBootLogbackApplication.class, args);
    }
 
}

 

[ DefaultController.java ]

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package io.home.test.base.web;
 
import io.home.test.base.errors.exceptions.TestException01;
import io.home.test.base.errors.exceptions.TestException02;
import io.home.test.base.errors.exceptions.TestException03;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
 
@RestController
@RequestMapping(value = "/api/access")
public class DefaultController {
    Logger LOG = LoggerFactory.getLogger(DefaultController.class);
 
    @GetMapping
    public ResponseEntity<String> defaultGetResource() {
        LOG.info("---- access controller GET");
        return ResponseEntity.ok("GET Hello World!");
    }
 
    @GetMapping(value = "/params")
    public ResponseEntity<String> defaultGetParamsResource(@RequestParam(value = "queryString"String queryString) {
        LOG.info("---- access controller GET + Params?" + queryString);
        return ResponseEntity.ok("GET + Params Hello World!");
    }
 
    @PostMapping
    public ResponseEntity<String> defaultPostResource(@RequestBody String body) {
        LOG.info("---- access controller POST\n" + body);
        return ResponseEntity.ok("POST Hello World!");
    }
 
    private static int a = 0;
    @GetMapping(value = "/error")
    public ResponseEntity<String> defaultErrorResource() {
        a++;
        try {
            switch (a) {
                case 1:
                    LOG.info("---- access controller GET /error");
                case 2:
                    throw new TestException01("Test Exception 01");
                case 3:
                    throw new TestException02("Test Exception 02");
                case 4:
                    throw new TestException03("Test Exception 03");
                case 5:
                    LOG.info("---- access controller GET /error");
                    a = 0;
                    break;
            }
            return ResponseEntity.ok().body("Error Hello World");
        } catch (Exception e) {
            String errorMsg = e.getMessage();
            if (a == 2) {
                LOG.warn(errorMsg);
            } else {
                LOG.error(errorMsg, e);
            }
            return ResponseEntity.ok().body(errorMsg);
        }
    }
}

 

[ AccessLogFilter.java ]

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
31
32
package io.home.test.base.filter;
 
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.filter.OncePerRequestFilter;
 
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
 
@WebFilter(urlPatterns = {"/api/*"}, asyncSupported = true)
@Component
public class AccessLogFilter extends OncePerRequestFilter {
 
    @Autowired
    AccessLogger accessLogger;
 
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        long stime = System.nanoTime();
        try {
            chain.doFilter(request, response);
        } finally {
            long etime = System.nanoTime();
            long elapsed = etime - stime;
            accessLogger.log(request, response, elapsed);
        }
    }
}

 

[ AccessLogger.java ]

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package io.home.test.base.filter;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
@Service
public class AccessLogger {
 
    Logger log = LoggerFactory.getLogger(AccessLogger.class);
 
    private String getURL(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        sb.append(request.getRequestURL());
 
        String queryString = request.getQueryString();
        if (queryString != null) {
            sb.append("?").append(queryString);
        }
        return sb.toString();
    }
 
    public void log(HttpServletRequest request, HttpServletResponse response, long elapsed) {
        String accessLog = buildAccessLog(request, response, elapsed);
        log.info(accessLog);
//        log.info("\"host\": {}, method: {}, url: {}, status: {}, elapsed: {}", remoteHost, method, url, status, elapsed);
    }
 
    private String buildAccessLog(HttpServletRequest request, HttpServletResponse response, long elapsed) {
 
        String remoteHost = request.getRemoteHost();
        String method = request.getMethod();
        String url = getURL(request);
        int status = response.getStatus();
 
        StringBuilder sb = new StringBuilder();
        sb.append("{");
        if (remoteHost != null) {
            sb
                    .append("\"").append("remoteHost").append("\"")
                    .append(":")
                    .append("\"").append(remoteHost).append("\"");
        }
        if (method != null) {
            sb
                    .append(",")
                    .append("\"").append("method").append("\"")
                    .append(":")
                    .append("\"").append(method).append("\"");
        }
        if (url != null) {
            sb
                    .append("\"").append("url").append("\"")
                    .append(":")
                    .append("\"").append(url).append("\"");
        }
        if (status != 0) {
            sb
                    .append(",")
                    .append("\"").append("status").append("\"")
                    .append(":")
                    .append("\"").append(status).append("\"");
        }
        if (elapsed != 0) {
            sb
                    .append(",")
                    .append("\"").append("elapsed").append("\"")
                    .append(":")
                    .append(elapsed);
        }
        sb.append("}");
        return sb.toString();
    }
}

 

[ TestException01.java ]

1
2
3
4
5
package io.home.test.base.errors.exceptions;
 
public class TestException01 extends RuntimeException {
    public TestException01(String message) { super(message); }
}

 

[ TestException02.java ]

1
2
3
4
5
package io.home.test.base.errors.exceptions;
 
public class TestException02 extends RuntimeException {
    public TestException02(String message) { super(message); }
}

 

[ TestException03.java ]

1
2
3
4
5
package io.home.test.base.errors.exceptions;
 
public class TestException03 extends RuntimeException {
    public TestException03(String message) { super(message); }
}

 

4. 결과

로그 파일 생성 확인

각 로그 파일에 접근하여 tail로 확인 

 

4.1. log4j2-file.log

 

 

4.2. log4j2-accessLog.log

 

4.3. log4j2-error.log

 

반응형