diff --git a/.gitignore b/.gitignore index 661ab51d..f395d417 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,6 @@ out/ ### 로컬 환경변수 ### local.properties /logs -docker-compose.override.yml \ No newline at end of file + +docker-compose.override.yml +opentelemetry-javaagent.jar \ No newline at end of file diff --git a/build.gradle b/build.gradle index 1e947b67..83ce59b6 100644 --- a/build.gradle +++ b/build.gradle @@ -57,6 +57,7 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8' 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 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.google.code.gson:gson:2.13.1' diff --git a/monitoring/docker-compose.yml b/monitoring/docker-compose.yml new file mode 100644 index 00000000..1337a0b1 --- /dev/null +++ b/monitoring/docker-compose.yml @@ -0,0 +1,106 @@ +version: '3.8' + +services: + app: + image: eclipse-temurin:21-jre-alpine + container_name: my-spring-app2 + volumes: + - ../build/libs/coin-0.0.1-SNAPSHOT.jar:/app/coin-0.0.1-SNAPSHOT.jar + - /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" ] + ports: + - "8080:8080" + env_file: + - ../docker/local.properties + environment: + - TZ=Asia/Seoul + - OTEL_SERVICE_NAME=my-spring-app + - OTEL_TRACES_EXPORTER=otlp + - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger: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 + depends_on: + mariadb: + condition: service_healthy + networks: + - app-network + - monitoring-net + + mariadb: + image: mariadb:latest + container_name: mariadb2 + ports: + - "3306:3306" + env_file: + - ../docker/local.properties + environment: + - TZ=Asia/Seoul + volumes: + - mariadb_data:/var/lib/mysql + - ../docker/mariadb/init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: [ "CMD", "healthcheck.sh", "--connect", "--innodb_initialized" ] + interval: 10s + timeout: 5s + retries: 10 + networks: + - app-network + - monitoring-net + + prometheus: + image: prom/prometheus:v2.53.0 + container_name: prometheus + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.retention.time=30d' + ports: + - "9090:9090" + networks: + - monitoring-net + restart: unless-stopped + + grafana: + image: grafana/grafana:11.0.0 + container_name: grafana + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + ports: + - "3000:3000" + networks: + - monitoring-net + restart: unless-stopped + depends_on: + - prometheus + - jaeger + + jaeger: + image: jaegertracing/all-in-one:latest + container_name: jaeger + ports: + - "16686:16686" + - "4317:4317" + - "4318:4318" + networks: + - monitoring-net + +volumes: + prometheus_data: {} + grafana_data: {} + mariadb_data: + driver: local + +networks: + app-network: + name: app-network + driver: bridge + monitoring-net: + name: monitoring-net + driver: bridge diff --git a/monitoring/grafana/provisioning/datsources/datasource.yml b/monitoring/grafana/provisioning/datsources/datasource.yml new file mode 100644 index 00000000..834801f3 --- /dev/null +++ b/monitoring/grafana/provisioning/datsources/datasource.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + url: http://prometheus:9090 + access: proxy + isDefault: true + editable: true \ No newline at end of file diff --git a/monitoring/prometheus/prometheus.yml b/monitoring/prometheus/prometheus.yml new file mode 100644 index 00000000..d1690101 --- /dev/null +++ b/monitoring/prometheus/prometheus.yml @@ -0,0 +1,11 @@ +global: + scrape_interval: 10s + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['prometheus:9090'] + - job_name: 'my-app' + static_configs: + - targets: [ 'app:8080' ] + metrics_path: /actuator/prometheus \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/common/annotation/StartNewTrace.java b/src/main/java/com/cleanengine/coin/common/annotation/StartNewTrace.java new file mode 100644 index 00000000..1d94c1d7 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/common/annotation/StartNewTrace.java @@ -0,0 +1,12 @@ +package com.cleanengine.coin.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface StartNewTrace { + String value() default ""; +} diff --git a/src/main/java/com/cleanengine/coin/common/annotation/StartNewTraceAspect.java b/src/main/java/com/cleanengine/coin/common/annotation/StartNewTraceAspect.java new file mode 100644 index 00000000..5c292600 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/common/annotation/StartNewTraceAspect.java @@ -0,0 +1,43 @@ +package com.cleanengine.coin.common.annotation; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; +import java.lang.reflect.Method; + +@Aspect +@Component +@RequiredArgsConstructor +public class StartNewTraceAspect { + + @Around("@annotation(com.cleanengine.coin.common.annotation.StartNewTrace)") + public Object createNewTrace(ProceedingJoinPoint joinPoint) throws Throwable { + Tracer tracer = GlobalOpenTelemetry.getTracer("com.cleanengine.coin"); + + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + StartNewTrace newTraceAnnotation = method.getAnnotation(StartNewTrace.class); + + String spanName = newTraceAnnotation.value().isEmpty() ? + signature.getDeclaringType().getSimpleName() + "." + method.getName() : + newTraceAnnotation.value(); + + Span span = tracer.spanBuilder(spanName).setNoParent().startSpan(); + + try (Scope scope = span.makeCurrent()) { + return joinPoint.proceed(); + } catch (Exception e) { + span.recordException(e); + throw e; + } finally { + span.end(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/configuration/WebSocketConfig.java b/src/main/java/com/cleanengine/coin/configuration/WebSocketConfig.java index 8c34c4bd..2341cfd7 100644 --- a/src/main/java/com/cleanengine/coin/configuration/WebSocketConfig.java +++ b/src/main/java/com/cleanengine/coin/configuration/WebSocketConfig.java @@ -1,6 +1,7 @@ package com.cleanengine.coin.configuration; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; @@ -11,14 +12,8 @@ @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - private static final String[] ALLOWED_ORIGINS = { - "http://localhost:63342", - "http://localhost:63343", - "http://localhost:8080", - "http://localhost:5500", - "http://localhost:5173", - "https://investfuture.my" - }; + @Value("${spring.security.allowed-origins}") + private String[] allowedOrigins; @Override public void configureMessageBroker(MessageBrokerRegistry config) { @@ -34,9 +29,7 @@ public void registerStompEndpoints(StompEndpointRegistry registry) { private void registerEndpoint(StompEndpointRegistry registry, String endpoint) { registry.addEndpoint(endpoint) - .setAllowedOrigins(ALLOWED_ORIGINS); + .setAllowedOrigins(allowedOrigins); } -} - - +} \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/configuration/apiSwagger/SwaggerConfig.java b/src/main/java/com/cleanengine/coin/configuration/apiSwagger/SwaggerConfig.java index 8c4ee557..9c52f45d 100644 --- a/src/main/java/com/cleanengine/coin/configuration/apiSwagger/SwaggerConfig.java +++ b/src/main/java/com/cleanengine/coin/configuration/apiSwagger/SwaggerConfig.java @@ -3,41 +3,26 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.security.SecurityRequirement; -import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.servers.Server; @Configuration public class SwaggerConfig { + @Bean public OpenAPI openAPI() { // API 기본 정보 설정 Info info = new Info() - .title("investFuture API Document") + .title("InvestFuture API Document") .version("1.0") - .description( - "환영합니다.\n") + .description("Private API 호출 시 Cookie에 직접 설정해주세요!\n") .contact(new io.swagger.v3.oas.models.info.Contact().email("billage.official@gmail.com")); - // JWT 인증 방식 설정 - String jwtScheme = "jwtAuth"; - SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwtScheme); - Components components = new Components() - .addSecuritySchemes(jwtScheme, new SecurityScheme() - .name("Authorization") - .type(SecurityScheme.Type.HTTP) - .in(SecurityScheme.In.HEADER) - .scheme("Bearer") - .bearerFormat("JWT")); - // Swagger UI 설정 및 보안 추가 return new OpenAPI() .addServersItem(new Server().url("http://localhost:8080")) // 추가적인 서버 URL 설정 가능 - .components(components) - .info(info) - .addSecurityItem(securityRequirement); + .info(info); } + } \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/orderbook/domain/OrderBook.java b/src/main/java/com/cleanengine/coin/orderbook/domain/OrderBook.java index adc60911..fb9fc341 100644 --- a/src/main/java/com/cleanengine/coin/orderbook/domain/OrderBook.java +++ b/src/main/java/com/cleanengine/coin/orderbook/domain/OrderBook.java @@ -22,18 +22,14 @@ public void updateOrderBookOnNewOrder(boolean isBuyOrder, Double price, Double o if(isBuyOrder){ BuyOrderBookUnit buyOrderBookUnit = buyOrderBookUnitMap.get(price); if(buyOrderBookUnit == null){ - buyOrderBookUnit = new BuyOrderBookUnit(price, orderSize); - buyOrderBookUnitMap.put(price, buyOrderBookUnit); - buyOrderBookUnitListSet.add(buyOrderBookUnit); + addBuyOrderBookUnit(price, orderSize); } else { buyOrderBookUnit.addOrder(orderSize); } } else { SellOrderBookUnit sellOrderBookUnit = sellOrderBookUnitMap.get(price); if(sellOrderBookUnit == null){ - sellOrderBookUnit = new SellOrderBookUnit(price, orderSize); - sellOrderBookUnitMap.put(price, sellOrderBookUnit); - sellOrderBookUnitListSet.add(sellOrderBookUnit); + addSellOrderBookUnit(price, orderSize); } else { sellOrderBookUnit.addOrder(orderSize); } @@ -41,19 +37,27 @@ public void updateOrderBookOnNewOrder(boolean isBuyOrder, Double price, Double o } public void updateOrderBookOnTradeExecuted(boolean isBuyOrder, Double price, Double orderSize) { - if(isBuyOrder){ + if(approxEquals(orderSize, 0.0) || approxEquals(price, 0.0)){ + throw new IllegalArgumentException("orderSize or price cannot be 0.0"); + } + + if(isBuyOrder) { BuyOrderBookUnit buyOrderBookUnit = buyOrderBookUnitMap.get(price); + if(buyOrderBookUnit == null) { + return; + } buyOrderBookUnit.executeTrade(orderSize); if(approxEquals(buyOrderBookUnit.getSize(), 0.0)){ - buyOrderBookUnitMap.remove(price); - buyOrderBookUnitListSet.remove(buyOrderBookUnit); + removeBuyOrderBookUnit(buyOrderBookUnit, price); } } else { SellOrderBookUnit sellOrderBookUnit = sellOrderBookUnitMap.get(price); + if(sellOrderBookUnit == null) { + return; + } sellOrderBookUnit.executeTrade(orderSize); if(approxEquals(sellOrderBookUnit.getSize(), 0.0)){ - sellOrderBookUnitMap.remove(price); - sellOrderBookUnitListSet.remove(sellOrderBookUnit); + removeSellOrderBookUnit(sellOrderBookUnit, price); } } } @@ -71,4 +75,40 @@ public List getSellOrderBookList(int size){ .limit(size) .collect(Collectors.toList()); } + + protected synchronized void addBuyOrderBookUnit(Double price, Double size) { + BuyOrderBookUnit buyOrderBookUnit = buyOrderBookUnitMap.get(price); + if(buyOrderBookUnit != null){ + return; + } + + buyOrderBookUnit = new BuyOrderBookUnit(price, size); + buyOrderBookUnitMap.put(price, buyOrderBookUnit); + buyOrderBookUnitListSet.add(buyOrderBookUnit); + } + + protected synchronized void addSellOrderBookUnit(Double price, Double size) { + SellOrderBookUnit sellOrderBookUnit = sellOrderBookUnitMap.get(price); + if(sellOrderBookUnit != null){ + return; + } + + sellOrderBookUnit = new SellOrderBookUnit(price, size); + sellOrderBookUnitMap.put(price, sellOrderBookUnit); + sellOrderBookUnitListSet.add(sellOrderBookUnit); + } + + protected synchronized void removeBuyOrderBookUnit(BuyOrderBookUnit buyOrderBookUnit, Double price) { + if(approxEquals(buyOrderBookUnit.getSize(), 0.0)) { + buyOrderBookUnitMap.remove(price); + buyOrderBookUnitListSet.remove(buyOrderBookUnit); + } + } + + protected synchronized void removeSellOrderBookUnit(SellOrderBookUnit sellOrderBookUnit, Double price) { + if(approxEquals(sellOrderBookUnit.getSize(), 0.0)) { + sellOrderBookUnitMap.remove(price); + sellOrderBookUnitListSet.remove(sellOrderBookUnit); + } + } } diff --git a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java index e55a2059..9bf89e12 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java @@ -21,7 +21,6 @@ @Slf4j @Component -@WorkingServerProfile @RequiredArgsConstructor public class ApiScheduler { diff --git a/src/main/java/com/cleanengine/coin/realitybot/config/SchedulerConfig.java b/src/main/java/com/cleanengine/coin/realitybot/config/SchedulerConfig.java index 86aa2a3a..c5cf789f 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/config/SchedulerConfig.java +++ b/src/main/java/com/cleanengine/coin/realitybot/config/SchedulerConfig.java @@ -1,5 +1,6 @@ package com.cleanengine.coin.realitybot.config; +import com.cleanengine.coin.common.annotation.WorkingServerProfile; import com.cleanengine.coin.realitybot.api.ApiScheduler; import com.cleanengine.coin.realitybot.api.UnitPriceRefresher; import org.springframework.beans.factory.annotation.Value; @@ -11,6 +12,7 @@ import java.time.Duration; @Configuration +@WorkingServerProfile @EnableScheduling //@RequiredArgsConstructor public class SchedulerConfig implements SchedulingConfigurer { diff --git a/src/main/java/com/cleanengine/coin/trade/application/TradeExecutor.java b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutor.java index cb7338f4..b8e4ca14 100644 --- a/src/main/java/com/cleanengine/coin/trade/application/TradeExecutor.java +++ b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutor.java @@ -71,15 +71,16 @@ public void executeTrade(WaitingOrders waitingOrders, TradePair tr tradeService.updateOrder(sellOrder); // 예수금 처리 + // - 매도 예수금 처리 + this.increaseAccountCash(sellOrder, totalTradedPrice); + // - 매수 잔여금액 반환 if (!isMarketOrder(buyOrder) && buyOrder.getPrice() > tradedPrice) { // 매도 호가보다 높은 가격에 매수를 시도한 경우, 차액 반환 + log.debug("[{}] 매도 호가보다 높은 가격에 매수를 시도한 경우, 차액 반환", Thread.currentThread().threadId()); double totalRefundAmount = (buyOrder.getPrice() - tradedPrice) * tradedSize; this.increaseAccountCash(buyOrder, totalRefundAmount); } - // - 매도 예수금 처리 - this.increaseAccountCash(sellOrder, totalTradedPrice); - // 지갑 누적계산 this.updateWalletAfterTrade(buyOrder, ticker, tradedSize, totalTradedPrice); this.updateWalletAfterTrade(sellOrder, ticker, tradedSize, totalTradedPrice); diff --git a/src/main/java/com/cleanengine/coin/user/info/application/AccountService.java b/src/main/java/com/cleanengine/coin/user/info/application/AccountService.java index 94329ad2..af743b5d 100644 --- a/src/main/java/com/cleanengine/coin/user/info/application/AccountService.java +++ b/src/main/java/com/cleanengine/coin/user/info/application/AccountService.java @@ -6,6 +6,7 @@ import com.cleanengine.coin.user.info.infra.WalletRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @@ -38,7 +39,7 @@ public Account createNewAccount(Integer userId, double cash) { return accountRepository.save(account); } - @Transactional + @Transactional(isolation = Isolation.READ_COMMITTED) public void resetBot(String ticker) { Account sellBotAccount = accountRepository.findByUserId(SELL_ORDER_BOT_ID).orElseThrow(); sellBotAccount.setCash(0.0); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e7402301..88ad9a86 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -24,10 +24,10 @@ spring: token-uri: https://kauth.kakao.com/oauth/token user-info-uri: https://kapi.kakao.com/v2/user/me user-name-attribute: id - allowed-origins: http://localhost:63342,http://localhost:8080,http://localhost:5500,http://localhost:5173,https://investfuture.my + allowed-origins: http://localhost:63342,http://localhost:63343,http://localhost:8080,http://localhost:5500,http://localhost:5173,http://localhost:3000,https://investfuture.my endpoints: public: - paths: /api/login,/api/asset,/api/oauth2,/api/healthcheck,/api/coin/realtime,/api/coin/min,/api/minute-ohlc,/v3/api-docs,/swagger,/swagger-ui,/swagger-ui.html,/swagger-resources,/webjars,/h2-console,/favicon.ico,/actuator,/test + paths: /api/login,/api/asset,/api/oauth2,/api/healthcheck,/api/coin/realtime,/api/coin/min,/api/minute-ohlc,/api/swagger,/h2-console,/actuator,/test websocket: paths: /api/coin/min,/api/coin/realtime,/api/coin/orderbook jwt: @@ -40,8 +40,14 @@ spring: hibernate: jdbc: time_zone: Asia/Seoul -order: - tickers: BTC, TRUMP + +springdoc: + api-docs: + path: /api/swagger/v3/api-docs + swagger-ui: + path: /api/swagger/swagger-ui.html + url: /api/swagger/v3/api-docs + server: forward-headers-strategy: native