From a4570de4e61f877f6fde85c90ee8d283a6adaba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EB=AF=B8?= Date: Tue, 21 Jul 2026 05:14:40 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EB=AA=A8=EB=8B=88=ED=84=B0?= =?UTF-8?q?=EB=A7=81=20=EC=9D=B8=ED=94=84=EB=9D=BC=20=EA=B5=AC=EC=B6=95=20?= =?UTF-8?q?(ELK=20+=20Prometheus/Grafana=20+=20Discord=20Alert)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MDC traceId 필터 + Logback 롤링/gzip 압축 설정 추가 - Docker화: Dockerfile, docker-compose.yml (app+postgres+redis) - ELK 스택 추가 (Elasticsearch/Logstash/Kibana) - 로그 중앙 검색 - Actuator 보안설정(base-path 변경, 노출 엔드포인트 제한) + Micrometer/Prometheus 연동 - Grafana 대시보드 및 서버 다운 감지 Alert Rule 프로비저닝 (Discord 웹훅 연동, URL은 플레이스홀더) 로컬 docker-compose 검증 완료. 실제 배포/CI-CD 반영 전 리뷰 필요. --- .gitignore | 2 + Dockerfile | 26 ++++ build.gradle | 3 + docker-compose.yml | 112 ++++++++++++++++++ .../provisioning/alerting/contact-points.yml | 11 ++ .../alerting/notification-policies.yml | 9 ++ grafana/provisioning/alerting/rules.yml | 48 ++++++++ grafana/provisioning/dashboards/dashboard.yml | 12 ++ .../dashboards/json/runnect-overview.json | 56 +++++++++ .../provisioning/datasources/prometheus.yml | 10 ++ logstash.conf | 27 +++++ prometheus.yml | 8 ++ .../config/logging/MdcLoggingFilter.java | 36 ++++++ src/main/resources/logback-spring.xml | 30 +++++ 14 files changed, 390 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 grafana/provisioning/alerting/contact-points.yml create mode 100644 grafana/provisioning/alerting/notification-policies.yml create mode 100644 grafana/provisioning/alerting/rules.yml create mode 100644 grafana/provisioning/dashboards/dashboard.yml create mode 100644 grafana/provisioning/dashboards/json/runnect-overview.json create mode 100644 grafana/provisioning/datasources/prometheus.yml create mode 100644 logstash.conf create mode 100644 prometheus.yml create mode 100644 src/main/java/org/runnect/server/config/logging/MdcLoggingFilter.java create mode 100644 src/main/resources/logback-spring.xml diff --git a/.gitignore b/.gitignore index 8aca08b..6bca2f9 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ application.yaml ### DS_Store .DS_Store +logs/ +test_write.tmp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1180b45 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# --- Build stage --- +FROM gradle:8.2.1-jdk11 AS build +WORKDIR /app + +COPY build.gradle settings.gradle ./ +COPY gradle ./gradle +COPY gradlew ./ + +COPY src ./src + +# 로컬/모니터링 검증용 application.yml은 build context 루트에 두고 복사한다. +# (CI/CD에서는 GitHub Secret으로 별도 생성 — 이 Dockerfile은 로컬 검증 전용) +COPY application.yml ./application.yml + +RUN ./gradlew bootJar -x test --no-daemon + +# --- Run stage --- +FROM eclipse-temurin:11-jre +WORKDIR /app + +COPY --from=build /app/build/libs/*.jar app.jar +COPY --from=build /app/application.yml application.yml + +EXPOSE 8080 + +ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul", "-jar", "app.jar", "--spring.config.location=file:./application.yml"] diff --git a/build.gradle b/build.gradle index c3ce667..84f1a4a 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,9 @@ dependencies { // Health Check implementation 'org.springframework.boot:spring-boot-starter-actuator' + // Monitoring - Prometheus + implementation 'io.micrometer:micrometer-registry-prometheus:1.9.14' + // JPA & Database implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation group: 'org.postgresql', name: 'postgresql', version: '42.2.27' diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..218f814 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,112 @@ +version: "3.8" + +services: + runnect-app: + build: + context: . + dockerfile: Dockerfile + container_name: runnect-app + ports: + - "8080:8080" + depends_on: + - postgres + - redis + volumes: + - ./logs:/app/logs + networks: + - runnect-net + + postgres: + image: postgres:15 + container_name: runnect-postgres + environment: + POSTGRES_DB: runnect + POSTGRES_USER: runnect + POSTGRES_PASSWORD: runnect_local_password + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - runnect-net + + redis: + image: redis:7 + container_name: runnect-redis + ports: + - "6379:6379" + networks: + - runnect-net + + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4 + container_name: runnect-elasticsearch + environment: + - discovery.type=single-node + - xpack.security.enabled=false + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" + ports: + - "9200:9200" + networks: + - runnect-net + + logstash: + image: docker.elastic.co/logstash/logstash:8.13.4 + container_name: runnect-logstash + depends_on: + - elasticsearch + volumes: + - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf + - ./logs:/usr/share/logstash/logs + ports: + - "5044:5044" + networks: + - runnect-net + + kibana: + image: docker.elastic.co/kibana/kibana:8.13.4 + container_name: runnect-kibana + depends_on: + - elasticsearch + environment: + - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 + ports: + - "5601:5601" + networks: + - runnect-net + + prometheus: + image: prom/prometheus:latest + container_name: runnect-prometheus + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + depends_on: + - runnect-app + networks: + - runnect-net + + grafana: + image: grafana/grafana:latest + container_name: runnect-grafana + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + volumes: + - ./grafana/provisioning:/etc/grafana/provisioning + - grafana_data:/var/lib/grafana + ports: + - "3000:3000" + depends_on: + - prometheus + networks: + - runnect-net + +networks: + runnect-net: + driver: bridge + +volumes: + postgres_data: + grafana_data: diff --git a/grafana/provisioning/alerting/contact-points.yml b/grafana/provisioning/alerting/contact-points.yml new file mode 100644 index 0000000..bbfe1bf --- /dev/null +++ b/grafana/provisioning/alerting/contact-points.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +contactPoints: + - orgId: 1 + name: discord-alert + receivers: + - uid: discord-alert-receiver + type: discord + settings: + url: "REPLACE_WITH_YOUR_DISCORD_WEBHOOK_URL" + use_discord_username: true diff --git a/grafana/provisioning/alerting/notification-policies.yml b/grafana/provisioning/alerting/notification-policies.yml new file mode 100644 index 0000000..cfe112e --- /dev/null +++ b/grafana/provisioning/alerting/notification-policies.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +policies: + - orgId: 1 + receiver: discord-alert + group_by: ["alertname"] + group_wait: 10s + group_interval: 1m + repeat_interval: 10m diff --git a/grafana/provisioning/alerting/rules.yml b/grafana/provisioning/alerting/rules.yml new file mode 100644 index 0000000..1e8603e --- /dev/null +++ b/grafana/provisioning/alerting/rules.yml @@ -0,0 +1,48 @@ +apiVersion: 1 + +groups: + - orgId: 1 + name: runnect-alerts + folder: Runnect Alerts + interval: 30s + rules: + - uid: runnect-server-down + title: "Runnect Server Down" + condition: C + for: 30s + noDataState: Alerting + execErrState: Alerting + data: + - refId: A + datasourceUid: prometheus-ds + relativeTimeRange: + from: 120 + to: 0 + model: + expr: 'up{job="runnect-server"}' + instant: true + refId: A + - refId: C + datasourceUid: "-100" + relativeTimeRange: + from: 120 + to: 0 + model: + type: threshold + refId: C + expression: A + conditions: + - evaluator: + type: lt + params: [1] + operator: + type: and + query: + params: ["A"] + reducer: + type: last + params: [] + annotations: + summary: "Runnect 서버가 응답하지 않고 있습니다 (up == 0)" + labels: + severity: critical diff --git a/grafana/provisioning/dashboards/dashboard.yml b/grafana/provisioning/dashboards/dashboard.yml new file mode 100644 index 0000000..25e6d25 --- /dev/null +++ b/grafana/provisioning/dashboards/dashboard.yml @@ -0,0 +1,12 @@ +apiVersion: 1 + +providers: + - name: "Runnect Dashboards" + orgId: 1 + folder: "" + type: file + disableDeletion: false + updateIntervalSeconds: 30 + options: + path: /etc/grafana/provisioning/dashboards/json + foldersFromFilesStructure: true diff --git a/grafana/provisioning/dashboards/json/runnect-overview.json b/grafana/provisioning/dashboards/json/runnect-overview.json new file mode 100644 index 0000000..1f50bbf --- /dev/null +++ b/grafana/provisioning/dashboards/json/runnect-overview.json @@ -0,0 +1,56 @@ +{ + "title": "Runnect Server Overview", + "timezone": "browser", + "schemaVersion": 39, + "refresh": "10s", + "panels": [ + { + "id": 1, + "title": "JVM Heap Used (bytes)", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }, + "targets": [ + { + "expr": "sum(jvm_memory_used_bytes{area=\"heap\", application=\"runnect-server\"})", + "legendFormat": "heap used" + } + ] + }, + { + "id": 2, + "title": "CPU Usage", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }, + "targets": [ + { + "expr": "process_cpu_usage{application=\"runnect-server\"}", + "legendFormat": "cpu usage" + } + ] + }, + { + "id": 3, + "title": "HTTP Requests per Second", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, + "targets": [ + { + "expr": "sum(rate(http_server_requests_seconds_count{application=\"runnect-server\"}[1m])) by (status)", + "legendFormat": "status {{status}}" + } + ] + }, + { + "id": 4, + "title": "Server Up", + "type": "stat", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, + "targets": [ + { + "expr": "up{job=\"runnect-server\"}", + "legendFormat": "up" + } + ] + } + ] +} diff --git a/grafana/provisioning/datasources/prometheus.yml b/grafana/provisioning/datasources/prometheus.yml new file mode 100644 index 0000000..450ef4d --- /dev/null +++ b/grafana/provisioning/datasources/prometheus.yml @@ -0,0 +1,10 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + uid: prometheus-ds + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: true diff --git a/logstash.conf b/logstash.conf new file mode 100644 index 0000000..115ca22 --- /dev/null +++ b/logstash.conf @@ -0,0 +1,27 @@ +input { + file { + path => "/usr/share/logstash/logs/runnect-server.log" + start_position => "beginning" + sincedb_path => "/dev/null" + } +} + +filter { + grok { + match => { + "message" => "%{TIMESTAMP_ISO8601:log_timestamp} \[%{DATA:traceId}\] %{LOGLEVEL:level}%{SPACE}\[%{DATA:thread}\] %{DATA:logger} - %{GREEDYDATA:log_message}" + } + } + + date { + match => ["log_timestamp", "yyyy-MM-dd HH:mm:ss.SSS"] + target => "@timestamp" + } +} + +output { + elasticsearch { + hosts => ["http://elasticsearch:9200"] + index => "runnect-logs-%{+YYYY.MM.dd}" + } +} diff --git a/prometheus.yml b/prometheus.yml new file mode 100644 index 0000000..8158a44 --- /dev/null +++ b/prometheus.yml @@ -0,0 +1,8 @@ +global: + scrape_interval: 5s + +scrape_configs: + - job_name: "runnect-server" + metrics_path: "/internal-monitor/prometheus" + static_configs: + - targets: ["runnect-app:8080"] diff --git a/src/main/java/org/runnect/server/config/logging/MdcLoggingFilter.java b/src/main/java/org/runnect/server/config/logging/MdcLoggingFilter.java new file mode 100644 index 0000000..c88c3b8 --- /dev/null +++ b/src/main/java/org/runnect/server/config/logging/MdcLoggingFilter.java @@ -0,0 +1,36 @@ +package org.runnect.server.config.logging; + +import org.slf4j.MDC; +import org.springframework.stereotype.Component; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.IOException; +import java.util.UUID; + +/** + * 요청마다 traceId를 발급해 MDC에 담아둔다. + * 로그 패턴에 %X{traceId}를 포함시키면(logback-spring.xml), 여러 요청이 뒤섞인 로그에서도 + * 같은 traceId로 특정 요청의 흐름만 추적할 수 있다. + */ +@Component +public class MdcLoggingFilter implements Filter { + + private static final String TRACE_ID_KEY = "traceId"; + private static final int TRACE_ID_LENGTH = 8; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + String traceId = UUID.randomUUID().toString().substring(0, TRACE_ID_LENGTH); + try { + MDC.put(TRACE_ID_KEY, traceId); + chain.doFilter(request, response); + } finally { + MDC.remove(TRACE_ID_KEY); + } + } +} diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..81b0490 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,30 @@ + + + + + + + + + ${LOG_PATTERN} + + + + + ${LOG_DIR}/runnect-server.log + + ${LOG_PATTERN} + + + ${LOG_DIR}/runnect-server.%d{yyyy-MM-dd}.log.gz + 30 + + + + + + + + + From 3df77b3394ea59ccc64b474951d915c36ebcf9c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EB=AF=B8?= Date: Tue, 21 Jul 2026 18:32:52 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20Slack=20=EC=9B=B9=ED=9B=85=20?= =?UTF-8?q?=EC=8B=9C=ED=81=AC=EB=A6=BF=EC=9D=84=20=EC=BB=A4=EB=B0=8B=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20=EB=9F=B0=ED=83=80=EC=9E=84=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EB=B3=80=EC=88=98=EB=A1=9C=20=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - contact-points.yml.tpl(플레이스홀더만 포함)을 커밋, 실제 URL은 .env(gitignore)로 로컬 주입 - docker-compose grafana 서비스에서 컨테이너 기동 시 sed로 치환 후 contact-points.yml 생성(비커밋) - .env.example로 필요한 환경변수 문서화 - 이전 커밋에 있던 실제 웹훅 URL은 push 전에 브랜치에서 제거됨(GitHub push protection에 의해 원격 유출은 없었음) --- .env.example | 4 ++++ .gitignore | 2 ++ docker-compose.yml | 6 ++++++ grafana/provisioning/alerting/contact-points.yml.tpl | 11 +++++++++++ .../provisioning/alerting/notification-policies.yml | 2 +- .../server/config/logging/MdcLoggingFilter.java | 5 +++++ 6 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 grafana/provisioning/alerting/contact-points.yml.tpl diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5ab4d78 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# 로컬 모니터링 스택 실행 전, 이 파일을 .env로 복사하고 실제 값을 채우세요. +# .env는 .gitignore에 포함되어 있어 커밋되지 않습니다. + +SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXXXXXXX/XXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX diff --git a/.gitignore b/.gitignore index 6bca2f9..75d7599 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ application.yaml .DS_Store logs/ test_write.tmp +.env +grafana/provisioning/alerting/contact-points.yml diff --git a/docker-compose.yml b/docker-compose.yml index 218f814..df93f15 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -93,6 +93,12 @@ services: environment: - GF_SECURITY_ADMIN_USER=admin - GF_SECURITY_ADMIN_PASSWORD=admin + - SLACK_WEBHOOK_URL=${SLACK_WEBHOOK_URL} + entrypoint: ["/bin/sh", "-c"] + command: + - | + sed "s|__SLACK_WEBHOOK_URL__|$${SLACK_WEBHOOK_URL}|g" /etc/grafana/provisioning/alerting/contact-points.yml.tpl > /etc/grafana/provisioning/alerting/contact-points.yml + exec /run.sh volumes: - ./grafana/provisioning:/etc/grafana/provisioning - grafana_data:/var/lib/grafana diff --git a/grafana/provisioning/alerting/contact-points.yml.tpl b/grafana/provisioning/alerting/contact-points.yml.tpl new file mode 100644 index 0000000..5039203 --- /dev/null +++ b/grafana/provisioning/alerting/contact-points.yml.tpl @@ -0,0 +1,11 @@ +apiVersion: 1 + +contactPoints: + - orgId: 1 + name: slack-alert + receivers: + - uid: slack-alert-receiver + type: slack + settings: + url: "__SLACK_WEBHOOK_URL__" + username: "Grafana" diff --git a/grafana/provisioning/alerting/notification-policies.yml b/grafana/provisioning/alerting/notification-policies.yml index cfe112e..105ac98 100644 --- a/grafana/provisioning/alerting/notification-policies.yml +++ b/grafana/provisioning/alerting/notification-policies.yml @@ -2,7 +2,7 @@ apiVersion: 1 policies: - orgId: 1 - receiver: discord-alert + receiver: slack-alert group_by: ["alertname"] group_wait: 10s group_interval: 1m diff --git a/src/main/java/org/runnect/server/config/logging/MdcLoggingFilter.java b/src/main/java/org/runnect/server/config/logging/MdcLoggingFilter.java index c88c3b8..d7fe4a3 100644 --- a/src/main/java/org/runnect/server/config/logging/MdcLoggingFilter.java +++ b/src/main/java/org/runnect/server/config/logging/MdcLoggingFilter.java @@ -1,5 +1,6 @@ package org.runnect.server.config.logging; +import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; import org.springframework.stereotype.Component; @@ -8,6 +9,7 @@ import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.UUID; @@ -16,6 +18,7 @@ * 로그 패턴에 %X{traceId}를 포함시키면(logback-spring.xml), 여러 요청이 뒤섞인 로그에서도 * 같은 traceId로 특정 요청의 흐름만 추적할 수 있다. */ +@Slf4j @Component public class MdcLoggingFilter implements Filter { @@ -28,6 +31,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha String traceId = UUID.randomUUID().toString().substring(0, TRACE_ID_LENGTH); try { MDC.put(TRACE_ID_KEY, traceId); + HttpServletRequest httpRequest = (HttpServletRequest) request; + log.info("{} {}", httpRequest.getMethod(), httpRequest.getRequestURI()); chain.doFilter(request, response); } finally { MDC.remove(TRACE_ID_KEY); From 71837bb38c1fefab0dbe21d2340c4b06de3fdd7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EB=AF=B8?= Date: Tue, 21 Jul 2026 18:40:41 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20CI=20mkdir=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20+=20CODEOWNERS=20=EB=B9=84=ED=99=9C?= =?UTF-8?q?=EB=8F=99=20=EC=9D=B8=EC=9B=90=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/main/resources 디렉토리가 이미 존재할 때 mkdir이 실패하던 문제 수정 (mkdir -p로 변경) - 이번 PR에서 logback-spring.xml을 추가하며 resources 디렉토리가 최초로 생겨서 노출된 버그 - CODEOWNERS에서 더 이상 팀에 참여하지 않는 인원(funnysunny08, RinRinPARK, YuSuhwa-ve) 제거 --- .github/CODEOWNERS | 1 - .github/workflows/dev-cd.yml | 2 +- .github/workflows/dev-ci.yml | 2 +- .github/workflows/prod-cd.yml | 2 +- .github/workflows/prod-ci.yml | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7bb5ecf..e69de29 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +0,0 @@ -*.java @funnysunny08 @RinRinPARK @YuSuhwa-ve diff --git a/.github/workflows/dev-cd.yml b/.github/workflows/dev-cd.yml index ec0cf9c..5aa6a6c 100644 --- a/.github/workflows/dev-cd.yml +++ b/.github/workflows/dev-cd.yml @@ -33,7 +33,7 @@ jobs: - name: make application.properties 파일 생성 run: | ## create application.yml - mkdir ./src/main/resources + mkdir -p ./src/main/resources cd ./src/main/resources # application.yml 파일 생성 diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index 6410227..3bfe94b 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -30,7 +30,7 @@ jobs: - name: make application.properties 파일 생성 run: | ## create application.yml - mkdir ./src/main/resources + mkdir -p ./src/main/resources cd ./src/main/resources # application.yml 파일 생성 diff --git a/.github/workflows/prod-cd.yml b/.github/workflows/prod-cd.yml index 9a88b77..fc61d88 100644 --- a/.github/workflows/prod-cd.yml +++ b/.github/workflows/prod-cd.yml @@ -34,7 +34,7 @@ jobs: - name: make application.properties 파일 생성 run: | ## create application.yml - mkdir ./src/main/resources + mkdir -p ./src/main/resources cd ./src/main/resources # application.yml 파일 생성 diff --git a/.github/workflows/prod-ci.yml b/.github/workflows/prod-ci.yml index ca4a4df..432523d 100644 --- a/.github/workflows/prod-ci.yml +++ b/.github/workflows/prod-ci.yml @@ -23,7 +23,7 @@ jobs: - name: make application.properties 파일 생성 run: | ## create application.yml - mkdir ./src/main/resources + mkdir -p ./src/main/resources cd ./src/main/resources # application.yml 파일 생성 From b76ffc42a1938613c0f4e019cec8c77cd69c2659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EB=AF=B8?= Date: Tue, 21 Jul 2026 18:51:13 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20CodeRabbit=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20-=20Grafana=20env=20var=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=ED=8B=B0=EB=B8=8C=20=EC=A7=80=EC=9B=90=20+=20Logstash?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=204=EA=B1=B4?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Grafana 웹훅 주입을 커스텀 entrypoint(sed) 대신 네이티브 $__env{} 문법으로 단순화 (bind mount 권한 이슈 회피) - Logstash: 멀티라인 코덱 추가로 스택트레이스가 leading 로그 라인과 분리되지 않도록 수정 - Logstash: sincedb_path를 /dev/null에서 기본 영구 경로로 변경 + 볼륨 추가 (재시작 시 전체 재색인 방지) - Logstash: date filter에 timezone => Asia/Seoul 명시 (Logback이 KST naive 타임스탬프를 기록하므로) - Logstash: 인덱스 패턴을 YYYY(week-based year)에서 yyyy(calendar year)로 수정 (연말/연초 오분류 방지) 모든 수정사항은 로컬 docker-compose로 재검증 완료: - $__env{} 방식으로도 실제 장애 시뮬레이션 → Slack 알림 firing 확인 - 스택트레이스 포함 500 에러가 하나의 이벤트(6868자)로 정상 색인됨 확인 - 인덱스명이 runnect-logs-2026.07.21 형식(소문자)으로 생성됨 확인 --- .gitignore | 1 - docker-compose.yml | 7 ++----- grafana/provisioning/alerting/contact-points.yml | 10 +++++----- logstash.conf | 9 +++++++-- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 75d7599..35b430d 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,3 @@ application.yaml logs/ test_write.tmp .env -grafana/provisioning/alerting/contact-points.yml diff --git a/docker-compose.yml b/docker-compose.yml index df93f15..875a184 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -58,6 +58,7 @@ services: volumes: - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf - ./logs:/usr/share/logstash/logs + - logstash_data:/usr/share/logstash/data ports: - "5044:5044" networks: @@ -94,11 +95,6 @@ services: - GF_SECURITY_ADMIN_USER=admin - GF_SECURITY_ADMIN_PASSWORD=admin - SLACK_WEBHOOK_URL=${SLACK_WEBHOOK_URL} - entrypoint: ["/bin/sh", "-c"] - command: - - | - sed "s|__SLACK_WEBHOOK_URL__|$${SLACK_WEBHOOK_URL}|g" /etc/grafana/provisioning/alerting/contact-points.yml.tpl > /etc/grafana/provisioning/alerting/contact-points.yml - exec /run.sh volumes: - ./grafana/provisioning:/etc/grafana/provisioning - grafana_data:/var/lib/grafana @@ -116,3 +112,4 @@ networks: volumes: postgres_data: grafana_data: + logstash_data: diff --git a/grafana/provisioning/alerting/contact-points.yml b/grafana/provisioning/alerting/contact-points.yml index bbfe1bf..a48c3d1 100644 --- a/grafana/provisioning/alerting/contact-points.yml +++ b/grafana/provisioning/alerting/contact-points.yml @@ -2,10 +2,10 @@ apiVersion: 1 contactPoints: - orgId: 1 - name: discord-alert + name: slack-alert receivers: - - uid: discord-alert-receiver - type: discord + - uid: slack-alert-receiver + type: slack settings: - url: "REPLACE_WITH_YOUR_DISCORD_WEBHOOK_URL" - use_discord_username: true + url: $__env{SLACK_WEBHOOK_URL} + username: "Grafana" diff --git a/logstash.conf b/logstash.conf index 115ca22..3b81b45 100644 --- a/logstash.conf +++ b/logstash.conf @@ -2,7 +2,11 @@ input { file { path => "/usr/share/logstash/logs/runnect-server.log" start_position => "beginning" - sincedb_path => "/dev/null" + codec => multiline { + pattern => "^%{TIMESTAMP_ISO8601}" + negate => true + what => "previous" + } } } @@ -15,6 +19,7 @@ filter { date { match => ["log_timestamp", "yyyy-MM-dd HH:mm:ss.SSS"] + timezone => "Asia/Seoul" target => "@timestamp" } } @@ -22,6 +27,6 @@ filter { output { elasticsearch { hosts => ["http://elasticsearch:9200"] - index => "runnect-logs-%{+YYYY.MM.dd}" + index => "runnect-logs-%{+yyyy.MM.dd}" } }