Skip to content

Commit 3a24091

Browse files
authored
[DDING-002] 운영 서버 매트릭 수집 세팅 및 요청 및 응답 로그 수집 (#314)
1 parent e073509 commit 3a24091

File tree

12 files changed

+346
-58
lines changed

12 files changed

+346
-58
lines changed

.ebextensions/03-swap-file

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
commands:
2+
01_create_swap:
3+
command: |
4+
# 스왑 파일이 이미 존재하는지 확인
5+
if [ ! -f /swapfile ]; then
6+
echo "Creating 1GB swap file..."
7+
8+
# 1GB 스왑 파일 생성 (fallocate 사용 - 더 빠름)
9+
fallocate -l 1G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=1024
10+
11+
# 스왑 파일 권한 설정 (보안상 중요)
12+
chmod 600 /swapfile
13+
14+
# 스왑 영역 설정
15+
mkswap /swapfile
16+
17+
# 스왑 활성화
18+
swapon /swapfile
19+
20+
# 부팅 시 자동 마운트를 위해 /etc/fstab에 추가 (중복 방지)
21+
if ! grep -q '/swapfile' /etc/fstab; then
22+
echo '/swapfile none swap sw 0 0' >> /etc/fstab
23+
fi
24+
25+
echo "1GB swap file created successfully"
26+
else
27+
echo "Swap file already exists, skipping creation"
28+
swapon -s
29+
fi
30+
ignoreErrors: false

.github/workflows/prod-server-deployer.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ jobs:
5353
cp build/libs/*.jar deploy/application.jar
5454
cp Procfile deploy/Procfile
5555
cp -r promtail deploy/promtail
56+
cp -r alloy deploy/alloy
5657
cp -r .ebextensions deploy/.ebextensions
5758
cp -r .platform deploy/.platform
5859
cd deploy && zip -r deploy.zip .

.platform/nginx/nginx.conf

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,25 @@ http {
2828
keepalive 1024;
2929
}
3030

31+
upstream springboot-actuator {
32+
server 127.0.0.1:9090;
33+
keepalive 38;
34+
}
35+
3136
server {
3237
listen 80 default_server;
3338

39+
location /server/actuator {
40+
proxy_pass http://springboot-actuator;
41+
proxy_http_version 1.1;
42+
proxy_set_header Connection $connection_upgrade;
43+
proxy_set_header Upgrade $http_upgrade;
44+
45+
proxy_set_header Host $host;
46+
proxy_set_header X-Real-IP $remote_addr;
47+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
48+
}
49+
3450
location / {
3551
proxy_pass http://springboot;
3652
proxy_http_version 1.1;

alloy/alloy-docker-compose.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
version: '3'
2+
3+
services:
4+
alloy:
5+
image: grafana/alloy:latest
6+
container_name: alloy
7+
network_mode: host
8+
volumes:
9+
- /var/app/current/alloy/config.alloy:/etc/alloy/config.alloy:ro
10+
environment:
11+
- ALLOY_ENV=prod
12+
- SERVER_NAME=prod-server

alloy/config.alloy

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
prometheus.scrape "spring_boot" {
2+
targets = [
3+
{
4+
"__address__" = "localhost:9090",
5+
"__metrics_path__" = "/server/actuator/prometheus",
6+
"job" = "spring-boot",
7+
"instance" = sys.env("SERVER_NAME"),
8+
"environment" = sys.env("ALLOY_ENV"),
9+
"server_type" = "application",
10+
},
11+
]
12+
scrape_interval = "15s"
13+
scrape_timeout = "10s"
14+
forward_to = [prometheus.remote_write.central.receiver]
15+
}
16+
17+
prometheus.exporter.unix "system" {
18+
filesystem {
19+
mount_points_exclude = "^/(sys|proc|dev|host|etc)($|/)"
20+
fs_types_exclude = "^(autofs|binfmt_misc|bpf|cgroup2?|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|iso9660|mqueue|nsfs|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|selinuxfs|squashfs|sysfs|tracefs)$"
21+
}
22+
enable_collectors = ["processes"]
23+
}
24+
25+
prometheus.scrape "system" {
26+
targets = prometheus.exporter.unix.system.targets
27+
job_name = "node"
28+
scrape_interval = "15s"
29+
scrape_timeout = "10s"
30+
31+
forward_to = [prometheus.relabel.system_labels.receiver]
32+
}
33+
34+
prometheus.relabel "system_labels" {
35+
forward_to = [prometheus.remote_write.central.receiver]
36+
37+
rule {
38+
target_label = "instance"
39+
replacement = sys.env("SERVER_NAME")
40+
}
41+
42+
rule {
43+
target_label = "environment"
44+
replacement = sys.env("ALLOY_ENV")
45+
}
46+
47+
rule {
48+
target_label = "server_type"
49+
replacement = "system"
50+
}
51+
}
52+
53+
54+
prometheus.remote_write "central" {
55+
endpoint {
56+
url = "http://3.39.151.102:9090/api/v1/write"
57+
remote_timeout = "30s"
58+
queue_config {
59+
capacity = 10000
60+
max_shards = 10
61+
min_shards = 1
62+
max_samples_per_send = 1000
63+
batch_send_deadline = "5s"
64+
}
65+
}
66+
wal {
67+
truncate_frequency = "2h"
68+
min_keepalive_time = "5m"
69+
max_keepalive_time = "8h"
70+
}
71+
}

promtail/promtail-docker-compose.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@ services:
55
promtail:
66
image: grafana/promtail:2.9.1
77
container_name: promtail
8-
environment:
9-
- LOKI_URL
108
volumes:
119
- /var/app/current/promtail/promtail-config.yml:/etc/promtail/config.yml
1210
- /var/app/current/logs:/var/logs # 로컬 로그 파일 경로를 Promtail에 마운트
1311
command: >
1412
-config.file=/etc/promtail/config.yml
15-
-client.url=${LOKI_URL}
13+
-client.url=http://3.39.151.102:3100/loki/api/v1/push
1614
ports:
1715
- "9080:9080" # Promtail의 웹 UI를 호스트와 매핑

src/main/java/ddingdong/ddingdongBE/common/config/SecurityConfig.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import ddingdong.ddingdongBE.common.filter.JwtAuthenticationFilter;
88
import ddingdong.ddingdongBE.common.handler.CustomAccessDeniedHandler;
99
import ddingdong.ddingdongBE.common.handler.RestAuthenticationEntryPoint;
10-
import org.springframework.beans.factory.annotation.Value;
1110
import org.springframework.context.annotation.Bean;
1211
import org.springframework.context.annotation.Configuration;
1312
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -27,9 +26,6 @@ public class SecurityConfig {
2726

2827
private static final String API_PREFIX = "/server";
2928

30-
@Value("security.actuator.base-path")
31-
private String actuatorPath;
32-
3329
@Bean
3430
public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthService authService, JwtConfig config)
3531
throws Exception {
@@ -40,7 +36,10 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthService authSer
4036
.permitAll()
4137
.requestMatchers(API_PREFIX + "/admin/**").hasRole("ADMIN")
4238
.requestMatchers(API_PREFIX + "/central/**").hasRole("CLUB")
43-
.requestMatchers(actuatorPath).hasRole("ADMIN")
39+
.requestMatchers(GET,
40+
"/server/actuator/health",
41+
"/server/actuator/prometheus",
42+
"/server/actuator/metrics").permitAll()
4443
.requestMatchers(GET,
4544
API_PREFIX + "/clubs/**",
4645
API_PREFIX + "/notices/**",

src/main/java/ddingdong/ddingdongBE/common/exception/CustomExceptionHandler.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public ErrorResponse handleSystemException(Throwable exception, HttpServletReque
4545

4646
loggingApplicationError(connectionInfo
4747
+ "\n"
48-
+ "[SYSTEM-ERROR]" + " : " + exception.getMessage());
48+
+ "[SYSTEM-ERROR]" + " : " + exception.getMessage(), exception);
4949

5050
Sentry.captureException(exception, scope -> {
5151
scope.setExtra("requestMethod", requestMethod);
@@ -70,7 +70,7 @@ public ErrorResponse handleIllegalArgumentException(IllegalArgumentException exc
7070

7171
loggingApplicationWarn(connectionInfo
7272
+ "\n"
73-
+ exception.getClass().getSimpleName() + " : " + exception.getMessage());
73+
+ exception.getClass().getSimpleName() + " : " + exception.getMessage(), exception);
7474

7575
return new ErrorResponse(BAD_REQUEST.value(), exception.getMessage(), LocalDateTime.now()
7676
);
@@ -83,7 +83,7 @@ public ErrorResponse handlePersistenceException(CustomException exception, HttpS
8383

8484
loggingApplicationWarn(connectionInfo
8585
+ "\n"
86-
+ exception.getErrorCode() + " : " + exception.getMessage());
86+
+ exception.getErrorCode() + " : " + exception.getMessage(), exception);
8787

8888
return new ErrorResponse(exception.getErrorCode(), exception.getMessage(), LocalDateTime.now()
8989
);
@@ -96,7 +96,7 @@ public ErrorResponse handleAuthenticationException(AuthenticationException excep
9696

9797
loggingApplicationWarn(connectionInfo
9898
+ "\n"
99-
+ exception.getClass().getSimpleName() + " : " + exception.getMessage());
99+
+ exception.getClass().getSimpleName() + " : " + exception.getMessage(), exception);
100100

101101
return new ErrorResponse(exception.getErrorCode(), exception.getMessage(), LocalDateTime.now()
102102
);
@@ -115,7 +115,7 @@ public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotVali
115115

116116
loggingApplicationWarn(connectionInfo
117117
+ "\n"
118-
+ exception.getClass().getSimpleName() + " : " + message);
118+
+ exception.getClass().getSimpleName() + " : " + message, exception);
119119

120120
return new ErrorResponse(BAD_REQUEST.value(), message, LocalDateTime.now()
121121
);
@@ -145,12 +145,12 @@ private String createLogConnectionInfo(HttpServletRequest request) {
145145
return requestMethod + requestUrl + "?" + queryString + " from ip: " + clientIp;
146146
}
147147

148-
private void loggingApplicationWarn(String applicationLog) {
149-
log.warn("errorLog = {}", applicationLog);
148+
private void loggingApplicationWarn(String applicationLog, Throwable e) {
149+
log.warn("errorLog = {}", applicationLog, e);
150150
}
151151

152-
private void loggingApplicationError(String applicationLog) {
153-
log.error("errorLog = {}", applicationLog);
152+
private void loggingApplicationError(String applicationLog, Throwable e) {
153+
log.error("errorLog = {}", applicationLog, e);
154154
}
155155

156156
private String getRequestBody(HttpServletRequest request) {
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package ddingdong.ddingdongBE.common.filter;
2+
3+
import jakarta.servlet.FilterChain;
4+
import jakarta.servlet.ServletException;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import java.io.IOException;
8+
import java.util.List;
9+
import java.util.UUID;
10+
import lombok.extern.slf4j.Slf4j;
11+
import org.slf4j.MDC;
12+
import org.springframework.stereotype.Component;
13+
import org.springframework.util.AntPathMatcher;
14+
import org.springframework.web.filter.OncePerRequestFilter;
15+
16+
@Slf4j
17+
@Component
18+
public class HttpLoggingFilter extends OncePerRequestFilter {
19+
20+
private static final List<String> EXCLUDE_URI = List.of(
21+
"/actuator/**",
22+
"/swagger-ui/**",
23+
"/api-docs",
24+
"/favicon.ico"
25+
);
26+
27+
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
28+
29+
@Override
30+
protected void doFilterInternal(
31+
final HttpServletRequest request,
32+
final HttpServletResponse response,
33+
final FilterChain filterChain
34+
) throws ServletException, IOException {
35+
36+
final String requestURI = request.getRequestURI();
37+
if (isExcluded(requestURI)) {
38+
filterChain.doFilter(request, response);
39+
return;
40+
}
41+
42+
final String requestId = createRequestId();
43+
final long startTime = System.currentTimeMillis();
44+
45+
logRequest(request, requestId);
46+
47+
try {
48+
filterChain.doFilter(request, response);
49+
} finally {
50+
final long duration = System.currentTimeMillis() - startTime;
51+
logResponse(request, response, requestId, duration);
52+
MDC.remove("request_id");
53+
}
54+
}
55+
56+
private String createRequestId() {
57+
String requestId = UUID.randomUUID().toString().substring(0, 8);
58+
MDC.put("request_id", requestId);
59+
return requestId;
60+
}
61+
62+
private void logRequest(HttpServletRequest request, String requestId) {
63+
log.info("[{}] → {} {} | IP: {}",
64+
requestId,
65+
request.getMethod(),
66+
getFullRequestUrl(request),
67+
getClientIpAddress(request)
68+
);
69+
}
70+
71+
private void logResponse(HttpServletRequest request, HttpServletResponse response,
72+
String requestId, long duration) {
73+
log.info("[{}] ← {} | Status: {} | {}ms",
74+
requestId,
75+
request.getMethod(),
76+
response.getStatus(),
77+
duration
78+
);
79+
}
80+
81+
private String getFullRequestUrl(HttpServletRequest request) {
82+
String queryString = request.getQueryString();
83+
return queryString != null ? request.getRequestURI() + "?" + queryString : request.getRequestURI();
84+
}
85+
86+
private String getClientIpAddress(HttpServletRequest request) {
87+
// AWS ALB에서 설정하는 X-Forwarded-For 헤더 확인
88+
String xForwardedFor = request.getHeader("X-Forwarded-For");
89+
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
90+
// 첫 번째 IP가 실제 클라이언트 IP
91+
return xForwardedFor.split(",")[0].trim();
92+
}
93+
94+
// fallback: Remote Address
95+
return request.getRemoteAddr();
96+
}
97+
98+
private boolean isExcluded(String requestURI) {
99+
return EXCLUDE_URI.stream()
100+
.anyMatch(pattern -> antPathMatcher.match(pattern, requestURI));
101+
}
102+
}

0 commit comments

Comments
 (0)