diff --git a/motionit/.gitignore b/motionit/.gitignore index 5d2c623..db1cd77 100644 --- a/motionit/.gitignore +++ b/motionit/.gitignore @@ -39,6 +39,9 @@ out/ ### VS Code ### .vscode/ +### log ### +/logs + ### custom ### db_dev.mv.db db_dev.trace.db diff --git a/motionit/build.gradle.kts b/motionit/build.gradle.kts index 0563eae..fbf53f4 100644 --- a/motionit/build.gradle.kts +++ b/motionit/build.gradle.kts @@ -62,6 +62,10 @@ dependencies { // OpenAI (GPT) implementation("com.theokanning.openai-gpt3-java:service:0.18.2") + + // actuator, micrometer + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("io.micrometer:micrometer-registry-prometheus") } diff --git a/motionit/src/main/java/com/back/motionit/domain/challenge/video/external/youtube/YoutubeMetadataClient.java b/motionit/src/main/java/com/back/motionit/domain/challenge/video/external/youtube/YoutubeMetadataClient.java index eee97ff..6b22e2b 100644 --- a/motionit/src/main/java/com/back/motionit/domain/challenge/video/external/youtube/YoutubeMetadataClient.java +++ b/motionit/src/main/java/com/back/motionit/domain/challenge/video/external/youtube/YoutubeMetadataClient.java @@ -28,14 +28,10 @@ public YoutubeVideoMetadata fetchMetedata(String youtubeUrl) { // YouTube Data API 호좜 URL ꡬ성 String url = YOUTUBE_API_URL + "?id=" + videoId + "&part=snippet,contentDetails&key=" + apiKey; - System.out.println("πŸ”₯ [YouTube API 호좜 URL] " + url); - // TODO: timeout μ„€μ • λ“± μΆ”κ°€ ꡬ성 ν•„μš” RestTemplate restTemplate = new RestTemplate(); Map response = restTemplate.getForObject(url, Map.class); - System.out.println("πŸ”₯ [YouTube API 응닡] " + response); - // μ‘λ‹΅μ—μ„œ ν•„μš”ν•œ 데이터 μΆ”μΆœ List items = (List)response.get("items"); if (items == null || items.isEmpty()) { diff --git a/motionit/src/main/java/com/back/motionit/global/init/PerfDataInitializer.java b/motionit/src/main/java/com/back/motionit/global/init/PerfDataInitializer.java new file mode 100644 index 0000000..76ea6d2 --- /dev/null +++ b/motionit/src/main/java/com/back/motionit/global/init/PerfDataInitializer.java @@ -0,0 +1,136 @@ +package com.back.motionit.global.init; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +import com.back.motionit.domain.challenge.mission.entity.ChallengeMissionStatus; +import com.back.motionit.domain.challenge.mission.repository.ChallengeMissionStatusRepository; +import com.back.motionit.domain.challenge.participant.entity.ChallengeParticipant; +import com.back.motionit.domain.challenge.participant.entity.ChallengeParticipantRole; +import com.back.motionit.domain.challenge.participant.repository.ChallengeParticipantRepository; +import com.back.motionit.domain.challenge.room.entity.ChallengeRoom; +import com.back.motionit.domain.challenge.room.repository.ChallengeRoomRepository; +import com.back.motionit.domain.challenge.video.entity.ChallengeVideo; +import com.back.motionit.domain.challenge.video.entity.OpenStatus; +import com.back.motionit.domain.challenge.video.repository.ChallengeVideoRepository; +import com.back.motionit.domain.user.entity.LoginType; +import com.back.motionit.domain.user.entity.User; +import com.back.motionit.domain.user.repository.UserRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Configuration +@Profile("perf") // βœ… perf ν”„λ‘œνŒŒμΌμ—μ„œλ§Œ μž‘λ™ +@RequiredArgsConstructor +public class PerfDataInitializer { + private final UserRepository userRepository; + private final ChallengeRoomRepository challengeRoomRepository; + private final ChallengeParticipantRepository challengeParticipantRepository; + private final ChallengeVideoRepository challengeVideoRepository; + private final ChallengeMissionStatusRepository challengeMissionStatusRepository; + + @Bean + public ApplicationRunner initPerfDummyData() { + return args -> { + if (challengeRoomRepository.count() > 0) { + log.info("βœ… perf 데이터 이미 쑴재. μ΄ˆκΈ°ν™” μŠ€ν‚΅"); + return; + } + + log.info("πŸš€ perf ν”„λ‘œνŒŒμΌμš© 더미 데이터 생성 μ‹œμž‘"); + + // 1️⃣ μœ μ € 생성 + User host = userRepository.save(User.builder() + .kakaoId(9001L) + .email("perf_host@example.com") + .nickname("PerfHost") + .password("1234") + .loginType(LoginType.KAKAO) + .userProfile("https://picsum.photos/100?perf1") + .build()); + + List users = IntStream.range(1, 11) + .mapToObj(i -> userRepository.save(User.builder() + .kakaoId(9100L + i) + .email("perf_user" + i + "@example.com") + .nickname("PerfUser" + i) + .password("pass" + i) + .loginType(LoginType.KAKAO) + .userProfile("https://picsum.photos/100?perf" + (i + 1)) + .build())) + .collect(Collectors.toList()); + + // 2️⃣ μ±Œλ¦°μ§€ λ°© 생성 + ChallengeRoom room = challengeRoomRepository.save(new ChallengeRoom( + host, + "πŸ”₯ K6 λΆ€ν•˜ν…ŒμŠ€νŠΈ μ „μš© λ°©", + "λΆ€ν•˜ν…ŒμŠ€νŠΈμš© λ°©μž…λ‹ˆλ‹€.", + 50, + OpenStatus.OPEN, + LocalDateTime.now().minusDays(1), + LocalDateTime.now().plusDays(7), + "images/test/perf_room.png", + null, + new ArrayList<>(), + new ArrayList<>() + )); + + // 3️⃣ μ°Έκ°€μž 생성 + ChallengeParticipant hostParticipant = challengeParticipantRepository.save( + ChallengeParticipant.builder() + .user(host) + .challengeRoom(room) + .role(ChallengeParticipantRole.HOST) + .quited(false) + .challengeStatus(false) + .build() + ); + + List participants = users.stream() + .map(u -> ChallengeParticipant.builder() + .user(u) + .challengeRoom(room) + .role(ChallengeParticipantRole.NORMAL) + .quited(false) + .challengeStatus(false) + .build()) + .collect(Collectors.toList()); + challengeParticipantRepository.saveAll(participants); + + // 4️⃣ 였늘의 μ˜μƒ 생성 + ChallengeVideo todayVideo = challengeVideoRepository.save(ChallengeVideo.builder() + .challengeRoom(room) + .user(host) + .youtubeVideoId("yt_perf_today") + .title("였늘의 퍼포먼슀 ν…ŒμŠ€νŠΈ μ˜μƒ") + .thumbnailUrl("https://img.youtube.com/vi/yt_perf_today/0.jpg") + .duration(300) + .uploadDate(LocalDate.now()) + .isTodayMission(true) + .build()); + + // 5️⃣ λ―Έμ…˜ μƒνƒœ + List missions = new ArrayList<>(); + missions.add(new ChallengeMissionStatus(hostParticipant, LocalDate.now())); + + for (ChallengeParticipant p : participants) { + missions.add(new ChallengeMissionStatus(p, LocalDate.now())); + } + challengeMissionStatusRepository.saveAll(missions); + + log.info("🎯 perf 더미데이터 생성 μ™„λ£Œ! [roomId={}, users={}, videoId={}]", + room.getId(), users.size(), todayVideo.getId()); + }; + } +} diff --git a/motionit/src/main/java/com/back/motionit/global/logging/PerformanceMonitoringAspect.java b/motionit/src/main/java/com/back/motionit/global/logging/PerformanceMonitoringAspect.java new file mode 100644 index 0000000..ac3d888 --- /dev/null +++ b/motionit/src/main/java/com/back/motionit/global/logging/PerformanceMonitoringAspect.java @@ -0,0 +1,61 @@ +package com.back.motionit.global.logging; + +import java.util.concurrent.TimeUnit; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class PerformanceMonitoringAspect { + + private final MeterRegistry meterRegistry; + + @Around("execution(* com.back.motionit.domain.challenge..controller..*(..)) || " + + "execution(* com.back.motionit.domain.challenge..service..*(..)) || " + + "execution(* com.back.motionit.domain.challenge..repository..*(..))") + public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { + long start = System.nanoTime(); + Object result = joinPoint.proceed(); + long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + + MethodSignature signature = (MethodSignature)joinPoint.getSignature(); + String className = signature.getDeclaringType().getSimpleName(); + String methodName = signature.getName(); + String layer = getLayer(className); + + Timer timer = Timer.builder("method.execution.time") + .description("Method execution time in milliseconds") + .tags("layer", layer, "class", className, "method", methodName) + .register(meterRegistry); + timer.record(durationMs, TimeUnit.MILLISECONDS); + + if (log.isInfoEnabled()) { + log.info("[PERF][{}] {}.{} executed in {} ms", layer, className, methodName, durationMs); + } + return result; + } + + private String getLayer(String className) { + if (className.toLowerCase().contains("controller")) { + return "controller"; + } + if (className.toLowerCase().contains("service")) { + return "service"; + } + if (className.toLowerCase().contains("repository")) { + return "repository"; + } + return "other"; + } +} diff --git a/motionit/src/main/java/com/back/motionit/security/CustomAuthenticationFilter.java b/motionit/src/main/java/com/back/motionit/security/CustomAuthenticationFilter.java index 911dbb7..e992449 100644 --- a/motionit/src/main/java/com/back/motionit/security/CustomAuthenticationFilter.java +++ b/motionit/src/main/java/com/back/motionit/security/CustomAuthenticationFilter.java @@ -70,18 +70,25 @@ private void authenticate(HttpServletRequest request, HttpServletResponse respon return; } - String accessToken = requestContext.getCookieValue("accessToken", ""); - if (jwtTokenProvider.isExpired(accessToken)) { - throw new BusinessException(AuthErrorCode.TOKEN_EXPIRED); - } - + // Authorization 헀더 확인 String headerAuthorization = requestContext.getHeader("Authorization", ""); + String accessToken = null; - if (!headerAuthorization.isBlank()) { - if (!headerAuthorization.startsWith(BEARER_PREFIX)) { - throw new BusinessException(AuthErrorCode.AUTH_HEADER_INVALID_SCHEME); - } + if (!headerAuthorization.isBlank() && headerAuthorization.startsWith(BEARER_PREFIX)) { accessToken = headerAuthorization.substring(BEARER_PREFIX.length()); + } else { + // 헀더 μ—†μœΌλ©΄ μΏ ν‚€μ—μ„œ accessToken κ°€μ Έμ˜€κΈ° (κΈ°μ‘΄ 둜직 κ·ΈλŒ€λ‘œ) + accessToken = requestContext.getCookieValue("accessToken", ""); + } + + // 토큰 쑴재 μ—¬λΆ€ 확인 + if (accessToken == null || accessToken.isBlank()) { + throw new BusinessException(AuthErrorCode.TOKEN_INVALID); + } + + // 만료 확인 + if (jwtTokenProvider.isExpired(accessToken)) { + throw new BusinessException(AuthErrorCode.TOKEN_EXPIRED); } Map payload = socialAuthService.payloadOrNull(accessToken); diff --git a/motionit/src/main/java/com/back/motionit/security/config/SecurityConfig.java b/motionit/src/main/java/com/back/motionit/security/config/SecurityConfig.java index 5a6688c..beffdd3 100644 --- a/motionit/src/main/java/com/back/motionit/security/config/SecurityConfig.java +++ b/motionit/src/main/java/com/back/motionit/security/config/SecurityConfig.java @@ -46,6 +46,11 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/api/v1/storage/**").permitAll() .requestMatchers("/api/v1/auth/**").permitAll() .requestMatchers("/ws/**").permitAll() + .requestMatchers( + "/actuator/health", + "/actuator/metrics/**", + "/actuator/prometheus" + ).permitAll() // λͺ¨λ‹ˆν„°λ§/Actuator κ΄€λ ¨ .anyRequest().authenticated()) .csrf((csrf) -> csrf.disable()) .headers((headers) -> headers diff --git a/perf/.gitignore b/perf/.gitignore new file mode 100644 index 0000000..3a8adc9 --- /dev/null +++ b/perf/.gitignore @@ -0,0 +1,9 @@ +# ν™˜κ²½ λ³€μˆ˜ 파일 +.env + +# k6 μ‹€ν–‰ 둜그 +logs/ + +# IDE μΊμ‹œ / μž„μ‹œνŒŒμΌ +.DS_Store +*.log \ No newline at end of file diff --git a/perf/docker-compose.yml b/perf/docker-compose.yml new file mode 100644 index 0000000..b4eff77 --- /dev/null +++ b/perf/docker-compose.yml @@ -0,0 +1,55 @@ +version: "3.8" + +services: + prometheus: + image: prom/prometheus:latest + container_name: perf_prometheus + ports: + - "9090:9090" + command: + - --web.enable-remote-write-receiver + - --enable-feature=native-histograms + - --config.file=/etc/prometheus/prometheus.yml + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + networks: + - monitoring + + grafana: + image: grafana/grafana:latest + container_name: perf_grafana + ports: + - "3300:3000" + volumes: + - ./grafana-provisioning:/etc/grafana/provisioning + - ./grafana-dashboard:/var/lib/grafana/dashboards + depends_on: + - prometheus + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer + networks: + - monitoring + + k6: + image: grafana/k6:latest + container_name: perf_k6 + volumes: + - ./k6-scripts:/scripts + environment: + - JWT_SECRET=${JWT_SECRET} + - K6_PROMETHEUS_RW_SERVER_URL=http://prometheus:9090/api/v1/write + - K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM=true + command: run -o experimental-prometheus-rw /scripts/loadtest.js + extra_hosts: + - "host.docker.internal:host-gateway" + depends_on: + - prometheus + networks: + - monitoring + +networks: + monitoring: + driver: bridge diff --git a/perf/grafana-dashboard/k6.json b/perf/grafana-dashboard/k6.json new file mode 100644 index 0000000..b24457d --- /dev/null +++ b/perf/grafana-dashboard/k6.json @@ -0,0 +1,2079 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "10.0.0" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "" + }, + { + "type": "panel", + "id": "text", + "name": "Text", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "Visualize k6 OSS results stored in Prometheus", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 19665, + "graphTooltip": 2, + "id": null, + "links": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Grafana k6 OSS Docs: Prometheus Remote Write", + "tooltip": "Open docs in a new tab", + "type": "link", + "url": "https://k6.io/docs/results-output/real-time/prometheus-remote-write/" + } + ], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "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 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "http_req_s_errors" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + }, + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "unit", + "value": "reqps" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "http_req_s" + }, + "properties": [ + { + "id": "unit", + "value": "reqps" + }, + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "vus" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed" + } + }, + { + "id": "unit", + "value": "VUs" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "http_req_duration_[a-zA-Z0-9_]+" + }, + "properties": [ + { + "id": "unit", + "value": "s" + }, + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 11, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(k6_vus{testid=~\"$testid\"})", + "instant": false, + "legendFormat": "vus", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_duration_$quantile_stat{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "http_req_duration_$quantile_stat", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(irate(k6_http_reqs_total{testid=~\"$testid\"}[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "http_req_s", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg(round(k6_http_req_failed_rate{testid=~\"$testid\"}, 0.1)*100)", + "hide": true, + "instant": false, + "legendFormat": "http_req_failed", + "range": true, + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(irate(k6_http_reqs_total{testid=~\"$testid\", expected_response=\"false\"}[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "http_req_s_errors", + "range": true, + "refId": "D" + } + ], + "title": "Performance Overview", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 11 + }, + "id": 1, + "panels": [], + "title": "Performance Overview", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 12 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(k6_http_reqs_total{testid=~\"$testid\"})", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "HTTP requests", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "red", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 6, + "y": 12 + }, + "id": 22, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(k6_http_reqs_total{testid=~\"$testid\", expected_response=\"false\"})", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "HTTP request failures", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 12, + "y": 12 + }, + "id": 20, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(irate(k6_http_reqs_total{testid=~\"$testid\"}[$__rate_interval]))", + "instant": false, + "interval": "", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Peak RPS", + "transformations": [ + { + "id": "reduce", + "options": {} + } + ], + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Select a different Stat to change the query", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 18, + "y": 12 + }, + "id": 21, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_duration_$quantile_stat{testid=~\"$testid\"})", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "HTTP Request Duration", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "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": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 15 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg(irate(k6_data_sent_total{testid=~\"$testid\"}[$__rate_interval]))", + "instant": false, + "legendFormat": "data_sent", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "avg(irate(k6_data_received_total{testid=~\"$testid\"}[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "data_received", + "range": true, + "refId": "B" + } + ], + "title": "Transfer Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "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": [ + { + "matcher": { + "id": "byName", + "options": "dropped_iterations" + }, + "properties": [ + { + "id": "unit", + "value": "none" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 15 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg(k6_iteration_duration_$quantile_stat{testid=~\"$testid\"})", + "instant": false, + "legendFormat": "iteration_duration_$quantile_stat", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "avg(k6_dropped_iterations_total{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "dropped_iterations", + "range": true, + "refId": "B" + } + ], + "title": "Iterations", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 16, + "panels": [], + "title": "HTTP", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Select a different Stat to change the query\n\nHTTP-specific built-in metrics", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "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" + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "http_req_duration_[a-zA-Z0-9_]+" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 24 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_blocked_$quantile_stat{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "http_req_blocked_$quantile_stat", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_tls_handshaking_$quantile_stat{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "http_req_tls_handshaking_$quantile_stat", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_sending_$quantile_stat{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "http_req_sending_$quantile_stat", + "range": true, + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_waiting_$quantile_stat{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "http_req_waiting_$quantile_stat", + "range": true, + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_receiving_$quantile_stat{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "http_req_receiving_$quantile_stat", + "range": true, + "refId": "F" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_duration_$quantile_stat{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "http_req_duration_$quantile_stat", + "range": true, + "refId": "A" + } + ], + "title": "HTTP Latency Timings", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Select a different Stat to change the query", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "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" + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "errors_http_req_duration_[a-zA-Z0-9_]+" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "success_http_req_duration_[a-zA-Z0-9_]+" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "http_req_duration_[a-zA-Z0-9_]+" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "http_req_duration_[a-zA-Z0-9_]+" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 24 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_duration_$quantile_stat{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "http_req_duration_$quantile_stat", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_duration_$quantile_stat{testid=~\"$testid\", expected_response=\"true\"})", + "instant": false, + "legendFormat": "success_http_req_duration_$quantile_stat", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_duration_$quantile_stat{testid=~\"$testid\", expected_response=\"false\"})", + "hide": false, + "instant": false, + "legendFormat": "errors_http_req_duration_$quantile_stat", + "range": true, + "refId": "B" + } + ], + "title": "HTTP Latency Stats", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "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" + } + ] + }, + "unit": "reqps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "http_req_s_errors" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "http_req_s" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "http_req_s_success" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + }, + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 24 + }, + "id": 18, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(irate(k6_http_reqs_total{testid=~\"$testid\"}[$__rate_interval]))", + "instant": false, + "legendFormat": "http_req_s", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(irate(k6_http_reqs_total{testid=~\"$testid\", expected_response=\"false\"}[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "http_req_s_errors", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(irate(k6_http_reqs_total{testid=~\"$testid\", expected_response=\"true\"}[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "http_req_s_success", + "range": true, + "refId": "C" + } + ], + "title": "HTTP Request Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "min/max/p95/p99 depends on the available Quantile Stats", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "name" + }, + "properties": [ + { + "id": "filterable", + "value": false + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "method" + }, + "properties": [ + { + "id": "filterable", + "value": false + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "filterable", + "value": false + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "min" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "max" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "p95" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "p99" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 32 + }, + "id": 17, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": true, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 2, + "showHeader": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg by(name, method, status) (k6_http_req_duration_min{testid=~\"$testid\"})", + "format": "table", + "hide": false, + "instant": false, + "legendFormat": "min", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg by(name, method, status) (k6_http_req_duration_max{testid=~\"$testid\"})", + "format": "table", + "hide": false, + "instant": false, + "legendFormat": "max", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg by(name, method, status) (k6_http_req_duration_p95{testid=~\"$testid\"})", + "format": "table", + "hide": false, + "instant": false, + "legendFormat": "p95", + "range": true, + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg by(name, method, status) (k6_http_req_duration_p99{testid=~\"$testid\"})", + "format": "table", + "hide": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "E" + } + ], + "title": "Requests by URL", + "transformations": [ + { + "id": "merge", + "options": {} + }, + { + "id": "groupBy", + "options": { + "fields": { + "Value #B": { + "aggregations": [ + "min" + ], + "operation": "aggregate" + }, + "Value #C": { + "aggregations": [ + "max" + ], + "operation": "aggregate" + }, + "Value #D": { + "aggregations": [ + "mean" + ], + "operation": "aggregate" + }, + "Value #E": { + "aggregations": [ + "mean" + ], + "operation": "aggregate" + }, + "method": { + "aggregations": [], + "operation": "groupby" + }, + "name": { + "aggregations": [], + "operation": "groupby" + }, + "status": { + "aggregations": [], + "operation": "groupby" + } + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "indexByName": { + "Time": 0, + "Value #B": 4, + "Value #C": 5, + "Value #D": 6, + "Value #E": 7, + "method": 2, + "name": 1, + "status": 3 + }, + "renameByName": { + "Value #B": "min", + "Value #B (min)": "min", + "Value #C": "max", + "Value #C (max)": "max", + "Value #D": "p95", + "Value #D (mean)": "p95", + "Value #E": "p99", + "Value #E (mean)": "p99" + } + } + } + ], + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 39 + }, + "id": 11, + "panels": [], + "title": "Checks", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Success Rate" + }, + "properties": [ + { + "id": "custom.hidden", + "value": false + }, + { + "id": "unit", + "value": "%" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Value (mean)" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "check" + }, + "properties": [ + { + "id": "filterable", + "value": false + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 40 + }, + "id": 12, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": true, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 2, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Value (count)" + } + ] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "round(k6_checks_rate{testid=~\"$testid\"}, 0.1)", + "format": "table", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Checks list", + "transformations": [ + { + "id": "labelsToFields", + "options": { + "keepLabels": [ + "__name__", + "check" + ], + "mode": "columns" + } + }, + { + "id": "groupBy", + "options": { + "fields": { + "Value": { + "aggregations": [ + "mean" + ], + "operation": "aggregate" + }, + "check": { + "aggregations": [], + "operation": "groupby" + }, + "k6_checks_rate": { + "aggregations": [ + "sum", + "count" + ], + "operation": "aggregate" + } + } + } + }, + { + "id": "calculateField", + "options": { + "alias": "Success Rate", + "binary": { + "left": "Value (mean)", + "operator": "*", + "reducer": "sum", + "right": "100" + }, + "mode": "binary", + "reduce": { + "reducer": "sum" + } + } + }, + { + "id": "convertFieldType", + "options": { + "conversions": [], + "fields": {} + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Filter by check name to query a particular check", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMax": 100, + "axisSoftMin": 0, + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "%" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 40 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "avg(round(k6_checks_rate{testid=~\"$testid\"}, 0.1)*100)", + "instant": false, + "legendFormat": "k6_checks_rate", + "range": true, + "refId": "A" + } + ], + "title": "Checks Success Rate (aggregate individual checks)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 48 + }, + "id": 23, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "### Visualize other k6 results \n\nAt the top of the dashboard, click `Add` and select `Visualization` from the dropdown menu. Choose the visualization type and input the PromQL queries for the `k6_` metric(s).\n\nAlternatively, click on the `Explore` icon on the menu bar and input the queries for the `k6_` metric(s). From `Explore`, you can add new Panels to this dashboard. \n\nNote that all k6 metrics are prefixed with the `k6_` namespace when sent to Prometheus.", + "mode": "markdown" + }, + "type": "text" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [ + "prometheus", + "k6" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "prometheus", + "value": "${DS_PROMETHEUS}" + }, + "description": "Choose a Prometheus Data Source", + "hide": 0, + "includeAll": false, + "label": "Prometheus DS", + "multi": false, + "name": "DS_PROMETHEUS", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "definition": "label_values(testid)", + "description": "Filter by \"testid\" tag. Define it by tagging: k6 run --tag testid=xyz", + "hide": 0, + "includeAll": true, + "label": "Test ID", + "multi": true, + "name": "testid", + "options": [], + "query": { + "query": "label_values(testid)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "definition": "metrics(k6_http_req_duration_)", + "description": "Statistic for Trend Metrics Queries. The available options depend on the values of the K6_PROMETHEUS_RW_TREND_STATS setting.", + "hide": 0, + "includeAll": false, + "label": "Trend Metrics Query", + "multi": false, + "name": "quantile_stat", + "options": [], + "query": { + "query": "metrics(k6_http_req_duration_)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "/http_req_duration_(min|max|count|sum|avg|med|p[0-9]+)/g", + "skipUrlSync": false, + "sort": 2, + "type": "query" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Adhoc filters are applied to all panels. To enable it, go to Dashboard Settings / Variables / adhoc_filter and select the target Prometheus data source.", + "filters": [], + "hide": 0, + "label": "AdhocFilter", + "name": "adhoc_filter", + "skipUrlSync": false, + "type": "adhoc" + } + ] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timeRangeUpdatedDuringEditOrView": false, + "timepicker": {}, + "timezone": "", + "title": "k6 Prometheus", + "uid": "ccbb2351-2ae2-462f-ae0e-f2c893ad1028", + "version": 3, + "weekStart": "" +} \ No newline at end of file diff --git a/perf/grafana-provisioning/dashboards/dashboards.yml b/perf/grafana-provisioning/dashboards/dashboards.yml new file mode 100644 index 0000000..f0551fb --- /dev/null +++ b/perf/grafana-provisioning/dashboards/dashboards.yml @@ -0,0 +1,12 @@ +apiVersion: 1 + +providers: + - name: "k6 Load Test Dashboard" + orgId: 1 + folder: "" + type: file + disableDeletion: false + updateIntervalSeconds: 10 + allowUiUpdates: true + options: + path: /var/lib/grafana/dashboards diff --git a/perf/grafana-provisioning/datasources/datasources.yml b/perf/grafana-provisioning/datasources/datasources.yml new file mode 100644 index 0000000..86fd346 --- /dev/null +++ b/perf/grafana-provisioning/datasources/datasources.yml @@ -0,0 +1,8 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true diff --git a/perf/k6-scripts/loadtest.js b/perf/k6-scripts/loadtest.js new file mode 100644 index 0000000..93bf61e --- /dev/null +++ b/perf/k6-scripts/loadtest.js @@ -0,0 +1,36 @@ +import { generateJWT } from "./util.js"; +import { getVideosToday, postCompleteMission, getTodayMissions } from "./scenarios/index.js"; +import { sleep } from "k6"; + +export const options = { + vus: 10, // λ™μ‹œ μœ μ € 수 + duration: "10s", // ν…ŒμŠ€νŠΈ μ‹œκ°„ +}; + +const BASE_URL = "http://host.docker.internal:8080"; +const ROOM_ID = 1; + +export function setup() { + const secret = __ENV.JWT_SECRET; + const tokens = Array.from({ length: 10 }, (_, i) => + generateJWT({ id: i + 1, nickname: `PerfUser${i + 1}` }, secret) + ); + return { tokens }; +} + +export default function (data) { + const token = data.tokens[__VU - 1]; + const testId = __ENV.TEST_ID || "default-test"; + const r = Math.random(); + + // 톡합 μ‹œλ‚˜λ¦¬μ˜€ + if (r < 0.33) { + getVideosToday(BASE_URL, token, testId); + } else if (r < 0.66) { + getTodayMissions(BASE_URL, token, testId, ROOM_ID); + } else { + postCompleteMission(BASE_URL, token, testId, ROOM_ID); + } + + sleep(1); +} \ No newline at end of file diff --git a/perf/k6-scripts/scenarios/getTodayMissions.js b/perf/k6-scripts/scenarios/getTodayMissions.js new file mode 100644 index 0000000..060ec51 --- /dev/null +++ b/perf/k6-scripts/scenarios/getTodayMissions.js @@ -0,0 +1,16 @@ +import http from "k6/http"; +import { check } from "k6"; + +export function getTodayMissions(baseUrl, token, testId, roomId) { + const res = http.get( + `${baseUrl}/api/v1/challenge/rooms/${roomId}/missions/today`, + { + headers: { Authorization: `Bearer ${token}` }, + tags: { api: "getTodayMissions", test_id: testId }, + } + ); + + check(res, { + "getTodayMissions 200": (r) => r.status === 200, + }); +} \ No newline at end of file diff --git a/perf/k6-scripts/scenarios/getVideosToday.js b/perf/k6-scripts/scenarios/getVideosToday.js new file mode 100644 index 0000000..65845e7 --- /dev/null +++ b/perf/k6-scripts/scenarios/getVideosToday.js @@ -0,0 +1,16 @@ +import http from "k6/http"; +import { check } from "k6"; + +export function getVideosToday(baseUrl, token, testId) { + const res = http.get( + `${baseUrl}/api/v1/challenge/rooms/1/videos/today`, + { + headers: { Authorization: `Bearer ${token}` }, + tags: { api: "getVideosToday", test_id: testId }, + } + ); + + check(res, { + "getVideosToday 200": (r) => r.status === 200, + }); +} \ No newline at end of file diff --git a/perf/k6-scripts/scenarios/index.js b/perf/k6-scripts/scenarios/index.js new file mode 100644 index 0000000..c3864c9 --- /dev/null +++ b/perf/k6-scripts/scenarios/index.js @@ -0,0 +1,3 @@ +export { getVideosToday } from "./getVideosToday.js"; +export { postCompleteMission } from "./postCompleteMission.js"; +export { getTodayMissions } from "./getTodayMissions.js"; \ No newline at end of file diff --git a/perf/k6-scripts/scenarios/postCompleteMission.js b/perf/k6-scripts/scenarios/postCompleteMission.js new file mode 100644 index 0000000..067e1a8 --- /dev/null +++ b/perf/k6-scripts/scenarios/postCompleteMission.js @@ -0,0 +1,17 @@ +import http from "k6/http"; +import { check } from "k6"; + +export function postCompleteMission(baseUrl, token, testId, roomId) { + const res = http.post( + `${baseUrl}/api/v1/challenge/rooms/${roomId}/missions/complete`, + null, // POST body μ—†μŒ + { + headers: { Authorization: `Bearer ${token}` }, + tags: { api: "postCompleteMission", test_id: testId }, + } + ); + + check(res, { + "postCompleteMission 200": (r) => r.status === 200, + }); +} \ No newline at end of file diff --git a/perf/k6-scripts/util.js b/perf/k6-scripts/util.js new file mode 100644 index 0000000..59e9fdd --- /dev/null +++ b/perf/k6-scripts/util.js @@ -0,0 +1,32 @@ +import encoding from "k6/encoding"; +import crypto from "k6/crypto"; + + +function sign(data, secretBase64) { + // Base64 decode ν›„ HMAC ν‚€λ‘œ μ‚¬μš© (μ„œλ²„μ™€ 동일) + const secretBytes = encoding.b64decode(secretBase64, "std"); + const hasher = crypto.createHMAC("sha256", secretBytes); + hasher.update(data); + return hasher.digest("base64") + .replace(/\//g, "_") + .replace(/\+/g, "-") + .replace(/=/g, ""); +} + + +export function generateJWT(payload, secretBase64) { + const header = { alg: "HS256", typ: "JWT" }; + + const issuedAt = Math.floor(Date.now() / 1000); + const exp = issuedAt + 3600; // 1μ‹œκ°„μ§œλ¦¬ 토큰 + + const fullPayload = { ...payload, iat: issuedAt, exp }; + + const encodedHeader = encoding.b64encode(JSON.stringify(header), "rawurl"); + const encodedPayload = encoding.b64encode(JSON.stringify(fullPayload), "rawurl"); + const signature = sign(`${encodedHeader}.${encodedPayload}`, secretBase64); + const token = `${encodedHeader}.${encodedPayload}.${signature}`; + + console.log("βœ… Generated JWT:", token); + return token; +} \ No newline at end of file diff --git a/perf/prometheus/prometheus.yml b/perf/prometheus/prometheus.yml new file mode 100644 index 0000000..80eee79 --- /dev/null +++ b/perf/prometheus/prometheus.yml @@ -0,0 +1,6 @@ +global: + scrape_interval: 5s +scrape_configs: + - job_name: "prometheus" + static_configs: + - targets: ["localhost:9090"] diff --git a/perf/run-loadtest.sh b/perf/run-loadtest.sh new file mode 100755 index 0000000..5fb4949 --- /dev/null +++ b/perf/run-loadtest.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +# πŸ•’ TEST_IDλ₯Ό νƒ€μž„μŠ€νƒ¬ν”„λ‘œ 생성 +TEST_ID=$(date +"%Y%m%d_%H%M%S") +LOG_DIR="./logs" +mkdir -p $LOG_DIR + +echo "🧹 Cleaning previous containers..." +docker compose down -v --remove-orphans > /dev/null 2>&1 +docker network prune -f > /dev/null 2>&1 + +echo "⬇️ Pulling latest images..." +docker compose pull prometheus grafana k6 > /dev/null 2>&1 + +echo "πŸš€ Starting Prometheus + Grafana..." +docker compose up -d prometheus grafana + +echo "⏳ Waiting for services to initialize..." +sleep 10 + +echo "πŸ” Current container status:" +docker compose ps + +echo "🎯 Running k6 load test with TEST_ID=$TEST_ID" +docker compose run --rm -e TEST_ID=$TEST_ID k6 | tee "$LOG_DIR/k6_${TEST_ID}.log" + +echo "βœ… Test complete! Logs saved to $LOG_DIR/k6_${TEST_ID}.log" +echo "πŸ“Š Grafana dashboard: http://localhost:3300" +echo "🧠 Prometheus: http://localhost:9090" \ No newline at end of file