From adaa6d8df498c14eb564d2de6eedb2183738dd96 Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Thu, 14 Nov 2024 20:46:19 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20logback=EA=B3=BC=20sentry=EB=A5=BC=20?= =?UTF-8?q?=ED=99=9C=EC=9A=A9=ED=95=9C=20=EC=97=90=EB=9F=AC=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=88=ED=84=B0=EB=A7=81=20=ED=88=B4=20=EA=B5=AC=EC=B6=95=20?= =?UTF-8?q?(#482)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: dev/prod 별 센트리 dsn, environment, servername 설정 * chore: logback 전용 Sentry 의존성 추가 * feat: Sentry logback 기능 추가 * feat: Sentry 속성을 주입하는 SentryConfig 추가 * feat: Sentry Exception 테스트용 임시 API 추가 * chore: slack webhook 의존성 주입 * chore: slack 웹훅에 사용될 환경변수 설정 * chore: 에러감지 시 Slack으로 Sentry 주소를 보내주도록 구현 * chore: test용 yml파일에 변경사항 업데이트 * feat: IOException Handler 메서드 추가 --- main/build.gradle | 55 +++++++++++-------- .../advice/ControllerExceptionAdvice.java | 13 ++++- .../main/global/config/SecurityConfig.java | 5 +- .../crew/main/global/config/SentryConfig.java | 29 ++++++++++ .../main/global/exception/ErrorStatus.java | 1 + .../main/global/sentry/SentryController.java | 26 +++++++++ main/src/main/resources/application-dev.yml | 13 +++++ main/src/main/resources/application-prod.yml | 13 +++++ main/src/main/resources/application-test.yml | 13 +++++ main/src/main/resources/logback-spring.xml | 38 ++++++++++++- 10 files changed, 177 insertions(+), 29 deletions(-) create mode 100644 main/src/main/java/org/sopt/makers/crew/main/global/config/SentryConfig.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/global/sentry/SentryController.java diff --git a/main/build.gradle b/main/build.gradle index c64f1fa7..1faa3948 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -23,71 +23,82 @@ repositories { } dependencies { + // Spring Boot Dependencies implementation 'org.springframework.boot:spring-boot-starter-web' - // https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-ui implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' implementation 'org.springframework.boot:spring-boot-starter-webflux:3.1.5' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' - // lombok + // Lombok Dependencies (Compile-time code generation library) compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' + // Development Tools developmentOnly 'org.springframework.boot:spring-boot-devtools' + // Test Dependencies testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.testcontainers:testcontainers:1.17.3' + testImplementation 'org.testcontainers:junit-jupiter:1.16.2' + testImplementation 'org.testcontainers:postgresql:1.17.3' + testImplementation 'org.testcontainers:jdbc' + + // Apache Commons Dependencies + implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.14.0' + + // AOP (Aspect-Oriented Programming) for cross-cutting concerns + implementation 'org.springframework.boot:spring-boot-starter-aop' + + // Database and Persistence Dependencies implementation group: 'org.postgresql', name: 'postgresql', version: '42.6.0' implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.postgresql:postgresql:42.3.0' - // jsonb 타입 핸들링 위함 + // jsonb Type Handling implementation 'io.hypersistence:hypersistence-utils-hibernate-62:3.6.0' + // JWT Dependencies for Security and Authentication implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' implementation 'com.auth0:java-jwt:4.4.0' - // h2 + // H2 Database (for testing and in-memory DB) runtimeOnly 'com.h2database:h2' - // mac m1 setting + // macOS M1 Support (Network Resolver for macOS) implementation 'io.netty:netty-resolver-dns-native-macos:4.1.68.Final:osx-aarch_64' + // Spring Cloud OpenFeign (Service-to-service communication) implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.0.3' - // test container - testImplementation 'org.testcontainers:testcontainers:1.17.3' // TC 의존성 - testImplementation 'org.testcontainers:junit-jupiter:1.16.2' // TC 의존성 - testImplementation 'org.testcontainers:postgresql:1.17.3' // PostgreSQL 컨테이너 사용 - testImplementation 'org.testcontainers:jdbc' // DB와의 JDBC connection - implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.14.0' - - // aop - implementation 'org.springframework.boot:spring-boot-starter-aop' - - // MapStruct + // MapStruct (DTO transformation code generation) implementation 'org.mapstruct:mapstruct:1.5.3.Final' annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final' - // queryDsl + // QueryDSL (JPA Query Generation) implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" - // AWS SDK for S3 + // AWS SDK for S3 (for file upload and download) implementation "software.amazon.awssdk:s3:2.27.0" - // csv 관련 + // CSV Processing Library implementation 'com.opencsv:opencsv:5.5.2' - // prometheus + // Prometheus (Monitoring and Metrics Collection) implementation 'io.micrometer:micrometer-registry-prometheus' + + // Sentry Logback (Error Tracking) + implementation 'io.sentry:sentry-logback:7.17.0' + + // Slack Webhook + implementation 'com.github.maricn:logback-slack-appender:1.4.0' } tasks.named('test') { @@ -157,4 +168,4 @@ jacocoTestCoverageVerification { ] } } -} \ No newline at end of file +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/advice/ControllerExceptionAdvice.java b/main/src/main/java/org/sopt/makers/crew/main/global/advice/ControllerExceptionAdvice.java index 5b361319..77bed3c7 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/global/advice/ControllerExceptionAdvice.java +++ b/main/src/main/java/org/sopt/makers/crew/main/global/advice/ControllerExceptionAdvice.java @@ -1,8 +1,10 @@ package org.sopt.makers.crew.main.global.advice; +import java.io.IOException; + import org.sopt.makers.crew.main.global.exception.BaseException; -import org.sopt.makers.crew.main.global.exception.ExceptionResponse; import org.sopt.makers.crew.main.global.exception.ErrorStatus; +import org.sopt.makers.crew.main.global.exception.ExceptionResponse; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -108,6 +110,14 @@ public ResponseEntity handleHttpRequestMethodNotSupportedExce ErrorStatus.INVALID_INPUT_VALUE.getErrorCode())); } + @ExceptionHandler(IOException.class) + public ResponseEntity handleIOException(IOException e) { + log.warn("{}", e.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(ExceptionResponse.fail( + ErrorStatus.IO_EXCEPTION.getErrorCode())); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception e) { log.error("{}", e.getMessage()); @@ -115,5 +125,4 @@ public ResponseEntity handleException(Exception e) { .body(ExceptionResponse.fail( ErrorStatus.INTERNAL_SERVER_ERROR.getErrorCode())); } - } diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/config/SecurityConfig.java b/main/src/main/java/org/sopt/makers/crew/main/global/config/SecurityConfig.java index f810a2af..5cff0a6e 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/global/config/SecurityConfig.java +++ b/main/src/main/java/org/sopt/makers/crew/main/global/config/SecurityConfig.java @@ -52,7 +52,8 @@ private String[] getAuthWhitelist() { "/meeting/v2/org-user/**", "/auth/v2", "/auth/v2/**", - actuatorEndPoint + "/health" + actuatorEndPoint + "/health", + "/sentry" // prod에서 테스트 후 삭제 }; } @@ -103,4 +104,4 @@ CorsConfigurationSource corsConfigurationSource() { return source; } -} \ No newline at end of file +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/config/SentryConfig.java b/main/src/main/java/org/sopt/makers/crew/main/global/config/SentryConfig.java new file mode 100644 index 00000000..e18d9647 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/global/config/SentryConfig.java @@ -0,0 +1,29 @@ +package org.sopt.makers.crew.main.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +import io.sentry.Sentry; +import jakarta.annotation.PostConstruct; + +@Configuration +public class SentryConfig { + + @Value("${sentry.dsn}") + private String sentryDsn; + + @Value("${sentry.environment}") + private String environment; + + @Value("${sentry.servername}") + private String serverName; + + @PostConstruct + public void initSentry() { + Sentry.init(options -> { + options.setDsn(sentryDsn); + options.setEnvironment(environment); + options.setServerName(serverName); + }); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java b/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java index bb4470f8..90e5e61d 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java +++ b/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java @@ -38,6 +38,7 @@ public enum ErrorStatus { CO_LEADER_CANNOT_APPLY("공동 모임장은 신청할 수 없습니다."), LEADER_CANNOT_BE_CO_LEADER_APPLY("모임장은 공동 모임장이 될 수 없습니다."), NOT_ALLOW_MEETING_APPLY("허용되지 않는 모임 신청입니다."), + IO_EXCEPTION("파일 입출력 오류가 발생했습니다."), /** * 401 UNAUTHORIZED diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/sentry/SentryController.java b/main/src/main/java/org/sopt/makers/crew/main/global/sentry/SentryController.java new file mode 100644 index 00000000..d16b2235 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/global/sentry/SentryController.java @@ -0,0 +1,26 @@ +package org.sopt.makers.crew.main.global.sentry; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.sentry.Sentry; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestController +@RequestMapping("/sentry") +public class SentryController { + + @GetMapping + public ResponseEntity testSentry() { // prod에서 테스트 후 삭제 + try { + throw new Exception("This is a test exception for Sentry."); + } catch (Exception e) { + Sentry.captureException(e); + log.error("Exception captured in Sentry", e); + return ResponseEntity.status(500).body("Exception captured in Sentry"); + } + } +} diff --git a/main/src/main/resources/application-dev.yml b/main/src/main/resources/application-dev.yml index 9c73fe89..9f73f98c 100644 --- a/main/src/main/resources/application-dev.yml +++ b/main/src/main/resources/application-dev.yml @@ -99,3 +99,16 @@ management: custom: paths: eventApply: ${DEV_EVENT_APPLY_PATH} + +sentry: + dsn: ${SENTRY_DSN} + environment: ${DEV_SENTRY_ENVIRONMENT} + servername: ${DEV_SENTRY_SERVERNAME} + +logging: + slack: + webhook-uri: ${SENTRY_SLACK_WEBHOOK_URI} + config: classpath:logback-spring.xml + sentry: + repository-uri: ${SENTRY_REPOSITORY_URI} + environment: dev diff --git a/main/src/main/resources/application-prod.yml b/main/src/main/resources/application-prod.yml index bba3a8ea..eaabdca2 100644 --- a/main/src/main/resources/application-prod.yml +++ b/main/src/main/resources/application-prod.yml @@ -99,3 +99,16 @@ management: custom: paths: eventApply: ${PROD_EVENT_APPLY_PATH} + +sentry: + dsn: ${SENTRY_DSN} + environment: ${PROD_SENTRY_ENVIRONMENT} + servername: ${PROD_SENTRY_SERVERNAME} + +logging: + slack: + webhook-uri: ${SENTRY_SLACK_WEBHOOK_URI} + config: classpath:logback-spring.xml + sentry: + repository-uri: ${SENTRY_REPOSITORY_URI} + environment: prod diff --git a/main/src/main/resources/application-test.yml b/main/src/main/resources/application-test.yml index a7e232fc..37ea31a7 100644 --- a/main/src/main/resources/application-test.yml +++ b/main/src/main/resources/application-test.yml @@ -98,3 +98,16 @@ management: custom: paths: eventApply: ${DEV_EVENT_APPLY_PATH} + +sentry: + dsn: ${SENTRY_DSN} + environment: ${DEV_SENTRY_ENVIRONMENT} + servername: ${DEV_SENTRY_SERVERNAME} + +logging: + slack: + webhook-uri: ${SENTRY_SLACK_WEBHOOK_URI} + config: classpath:logback-spring.xml + sentry: + repository-uri: ${SENTRY_REPOSITORY_URI} + environment: test diff --git a/main/src/main/resources/logback-spring.xml b/main/src/main/resources/logback-spring.xml index 3c008a2b..a96141ca 100644 --- a/main/src/main/resources/logback-spring.xml +++ b/main/src/main/resources/logback-spring.xml @@ -3,9 +3,38 @@ + + always + true + 1.0 + ERROR + DEBUG + + + + + + + ${SLACK_WEBHOOK_URI} + + *🚨[${ENVIRONMENT}] %d{yyyy-MM-dd HH:mm:ss.SSS} %-4relative [%thread] %-5level %class - %msg <${SENTRY_REPOSITORY_URI}|Go-To-Sentry>* + %n + + + posting bot + :robot_face: + true + + + + + + ERROR + + + - @@ -13,9 +42,10 @@ - + + @@ -23,6 +53,8 @@ + + @@ -32,4 +64,4 @@ - \ No newline at end of file +