diff --git a/build.gradle b/build.gradle index 83ce59b6..f01680fb 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:2.16.0") + implementation 'org.hibernate.orm:hibernate-micrometer' implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.google.code.gson:gson:2.13.1' diff --git a/docker/mariadb/init.sql b/docker/mariadb/init.sql index 29b57767..eb9362b2 100644 --- a/docker/mariadb/init.sql +++ b/docker/mariadb/init.sql @@ -110,3 +110,5 @@ INSERT INTO `if`.asset (ticker, name) VALUES ('TRUMP', '오피셜트럼프'); INSERT INTO `if`.wallet (wallet_id, account_id, buy_price, roi, size, ticker) VALUES (1, 1, 0, 0, 500000000, 'BTC'); INSERT INTO `if`.wallet (wallet_id, account_id, buy_price, roi, size, ticker) VALUES (2, 1, 0, 0, 500000000, 'TRUMP'); +INSERT INTO `if`.wallet (wallet_id, account_id, buy_price, roi, size, ticker) VALUES (3, 2, 0, 0, 500000000, 'BTC'); +INSERT INTO `if`.wallet (wallet_id, account_id, buy_price, roi, size, ticker) VALUES (4, 2, 0, 0, 500000000, 'TRUMP'); diff --git a/monitoring/docker-compose.yml b/monitoring/docker-compose.yml index 46e09705..6e6431d4 100644 --- a/monitoring/docker-compose.yml +++ b/monitoring/docker-compose.yml @@ -9,7 +9,7 @@ services: - /etc/localtime:/etc/localtime:ro - ./opentelemetry-javaagent.jar:/app/opentelemetry-javaagent.jar working_dir: /app - command: [ "java", "-jar", "coin-0.0.1-SNAPSHOT.jar", "--spring.profiles.active=dev,mariadb-local,actuator,apm" ] + command: [ "java", "-jar", "coin-0.0.1-SNAPSHOT.jar", "--spring.profiles.active=dev,it,mariadb-local,actuator,apm" ] ports: - "8080:8080" env_file: @@ -18,14 +18,16 @@ services: - TZ=Asia/Seoul - OTEL_SERVICE_NAME=my-spring-app - OTEL_TRACES_EXPORTER=otlp - - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318 + - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 - OTEL_LOGS_EXPORTER=none - OTEL_METRICS_EXPORTER=none - OTEL_INSTRUMENTATION_METHODS_ENABLED=true - - JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005, -javaagent:/app/opentelemetry-javaagent.jar + - JAVA_TOOL_OPTIONS=-javaagent:/app/opentelemetry-javaagent.jar depends_on: mariadb: condition: service_healthy + otel-collector: + condition: service_started networks: - app-network - monitoring-net @@ -34,7 +36,7 @@ services: image: mariadb:latest container_name: mariadb2 ports: - - "3307:3306" + - "3306:3306" env_file: - ../docker/local.properties environment: @@ -60,13 +62,13 @@ services: command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.retention.time=30d' + - '--web.enable-remote-write-receiver' ports: - "9090:9090" networks: - monitoring-net restart: unless-stopped - # --- 추가된 InfluxDB 서비스 --- influxdb: image: influxdb:2.7 container_name: influxdb @@ -96,7 +98,7 @@ services: networks: - monitoring-net restart: unless-stopped - depends_on: # --- 수정된 부분 --- + depends_on: - prometheus - jaeger - influxdb @@ -104,19 +106,48 @@ services: jaeger: image: jaegertracing/all-in-one:latest container_name: jaeger + user: "${UID}:${GID}" + environment: + - SPAN_STORAGE_TYPE=badger + - BADGER_EPHEMERAL=false + - BADGER_DIRECTORY_VALUE=/tmp/jaeger/data + - BADGER_DIRECTORY_KEY=/tmp/jaeger/keys + - COLLECTOR_OTLP_ENABLED=true + - COLLECTOR_OTLP_GRPC_HOST_PORT=0.0.0.0:4317 + - COLLECTOR_OTLP_HTTP_HOST_PORT=0.0.0.0:4318 + volumes: + - jaeger_data:/tmp/jaeger + ports: + - "16686:16686" # UI + - "4319:4317" + - "4320:4318" + networks: + - monitoring-net + + + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + command: ["--config=/etc/otel-collector-config.yml"] + volumes: + - ./otel/otel-collector-config.yml:/etc/otel-collector-config.yml ports: - - "16686:16686" + - "8889:8889" - "4317:4317" - "4318:4318" + - "13133:13133" networks: - monitoring-net + depends_on: + jaeger: + condition: service_started volumes: prometheus_data: {} grafana_data: {} mariadb_data: driver: local - influxdb_data: {} # --- 추가된 부분 --- + influxdb_data: {} + jaeger_data: {} networks: app-network: diff --git a/monitoring/otel/otel-collector-config.yml b/monitoring/otel/otel-collector-config.yml new file mode 100644 index 00000000..c5852c05 --- /dev/null +++ b/monitoring/otel/otel-collector-config.yml @@ -0,0 +1,47 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + timeout: 500ms + send_batch_size: 8192 + send_batch_max_size: 16384 + probabilistic_sampler: + sampling_percentage: 1 + +connectors: + spanmetrics: + histogram: + explicit: + buckets: [100us, 1ms, 2ms, 6ms, 10ms, 100ms, 250ms, 500ms, 1s] + +exporters: + otlp/jaeger: + endpoint: jaeger:4317 + tls: + insecure: true + + prometheus: + endpoint: "0.0.0.0:8889" + +service: + pipelines: + traces/metrics: + receivers: [otlp] + processors: [batch] + exporters: [spanmetrics] + + traces/jaeger: + receivers: [otlp] + processors: [probabilistic_sampler,batch] + exporters: [otlp/jaeger] + + metrics: + receivers: [spanmetrics] + processors: [batch] + exporters: [prometheus] \ No newline at end of file diff --git a/monitoring/prometheus/prometheus.yml b/monitoring/prometheus/prometheus.yml index d1690101..59130832 100644 --- a/monitoring/prometheus/prometheus.yml +++ b/monitoring/prometheus/prometheus.yml @@ -8,4 +8,8 @@ scrape_configs: - job_name: 'my-app' static_configs: - targets: [ 'app:8080' ] - metrics_path: /actuator/prometheus \ No newline at end of file + metrics_path: /actuator/prometheus + - job_name: 'otel-collector' + scrape_interval: 15s + static_configs: + - targets: [ 'otel-collector:8889' ] \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/configuration/MicrometerConfig.java b/src/main/java/com/cleanengine/coin/configuration/MicrometerConfig.java new file mode 100644 index 00000000..68cd9103 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/configuration/MicrometerConfig.java @@ -0,0 +1,32 @@ +package com.cleanengine.coin.configuration; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.config.MeterFilter; +import io.micrometer.core.instrument.config.MeterFilterReply; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Profile("actuator") +@Configuration +public class MicrometerConfig { + @Bean + public MeterFilter meterFilter() { + final String hibernateFilterPrefix = "hibernate.statements"; + + return new MeterFilter() { + @Override + public MeterFilterReply accept(Meter.Id id) { + if (id.getName().startsWith("hibernate.")) { + if(id.getName().startsWith(hibernateFilterPrefix)) { + return MeterFilterReply.ACCEPT; + } + else{ + return MeterFilterReply.DENY; + } + } + return MeterFilterReply.NEUTRAL; + } + }; + } +} diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/InMemoryWaitingOrdersManager.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/InMemoryWaitingOrdersManager.java index 83d29bf6..3530d960 100644 --- a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/InMemoryWaitingOrdersManager.java +++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/InMemoryWaitingOrdersManager.java @@ -9,7 +9,7 @@ import java.util.Optional; @Slf4j -@Component +//@Component public class InMemoryWaitingOrdersManager implements WaitingOrdersManager { private final HashMap waitingOrdersMap = new HashMap<>(); @@ -29,7 +29,7 @@ public WaitingOrders getWaitingOrders(String ticker) { @Override public void removeWaitingOrders(String ticker) { - + waitingOrdersMap.remove(ticker); } @Override diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryBuyOrderSkipListSet.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryBuyOrderSkipListSet.java new file mode 100644 index 00000000..ab35bef3 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryBuyOrderSkipListSet.java @@ -0,0 +1,96 @@ +package com.cleanengine.coin.order.adapter.out.persistentce.order.queue; + +import com.cleanengine.coin.common.domain.port.PriorityQueueStore; +import com.cleanengine.coin.order.domain.BuyOrder; + +import java.util.Comparator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicLong; + +public class NestedInMemoryBuyOrderSkipListSet implements PriorityQueueStore { + private final ConcurrentSkipListMap> map = new ConcurrentSkipListMap<>(Comparator.reverseOrder()); + private final AtomicLong size = new AtomicLong(); + + @Override + public void put(BuyOrder item) { + if(item == null) throw new IllegalArgumentException("item cannot be null."); + + map.compute(item.getPrice(), (key, buyOrders) -> { + if(buyOrders == null) { + buyOrders = new ConcurrentSkipListSet<>(); + } + boolean added = buyOrders.add(item); + if(added) size.incrementAndGet(); + return buyOrders; + }); + } + + @Override + public BuyOrder poll() { + while(true) { + Map.Entry> firstEntry = map.firstEntry(); + + if (firstEntry == null) { + return null; + } + + ConcurrentSkipListSet buyOrders = firstEntry.getValue(); + try { + BuyOrder order = buyOrders.first(); + this.remove(order); + return order; + } catch (NoSuchElementException e) { + continue; + } + } + } + + @Override + public BuyOrder peek() { + while(true) { + Map.Entry> firstEntry = map.firstEntry(); + + if (firstEntry == null) { + return null; + } + + ConcurrentSkipListSet buyOrders = firstEntry.getValue(); + try { + return buyOrders.first(); + } catch (NoSuchElementException e) { + continue; + } + } + } + + @Override + public BuyOrder remove(BuyOrder item) { + if (item == null) return null; + + map.computeIfPresent(item.getPrice(), (key, orders) -> { + boolean removed = orders.remove(item); + if (removed) size.decrementAndGet(); + return orders.isEmpty() ? null : orders; + }); + + return item; + } + + @Override + public long size() { + return size.get(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public void clear() { + map.clear(); + } +} diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemorySellOrderSkipListSet.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemorySellOrderSkipListSet.java new file mode 100644 index 00000000..29cd1368 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemorySellOrderSkipListSet.java @@ -0,0 +1,97 @@ +package com.cleanengine.coin.order.adapter.out.persistentce.order.queue; + +import com.cleanengine.coin.common.domain.port.PriorityQueueStore; +import com.cleanengine.coin.order.domain.BuyOrder; +import com.cleanengine.coin.order.domain.SellOrder; + +import java.util.Comparator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicLong; + +public class NestedInMemorySellOrderSkipListSet implements PriorityQueueStore { + private final ConcurrentSkipListMap> map = new ConcurrentSkipListMap<>(); + private final AtomicLong size = new AtomicLong(); + + @Override + public void put(SellOrder item) { + if(item == null) throw new IllegalArgumentException("item cannot be null."); + + map.compute(item.getPrice(), (key, sellOrders) -> { + if(sellOrders == null) { + sellOrders = new ConcurrentSkipListSet<>(); + } + boolean added = sellOrders.add(item); + if(added) size.incrementAndGet(); + return sellOrders; + }); + } + + @Override + public SellOrder poll() { + while(true) { + Map.Entry> firstEntry = map.firstEntry(); + + if (firstEntry == null) { + return null; + } + + ConcurrentSkipListSet sellOrders = firstEntry.getValue(); + try { + SellOrder order = sellOrders.first(); + this.remove(order); + return order; + } catch (NoSuchElementException e) { + continue; + } + } + } + + @Override + public SellOrder peek() { + while(true) { + Map.Entry> firstEntry = map.firstEntry(); + + if (firstEntry == null) { + return null; + } + + ConcurrentSkipListSet sellOrders = firstEntry.getValue(); + try { + return sellOrders.first(); + } catch (NoSuchElementException e) { + continue; + } + } + } + + @Override + public SellOrder remove(SellOrder item) { + if (item == null) return null; + + map.computeIfPresent(item.getPrice(), (key, orders) -> { + boolean removed = orders.remove(item); + if (removed) size.decrementAndGet(); + return orders.isEmpty() ? null : orders; + }); + + return item; + } + + @Override + public long size() { + return size.get(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public void clear() { + map.clear(); + } +} diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryWaitingOrders.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryWaitingOrders.java new file mode 100644 index 00000000..73c44c11 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryWaitingOrders.java @@ -0,0 +1,89 @@ +package com.cleanengine.coin.order.adapter.out.persistentce.order.queue; + +import com.cleanengine.coin.common.adapter.out.store.InMemoryPriorityQueueStore; +import com.cleanengine.coin.common.domain.port.PriorityQueueStore; +import com.cleanengine.coin.order.domain.BuyOrder; +import com.cleanengine.coin.order.domain.Order; +import com.cleanengine.coin.order.domain.OrderType; +import com.cleanengine.coin.order.domain.SellOrder; +import com.cleanengine.coin.order.domain.spi.WaitingOrders; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class NestedInMemoryWaitingOrders implements WaitingOrders { + private final String ticker; + private final PriorityQueueStore limitBuyOrderPriorityQueueStore = new NestedInMemoryBuyOrderSkipListSet(); + private final PriorityQueueStore marketBuyOrderPriorityQueueStore = new InMemoryPriorityQueueStore<>(); + private final PriorityQueueStore limitSellOrderPriorityQueueStore = new NestedInMemorySellOrderSkipListSet(); + private final PriorityQueueStore marketSellOrderPriorityQueueStore = new InMemoryPriorityQueueStore<>(); + + @Override + public String getTicker() { + return ticker; + } + + @Override + public void addOrder(Order order) { + if(order == null) throw new IllegalArgumentException("order cannot be null."); + + if (order instanceof BuyOrder) { + if(order.getIsMarketOrder()) { + marketBuyOrderPriorityQueueStore.put((BuyOrder) order); + } + else{ + limitBuyOrderPriorityQueueStore.put((BuyOrder) order); + } + } else { + if(order.getIsMarketOrder()) { + marketSellOrderPriorityQueueStore.put((SellOrder) order); + } + else{ + limitSellOrderPriorityQueueStore.put((SellOrder) order); + } + } + } + + @Override + public PriorityQueueStore getBuyOrderPriorityQueueStore(OrderType orderType) { + return orderType == OrderType.MARKET ? marketBuyOrderPriorityQueueStore : limitBuyOrderPriorityQueueStore; + } + + @Override + public PriorityQueueStore getSellOrderPriorityQueueStore(OrderType orderType) { + return orderType == OrderType.MARKET ? marketSellOrderPriorityQueueStore : limitSellOrderPriorityQueueStore; + } + + @Override + public void removeOrder(Order order) { + if(order == null) throw new IllegalArgumentException("order cannot be null."); + + if (order instanceof BuyOrder) { + if(order.getIsMarketOrder()) { + marketBuyOrderPriorityQueueStore.remove((BuyOrder) order); + } + else{ + limitBuyOrderPriorityQueueStore.remove((BuyOrder) order); + } + } else { + if(order.getIsMarketOrder()) { + marketSellOrderPriorityQueueStore.remove((SellOrder) order); + } + else{ + limitSellOrderPriorityQueueStore.remove((SellOrder) order); + } + } + } + + @Override + public void clearAllQueues() { + limitBuyOrderPriorityQueueStore.clear(); + marketBuyOrderPriorityQueueStore.clear(); + limitSellOrderPriorityQueueStore.clear(); + marketSellOrderPriorityQueueStore.clear(); + } + + @Override + public void close() { + + } +} diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryWaitingOrdersManager.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryWaitingOrdersManager.java new file mode 100644 index 00000000..0fd9b21e --- /dev/null +++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryWaitingOrdersManager.java @@ -0,0 +1,44 @@ +package com.cleanengine.coin.order.adapter.out.persistentce.order.queue; + +import com.cleanengine.coin.order.domain.spi.WaitingOrders; +import com.cleanengine.coin.order.domain.spi.WaitingOrdersManager; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Optional; + +@Slf4j +@Component +public class NestedInMemoryWaitingOrdersManager implements WaitingOrdersManager { + private final HashMap waitingOrdersMap = new HashMap<>(); + + @Override + public WaitingOrders getWaitingOrders(String ticker) { + if(!waitingOrdersMap.containsKey(ticker)) { + addWaitingOrders(ticker); + } + + Optional waitingOrdersOpt = Optional.ofNullable(waitingOrdersMap.get(ticker)); + if(waitingOrdersOpt.isEmpty()){ + log.debug("WaitingOrders not found. with " + ticker); + throw new RuntimeException("WaitingOrders not found with " + ticker); + } + return waitingOrdersOpt.get(); } + + @Override + public void removeWaitingOrders(String ticker) { + waitingOrdersMap.remove(ticker); + } + + @Override + public void close() { + + } + + protected synchronized void addWaitingOrders(String ticker){ + if(!waitingOrdersMap.containsKey(ticker)){ + waitingOrdersMap.put(ticker, new NestedInMemoryWaitingOrders(ticker)); + } + } +} diff --git a/src/main/resources/application-actuator.yml b/src/main/resources/application-actuator.yml index 167d9532..bd3c931b 100644 --- a/src/main/resources/application-actuator.yml +++ b/src/main/resources/application-actuator.yml @@ -9,4 +9,19 @@ management: prometheus: metrics: export: - enabled: true \ No newline at end of file + enabled: true +server: + tomcat: + mbeanregistry: + enabled: true + +spring: + jmx: + enabled: true + jpa: + properties: + hibernate: + generate_statistics: true +logging: + level: + org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN diff --git a/src/main/resources/application-loadtest.yml b/src/main/resources/application-loadtest.yml new file mode 100644 index 00000000..e5b0bc62 --- /dev/null +++ b/src/main/resources/application-loadtest.yml @@ -0,0 +1,5 @@ +spring: + sql: + init: + mode: always + data-locations: classpath:dataset/`if`.users.sql,classpath:dataset/`if`.account.sql,classpath:dataset/`if`.walletBTC.sql,classpath:dataset/`if`.walletTRUMP.sql diff --git a/src/test/java/com/cleanengine/coin/tool/TokenGenerator.java b/src/test/java/com/cleanengine/coin/tool/TokenGenerator.java new file mode 100644 index 00000000..a89b282e --- /dev/null +++ b/src/test/java/com/cleanengine/coin/tool/TokenGenerator.java @@ -0,0 +1,60 @@ +package com.cleanengine.coin.tool; + +import com.cleanengine.coin.user.login.application.JWTUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Profile; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +@SpringBootTest +@Profile("dev, it") +public class TokenGenerator { + + @Value("${JWT_SECRET}") + private String secret; + + @Test + public void createToken(){ + JWTUtil jwtUtil = new JWTUtil(secret); + System.out.println(jwtUtil.createJwt(1001, 1000 * 60*60*24*365L)); + System.out.println(jwtUtil.createJwt(1002, 1000 * 60*60*24*365L)); + } + + @Test + public void createTokens(){ + int size = 0; + List tokens = new ArrayList<>(); + + JWTUtil jwtUtil = new JWTUtil(secret); + + for (int i = 1; i<=1000; i++){ + tokens.add(jwtUtil.createJwt(i, 1000 * 60*60*24*365L)); + size++; + } + + ObjectMapper objectMapper = new ObjectMapper(); + + try{ + File outputFile = new File("tokens.json"); + objectMapper.writerWithDefaultPrettyPrinter().writeValue(outputFile, new UserTokens(size, tokens)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static class UserTokens{ + public int size; + public List tokens; + + public UserTokens(int size, List tokens) { + this.size = size; + this.tokens = tokens; + } + } +}