diff --git a/build-logic/src/main/kotlin/transentia/KafkaConfigConventionPlugin.kt b/build-logic/src/main/kotlin/transentia/KafkaConfigConventionPlugin.kt index fc43b00..0c46bf7 100644 --- a/build-logic/src/main/kotlin/transentia/KafkaConfigConventionPlugin.kt +++ b/build-logic/src/main/kotlin/transentia/KafkaConfigConventionPlugin.kt @@ -2,7 +2,9 @@ package transentia import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension class KafkaConfigConventionPlugin : Plugin { override fun apply(target: Project) = with(target) { @@ -12,6 +14,12 @@ class KafkaConfigConventionPlugin : Plugin { pluginManager.apply("org.jetbrains.kotlin.kapt") pluginManager.apply("org.jetbrains.kotlin.plugin.allopen") pluginManager.apply("org.jetbrains.kotlin.plugin.spring") + + // JVM 21 설정 + extensions.configure { + jvmToolchain(21) + } + // 필요한 최소 의존성만 추가 dependencies { add("implementation", "org.springframework.boot:spring-boot-starter-validation") diff --git a/build-logic/src/main/kotlin/transentia/SpringBootAppConventionPlugin.kt b/build-logic/src/main/kotlin/transentia/SpringBootAppConventionPlugin.kt index cbe5749..717a5ba 100644 --- a/build-logic/src/main/kotlin/transentia/SpringBootAppConventionPlugin.kt +++ b/build-logic/src/main/kotlin/transentia/SpringBootAppConventionPlugin.kt @@ -43,9 +43,10 @@ class SpringBootAppConventionPlugin : Plugin { add("implementation", "org.springframework.boot:spring-boot-starter-json") add("implementation", "org.jetbrains.kotlin:kotlin-reflect") - // Observability - Actuator + Prometheus + // Observability - Actuator + Prometheus + Tracing add("implementation", "org.springframework.boot:spring-boot-starter-actuator") add("implementation", "io.micrometer:micrometer-registry-prometheus") + add("implementation", "io.micrometer:micrometer-tracing-bridge-otel") add("testImplementation", "org.springframework.boot:spring-boot-starter-test") } diff --git a/infrastructure/kafka/kafka-consumer/src/main/kotlin/io/github/hyungkishin/transentia/infrastructure/kafka/consumer/config/KafkaConsumerConfig.kt b/infrastructure/kafka/kafka-consumer/src/main/kotlin/io/github/hyungkishin/transentia/infrastructure/kafka/consumer/config/KafkaConsumerConfig.kt index a336f68..c72d751 100644 --- a/infrastructure/kafka/kafka-consumer/src/main/kotlin/io/github/hyungkishin/transentia/infrastructure/kafka/consumer/config/KafkaConsumerConfig.kt +++ b/infrastructure/kafka/kafka-consumer/src/main/kotlin/io/github/hyungkishin/transentia/infrastructure/kafka/consumer/config/KafkaConsumerConfig.kt @@ -101,6 +101,7 @@ class KafkaConsumerConfig( factory.containerProperties.apply { pollTimeout = kafkaConsumerConfigData.pollTimeoutMs ackMode = ContainerProperties.AckMode.MANUAL_IMMEDIATE + isObservationEnabled = true // 트레이싱 활성화 } return factory diff --git a/infrastructure/kafka/kafka-producer/build.gradle.kts b/infrastructure/kafka/kafka-producer/build.gradle.kts index 9ce2e23..ac35fcc 100644 --- a/infrastructure/kafka/kafka-producer/build.gradle.kts +++ b/infrastructure/kafka/kafka-producer/build.gradle.kts @@ -7,4 +7,4 @@ dependencies { implementation(project(":kafka-config")) implementation("org.apache.avro:avro:1.11.4") implementation("io.confluent:kafka-avro-serializer:7.9.2") -} \ No newline at end of file +} diff --git a/infrastructure/kafka/kafka-producer/src/main/kotlin/io/github/hyungkishin/transentia/infrastructure/kafka/producer/KafkaProducerConfig.kt b/infrastructure/kafka/kafka-producer/src/main/kotlin/io/github/hyungkishin/transentia/infrastructure/kafka/producer/KafkaProducerConfig.kt index 56f2ea9..64e33dd 100644 --- a/infrastructure/kafka/kafka-producer/src/main/kotlin/io/github/hyungkishin/transentia/infrastructure/kafka/producer/KafkaProducerConfig.kt +++ b/infrastructure/kafka/kafka-producer/src/main/kotlin/io/github/hyungkishin/transentia/infrastructure/kafka/producer/KafkaProducerConfig.kt @@ -41,6 +41,8 @@ class KafkaProducerConfig( @Bean fun kafkaTemplate(): KafkaTemplate { - return KafkaTemplate(producerFactory()) + return KafkaTemplate(producerFactory()).apply { + setObservationEnabled(true) + } } -} \ No newline at end of file +} diff --git a/infrastructure/kafka/kafka-producer/src/main/kotlin/io/github/hyungkishin/transentia/infrastructure/kafka/producer/service/impl/KafkaProducerImpl.kt b/infrastructure/kafka/kafka-producer/src/main/kotlin/io/github/hyungkishin/transentia/infrastructure/kafka/producer/service/impl/KafkaProducerImpl.kt index 2d41ff5..b75a621 100644 --- a/infrastructure/kafka/kafka-producer/src/main/kotlin/io/github/hyungkishin/transentia/infrastructure/kafka/producer/service/impl/KafkaProducerImpl.kt +++ b/infrastructure/kafka/kafka-producer/src/main/kotlin/io/github/hyungkishin/transentia/infrastructure/kafka/producer/service/impl/KafkaProducerImpl.kt @@ -50,4 +50,4 @@ class KafkaProducerImpl( return kafkaTemplate.send(topicName, message) } -} \ No newline at end of file +} diff --git a/services/fds/infra/src/main/kotlin/io/github/hyungkishin/transentia/infra/adapter/in/messaging/TransferEventConsumer.kt b/services/fds/infra/src/main/kotlin/io/github/hyungkishin/transentia/infra/adapter/in/messaging/TransferEventConsumer.kt index f1b56e2..5f6884c 100644 --- a/services/fds/infra/src/main/kotlin/io/github/hyungkishin/transentia/infra/adapter/in/messaging/TransferEventConsumer.kt +++ b/services/fds/infra/src/main/kotlin/io/github/hyungkishin/transentia/infra/adapter/in/messaging/TransferEventConsumer.kt @@ -2,10 +2,12 @@ package io.github.hyungkishin.transentia.infra.adapter.`in`.messaging import com.fasterxml.jackson.databind.ObjectMapper import io.github.hyungkishin.transentia.application.service.AnalyzeTransferService +import io.github.hyungkishin.transentia.infra.config.TracingTransformerSupplier import io.github.hyungkishin.transentia.infra.event.TransferEventMapper import io.github.hyungkishin.transentia.infrastructure.kafka.model.TransferEventAvroModel import org.apache.kafka.streams.kstream.KStream import org.slf4j.LoggerFactory +import org.slf4j.MDC import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import java.util.function.Function @@ -22,6 +24,7 @@ import java.util.function.Function * 특징: * - Stateless 처리 (이전 이벤트 참조 불필요) * - 실시간 차단/리뷰 판정 + * - TracingTransformer로 분산 트레이싱 컨텍스트 전파 */ @Configuration class TransferEventConsumer( @@ -35,15 +38,16 @@ class TransferEventConsumer( fun processTransferEvents(): Function, KStream> { return Function { input -> input + .transformValues(TracingTransformerSupplier()) .peek { key, event -> log.info( "[FDS단일분석] 이벤트 수신 - key={} eventId={} accountId={} amount={}", key, event.eventId, event.receiverId, event.amount ) } - .mapValues { _, event -> + .mapValues { event -> try { - // 1. Avro → Domain Event 변환 + // 1. Domain Event 변환 val domainEvent = transferEventMapper.toDomain(event) // 2. FDS 분석 실행 (Application Layer) @@ -92,6 +96,9 @@ class TransferEventConsumer( ) objectMapper.writeValueAsString(errorResult) + } finally { + // MDC 정리 + MDC.clear() } } } diff --git a/services/fds/infra/src/main/kotlin/io/github/hyungkishin/transentia/infra/config/TracingTransformer.kt b/services/fds/infra/src/main/kotlin/io/github/hyungkishin/transentia/infra/config/TracingTransformer.kt new file mode 100644 index 0000000..d10227b --- /dev/null +++ b/services/fds/infra/src/main/kotlin/io/github/hyungkishin/transentia/infra/config/TracingTransformer.kt @@ -0,0 +1,57 @@ +package io.github.hyungkishin.transentia.infra.config + +import io.github.hyungkishin.transentia.infrastructure.kafka.model.TransferEventAvroModel +import org.apache.kafka.streams.kstream.ValueTransformerWithKey +import org.apache.kafka.streams.kstream.ValueTransformerWithKeySupplier +import org.apache.kafka.streams.processor.ProcessorContext +import org.slf4j.MDC + +/** + * Kafka Record 헤더에서 traceparent를 추출하여 MDC에 설정하는 Transformer + * + * Kafka Streams는 일반 KafkaConsumer와 달리 observationEnabled가 적용되지 않아서 + * 수동으로 W3C Trace Context 헤더를 파싱해야 함 + * + * traceparent 형식: {version}-{traceId}-{spanId}-{flags} + * 예: 00-130c0e23e150eb0ec69d4a4774cc1f03-47684cf7bc701ad3-01 + */ +class TracingTransformer : ValueTransformerWithKey { + + private lateinit var context: ProcessorContext + + override fun init(context: ProcessorContext) { + this.context = context + } + + override fun transform(key: String?, value: TransferEventAvroModel): TransferEventAvroModel { + // 이전 MDC 정리 + MDC.clear() + + val headers = context.headers() + val traceparent = headers.lastHeader("traceparent")?.value()?.let { String(it) } + + if (traceparent != null) { + // traceparent 형식: 00-{traceId}-{spanId}-{flags} + val parts = traceparent.split("-") + if (parts.size >= 3) { + MDC.put("traceId", parts[1]) + MDC.put("spanId", parts[2]) + } + } + + return value + } + + override fun close() { + MDC.clear() + } +} + +/** + * TracingTransformer를 생성하는 Supplier + */ +class TracingTransformerSupplier : ValueTransformerWithKeySupplier { + override fun get(): ValueTransformerWithKey { + return TracingTransformer() + } +} diff --git a/services/fds/instances/api/src/main/resources/application-local.yml b/services/fds/instances/api/src/main/resources/application-local.yml new file mode 100644 index 0000000..80132f3 --- /dev/null +++ b/services/fds/instances/api/src/main/resources/application-local.yml @@ -0,0 +1,20 @@ +# ============================================ +# Local 환경용 설정 (Docker 호스트명 → localhost) +# ============================================ +spring: + kafka: + bootstrap-servers: localhost:9094 + cloud: + stream: + kafka: + binder: + brokers: localhost:9094 + streams: + binder: + brokers: localhost:9094 + configuration: + schema.registry.url: http://localhost:8085 + +kafka-config: + bootstrap-servers: localhost:9094 + schema-registry-url: http://localhost:8085 diff --git a/services/fds/instances/api/src/main/resources/application.yml b/services/fds/instances/api/src/main/resources/application.yml index 356384d..dbe98ac 100644 --- a/services/fds/instances/api/src/main/resources/application.yml +++ b/services/fds/instances/api/src/main/resources/application.yml @@ -4,6 +4,9 @@ server: spring: application: name: fds-service + autoconfigure: + exclude: + - org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration # ============================================ # Spring Cloud Stream 설정 @@ -120,7 +123,6 @@ management: tags: application: ${spring.application.name} tracing: - enabled: false - zipkin: - tracing: - endpoint: http://localhost:9999/disabled + enabled: true + sampling: + probability: 1.0 # 개발환경 100% 샘플링 diff --git a/services/fds/instances/api/src/main/resources/logback-spring.xml b/services/fds/instances/api/src/main/resources/logback-spring.xml index acd359a..5dcc5d5 100644 --- a/services/fds/instances/api/src/main/resources/logback-spring.xml +++ b/services/fds/instances/api/src/main/resources/logback-spring.xml @@ -1,9 +1,7 @@ - - [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [trace:%X{X-Trace-Id}] %logger{36} - %msg%n - + %d{HH:mm:ss.SSS} [%5level] [%X{traceId:-}] %logger{25} - %msg%n diff --git a/services/transfer/infra/src/main/kotlin/io/github/hyungkishin/transentia/infra/adapter/KafkaTransferEventPublisher.kt b/services/transfer/infra/src/main/kotlin/io/github/hyungkishin/transentia/infra/adapter/KafkaTransferEventPublisher.kt index 65afd2b..2e29758 100644 --- a/services/transfer/infra/src/main/kotlin/io/github/hyungkishin/transentia/infra/adapter/KafkaTransferEventPublisher.kt +++ b/services/transfer/infra/src/main/kotlin/io/github/hyungkishin/transentia/infra/adapter/KafkaTransferEventPublisher.kt @@ -9,11 +9,9 @@ import io.github.hyungkishin.transentia.infrastructure.kafka.model.TransferEvent import io.github.hyungkishin.transentia.infrastructure.kafka.model.TransferStatus import io.github.hyungkishin.transentia.infrastructure.kafka.producer.service.KafkaProducer import org.slf4j.LoggerFactory -import org.slf4j.MDC import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import java.time.Instant -import java.util.* @Component class KafkaTransferEventPublisher( @@ -41,7 +39,6 @@ class KafkaTransferEventPublisher( mapOf( "eventType" to "TRANSFER_COMPLETED", "eventVersion" to "v1", - "traceId" to (MDC.get("traceId") ?: UUID.randomUUID().toString()), "producer" to "transfer-api", "contentType" to "application/json" ) @@ -79,7 +76,6 @@ class KafkaTransferEventPublisher( mapOf( "eventType" to "TRANSFER_COMPLETED", "eventVersion" to "v1", - "traceId" to (MDC.get("traceId") ?: UUID.randomUUID().toString()), "producer" to "transfer-api-fallback", "contentType" to "application/json" ) diff --git a/services/transfer/instances/api/src/main/kotlin/io/github/hyungkishin/transentia/api/config/AsyncConfig.kt b/services/transfer/instances/api/src/main/kotlin/io/github/hyungkishin/transentia/api/config/AsyncConfig.kt index 353cd23..d655958 100644 --- a/services/transfer/instances/api/src/main/kotlin/io/github/hyungkishin/transentia/api/config/AsyncConfig.kt +++ b/services/transfer/instances/api/src/main/kotlin/io/github/hyungkishin/transentia/api/config/AsyncConfig.kt @@ -1,9 +1,8 @@ package io.github.hyungkishin.transentia.api.config -import org.slf4j.MDC import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.core.task.TaskDecorator +import org.springframework.core.task.support.ContextPropagatingTaskDecorator import org.springframework.scheduling.annotation.EnableAsync import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor import java.util.concurrent.Executor @@ -24,28 +23,12 @@ class AsyncConfig { executor.setWaitForTasksToCompleteOnShutdown(true) executor.setAwaitTerminationSeconds(30) - // TaskDecorator 적용 - executor.setTaskDecorator(mdcTaskDecorator()) + // Spring Boot 3.0+ ContextPropagatingTaskDecorator + // MDC + Micrometer Observation Context 모두 전파 + executor.setTaskDecorator(ContextPropagatingTaskDecorator()) executor.initialize() return executor } - // MDC 정보 전파를 위한 TaskDecorator - private fun mdcTaskDecorator(): TaskDecorator { - return TaskDecorator { runnable -> - val contextMap = MDC.getCopyOfContextMap() - Runnable { - try { - if (contextMap != null) { - MDC.setContextMap(contextMap) - } - runnable.run() - } finally { - MDC.clear() - } - } - } - } - -} \ No newline at end of file +} diff --git a/services/transfer/instances/api/src/main/resources/application-local.yml b/services/transfer/instances/api/src/main/resources/application-local.yml new file mode 100644 index 0000000..854a1a1 --- /dev/null +++ b/services/transfer/instances/api/src/main/resources/application-local.yml @@ -0,0 +1,6 @@ +# ============================================ +# Local 환경용 설정 (Docker 호스트명 → localhost) +# ============================================ +kafka-config: + bootstrap-servers: localhost:9094 + schema-registry-url: http://localhost:8085 diff --git a/services/transfer/instances/api/src/main/resources/application.yml b/services/transfer/instances/api/src/main/resources/application.yml index f7d7a46..d4ffc28 100644 --- a/services/transfer/instances/api/src/main/resources/application.yml +++ b/services/transfer/instances/api/src/main/resources/application.yml @@ -1,6 +1,9 @@ spring: application: name: transfer-api + autoconfigure: + exclude: + - org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration datasource: url: jdbc:postgresql://localhost:5432/transfer username: postgres @@ -79,7 +82,6 @@ management: tags: application: ${spring.application.name} tracing: - enabled: false - zipkin: - tracing: - endpoint: http://localhost:9999/disabled \ No newline at end of file + enabled: true + sampling: + probability: 1.0 # 개발환경 100% 샘플링 \ No newline at end of file