Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/github-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ jobs:

- name: Build with Gradle
run: ./gradlew bootJar --no-daemon

env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: Docker build and push
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
Expand All @@ -62,6 +63,6 @@ jobs:
sudo docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
sudo docker image rm ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} || true
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}
sudo docker-compose up -d --no-deps app
sudo docker-compose up -d
sudo docker-compose logs
sudo docker image prune -f
14 changes: 14 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id 'java'
id 'org.springframework.boot' version '3.4.1'
id 'io.spring.dependency-management' version '1.1.7'
id "io.sentry.jvm.gradle" version "5.12.2"
}

group = 'umc'
Expand All @@ -19,6 +20,12 @@ dependencyManagement {
}
}

sentry {
includeSourceContext = true
org = "catchy"
projectName = "java-spring-boot"
}

configurations {
compileOnly {
extendsFrom annotationProcessor
Expand Down Expand Up @@ -86,6 +93,13 @@ dependencies {

// OpenFeign
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

// Sentry
implementation 'io.sentry:sentry-spring-boot-starter:8.22.0'

// 성능 모니터링
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
}

tasks.named('test') {
Expand Down
35 changes: 34 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ services:
- "8081:8081"
volumes:
- ./src/main/resources/application-prod.yml:/config/application-prod.yml
networks:
- catchy-network

osrm-routed:
image: osrm/osrm-backend:latest
Expand All @@ -18,4 +20,35 @@ services:
volumes:
- .:/data
command: >
osrm-routed --algorithm mld /data/OSM/south-korea-latest.osrm
osrm-routed --algorithm mld /data/OSM/south-korea-latest.osrm
networks:
- catchy-network

prometheus:
image: prom/prometheus:latest
container_name: catchy-prometheus
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- "9090:9090"
networks:
- catchy-network

grafana:
image: grafana/grafana:latest
container_name: catchy-grafana
ports:
- "3000:3000"
volumes:
- grafana-storage:/var/lib/grafana
networks:
- catchy-network

networks:
catchy-network:
driver: bridge

volumes:
grafana-storage:
8 changes: 8 additions & 0 deletions monitoring/prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
global:
scrape_interval: 15s

scrape_configs:
- job_name: 'catchy-prod-app'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['catchy-server:8081']
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,10 @@ public class ErrorReasonDTO {
private final boolean isSuccess;
private final String code;
private final String message;

@Override
public String toString() {
return String.format("ErrorReason{code='%s', message='%s', status=%s}",
code, message, httpStatus);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
, "/member/reissue", "/member/callback/**"
,"/course/generate-ai", "/health"
,"/course/place/current", "/course/place/region"
, "/member/mypage/nickname").permitAll()
, "/member/mypage/nickname", "/actuator/**").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(exception -> exception
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package umc.catchy.global.error.exception;

import io.sentry.Sentry;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
Expand Down Expand Up @@ -28,21 +29,52 @@ public class ExceptionAdvice extends ResponseEntityExceptionHandler {

private ResponseEntity<Object> createErrorResponse(
Exception e, ErrorStatus errorStatus, String errorMessage, HttpHeaders headers, WebRequest request) {
log.error("Exception: {}, Status: {}, Message: {}", e.getClass().getSimpleName(), errorStatus, errorMessage);
log.error("Exception: {}, Code: {}, Message: {}",
e.getClass().getSimpleName(), errorStatus.getCode(), errorMessage);
BaseResponse<Object> body = BaseResponse.onFailure(errorStatus, errorMessage);
return super.handleExceptionInternal(e, body, headers, errorStatus.getHttpStatus(), request);
}

private ResponseEntity<Object> createErrorResponse(
Exception e, ErrorReasonDTO reason, HttpHeaders headers, HttpServletRequest request) {
log.error("Exception: {}, Reason: {}", e.getClass().getSimpleName(), reason);

// 상세 로그 출력 (Sentry가 읽기 쉽게)
log.error("Exception: {}, Code: {}, Message: {}, Status: {}",
e.getClass().getSimpleName(),
reason.getCode(),
reason.getMessage(),
reason.getHttpStatus().value());

// Sentry에 상세 정보 추가
Sentry.configureScope(scope -> {
scope.setTag("error_code", reason.getCode());
scope.setTag("http_status", String.valueOf(reason.getHttpStatus().value()));
scope.setTag("endpoint", request.getRequestURI());
scope.setTag("method", request.getMethod());
scope.setExtra("error_message", reason.getMessage());
});

// 500 에러만 Sentry에 전송
if (reason.getHttpStatus().is5xxServerError()) {
Sentry.captureException(e);
}

BaseResponse<Object> body = BaseResponse.onFailure(reason, null);
WebRequest webRequest = new ServletWebRequest(request);
return super.handleExceptionInternal(e, body, headers, reason.getHttpStatus(), webRequest);
}

@ExceptionHandler
public ResponseEntity<Object> handleGlobalException(Exception e, WebRequest request) {
// 예상치 못한 에러는 무조건 Sentry에
log.error("Unexpected exception occurred", e);

Sentry.configureScope(scope -> {
scope.setTag("error_type", e.getClass().getSimpleName());
scope.setExtra("error_message", e.getMessage());
});
Sentry.captureException(e);

return createErrorResponse(e, ErrorStatus._INTERNAL_SERVER_ERROR, e.getMessage(), HttpHeaders.EMPTY, request);
}

Expand All @@ -52,6 +84,9 @@ public ResponseEntity<Object> handleValidationException(ConstraintViolationExcep
.map(ConstraintViolation::getMessage)
.reduce((first, second) -> first + ", " + second)
.orElse("Validation error occurred");

log.warn("Validation error: {}", errorMessage);

return createErrorResponse(e, ErrorStatus.VALIDATION_ERROR, errorMessage, HttpHeaders.EMPTY, request);
}

Expand All @@ -70,13 +105,18 @@ protected ResponseEntity<Object> handleMethodArgumentNotValid(
e.getBindingResult().getFieldErrors()
.forEach(fieldError -> errors.put(fieldError.getField(), fieldError.getDefaultMessage()));
String errorMessage = String.join(", ", errors.values());

log.warn("Method argument validation failed: {}", errorMessage);

return createErrorResponse(e, ErrorStatus._BAD_REQUEST, errorMessage, headers, request);
}

@ExceptionHandler(ResultEmptyListException.class)
public ResponseEntity<Object> handleResultEmptyListException(ResultEmptyListException e, WebRequest request) {
log.info("Empty result list: {}", e.getErrorStatus().getCode());

return ResponseEntity
.status(e.getErrorStatus().getHttpStatus())
.body(BaseResponse.onFailureWithEmptyList(e.getErrorStatus()));
}
}
}
18 changes: 18 additions & 0 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,23 @@ cache:
fcm:
certification: ${FCM_CERTIFICATION:}

sentry:
dsn: ${SENTRY_DSN}
environment: production
send-default-pii: true
attach-stacktrace: true
traces-sample-rate: 1.0

management:
endpoints:
web:
exposure:
include: prometheus, health, info
endpoint:
prometheus:
enabled: true
health:
show-details: always

server:
port: 8081