From 85a6d2d7e1e43f5e84ccf08dd6603784fa19b0e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=A7=80=EC=9B=90?= Date: Tue, 8 Oct 2024 05:56:58 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:=20HTTP=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EA=B1=B4=EC=88=98=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 5시간 이내의 HTTP 요청 10분 별로 집계 - 메트릭 데이터 수집 및 저장 --- .../api/metric/ServiceMetricsApi.java | 39 +++++ .../api/metric/ServiceMetricsApiDocs.java | 23 +++ .../dto/AppendServiceMetricsRequest.java | 50 ++++++ .../metric/dto/HttpRequestCountResponse.java | 13 ++ .../com/univ/tracedin/api/span/SpanApi.java | 6 +- .../univ/tracedin/api/span/SpanApiDocs.java | 4 +- .../domain/metric/HttpRequestCount.java | 15 ++ .../univ/tracedin/domain/metric/Metric.java | 27 ++++ .../tracedin/domain/metric/MetricType.java | 20 +++ .../domain/metric/ServiceMetrics.java | 18 +++ .../domain/metric/ServiceMetricsAppender.java | 16 ++ .../domain/metric/ServiceMetricsReader.java | 20 +++ .../metric/ServiceMetricsRepository.java | 12 ++ .../domain/metric/ServiceMetricsService.java | 25 +++ .../tracedin/domain/span/SpanAppender.java | 6 +- .../tracedin/domain/span/SpanRepository.java | 2 +- .../tracedin/domain/span/SpanService.java | 4 +- .../cache/RefreshTokenCacheAdapter.java | 2 +- .../config/TokenRedisConfig.java | 2 +- .../ESSupplier.java | 2 +- .../tracedin/infra/elasticsearch/ESUtils.java | 20 +++ .../document/ServiceMetricsDocument.java | 53 +++++++ .../ServiceMetricsCoreRepository.java | 33 ++++ ...ServiceMetricsElasticSearchRepository.java | 9 ++ ...eMetricsElasticSearchRepositoryCustom.java | 10 ++ ...ricsElasticSearchRepositoryCustomImpl.java | 144 ++++++++++++++++++ .../span/repository/SpanCoreRepository.java | 4 +- ...SpanElasticSearchRepositoryCustomImpl.java | 16 +- .../resources/elastic/metric-mappings.json | 51 +++++++ .../resources/elastic/metric-settings.json | 6 + 30 files changed, 625 insertions(+), 27 deletions(-) create mode 100644 tracedin-application/app-api/src/main/java/com/univ/tracedin/api/metric/ServiceMetricsApi.java create mode 100644 tracedin-application/app-api/src/main/java/com/univ/tracedin/api/metric/ServiceMetricsApiDocs.java create mode 100644 tracedin-application/app-api/src/main/java/com/univ/tracedin/api/metric/dto/AppendServiceMetricsRequest.java create mode 100644 tracedin-application/app-api/src/main/java/com/univ/tracedin/api/metric/dto/HttpRequestCountResponse.java create mode 100644 tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/HttpRequestCount.java create mode 100644 tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/Metric.java create mode 100644 tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/MetricType.java create mode 100644 tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetrics.java create mode 100644 tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetricsAppender.java create mode 100644 tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetricsReader.java create mode 100644 tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetricsRepository.java create mode 100644 tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetricsService.java rename tracedin-infra/src/main/java/com/univ/tracedin/infra/{user => auth}/cache/RefreshTokenCacheAdapter.java (96%) rename tracedin-infra/src/main/java/com/univ/tracedin/infra/{user => auth}/config/TokenRedisConfig.java (94%) rename tracedin-infra/src/main/java/com/univ/tracedin/infra/{span/repository => elasticsearch}/ESSupplier.java (66%) create mode 100644 tracedin-infra/src/main/java/com/univ/tracedin/infra/elasticsearch/ESUtils.java create mode 100644 tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/document/ServiceMetricsDocument.java create mode 100644 tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/repository/ServiceMetricsCoreRepository.java create mode 100644 tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/repository/ServiceMetricsElasticSearchRepository.java create mode 100644 tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/repository/ServiceMetricsElasticSearchRepositoryCustom.java create mode 100644 tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/repository/ServiceMetricsElasticSearchRepositoryCustomImpl.java create mode 100644 tracedin-infra/src/main/resources/elastic/metric-mappings.json create mode 100644 tracedin-infra/src/main/resources/elastic/metric-settings.json diff --git a/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/metric/ServiceMetricsApi.java b/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/metric/ServiceMetricsApi.java new file mode 100644 index 0000000..456a7fa --- /dev/null +++ b/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/metric/ServiceMetricsApi.java @@ -0,0 +1,39 @@ +package com.univ.tracedin.api.metric; + +import java.util.List; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; + +import com.univ.tracedin.api.global.dto.Response; +import com.univ.tracedin.api.metric.dto.AppendServiceMetricsRequest; +import com.univ.tracedin.api.metric.dto.HttpRequestCountResponse; +import com.univ.tracedin.domain.metric.ServiceMetricsService; +import com.univ.tracedin.domain.project.ServiceNode; + +@RestController +@RequestMapping("/api/v1/metrics") +@RequiredArgsConstructor +public class ServiceMetricsApi implements ServiceMetricsApiDocs { + + private final ServiceMetricsService serviceMetricService; + + @PostMapping + public void appendMetrics(@RequestBody AppendServiceMetricsRequest requests) { + serviceMetricService.appendMetrics(requests.toDomain()); + } + + @GetMapping("/http-request-count") + public Response> getHttpRequestCount(ServiceNode serviceNode) { + List responses = + serviceMetricService.getHttpRequestCount(serviceNode).stream() + .map(HttpRequestCountResponse::from) + .toList(); + return Response.success(responses); + } +} diff --git a/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/metric/ServiceMetricsApiDocs.java b/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/metric/ServiceMetricsApiDocs.java new file mode 100644 index 0000000..5b078a5 --- /dev/null +++ b/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/metric/ServiceMetricsApiDocs.java @@ -0,0 +1,23 @@ +package com.univ.tracedin.api.metric; + +import java.util.List; + +import com.univ.tracedin.api.global.dto.Response; +import com.univ.tracedin.api.metric.dto.AppendServiceMetricsRequest; +import com.univ.tracedin.api.metric.dto.HttpRequestCountResponse; +import com.univ.tracedin.domain.project.ServiceNode; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "메트릭 API") +public interface ServiceMetricsApiDocs { + + @Operation(summary = "메트릭 추가(라이브러리 전용)") + void appendMetrics(AppendServiceMetricsRequest requests); + + @Operation( + summary = "5시간 이내의 10분 별로 HTTP 요청 횟수 조회", + description = "5시간 이내의 10분 별로 HTTP 요청 횟수 조회, 개발 기간에는 5시간 이내 조건 없음") + Response> getHttpRequestCount(ServiceNode serviceNode); +} diff --git a/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/metric/dto/AppendServiceMetricsRequest.java b/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/metric/dto/AppendServiceMetricsRequest.java new file mode 100644 index 0000000..d9f7516 --- /dev/null +++ b/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/metric/dto/AppendServiceMetricsRequest.java @@ -0,0 +1,50 @@ +package com.univ.tracedin.api.metric.dto; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import com.univ.tracedin.domain.metric.Metric; +import com.univ.tracedin.domain.metric.MetricType; +import com.univ.tracedin.domain.metric.ServiceMetrics; + +public record AppendServiceMetricsRequest( + String projectKey, String serviceName, List metrics) { + + public record MetricRequest( + String name, + String description, + String unit, + String type, + Double value, + Long count, + Double sum, + Double min, + Double max, + Map attributes) { + + public Metric toDomain() { + return Metric.builder() + .name(name) + .description(description) + .unit(unit) + .type(MetricType.valueOf(type)) + .value(value) + .count(count) + .sum(sum) + .min(min) + .max(max) + .attributes(attributes) + .timestamp(LocalDateTime.now()) + .build(); + } + } + + public ServiceMetrics toDomain() { + return ServiceMetrics.builder() + .projectKey(projectKey) + .serviceName(serviceName) + .metrics(metrics.stream().map(MetricRequest::toDomain).toList()) + .build(); + } +} diff --git a/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/metric/dto/HttpRequestCountResponse.java b/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/metric/dto/HttpRequestCountResponse.java new file mode 100644 index 0000000..c6ff521 --- /dev/null +++ b/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/metric/dto/HttpRequestCountResponse.java @@ -0,0 +1,13 @@ +package com.univ.tracedin.api.metric.dto; + +import java.time.LocalDateTime; + +import com.univ.tracedin.domain.metric.HttpRequestCount; + +public record HttpRequestCountResponse(LocalDateTime timestamp, Long httpRequestCount) { + + public static HttpRequestCountResponse from(HttpRequestCount httpRequestCount) { + return new HttpRequestCountResponse( + httpRequestCount.timestamp(), httpRequestCount.httpRequestCount()); + } +} diff --git a/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/span/SpanApi.java b/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/span/SpanApi.java index c3c3a2d..dee9e54 100644 --- a/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/span/SpanApi.java +++ b/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/span/SpanApi.java @@ -1,5 +1,7 @@ package com.univ.tracedin.api.span; +import java.util.List; + import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -28,9 +30,9 @@ public class SpanApi implements SpanApiDocs { private final SpanService spanService; @PostMapping - public void appendSpan(@RequestBody AppendSpanRequest request) { + public void appendSpan(@RequestBody List request) { log.info("appendSpan request: {}", request.toString()); - spanService.appendSpan(request.toSpan()); + spanService.appendSpan(request.stream().map(AppendSpanRequest::toSpan).toList()); } @GetMapping("/traces") diff --git a/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/span/SpanApiDocs.java b/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/span/SpanApiDocs.java index 3b93489..63181ad 100644 --- a/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/span/SpanApiDocs.java +++ b/tracedin-application/app-api/src/main/java/com/univ/tracedin/api/span/SpanApiDocs.java @@ -1,5 +1,7 @@ package com.univ.tracedin.api.span; +import java.util.List; + import com.univ.tracedin.api.global.dto.Response; import com.univ.tracedin.api.span.dto.AppendSpanRequest; import com.univ.tracedin.api.span.dto.ReadSpanRequest; @@ -15,7 +17,7 @@ public interface SpanApiDocs { @Operation(summary = "스팬 데이터 추가 API(라이브러리 전용)", description = "라이브러리에서 수집한 스팬 데이터를 추가합니다.") - void appendSpan(AppendSpanRequest request); + void appendSpan(List request); @Operation(summary = "트레이스(트랜잭션) 조회 API", description = "프로젝트의 특정 서비스의 트레이스(트랜잭션)를 조회합니다.") Response> getTraces(ReadSpanRequest request, SearchCursor cursor); diff --git a/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/HttpRequestCount.java b/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/HttpRequestCount.java new file mode 100644 index 0000000..c90c572 --- /dev/null +++ b/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/HttpRequestCount.java @@ -0,0 +1,15 @@ +package com.univ.tracedin.domain.metric; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + +public record HttpRequestCount(LocalDateTime timestamp, Long httpRequestCount) { + + public static HttpRequestCount of(long endEpochMillis, Long httpRequestCount) { + LocalDateTime timestamp = + LocalDateTime.ofInstant( + Instant.ofEpochMilli(endEpochMillis), ZoneId.systemDefault()); + return new HttpRequestCount(timestamp, httpRequestCount); + } +} diff --git a/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/Metric.java b/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/Metric.java new file mode 100644 index 0000000..438240d --- /dev/null +++ b/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/Metric.java @@ -0,0 +1,27 @@ +package com.univ.tracedin.domain.metric; + +import java.time.LocalDateTime; +import java.util.Map; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Metric { + + private String name; + private String description; + private String unit; + private MetricType type; + private Double value; + private Long count; + private Double sum; + private Double min; + private Double max; + private Map attributes; + private LocalDateTime timestamp; +} diff --git a/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/MetricType.java b/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/MetricType.java new file mode 100644 index 0000000..447839d --- /dev/null +++ b/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/MetricType.java @@ -0,0 +1,20 @@ +package com.univ.tracedin.domain.metric; + +public enum MetricType { + LONG_GAUGE, + DOUBLE_GAUGE, + LONG_SUM, + DOUBLE_SUM, + SUMMARY, + HISTOGRAM, + EXPONENTIAL_HISTOGRAM; + + public static MetricType fromValue(String value) { + for (MetricType type : MetricType.values()) { + if (type.name().equalsIgnoreCase(value)) { + return type; + } + } + throw new IllegalArgumentException("Unknown MetricType: " + value); + } +} diff --git a/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetrics.java b/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetrics.java new file mode 100644 index 0000000..6329c6f --- /dev/null +++ b/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetrics.java @@ -0,0 +1,18 @@ +package com.univ.tracedin.domain.metric; + +import java.util.List; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ServiceMetrics { + + private String projectKey; + private String serviceName; + List metrics; +} diff --git a/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetricsAppender.java b/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetricsAppender.java new file mode 100644 index 0000000..bbbe840 --- /dev/null +++ b/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetricsAppender.java @@ -0,0 +1,16 @@ +package com.univ.tracedin.domain.metric; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class ServiceMetricsAppender { + + private final ServiceMetricsRepository serviceMetricsRepository; + + public void append(ServiceMetrics metrics) { + serviceMetricsRepository.save(metrics); + } +} diff --git a/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetricsReader.java b/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetricsReader.java new file mode 100644 index 0000000..38e2eff --- /dev/null +++ b/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetricsReader.java @@ -0,0 +1,20 @@ +package com.univ.tracedin.domain.metric; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +import com.univ.tracedin.domain.project.ServiceNode; + +@Component +@RequiredArgsConstructor +public class ServiceMetricsReader { + + private final ServiceMetricsRepository serviceMetricsRepository; + + public List readHttpRequestCount(ServiceNode serviceNode) { + return serviceMetricsRepository.getHttpRequestCount(serviceNode); + } +} diff --git a/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetricsRepository.java b/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetricsRepository.java new file mode 100644 index 0000000..ffb7c19 --- /dev/null +++ b/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetricsRepository.java @@ -0,0 +1,12 @@ +package com.univ.tracedin.domain.metric; + +import java.util.List; + +import com.univ.tracedin.domain.project.ServiceNode; + +public interface ServiceMetricsRepository { + + void save(ServiceMetrics metrics); + + List getHttpRequestCount(ServiceNode serviceNode); +} diff --git a/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetricsService.java b/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetricsService.java new file mode 100644 index 0000000..0e22f9e --- /dev/null +++ b/tracedin-domain/src/main/java/com/univ/tracedin/domain/metric/ServiceMetricsService.java @@ -0,0 +1,25 @@ +package com.univ.tracedin.domain.metric; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +import com.univ.tracedin.domain.project.ServiceNode; + +@Service +@RequiredArgsConstructor +public class ServiceMetricsService { + + private final ServiceMetricsAppender serviceMetricAppender; + private final ServiceMetricsReader serviceMetricReader; + + public void appendMetrics(ServiceMetrics metrics) { + serviceMetricAppender.append(metrics); + } + + public List getHttpRequestCount(ServiceNode serviceNode) { + return serviceMetricReader.readHttpRequestCount(serviceNode); + } +} diff --git a/tracedin-domain/src/main/java/com/univ/tracedin/domain/span/SpanAppender.java b/tracedin-domain/src/main/java/com/univ/tracedin/domain/span/SpanAppender.java index 8df0827..17bbfde 100644 --- a/tracedin-domain/src/main/java/com/univ/tracedin/domain/span/SpanAppender.java +++ b/tracedin-domain/src/main/java/com/univ/tracedin/domain/span/SpanAppender.java @@ -1,5 +1,7 @@ package com.univ.tracedin.domain.span; +import java.util.List; + import org.springframework.stereotype.Component; import lombok.RequiredArgsConstructor; @@ -10,7 +12,7 @@ public class SpanAppender { private final SpanRepository spanRepository; - public void append(Span span) { - spanRepository.save(span); + public void append(List spans) { + spanRepository.save(spans); } } diff --git a/tracedin-domain/src/main/java/com/univ/tracedin/domain/span/SpanRepository.java b/tracedin-domain/src/main/java/com/univ/tracedin/domain/span/SpanRepository.java index be7e970..1ff82eb 100644 --- a/tracedin-domain/src/main/java/com/univ/tracedin/domain/span/SpanRepository.java +++ b/tracedin-domain/src/main/java/com/univ/tracedin/domain/span/SpanRepository.java @@ -9,7 +9,7 @@ public interface SpanRepository { - void save(Span span); + void save(List spans); Span findById(String id); diff --git a/tracedin-domain/src/main/java/com/univ/tracedin/domain/span/SpanService.java b/tracedin-domain/src/main/java/com/univ/tracedin/domain/span/SpanService.java index 1dca8b2..1a68069 100644 --- a/tracedin-domain/src/main/java/com/univ/tracedin/domain/span/SpanService.java +++ b/tracedin-domain/src/main/java/com/univ/tracedin/domain/span/SpanService.java @@ -17,8 +17,8 @@ public class SpanService { private final SpanAppender spanAppender; private final SpanReader spanReader; - public void appendSpan(Span span) { - spanAppender.append(span); + public void appendSpan(List spans) { + spanAppender.append(spans); } public SearchResult getTraces(ServiceNode serviceNode, SearchCursor cursor) { diff --git a/tracedin-infra/src/main/java/com/univ/tracedin/infra/user/cache/RefreshTokenCacheAdapter.java b/tracedin-infra/src/main/java/com/univ/tracedin/infra/auth/cache/RefreshTokenCacheAdapter.java similarity index 96% rename from tracedin-infra/src/main/java/com/univ/tracedin/infra/user/cache/RefreshTokenCacheAdapter.java rename to tracedin-infra/src/main/java/com/univ/tracedin/infra/auth/cache/RefreshTokenCacheAdapter.java index 5d75bd1..0aae3fd 100644 --- a/tracedin-infra/src/main/java/com/univ/tracedin/infra/user/cache/RefreshTokenCacheAdapter.java +++ b/tracedin-infra/src/main/java/com/univ/tracedin/infra/auth/cache/RefreshTokenCacheAdapter.java @@ -1,4 +1,4 @@ -package com.univ.tracedin.infra.user.cache; +package com.univ.tracedin.infra.auth.cache; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; diff --git a/tracedin-infra/src/main/java/com/univ/tracedin/infra/user/config/TokenRedisConfig.java b/tracedin-infra/src/main/java/com/univ/tracedin/infra/auth/config/TokenRedisConfig.java similarity index 94% rename from tracedin-infra/src/main/java/com/univ/tracedin/infra/user/config/TokenRedisConfig.java rename to tracedin-infra/src/main/java/com/univ/tracedin/infra/auth/config/TokenRedisConfig.java index cf2457b..e210e35 100644 --- a/tracedin-infra/src/main/java/com/univ/tracedin/infra/user/config/TokenRedisConfig.java +++ b/tracedin-infra/src/main/java/com/univ/tracedin/infra/auth/config/TokenRedisConfig.java @@ -1,4 +1,4 @@ -package com.univ.tracedin.infra.user.config; +package com.univ.tracedin.infra.auth.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/tracedin-infra/src/main/java/com/univ/tracedin/infra/span/repository/ESSupplier.java b/tracedin-infra/src/main/java/com/univ/tracedin/infra/elasticsearch/ESSupplier.java similarity index 66% rename from tracedin-infra/src/main/java/com/univ/tracedin/infra/span/repository/ESSupplier.java rename to tracedin-infra/src/main/java/com/univ/tracedin/infra/elasticsearch/ESSupplier.java index 430340e..b51b707 100644 --- a/tracedin-infra/src/main/java/com/univ/tracedin/infra/span/repository/ESSupplier.java +++ b/tracedin-infra/src/main/java/com/univ/tracedin/infra/elasticsearch/ESSupplier.java @@ -1,4 +1,4 @@ -package com.univ.tracedin.infra.span.repository; +package com.univ.tracedin.infra.elasticsearch; import java.io.IOException; diff --git a/tracedin-infra/src/main/java/com/univ/tracedin/infra/elasticsearch/ESUtils.java b/tracedin-infra/src/main/java/com/univ/tracedin/infra/elasticsearch/ESUtils.java new file mode 100644 index 0000000..a51f185 --- /dev/null +++ b/tracedin-infra/src/main/java/com/univ/tracedin/infra/elasticsearch/ESUtils.java @@ -0,0 +1,20 @@ +package com.univ.tracedin.infra.elasticsearch; + +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +import com.univ.tracedin.infra.span.exception.ElasticSearchException; + +@Slf4j +public class ESUtils { + + public static T executeESQuery(ESSupplier request) { + try { + return request.get(); + } catch (IOException e) { + log.error("Failed to search spans", e); + throw ElasticSearchException.EXCEPTION; + } + } +} diff --git a/tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/document/ServiceMetricsDocument.java b/tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/document/ServiceMetricsDocument.java new file mode 100644 index 0000000..76b0fe8 --- /dev/null +++ b/tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/document/ServiceMetricsDocument.java @@ -0,0 +1,53 @@ +package com.univ.tracedin.infra.metric.document; + +import java.util.List; + +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Mapping; +import org.springframework.data.elasticsearch.annotations.Setting; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.univ.tracedin.domain.metric.Metric; +import com.univ.tracedin.domain.metric.ServiceMetrics; + +@JsonIgnoreProperties(ignoreUnknown = true) +@Document(indexName = "service-metrics") +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +@Getter +@Mapping(mappingPath = "elastic/metric-mappings.json") +@Setting(settingPath = "elastic/metric-settings.json") +public class ServiceMetricsDocument { + + @Id private String id; + + private String projectKey; + + private String serviceName; + + List metrics; + + public static ServiceMetricsDocument from(ServiceMetrics serviceMetrics) { + return ServiceMetricsDocument.builder() + .projectKey(serviceMetrics.getProjectKey()) + .serviceName(serviceMetrics.getServiceName()) + .metrics(serviceMetrics.getMetrics()) + .build(); + } + + public ServiceMetrics toServiceMetrics() { + return ServiceMetrics.builder() + .projectKey(projectKey) + .serviceName(serviceName) + .metrics(metrics) + .build(); + } +} diff --git a/tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/repository/ServiceMetricsCoreRepository.java b/tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/repository/ServiceMetricsCoreRepository.java new file mode 100644 index 0000000..141fe54 --- /dev/null +++ b/tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/repository/ServiceMetricsCoreRepository.java @@ -0,0 +1,33 @@ +package com.univ.tracedin.infra.metric.repository; + +import java.util.List; + +import jakarta.transaction.Transactional; + +import org.springframework.stereotype.Repository; + +import lombok.RequiredArgsConstructor; + +import com.univ.tracedin.domain.metric.HttpRequestCount; +import com.univ.tracedin.domain.metric.ServiceMetrics; +import com.univ.tracedin.domain.metric.ServiceMetricsRepository; +import com.univ.tracedin.domain.project.ServiceNode; +import com.univ.tracedin.infra.metric.document.ServiceMetricsDocument; + +@Repository +@Transactional +@RequiredArgsConstructor +public class ServiceMetricsCoreRepository implements ServiceMetricsRepository { + + private final ServiceMetricsElasticSearchRepository serviceMetricsElasticSearchRepository; + + public void save(ServiceMetrics metrics) { + serviceMetricsElasticSearchRepository.save(ServiceMetricsDocument.from(metrics)); + } + + @Override + public List getHttpRequestCount(ServiceNode serviceNode) { + return serviceMetricsElasticSearchRepository.getHttpRequestCount( + serviceNode.getProjectKey(), serviceNode.getName()); + } +} diff --git a/tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/repository/ServiceMetricsElasticSearchRepository.java b/tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/repository/ServiceMetricsElasticSearchRepository.java new file mode 100644 index 0000000..997ce80 --- /dev/null +++ b/tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/repository/ServiceMetricsElasticSearchRepository.java @@ -0,0 +1,9 @@ +package com.univ.tracedin.infra.metric.repository; + +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +import com.univ.tracedin.infra.metric.document.ServiceMetricsDocument; + +public interface ServiceMetricsElasticSearchRepository + extends ElasticsearchRepository, + ServiceMetricsElasticSearchRepositoryCustom {} diff --git a/tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/repository/ServiceMetricsElasticSearchRepositoryCustom.java b/tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/repository/ServiceMetricsElasticSearchRepositoryCustom.java new file mode 100644 index 0000000..cb3b783 --- /dev/null +++ b/tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/repository/ServiceMetricsElasticSearchRepositoryCustom.java @@ -0,0 +1,10 @@ +package com.univ.tracedin.infra.metric.repository; + +import java.util.List; + +import com.univ.tracedin.domain.metric.HttpRequestCount; + +public interface ServiceMetricsElasticSearchRepositoryCustom { + + List getHttpRequestCount(String projectKey, String serviceName); +} diff --git a/tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/repository/ServiceMetricsElasticSearchRepositoryCustomImpl.java b/tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/repository/ServiceMetricsElasticSearchRepositoryCustomImpl.java new file mode 100644 index 0000000..133004a --- /dev/null +++ b/tracedin-infra/src/main/java/com/univ/tracedin/infra/metric/repository/ServiceMetricsElasticSearchRepositoryCustomImpl.java @@ -0,0 +1,144 @@ +package com.univ.tracedin.infra.metric.repository; + +import static com.univ.tracedin.infra.elasticsearch.ESUtils.executeESQuery; + +import java.util.List; +import java.util.Map; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import com.univ.tracedin.domain.metric.HttpRequestCount; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.aggregations.Aggregation; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders; +import co.elastic.clients.elasticsearch.core.SearchResponse; +import co.elastic.clients.json.JsonData; + +@Slf4j +@RequiredArgsConstructor +public class ServiceMetricsElasticSearchRepositoryCustomImpl + implements ServiceMetricsElasticSearchRepositoryCustom { + + private final String INDEX_NAME = "service-metrics"; + private final ElasticsearchClient client; + + @Override + public List getHttpRequestCount(String projectKey, String serviceName) { + Query serviceHttpRequestCountQuery = createHttpRequestCountQuery(projectKey, serviceName); + Aggregation timestampAggregationHistogram = createTimestampAggregationHistogram(); + + return executeESQuery( + () -> { + SearchResponse response = + client.search( + s -> + s.index(INDEX_NAME) + .size(10000) + .query(serviceHttpRequestCountQuery) + .aggregations( + "metrics_nested", + timestampAggregationHistogram), + Void.class); + return response + .aggregations() + .get("metrics_nested") + .nested() + .aggregations() + .get("filtered_http_requests") + .filter() + .aggregations() + .get("by_timestamp") + .dateHistogram() + .buckets() + .array() + .stream() + .map( + bucket -> { + long timestamp = bucket.key(); + double value = + bucket.aggregations() + .get("request_count_sum") + .sum() + .value(); + return HttpRequestCount.of(timestamp, (long) value); + }) + .toList(); + }); + } + + private Query createHttpRequestCountQuery(String projectKey, String serviceName) { + return QueryBuilders.bool( + b -> + b.must( + List.of( + // projectKey와 serviceName은 일반 필드이므로 nested 쿼리 바깥에 위치 + QueryBuilders.term( + t -> t.field("projectKey").value(projectKey)), + QueryBuilders.term( + t -> t.field("serviceName").value(serviceName)), + // metrics는 nested 필드이므로 nested 쿼리 안에서 처리 + QueryBuilders.nested( + n -> + n.path("metrics") + .query( + q -> + q.bool( + bq -> + bq + .must( + List + .of( + QueryBuilders + .term( + t -> + t.field( + "metrics.name") + .value( + "http.request.count")), + QueryBuilders + .range( + r -> + r.field( + "metrics.timestamp") + .gte( + JsonData + .of( + "now-5h/h"))))))))))); + } + + private Aggregation createTimestampAggregationHistogram() { + + Aggregation requestCountSum = Aggregation.of(a -> a.sum(s -> s.field("metrics.value"))); + + Aggregation byTimeStamp = + Aggregation.of( + a -> + a.dateHistogram( + dh -> + dh.field("metrics.timestamp") + .fixedInterval(fi -> fi.time("10m")) + .minDocCount(1)) + .aggregations( + Map.of("request_count_sum", requestCountSum))); + + Aggregation httpRequestMetric = + Aggregation.of( + a -> + a.filter( + f -> + f.term( + t -> + t.field("metrics.name") + .value( + "http.request.count"))) + .aggregations(Map.of("by_timestamp", byTimeStamp))); + + return Aggregation.of( + a -> + a.nested(n -> n.path("metrics")) + .aggregations(Map.of("filtered_http_requests", httpRequestMetric))); + } +} diff --git a/tracedin-infra/src/main/java/com/univ/tracedin/infra/span/repository/SpanCoreRepository.java b/tracedin-infra/src/main/java/com/univ/tracedin/infra/span/repository/SpanCoreRepository.java index f8f6126..0bb367d 100644 --- a/tracedin-infra/src/main/java/com/univ/tracedin/infra/span/repository/SpanCoreRepository.java +++ b/tracedin-infra/src/main/java/com/univ/tracedin/infra/span/repository/SpanCoreRepository.java @@ -26,8 +26,8 @@ public class SpanCoreRepository implements SpanRepository { private final SpanElasticSearchRepository spanElasticSearchRepository; @Override - public void save(Span span) { - spanElasticSearchRepository.save(SpanDocument.from(span)); + public void save(List spans) { + spanElasticSearchRepository.saveAll(spans.stream().map(SpanDocument::from).toList()); } @Override diff --git a/tracedin-infra/src/main/java/com/univ/tracedin/infra/span/repository/SpanElasticSearchRepositoryCustomImpl.java b/tracedin-infra/src/main/java/com/univ/tracedin/infra/span/repository/SpanElasticSearchRepositoryCustomImpl.java index 684e610..4e036cc 100644 --- a/tracedin-infra/src/main/java/com/univ/tracedin/infra/span/repository/SpanElasticSearchRepositoryCustomImpl.java +++ b/tracedin-infra/src/main/java/com/univ/tracedin/infra/span/repository/SpanElasticSearchRepositoryCustomImpl.java @@ -1,6 +1,7 @@ package com.univ.tracedin.infra.span.repository; -import java.io.IOException; +import static com.univ.tracedin.infra.elasticsearch.ESUtils.executeESQuery; + import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -8,8 +9,6 @@ import jakarta.json.JsonObject; -import org.springframework.stereotype.Repository; - import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -21,7 +20,6 @@ import com.univ.tracedin.domain.span.Trace; import com.univ.tracedin.domain.span.TraceId; import com.univ.tracedin.infra.span.document.SpanDocument; -import com.univ.tracedin.infra.span.exception.ElasticSearchException; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch._types.FieldValue; @@ -45,7 +43,6 @@ import co.elastic.clients.json.JsonData; @Slf4j -@Repository @RequiredArgsConstructor public class SpanElasticSearchRepositoryCustomImpl implements SpanElasticSearchRepositoryCustom { @@ -436,13 +433,4 @@ private Trace mapToTrace(JsonObject source) { .startEpochMillis(source.getJsonNumber("startEpochMillis").longValue()) .build(); } - - private T executeESQuery(ESSupplier request) { - try { - return request.get(); - } catch (IOException e) { - log.error("Failed to search spans", e); - throw ElasticSearchException.EXCEPTION; - } - } } diff --git a/tracedin-infra/src/main/resources/elastic/metric-mappings.json b/tracedin-infra/src/main/resources/elastic/metric-mappings.json new file mode 100644 index 0000000..5b8f0a9 --- /dev/null +++ b/tracedin-infra/src/main/resources/elastic/metric-mappings.json @@ -0,0 +1,51 @@ +{ + "properties": { + "projectKey": { + "type": "keyword" + }, + "serviceName": { + "type": "keyword" + }, + "metrics": { + "type": "nested", + "properties": { + "name": { + "type": "keyword" + }, + "description": { + "type": "text", + "analyzer": "standard" + }, + "unit": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "double" + }, + "count": { + "type": "long" + }, + "sum": { + "type": "double" + }, + "min": { + "type": "double" + }, + "max": { + "type": "double" + }, + "attributes": { + "type": "object", + "dynamic": true + }, + "timestamp": { + "type": "date", + "format": "epoch_millis" + } + } + } + } +} diff --git a/tracedin-infra/src/main/resources/elastic/metric-settings.json b/tracedin-infra/src/main/resources/elastic/metric-settings.json new file mode 100644 index 0000000..9761fcd --- /dev/null +++ b/tracedin-infra/src/main/resources/elastic/metric-settings.json @@ -0,0 +1,6 @@ +{ + "index": { + "number_of_shards": 1, + "number_of_replicas": 1 + } +} \ No newline at end of file