-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 이력서 크롤링 실패 슬랙봇 알림 #127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
yuripbong
wants to merge
95
commits into
develop
Choose a base branch
from
BACKEND-166
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+5,322
−839
Open
Changes from 9 commits
Commits
Show all changes
95 commits
Select commit
Hold shift + click to select a range
20a1719
ãcs 스케줄러 및 테스트코드
HwangMinSeon 17a6fac
fix: 테스트 환경변수 설정변경
HwangMinSeon 7feb4a6
test: TodayCsScheduler 유닛 테스트 작성
HwangMinSeon b689abc
fix: 전체 주석 처리
HwangMinSeon ee53f65
fix: 큐 파일 정리
HwangMinSeon be75380
setting: resume id 추가
yuripbong 0e7ea94
feat: 이력서 크롤링 실패 처리
yuripbong d262f2c
feat: 이력서 작업 처리 상태 감지
yuripbong 60d8487
feat: 이력서 크롤링 실패 분류
yuripbong 35c6b58
feat: 이력서 Id 추출
yuripbong 71f0b9b
feat: 이력서 처리 작업 모니터링
yuripbong cc08fab
feat: 이력서 크롤링 실패 슬랙 알림
yuripbong 1ddd007
style: 포맷팅
yuripbong c2924e6
setting: 도커 이미지 변경
yuripbong dd9fef0
fix: 이미지 수정
yuripbong 9422ed3
setting: 도커 이미지 변경
yuripbong 1c817c0
Merge branch 'develop' of https://github.com/Techeer-Hogwarts/backend…
HwangMinSeon f9ec269
fix: 알림 중복 발송 방지
yuripbong 2029291
fix: 레디스 조회 null 검사
yuripbong 189e336
refactor: SCAN 기반 조회 적용
yuripbong 619a937
fix: 알림 발송 완료 태스크 정리
yuripbong e8eb606
fix : 스케줄러에서 이모지 제거
HwangMinSeon 311d037
fix : 도커파일 수정
HwangMinSeon 9d12337
fix : 도커파일 수정, Runtime jre로 수정. builder는 유지
HwangMinSeon a8e141b
fix : 함수명 소문자로 수정
HwangMinSeon 2f30cee
fix : instanceOf 제거
HwangMinSeon 98dd1fd
Merge pull request #126 from Techeer-Hogwarts/BACKEND-179
HwangMinSeon ebda8e4
fix: 부트캠프 기간 생성 LocalDate 의존성 제거
dongwooooooo f9812d2
fix: BootcampMapper 정적 메서드로 변경
dongwooooooo 8689b53
BootcampPermissionEvaluator 구현 및 CustomUserPrincipal 수정
dongwooooooo d09c2eb
BootcampService 검증 로직 추가, 기타 적용사항
dongwooooooo 57ea6e2
User에 bootcamp로직 수정
dongwooooooo 42d97fc
Test: BootcampTest 코드 작성
dongwooooooo 2b28068
기타 변경사항, SpotlessApply
dongwooooooo 38f4432
커서 룰 하나 추가
dongwooooooo 739e1c8
카멜케이스, 리터럴 중복 해결
dongwooooooo edffe27
LocalDate 빈으로 주입
dongwooooooo 1081397
DelegatingPermissionEvaluator long 형변환
dongwooooooo 5c8c1f4
DelegatingPermissionEvaluator long 형변환
dongwooooooo 47f01bc
순환참조 수정
dongwooooooo 5ac4200
evaluator 수정
dongwooooooo a1c660c
spotlessApply
dongwooooooo 160859c
ci test 주석 해제
dongwooooooo f6797b7
순환참조 해제
dongwooooooo 3780770
외부 환경변수 테스트환경에서 격리
dongwooooooo 3ac8de2
테스트 컨테이너 적용
dongwooooooo 2f99b6b
테스트컨테이너 변경 적
dongwooooooo 3b69ed3
프로젝트 멤버 테스트코드 작성
dongwooooooo fb2bdee
spotlessApply
dongwooooooo dbd1da5
Merge branch 'develop' of https://github.com/Techeer-Hogwarts/backend…
dongwooooooo ae82e00
test.properties 추가
dongwooooooo af5b3e3
ci jacoco 종류 변경
dongwooooooo 536bb73
코더레빗 리뷰 적용
dongwooooooo 1f66bc1
Merge pull request #128 from Techeer-Hogwarts/BACKEND-187
dongwooooooo aa2484b
zoom header debugging
dongwooooooo 8dd61d1
zoom testConnection 체
dongwooooooo e79f956
zoom event notification endpoint url validate api
dongwooooooo ebffde4
zoom event notification endpoint url validate api
dongwooooooo 11ac775
zoom event notification endpoint url validate api
dongwooooooo 7c4744c
zoom event notification endpoint url validate api
dongwooooooo 55aaf53
zoom 안 쓰는 코드 정리
dongwooooooo 800d96a
zoom 안 쓰는 코드 정리
dongwooooooo 042258d
zoom api 토큰 설정
dongwooooooo 0523e24
zoom api 토큰 설정
dongwooooooo 8a398df
zoom 웹훅 api 수정 및 정리
dongwooooooo fc22dee
zoom webhook exception
dongwooooooo 59ec0d9
spotlessApply
dongwooooooo 276a716
zoomWebhook 인증 리팩터링
dongwooooooo 9694580
spotlessApply
dongwooooooo a080e48
래빗 리뷰
dongwooooooo 86c87ef
zoom 인증 경로 설정
dongwooooooo f953881
zoom 웹훅 이벤트 body 실종 수정
dongwooooooo 5591e9a
spotlessApply
dongwooooooo be72a68
zoom 인증 경로 설정
dongwooooooo 5213a47
zoomWebhook filter에서 aop로 인증 변경
dongwooooooo dc53254
spotlessApply
dongwooooooo 0b812e2
zoom 12시 이후로 나가면 조회 못하는 에러 해결
dongwooooooo 02288ea
spotlessApply
dongwooooooo 695f534
Merge pull request #132 from Techeer-Hogwarts/BACKEND-193
dongwooooooo 8f5c10b
feat: 부트캠프 목록 조회 응답값 수정
kimzini adaa34b
feat: 부트캠프 목록 조회 응답값 수정
kimzini 4da0d5a
setting: resume id 추가
yuripbong 38f3f52
feat: 이력서 크롤링 실패 처리
yuripbong e03446c
feat: 이력서 작업 처리 상태 감지
yuripbong 1c4058b
feat: 이력서 크롤링 실패 분류
yuripbong e407487
feat: 이력서 Id 추출
yuripbong 97b0cf7
feat: 이력서 처리 작업 모니터링
yuripbong 841726c
feat: 이력서 크롤링 실패 슬랙 알림
yuripbong f9c0570
style: 포맷팅
yuripbong 44ea627
setting: 도커 이미지 변경
yuripbong 94bef45
fix: 알림 중복 발송 방지
yuripbong 7cf8f14
fix: 레디스 조회 null 검사
yuripbong 4c23268
refactor: SCAN 기반 조회 적용
yuripbong c8c3d00
fix: 알림 발송 완료 태스크 정리
yuripbong b8ad0a8
Merge branch 'BACKEND-166' of https://github.com/Techeer-Hogwarts/bac…
yuripbong File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 55 additions & 0 deletions
55
techeerzip/src/main/java/backend/techeerzip/domain/resume/alert/ResumeAlertNotifier.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package backend.techeerzip.domain.resume.alert; | ||
|
|
||
| import org.springframework.context.ApplicationEventPublisher; | ||
| import org.springframework.data.redis.core.RedisTemplate; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import backend.techeerzip.infra.slack.event.SlackEvent; | ||
| import backend.techeerzip.infra.slack.util.SlackChannelType; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| @Slf4j | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class ResumeAlertNotifier { | ||
| private static final String ALERT_SENT_KEY_PREFIX = "resume_alert_sent:"; | ||
|
|
||
| private final ApplicationEventPublisher eventPublisher; | ||
| private final RedisTemplate<String, String> redisTemplate; | ||
|
|
||
| public void notify( | ||
| String reason, String stage, String taskId, Long userId, Long resumeId, String detail) { | ||
| if (resumeId == null) { | ||
| log.warn("resumeId가 null이어서 알림 발송 불가 - taskId: {}", taskId); | ||
| return; | ||
| } | ||
|
|
||
| final String alertKey = ALERT_SENT_KEY_PREFIX + resumeId; | ||
|
|
||
| Boolean alreadySent = redisTemplate.hasKey(alertKey); | ||
| if (Boolean.TRUE.equals(alreadySent)) { | ||
| log.debug("이미 알림이 발송된 이력서 - resumeId: {}, taskId: {}", resumeId, taskId); | ||
| return; | ||
| } | ||
|
|
||
| final String message = | ||
| String.format( | ||
| "❗️ 이력서 크롤링 실패\n- reason: %s\n- stage: %s\n- taskId: %s\n- userId: %s\n- resumeId: %s\n- detail: %s", | ||
| reason, | ||
| stage, | ||
| taskId, | ||
| String.valueOf(userId), | ||
| String.valueOf(resumeId), | ||
| detail != null ? detail : "-"); | ||
|
|
||
| eventPublisher.publishEvent( | ||
| SlackEvent.Channel.builder() | ||
| .channelType(SlackChannelType.RESUME) | ||
| .message(message) | ||
| .build()); | ||
|
|
||
| redisTemplate.opsForValue().set(alertKey, "sent"); | ||
| log.info("이력서 실패 알림 발송 완료 - resumeId: {}, taskId: {}", resumeId, taskId); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
...rzip/src/main/java/backend/techeerzip/domain/resume/monitoring/ResumeMetricsRecorder.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package backend.techeerzip.domain.resume.monitoring; | ||
|
|
||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import io.micrometer.core.instrument.Counter; | ||
| import io.micrometer.core.instrument.MeterRegistry; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class ResumeMetricsRecorder { | ||
| private final MeterRegistry meterRegistry; | ||
|
|
||
| public void markTotal() { | ||
| Counter.builder("resume_extraction_total").register(meterRegistry).increment(); | ||
| } | ||
|
|
||
| public void markFail(String reason, String stage) { | ||
| Counter.builder("resume_extraction_fail_total") | ||
| .tag("reason", reason) | ||
| .tag("stage", stage) | ||
| .register(meterRegistry) | ||
| .increment(); | ||
| } | ||
| } |
105 changes: 105 additions & 0 deletions
105
techeerzip/src/main/java/backend/techeerzip/domain/resume/scheduler/ResumeTaskWatchdog.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| package backend.techeerzip.domain.resume.scheduler; | ||
|
|
||
| import java.time.Duration; | ||
| import java.time.Instant; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
|
|
||
| import org.springframework.data.redis.core.RedisTemplate; | ||
| import org.springframework.scheduling.annotation.Scheduled; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import backend.techeerzip.domain.resume.alert.ResumeAlertNotifier; | ||
| import backend.techeerzip.domain.resume.monitoring.ResumeMetricsRecorder; | ||
| import backend.techeerzip.domain.resume.support.ResumeTaskContextExtractor; | ||
| import backend.techeerzip.infra.redis.RedisTaskReader; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| @Slf4j | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class ResumeTaskWatchdog { | ||
| private final RedisTemplate<String, String> redisTemplate; | ||
| private final RedisTaskReader redisTaskReader; | ||
| private final ResumeAlertNotifier alertNotifier; | ||
| private final ResumeMetricsRecorder metrics; | ||
|
|
||
| private static final Duration TIMEOUT = Duration.ofMinutes(20); | ||
|
|
||
| @Scheduled(fixedRate = 60_000) | ||
| public void checkTimeouts() { | ||
| try { | ||
| // "resume_extraction-<userId>-<resumeId>" 형식 | ||
| final Set<String> taskIds = redisTemplate.keys("resume_extraction-*"); | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (taskIds == null || taskIds.isEmpty()) { | ||
| return; | ||
| } | ||
| final Instant now = Instant.now(); | ||
|
|
||
| for (String taskId : taskIds) { | ||
| try { | ||
| final Map<String, Object> details = redisTaskReader.read(taskId); | ||
| final Long userId = parseLong(details.get("userId")); | ||
| final Long resumeId = ResumeTaskContextExtractor.extractResumeId(taskId); | ||
|
|
||
| final Instant createdAt = parseInstant(details.get("createdAt")); | ||
| final String status = String.valueOf(details.getOrDefault("status", "")); | ||
| final boolean processed = | ||
| "PROCESSED".equalsIgnoreCase(status) | ||
| || "COMPLETED".equalsIgnoreCase(status); | ||
| final boolean hasResult = details.containsKey("result"); | ||
|
|
||
| if (createdAt != null && !processed && createdAt.isBefore(now.minus(TIMEOUT))) { | ||
| metrics.markFail("worker_timeout", "worker_execute"); | ||
| alertNotifier.notify( | ||
| "worker_timeout", | ||
| "worker_execute", | ||
| taskId, | ||
| userId, | ||
| resumeId, | ||
| "no-completion-within-20m"); | ||
| continue; | ||
| } | ||
|
|
||
| if (processed && !hasResult) { | ||
| metrics.markFail("result_missing", "worker_execute"); | ||
| alertNotifier.notify( | ||
| "result_missing", | ||
| "worker_execute", | ||
| taskId, | ||
| userId, | ||
| resumeId, | ||
| "processed-without-result"); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } catch (Exception e) { | ||
| log.warn( | ||
| "Watchdog inspection failed - taskId: {}, err: {}", | ||
| taskId, | ||
| e.getMessage()); | ||
| } | ||
| } | ||
| } catch (Exception e) { | ||
| log.warn("Watchdog scan failed - err: {}", e.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| private static Long parseLong(Object v) { | ||
| try { | ||
| if (v == null) return null; | ||
| return Long.parseLong(String.valueOf(v)); | ||
| } catch (Exception e) { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| private static Instant parseInstant(Object v) { | ||
| try { | ||
| if (v == null) return null; | ||
| long epochMillis = Long.parseLong(String.valueOf(v)); | ||
| return Instant.ofEpochMilli(epochMillis); | ||
| } catch (Exception e) { | ||
| return null; | ||
| } | ||
| } | ||
| } | ||
46 changes: 46 additions & 0 deletions
46
...erzip/src/main/java/backend/techeerzip/domain/resume/support/ResumeFailureClassifier.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package backend.techeerzip.domain.resume.support; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
|
|
||
| public class ResumeFailureClassifier { | ||
| private static final ObjectMapper mapper = new ObjectMapper(); | ||
|
|
||
| public static Optional<FailureInfo> classify(String resultJson) { | ||
| try { | ||
| final JsonNode root = mapper.readTree(resultJson); | ||
| final String errorCode = | ||
| getFirstNonNullText(root, "errorCode", "error", "code", "status"); | ||
| if (errorCode == null) return Optional.empty(); | ||
|
|
||
| if (match(errorCode, "download_failed", "unauthorized_url", "not_found")) { | ||
| return Optional.of(new FailureInfo("worker_download", errorCode)); | ||
| } | ||
| if (match(errorCode, "parsing_failed", "ocr_failed", "model_error")) { | ||
| return Optional.of(new FailureInfo("worker_parse", errorCode)); | ||
| } | ||
| return Optional.empty(); | ||
| } catch (Exception e) { | ||
| return Optional.empty(); | ||
| } | ||
| } | ||
|
|
||
| private static boolean match(String code, String... candidates) { | ||
| for (String c : candidates) if (c.equalsIgnoreCase(code)) return true; | ||
| return false; | ||
| } | ||
|
|
||
| private static String getFirstNonNullText(JsonNode node, String... keys) { | ||
| for (String k : keys) { | ||
| if (node.has(k) && !node.get(k).isNull()) { | ||
| final String v = node.get(k).asText(null); | ||
| if (v != null && !v.isBlank()) return v; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| public record FailureInfo(String stage, String reason) {} | ||
| } |
11 changes: 11 additions & 0 deletions
11
...ip/src/main/java/backend/techeerzip/domain/resume/support/ResumeTaskContextExtractor.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package backend.techeerzip.domain.resume.support; | ||
|
|
||
| import backend.techeerzip.domain.task.dto.TaskIdInfo; | ||
| import backend.techeerzip.domain.task.util.TaskIdHandler; | ||
|
|
||
| public class ResumeTaskContextExtractor { | ||
| public static Long extractResumeId(String taskId) { | ||
| final TaskIdInfo info = TaskIdHandler.extractTaskId(taskId); | ||
| return info.getDomainId(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.