Skip to content

Commit 202229e

Browse files
committed
fix: Parallel Call Observation Add And DB/Redis Observation Add #105
1 parent 8378704 commit 202229e

6 files changed

Lines changed: 68 additions & 17 deletions

File tree

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ dependencies {
103103
implementation 'io.micrometer:micrometer-tracing-bridge-otel'
104104
// OTLP Exporter — Alloy로 트레이스 전송
105105
implementation 'io.opentelemetry:opentelemetry-exporter-otlp'
106+
// DB (JDBC/JPA) 쿼리 단위 Span — 각 SQL 쿼리를 child Span으로 계측
107+
implementation 'net.ttddyy.observation:datasource-micrometer-spring-boot:1.0.6'
106108

107109
//resilience4j
108110
implementation 'io.github.resilience4j:resilience4j-spring-boot3'

src/main/java/sopt/comfit/global/config/RedisConfig.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package sopt.comfit.global.config;
22

3+
import io.lettuce.core.tracing.MicrometerTracing;
4+
import io.micrometer.observation.ObservationRegistry;
5+
import org.springframework.boot.autoconfigure.data.redis.ClientResourcesBuilderCustomizer;
36
import org.springframework.cache.annotation.EnableCaching;
47
import org.springframework.context.annotation.Bean;
58
import org.springframework.context.annotation.Configuration;
@@ -17,6 +20,15 @@
1720
@Configuration
1821
public class RedisConfig {
1922

23+
/**
24+
* Lettuce Redis 커맨드를 Micrometer Observation으로 계측 → Tempo에 child Span으로 전송
25+
* includeCommandArgsInSpanTags=true : GET key, SET key value 등 인자를 span tag로 기록
26+
*/
27+
@Bean
28+
public ClientResourcesBuilderCustomizer lettuceTracingCustomizer(ObservationRegistry observationRegistry) {
29+
return builder -> builder.tracing(new MicrometerTracing(observationRegistry, "redis", true));
30+
}
31+
2032
@Bean
2133
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
2234
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();

src/main/java/sopt/comfit/report/job/AIReportJobService.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package sopt.comfit.report.job;
22

3+
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
4+
import io.opentelemetry.context.Context;
35
import lombok.RequiredArgsConstructor;
46
import org.springframework.context.ApplicationEventPublisher;
57
import org.springframework.data.redis.core.StringRedisTemplate;
@@ -12,6 +14,9 @@
1214
import sopt.comfit.report.dto.command.MatchExperienceCommandDto;
1315
import sopt.comfit.report.exception.AIReportErrorCode;
1416

17+
import java.util.HashMap;
18+
import java.util.Map;
19+
1520
@Service
1621
@RequiredArgsConstructor
1722
public class AIReportJobService {
@@ -36,7 +41,13 @@ public Long createJob(MatchExperienceCommandDto command) {
3641

3742
reportJobRepository.save(job);
3843

39-
eventPublisher.publishEvent(new JobCreatedEvent(job.getId()));
44+
// 현재 HTTP 요청의 trace context를 W3C traceparent 포맷으로 추출
45+
// Worker에서 복원해 job.process span을 HTTP 요청 trace의 child로 연결
46+
Map<String, String> carrier = new HashMap<>();
47+
W3CTraceContextPropagator.getInstance().inject(Context.current(), carrier, Map::put);
48+
String traceparent = carrier.get("traceparent");
49+
50+
eventPublisher.publishEvent(new JobCreatedEvent(job.getId(), traceparent));
4051

4152
return job.getId();
4253
}

src/main/java/sopt/comfit/report/job/AIReportJobWorker.java

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import io.micrometer.observation.Observation;
44
import io.micrometer.observation.ObservationRegistry;
5+
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
6+
import io.opentelemetry.context.Context;
7+
import io.opentelemetry.context.propagation.TextMapGetter;
58
import jakarta.annotation.PostConstruct;
69
import jakarta.annotation.PreDestroy;
710
import lombok.RequiredArgsConstructor;
@@ -86,10 +89,16 @@ private void guardedListen(int index) {
8689
private void listen() {
8790
while (!Thread.currentThread().isInterrupted() && running.get()) {
8891
try {
89-
String jobId = redisTemplate.opsForList()
92+
String raw = redisTemplate.opsForList()
9093
.rightPop(Constants.JOB_QUEUE_KEY, Duration.ofSeconds(30));
91-
if (jobId == null) continue;
92-
processJob(Long.parseLong(jobId));
94+
if (raw == null) continue;
95+
96+
// "jobId|traceparent" 또는 "jobId" 형태 파싱
97+
String[] parts = raw.split("\\|", 2);
98+
Long jobId = Long.parseLong(parts[0]);
99+
String traceparent = parts.length > 1 ? parts[1] : null;
100+
101+
processJob(jobId, traceparent);
93102

94103
} catch (QueryTimeoutException e) {
95104
log.debug("BRPOP timeout, 재시도");
@@ -109,14 +118,29 @@ private void sleep(int seconds) {
109118
}
110119
}
111120

112-
private void processJob(Long jobId) {
113-
// 루트 Span 생성 — Worker는 HTTP 요청이 아니라 Micrometer가 자동 생성 안 함
114-
// Observation을 시작하면 traceId/spanId가 MDC에 자동 주입되고
115-
// 내부의 Feign 호출(perspectives, density 등)이 child Span으로 자동 연결됨
116-
Observation observation = Observation.createNotStarted("job.process", observationRegistry)
117-
.lowCardinalityKeyValue("jobId", String.valueOf(jobId));
118-
119-
observation.observe(() -> {
121+
private void processJob(Long jobId, String traceparent) {
122+
// traceparent가 있으면 HTTP 요청 trace의 child span으로 연결
123+
// 없으면 새 root trace 시작 (이전 버전 Redis 값 대비 방어)
124+
Context parentContext = traceparent != null
125+
? W3CTraceContextPropagator.getInstance().extract(
126+
Context.root(), traceparent,
127+
new TextMapGetter<String>() {
128+
@Override
129+
public Iterable<String> keys(String carrier) {
130+
return java.util.List.of("traceparent");
131+
}
132+
@Override
133+
public String get(String carrier, String key) {
134+
return "traceparent".equals(key) ? carrier : null;
135+
}
136+
})
137+
: Context.current();
138+
139+
try (io.opentelemetry.context.Scope ignored = parentContext.makeCurrent()) {
140+
Observation observation = Observation.createNotStarted("job.process", observationRegistry)
141+
.lowCardinalityKeyValue("jobId", String.valueOf(jobId));
142+
143+
observation.observe(() -> {
120144
try {
121145
// jobId는 Micrometer가 자동으로 안 넣어주므로 직접 설정
122146
MdcUtils.setJobId(jobId);
@@ -151,6 +175,7 @@ private void processJob(Long jobId) {
151175
MdcUtils.clear();
152176
}
153177
});
178+
} // try (parentContext.makeCurrent())
154179
}
155180

156181
private MatchExperienceCommandDto buildCommand(Long jobId) {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
package sopt.comfit.report.job;
22

3-
public record JobCreatedEvent(Long jobId) {
3+
public record JobCreatedEvent(Long jobId, String traceparent) {
44
}

src/main/java/sopt/comfit/report/job/JobEventListener.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ public class JobEventListener {
1515

1616
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
1717
public void handleJobCreated(JobCreatedEvent event) {
18-
stringRedisTemplate.opsForList().leftPush(
19-
Constants.JOB_QUEUE_KEY,
20-
String.valueOf(event.jobId())
21-
);
18+
// traceparent가 있으면 "jobId|traceparent" 형태로 저장해 Worker가 parent trace에 연결할 수 있도록
19+
String value = event.traceparent() != null
20+
? event.jobId() + "|" + event.traceparent()
21+
: String.valueOf(event.jobId());
22+
stringRedisTemplate.opsForList().leftPush(Constants.JOB_QUEUE_KEY, value);
2223
}
2324
}

0 commit comments

Comments
 (0)