diff --git a/.github/workflows/cd-api-dev.yml b/.github/workflows/cd-api-dev.yml index 7f91bba..e35a7ae 100644 --- a/.github/workflows/cd-api-dev.yml +++ b/.github/workflows/cd-api-dev.yml @@ -1,7 +1,7 @@ name: CD API DEV on: - push: + pull_request: branches: [ develop ] jobs: @@ -78,10 +78,10 @@ jobs: [ -d kokomen-notification ] || git clone --filter=blob:none --no-checkout https://github.com/samhap-soft/kokomen-notification.git cd kokomen-notification git sparse-checkout init --cone - git fetch origin develop - git checkout develop + git fetch origin feature/#10 + git checkout feature/#10 git sparse-checkout set docker/dev - git pull origin develop + git pull origin feature/#10 - name: Docker Image pull run: sudo docker pull samhap/kokomen-notification-api:dev @@ -95,4 +95,4 @@ jobs: run: | export HOSTNAME=$(hostname) cd kokomen-notification/docker/dev - sudo -E docker compose -f docker-compose-dev.yml up -d kokomen-notification-dev-api + sudo -E docker compose -f docker-compose-dev.yml up -d kokomen-notification-dev-api kokomen-notification-mysql-dev diff --git a/.github/workflows/cd-internal-dev.yml b/.github/workflows/cd-internal-dev.yml index de5d175..3db28b8 100644 --- a/.github/workflows/cd-internal-dev.yml +++ b/.github/workflows/cd-internal-dev.yml @@ -1,7 +1,7 @@ name: CD INTERNAL DEV on: - push: + pull_request: branches: [ develop ] jobs: @@ -78,10 +78,10 @@ jobs: [ -d kokomen-notification ] || git clone --filter=blob:none --no-checkout https://github.com/samhap-soft/kokomen-notification.git cd kokomen-notification git sparse-checkout init --cone - git fetch origin develop - git checkout develop + git fetch origin feature/#10 + git checkout feature/#10 git sparse-checkout set docker/dev - git pull origin develop + git pull origin feature/#10 - name: Docker Image pull run: sudo docker pull samhap/kokomen-notification-internal:dev @@ -95,4 +95,4 @@ jobs: run: | export HOSTNAME=$(hostname) cd kokomen-notification/docker/dev - sudo -E docker compose -f docker-compose-dev.yml up -d kokomen-notification-dev-internal + sudo -E docker compose -f docker-compose-dev.yml up -d kokomen-notification-dev-internal kokomen-notification-mysql-dev diff --git a/api/local-api-docker-compose.yml b/api/local-api-docker-compose.yml index b648071..de7418f 100644 --- a/api/local-api-docker-compose.yml +++ b/api/local-api-docker-compose.yml @@ -6,6 +6,7 @@ services: no_cache: true ports: - 8100:8080 + - 8001:8001 environment: SPRING_PROFILES_ACTIVE: local networks: diff --git a/api/src/main/java/com/samhap/kokomen/global/logging/LoggingFilter.java b/api/src/main/java/com/samhap/kokomen/global/logging/LoggingFilter.java new file mode 100644 index 0000000..52bc5ed --- /dev/null +++ b/api/src/main/java/com/samhap/kokomen/global/logging/LoggingFilter.java @@ -0,0 +1,78 @@ +package com.samhap.kokomen.global.logging; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import java.io.IOException; +import java.util.List; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.StopWatch; +import org.springframework.web.filter.OncePerRequestFilter; + +@Slf4j +@Component +public class LoggingFilter extends OncePerRequestFilter { + + private static final List WHITE_LIST = List.of( + "/favicon.ico", + "/docs/index.html", + "/metrics", + "/actuator/**"); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + + String requestId = readRequestId(request); + MDC.put("requestId", requestId); + + try { + filterChain.doFilter(request, response); + } finally { + stopWatch.stop(); + log.info("{} {} {} ({}) - {}ms", + readMemberId(request), + request.getMethod(), + request.getRequestURI(), + HttpStatus.valueOf(response.getStatus()), + stopWatch.getTotalTimeMillis()); + + MDC.clear(); + } + } + + private String readRequestId(HttpServletRequest request) { + String requestId = request.getHeader("X-RequestID"); + if (requestId != null && !requestId.isEmpty()) { + return requestId; + } + return UUID.randomUUID().toString(); + } + + private String readMemberId(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session != null) { + Long memberId = (Long) session.getAttribute("MEMBER_ID"); + if (memberId != null) { + return "memberId=" + memberId; + } + } + return ""; + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + String requestURI = request.getRequestURI(); + AntPathMatcher antPathMatcher = new AntPathMatcher(); + return WHITE_LIST.stream().anyMatch(path -> antPathMatcher.match(path, requestURI)); + } +} diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml index a33bbbb..ac41bde 100644 --- a/api/src/main/resources/application.yml +++ b/api/src/main/resources/application.yml @@ -1,4 +1,10 @@ +management: + server: + address: 0.0.0.0 + port: 8001 spring: + application: + name: kokomen-notification-api profiles: include: - domain @@ -39,10 +45,24 @@ spring: activate: on-profile: dev cors: - allowed-origins: https://dev.kokomen.kr, https://kokomen.kr:3000, https://local.kokomen.kr:3000, http://local.kokomen.kr:3000 + allowed-origins: https://dev.kokomen.kr, https://kokomen.kr:3000, https://local.kokomen.kr:3000, http://local.kokomen.kr:3000, https://www.webview-dev.kokomen.kr, https://webview-dev.kokomen.kr server: servlet: session: cookie: domain: kokomen.kr secure: false +--- +# prod profile +spring: + config: + activate: + on-profile: prod +cors: + allowed-origins: https://kokomen.kr, https://www.kokomen.kr, https://www.webview.kokomen.kr, https://webview.kokomen.kr +server: + servlet: + session: + cookie: + domain: kokomen.kr + secure: true diff --git a/common/build.gradle b/common/build.gradle index 82fe083..e6d9b47 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -1,5 +1,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + + implementation 'io.micrometer:micrometer-registry-prometheus' } bootJar { diff --git a/common/src/main/resources/application-common.yml b/common/src/main/resources/application-common.yml index b975e1e..42e3aa5 100644 --- a/common/src/main/resources/application-common.yml +++ b/common/src/main/resources/application-common.yml @@ -1,4 +1,18 @@ -# Common configuration for all profiles +management: + endpoint: + health: + show-components: always + access: read_only + info: + access: read_only + metrics: + access: read_only + prometheus: + access: read_only + endpoints: + web: + exposure: + include: prometheus, info, health, metrics --- # local profile spring: @@ -20,3 +34,13 @@ spring: redis: host: kokomen-redis-dev port: 6379 +--- +# prod profile +spring: + config: + activate: + on-profile: prod + data: + redis: + host: ${REDIS_PRIMARY_HOST_PROD} + port: 6379 diff --git a/common/src/main/resources/logback-spring.xml b/common/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..3f75031 --- /dev/null +++ b/common/src/main/resources/logback-spring.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + ./logs/app.log + + ./logs/app.%d{yyyy-MM-dd}.log + 30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docker/dev/docker-compose-dev.yml b/docker/dev/docker-compose-dev.yml index 1e00031..7ca932b 100644 --- a/docker/dev/docker-compose-dev.yml +++ b/docker/dev/docker-compose-dev.yml @@ -6,6 +6,7 @@ services: restart: on-failure:3 expose: - 8080 + - 8000 volumes: - ./notification/internal/app/logs:/logs environment: @@ -24,6 +25,7 @@ services: restart: on-failure:3 expose: - 8080 + - 8001 volumes: - ./notification/api/app/logs:/logs environment: @@ -76,9 +78,28 @@ services: networks: - dev-kokomen-net + promtail-notification-dev: + image: grafana/promtail + container_name: promtail-notification-dev + volumes: + - ./promtail/promtail.yaml:/etc/promtail/promtail.yaml + - ./notification/internal/app/logs:/logs/internal + - ./notification/api/app/logs:/logs/api + - promtail-notification-tmp:/tmp + command: + - "-config.file=/etc/promtail/promtail.yaml" + - "-config.expand-env=true" + restart: unless-stopped + environment: + TZ: Asia/Seoul + HOSTNAME: ${HOSTNAME} + networks: + - dev-kokomen-net + volumes: notification-mysql-data: notification-mysql-init: + promtail-notification-tmp: networks: dev-kokomen-net: diff --git a/docker/dev/promtail/promtail.yaml b/docker/dev/promtail/promtail.yaml new file mode 100644 index 0000000..1703787 --- /dev/null +++ b/docker/dev/promtail/promtail.yaml @@ -0,0 +1,34 @@ +server: + http_listen_port: 9080 + +positions: + filename: /tmp/positions.yaml + +clients: + - url: http://loki:3100/loki/api/v1/push + +scrape_configs: + - job_name: kokomen-notification-api-dev + static_configs: + - labels: + job: kokomen-notification-api + app: kokomen-notification-api + host: ${HOSTNAME} + __path__: /logs/api/app.log + pipeline_stages: + - regex: + expression: '\[.*?\] [\d\-:.\s]+ [^\[\]]+ \[.*?\]\s+(?P[A-Z]+)\s+[^\s]+' + - labels: + level: + - job_name: kokomen-notification-internal-dev + static_configs: + - labels: + job: kokomen-notification-internal + app: kokomen-notification-internal + host: ${HOSTNAME} + __path__: /logs/internal/app.log + pipeline_stages: + - regex: + expression: '\[.*?\] [\d\-:.\s]+ [^\[\]]+ \[.*?\]\s+(?P[A-Z]+)\s+[^\s]+' + - labels: + level: diff --git a/docker/prod/docker-compose-prod.yml b/docker/prod/docker-compose-prod.yml new file mode 100644 index 0000000..73dd388 --- /dev/null +++ b/docker/prod/docker-compose-prod.yml @@ -0,0 +1,86 @@ +services: + kokomen-notification-api: + image: samhap/kokomen-notification-api:prod + container_name: kokomen-notification-api + restart: on-failure:3 + expose: + - 8080 + ports: + - "8001:8001" + volumes: + - ./notification/api/logs:/logs + environment: + TZ: Asia/Seoul + JAVA_TOOL_OPTIONS: -Duser.timezone=Asia/Seoul + HOSTNAME: ${HOSTNAME} + SPRING_PROFILES_ACTIVE: prod + REDIS_PRIMARY_HOST_PROD: ${REDIS_PRIMARY_HOST_PROD} + NOTIFICATION_DATASOURCE_URL_PROD: ${NOTIFICATION_DATASOURCE_URL_PROD} + NOTIFICATION_DATASOURCE_USERNAME_PROD: ${NOTIFICATION_DATASOURCE_USERNAME_PROD} + NOTIFICATION_DATASOURCE_PASSWORD_PROD: ${NOTIFICATION_DATASOURCE_PASSWORD_PROD} + + kokomen-notification-internal: + image: samhap/kokomen-notification-internal:prod + container_name: kokomen-notification-internal + restart: on-failure:3 + expose: + - 8080 + ports: + - "8000:8000" + volumes: + - ./notification/internal/logs:/logs + environment: + TZ: Asia/Seoul + JAVA_TOOL_OPTIONS: -Duser.timezone=Asia/Seoul + HOSTNAME: ${HOSTNAME} + SPRING_PROFILES_ACTIVE: prod + REDIS_PRIMARY_HOST_PROD: ${REDIS_PRIMARY_HOST_PROD} + NOTIFICATION_DATASOURCE_URL_PROD: ${NOTIFICATION_DATASOURCE_URL_PROD} + NOTIFICATION_DATASOURCE_USERNAME_PROD: ${NOTIFICATION_DATASOURCE_USERNAME_PROD} + NOTIFICATION_DATASOURCE_PASSWORD_PROD: ${NOTIFICATION_DATASOURCE_PASSWORD_PROD} + + nginx: + image: nginx:1.28.0 + container_name: nginx + ports: + - "80:80" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf + - ./nginx/logs/api:/var/log/nginx/api + - ./nginx/logs/internal:/var/log/nginx/internal + restart: unless-stopped + environment: + TZ: Asia/Seoul + + node: + image: prom/node-exporter + container_name: node + restart: unless-stopped + pid: host + ports: + - 9100:9100 + command: + - '--path.rootfs=/host' + volumes: + - '/:/host:ro' + environment: + TZ: Asia/Seoul + + promtail: + image: grafana/promtail + container_name: promtail + volumes: + - ./promtail/promtail.yaml:/etc/promtail/promtail.yaml + - ./notification/api/logs:/logs/api + - ./notification/internal/logs:/logs/internal + - promtail-tmp:/tmp + command: + - "-config.file=/etc/promtail/promtail.yaml" + - "-config.expand-env=true" + restart: unless-stopped + environment: + TZ: Asia/Seoul + HOSTNAME: ${HOSTNAME} + +volumes: + promtail-tmp: diff --git a/docker/prod/nginx/nginx.conf b/docker/prod/nginx/nginx.conf new file mode 100644 index 0000000..e51b6b5 --- /dev/null +++ b/docker/prod/nginx/nginx.conf @@ -0,0 +1,66 @@ +events {} + +http { + charset utf-8; + + server_names_hash_bucket_size 128; # 서버 네임 길이 짧아서 안된다고함 + + log_format api_log '$request_id $remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" "$request_time" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + '"$ssl_protocol/$ssl_cipher" "$content_length" "$request_length"'; + + log_format internal_log '$http_x_requestid $remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" "$request_time" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + '"$ssl_protocol/$ssl_cipher" "$content_length" "$request_length"'; + + server { + listen 80 default_server; + server_name _; + + return 404; + } + + server { + listen 80; + access_log /var/log/nginx/api/access.log api_log; + + set_real_ip_from 10.0.0.0/16; + set_real_ip_from 43.203.50.14; + real_ip_header X-Forwarded-For; + real_ip_recursive on; + + server_name notification-api.kokomen.kr; + server_tokens off; + + location / { + set $backend "kokomen-notification-api:8080"; + proxy_pass http://$backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-RequestID $request_id; + resolver 127.0.0.11 valid=5s; + } + } + + server { + listen 80; + access_log /var/log/nginx/internal/access.log internal_log; + + set_real_ip_from 10.0.0.0/16; + real_ip_header X-Forwarded-For; + real_ip_recursive on; + + server_name internal-notification-private-alb-prod-190218547.ap-northeast-2.elb.amazonaws.com; + server_tokens off; + + location / { + set $backend "kokomen-notification-internal:8080"; + proxy_pass http://$backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + resolver 127.0.0.11 valid=5s; + } + } +} diff --git a/docker/prod/promtail/promtail.yaml b/docker/prod/promtail/promtail.yaml new file mode 100644 index 0000000..622655b --- /dev/null +++ b/docker/prod/promtail/promtail.yaml @@ -0,0 +1,34 @@ +server: + http_listen_port: 9080 + +positions: + filename: /tmp/positions.yaml + +clients: + - url: http://10.0.37.94:3100/loki/api/v1/push + +scrape_configs: + - job_name: kokomen-notification-api + static_configs: + - labels: + job: kokomen-notification-api + app: kokomen-notification-api + host: ${HOSTNAME} + __path__: /logs/api/app.log + pipeline_stages: + - regex: + expression: '\[.*?\] [\d\-:.\s]+ [^\[\]]+ \[.*?\]\s+(?P[A-Z]+)\s+[^\s]+' + - labels: + level: + - job_name: kokomen-notification-internal + static_configs: + - labels: + job: kokomen-notification-internal + app: kokomen-notification-internal + host: ${HOSTNAME} + __path__: /logs/internal/app.log + pipeline_stages: + - regex: + expression: '\[.*?\] [\d\-:.\s]+ [^\[\]]+ \[.*?\]\s+(?P[A-Z]+)\s+[^\s]+' + - labels: + level: diff --git a/domain/src/main/resources/application-domain.yml b/domain/src/main/resources/application-domain.yml index 1e73ec2..d70c17d 100644 --- a/domain/src/main/resources/application-domain.yml +++ b/domain/src/main/resources/application-domain.yml @@ -42,3 +42,19 @@ spring: show-sql: false hibernate: ddl-auto: validate +--- +# prod profile +spring: + config: + activate: + on-profile: prod + datasource: + url: ${NOTIFICATION_DATASOURCE_URL_PROD} + username: ${NOTIFICATION_DATASOURCE_USERNAME_PROD} + password: ${NOTIFICATION_DATASOURCE_PASSWORD_PROD} + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + database: mysql + show-sql: false + hibernate: + ddl-auto: validate diff --git a/internal/build.gradle b/internal/build.gradle index 45dcdee..376861f 100644 --- a/internal/build.gradle +++ b/internal/build.gradle @@ -8,6 +8,7 @@ ext { dependencies { implementation project(':domain') + implementation project(':common') implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' diff --git a/internal/local-internal-docker-compose.yml b/internal/local-internal-docker-compose.yml index b91c25f..295b201 100644 --- a/internal/local-internal-docker-compose.yml +++ b/internal/local-internal-docker-compose.yml @@ -7,6 +7,7 @@ services: no_cache: true ports: - 8090:8080 + - 8000:8000 environment: SPRING_PROFILES_ACTIVE: local networks: diff --git a/internal/src/main/java/com/samhap/kokomen/global/logging/LoggingFilter.java b/internal/src/main/java/com/samhap/kokomen/global/logging/LoggingFilter.java new file mode 100644 index 0000000..2b4e7af --- /dev/null +++ b/internal/src/main/java/com/samhap/kokomen/global/logging/LoggingFilter.java @@ -0,0 +1,65 @@ +package com.samhap.kokomen.global.logging; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.StopWatch; +import org.springframework.web.filter.OncePerRequestFilter; + +@Slf4j +@Component +public class LoggingFilter extends OncePerRequestFilter { + + private static final List WHITE_LIST = List.of( + "/favicon.ico", + "/docs/index.html", + "/metrics", + "/actuator/**"); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + + String requestId = readRequestId(request); + MDC.put("requestId", requestId); + + try { + filterChain.doFilter(request, response); + } finally { + stopWatch.stop(); + log.info("{} {} ({}) - {}ms", + request.getMethod(), + request.getRequestURI(), + HttpStatus.valueOf(response.getStatus()), + stopWatch.getTotalTimeMillis()); + + MDC.clear(); + } + } + + private String readRequestId(HttpServletRequest request) { + String requestId = request.getHeader("X-RequestID"); + if (requestId != null && !requestId.isEmpty()) { + return requestId; + } + return UUID.randomUUID().toString(); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + String requestURI = request.getRequestURI(); + AntPathMatcher antPathMatcher = new AntPathMatcher(); + return WHITE_LIST.stream().anyMatch(path -> antPathMatcher.match(path, requestURI)); + } +} diff --git a/internal/src/main/resources/application.yml b/internal/src/main/resources/application.yml index 801df34..31b55bf 100644 --- a/internal/src/main/resources/application.yml +++ b/internal/src/main/resources/application.yml @@ -1,13 +1,14 @@ +management: + server: + address: 0.0.0.0 + port: 8000 spring: + application: + name: kokomen-notification-internal profiles: - include: domain + include: + - domain + - common jackson: property-naming-strategy: SNAKE_CASE default-property-inclusion: non_null - ---- -# local profile -spring: - config: - activate: - on-profile: local diff --git a/internal/src/test/resources/application.yml b/internal/src/test/resources/application.yml index 47869b4..71202a2 100644 --- a/internal/src/test/resources/application.yml +++ b/internal/src/test/resources/application.yml @@ -1,6 +1,8 @@ spring: profiles: - include: domain-test + include: + - domain-test + - common-test main: lazy-initialization: true jackson: