diff --git a/.github/workflows/backend-cd.yml b/.github/workflows/backend-cd.yml index e8ceedae..e8036729 100644 --- a/.github/workflows/backend-cd.yml +++ b/.github/workflows/backend-cd.yml @@ -78,6 +78,12 @@ jobs: - name: ✨ Checkout repository uses: actions/checkout@v5 + - name: πŸ—‚οΈ Grafana ν™˜κ²½λ³€μˆ˜ μ„€μ • + run: | + echo "GRAFANA_ADMIN_USER=admin" > ./docker/.env + echo "GRAFANA_ADMIN_PASSWORD=${{ secrets.GRAFANA_ADMIN_PASSWORD }}" >> ./docker/.env + shell: bash + - name: ✨ 배포 슀크립트 μ‹€ν–‰ run: | chmod +x deploy.sh diff --git a/.gitignore b/.gitignore index a0cb7de3..aac9bcd6 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ out/ ### Spring *.yml +!docker/prometheus.yml +!docker/grafana/**/*.yml .editorconfig diff --git a/build.gradle b/build.gradle index bf335544..25d76912 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-actuator' + runtimeOnly 'io.micrometer:micrometer-registry-prometheus' //Test testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/deploy.sh b/deploy.sh index 6e69a6d0..12287fa5 100644 --- a/deploy.sh +++ b/deploy.sh @@ -32,6 +32,11 @@ if ! ${COMPOSE} pull; then fi echo "βœ… μƒˆλ‘œμš΄ 이미지가 μ„±κ³΅μ μœΌλ‘œ pullλ˜μ—ˆμŠ΅λ‹ˆλ‹€." +# Prometheus & Grafana μ‹€ν–‰ (μ„€μ • λ³€κ²½ μ‹œ μžλ™ 반영) +echo "λͺ¨λ‹ˆν„°λ§ μ„œλΉ„μŠ€ μ‹œμž‘ 쀑..." +${COMPOSE} up -d prometheus grafana +echo "βœ… Prometheus & Grafanaκ°€ μ‹œμž‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€." + echo "μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” 이미지 정리 쀑..." sudo docker image prune -f diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index adca8632..0a6c4e82 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,6 +1,4 @@ # docker-compose.yml -version: '3.8' - services: blue: image: linkivingsofa/core:latest @@ -52,6 +50,62 @@ services: max-size: "10m" max-file: "3" + prometheus: + image: prom/prometheus:v2.53.0 + container_name: prometheus + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus_data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--storage.tsdb.retention.time=3d' + - '--web.enable-lifecycle' + restart: unless-stopped + networks: + - app-network + deploy: + resources: + limits: + memory: 256M + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + grafana: + image: grafana/grafana:11.1.0 + container_name: grafana + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning:ro + - ./grafana/dashboards:/var/lib/grafana/dashboards:ro + environment: + - GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER:-admin} + - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD} + - GF_USERS_ALLOW_SIGN_UP=false + ports: + - '3001:3000' + restart: unless-stopped + networks: + - app-network + deploy: + resources: + limits: + memory: 256M + depends_on: + - prometheus + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +volumes: + prometheus_data: + grafana_data: + networks: app-network: driver: bridge diff --git a/docker/grafana/dashboards/http-overview.json b/docker/grafana/dashboards/http-overview.json new file mode 100644 index 00000000..6fdedbce --- /dev/null +++ b/docker/grafana/dashboards/http-overview.json @@ -0,0 +1,635 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "sum(rate(http_server_requests_seconds_count{application=\"linkiving-core\"}[1m]))", + "legendFormat": "Total RPS", + "refId": "A" + } + ], + "title": "HTTP Request Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "sum(rate(http_server_requests_seconds_count{application=\"linkiving-core\", status=~\"5..\"}[1m])) / sum(rate(http_server_requests_seconds_count{application=\"linkiving-core\"}[1m])) * 100", + "legendFormat": "5xx Error Rate", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "sum(rate(http_server_requests_seconds_count{application=\"linkiving-core\", status=~\"4..\"}[1m])) / sum(rate(http_server_requests_seconds_count{application=\"linkiving-core\"}[1m])) * 100", + "legendFormat": "4xx Error Rate", + "refId": "B" + } + ], + "title": "HTTP Error Rate (%)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "histogram_quantile(0.50, sum(rate(http_server_requests_seconds_bucket{application=\"linkiving-core\"}[1m])) by (le))", + "legendFormat": "p50", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{application=\"linkiving-core\"}[1m])) by (le))", + "legendFormat": "p95", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket{application=\"linkiving-core\"}[1m])) by (le))", + "legendFormat": "p99", + "refId": "C" + } + ], + "title": "HTTP Response Time (Percentiles)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "2.." + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "4.." + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "5.." + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "sum(rate(http_server_requests_seconds_count{application=\"linkiving-core\"}[1m])) by (status)", + "legendFormat": "{{status}}", + "refId": "A" + } + ], + "title": "HTTP Requests by Status Code", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "topk(10, avg(rate(http_server_requests_seconds_sum{application=\"linkiving-core\"}[1m])) by (uri) / avg(rate(http_server_requests_seconds_count{application=\"linkiving-core\"}[1m])) by (uri))", + "legendFormat": "{{uri}}", + "refId": "A" + } + ], + "title": "Top 10 Slowest Endpoints (Avg Response Time)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "topk(10, sum(rate(http_server_requests_seconds_count{application=\"linkiving-core\"}[1m])) by (uri))", + "legendFormat": "{{uri}}", + "refId": "A" + } + ], + "title": "Top 10 Most Requested Endpoints", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": ["http", "spring-boot"], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "Asia/Seoul", + "title": "Linkiving HTTP Overview", + "uid": "linkiving-http-overview", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/docker/grafana/provisioning/dashboards/dashboards.yml b/docker/grafana/provisioning/dashboards/dashboards.yml new file mode 100644 index 00000000..391325b2 --- /dev/null +++ b/docker/grafana/provisioning/dashboards/dashboards.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: 'default' + orgId: 1 + folder: '' + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards \ No newline at end of file diff --git a/docker/grafana/provisioning/datasources/prometheus.yml b/docker/grafana/provisioning/datasources/prometheus.yml new file mode 100644 index 00000000..12914e77 --- /dev/null +++ b/docker/grafana/provisioning/datasources/prometheus.yml @@ -0,0 +1,10 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + uid: prometheus + isDefault: true + editable: false \ No newline at end of file diff --git a/docker/prometheus.yml b/docker/prometheus.yml new file mode 100644 index 00000000..86d2b091 --- /dev/null +++ b/docker/prometheus.yml @@ -0,0 +1,13 @@ +global: + scrape_interval: 30s + evaluation_interval: 30s + +scrape_configs: + - job_name: 'linkiving-core' + metrics_path: '/actuator/prometheus' + static_configs: + - targets: + - 'blue:8080' + - 'green:8080' + labels: + application: 'linkiving-core' \ No newline at end of file diff --git a/src/main/java/com/sofa/linkiving/security/auth/config/SecurityConstants.java b/src/main/java/com/sofa/linkiving/security/auth/config/SecurityConstants.java index 2df653a2..34f0a59d 100644 --- a/src/main/java/com/sofa/linkiving/security/auth/config/SecurityConstants.java +++ b/src/main/java/com/sofa/linkiving/security/auth/config/SecurityConstants.java @@ -5,6 +5,10 @@ public abstract class SecurityConstants { /* swagger */ "/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources", "/swagger-resources/**", + /* actuator */ + "/actuator/health", + "/actuator/prometheus", + /* health check */ "/health-check",