From 67e44efc9924f90e615beb2c2581b88581b6991b Mon Sep 17 00:00:00 2001 From: hyxklee Date: Sun, 3 May 2026 00:53:03 +0900 Subject: [PATCH 01/15] =?UTF-8?q?refactor:=20tempo=20=EB=8F=99=EC=9E=91=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=EB=AF=B8=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/prod/monitoring/scripts/deploy.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/infra/prod/monitoring/scripts/deploy.sh b/infra/prod/monitoring/scripts/deploy.sh index 005a91a4..dc668cb2 100644 --- a/infra/prod/monitoring/scripts/deploy.sh +++ b/infra/prod/monitoring/scripts/deploy.sh @@ -42,7 +42,6 @@ for i in {1..30}; do if curl -fsS "http://127.0.0.1:12345/-/ready" >/dev/null 2>&1 && curl -fsS "http://127.0.0.1:9090/-/ready" >/dev/null 2>&1 && curl -fsS "http://127.0.0.1:3100/ready" >/dev/null 2>&1 && - curl -fsS "http://127.0.0.1:3200/ready" >/dev/null 2>&1 && curl -fsS "http://127.0.0.1:3000/api/health" >/dev/null 2>&1; then echo "[monitoring] all services healthy" break From f6d904d14f5f2aae91b2dee2c6947ecc44f01f7d Mon Sep 17 00:00:00 2001 From: hyxklee Date: Mon, 4 May 2026 10:55:27 +0900 Subject: [PATCH 02/15] =?UTF-8?q?refactor:=20=EC=9A=B4=EC=98=81=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20MySQL=20=EC=BB=A8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=84=88=20=EC=84=A4=EC=A0=95=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/prod/docker-compose.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/infra/prod/docker-compose.yml b/infra/prod/docker-compose.yml index bf2b52be..e345a5bd 100644 --- a/infra/prod/docker-compose.yml +++ b/infra/prod/docker-compose.yml @@ -19,22 +19,6 @@ services: networks: - web - mysql: - image: mysql:8.0 - container_name: weeth-prod-mysql - restart: unless-stopped - env_file: - - .env - environment: - TZ: Asia/Seoul - ports: - - "127.0.0.1:3306:3306" - volumes: - - mysql_data:/var/lib/mysql - command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci - networks: - - web - redis: image: redis:7.0 container_name: weeth-prod-redis @@ -110,8 +94,6 @@ services: ports: - "127.0.0.1:18082:8080" depends_on: - mysql: - condition: service_started redis: condition: service_started logging: @@ -129,5 +111,4 @@ networks: volumes: caddy_data: caddy_config: - mysql_data: redis_data: From 9bc74c3946158c1fc211c6023e19ae7976f7187d Mon Sep 17 00:00:00 2001 From: hyxklee Date: Mon, 4 May 2026 10:55:49 +0900 Subject: [PATCH 03/15] =?UTF-8?q?refactor:=20caddy=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/dev/scripts/deploy.sh | 2 +- infra/prod/scripts/deploy.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/dev/scripts/deploy.sh b/infra/dev/scripts/deploy.sh index f62f04dd..39927165 100755 --- a/infra/dev/scripts/deploy.sh +++ b/infra/dev/scripts/deploy.sh @@ -49,7 +49,7 @@ done echo "reverse_proxy weeth-dev-app-${NEW_COLOR}:8080" > ./caddy/upstream.conf # 현재 Caddy 컨테이너의 DOMAIN과 비교하여 변경 시에만 재생성 -CURRENT_DOMAIN=$(docker inspect weeth-dev-caddy --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^DOMAIN=' | cut -d= -f2-) +CURRENT_DOMAIN=$(docker inspect weeth-dev-caddy --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^DOMAIN=' | cut -d= -f2- || true) if [ "$CURRENT_DOMAIN" != "$DOMAIN" ]; then echo "[deploy] domain changed, recreating caddy" diff --git a/infra/prod/scripts/deploy.sh b/infra/prod/scripts/deploy.sh index bd623675..dbcf365f 100755 --- a/infra/prod/scripts/deploy.sh +++ b/infra/prod/scripts/deploy.sh @@ -49,7 +49,7 @@ done echo "reverse_proxy weeth-prod-app-${NEW_COLOR}:8080" > ./caddy/upstream.conf # 현재 Caddy 컨테이너의 DOMAIN과 비교하여 변경 시에만 재생성 -CURRENT_DOMAIN=$(docker inspect weeth-prod-caddy --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^DOMAIN=' | cut -d= -f2-) +CURRENT_DOMAIN=$(docker inspect weeth-prod-caddy --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^DOMAIN=' | cut -d= -f2- || true) if [ "$CURRENT_DOMAIN" != "$DOMAIN" ]; then echo "[deploy] domain changed, recreating caddy" From d7260b563d4ff51cf5dc29ae8a0c25a3b3a9e1be Mon Sep 17 00:00:00 2001 From: hyxklee Date: Mon, 4 May 2026 10:58:58 +0900 Subject: [PATCH 04/15] =?UTF-8?q?refactor:=20caddy=20=EC=95=B1=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=9D=B4=20=EB=8D=AE=EC=96=B4=EC=8D=A8=EC=A7=80?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ infra/dev/caddy/upstream.conf | 1 - infra/dev/scripts/deploy.sh | 7 ++++++- infra/prod/caddy/upstream.conf | 1 - infra/prod/scripts/deploy.sh | 7 ++++++- 5 files changed, 15 insertions(+), 4 deletions(-) delete mode 100644 infra/dev/caddy/upstream.conf delete mode 100644 infra/prod/caddy/upstream.conf diff --git a/.gitignore b/.gitignore index 820815ce..558383b0 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,9 @@ out/ .vscode/ /.idea/ +### Caddy upstream (runtime-managed by deploy.sh) ### +infra/*/caddy/upstream.conf + ### Environment Variables ### src/main/resources/*.env src/main/resources/*.p8 diff --git a/infra/dev/caddy/upstream.conf b/infra/dev/caddy/upstream.conf deleted file mode 100644 index 74cf5084..00000000 --- a/infra/dev/caddy/upstream.conf +++ /dev/null @@ -1 +0,0 @@ -reverse_proxy weeth-dev-app-blue:8080 diff --git a/infra/dev/scripts/deploy.sh b/infra/dev/scripts/deploy.sh index 39927165..6057338c 100755 --- a/infra/dev/scripts/deploy.sh +++ b/infra/dev/scripts/deploy.sh @@ -13,8 +13,13 @@ export APP_IMAGE DOMAIN # EC2 홈 디렉토리의 .env를 심링크 ln -sf "$HOME/.env" "$DEPLOY_DIR/.env" +# upstream.conf가 없으면 현재 실행 중인 컨테이너를 감지하여 생성 if [ ! -f ./caddy/upstream.conf ]; then - echo "reverse_proxy weeth-dev-app-blue:8080" > ./caddy/upstream.conf + if docker ps --format '{{.Names}}' | grep -q "weeth-dev-app-green"; then + echo "reverse_proxy weeth-dev-app-green:8080" > ./caddy/upstream.conf + else + echo "reverse_proxy weeth-dev-app-blue:8080" > ./caddy/upstream.conf + fi fi if grep -q "app-blue" ./caddy/upstream.conf; then diff --git a/infra/prod/caddy/upstream.conf b/infra/prod/caddy/upstream.conf deleted file mode 100644 index 7a77edec..00000000 --- a/infra/prod/caddy/upstream.conf +++ /dev/null @@ -1 +0,0 @@ -reverse_proxy weeth-prod-app-blue:8080 diff --git a/infra/prod/scripts/deploy.sh b/infra/prod/scripts/deploy.sh index dbcf365f..a1509f29 100755 --- a/infra/prod/scripts/deploy.sh +++ b/infra/prod/scripts/deploy.sh @@ -13,8 +13,13 @@ export APP_IMAGE DOMAIN # EC2 홈 디렉토리의 .env를 심링크 ln -sf "$HOME/.env" "$DEPLOY_DIR/.env" +# upstream.conf가 없으면 현재 실행 중인 컨테이너를 감지하여 생성 if [ ! -f ./caddy/upstream.conf ]; then - echo "reverse_proxy weeth-prod-app-blue:8080" > ./caddy/upstream.conf + if docker ps --format '{{.Names}}' | grep -q "weeth-prod-app-green"; then + echo "reverse_proxy weeth-prod-app-green:8080" > ./caddy/upstream.conf + else + echo "reverse_proxy weeth-prod-app-blue:8080" > ./caddy/upstream.conf + fi fi if grep -q "app-blue" ./caddy/upstream.conf; then From 319fd1d77b9922d04a366363fb2693e2ad328ae2 Mon Sep 17 00:00:00 2001 From: hyxklee Date: Mon, 4 May 2026 11:28:50 +0900 Subject: [PATCH 05/15] =?UTF-8?q?refactor:=20=ED=97=AC=EC=8A=A4=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20=EC=8B=A4=ED=8C=A8=EC=8B=9C=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EC=9A=B4=20=EB=B2=84=EC=A0=84=20=EC=A0=95=EB=A6=AC=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/dev/scripts/deploy.sh | 4 +++- infra/prod/scripts/deploy.sh | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/infra/dev/scripts/deploy.sh b/infra/dev/scripts/deploy.sh index 6057338c..b692e30a 100755 --- a/infra/dev/scripts/deploy.sh +++ b/infra/dev/scripts/deploy.sh @@ -44,7 +44,9 @@ for i in {1..20}; do fi if [ "$i" -eq 20 ]; then - echo "[deploy] health check failed" + echo "[deploy] health check failed, stopping new container" + docker compose --profile "$NEW_COLOR" -f docker-compose.yml stop "app-$NEW_COLOR" || true + docker compose --profile "$NEW_COLOR" -f docker-compose.yml rm -f "app-$NEW_COLOR" || true exit 1 fi diff --git a/infra/prod/scripts/deploy.sh b/infra/prod/scripts/deploy.sh index a1509f29..91b62399 100755 --- a/infra/prod/scripts/deploy.sh +++ b/infra/prod/scripts/deploy.sh @@ -44,7 +44,9 @@ for i in {1..20}; do fi if [ "$i" -eq 20 ]; then - echo "[deploy] health check failed" + echo "[deploy] health check failed, stopping new container" + docker compose --profile "$NEW_COLOR" -f docker-compose.yml stop "app-$NEW_COLOR" || true + docker compose --profile "$NEW_COLOR" -f docker-compose.yml rm -f "app-$NEW_COLOR" || true exit 1 fi From 6b6482d59609dfe940e48ba0a390c7b741d7cf42 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Mon, 4 May 2026 14:43:05 +0900 Subject: [PATCH 06/15] =?UTF-8?q?chore:=20=EA=B0=9C=EB=B0=9C=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=88=ED=84=B0=EB=A7=81=20compose=EB=A5=BC=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=A0=84=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=EB=8B=A8?= =?UTF-8?q?=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/dev/monitoring/docker-compose.yml | 74 ------------------------- 1 file changed, 74 deletions(-) diff --git a/infra/dev/monitoring/docker-compose.yml b/infra/dev/monitoring/docker-compose.yml index cfb93f78..e42e6141 100644 --- a/infra/dev/monitoring/docker-compose.yml +++ b/infra/dev/monitoring/docker-compose.yml @@ -14,10 +14,8 @@ services: command: ["run", "--server.http.listen-addr=0.0.0.0:12345", "/etc/alloy/config.alloy"] ports: - "127.0.0.1:12345:12345" - - "127.0.0.1:4318:4318" depends_on: - loki - - tempo networks: - monitoring - weeth-app @@ -37,74 +35,6 @@ services: - monitoring restart: unless-stopped - tempo: - image: grafana/tempo:2.7.1 - env_file: - - ${MONITORING_ENV_FILE:-../.env.monitoring} - volumes: - - ./tempo/tempo-config.yaml:/etc/tempo/tempo-config.yaml:ro - - tempo_data:/var/tempo - command: ["-config.file=/etc/tempo/tempo-config.yaml", "-config.expand-env=true"] - ports: - - "127.0.0.1:3200:3200" - networks: - - monitoring - restart: unless-stopped - - redis-exporter: - image: oliver006/redis_exporter:v1.67.0 - environment: - REDIS_ADDR: redis:6379 - REDIS_PASSWORD: ${REDIS_PASSWORD:-} - networks: - - monitoring - - weeth-app - restart: unless-stopped - - prometheus: - image: prom/prometheus:v2.53.0 - volumes: - - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro - - prometheus_data:/prometheus - ports: - - "127.0.0.1:9090:9090" - networks: - - monitoring - - weeth-app - restart: unless-stopped - - node-exporter: - image: prom/node-exporter:v1.9.0 - pid: host - volumes: - - /proc:/host/proc:ro - - /sys:/host/sys:ro - - /:/rootfs:ro - command: - - "--path.procfs=/host/proc" - - "--path.sysfs=/host/sys" - - "--path.rootfs=/rootfs" - - "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)" - networks: - - monitoring - restart: unless-stopped - - cadvisor: - image: gcr.io/cadvisor/cadvisor:v0.51.0 - privileged: true - devices: - - /dev/kmsg - volumes: - - /:/rootfs:ro - - /var/run:/var/run:ro - - /sys:/sys:ro - - /var/lib/docker:/var/lib/docker:ro - - /dev/disk:/dev/disk:ro - networks: - - monitoring - - weeth-app - restart: unless-stopped - grafana: image: grafana/grafana:11.5.2 env_file: @@ -122,8 +52,6 @@ services: - "127.0.0.1:3000:3000" depends_on: - loki - - prometheus - - tempo networks: - monitoring - weeth-app @@ -139,5 +67,3 @@ networks: volumes: grafana_data: loki_data: - prometheus_data: - tempo_data: From 81bcc951ca08eda49935624494aebb3a9c9ea5dd Mon Sep 17 00:00:00 2001 From: soo0711 Date: Mon, 4 May 2026 14:44:55 +0900 Subject: [PATCH 07/15] =?UTF-8?q?chore:=20=EA=B0=9C=EB=B0=9C=20Alloy=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=EC=97=90=EC=84=9C=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EC=88=98=EC=8B=A0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/dev/monitoring/alloy/config.alloy | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/infra/dev/monitoring/alloy/config.alloy b/infra/dev/monitoring/alloy/config.alloy index c501520d..b0af3995 100644 --- a/infra/dev/monitoring/alloy/config.alloy +++ b/infra/dev/monitoring/alloy/config.alloy @@ -56,21 +56,3 @@ loki.write "default" { url = "http://loki:3100/loki/api/v1/push" } } - -otelcol.receiver.otlp "default" { - http { - endpoint = "0.0.0.0:4318" - } - output { - traces = [otelcol.exporter.otlp.tempo.input] - } -} - -otelcol.exporter.otlp "tempo" { - client { - endpoint = "tempo:4317" - tls { - insecure = true - } - } -} From acca0c22505eb8e9f2d26e6d225602f5012c2ce5 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Mon, 4 May 2026 14:52:29 +0900 Subject: [PATCH 08/15] =?UTF-8?q?chore:=20=EA=B0=9C=EB=B0=9C=20Grafana=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EC=86=8C=EC=8A=A4=EC=97=90=EC=84=9C?= =?UTF-8?q?=20Prometheus=EC=99=80=20Tempo=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../provisioning/datasources/datasources.yaml | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/infra/dev/monitoring/grafana/provisioning/datasources/datasources.yaml b/infra/dev/monitoring/grafana/provisioning/datasources/datasources.yaml index f6118e0a..1bbf4d07 100644 --- a/infra/dev/monitoring/grafana/provisioning/datasources/datasources.yaml +++ b/infra/dev/monitoring/grafana/provisioning/datasources/datasources.yaml @@ -1,31 +1,10 @@ apiVersion: 1 datasources: - - name: Prometheus - type: prometheus - access: proxy - url: http://prometheus:9090 - uid: prometheus - isDefault: true - editable: true - - - name: Tempo - type: tempo - access: proxy - url: http://tempo:3200 - uid: tempo - editable: true - - name: Loki type: loki access: proxy url: http://loki:3100 uid: loki + isDefault: true editable: true - jsonData: - derivedFields: - - datasourceUid: tempo - matcherRegex: '"(?:traceId|trace_id|mdc_traceId|mdc_trace_id)"\s*:\s*"([^"]+)"' - name: traceId - url: "$${__value.raw}" - urlDisplayLabel: "View Trace" From daa4042c935c5557e2cb8d762cfffe61768c1f07 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Mon, 4 May 2026 14:53:50 +0900 Subject: [PATCH 09/15] =?UTF-8?q?chore:=20=EA=B0=9C=EB=B0=9C=20Grafana?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=A1=9C=EA=B7=B8=20=EC=99=B8=20=EB=8C=80?= =?UTF-8?q?=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../provisioning/dashboards/api-overview.json | 388 ------------- .../dashboards/external-infra.json | 373 ------------- .../dashboards/internal-infra.json | 334 ------------ .../dashboards/trace-explorer.json | 513 ------------------ 4 files changed, 1608 deletions(-) delete mode 100644 infra/dev/monitoring/grafana/provisioning/dashboards/api-overview.json delete mode 100644 infra/dev/monitoring/grafana/provisioning/dashboards/external-infra.json delete mode 100644 infra/dev/monitoring/grafana/provisioning/dashboards/internal-infra.json delete mode 100644 infra/dev/monitoring/grafana/provisioning/dashboards/trace-explorer.json diff --git a/infra/dev/monitoring/grafana/provisioning/dashboards/api-overview.json b/infra/dev/monitoring/grafana/provisioning/dashboards/api-overview.json deleted file mode 100644 index f7cf5807..00000000 --- a/infra/dev/monitoring/grafana/provisioning/dashboards/api-overview.json +++ /dev/null @@ -1,388 +0,0 @@ -{ - "uid": "weeth-api-overview", - "title": "API Overview", - "tags": ["weeth", "api"], - "timezone": "Asia/Seoul", - "refresh": "10s", - "time": { "from": "now-1h", "to": "now" }, - "templating": { - "list": [ - { - "name": "uri", - "type": "query", - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "query": "label_values(http_server_requests_seconds_count{uri!~\"/actuator.*|/health-check\"}, uri)", - "includeAll": true, - "multi": true, - "current": { "text": "All", "value": "$__all" }, - "refresh": 2 - } - ] - }, - "panels": [ - { - "id": 1, - "title": "Total Requests / min", - "type": "stat", - "gridPos": { "h": 4, "w": 3, "x": 15, "y": 0 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "sum(rate(http_server_requests_seconds_count{uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[1m])) * 60", - "legendFormat": "req/min" - } - ], - "fieldConfig": { - "defaults": { - "unit": "reqpm", - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 100 }, - { "color": "red", "value": 500 } - ] - } - } - } - }, - { - "id": 2, - "title": "5xx Error Rate", - "type": "stat", - "gridPos": { "h": 4, "w": 3, "x": 0, "y": 0 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "((sum(rate(http_server_requests_seconds_count{status=~\"5..\", uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m])) or vector(0)) / clamp_min(sum(rate(http_server_requests_seconds_count{uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m])), 0.001)) * 100", - "legendFormat": "5xx %" - } - ], - "fieldConfig": { - "defaults": { - "unit": "percent", - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 1 }, - { "color": "red", "value": 5 } - ] - } - } - } - }, - { - "id": 3, - "title": "4xx Error Rate", - "type": "stat", - "gridPos": { "h": 4, "w": 3, "x": 3, "y": 0 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "((sum(rate(http_server_requests_seconds_count{status=~\"4..\", uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m])) or vector(0)) / clamp_min(sum(rate(http_server_requests_seconds_count{uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m])), 0.001)) * 100", - "legendFormat": "4xx %" - } - ], - "fieldConfig": { - "defaults": { - "unit": "percent", - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 5 }, - { "color": "red", "value": 20 } - ] - } - } - } - }, - { - "id": 16, - "title": "Avg Response Time", - "type": "stat", - "gridPos": { "h": 4, "w": 3, "x": 6, "y": 0 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "sum(rate(http_server_requests_seconds_sum{uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m])) / clamp_min(sum(rate(http_server_requests_seconds_count{uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m])), 0.001)", - "legendFormat": "avg" - } - ], - "fieldConfig": { - "defaults": { - "unit": "s", - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 0.3 }, - { "color": "red", "value": 0.5 } - ] - } - } - } - }, - { - "id": 4, - "title": "Apdex (0.5s)", - "type": "stat", - "gridPos": { "h": 4, "w": 6, "x": 18, "y": 0 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "description": "Apdex score (사용자 체감 만족도). 1.0=최고, >0.94=훌륭함, >0.85=좋음, >0.7=나쁨, <0.7=매우 나쁨", - "targets": [ - { - "expr": "(\n sum(rate(http_server_requests_seconds_bucket{le=\"0.5\", uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m]))\n + (\n sum(rate(http_server_requests_seconds_bucket{le=\"2.0\", uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m]))\n - sum(rate(http_server_requests_seconds_bucket{le=\"0.5\", uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m]))\n ) / 2\n) / sum(rate(http_server_requests_seconds_count{uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m]))", - "legendFormat": "Apdex" - } - ], - "fieldConfig": { - "defaults": { - "unit": "short", - "decimals": 2, - "min": 0, - "max": 1, - "thresholds": { - "steps": [ - { "color": "red", "value": null }, - { "color": "orange", "value": 0.7 }, - { "color": "yellow", "value": 0.85 }, - { "color": "green", "value": 0.94 } - ] - } - } - } - }, - { - "id": 5, - "title": "P95 Response Time", - "type": "stat", - "gridPos": { "h": 4, "w": 3, "x": 9, "y": 0 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m])) by (le))", - "legendFormat": "P95" - } - ], - "fieldConfig": { - "defaults": { - "unit": "s", - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 0.5 }, - { "color": "red", "value": 1 } - ] - } - } - } - }, - { - "id": 6, - "title": "P99 Response Time", - "type": "stat", - "gridPos": { "h": 4, "w": 3, "x": 12, "y": 0 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket{uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m])) by (le))", - "legendFormat": "P99" - } - ], - "fieldConfig": { - "defaults": { - "unit": "s", - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 1 }, - { "color": "red", "value": 2 } - ] - } - } - } - }, - { - "id": 7, - "title": "Requests per Second", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "sum(rate(http_server_requests_seconds_count{uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[1m]))", - "legendFormat": "total" - }, - { - "expr": "sum(rate(http_server_requests_seconds_count{status=~\"2..\", uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[1m])) or vector(0)", - "legendFormat": "2xx" - }, - { - "expr": "sum(rate(http_server_requests_seconds_count{status=~\"4..\", uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[1m])) or vector(0)", - "legendFormat": "4xx" - }, - { - "expr": "sum(rate(http_server_requests_seconds_count{status=~\"5..\", uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[1m])) or vector(0)", - "legendFormat": "5xx" - } - ], - "fieldConfig": { - "defaults": { "unit": "reqps", "custom": { "fillOpacity": 10 } } - } - }, - { - "id": 8, - "title": "Response Time Distribution", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "description": "엔드포인트별 P50/P95/P99 응답시간 추이. 기본(All)은 전체 엔드포인트별 분포를 표시하고, 상단 uri 필터에서 특정 엔드포인트를 선택하면 해당 API만 표시됩니다.", - "targets": [ - { - "expr": "histogram_quantile(0.50, sum by (le) (rate(http_server_requests_seconds_bucket{uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m])))", - "legendFormat": "P50" - }, - { - "expr": "histogram_quantile(0.95, sum by (le) (rate(http_server_requests_seconds_bucket{uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m])))", - "legendFormat": "P95" - }, - { - "expr": "histogram_quantile(0.99, sum by (le) (rate(http_server_requests_seconds_bucket{uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m])))", - "legendFormat": "P99" - } - ], - "fieldConfig": { - "defaults": { "unit": "s", "custom": { "fillOpacity": 5 } } - } - }, - { - "id": 9, - "title": "HTTP Status Code Distribution", - "type": "piechart", - "gridPos": { "h": 8, "w": 8, "x": 16, "y": 12 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "sum by(status) (increase(http_server_requests_seconds_count{uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[1h]))", - "legendFormat": "{{status}}" - } - ], - "fieldConfig": { - "overrides": [ - { "matcher": { "id": "byRegexp", "options": "^2\\d{2}$" }, "properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }] }, - { "matcher": { "id": "byRegexp", "options": "^3\\d{2}$" }, "properties": [{ "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } }] }, - { "matcher": { "id": "byRegexp", "options": "^4\\d{2}$" }, "properties": [{ "id": "color", "value": { "fixedColor": "orange", "mode": "fixed" } }] }, - { "matcher": { "id": "byRegexp", "options": "^5\\d{2}$" }, "properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }] } - ] - } - }, - { - "id": 10, - "title": "Slowest Endpoints (P95)", - "type": "bargauge", - "gridPos": { "h": 8, "w": 8, "x": 8, "y": 12 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "topk(10, histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{uri!~\"/actuator.*|/health-check\"}[5m])) by (le, uri)))", - "legendFormat": "{{uri}}" - } - ], - "fieldConfig": { - "defaults": { "unit": "s" } - }, - "options": { - "orientation": "horizontal", - "displayMode": "gradient" - } - }, - { - "id": 11, - "title": "Top 10 Error Endpoints (5xx)", - "type": "bargauge", - "gridPos": { "h": 8, "w": 8, "x": 0, "y": 12 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "topk(10, sum by(uri) (rate(http_server_requests_seconds_count{status=~\"5..\", uri!~\"/actuator.*|/health-check\"}[5m])))", - "legendFormat": "{{uri}}" - } - ], - "fieldConfig": { - "defaults": { "unit": "reqps" } - }, - "options": { - "orientation": "horizontal", - "displayMode": "gradient" - } - }, - { - "id": 12, - "title": "Request Rate by Endpoint", - "type": "timeseries", - "gridPos": { "h": 8, "w": 24, "x": 0, "y": 20 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "sum by(uri, method) (rate(http_server_requests_seconds_count{uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[1m]))", - "legendFormat": "{{method}} {{uri}}" - } - ], - "fieldConfig": { - "defaults": { "unit": "reqps", "custom": { "fillOpacity": 5 } } - } - }, - { - "id": 13, - "title": "Top 10 High-Traffic Endpoints (4xx)", - "type": "bargauge", - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 28 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "topk(10, sum by(uri) (rate(http_server_requests_seconds_count{status=~\"4..\", uri!~\"/actuator.*|/health-check\"}[5m])))", - "legendFormat": "{{uri}}" - } - ], - "fieldConfig": { - "defaults": { "unit": "reqps" } - }, - "options": { - "orientation": "horizontal", - "displayMode": "gradient" - } - }, - { - "id": 14, - "title": "Apdex Over Time (0.5s)", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 28 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "description": "Apdex trend. T=0.5s (satisfied), 4T=2.0s (tolerating). Below 0.85 = degraded experience.", - "targets": [ - { - "expr": "(\n sum(rate(http_server_requests_seconds_bucket{le=\"0.5\", uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m]))\n + (\n sum(rate(http_server_requests_seconds_bucket{le=\"2.0\", uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m]))\n - sum(rate(http_server_requests_seconds_bucket{le=\"0.5\", uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m]))\n ) / 2\n) / sum(rate(http_server_requests_seconds_count{uri!~\"/actuator.*|/health-check\", uri=~\"$uri\"}[5m]))", - "legendFormat": "Apdex" - } - ], - "fieldConfig": { - "defaults": { - "unit": "short", - "decimals": 2, - "min": 0, - "max": 1, - "custom": { "fillOpacity": 10 }, - "thresholds": { - "mode": "absolute", - "steps": [ - { "color": "red", "value": null }, - { "color": "orange", "value": 0.7 }, - { "color": "yellow", "value": 0.85 }, - { "color": "green", "value": 0.94 } - ] - }, - "color": { "mode": "continuous-GrYlRd" } - } - } - } - ], - "schemaVersion": 39 -} diff --git a/infra/dev/monitoring/grafana/provisioning/dashboards/external-infra.json b/infra/dev/monitoring/grafana/provisioning/dashboards/external-infra.json deleted file mode 100644 index f38d9eb7..00000000 --- a/infra/dev/monitoring/grafana/provisioning/dashboards/external-infra.json +++ /dev/null @@ -1,373 +0,0 @@ -{ - "uid": "weeth-external-infra", - "title": "External Infrastructure", - "tags": ["weeth", "db", "redis"], - "timezone": "Asia/Seoul", - "refresh": "10s", - "time": { "from": "now-1h", "to": "now" }, - "panels": [ - { - "id": 1, - "title": "HikariCP (MySQL)", - "type": "row", - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, - "collapsed": false, - "panels": [] - }, - { - "id": 2, - "title": "Active", - "type": "stat", - "gridPos": { "h": 4, "w": 4, "x": 0, "y": 1 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "hikaricp_connections_active", - "legendFormat": "active" - } - ], - "fieldConfig": { - "defaults": { - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 5 }, - { "color": "red", "value": 8 } - ] - } - } - } - }, - { - "id": 3, - "title": "Idle", - "type": "stat", - "gridPos": { "h": 4, "w": 4, "x": 4, "y": 1 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "hikaricp_connections_idle", - "legendFormat": "idle" - } - ], - "fieldConfig": { - "defaults": { - "thresholds": { - "steps": [ - { "color": "red", "value": null }, - { "color": "yellow", "value": 1 }, - { "color": "green", "value": 3 } - ] - } - } - } - }, - { - "id": 4, - "title": "Pending", - "type": "stat", - "gridPos": { "h": 4, "w": 4, "x": 8, "y": 1 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "hikaricp_connections_pending", - "legendFormat": "pending" - } - ], - "fieldConfig": { - "defaults": { - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 1 }, - { "color": "red", "value": 3 } - ] - } - } - } - }, - { - "id": 5, - "title": "Total / Max", - "type": "stat", - "gridPos": { "h": 4, "w": 4, "x": 12, "y": 1 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "hikaricp_connections", - "legendFormat": "total" - }, - { - "expr": "hikaricp_connections_max", - "legendFormat": "max" - } - ] - }, - { - "id": 6, - "title": "Timeout Total", - "type": "stat", - "gridPos": { "h": 4, "w": 4, "x": 16, "y": 1 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "description": "커넥션 획득 timeout 누적 횟수. 0이 아니면 커넥션 풀 고갈 발생.", - "targets": [ - { - "expr": "hikaricp_connections_timeout_total", - "legendFormat": "timeouts" - } - ], - "fieldConfig": { - "defaults": { - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "red", "value": 1 } - ] - } - } - } - }, - { - "id": 7, - "title": "Timeout Rate / min", - "type": "stat", - "gridPos": { "h": 4, "w": 4, "x": 20, "y": 1 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "rate(hikaricp_connections_timeout_total[1m]) * 60", - "legendFormat": "timeouts/min" - } - ], - "fieldConfig": { - "defaults": { - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "red", "value": 0.1 } - ] - } - } - } - }, - { - "id": 8, - "title": "Connection Pool Over Time", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 5 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "hikaricp_connections_active", - "legendFormat": "active" - }, - { - "expr": "hikaricp_connections_idle", - "legendFormat": "idle" - }, - { - "expr": "hikaricp_connections_pending", - "legendFormat": "pending" - }, - { - "expr": "hikaricp_connections", - "legendFormat": "total" - } - ], - "fieldConfig": { - "defaults": { "unit": "short", "custom": { "fillOpacity": 10 } } - } - }, - { - "id": 9, - "title": "Connection Acquire Time", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 5 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "rate(hikaricp_connections_acquire_seconds_sum[1m]) / rate(hikaricp_connections_acquire_seconds_count[1m])", - "legendFormat": "avg acquire time" - }, - { - "expr": "hikaricp_connections_acquire_seconds_max", - "legendFormat": "max acquire time" - } - ], - "fieldConfig": { - "defaults": { "unit": "s", "custom": { "fillOpacity": 10 } } - } - }, - { - "id": 10, - "title": "Connection Creation Time", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 30 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "rate(hikaricp_connections_creation_seconds_sum[1m]) / rate(hikaricp_connections_creation_seconds_count[1m])", - "legendFormat": "avg creation time" - }, - { - "expr": "hikaricp_connections_creation_seconds_max", - "legendFormat": "max creation time" - } - ], - "fieldConfig": { - "defaults": { "unit": "s", "custom": { "fillOpacity": 10 } } - } - }, - { - "id": 11, - "title": "Connection Usage Time", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 30 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "rate(hikaricp_connections_usage_seconds_sum[1m]) / rate(hikaricp_connections_usage_seconds_count[1m])", - "legendFormat": "avg usage time" - }, - { - "expr": "hikaricp_connections_usage_seconds_max", - "legendFormat": "max usage time" - } - ], - "fieldConfig": { - "defaults": { "unit": "s", "custom": { "fillOpacity": 10 } } - } - }, - { - "id": 12, - "title": "Redis", - "type": "row", - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 13 }, - "collapsed": false, - "panels": [] - }, - { - "id": 13, - "title": "Redis Command Rate", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 22 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "sum(rate(lettuce_command_completion_seconds_count[1m]))", - "legendFormat": "commands/sec" - } - ], - "fieldConfig": { - "defaults": { "unit": "ops", "custom": { "fillOpacity": 10 } } - } - }, - { - "id": 14, - "title": "Redis Command Latency", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 14 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "rate(lettuce_command_completion_seconds_sum[1m]) / rate(lettuce_command_completion_seconds_count[1m])", - "legendFormat": "avg latency" - }, - { - "expr": "lettuce_command_completion_seconds_max", - "legendFormat": "max latency" - } - ], - "fieldConfig": { - "defaults": { "unit": "s", "custom": { "fillOpacity": 10 } } - } - }, - { - "id": 15, - "title": "Redis Command by Type", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 22 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "sum by(command) (rate(lettuce_command_completion_seconds_count[1m]))", - "legendFormat": "{{command}}" - } - ], - "fieldConfig": { - "defaults": { "unit": "ops", "custom": { "fillOpacity": 5 } } - } - }, - { - "id": 16, - "title": "Redis Command Errors", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 14 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "sum(rate(lettuce_command_firstresponse_seconds_count{outcome=\"ERROR\"}[1m]))", - "legendFormat": "errors/sec" - } - ], - "fieldConfig": { - "defaults": { - "unit": "ops", - "custom": { "fillOpacity": 10 }, - "color": { "fixedColor": "red", "mode": "fixed" } - } - } - }, - { - "id": 17, - "title": "Redis Cache Hit Rate", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 30 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "description": "Redis 서버 전체 캐시 히트율. redis_exporter의 keyspace_hits/misses 기반.", - "targets": [ - { - "expr": "rate(redis_keyspace_hits_total[5m]) / clamp_min(rate(redis_keyspace_hits_total[5m]) + rate(redis_keyspace_misses_total[5m]), 0.001) * 100", - "legendFormat": "hit rate" - } - ], - "fieldConfig": { - "defaults": { - "unit": "percent", - "min": 0, - "max": 100, - "custom": { "fillOpacity": 10 }, - "thresholds": { - "mode": "absolute", - "steps": [ - { "color": "red", "value": null }, - { "color": "yellow", "value": 50 }, - { "color": "green", "value": 80 } - ] - } - } - } - }, - { - "id": 18, - "title": "Redis Cache Hits / Misses", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 30 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "rate(redis_keyspace_hits_total[1m])", - "legendFormat": "hits/sec" - }, - { - "expr": "rate(redis_keyspace_misses_total[1m])", - "legendFormat": "misses/sec" - } - ], - "fieldConfig": { - "defaults": { "unit": "ops", "custom": { "fillOpacity": 10 } } - } - } - ], - "schemaVersion": 39 -} diff --git a/infra/dev/monitoring/grafana/provisioning/dashboards/internal-infra.json b/infra/dev/monitoring/grafana/provisioning/dashboards/internal-infra.json deleted file mode 100644 index 50ed2b9e..00000000 --- a/infra/dev/monitoring/grafana/provisioning/dashboards/internal-infra.json +++ /dev/null @@ -1,334 +0,0 @@ -{ - "uid": "weeth-internal-infra", - "title": "Internal Infrastructure", - "tags": ["weeth", "jvm", "infra"], - "timezone": "Asia/Seoul", - "refresh": "10s", - "time": { "from": "now-1h", "to": "now" }, - "panels": [ - { - "id": 1, - "title": "Uptime", - "type": "stat", - "gridPos": { "h": 3, "w": 6, "x": 0, "y": 0 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "process_uptime_seconds", - "legendFormat": "uptime" - } - ], - "fieldConfig": { - "defaults": { "unit": "s" } - } - }, - { - "id": 2, - "title": "Heap Usage %", - "type": "stat", - "gridPos": { "h": 3, "w": 6, "x": 6, "y": 0 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "sum(jvm_memory_used_bytes{area=\"heap\"}) / sum(jvm_memory_max_bytes{area=\"heap\"}) * 100", - "legendFormat": "heap %" - } - ], - "fieldConfig": { - "defaults": { - "unit": "percent", - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 70 }, - { "color": "red", "value": 90 } - ] - } - } - } - }, - { - "id": 3, - "title": "Live Threads", - "type": "stat", - "gridPos": { "h": 3, "w": 6, "x": 12, "y": 0 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "jvm_threads_live_threads", - "legendFormat": "live" - } - ] - }, - { - "id": 4, - "title": "App CPU", - "type": "stat", - "gridPos": { "h": 3, "w": 6, "x": 18, "y": 0 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "process_cpu_usage * 100", - "legendFormat": "CPU %" - } - ], - "fieldConfig": { - "defaults": { - "unit": "percent", - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 60 }, - { "color": "red", "value": 85 } - ] - } - } - } - }, - { - "id": 5, - "title": "JVM", - "type": "row", - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 12 }, - "collapsed": false, - "panels": [] - }, - { - "id": 6, - "title": "JVM Heap Used", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 13 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "jvm_memory_used_bytes{area=\"heap\"}", - "legendFormat": "{{id}} used" - }, - { - "expr": "jvm_memory_committed_bytes{area=\"heap\"}", - "legendFormat": "{{id}} committed" - } - ], - "fieldConfig": { - "defaults": { "unit": "bytes", "custom": { "fillOpacity": 10 } } - } - }, - { - "id": 7, - "title": "JVM Heap Summary", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 13 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "sum(jvm_memory_used_bytes{area=\"heap\"})", - "legendFormat": "used" - }, - { - "expr": "sum(jvm_memory_committed_bytes{area=\"heap\"})", - "legendFormat": "committed" - }, - { - "expr": "sum(jvm_memory_max_bytes{area=\"heap\"})", - "legendFormat": "max" - } - ], - "fieldConfig": { - "defaults": { "unit": "bytes", "custom": { "fillOpacity": 10 } } - } - }, - { - "id": 8, - "title": "JVM Non-Heap Used", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 29 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "jvm_memory_used_bytes{area=\"nonheap\"}", - "legendFormat": "{{id}} used" - } - ], - "fieldConfig": { - "defaults": { "unit": "bytes", "custom": { "fillOpacity": 10 } } - } - }, - { - "id": 9, - "title": "GC Pause Time", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 21 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "rate(jvm_gc_pause_seconds_sum[1m])", - "legendFormat": "{{action}} {{cause}}" - } - ], - "fieldConfig": { - "defaults": { "unit": "s", "custom": { "fillOpacity": 10 } } - } - }, - { - "id": 10, - "title": "GC Count / min", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 21 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "rate(jvm_gc_pause_seconds_count[1m]) * 60", - "legendFormat": "{{action}} {{cause}}" - } - ], - "fieldConfig": { - "defaults": { "unit": "short", "custom": { "fillOpacity": 10 } } - } - }, - { - "id": 11, - "title": "Thread Count Over Time", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 29 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "jvm_threads_live_threads", - "legendFormat": "live" - }, - { - "expr": "jvm_threads_daemon_threads", - "legendFormat": "daemon" - }, - { - "expr": "jvm_threads_peak_threads", - "legendFormat": "peak" - } - ], - "fieldConfig": { - "defaults": { "unit": "short" } - } - }, - { - "id": 12, - "title": "Tomcat & System", - "type": "row", - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 3 }, - "collapsed": false, - "panels": [] - }, - { - "id": 13, - "title": "Tomcat Threads", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "tomcat_threads_current_threads", - "legendFormat": "current" - }, - { - "expr": "tomcat_threads_busy_threads", - "legendFormat": "busy" - }, - { - "expr": "tomcat_threads_config_max_threads", - "legendFormat": "max" - } - ], - "fieldConfig": { - "defaults": { "unit": "short" } - } - }, - { - "id": 14, - "title": "CPU Usage", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "process_cpu_usage", - "legendFormat": "app CPU" - }, - { - "expr": "system_cpu_usage", - "legendFormat": "system CPU" - } - ], - "fieldConfig": { - "defaults": { "unit": "percentunit", "max": 1, "custom": { "fillOpacity": 10 } } - } - }, - { - "id": 15, - "title": "Host & Docker", - "type": "row", - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 37 }, - "collapsed": false, - "panels": [] - }, - { - "id": 16, - "title": "Host Memory Used / Total", - "type": "stat", - "gridPos": { "h": 4, "w": 12, "x": 0, "y": 38 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes", - "legendFormat": "used" - }, - { - "expr": "node_memory_MemTotal_bytes", - "legendFormat": "total" - } - ], - "fieldConfig": { - "defaults": { "unit": "bytes" } - }, - "options": { - "reduceOptions": { - "values": false, - "calcs": ["lastNotNull"], - "fields": "" - }, - "orientation": "auto", - "textMode": "value_and_name", - "wideLayout": true - } - }, - { - "id": 17, - "title": "Swap Used / Total", - "type": "stat", - "gridPos": { "h": 4, "w": 12, "x": 12, "y": 38 }, - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "targets": [ - { - "expr": "node_memory_SwapTotal_bytes - node_memory_SwapFree_bytes", - "legendFormat": "used" - }, - { - "expr": "node_memory_SwapTotal_bytes", - "legendFormat": "total" - } - ], - "fieldConfig": { - "defaults": { "unit": "bytes" } - }, - "options": { - "reduceOptions": { - "values": false, - "calcs": ["lastNotNull"], - "fields": "" - }, - "orientation": "auto", - "textMode": "value_and_name", - "wideLayout": true - } - } - ], - "schemaVersion": 39 -} diff --git a/infra/dev/monitoring/grafana/provisioning/dashboards/trace-explorer.json b/infra/dev/monitoring/grafana/provisioning/dashboards/trace-explorer.json deleted file mode 100644 index 8be83176..00000000 --- a/infra/dev/monitoring/grafana/provisioning/dashboards/trace-explorer.json +++ /dev/null @@ -1,513 +0,0 @@ -{ - "uid": "weeth-trace-explorer", - "title": "Trace Explorer", - "tags": [ - "weeth", - "trace" - ], - "timezone": "Asia/Seoul", - "refresh": "10s", - "time": { - "from": "now-6h", - "to": "now" - }, - "templating": { - "list": [ - { - "name": "traceId", - "type": "textbox", - "current": { - "text": "", - "value": "" - }, - "label": "Trace ID" - } - ] - }, - "panels": [ - { - "id": 1, - "title": "All Traces", - "description": "최근 전체 트레이스 목록입니다. actuator/health-check는 애플리케이션 observation 필터에서 제외합니다.", - "type": "table", - "gridPos": { - "h": 10, - "w": 24, - "x": 0, - "y": 0 - }, - "datasource": { - "type": "tempo", - "uid": "tempo" - }, - "targets": [ - { - "queryType": "traceql", - "query": "{}", - "limit": 200, - "tableType": "traces", - "refId": "A" - } - ], - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - }, - "indexByName": { - "Service": 0, - "TraceId": 1, - "Name": 2, - "StartTime": 3, - "Duration": 4, - "Service Name": 0, - "Trace ID": 1, - "Start time": 3, - "duration": 4, - "rootServiceName": 0, - "traceID": 1, - "rootTraceName": 2, - "startTime": 3 - }, - "renameByName": { - "Trace ID": "TraceId", - "TraceID": "TraceId", - "traceID": "TraceId", - "traceId": "TraceId", - "Service Name": "Service", - "Root Service Name": "Service", - "rootServiceName": "Service", - "serviceName": "Service", - "Root Trace Name": "Name", - "Trace Name": "Name", - "rootTraceName": "Name", - "name": "Name", - "Start time": "StartTime", - "Start Time": "StartTime", - "startTime": "StartTime", - "duration": "Duration", - "Duration": "Duration" - } - } - } - ], - "fieldConfig": { - "defaults": { - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "TraceId" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "Open in Explore", - "url": "/explore?schemaVersion=1&panes=%7B%22trace%22:%7B%22datasource%22:%22tempo%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22datasource%22:%7B%22type%22:%22tempo%22,%22uid%22:%22tempo%22%7D,%22queryType%22:%22traceId%22,%22query%22:%22${__value.raw}%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1", - "targetBlank": false - } - ] - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Trace ID" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "Open in Explore", - "url": "/explore?schemaVersion=1&panes=%7B%22trace%22:%7B%22datasource%22:%22tempo%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22datasource%22:%7B%22type%22:%22tempo%22,%22uid%22:%22tempo%22%7D,%22queryType%22:%22traceId%22,%22query%22:%22${__value.raw}%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1", - "targetBlank": false - } - ] - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "traceID" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "Open in Explore", - "url": "/explore?schemaVersion=1&panes=%7B%22trace%22:%7B%22datasource%22:%22tempo%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22datasource%22:%7B%22type%22:%22tempo%22,%22uid%22:%22tempo%22%7D,%22queryType%22:%22traceId%22,%22query%22:%22${__value.raw}%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1", - "targetBlank": false - } - ] - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "traceId" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "Open in Explore", - "url": "/explore?schemaVersion=1&panes=%7B%22trace%22:%7B%22datasource%22:%22tempo%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22datasource%22:%7B%22type%22:%22tempo%22,%22uid%22:%22tempo%22%7D,%22queryType%22:%22traceId%22,%22query%22:%22${__value.raw}%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1", - "targetBlank": false - } - ] - } - ] - } - ] - } - }, - { - "id": 6, - "title": "Trace Search", - "description": "Tempo Explore에서 TraceQL로 검색합니다.", - "type": "text", - "gridPos": { - "h": 4, - "w": 24, - "x": 0, - "y": 10 - }, - "options": { - "mode": "markdown", - "content": "TraceId를 선택하면 Tempo Explore에서 해당 TraceId를 바로 조회합니다.\n\n기본 조회 범위는 최근 6시간, 목록 제한은 200건입니다." - } - }, - { - "id": 4, - "title": "Error Traces", - "description": "에러가 발생한 트레이스를 검색합니다.", - "type": "table", - "gridPos": { - "h": 10, - "w": 24, - "x": 0, - "y": 14 - }, - "datasource": { - "type": "tempo", - "uid": "tempo" - }, - "targets": [ - { - "queryType": "traceql", - "query": "{ status = error }", - "limit": 200, - "tableType": "traces", - "refId": "A" - } - ], - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - }, - "indexByName": { - "Service": 0, - "TraceId": 1, - "Name": 2, - "StartTime": 3, - "Duration": 4, - "Service Name": 0, - "Trace ID": 1, - "Start time": 3, - "duration": 4, - "rootServiceName": 0, - "traceID": 1, - "rootTraceName": 2, - "startTime": 3 - }, - "renameByName": { - "Trace ID": "TraceId", - "TraceID": "TraceId", - "traceID": "TraceId", - "traceId": "TraceId", - "Service Name": "Service", - "Root Service Name": "Service", - "rootServiceName": "Service", - "serviceName": "Service", - "Root Trace Name": "Name", - "Trace Name": "Name", - "rootTraceName": "Name", - "name": "Name", - "Start time": "StartTime", - "Start Time": "StartTime", - "startTime": "StartTime", - "duration": "Duration", - "Duration": "Duration" - } - } - } - ], - "fieldConfig": { - "defaults": { - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "TraceId" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "Open in Explore", - "url": "/explore?schemaVersion=1&panes=%7B%22trace%22:%7B%22datasource%22:%22tempo%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22datasource%22:%7B%22type%22:%22tempo%22,%22uid%22:%22tempo%22%7D,%22queryType%22:%22traceId%22,%22query%22:%22${__value.raw}%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1", - "targetBlank": false - } - ] - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Trace ID" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "Open in Explore", - "url": "/explore?schemaVersion=1&panes=%7B%22trace%22:%7B%22datasource%22:%22tempo%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22datasource%22:%7B%22type%22:%22tempo%22,%22uid%22:%22tempo%22%7D,%22queryType%22:%22traceId%22,%22query%22:%22${__value.raw}%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1", - "targetBlank": false - } - ] - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "traceID" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "Open in Explore", - "url": "/explore?schemaVersion=1&panes=%7B%22trace%22:%7B%22datasource%22:%22tempo%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22datasource%22:%7B%22type%22:%22tempo%22,%22uid%22:%22tempo%22%7D,%22queryType%22:%22traceId%22,%22query%22:%22${__value.raw}%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1", - "targetBlank": false - } - ] - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "traceId" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "Open in Explore", - "url": "/explore?schemaVersion=1&panes=%7B%22trace%22:%7B%22datasource%22:%22tempo%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22datasource%22:%7B%22type%22:%22tempo%22,%22uid%22:%22tempo%22%7D,%22queryType%22:%22traceId%22,%22query%22:%22${__value.raw}%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1", - "targetBlank": false - } - ] - } - ] - } - ] - } - }, - { - "id": 3, - "title": "Slow Traces (> 500ms)", - "description": "500ms 이상 걸린 요청의 트레이스를 자동 검색합니다.", - "type": "table", - "gridPos": { - "h": 10, - "w": 24, - "x": 0, - "y": 24 - }, - "datasource": { - "type": "tempo", - "uid": "tempo" - }, - "targets": [ - { - "queryType": "traceql", - "query": "{ duration > 500ms }", - "limit": 200, - "tableType": "traces", - "refId": "A" - } - ], - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - }, - "indexByName": { - "Service": 0, - "TraceId": 1, - "Name": 2, - "StartTime": 3, - "Duration": 4, - "Service Name": 0, - "Trace ID": 1, - "Start time": 3, - "duration": 4, - "rootServiceName": 0, - "traceID": 1, - "rootTraceName": 2, - "startTime": 3 - }, - "renameByName": { - "Trace ID": "TraceId", - "TraceID": "TraceId", - "traceID": "TraceId", - "traceId": "TraceId", - "Service Name": "Service", - "Root Service Name": "Service", - "rootServiceName": "Service", - "serviceName": "Service", - "Root Trace Name": "Name", - "Trace Name": "Name", - "rootTraceName": "Name", - "name": "Name", - "Start time": "StartTime", - "Start Time": "StartTime", - "startTime": "StartTime", - "duration": "Duration", - "Duration": "Duration" - } - } - } - ], - "fieldConfig": { - "defaults": { - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "TraceId" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "Open in Explore", - "url": "/explore?schemaVersion=1&panes=%7B%22trace%22:%7B%22datasource%22:%22tempo%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22datasource%22:%7B%22type%22:%22tempo%22,%22uid%22:%22tempo%22%7D,%22queryType%22:%22traceId%22,%22query%22:%22${__value.raw}%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1", - "targetBlank": false - } - ] - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Trace ID" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "Open in Explore", - "url": "/explore?schemaVersion=1&panes=%7B%22trace%22:%7B%22datasource%22:%22tempo%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22datasource%22:%7B%22type%22:%22tempo%22,%22uid%22:%22tempo%22%7D,%22queryType%22:%22traceId%22,%22query%22:%22${__value.raw}%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1", - "targetBlank": false - } - ] - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "traceID" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "Open in Explore", - "url": "/explore?schemaVersion=1&panes=%7B%22trace%22:%7B%22datasource%22:%22tempo%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22datasource%22:%7B%22type%22:%22tempo%22,%22uid%22:%22tempo%22%7D,%22queryType%22:%22traceId%22,%22query%22:%22${__value.raw}%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1", - "targetBlank": false - } - ] - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "traceId" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "Open in Explore", - "url": "/explore?schemaVersion=1&panes=%7B%22trace%22:%7B%22datasource%22:%22tempo%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22datasource%22:%7B%22type%22:%22tempo%22,%22uid%22:%22tempo%22%7D,%22queryType%22:%22traceId%22,%22query%22:%22${__value.raw}%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1", - "targetBlank": false - } - ] - } - ] - } - ] - } - }, - { - "id": 5, - "title": "Related Logs (by Trace ID)", - "description": "Trace ID를 입력하면 해당 트레이스와 연관된 로그를 확인합니다.", - "type": "logs", - "gridPos": { - "h": 10, - "w": 24, - "x": 0, - "y": 34 - }, - "datasource": { - "type": "loki", - "uid": "loki" - }, - "targets": [ - { - "expr": "{app=\"weeth\"} |= `$traceId` | json", - "refId": "A" - } - ], - "options": { - "showTime": true, - "showLabels": true, - "wrapLogMessage": true, - "enableLogDetails": true, - "sortOrder": "Descending" - } - } - ], - "schemaVersion": 39 -} From 4b452be85f8ae5dbc20be3ab3f4c46fb8327b683 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Mon, 4 May 2026 14:55:02 +0900 Subject: [PATCH 10/15] =?UTF-8?q?chore:=20=EA=B0=9C=EB=B0=9C=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=88=ED=84=B0=EB=A7=81=20=EB=B0=B0=ED=8F=AC=20=ED=97=AC?= =?UTF-8?q?=EC=8A=A4=EC=B2=B4=ED=81=AC=20=EB=8C=80=EC=83=81=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/dev/monitoring/scripts/deploy.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/infra/dev/monitoring/scripts/deploy.sh b/infra/dev/monitoring/scripts/deploy.sh index 9a273d6a..fbdcd2f1 100644 --- a/infra/dev/monitoring/scripts/deploy.sh +++ b/infra/dev/monitoring/scripts/deploy.sh @@ -40,9 +40,7 @@ docker compose --env-file "$MONITORING_ENV_FILE" up -d echo "[monitoring] waiting for services to be healthy..." for i in {1..30}; do if curl -fsS "http://127.0.0.1:12345/-/ready" >/dev/null 2>&1 && - curl -fsS "http://127.0.0.1:9090/-/ready" >/dev/null 2>&1 && curl -fsS "http://127.0.0.1:3100/ready" >/dev/null 2>&1 && - curl -fsS "http://127.0.0.1:3200/ready" >/dev/null 2>&1 && curl -fsS "http://127.0.0.1:3000/api/health" >/dev/null 2>&1; then echo "[monitoring] all services healthy" break From ed82d64879d621ce5405f43b8f7a71008f8d01bb Mon Sep 17 00:00:00 2001 From: soo0711 Date: Mon, 4 May 2026 14:57:54 +0900 Subject: [PATCH 11/15] =?UTF-8?q?chore:=20=EA=B0=9C=EB=B0=9C=20=EC=95=B1?= =?UTF-8?q?=20=EA=B8=B0=EB=B3=B8=20=ED=8A=B8=EB=A0=88=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/dev/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/dev/docker-compose.yml b/infra/dev/docker-compose.yml index 84d3d5ff..2b111fc2 100644 --- a/infra/dev/docker-compose.yml +++ b/infra/dev/docker-compose.yml @@ -59,7 +59,7 @@ services: OTEL_JAVAAGENT_ENABLED: ${OTEL_JAVAAGENT_ENABLED:-true} OTEL_SERVICE_NAME: ${OTEL_SERVICE_NAME:-weeth-server} OTEL_RESOURCE_ATTRIBUTES: ${OTEL_RESOURCE_ATTRIBUTES:-deployment.environment=dev} - OTEL_TRACES_EXPORTER: ${OTEL_TRACES_EXPORTER:-otlp} + OTEL_TRACES_EXPORTER: ${OTEL_TRACES_EXPORTER:-none} OTEL_TRACES_SAMPLER: ${OTEL_TRACES_SAMPLER:-parentbased_traceidratio} OTEL_TRACES_SAMPLER_ARG: ${OTEL_TRACES_SAMPLER_ARG:-0.1} OTEL_BSP_SCHEDULE_DELAY: ${OTEL_BSP_SCHEDULE_DELAY:-15000} @@ -98,7 +98,7 @@ services: OTEL_JAVAAGENT_ENABLED: ${OTEL_JAVAAGENT_ENABLED:-true} OTEL_SERVICE_NAME: ${OTEL_SERVICE_NAME:-weeth-server} OTEL_RESOURCE_ATTRIBUTES: ${OTEL_RESOURCE_ATTRIBUTES:-deployment.environment=dev} - OTEL_TRACES_EXPORTER: ${OTEL_TRACES_EXPORTER:-otlp} + OTEL_TRACES_EXPORTER: ${OTEL_TRACES_EXPORTER:-none} OTEL_TRACES_SAMPLER: ${OTEL_TRACES_SAMPLER:-parentbased_traceidratio} OTEL_TRACES_SAMPLER_ARG: ${OTEL_TRACES_SAMPLER_ARG:-0.1} OTEL_BSP_SCHEDULE_DELAY: ${OTEL_BSP_SCHEDULE_DELAY:-15000} From ea4cc0de6948ecb6ebcdf049643c78a6ac4916e1 Mon Sep 17 00:00:00 2001 From: soo0711 Date: Mon, 4 May 2026 15:07:22 +0900 Subject: [PATCH 12/15] =?UTF-8?q?chore:=20=EA=B0=9C=EB=B0=9C=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=88=ED=84=B0=EB=A7=81=EC=97=90=EC=84=9C=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20Prometheus=EC=99=80=20Tempo=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/monitoring/prometheus/prometheus.yml | 48 ------------------- infra/dev/monitoring/tempo/tempo-config.yaml | 38 --------------- 2 files changed, 86 deletions(-) delete mode 100644 infra/dev/monitoring/prometheus/prometheus.yml delete mode 100644 infra/dev/monitoring/tempo/tempo-config.yaml diff --git a/infra/dev/monitoring/prometheus/prometheus.yml b/infra/dev/monitoring/prometheus/prometheus.yml deleted file mode 100644 index f2424afa..00000000 --- a/infra/dev/monitoring/prometheus/prometheus.yml +++ /dev/null @@ -1,48 +0,0 @@ -global: - scrape_interval: 30s - evaluation_interval: 30s - -scrape_configs: - - job_name: "weeth-app" - metrics_path: "/actuator/prometheus" - static_configs: - - targets: ["app-blue:8080", "app-green:8080"] - labels: - app: weeth - env: dev - - - job_name: "node-exporter" - static_configs: - - targets: ["node-exporter:9100"] - labels: - env: dev - - - job_name: "cadvisor" - static_configs: - - targets: ["cadvisor:8080"] - labels: - env: dev - - - job_name: "prometheus" - static_configs: - - targets: ["prometheus:9090"] - labels: - env: dev - - - job_name: "loki" - static_configs: - - targets: ["loki:3100"] - labels: - env: dev - - - job_name: "tempo" - static_configs: - - targets: ["tempo:3200"] - labels: - env: dev - - - job_name: "redis" - static_configs: - - targets: ["redis-exporter:9121"] - labels: - env: dev diff --git a/infra/dev/monitoring/tempo/tempo-config.yaml b/infra/dev/monitoring/tempo/tempo-config.yaml deleted file mode 100644 index ce62c027..00000000 --- a/infra/dev/monitoring/tempo/tempo-config.yaml +++ /dev/null @@ -1,38 +0,0 @@ -stream_over_http_enabled: true - -server: - http_listen_port: 3200 - -distributor: - max_attribute_bytes: 1024 - receivers: - otlp: - protocols: - grpc: - endpoint: "0.0.0.0:4317" - -query_frontend: - max_query_expression_size_bytes: 32768 - search: - default_spans_per_span_set: 1 - max_spans_per_span_set: 20 - -storage: - trace: - backend: s3 - s3: - bucket: ${TEMPO_S3_BUCKET} - endpoint: s3.${AWS_REGION}.amazonaws.com - wal: - path: /var/tempo/wal - local: - path: /var/tempo/blocks - -compactor: - compaction: - block_retention: 168h - -overrides: - defaults: - global: - max_bytes_per_trace: 3000000 From a0f446e53bc102d671d90405cd31a8e2a35f142e Mon Sep 17 00:00:00 2001 From: soo0711 Date: Mon, 4 May 2026 15:18:53 +0900 Subject: [PATCH 13/15] =?UTF-8?q?chore:=20=EA=B0=9C=EB=B0=9C=20=EC=95=B1?= =?UTF-8?q?=20compose=EC=97=90=EC=84=9C=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20OT?= =?UTF-8?q?LP=20=EC=84=A4=EC=A0=95=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/dev/docker-compose.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/infra/dev/docker-compose.yml b/infra/dev/docker-compose.yml index 2b111fc2..4b515b82 100644 --- a/infra/dev/docker-compose.yml +++ b/infra/dev/docker-compose.yml @@ -65,9 +65,6 @@ services: OTEL_BSP_SCHEDULE_DELAY: ${OTEL_BSP_SCHEDULE_DELAY:-15000} OTEL_METRICS_EXPORTER: ${OTEL_METRICS_EXPORTER:-none} OTEL_LOGS_EXPORTER: ${OTEL_LOGS_EXPORTER:-none} - OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://alloy:4318} - OTEL_EXPORTER_OTLP_PROTOCOL: ${OTEL_EXPORTER_OTLP_PROTOCOL:-http/protobuf} - OTEL_EXPORTER_OTLP_COMPRESSION: ${OTEL_EXPORTER_OTLP_COMPRESSION:-gzip} volumes: - ${HOME}/keys:/app/keys:ro ports: @@ -104,9 +101,6 @@ services: OTEL_BSP_SCHEDULE_DELAY: ${OTEL_BSP_SCHEDULE_DELAY:-15000} OTEL_METRICS_EXPORTER: ${OTEL_METRICS_EXPORTER:-none} OTEL_LOGS_EXPORTER: ${OTEL_LOGS_EXPORTER:-none} - OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://alloy:4318} - OTEL_EXPORTER_OTLP_PROTOCOL: ${OTEL_EXPORTER_OTLP_PROTOCOL:-http/protobuf} - OTEL_EXPORTER_OTLP_COMPRESSION: ${OTEL_EXPORTER_OTLP_COMPRESSION:-gzip} volumes: - ${HOME}/keys:/app/keys:ro ports: From b2c45c052fa7ee5b7a2410d0a9969a3662c43227 Mon Sep 17 00:00:00 2001 From: hyxklee Date: Wed, 6 May 2026 11:35:10 +0900 Subject: [PATCH 14/15] =?UTF-8?q?fix:=20=EB=A7=A4=20=EB=B0=B0=ED=8F=AC?= =?UTF-8?q?=EC=8B=9C=20Caddy=EA=B0=80=20Reload=20=EB=90=98=EA=B1=B0?= =?UTF-8?q?=EB=82=98=20Restart=20=EB=90=98=EB=8F=84=EB=A1=9D=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/dev/scripts/deploy.sh | 7 ++++++- infra/prod/scripts/deploy.sh | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/infra/dev/scripts/deploy.sh b/infra/dev/scripts/deploy.sh index b692e30a..de4e3faf 100755 --- a/infra/dev/scripts/deploy.sh +++ b/infra/dev/scripts/deploy.sh @@ -62,7 +62,12 @@ if [ "$CURRENT_DOMAIN" != "$DOMAIN" ]; then echo "[deploy] domain changed, recreating caddy" docker compose up -d --force-recreate caddy elif docker compose ps caddy --format '{{.State}}' 2>/dev/null | grep -q running; then - docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile + if docker compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile; then + echo "[deploy] caddy reloaded" + else + echo "[deploy] caddy reload failed, restarting caddy" + docker compose restart caddy + fi else docker compose up -d caddy fi diff --git a/infra/prod/scripts/deploy.sh b/infra/prod/scripts/deploy.sh index 91b62399..f24da940 100755 --- a/infra/prod/scripts/deploy.sh +++ b/infra/prod/scripts/deploy.sh @@ -62,7 +62,12 @@ if [ "$CURRENT_DOMAIN" != "$DOMAIN" ]; then echo "[deploy] domain changed, recreating caddy" docker compose up -d --force-recreate caddy elif docker compose ps caddy --format '{{.State}}' 2>/dev/null | grep -q running; then - docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile + if docker compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile; then + echo "[deploy] caddy reloaded" + else + echo "[deploy] caddy reload failed, restarting caddy" + docker compose restart caddy + fi else docker compose up -d caddy fi From 90280ee24a69973c7e6ae78c7d4b86ebbeaf506a Mon Sep 17 00:00:00 2001 From: Jeon Soo Hyeon <128474444+soo0711@users.noreply.github.com> Date: Fri, 8 May 2026 14:53:16 +0900 Subject: [PATCH 15/15] =?UTF-8?q?[WTH-376]=20=EA=B3=B5=EC=A7=80=20?= =?UTF-8?q?=EC=9D=BD=EC=9D=80=20=EC=82=AC=EB=9E=8C=20club=20member?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95=20(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 공지 읽음 상태를 클럽 멤버 기준으로 변경 * test: 관련 테스트 수정 * style: 린트 적용 * refactor: 공지 읽음 처리를 upsert로 변경 * test: upsert 쿼리 통합 테스트 추가 --- .../usecase/command/MarkNoticeReadUseCase.kt | 20 +++----- .../board/domain/entity/LastNoticeRead.kt | 14 +++--- .../domain/repository/LastNoticeReadReader.kt | 4 +- .../repository/LastNoticeReadRepository.kt | 23 ++++++++- .../board/domain/repository/PostReader.kt | 2 +- .../board/domain/repository/PostRepository.kt | 8 ++-- .../usecase/query/GetDashboardQueryService.kt | 4 +- .../command/MarkNoticeReadUseCaseTest.kt | 42 ++++------------- .../LastNoticeReadRepositoryTest.kt | 47 +++++++++++++++++++ .../query/GetDashboardQueryServiceTest.kt | 27 +++++++++-- 10 files changed, 123 insertions(+), 68 deletions(-) create mode 100644 src/test/kotlin/com/weeth/domain/board/domain/repository/LastNoticeReadRepositoryTest.kt diff --git a/src/main/kotlin/com/weeth/domain/board/application/usecase/command/MarkNoticeReadUseCase.kt b/src/main/kotlin/com/weeth/domain/board/application/usecase/command/MarkNoticeReadUseCase.kt index 1f41eebd..c93d68f8 100644 --- a/src/main/kotlin/com/weeth/domain/board/application/usecase/command/MarkNoticeReadUseCase.kt +++ b/src/main/kotlin/com/weeth/domain/board/application/usecase/command/MarkNoticeReadUseCase.kt @@ -3,13 +3,10 @@ package com.weeth.domain.board.application.usecase.command import com.weeth.domain.board.application.exception.BoardNotFoundException import com.weeth.domain.board.application.exception.BoardNotInClubException import com.weeth.domain.board.application.exception.BoardTypeMismatchException -import com.weeth.domain.board.domain.entity.LastNoticeRead import com.weeth.domain.board.domain.enums.BoardType import com.weeth.domain.board.domain.repository.BoardRepository -import com.weeth.domain.board.domain.repository.LastNoticeReadReader import com.weeth.domain.board.domain.repository.LastNoticeReadRepository import com.weeth.domain.club.domain.service.ClubMemberPolicy -import com.weeth.domain.user.domain.repository.UserReader import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDateTime @@ -17,9 +14,7 @@ import java.time.LocalDateTime @Service class MarkNoticeReadUseCase( private val boardRepository: BoardRepository, - private val lastNoticeReadReader: LastNoticeReadReader, private val lastNoticeReadRepository: LastNoticeReadRepository, - private val userReader: UserReader, private val clubMemberPolicy: ClubMemberPolicy, ) { @Transactional @@ -28,7 +23,7 @@ class MarkNoticeReadUseCase( clubId: Long, boardId: Long, ) { - clubMemberPolicy.getActiveMember(clubId, userId) + val clubMember = clubMemberPolicy.getActiveMember(clubId, userId) val board = boardRepository.findByIdAndIsDeletedFalse(boardId) @@ -36,13 +31,10 @@ class MarkNoticeReadUseCase( if (board.club.id != clubId) throw BoardNotInClubException() if (board.type != BoardType.NOTICE) throw BoardTypeMismatchException() - val existing = lastNoticeReadReader.findByUserIdAndBoardId(userId, boardId) - if (existing != null) { - existing.updateLastReadAt(LocalDateTime.now()) - return - } - - val user = userReader.getById(userId) - lastNoticeReadRepository.save(LastNoticeRead.create(user = user, board = board)) + lastNoticeReadRepository.markRead( + clubMemberId = clubMember.id, + boardId = board.id, + lastReadAt = LocalDateTime.now(), + ) } } diff --git a/src/main/kotlin/com/weeth/domain/board/domain/entity/LastNoticeRead.kt b/src/main/kotlin/com/weeth/domain/board/domain/entity/LastNoticeRead.kt index cb052c3f..4aeb7bc9 100644 --- a/src/main/kotlin/com/weeth/domain/board/domain/entity/LastNoticeRead.kt +++ b/src/main/kotlin/com/weeth/domain/board/domain/entity/LastNoticeRead.kt @@ -1,6 +1,6 @@ package com.weeth.domain.board.domain.entity -import com.weeth.domain.user.domain.entity.User +import com.weeth.domain.club.domain.entity.ClubMember import jakarta.persistence.Column import jakarta.persistence.Entity import jakarta.persistence.FetchType @@ -16,10 +16,10 @@ import java.time.LocalDateTime @Entity @Table( name = "last_notice_read", - uniqueConstraints = [UniqueConstraint(columnNames = ["user_id", "board_id"])], + uniqueConstraints = [UniqueConstraint(columnNames = ["club_member_id", "board_id"])], ) class LastNoticeRead( - user: User, + clubMember: ClubMember, board: Board, ) { @Id @@ -28,8 +28,8 @@ class LastNoticeRead( private set @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - var user: User = user + @JoinColumn(name = "club_member_id", nullable = false) + var clubMember: ClubMember = clubMember private set @ManyToOne(fetch = FetchType.LAZY) @@ -47,8 +47,8 @@ class LastNoticeRead( companion object { fun create( - user: User, + clubMember: ClubMember, board: Board, - ) = LastNoticeRead(user = user, board = board) + ) = LastNoticeRead(clubMember = clubMember, board = board) } } diff --git a/src/main/kotlin/com/weeth/domain/board/domain/repository/LastNoticeReadReader.kt b/src/main/kotlin/com/weeth/domain/board/domain/repository/LastNoticeReadReader.kt index edd3fac0..a2779880 100644 --- a/src/main/kotlin/com/weeth/domain/board/domain/repository/LastNoticeReadReader.kt +++ b/src/main/kotlin/com/weeth/domain/board/domain/repository/LastNoticeReadReader.kt @@ -3,8 +3,8 @@ package com.weeth.domain.board.domain.repository import com.weeth.domain.board.domain.entity.LastNoticeRead interface LastNoticeReadReader { - fun findByUserIdAndBoardId( - userId: Long, + fun findByClubMemberIdAndBoardId( + clubMemberId: Long, boardId: Long, ): LastNoticeRead? } diff --git a/src/main/kotlin/com/weeth/domain/board/domain/repository/LastNoticeReadRepository.kt b/src/main/kotlin/com/weeth/domain/board/domain/repository/LastNoticeReadRepository.kt index bbb241c8..8bdb2894 100644 --- a/src/main/kotlin/com/weeth/domain/board/domain/repository/LastNoticeReadRepository.kt +++ b/src/main/kotlin/com/weeth/domain/board/domain/repository/LastNoticeReadRepository.kt @@ -2,12 +2,31 @@ package com.weeth.domain.board.domain.repository import com.weeth.domain.board.domain.entity.LastNoticeRead import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Modifying +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param +import java.time.LocalDateTime interface LastNoticeReadRepository : JpaRepository, LastNoticeReadReader { - override fun findByUserIdAndBoardId( - userId: Long, + override fun findByClubMemberIdAndBoardId( + clubMemberId: Long, boardId: Long, ): LastNoticeRead? + + @Modifying + @Query( + value = """ + INSERT INTO last_notice_read (club_member_id, board_id, last_read_at) + VALUES (:clubMemberId, :boardId, :lastReadAt) + ON DUPLICATE KEY UPDATE last_read_at = :lastReadAt + """, + nativeQuery = true, + ) + fun markRead( + @Param("clubMemberId") clubMemberId: Long, + @Param("boardId") boardId: Long, + @Param("lastReadAt") lastReadAt: LocalDateTime, + ): Int } diff --git a/src/main/kotlin/com/weeth/domain/board/domain/repository/PostReader.kt b/src/main/kotlin/com/weeth/domain/board/domain/repository/PostReader.kt index 0cac837d..82e38c8f 100644 --- a/src/main/kotlin/com/weeth/domain/board/domain/repository/PostReader.kt +++ b/src/main/kotlin/com/weeth/domain/board/domain/repository/PostReader.kt @@ -34,7 +34,7 @@ interface PostReader { fun findFirstUnreadNoticeSince( clubId: Long, - userId: Long, + clubMemberId: Long, boardType: BoardType, since: LocalDateTime, ): Post? diff --git a/src/main/kotlin/com/weeth/domain/board/domain/repository/PostRepository.kt b/src/main/kotlin/com/weeth/domain/board/domain/repository/PostRepository.kt index d15a994f..25a5b53e 100644 --- a/src/main/kotlin/com/weeth/domain/board/domain/repository/PostRepository.kt +++ b/src/main/kotlin/com/weeth/domain/board/domain/repository/PostRepository.kt @@ -163,7 +163,7 @@ interface PostRepository : """ SELECT p FROM Post p - LEFT JOIN LastNoticeRead lr ON lr.user.id = :userId AND lr.board.id = p.board.id + LEFT JOIN LastNoticeRead lr ON lr.clubMember.id = :clubMemberId AND lr.board.id = p.board.id WHERE p.board.club.id = :clubId AND p.board.type = :boardType AND p.isDeleted = false @@ -175,7 +175,7 @@ interface PostRepository : ) fun findUnreadNoticeSince( @Param("clubId") clubId: Long, - @Param("userId") userId: Long, + @Param("clubMemberId") clubMemberId: Long, @Param("boardType") boardType: BoardType, @Param("since") since: LocalDateTime, pageable: Pageable, @@ -183,10 +183,10 @@ interface PostRepository : override fun findFirstUnreadNoticeSince( clubId: Long, - userId: Long, + clubMemberId: Long, boardType: BoardType, since: LocalDateTime, - ): Post? = findUnreadNoticeSince(clubId, userId, boardType, since, PageRequest.of(0, 1)).firstOrNull() + ): Post? = findUnreadNoticeSince(clubId, clubMemberId, boardType, since, PageRequest.of(0, 1)).firstOrNull() @Query( """ diff --git a/src/main/kotlin/com/weeth/domain/dashboard/application/usecase/query/GetDashboardQueryService.kt b/src/main/kotlin/com/weeth/domain/dashboard/application/usecase/query/GetDashboardQueryService.kt index b2310e4f..e9e5e3fa 100644 --- a/src/main/kotlin/com/weeth/domain/dashboard/application/usecase/query/GetDashboardQueryService.kt +++ b/src/main/kotlin/com/weeth/domain/dashboard/application/usecase/query/GetDashboardQueryService.kt @@ -139,11 +139,11 @@ class GetDashboardQueryService( clubId: Long, userId: Long, ): DashboardUnreadNoticeResponse? { - clubMemberPolicy.getActiveMember(clubId, userId) + val member = clubMemberPolicy.getActiveMember(clubId, userId) val since = LocalDateTime.now().minusWeeks(2) return postReader - .findFirstUnreadNoticeSince(clubId, userId, BoardType.NOTICE, since) + .findFirstUnreadNoticeSince(clubId, member.id, BoardType.NOTICE, since) ?.let(dashboardMapper::toUnreadNoticeResponse) } } diff --git a/src/test/kotlin/com/weeth/domain/board/application/usecase/command/MarkNoticeReadUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/board/application/usecase/command/MarkNoticeReadUseCaseTest.kt index e0243796..2a24d0ac 100644 --- a/src/test/kotlin/com/weeth/domain/board/application/usecase/command/MarkNoticeReadUseCaseTest.kt +++ b/src/test/kotlin/com/weeth/domain/board/application/usecase/command/MarkNoticeReadUseCaseTest.kt @@ -2,19 +2,15 @@ package com.weeth.domain.board.application.usecase.command import com.weeth.domain.board.application.exception.BoardNotInClubException import com.weeth.domain.board.application.exception.BoardTypeMismatchException -import com.weeth.domain.board.domain.entity.LastNoticeRead import com.weeth.domain.board.domain.repository.BoardRepository -import com.weeth.domain.board.domain.repository.LastNoticeReadReader import com.weeth.domain.board.domain.repository.LastNoticeReadRepository import com.weeth.domain.board.fixture.BoardTestFixture import com.weeth.domain.club.domain.repository.ClubMemberReader import com.weeth.domain.club.domain.service.ClubMemberPolicy import com.weeth.domain.club.fixture.ClubTestFixture -import com.weeth.domain.user.domain.repository.UserReader import com.weeth.domain.user.fixture.UserTestFixture import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.date.shouldBeAfter import io.mockk.clearMocks import io.mockk.every import io.mockk.mockk @@ -24,23 +20,19 @@ import org.springframework.test.util.ReflectionTestUtils class MarkNoticeReadUseCaseTest : DescribeSpec({ val boardRepository = mockk() - val lastNoticeReadReader = mockk() val lastNoticeReadRepository = mockk() - val userReader = mockk() val clubMemberReader = mockk() val clubMemberPolicy = ClubMemberPolicy(clubMemberReader) val useCase = MarkNoticeReadUseCase( boardRepository = boardRepository, - lastNoticeReadReader = lastNoticeReadReader, lastNoticeReadRepository = lastNoticeReadRepository, - userReader = userReader, clubMemberPolicy = clubMemberPolicy, ) beforeTest { - clearMocks(boardRepository, lastNoticeReadReader, lastNoticeReadRepository, userReader, clubMemberReader) + clearMocks(boardRepository, lastNoticeReadRepository, clubMemberReader) } describe("execute") { @@ -49,7 +41,10 @@ class MarkNoticeReadUseCaseTest : val boardId = 1L val user = UserTestFixture.createActiveUser1(1L) val club = ClubTestFixture.createClub().also { ReflectionTestUtils.setField(it, "id", clubId) } - val clubMember = ClubTestFixture.createClubMember(club = club, user = user) + val clubMember = + ClubTestFixture + .createClubMember(club = club, user = user) + .also { ReflectionTestUtils.setField(it, "id", 10L) } val noticeBoard = BoardTestFixture.createNoticeBoard(club = club) context("클럽 멤버가 아닌 경우") { @@ -88,34 +83,15 @@ class MarkNoticeReadUseCaseTest : } } - context("이미 읽은 기록이 있는 경우") { - it("lastReadAt을 현재 시각으로 갱신하고 새 레코드를 저장하지 않는다") { - val existing = LastNoticeRead.create(user = user, board = noticeBoard) - val beforeExecute = existing.lastReadAt + context("공지 게시판 읽음 처리 요청이 유효한 경우") { + it("clubMember와 board 기준으로 마지막 읽음 시간을 기록한다") { every { clubMemberReader.findByClubIdAndUserId(clubId, userId) } returns clubMember every { boardRepository.findByIdAndIsDeletedFalse(boardId) } returns noticeBoard - every { lastNoticeReadReader.findByUserIdAndBoardId(userId, boardId) } returns existing + every { lastNoticeReadRepository.markRead(clubMember.id, noticeBoard.id, any()) } returns 1 useCase.execute(userId, clubId, boardId) - existing.lastReadAt shouldBeAfter beforeExecute - verify(exactly = 0) { userReader.getById(any()) } - verify(exactly = 0) { lastNoticeReadRepository.save(any()) } - } - } - - context("처음 읽는 경우") { - it("새 LastNoticeRead 레코드를 저장한다") { - every { clubMemberReader.findByClubIdAndUserId(clubId, userId) } returns clubMember - every { boardRepository.findByIdAndIsDeletedFalse(boardId) } returns noticeBoard - every { lastNoticeReadReader.findByUserIdAndBoardId(userId, boardId) } returns null - every { userReader.getById(userId) } returns user - every { lastNoticeReadRepository.save(any()) } answers { firstArg() } - - useCase.execute(userId, clubId, boardId) - - verify(exactly = 1) { userReader.getById(userId) } - verify(exactly = 1) { lastNoticeReadRepository.save(any()) } + verify(exactly = 1) { lastNoticeReadRepository.markRead(clubMember.id, noticeBoard.id, any()) } } } } diff --git a/src/test/kotlin/com/weeth/domain/board/domain/repository/LastNoticeReadRepositoryTest.kt b/src/test/kotlin/com/weeth/domain/board/domain/repository/LastNoticeReadRepositoryTest.kt new file mode 100644 index 00000000..864cb79b --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/board/domain/repository/LastNoticeReadRepositoryTest.kt @@ -0,0 +1,47 @@ +package com.weeth.domain.board.domain.repository + +import com.weeth.config.TestContainersConfig +import com.weeth.domain.board.fixture.BoardTestFixture +import com.weeth.domain.club.domain.repository.ClubMemberRepository +import com.weeth.domain.club.domain.repository.ClubRepository +import com.weeth.domain.club.fixture.ClubTestFixture +import com.weeth.domain.user.domain.repository.UserRepository +import com.weeth.domain.user.fixture.UserTestFixture +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.context.annotation.Import +import java.time.LocalDateTime + +@DataJpaTest +@Import(TestContainersConfig::class) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class LastNoticeReadRepositoryTest( + private val lastNoticeReadRepository: LastNoticeReadRepository, + private val boardRepository: BoardRepository, + private val clubRepository: ClubRepository, + private val clubMemberRepository: ClubMemberRepository, + private val userRepository: UserRepository, +) : StringSpec({ + + "markRead는 최초 호출 시 읽음 기록을 생성하고 재호출 시 lastReadAt을 갱신한다" { + val user = userRepository.save(UserTestFixture.createActiveUser1()) + val club = clubRepository.save(ClubTestFixture.createClub()) + val clubMember = clubMemberRepository.save(ClubTestFixture.createClubMember(club = club, user = user)) + val board = boardRepository.save(BoardTestFixture.createNoticeBoard(club = club)) + val firstReadAt = LocalDateTime.of(2026, 5, 7, 10, 0) + val secondReadAt = LocalDateTime.of(2026, 5, 7, 10, 5) + + lastNoticeReadRepository.markRead(clubMember.id, board.id, firstReadAt) + lastNoticeReadRepository.markRead(clubMember.id, board.id, secondReadAt) + + val result = lastNoticeReadRepository.findByClubMemberIdAndBoardId(clubMember.id, board.id) + + result.shouldNotBeNull() + result.lastReadAt shouldBe secondReadAt + lastNoticeReadRepository.findAll() shouldHaveSize 1 + } + }) diff --git a/src/test/kotlin/com/weeth/domain/dashboard/application/usecase/query/GetDashboardQueryServiceTest.kt b/src/test/kotlin/com/weeth/domain/dashboard/application/usecase/query/GetDashboardQueryServiceTest.kt index 1121be26..418ea509 100644 --- a/src/test/kotlin/com/weeth/domain/dashboard/application/usecase/query/GetDashboardQueryServiceTest.kt +++ b/src/test/kotlin/com/weeth/domain/dashboard/application/usecase/query/GetDashboardQueryServiceTest.kt @@ -312,7 +312,14 @@ class GetDashboardQueryServiceTest : val notice = PostTestFixture.create(board = noticeBoard) every { clubMemberReader.findByClubIdAndUserId(clubId, userId) } returns clubMember - every { postReader.findFirstUnreadNoticeSince(clubId, userId, BoardType.NOTICE, any()) } returns + every { + postReader.findFirstUnreadNoticeSince( + clubId, + clubMember.id, + BoardType.NOTICE, + any(), + ) + } returns notice val result = queryService.getUnreadNotice(clubId, userId) @@ -324,7 +331,14 @@ class GetDashboardQueryServiceTest : context("모든 공지를 읽은 경우") { it("null을 반환한다") { every { clubMemberReader.findByClubIdAndUserId(clubId, userId) } returns clubMember - every { postReader.findFirstUnreadNoticeSince(clubId, userId, BoardType.NOTICE, any()) } returns + every { + postReader.findFirstUnreadNoticeSince( + clubId, + clubMember.id, + BoardType.NOTICE, + any(), + ) + } returns null val result = queryService.getUnreadNotice(clubId, userId) @@ -336,7 +350,14 @@ class GetDashboardQueryServiceTest : context("2주 내 공지가 없는 경우") { it("null을 반환한다") { every { clubMemberReader.findByClubIdAndUserId(clubId, userId) } returns clubMember - every { postReader.findFirstUnreadNoticeSince(clubId, userId, BoardType.NOTICE, any()) } returns + every { + postReader.findFirstUnreadNoticeSince( + clubId, + clubMember.id, + BoardType.NOTICE, + any(), + ) + } returns null val result = queryService.getUnreadNotice(clubId, userId)