From cb0c0a0de62f079b5df0daa505e5684b49795552 Mon Sep 17 00:00:00 2001 From: Hogeun Lee Date: Sat, 5 Sep 2026 17:24:31 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[FIX/#447]=20Micrometer=20=EB=A9=94?= =?UTF-8?q?=ED=8A=B8=EB=A6=AD=EC=97=90=20application=20=ED=83=9C=EA=B7=B8?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Grafana Cloud로 데이터는 정상 수신되나, 커뮤니티 JVM 대시보드가 기대하는 application 라벨이 없어 대시보드 전체가 N/A로 표시되는 문제 수정 - management.metrics.tags.application 설정으로 표준 Micrometer application 태그 노출 Co-Authored-By: Claude Sonnet 5 --- src/main/resources/application.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 35db97f2..8a25caf1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -56,3 +56,6 @@ management: health: probes: enabled: true + metrics: + tags: + application: ${spring.application.name} From e8fba974a049b9798d1606256944d745d30a0229 Mon Sep 17 00:00:00 2001 From: Hogeun Lee Date: Sat, 5 Sep 2026 17:29:40 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[FEAT/#447]=20http.server.requests=20?= =?UTF-8?q?=ED=9E=88=EC=8A=A4=ED=86=A0=EA=B7=B8=EB=9E=A8=20=EB=B2=84?= =?UTF-8?q?=ED=82=B7=20=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - percentiles-histogram 없이는 p95/p99 지연율 계산이 불가능해 SLO 대시보드 구축을 위해 활성화 Co-Authored-By: Claude Sonnet 5 --- src/main/resources/application.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8a25caf1..582cd4a2 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -59,3 +59,6 @@ management: metrics: tags: application: ${spring.application.name} + distribution: + percentiles-histogram: + http.server.requests: true From 206c8f522668484b24a4fa83fb85d0d2f6a1cd82 Mon Sep 17 00:00:00 2001 From: Hogeun Lee Date: Sun, 6 Sep 2026 19:06:29 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[FEAT/#447]=20request-id=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=20=ED=95=84=ED=84=B0=20=EB=B0=8F=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=A9=94=ED=8A=B8?= =?UTF-8?q?=EB=A6=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RequestIdFilter 추가: 요청마다 UUID 발급, MDC/응답헤더(X-Request-Id)에 기록 (메트릭/Loki 라벨에는 미포함이라 카디널리티 영향 없음) - logging.pattern.console에 reqId 출력 추가 - notification: outbox 발행, FCM 발송, 재시도 Counter 추가 - certification: 그룹 인증(WebSocket) 성공/실패 Counter, 소요시간 Timer 추가 - chat: 메시지 전송 Counter, 활성 세션 Gauge 추가 Co-Authored-By: Claude Sonnet 5 --- .../GroupCertificationController.java | 11 +++++- .../domain/chat/service/ChatServiceImpl.java | 3 ++ .../service/NotificationListener.java | 8 ++++ .../service/OutboxAfterCommitPublisher.java | 4 ++ .../service/OutboxRetryProcessor.java | 5 ++- .../server/global/filter/RequestIdFilter.java | 38 +++++++++++++++++++ .../server/global/util/PresenceTracker.java | 7 ++++ src/main/resources/application.yml | 4 ++ 8 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/assu/server/global/filter/RequestIdFilter.java diff --git a/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java b/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java index bd9bd0fa..6e9f9746 100644 --- a/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java +++ b/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java @@ -14,6 +14,8 @@ import com.assu.server.domain.certification.service.CertificationService; import com.assu.server.global.util.PrincipalDetails; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -26,6 +28,7 @@ public class GroupCertificationController { private final CertificationService certificationService; + private final MeterRegistry meterRegistry; @MessageMapping("/certify") @Operation( @@ -44,15 +47,21 @@ public CertificationProgressResponseDTO certifyGroup(@Payload GroupSessionReques UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken)principal; PrincipalDetails principalDetails = (PrincipalDetails)auth.getPrincipal(); + Timer.Sample sample = Timer.start(meterRegistry); try { log.info("### SUCCESS ### 인증 요청 메시지 수신 - user: {}, adminId: {}, sessionId: {}", principalDetails.getUsername(), dto.adminId(), dto.sessionId()); if (principalDetails != null) { - return certificationService.handleCertification(dto, principalDetails.getMember()); + CertificationProgressResponseDTO result = certificationService.handleCertification(dto, principalDetails.getMember()); + meterRegistry.counter("certification.group.result", "result", "success").increment(); + return result; } } catch (Exception e) { log.error("### ERROR ### 인증 처리 중 오류 발생: {}", e.getMessage(), e); + meterRegistry.counter("certification.group.result", "result", "failure").increment(); + } finally { + sample.stop(meterRegistry.timer("certification.group.duration")); } } return null; diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index 8bbf1a6b..61d55248 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -21,6 +21,7 @@ import com.assu.server.global.exception.DatabaseException; import com.assu.server.global.exception.GeneralException; import com.assu.server.global.util.PresenceTracker; +import io.micrometer.core.instrument.MeterRegistry; import jakarta.transaction.Transactional; import org.springframework.dao.DataIntegrityViolationException; import lombok.RequiredArgsConstructor; @@ -45,6 +46,7 @@ public class ChatServiceImpl implements ChatService { private final NotificationCommandService notificationCommandService; private final PresenceTracker presenceTracker; private final BlockRepository blockRepository; + private final MeterRegistry meterRegistry; @Override @@ -115,6 +117,7 @@ public MessageHandlingResult handleMessage(ChatRequestDTO.ChatMessageRequestDTO // 3. 메시지 저장 (기존 로직) Message message = Message.toMessageEntity(request, room, sender, receiver, unreadForSender); Message saved = messageRepository.saveAndFlush(message); + meterRegistry.counter("chat.message.sent").increment(); log.info("saved message id={}, roomId={}, senderId={}, receiverId={}", saved.getId(), room.getId(), sender.getId(), receiver.getId()); diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationListener.java b/src/main/java/com/assu/server/domain/notification/service/NotificationListener.java index 25dcc400..2ae14a34 100644 --- a/src/main/java/com/assu/server/domain/notification/service/NotificationListener.java +++ b/src/main/java/com/assu/server/domain/notification/service/NotificationListener.java @@ -8,6 +8,7 @@ import com.assu.server.domain.notification.dto.NotificationMessageDTO; import com.google.firebase.messaging.FirebaseMessagingException; import com.rabbitmq.client.Channel; +import io.micrometer.core.instrument.MeterRegistry; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitListener; @@ -28,6 +29,7 @@ public class NotificationListener { private final FcmClient fcmClient; private final OutboxStatusService outboxStatus; private final ApplicationEventPublisher eventPublisher; + private final MeterRegistry meterRegistry; @RabbitListener(queues = AmqpConfig.QUEUE, ackMode = "MANUAL") public void onMessage(@Payload NotificationMessageDTO notificationMessageDTO, @@ -67,11 +69,17 @@ private void sendNotification(NotificationMessageDTO dto, Long outboxId) if (outboxId != null) outboxStatus.markSent(outboxId); + meterRegistry.counter("notification.fcm.send", "result", "success").increment(result.successCount()); + if (result.failureCount() > 0) { + meterRegistry.counter("notification.fcm.send", "result", "failure").increment(result.failureCount()); + } + log.info("[Notify] sent outboxId={} memberId={} success={} fail={} invalidTokens={}", outboxId, dto.receiverId(), result.successCount(), result.failureCount(), result.invalidTokens()); } private void handleException(Exception e, Long outboxId, Long memberId) { + meterRegistry.counter("notification.fcm.send", "result", "exception").increment(); if (outboxId != null) { outboxStatus.markFailed(outboxId); diff --git a/src/main/java/com/assu/server/domain/notification/service/OutboxAfterCommitPublisher.java b/src/main/java/com/assu/server/domain/notification/service/OutboxAfterCommitPublisher.java index a18fc0ae..cd28d1fd 100644 --- a/src/main/java/com/assu/server/domain/notification/service/OutboxAfterCommitPublisher.java +++ b/src/main/java/com/assu/server/domain/notification/service/OutboxAfterCommitPublisher.java @@ -6,6 +6,7 @@ import com.assu.server.domain.notification.event.NotificationFailedEvent; import com.assu.server.infra.messaging.AmqpConfig; import com.assu.server.infra.messaging.ConditionalOnRabbitEnabled; +import io.micrometer.core.instrument.MeterRegistry; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; @@ -24,6 +25,7 @@ public class OutboxAfterCommitPublisher { private final RabbitTemplate rabbit; private final OutboxStatusService outboxStatus; private final ApplicationEventPublisher eventPublisher; + private final MeterRegistry meterRegistry; @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onOutboxCreated(OutboxCreatedEvent e) { @@ -44,8 +46,10 @@ public void onOutboxCreated(OutboxCreatedEvent e) { rabbit.convertAndSend(AmqpConfig.EXCHANGE, AmqpConfig.ROUTING_KEY, dto); log.info("[Outbox] Message sent to queue for outboxId={}", e.getOutboxId()); outboxStatus.markDispatched(e.getOutboxId()); + meterRegistry.counter("notification.outbox.publish", "result", "success").increment(); } catch (Exception ex) { log.error("[Outbox] Failed to send message for outboxId={}", e.getOutboxId(), ex); + meterRegistry.counter("notification.outbox.publish", "result", "failure").increment(); // 큐 전송 실패 시 재시도 이벤트 발행 eventPublisher.publishEvent(new NotificationFailedEvent(e.getOutboxId(), 0)); } diff --git a/src/main/java/com/assu/server/domain/notification/service/OutboxRetryProcessor.java b/src/main/java/com/assu/server/domain/notification/service/OutboxRetryProcessor.java index 64dfb404..aebd5ce2 100644 --- a/src/main/java/com/assu/server/domain/notification/service/OutboxRetryProcessor.java +++ b/src/main/java/com/assu/server/domain/notification/service/OutboxRetryProcessor.java @@ -4,6 +4,7 @@ import com.assu.server.domain.notification.entity.OutboxCreatedEvent; import com.assu.server.domain.notification.event.NotificationFailedEvent; import com.assu.server.domain.notification.repository.NotificationOutboxRepository; +import io.micrometer.core.instrument.MeterRegistry; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; @@ -19,9 +20,11 @@ public class OutboxRetryProcessor { private final NotificationOutboxRepository outboxRepository; private final ApplicationEventPublisher eventPublisher; - + private final MeterRegistry meterRegistry; + @Transactional(propagation = Propagation.REQUIRES_NEW) public void processRetry(NotificationOutbox outbox) { + meterRegistry.counter("notification.outbox.retry").increment(); try { outbox.incrementRetryCount(); outboxRepository.save(outbox); diff --git a/src/main/java/com/assu/server/global/filter/RequestIdFilter.java b/src/main/java/com/assu/server/global/filter/RequestIdFilter.java new file mode 100644 index 00000000..303b6d0a --- /dev/null +++ b/src/main/java/com/assu/server/global/filter/RequestIdFilter.java @@ -0,0 +1,38 @@ +package com.assu.server.global.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.MDC; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.UUID; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class RequestIdFilter extends OncePerRequestFilter { + + public static final String REQUEST_ID_HEADER = "X-Request-Id"; + public static final String MDC_KEY = "requestId"; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + String requestId = UUID.randomUUID().toString(); + MDC.put(MDC_KEY, requestId); + response.setHeader(REQUEST_ID_HEADER, requestId); + try { + filterChain.doFilter(request, response); + } finally { + MDC.remove(MDC_KEY); + } + } +} diff --git a/src/main/java/com/assu/server/global/util/PresenceTracker.java b/src/main/java/com/assu/server/global/util/PresenceTracker.java index 549d8694..367c45a9 100644 --- a/src/main/java/com/assu/server/global/util/PresenceTracker.java +++ b/src/main/java/com/assu/server/global/util/PresenceTracker.java @@ -1,5 +1,7 @@ package com.assu.server.global.util; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.messaging.simp.stomp.StompHeaderAccessor; @@ -22,6 +24,11 @@ public class PresenceTracker { // "sessionId:subscriptionId" -> roomId: 특정 구독 해제 시 어느 방인지 추적 private final Map subToRoom = new ConcurrentHashMap<>(); + public PresenceTracker(MeterRegistry meterRegistry) { + Gauge.builder("chat.active.sessions", sessionToMember, Map::size) + .register(meterRegistry); + } + private Long parseRoomId(String dest) { // "/sub/chat/26" -> 26 if (dest == null) return null; String[] p = dest.split("/"); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 582cd4a2..8b2a1848 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -47,6 +47,10 @@ spring: server: shutdown: graceful +logging: + pattern: + console: "%clr(%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} [reqId=%X{requestId}] %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}" + management: endpoints: web: From 3fc073f199accfce8c20cc40375e43d3fe0b23a7 Mon Sep 17 00:00:00 2001 From: Hogeun Lee Date: Sun, 6 Sep 2026 19:07:24 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[FEAT/#447]=20manifest=20=EC=84=9C=EB=B8=8C?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=ED=8F=AC=EC=9D=B8=ED=84=B0=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0=20(=EC=95=B1=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C?= =?UTF-8?q?=20JSON)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 --- manifest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest b/manifest index 4da8233d..33900e51 160000 --- a/manifest +++ b/manifest @@ -1 +1 @@ -Subproject commit 4da8233d2a0931d6f1d7c8460f3c516e6ac0dc31 +Subproject commit 33900e512ae1fd80e0c18279172226948362db58 From c30a1f57236fd53a7bf45fe1fe1035379097bded Mon Sep 17 00:00:00 2001 From: Hogeun Lee Date: Sun, 13 Sep 2026 21:33:19 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[FIX/#447]=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EB=A9=94=ED=8A=B8=EB=A6=AD=20=EC=A7=91=EA=B3=84=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 그룹 인증 결과 Counter를 응답 type(completed/progress/mismatch) 기준으로 집계해 mismatch가 성공으로 집계되던 문제 수정 - chat.message.sent를 트랜잭션 커밋 후 증가시켜 롤백된 메시지가 집계되지 않도록 수정 - ChatServiceImplTest에 MeterRegistry 주입이 없어 발생한 NPE(CI 실패) 수정 - HTTP/그룹 인증 지연 히스토그램을 SLO 버킷으로 제한해 시계열 수 절감 (Grafana Cloud 무료 티어 10k 한도 대응) Co-Authored-By: Claude Opus 5 --- .../GroupCertificationController.java | 2 +- .../domain/chat/service/ChatServiceImpl.java | 18 +++++++++++++++++- src/main/resources/application.yml | 5 +++-- .../chat/service/ChatServiceImplTest.java | 8 ++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java b/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java index 6e9f9746..db2d146b 100644 --- a/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java +++ b/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java @@ -54,7 +54,7 @@ public CertificationProgressResponseDTO certifyGroup(@Payload GroupSessionReques if (principalDetails != null) { CertificationProgressResponseDTO result = certificationService.handleCertification(dto, principalDetails.getMember()); - meterRegistry.counter("certification.group.result", "result", "success").increment(); + meterRegistry.counter("certification.group.result", "result", result.type()).increment(); return result; } } catch (Exception e) { diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index 61d55248..e9df8f52 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -21,6 +21,7 @@ import com.assu.server.global.exception.DatabaseException; import com.assu.server.global.exception.GeneralException; import com.assu.server.global.util.PresenceTracker; +import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import jakarta.transaction.Transactional; import org.springframework.dao.DataIntegrityViolationException; @@ -28,6 +29,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.ArrayList; import java.util.List; @@ -117,7 +120,7 @@ public MessageHandlingResult handleMessage(ChatRequestDTO.ChatMessageRequestDTO // 3. 메시지 저장 (기존 로직) Message message = Message.toMessageEntity(request, room, sender, receiver, unreadForSender); Message saved = messageRepository.saveAndFlush(message); - meterRegistry.counter("chat.message.sent").increment(); + incrementAfterCommit(meterRegistry.counter("chat.message.sent")); log.info("saved message id={}, roomId={}, senderId={}, receiverId={}", saved.getId(), room.getId(), sender.getId(), receiver.getId()); @@ -258,4 +261,17 @@ public ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomI } return new ChatResponseDTO.LeaveChattingRoomResponseDTO(roomId, isLeftSuccessfully,isRoomDeleted); } + + private void incrementAfterCommit(Counter counter) { + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + counter.increment(); + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + counter.increment(); + } + }); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8b2a1848..1b96ecba 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -64,5 +64,6 @@ management: tags: application: ${spring.application.name} distribution: - percentiles-histogram: - http.server.requests: true + slo: + http.server.requests: 50ms,100ms,200ms,500ms,800ms,1s,2s,5s + certification.group.duration: 100ms,500ms,1s,3s,10s diff --git a/src/test/java/com/assu/server/domain/chat/service/ChatServiceImplTest.java b/src/test/java/com/assu/server/domain/chat/service/ChatServiceImplTest.java index 70641431..363521ea 100644 --- a/src/test/java/com/assu/server/domain/chat/service/ChatServiceImplTest.java +++ b/src/test/java/com/assu/server/domain/chat/service/ChatServiceImplTest.java @@ -10,12 +10,16 @@ import java.util.List; import java.util.Optional; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.messaging.simp.SimpMessagingTemplate; @@ -78,6 +82,9 @@ class ChatServiceImplTest { @Mock private BlockRepository blockRepository; + @Spy + private MeterRegistry meterRegistry = new SimpleMeterRegistry(); + private static final Long ADMIN_ID = 10L; private static final Long PARTNER_ID = 20L; private static final Long ROOM_ID = 1L; @@ -260,6 +267,7 @@ void handleMessage_ReceiverInRoom_NoNotification() { verify(messageRepository).saveAndFlush(captor.capture()); assertEquals(0, captor.getValue().getUnreadCount()); assertTrue(captor.getValue().isRead()); + assertEquals(1.0, meterRegistry.counter("chat.message.sent").count()); } @Test From ebcfe8c1b5fae440aec7427cf6974e9b87a3910a Mon Sep 17 00:00:00 2001 From: Hogeun Lee Date: Sun, 13 Sep 2026 21:35:40 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[FIX/#447]=20manifest=20=EC=84=9C=EB=B8=8C?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=ED=8F=AC=EC=9D=B8=ED=84=B0=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0=20(=ED=81=B4=EB=9F=AC=EC=8A=A4=ED=84=B0=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=88=ED=84=B0=EB=A7=81=20=ED=99=9C=EC=84=B1=ED=99=94,=20Lo?= =?UTF-8?q?ki=20env=20=EB=9D=BC=EB=B2=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 --- manifest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest b/manifest index 6a95304e..72095f3f 160000 --- a/manifest +++ b/manifest @@ -1 +1 @@ -Subproject commit 6a95304e98dc46b016343cb52621a3bb46a0054c +Subproject commit 72095f3f589da45c92a7e55ccb6d69ee7623ca7b From 16c2b76a16576292ee096f3fa17e9c22549b4849 Mon Sep 17 00:00:00 2001 From: Hogeun Lee Date: Tue, 15 Sep 2026 22:56:46 +0900 Subject: [PATCH 7/7] =?UTF-8?q?[FIX/#447]=20CodeRabbit=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81:=20SLO=20=EC=83=81=EC=9C=84=20?= =?UTF-8?q?=EB=B2=84=ED=82=B7=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B=20=ED=9B=84=20=EC=B9=B4=EC=9A=B4=ED=84=B0=20=EC=A6=9D?= =?UTF-8?q?=EA=B0=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - http.server.requests(10s,30s), certification.group.duration(30s) 상위 버킷 추가: 최상위 유한 버킷에서 p95/p99가 포화돼 tail latency가 가려지는 문제 방지 - 트랜잭션 동기화 활성 상태에서 chat.message.sent가 커밋 전 0, afterCommit 후 1인지 검증하는 테스트 추가 Co-Authored-By: Claude Opus 5 --- src/main/resources/application.yml | 4 +-- .../chat/service/ChatServiceImplTest.java | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1b96ecba..bcb20193 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -65,5 +65,5 @@ management: application: ${spring.application.name} distribution: slo: - http.server.requests: 50ms,100ms,200ms,500ms,800ms,1s,2s,5s - certification.group.duration: 100ms,500ms,1s,3s,10s + http.server.requests: 50ms,100ms,200ms,500ms,800ms,1s,2s,5s,10s,30s + certification.group.duration: 100ms,500ms,1s,3s,10s,30s diff --git a/src/test/java/com/assu/server/domain/chat/service/ChatServiceImplTest.java b/src/test/java/com/assu/server/domain/chat/service/ChatServiceImplTest.java index 363521ea..f0c4acdc 100644 --- a/src/test/java/com/assu/server/domain/chat/service/ChatServiceImplTest.java +++ b/src/test/java/com/assu/server/domain/chat/service/ChatServiceImplTest.java @@ -22,6 +22,8 @@ import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import com.assu.server.domain.admin.entity.Admin; import com.assu.server.domain.admin.repository.AdminRepository; @@ -270,6 +272,39 @@ void handleMessage_ReceiverInRoom_NoNotification() { assertEquals(1.0, meterRegistry.counter("chat.message.sent").count()); } + @Test + @DisplayName("트랜잭션 동기화가 활성화되어 있으면 메시지 전송 카운터는 커밋 이후에 증가한다") + void handleMessage_WithTransactionSynchronization_IncrementsCounterAfterCommit() { + // 1. Given + ChatRequestDTO.ChatMessageRequestDTO request = + new ChatRequestDTO.ChatMessageRequestDTO(ROOM_ID, PARTNER_ID, ADMIN_ID, "안녕하세요"); + + ChattingRoom room = ChattingRoom.builder().id(ROOM_ID).build(); + when(chatRepository.findById(ROOM_ID)).thenReturn(Optional.of(room)); + Member sender = givenMember(PARTNER_ID); + Member receiver = givenMember(ADMIN_ID); + when(presenceTracker.isInRoom(ADMIN_ID, ROOM_ID)).thenReturn(true); + + Message saved = Message.builder() + .id(100L).chattingRoom(room).sender(sender).receiver(receiver) + .message("안녕하세요").unreadCount(0).isRead(true).type(MessageType.TEXT) + .build(); + when(messageRepository.saveAndFlush(any(Message.class))).thenReturn(saved); + + TransactionSynchronizationManager.initSynchronization(); + try { + // 2. When + chatService.handleMessage(request); + + // 3. Then + assertEquals(0.0, meterRegistry.counter("chat.message.sent").count()); + TransactionSynchronizationManager.getSynchronizations().forEach(TransactionSynchronization::afterCommit); + assertEquals(1.0, meterRegistry.counter("chat.message.sent").count()); + } finally { + TransactionSynchronizationManager.clearSynchronization(); + } + } + @Test @DisplayName("수신자가 부재중이면 안읽음 메시지로 저장되고 채팅방 업데이트와 푸시 알림이 발생한다") void handleMessage_ReceiverAbsent_SendsNotificationAndUpdates() {