Skip to content

Commit a968766

Browse files
authored
Merge pull request #178 from CleanEngine/dev
Dev
2 parents 9809fdd + 8bf7b2e commit a968766

File tree

48 files changed

+1514
-884
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1514
-884
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ dependencies {
5858
implementation 'org.springframework.boot:spring-boot-starter-actuator'
5959
implementation 'io.micrometer:micrometer-registry-prometheus'
6060
implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:2.16.0")
61+
implementation 'org.hibernate.orm:hibernate-micrometer'
6162

6263
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
6364
implementation 'com.google.code.gson:gson:2.13.1'

docker/mariadb/init.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,5 @@ INSERT INTO `if`.asset (ticker, name) VALUES ('TRUMP', '오피셜트럼프');
110110

111111
INSERT INTO `if`.wallet (wallet_id, account_id, buy_price, roi, size, ticker) VALUES (1, 1, 0, 0, 500000000, 'BTC');
112112
INSERT INTO `if`.wallet (wallet_id, account_id, buy_price, roi, size, ticker) VALUES (2, 1, 0, 0, 500000000, 'TRUMP');
113+
INSERT INTO `if`.wallet (wallet_id, account_id, buy_price, roi, size, ticker) VALUES (3, 2, 0, 0, 500000000, 'BTC');
114+
INSERT INTO `if`.wallet (wallet_id, account_id, buy_price, roi, size, ticker) VALUES (4, 2, 0, 0, 500000000, 'TRUMP');

monitoring/docker-compose.yml

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ services:
99
- /etc/localtime:/etc/localtime:ro
1010
- ./opentelemetry-javaagent.jar:/app/opentelemetry-javaagent.jar
1111
working_dir: /app
12-
command: [ "java", "-jar", "coin-0.0.1-SNAPSHOT.jar", "--spring.profiles.active=dev,mariadb-local,actuator,apm" ]
12+
command: [ "java", "-jar", "coin-0.0.1-SNAPSHOT.jar", "--spring.profiles.active=dev,it,mariadb-local,actuator,apm" ]
1313
ports:
1414
- "8080:8080"
1515
env_file:
@@ -18,14 +18,16 @@ services:
1818
- TZ=Asia/Seoul
1919
- OTEL_SERVICE_NAME=my-spring-app
2020
- OTEL_TRACES_EXPORTER=otlp
21-
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318
21+
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
2222
- OTEL_LOGS_EXPORTER=none
2323
- OTEL_METRICS_EXPORTER=none
2424
- OTEL_INSTRUMENTATION_METHODS_ENABLED=true
25-
- JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005, -javaagent:/app/opentelemetry-javaagent.jar
25+
- JAVA_TOOL_OPTIONS=-javaagent:/app/opentelemetry-javaagent.jar
2626
depends_on:
2727
mariadb:
2828
condition: service_healthy
29+
otel-collector:
30+
condition: service_started
2931
networks:
3032
- app-network
3133
- monitoring-net
@@ -60,12 +62,31 @@ services:
6062
command:
6163
- '--config.file=/etc/prometheus/prometheus.yml'
6264
- '--storage.tsdb.retention.time=30d'
65+
- '--web.enable-remote-write-receiver'
6366
ports:
6467
- "9090:9090"
6568
networks:
6669
- monitoring-net
6770
restart: unless-stopped
6871

72+
influxdb:
73+
image: influxdb:2.7
74+
container_name: influxdb
75+
ports:
76+
- "8086:8086"
77+
volumes:
78+
- influxdb_data:/var/lib/influxdb2
79+
environment:
80+
- DOCKER_INFLUXDB_INIT_MODE=setup
81+
- DOCKER_INFLUXDB_INIT_USERNAME=k6-user # 임의의 사용자/비밀번호
82+
- DOCKER_INFLUXDB_INIT_PASSWORD=k6-password
83+
- DOCKER_INFLUXDB_INIT_ORG=k6-org
84+
- DOCKER_INFLUXDB_INIT_BUCKET=k6-bucket
85+
- DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=vJvTx5b8vjgH7mu-M-BsEvdy5_COovexAjyniVhMF1yPzvsp2g8kp62opqXVMq6ICAq2tLJxxD6ifXPBZ9YVmA==
86+
networks:
87+
- monitoring-net
88+
restart: unless-stopped
89+
6990
grafana:
7091
image: grafana/grafana:11.0.0
7192
container_name: grafana
@@ -80,22 +101,53 @@ services:
80101
depends_on:
81102
- prometheus
82103
- jaeger
104+
- influxdb
83105

84106
jaeger:
85107
image: jaegertracing/all-in-one:latest
86108
container_name: jaeger
109+
user: "${UID}:${GID}"
110+
environment:
111+
- SPAN_STORAGE_TYPE=badger
112+
- BADGER_EPHEMERAL=false
113+
- BADGER_DIRECTORY_VALUE=/tmp/jaeger/data
114+
- BADGER_DIRECTORY_KEY=/tmp/jaeger/keys
115+
- COLLECTOR_OTLP_ENABLED=true
116+
- COLLECTOR_OTLP_GRPC_HOST_PORT=0.0.0.0:4317
117+
- COLLECTOR_OTLP_HTTP_HOST_PORT=0.0.0.0:4318
118+
volumes:
119+
- jaeger_data:/tmp/jaeger
87120
ports:
88-
- "16686:16686"
121+
- "16686:16686" # UI
122+
- "4319:4317"
123+
- "4320:4318"
124+
networks:
125+
- monitoring-net
126+
127+
128+
otel-collector:
129+
image: otel/opentelemetry-collector-contrib:latest
130+
command: ["--config=/etc/otel-collector-config.yml"]
131+
volumes:
132+
- ./otel/otel-collector-config.yml:/etc/otel-collector-config.yml
133+
ports:
134+
- "8889:8889"
89135
- "4317:4317"
90136
- "4318:4318"
137+
- "13133:13133"
91138
networks:
92139
- monitoring-net
140+
depends_on:
141+
jaeger:
142+
condition: service_started
93143

94144
volumes:
95145
prometheus_data: {}
96146
grafana_data: {}
97147
mariadb_data:
98148
driver: local
149+
influxdb_data: {}
150+
jaeger_data: {}
99151

100152
networks:
101153
app-network:
@@ -104,3 +156,8 @@ networks:
104156
monitoring-net:
105157
name: monitoring-net
106158
driver: bridge
159+
160+
161+
# export K6_INFLUXDB_ORGANIZATION=k6-org
162+
# - export K6_INFLUXDB_BUCKET=k6-bucket
163+
# ./k6 run --out xk6-influxdb=http://localhost:8086 /Users/jangbongjun/coin/src/test/resources/k6/chart-stomp-test.js

monitoring/grafana/provisioning/datsources/datasource.yml renamed to monitoring/grafana/provisioning/datasources/datasource.yml

File renamed without changes.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
receivers:
2+
otlp:
3+
protocols:
4+
grpc:
5+
endpoint: 0.0.0.0:4317
6+
http:
7+
endpoint: 0.0.0.0:4318
8+
9+
processors:
10+
batch:
11+
timeout: 500ms
12+
send_batch_size: 8192
13+
send_batch_max_size: 16384
14+
probabilistic_sampler:
15+
sampling_percentage: 1
16+
17+
connectors:
18+
spanmetrics:
19+
histogram:
20+
explicit:
21+
buckets: [100us, 1ms, 2ms, 6ms, 10ms, 100ms, 250ms, 500ms, 1s]
22+
23+
exporters:
24+
otlp/jaeger:
25+
endpoint: jaeger:4317
26+
tls:
27+
insecure: true
28+
29+
prometheus:
30+
endpoint: "0.0.0.0:8889"
31+
32+
service:
33+
pipelines:
34+
traces/metrics:
35+
receivers: [otlp]
36+
processors: [batch]
37+
exporters: [spanmetrics]
38+
39+
traces/jaeger:
40+
receivers: [otlp]
41+
processors: [probabilistic_sampler,batch]
42+
exporters: [otlp/jaeger]
43+
44+
metrics:
45+
receivers: [spanmetrics]
46+
processors: [batch]
47+
exporters: [prometheus]

monitoring/prometheus/prometheus.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ scrape_configs:
88
- job_name: 'my-app'
99
static_configs:
1010
- targets: [ 'app:8080' ]
11-
metrics_path: /actuator/prometheus
11+
metrics_path: /actuator/prometheus
12+
- job_name: 'otel-collector'
13+
scrape_interval: 15s
14+
static_configs:
15+
- targets: [ 'otel-collector:8889' ]

src/main/java/com/cleanengine/coin/chart/controller/ChartDataController.java

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -37,51 +37,35 @@ public void publishRealTimeOhlc() {
3737
try {
3838
log.debug("△ 실시간 OHLC 데이터 스케줄러 실행");
3939

40-
// 구독된 티커가 없으면 조기 종료
4140
if (subscriptionService.getAllRealTimeOhlcSubscribedTickers().isEmpty()) {
4241
log.debug("실시간 OHLC 구독된 티커 없음, 전송 생략");
4342
return;
4443
}
44+
final LocalDateTime now = LocalDateTime.now();
4545

46-
// 모든 구독된 티커에 대해 데이터 전송
4746
for (String ticker : subscriptionService.getAllRealTimeOhlcSubscribedTickers()) {
4847
try {
4948
log.debug("티커 {} 실시간 OHLC 데이터 전송 중...", ticker);
5049

51-
// 티커별 최신 OHLC 데이터 조회 및 전송
52-
RealTimeOhlcDto ohlcData = realTimeOhlcService.getRealTimeOhlc(ticker);
50+
RealTimeOhlcDto ohlcData = realTimeOhlcService.getAndUpdateCumulative1mOhlc(ticker, now);
5351

5452
if (ohlcData == null) {
55-
// 이전에 전송한 데이터가 있는지 확인
5653
RealTimeOhlcDto lastSentData = lastSentOhlcDataMap.get(ticker);
57-
5854
if (lastSentData != null) {
59-
// 이전 데이터가 있으면 타임스탬프만 업데이트하여 재사용
6055
log.debug("티커 {}의 실시간 OHLC 데이터가 없습니다. 이전 데이터 재사용", ticker);
61-
RealTimeOhlcDto updatedData = new RealTimeOhlcDto(lastSentData.getTicker(), LocalDateTime.now(), // 현재 시간으로 업데이트
56+
RealTimeOhlcDto updatedData = new RealTimeOhlcDto(lastSentData.getTicker(), now,
6257
lastSentData.getOpen(), lastSentData.getHigh(), lastSentData.getLow(), lastSentData.getClose(), lastSentData.getVolume());
63-
6458
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, updatedData);
65-
lastSentOhlcDataMap.put(ticker, updatedData); // 캐시 업데이트
59+
lastSentOhlcDataMap.put(ticker, updatedData);
6660
} else {
67-
// 이전 데이터도 없는 경우 빈 데이터 전송 (첫 구독 시)
6861
log.debug("티커 {}의 이전 OHLC 데이터도 없습니다. 빈 데이터 전송", ticker);
69-
RealTimeOhlcDto emptyData = new RealTimeOhlcDto();
70-
emptyData.setTicker(ticker);
71-
emptyData.setTimestamp(LocalDateTime.now());
72-
emptyData.setOpen(0.0);
73-
emptyData.setHigh(0.0);
74-
emptyData.setLow(0.0);
75-
emptyData.setClose(0.0);
76-
emptyData.setVolume(0.0);
77-
62+
RealTimeOhlcDto emptyData = new RealTimeOhlcDto(ticker, now, 0.0, 0.0, 0.0, 0.0, 0.0);
7863
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, emptyData);
79-
lastSentOhlcDataMap.put(ticker, emptyData); // 캐시 업데이트
64+
lastSentOhlcDataMap.put(ticker, emptyData);
8065
}
8166
} else {
82-
// 조회된 실시간 OHLC 데이터 전송
8367
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, ohlcData);
84-
lastSentOhlcDataMap.put(ticker, ohlcData); // 캐시 업데이트
68+
lastSentOhlcDataMap.put(ticker, ohlcData);
8569
log.debug("실시간 OHLC 데이터 전송: {}", ohlcData);
8670
}
8771
} catch (Exception e) {
@@ -91,8 +75,5 @@ public void publishRealTimeOhlc() {
9175
} catch (Exception e) {
9276
log.error("△ 실시간 OHLC 데이터 발행 중 오류: {}", e.getMessage(), e);
9377
}
94-
9578
}
96-
97-
9879
}
Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,38 @@
11
package com.cleanengine.coin.chart.controller;
22

33
import com.cleanengine.coin.chart.dto.RealTimeOhlcDto;
4-
import com.cleanengine.coin.chart.service.minute.MinuteOhlcDataService;
4+
import com.cleanengine.coin.chart.service.minute.PagingMinuteOhlcDataService;
55
import lombok.RequiredArgsConstructor;
66
import org.springframework.http.ResponseEntity;
77
import org.springframework.web.bind.annotation.*;
88

9+
import java.time.LocalDateTime;
910
import java.util.List;
1011

1112
@RestController
1213
@RequestMapping("/api/minute-ohlc")
1314
@RequiredArgsConstructor
1415
public class MinuteOhlcDataController {
1516

16-
private final MinuteOhlcDataService service;
17+
private final PagingMinuteOhlcDataService service;
1718

1819
/**
19-
* GET /api/minute-ohlc?ticker=BTC
20-
* DB에 있는 과거 거래를 1분 단위로 묶어 OHLC+volume을 계산한 리스트 반환
20+
* GET /api/minute-ohlc?ticker=BTC&count=100&interval=1&from=2025-06-19T10:30
21+
* DB에 있는 과거 거래를 interval 단위로 묶어 OHLC+volume을 계산한 리스트 반환
2122
*/
2223
@GetMapping
2324
public ResponseEntity<List<RealTimeOhlcDto>> getMinuteOhlc(
24-
@RequestParam("ticker") String ticker
25+
@RequestParam("ticker") String ticker,
26+
@RequestParam(value = "count", defaultValue = "100") int count,
27+
@RequestParam(value = "interval", defaultValue = "1") int interval,
28+
@RequestParam(value = "from", required = false) LocalDateTime from
2529
) {
26-
List<RealTimeOhlcDto> data = service.getMinuteOhlcData(ticker);
30+
if (from == null) {
31+
from = LocalDateTime.now();
32+
}
33+
34+
List<RealTimeOhlcDto> data = service.getMinuteOhlcData(ticker, count, interval, from.minusMinutes(1));
2735
return ResponseEntity.ok(data);
2836
}
37+
2938
}

src/main/java/com/cleanengine/coin/chart/controller/WebSocketMessageController.java

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import com.cleanengine.coin.chart.dto.RealTimeOhlcDto;
44
import com.cleanengine.coin.chart.service.ChartSubscriptionService;
5-
import com.cleanengine.coin.chart.service.RealTimeOhlcService;
5+
import com.cleanengine.coin.chart.service.RealTimeOhlcService; // RealTimeOhlcService 의존성은 이제 불필요
66
import lombok.Getter;
77
import lombok.RequiredArgsConstructor;
88
import lombok.Setter;
@@ -19,9 +19,8 @@
1919
public class WebSocketMessageController {
2020

2121
private final ChartSubscriptionService subscriptionService;
22-
private final RealTimeOhlcService realTimeOhlcService;
2322
private final SimpMessagingTemplate messagingTemplate;
24-
private final ChartDataController chartDataController;
23+
private final ChartDataController chartDataController; // 이미 계산된 데이터를 가진 컨트롤러를 활용
2524

2625
/**
2726
* 실시간 OHLC 데이터 구독 처리
@@ -34,30 +33,16 @@ public void subscribeRealTimeOhlc(RealTimeTradeMappingDto request) {
3433
// 구독 목록에 추가
3534
subscriptionService.subscribeRealTimeOhlc(ticker);
3635

37-
// 구독 즉시 최근 실시간 OHLC 데이터 전송
38-
RealTimeOhlcDto latestOhlcData = realTimeOhlcService.getRealTimeOhlc(ticker);
39-
4036
RealTimeOhlcDto lastSentData = chartDataController.getLastSentOhlcDataMap().get(ticker);
4137

42-
if (latestOhlcData == null) {
43-
if (lastSentData != null) {
44-
// 이전에 전송한 데이터가 있으면 재사용
45-
log.debug("티커 {}의 실시간 OHLC 데이터가 없습니다. 이전 데이터 재사용", ticker);
46-
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, lastSentData);
47-
} else {
48-
// 이전 데이터도 없는 경우 빈 데이터 전송
49-
log.debug("티커 {}의 실시간 OHLC 데이터가 없습니다. 빈 데이터 전송", ticker);
50-
RealTimeOhlcDto emptyData = createEmptyRealTimeOhlcDto(ticker);
51-
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, emptyData);
52-
// 빈 데이터도 캐시에 저장
53-
chartDataController.getLastSentOhlcDataMap().put(ticker, emptyData);
54-
}
38+
if (lastSentData == null) {
39+
log.debug("티커 {}의 캐시된 OHLC 데이터가 없습니다. 빈 데이터 전송", ticker);
40+
RealTimeOhlcDto emptyData = createEmptyRealTimeOhlcDto(ticker);
41+
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, emptyData);
5542
} else {
56-
log.debug("티커 {}의 실시간 OHLC 데이터 전송: {}", ticker, latestOhlcData);
57-
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, latestOhlcData);
58-
// 데이터 캐시에 저장
59-
chartDataController.getLastSentOhlcDataMap().put(ticker, latestOhlcData);
60-
43+
// 캐시된 데이터가 있으면 즉시 전송
44+
log.debug("티커 {}의 캐시된 OHLC 데이터 즉시 전송: {}", ticker, lastSentData);
45+
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, lastSentData);
6146
}
6247
}
6348

@@ -80,6 +65,5 @@ private RealTimeOhlcDto createEmptyRealTimeOhlcDto(String ticker) {
8065
@Getter
8166
public static class RealTimeTradeMappingDto {
8267
private String ticker;
83-
8468
}
8569
}

0 commit comments

Comments
 (0)