diff --git a/backend/pom.xml b/backend/pom.xml index faa27b2..a1e6ad3 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -151,6 +151,14 @@ org.springframework spring-web + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-cache + diff --git a/backend/src/main/java/programming/tutorial/BackendApplication.java b/backend/src/main/java/programming/tutorial/BackendApplication.java index 2459e8a..0b51fa9 100644 --- a/backend/src/main/java/programming/tutorial/BackendApplication.java +++ b/backend/src/main/java/programming/tutorial/BackendApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; @SpringBootApplication +@EnableCaching public class BackendApplication { public static void main(String[] args) { diff --git a/backend/src/main/java/programming/tutorial/config/CacheConfig.java b/backend/src/main/java/programming/tutorial/config/CacheConfig.java new file mode 100644 index 0000000..91e9971 --- /dev/null +++ b/backend/src/main/java/programming/tutorial/config/CacheConfig.java @@ -0,0 +1,36 @@ +package programming.tutorial.config; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; + +import java.time.Duration; + +@Configuration +@EnableCaching +public class CacheConfig { + + @Bean + public CacheManager cacheManager() { + try { + LettuceConnectionFactory redisConnectionFactory = new LettuceConnectionFactory("localhost", 6379); + redisConnectionFactory.afterPropertiesSet(); + redisConnectionFactory.getConnection().ping(); + + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofMinutes(10)); + return RedisCacheManager.builder(redisConnectionFactory) + .cacheDefaults(config) + .build(); + } catch (Exception e) { + System.out.println("Redis not available. Using in-memory cache."); + return new ConcurrentMapCacheManager(); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/programming/tutorial/controller/CacheController.java b/backend/src/main/java/programming/tutorial/controller/CacheController.java new file mode 100644 index 0000000..ea9fdbc --- /dev/null +++ b/backend/src/main/java/programming/tutorial/controller/CacheController.java @@ -0,0 +1,29 @@ +package programming.tutorial.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import programming.tutorial.services.impl.CacheServiceJpa; + +@RestController +@RequestMapping("/admin/cache") +public class CacheController { + + @Autowired + private CacheServiceJpa cacheService; + + @DeleteMapping("/courses") + public ResponseEntity clearAllCoursesCache() { + cacheService.evictAllCoursesCache(); + System.out.println("Called"); + return ResponseEntity.ok("All course caches cleared!"); + } + + @DeleteMapping("/all") + public ResponseEntity clearAllCaches() { + cacheService.evictAllCaches(); + return ResponseEntity.ok("All caches cleared"); + } +} diff --git a/backend/src/main/java/programming/tutorial/controller/CourseController.java b/backend/src/main/java/programming/tutorial/controller/CourseController.java index c9b569b..d56f96e 100644 --- a/backend/src/main/java/programming/tutorial/controller/CourseController.java +++ b/backend/src/main/java/programming/tutorial/controller/CourseController.java @@ -66,6 +66,10 @@ public List getAllCourses() { } return courses; } + @GetMapping("/uncached") + public List getAllCoursesUncached() { + return courseService.getAllCoursesUncached(); + } @PostMapping("/create-with-lessons") public ResponseEntity createCourseWithLessons(@RequestBody CourseWithLessonsDTO dto) { diff --git a/backend/src/main/java/programming/tutorial/dto/CourseDTO.java b/backend/src/main/java/programming/tutorial/dto/CourseDTO.java index ca3da71..c889419 100644 --- a/backend/src/main/java/programming/tutorial/dto/CourseDTO.java +++ b/backend/src/main/java/programming/tutorial/dto/CourseDTO.java @@ -2,15 +2,19 @@ import programming.tutorial.domain.User; -public class CourseDTO { +import java.io.Serial; +import java.io.Serializable; +public class CourseDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; private Integer courseId; private String courseName; private int courseLength; private String description; private String category; private String creatorId; - private User creator; + private UserDTO creator; private boolean systemCourse; public CourseDTO() { @@ -85,11 +89,11 @@ public void setCreatorId(String creatorId) { this.creatorId = creatorId; } - public User getCreator() { + public UserDTO getCreator() { return creator; } - public void setCreator(User creator) { + public void setCreator(UserDTO creator) { this.creator = creator; } diff --git a/backend/src/main/java/programming/tutorial/dto/UserDTO.java b/backend/src/main/java/programming/tutorial/dto/UserDTO.java index 5932824..86f9724 100644 --- a/backend/src/main/java/programming/tutorial/dto/UserDTO.java +++ b/backend/src/main/java/programming/tutorial/dto/UserDTO.java @@ -3,9 +3,13 @@ import programming.tutorial.domain.Role; import programming.tutorial.domain.Tier; +import java.io.Serial; +import java.io.Serializable; import java.time.LocalDateTime; -public class UserDTO { +public class UserDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; public Integer id; public String name; public String surname; diff --git a/backend/src/main/java/programming/tutorial/monitoring/CacheMetrics.java b/backend/src/main/java/programming/tutorial/monitoring/CacheMetrics.java new file mode 100644 index 0000000..19b4ab6 --- /dev/null +++ b/backend/src/main/java/programming/tutorial/monitoring/CacheMetrics.java @@ -0,0 +1,25 @@ +package programming.tutorial.monitoring; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.stereotype.Component; + +@Component +public class CacheMetrics { + + private final Counter cacheHit; + private final Counter cacheMiss; + + public CacheMetrics(MeterRegistry registry) { + this.cacheHit = Counter.builder("redis_cache_hit_total") + .description("Cache hits") + .register(registry); + + this.cacheMiss = Counter.builder("redis_cache_miss_total") + .description("Cache misses") + .register(registry); + } + + public void hit() { cacheHit.increment(); } + public void miss() { cacheMiss.increment(); } +} diff --git a/backend/src/main/java/programming/tutorial/services/CourseService.java b/backend/src/main/java/programming/tutorial/services/CourseService.java index dbfb1ca..9f8aef4 100644 --- a/backend/src/main/java/programming/tutorial/services/CourseService.java +++ b/backend/src/main/java/programming/tutorial/services/CourseService.java @@ -27,4 +27,6 @@ public interface CourseService { boolean isCourseOwner(String userId, Integer courseId); List getLessonsForCourse(Integer courseId); + + List getAllCoursesUncached(); } diff --git a/backend/src/main/java/programming/tutorial/services/impl/CacheServiceJpa.java b/backend/src/main/java/programming/tutorial/services/impl/CacheServiceJpa.java new file mode 100644 index 0000000..b64a341 --- /dev/null +++ b/backend/src/main/java/programming/tutorial/services/impl/CacheServiceJpa.java @@ -0,0 +1,35 @@ +package programming.tutorial.services.impl; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; + +@Service +public class CacheServiceJpa { + + @Autowired + private CacheManager cacheManager; + + public void evictAllCoursesCache() { + var cache = cacheManager.getCache("all_courses"); + if (cache != null) { + cache.clear(); + } + } + + public void evictCoursesByUser(String auth0UserId) { + var cache = cacheManager.getCache("courses_by_user"); + if (cache != null) { + cache.evict(auth0UserId); + } + } + + public void evictAllCaches() { + cacheManager.getCacheNames().forEach(name -> { + var cache = cacheManager.getCache(name); + if (cache != null) { + cache.clear(); + } + }); + } +} \ No newline at end of file diff --git a/backend/src/main/java/programming/tutorial/services/impl/CourseServiceJpa.java b/backend/src/main/java/programming/tutorial/services/impl/CourseServiceJpa.java index fe48a7c..794ccbc 100644 --- a/backend/src/main/java/programming/tutorial/services/impl/CourseServiceJpa.java +++ b/backend/src/main/java/programming/tutorial/services/impl/CourseServiceJpa.java @@ -1,6 +1,10 @@ package programming.tutorial.services.impl; +import io.micrometer.core.instrument.MeterRegistry; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; import org.springframework.stereotype.Service; import programming.tutorial.dao.CourseRepository; import programming.tutorial.dao.LessonRepository; @@ -9,7 +13,10 @@ import programming.tutorial.dto.CourseDTO; import programming.tutorial.dto.CourseWithLessonsDTO; import programming.tutorial.dto.LessonDTO; +import programming.tutorial.dto.UserDTO; +import programming.tutorial.monitoring.CacheMetrics; import programming.tutorial.services.CourseService; +import io.micrometer.core.instrument.Timer; import java.util.*; import java.util.stream.Collectors; @@ -25,18 +32,32 @@ public class CourseServiceJpa implements CourseService { @Autowired private LessonRepository lessonRepository; + @Autowired + private CacheServiceJpa cacheService; + @Autowired + private CacheMetrics cacheMetrics; + @Autowired + private MeterRegistry meterRegistry; @Override + @Cacheable(value = "course_by_name", key = "#name") public Optional findByName(CourseDTO courseDTO) { return Optional.ofNullable(courseRepository.findByCourseName(courseDTO.getCourseName())); } @Override + @Cacheable(value = "course_by_id", key = "#courseDTO.courseId") public Optional findById(CourseDTO courseDTO) { return courseRepository.findById(courseDTO.getCourseId()); } @Override + @Caching(evict = { + @CacheEvict(value = "all_courses", allEntries = true), + @CacheEvict(value = "course_by_id", key = "#courseDTO.courseId"), + @CacheEvict(value = "course_by_name", key = "#courseDTO.courseName"), + @CacheEvict(value = "courses_by_user", key = "#courseDTO.creator.auth0UserId") + }) public Course saveCourse(CourseDTO courseDTO) { Course course = new Course(); course.setId(courseDTO.getCourseId()); @@ -48,23 +69,86 @@ public Course saveCourse(CourseDTO courseDTO) { } @Override + @CacheEvict(value = "all_courses", allEntries = true) public void deleteCourse(Integer courseId) { courseRepository.deleteById(courseId); } + private UserDTO mapUser(User user) { + if (user == null) return null; + UserDTO dto = new UserDTO(); + dto.setId(user.getId()); + dto.setName(user.getName()); + dto.setSurname(user.getSurname()); + dto.setUsername(user.getUsername()); + dto.setAuth0UserId(user.getAuth0UserId()); + dto.setRole(user.getRole()); + dto.setActive(user.isActive()); + dto.setDateCreated(user.getDateCreated()); + dto.setTier(user.getTier()); + return dto; + } + @Override + @Cacheable(value = "all_courses") public List getAllCourses() { - List courses = courseRepository.findAll(); - System.out.println("Retrieved courses from database:"); - for (Course course : courses) { - System.out.println("Course ID: " + course.getId() + ", Name: " + course.getCourseName()); - } - return courses.stream() - .map(course -> new CourseDTO(course.getId(), course.getCourseName(), course.getLength(), course.getDescription(), course.getCategory())) + /* First run will print this if cache isn't evicted previously + * Other runs will not print this unless cache is invalidated + * */ + System.out.println(">>> DB QUERY EXECUTED — NO CACHE HIT <<<"); + List courses = courseRepository.findAll().stream() + .map(course -> { + CourseDTO dto = new CourseDTO( + course.getId(), + course.getCourseName(), + course.getLength(), + course.getDescription(), + course.getCategory(), + course.getCreator() != null ? course.getCreator().getId() : null, + course.isSystemCourse() + ); + dto.setCreator(mapUser(course.getCreator())); + return dto; + }) .collect(Collectors.toList()); + + return courses; } + /** + * Method to check how caching improves/behaves versus regular method. + * This bypasses the cache entirely and records execution time for comparison. + */ + public List getAllCoursesUncached() { + Timer.Sample sample = Timer.start(meterRegistry); + long start = System.currentTimeMillis(); + System.out.println("Uncached getAllCourses method called"); + List courses = courseRepository.findAll().stream() + .map(course -> new CourseDTO( + course.getId(), + course.getCourseName(), + course.getLength(), + course.getDescription(), + course.getCategory() + )) + .collect(Collectors.toList()); + + sample.stop(Timer.builder("app.cache.getAllCourses_uncached.time") + .description("Execution time for getAllCourses WITHOUT cache") + .register(meterRegistry)); + + long duration = System.currentTimeMillis() - start; + System.out.println("getAllCoursesUncached execution time: " + duration + " ms"); + return courses; + } + + @Override + @Caching(evict = { + @CacheEvict(value = "all_courses", allEntries = true), + @CacheEvict(value = "courses_by_user", key = "#dto.auth0UserId"), + @CacheEvict(value = "lessons_for_course", key = "#result.id") + }) public Course createCourseWithLessons(CourseWithLessonsDTO dto) { User creator = userRepository.findByAuth0UserId(dto.getAuth0UserId()) .orElseThrow(() -> new IllegalArgumentException("User not found for Auth0 ID: " + dto.getAuth0UserId())); @@ -143,6 +227,7 @@ private static int getRequestedLessonCount(CourseWithLessonsDTO dto, User creato } @Override + @Cacheable(value = "courses_by_user", key = "#auth0UserId") public List getCoursesByUserAuth0Id(String auth0UserId) { User user = userRepository.findByAuth0UserId(auth0UserId) .orElseThrow(() -> new RuntimeException("User not found")); @@ -162,6 +247,7 @@ public boolean isCourseOwner(String userId, Integer courseId) { } @Override + @Cacheable(value = "lessons_for_course", key = "#courseId") public List getLessonsForCourse(Integer courseId) { List lessons = lessonRepository.findByCourseIdOrderByIdAsc(courseId); return lessons.stream() diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 67d6ac2..2e1ca8a 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -24,7 +24,13 @@ management.metrics.export.prometheus.enabled=true management.endpoints.web.exposure.include=* management.endpoints.web.base-path=/actuator management.endpoint.health.show-details=always +management.metrics.enable-all=true +management.endpoint.metrics.enabled=true +logging.level.org.springframework.cache=DEBUG +#spring.cache.type=redis +spring.cache.type=simple +spring.cache.redis.time-to-live=600000 #Mock data for payment # email: fudansfudans@gmail.com diff --git a/backend/src/test/java/programming/tutorial/controller/UserProgressControllerTest.java b/backend/src/test/java/programming/tutorial/controller/UserProgressControllerTest.java index 7a6fbd4..db1ca23 100644 --- a/backend/src/test/java/programming/tutorial/controller/UserProgressControllerTest.java +++ b/backend/src/test/java/programming/tutorial/controller/UserProgressControllerTest.java @@ -1,7 +1,9 @@ package programming.tutorial.controller; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -24,10 +26,13 @@ import java.util.Optional; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; @WebMvcTest(UserProgressController.class) @@ -36,7 +41,6 @@ class UserProgressControllerTest { @Autowired private MockMvc mockMvc; - @MockBean private UserProgressService userProgressService; @MockBean @@ -47,6 +51,8 @@ class UserProgressControllerTest { private UserService userService; @Mock private Authentication authentication; + @InjectMocks + private UserProgressController progressController; private final String auth0Id = "auth0|123"; private final Integer courseId = 1; @@ -59,6 +65,81 @@ void setup() { when(userService.getUserId(auth0Id)).thenReturn(userId); } + @Test + void updateProgress_userNotEnrolledAndNotOwner_returnsForbidden() throws Exception { + UserProgressController.ProgressUpdateRequest request = new UserProgressController.ProgressUpdateRequest(); + request.courseId = 1; + request.lessonId = 1; + + + Jwt jwt = Jwt.withTokenValue("token") + .header("alg", "none") + .claim("sub", auth0Id) + .build(); + + when(authentication.getPrincipal()).thenReturn(jwt); + when(userProgressService.isUserEnrolled(anyString(), eq(request.courseId))).thenReturn(false); + when(courseService.isCourseOwner(anyString(), eq(request.courseId))).thenReturn(false); + + mockMvc.perform(post("/api/progress/update") + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(request)) + .principal(authentication)) + .andExpect(status().isForbidden()) + .andExpect(content().string("")); + + verify(userProgressService).isUserEnrolled(anyString(), eq(request.courseId)); + verify(courseService).isCourseOwner(anyString(), eq(request.courseId)); + verifyNoMoreInteractions(userProgressService, courseService); + } + + @Test + void updateProgress_userEnrolled_returnsOkWithMessage() throws Exception { + UserProgressController.ProgressUpdateRequest request = new UserProgressController.ProgressUpdateRequest(); + request.courseId = 2; + request.lessonId = 2; + + Jwt jwt = Jwt.withTokenValue("token") + .header("alg", "none") + .claim("sub", auth0Id) + .build(); + + when(authentication.getPrincipal()).thenReturn(jwt); + when(userProgressService.isUserEnrolled(anyString(), eq(request.courseId))).thenReturn(true); + when(courseService.isCourseOwner(anyString(), eq(request.courseId))).thenReturn(false); + when(userProgressService.updateProgress(eq("auth0|123"), eq(request.courseId), eq(request.lessonId))) + .thenReturn("Progress updated"); + + mockMvc.perform(post("/api/progress/update") + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(request)) + .principal(authentication)) + .andDo(print()) + .andExpect(status().isOk()); + + verify(userProgressService).updateProgress(eq("auth0|123"), eq(request.courseId), eq(request.lessonId)); + } + + @Test + void updateProgress_userIsOwner_returnsOkWithMessage() throws Exception { + UserProgressController.ProgressUpdateRequest request = new UserProgressController.ProgressUpdateRequest(); + request.courseId = 3; + request.lessonId = 3; + + Jwt jwt = Jwt.withTokenValue("token").header("alg", "none").claim("sub", auth0Id).build(); + when(authentication.getPrincipal()).thenReturn(jwt); + when(userProgressService.isUserEnrolled(anyString(), eq(request.courseId))).thenReturn(false); + when(courseService.isCourseOwner(anyString(), eq(request.courseId))).thenReturn(true); + when(userProgressService.updateProgress(eq("auth0|123"), eq(request.courseId), eq(request.lessonId))) + .thenReturn("Progress updated"); + + mockMvc.perform(post("/api/progress/update") + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(request)) + .principal(authentication)) + .andExpect(status().isOk()); + } + @Test void getCurrentLessonNumber_shouldReturnLessonNumberIfExists() throws Exception { LessonDTO lessonDTO = new LessonDTO(); diff --git a/backend/src/test/java/programming/tutorial/services/CourseServiceTest.java b/backend/src/test/java/programming/tutorial/services/CourseServiceTest.java index 7cd6fa3..44203a8 100644 --- a/backend/src/test/java/programming/tutorial/services/CourseServiceTest.java +++ b/backend/src/test/java/programming/tutorial/services/CourseServiceTest.java @@ -17,11 +17,11 @@ import programming.tutorial.dto.CourseWithLessonsDTO; import programming.tutorial.dto.LessonDTO; import programming.tutorial.services.impl.CourseServiceJpa; - +import io.micrometer.core.instrument.Timer; import java.util.*; +import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class CourseServiceTest { diff --git a/docker-compose.yml b/docker-compose.yml index 49834b5..e9b5461 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -91,7 +91,11 @@ services: - backend prometheus: + container_name: prometheus image: prom/prometheus:latest + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--web.enable-remote-write-receiver' volumes: - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml ports: @@ -100,6 +104,7 @@ services: - backend grafana: + container_name: grafana image: grafana/grafana:latest environment: GF_SECURITY_ADMIN_USER: admin @@ -113,6 +118,12 @@ services: volumes: - grafana-data:/var/lib/grafana + pushgateway: + container_name: pushgateway + image: prom/pushgateway:latest + ports: + - "9091:9091" + volumes: pgdata: kafka-data: diff --git a/microservice-moderation b/microservice-moderation index 2ff6319..e354deb 160000 --- a/microservice-moderation +++ b/microservice-moderation @@ -1 +1 @@ -Subproject commit 2ff63199ace1728bd19cea3c47174c8a2e51fcb5 +Subproject commit e354deb44a2771cc3d0a97da408e77502499978b diff --git a/monitoring/prometheus.yml b/monitoring/prometheus.yml index c000335..735cf42 100644 --- a/monitoring/prometheus.yml +++ b/monitoring/prometheus.yml @@ -1,8 +1,15 @@ global: scrape_interval: 5s +remote_write: + - url: http://prometheus:9090/api/v1/write + scrape_configs: - job_name: 'springboot-app' metrics_path: '/actuator/prometheus' static_configs: - - targets: ['backend:8080'] \ No newline at end of file + - targets: ['backend:8080'] + + - job_name: 'k6' + static_configs: + - targets: ['pushgateway:9091'] diff --git a/tests/performance/controllers/course_controller/delete_course_by_id/load_delete_course_by_id.js b/tests/performance/controllers/course_controller/delete_course_by_id/load_delete_course_by_id.js index 2e674ea..060d93f 100644 --- a/tests/performance/controllers/course_controller/delete_course_by_id/load_delete_course_by_id.js +++ b/tests/performance/controllers/course_controller/delete_course_by_id/load_delete_course_by_id.js @@ -16,3 +16,10 @@ export default function () { check(res, { 'DELETE load': (r) => [200, 204, 404].includes(r.status) }); sleep(Math.random() * 5 + 2) } + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_load_delete_course_by_id_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; +} \ No newline at end of file diff --git a/tests/performance/controllers/course_controller/delete_course_by_id/spike_delete_course_by_id.js b/tests/performance/controllers/course_controller/delete_course_by_id/spike_delete_course_by_id.js index 4ee9836..47db2f0 100644 --- a/tests/performance/controllers/course_controller/delete_course_by_id/spike_delete_course_by_id.js +++ b/tests/performance/controllers/course_controller/delete_course_by_id/spike_delete_course_by_id.js @@ -17,4 +17,11 @@ export default function () { const res = http.del(`${BASE_URL}/${courseId}`, null, { headers: defaultHeaders }); check(res, { 'DELETE spike': (r) => [200, 204, 404].includes(r.status) }); sleep(Math.random() * 5 + 2) +} + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_spike_delete_course_by_id_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; } \ No newline at end of file diff --git a/tests/performance/controllers/course_controller/delete_course_by_id/stress_delete_course_by_id.js b/tests/performance/controllers/course_controller/delete_course_by_id/stress_delete_course_by_id.js index 10fb7c5..7e18c8d 100644 --- a/tests/performance/controllers/course_controller/delete_course_by_id/stress_delete_course_by_id.js +++ b/tests/performance/controllers/course_controller/delete_course_by_id/stress_delete_course_by_id.js @@ -12,4 +12,11 @@ export default function () { const res = http.del(`${BASE_URL}/${courseId}`, null, { headers: defaultHeaders }); check(res, { 'DELETE stress': (r) => [200, 204, 404].includes(r.status) }); sleep(Math.random() * 5 + 2); +} + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_stress_delete_course_by_id_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; } \ No newline at end of file diff --git a/tests/performance/controllers/course_controller/get_all_courses/load_get_all_courses.js b/tests/performance/controllers/course_controller/get_all_courses/load_get_all_courses.js index effdcb4..10da7a0 100644 --- a/tests/performance/controllers/course_controller/get_all_courses/load_get_all_courses.js +++ b/tests/performance/controllers/course_controller/get_all_courses/load_get_all_courses.js @@ -19,3 +19,10 @@ export default function () { check(res, { 'GET /api/courses status 200': (r) => checkResponse(r, 200, 'GET /api/courses') }); sleep(Math.random() * 2 + 1); } + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_load_get_all_courses_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; +} \ No newline at end of file diff --git a/tests/performance/controllers/course_controller/get_all_courses/spike_get_all_courses.js b/tests/performance/controllers/course_controller/get_all_courses/spike_get_all_courses.js index 806018a..d3797d0 100644 --- a/tests/performance/controllers/course_controller/get_all_courses/spike_get_all_courses.js +++ b/tests/performance/controllers/course_controller/get_all_courses/spike_get_all_courses.js @@ -19,3 +19,10 @@ export default function () { const res = http.get(BASE_URL, { headers: defaultHeaders }); check(res, { 'GET /api/courses status 200': (r) => checkResponse(r, 200, 'GET /api/courses') }); } + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_spike_get_all_courses_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; +} \ No newline at end of file diff --git a/tests/performance/controllers/course_controller/get_all_courses/stress_get_all_courses.js b/tests/performance/controllers/course_controller/get_all_courses/stress_get_all_courses.js index 3fd8b9f..190a06b 100644 --- a/tests/performance/controllers/course_controller/get_all_courses/stress_get_all_courses.js +++ b/tests/performance/controllers/course_controller/get_all_courses/stress_get_all_courses.js @@ -22,3 +22,10 @@ export default function () { const res = http.get(BASE_URL, { headers: defaultHeaders }); check(res, { 'GET /api/courses status 200': (r) => checkResponse(r, 200, 'GET /api/courses') }); } + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_stress_get_all_courses_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; +} \ No newline at end of file diff --git a/tests/performance/controllers/course_controller/get_course_by_id/load_get_course_by_id.js b/tests/performance/controllers/course_controller/get_course_by_id/load_get_course_by_id.js index bba9cf9..185be0a 100644 --- a/tests/performance/controllers/course_controller/get_course_by_id/load_get_course_by_id.js +++ b/tests/performance/controllers/course_controller/get_course_by_id/load_get_course_by_id.js @@ -19,4 +19,11 @@ const COURSE_ID = __ENV.COURSE_ID || '1'; export default function () { const res = http.get(`${BASE_URL}/${COURSE_ID}`, { headers: defaultHeaders }); check(res, { 'GET /api/courses/{id} status 200': (r) => checkResponse(r, 200, 'GET /api/courses/{id}') }); +} + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_load_get_courses_by_id_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; } \ No newline at end of file diff --git a/tests/performance/controllers/course_controller/get_course_by_id/spike_get_course_by_id.js b/tests/performance/controllers/course_controller/get_course_by_id/spike_get_course_by_id.js index 4fa0e57..73a69f0 100644 --- a/tests/performance/controllers/course_controller/get_course_by_id/spike_get_course_by_id.js +++ b/tests/performance/controllers/course_controller/get_course_by_id/spike_get_course_by_id.js @@ -15,4 +15,11 @@ const COURSE_ID = __ENV.COURSE_ID || '1'; export default function () { const res = http.get(`${BASE_URL}/${COURSE_ID}`, { headers: defaultHeaders }); check(res, { 'GET /api/courses/{id} status 200': (r) => checkResponse(r, 200, 'GET /api/courses/{id}') }); +} + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_spike_get_courses_by_id_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; } \ No newline at end of file diff --git a/tests/performance/controllers/course_controller/get_course_by_id/stress_get_course_by_id.js b/tests/performance/controllers/course_controller/get_course_by_id/stress_get_course_by_id.js index a2baebf..ff3ebaa 100644 --- a/tests/performance/controllers/course_controller/get_course_by_id/stress_get_course_by_id.js +++ b/tests/performance/controllers/course_controller/get_course_by_id/stress_get_course_by_id.js @@ -23,4 +23,11 @@ const COURSE_ID = __ENV.COURSE_ID || '1'; export default function () { const res = http.get(`${BASE_URL}/${COURSE_ID}`, { headers: defaultHeaders }); check(res, { 'GET /api/courses/{id} status 200': (r) => checkResponse(r, 200, 'GET /api/courses/{id}') }); +} + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_stress_get_courses_by_id_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; } \ No newline at end of file diff --git a/tests/performance/controllers/course_controller/get_course_by_name/load_get_course_by_name.js b/tests/performance/controllers/course_controller/get_course_by_name/load_get_course_by_name.js index 240a532..c56c964 100644 --- a/tests/performance/controllers/course_controller/get_course_by_name/load_get_course_by_name.js +++ b/tests/performance/controllers/course_controller/get_course_by_name/load_get_course_by_name.js @@ -17,3 +17,10 @@ export default function () { check(res, { 'GET /api/courses/name/{name} returns 200': (r) => checkResponse(r, 200) }); sleep(0.5); } + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_load_get_courses_by_name_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; +} \ No newline at end of file diff --git a/tests/performance/controllers/course_controller/get_course_by_name/spike_get_course_by_name.js b/tests/performance/controllers/course_controller/get_course_by_name/spike_get_course_by_name.js index 07692d0..f0efdad 100644 --- a/tests/performance/controllers/course_controller/get_course_by_name/spike_get_course_by_name.js +++ b/tests/performance/controllers/course_controller/get_course_by_name/spike_get_course_by_name.js @@ -20,4 +20,11 @@ export default function () { const res = http.get(`${BASE_URL}/name/${courseName}`, { headers: defaultHeaders }); check(res, { 'GET /api/courses/name/{name} returns 200': (r) => checkResponse(r, 200) }); +} + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_spike_get_courses_by_name_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; } \ No newline at end of file diff --git a/tests/performance/controllers/course_controller/get_course_by_name/stress_get_course_by_name.js b/tests/performance/controllers/course_controller/get_course_by_name/stress_get_course_by_name.js index 04bed14..18e058f 100644 --- a/tests/performance/controllers/course_controller/get_course_by_name/stress_get_course_by_name.js +++ b/tests/performance/controllers/course_controller/get_course_by_name/stress_get_course_by_name.js @@ -23,4 +23,11 @@ export default function () { const res = http.get(`${BASE_URL}/name/${courseName}`, { headers: defaultHeaders }); check(res, { 'GET /api/courses/name/{name} returns 200': (r) => checkResponse(r, 200) }); +} + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_stress_get_courses_by_name_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; } \ No newline at end of file diff --git a/tests/performance/controllers/course_controller/get_course_lessons_by_id/load_get_course_lessons_by_id.js b/tests/performance/controllers/course_controller/get_course_lessons_by_id/load_get_course_lessons_by_id.js index e8eee45..9ad07d5 100644 --- a/tests/performance/controllers/course_controller/get_course_lessons_by_id/load_get_course_lessons_by_id.js +++ b/tests/performance/controllers/course_controller/get_course_lessons_by_id/load_get_course_lessons_by_id.js @@ -15,4 +15,11 @@ export default function () { const res = http.get(`${BASE_URL}/1/lessons`, { headers: defaultHeaders }); check(res, { 'GET /api/courses/1/lessons returns 200': (r) => checkResponse(r, 200) }); sleep(0.5); +} + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_load_get_course_lessons_by_id_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; } \ No newline at end of file diff --git a/tests/performance/controllers/course_controller/get_course_lessons_by_id/spike_get_course_lessons_by_id.js b/tests/performance/controllers/course_controller/get_course_lessons_by_id/spike_get_course_lessons_by_id.js index 697635f..21eb91b 100644 --- a/tests/performance/controllers/course_controller/get_course_lessons_by_id/spike_get_course_lessons_by_id.js +++ b/tests/performance/controllers/course_controller/get_course_lessons_by_id/spike_get_course_lessons_by_id.js @@ -18,4 +18,11 @@ export let options = { export default function () { const res = http.get(`${BASE_URL}/1/lessons`, { headers: defaultHeaders }); check(res, { 'GET /api/courses/1/lessons returns 200': (r) => checkResponse(r, 200) }); +} + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_spike_get_course_lessons_by_id_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; } \ No newline at end of file diff --git a/tests/performance/controllers/course_controller/get_course_lessons_by_id/stress_get_course_lessons_by_id.js b/tests/performance/controllers/course_controller/get_course_lessons_by_id/stress_get_course_lessons_by_id.js index 374ffad..20649b6 100644 --- a/tests/performance/controllers/course_controller/get_course_lessons_by_id/stress_get_course_lessons_by_id.js +++ b/tests/performance/controllers/course_controller/get_course_lessons_by_id/stress_get_course_lessons_by_id.js @@ -22,4 +22,11 @@ export default function () { const res = http.get(`${BASE_URL}/1/lessons`, { headers: defaultHeaders }); check(res, { 'GET /api/courses/1/lessons returns 200': (r) => checkResponse(r, 200) }); +} + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_stress_get_course_lessons_by_id_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; } \ No newline at end of file diff --git a/tests/performance/controllers/course_controller/post_all_courses/load_post_all_courses.js b/tests/performance/controllers/course_controller/post_all_courses/load_post_all_courses.js index a0892cf..6a03427 100644 --- a/tests/performance/controllers/course_controller/post_all_courses/load_post_all_courses.js +++ b/tests/performance/controllers/course_controller/post_all_courses/load_post_all_courses.js @@ -18,4 +18,11 @@ export default function () { const res = http.post(BASE_URL, { headers: defaultHeaders }); check(res, { 'POST /api/courses status 200': (r) => checkResponse(r, 200, 'POST /api/courses') }); sleep(Math.random() * 2 + 1); +} + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_load_post_all_courses_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; } \ No newline at end of file diff --git a/tests/performance/controllers/course_controller/post_all_courses/spike_post_all_courses.js b/tests/performance/controllers/course_controller/post_all_courses/spike_post_all_courses.js index d9b5d4f..c22512b 100644 --- a/tests/performance/controllers/course_controller/post_all_courses/spike_post_all_courses.js +++ b/tests/performance/controllers/course_controller/post_all_courses/spike_post_all_courses.js @@ -31,4 +31,11 @@ export default function () { 'POST /api/courses → status 201': (r) => checkResponse(r, 201, 'POST /api/courses'), }); +} + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_spike_post_all_courses_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; } \ No newline at end of file diff --git a/tests/performance/controllers/course_controller/post_all_courses/stress_post_all_courses.js b/tests/performance/controllers/course_controller/post_all_courses/stress_post_all_courses.js index 19efa26..0388eed 100644 --- a/tests/performance/controllers/course_controller/post_all_courses/stress_post_all_courses.js +++ b/tests/performance/controllers/course_controller/post_all_courses/stress_post_all_courses.js @@ -39,4 +39,11 @@ export default function () { checkResponse(r, [200, 201], 'POST /api/courses'), }); sleep(Math.random() * 5 + 2); +} + +export function handleSummary(data) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return { + [`./tests/performance/results/summary_stress_post_all_courses_${timestamp}.json`]: JSON.stringify(data, null, 2), + }; } \ No newline at end of file diff --git a/tests/performance/results/summary_load_get_courses_by_name_2025-12-08T17-45-06-298Z.json b/tests/performance/results/summary_load_get_courses_by_name_2025-12-08T17-45-06-298Z.json new file mode 100644 index 0000000..a2a1243 --- /dev/null +++ b/tests/performance/results/summary_load_get_courses_by_name_2025-12-08T17-45-06-298Z.json @@ -0,0 +1,212 @@ +{ + "options": { + "summaryTrendStats": [ + "avg", + "min", + "med", + "max", + "p(90)", + "p(95)" + ], + "summaryTimeUnit": "", + "noColor": false + }, + "state": { + "isStdOutTTY": true, + "isStdErrTTY": true, + "testRunDurationMs": 50271.384 + }, + "metrics": { + "http_req_tls_handshaking": { + "type": "trend", + "contains": "time", + "values": { + "avg": 0, + "min": 0, + "med": 0, + "max": 0, + "p(90)": 0, + "p(95)": 0 + } + }, + "http_req_receiving": { + "type": "trend", + "contains": "time", + "values": { + "avg": 0.2379822510822513, + "min": 0.007, + "med": 0.076, + "max": 16.035, + "p(90)": 0.20810000000000003, + "p(95)": 0.3476499999999997 + } + }, + "data_sent": { + "type": "counter", + "contains": "data", + "values": { + "count": 2605680, + "rate": 51832.27101923432 + } + }, + "http_req_waiting": { + "type": "trend", + "contains": "time", + "values": { + "max": 118.755, + "p(90)": 57.7513, + "p(95)": 64.247, + "avg": 40.99053030303033, + "min": 9.778, + "med": 40.321 + } + }, + "checks": { + "contains": "default", + "values": { + "rate": 1, + "passes": 2310, + "fails": 0 + }, + "type": "rate" + }, + "iterations": { + "type": "counter", + "contains": "default", + "values": { + "rate": 45.950594875207734, + "count": 2310 + } + }, + "http_reqs": { + "type": "counter", + "contains": "default", + "values": { + "count": 2310, + "rate": 45.950594875207734 + } + }, + "iteration_duration": { + "type": "trend", + "contains": "time", + "values": { + "p(95)": 566.4199189, + "avg": 542.7420781367962, + "min": 510.738083, + "med": 541.99, + "max": 618.941333, + "p(90)": 559.6669331 + } + }, + "http_req_failed": { + "contains": "default", + "values": { + "rate": 0, + "passes": 0, + "fails": 2310 + }, + "type": "rate" + }, + "http_req_blocked": { + "contains": "time", + "values": { + "avg": 0.028522077922078178, + "min": 0.001, + "med": 0.006, + "max": 3.266, + "p(90)": 0.015, + "p(95)": 0.025 + }, + "type": "trend" + }, + "http_req_sending": { + "values": { + "max": 8.574, + "p(90)": 0.05110000000000005, + "p(95)": 0.0775499999999999, + "avg": 0.04051471861471894, + "min": 0.004, + "med": 0.019 + }, + "type": "trend", + "contains": "time" + }, + "http_req_duration": { + "type": "trend", + "contains": "time", + "values": { + "avg": 41.269027272727314, + "min": 9.816, + "med": 40.524, + "max": 118.789, + "p(90)": 58.074400000000004, + "p(95)": 64.67054999999999 + } + }, + "data_received": { + "type": "counter", + "contains": "data", + "values": { + "count": 1275120, + "rate": 25364.72837111467 + } + }, + "vus_max": { + "contains": "default", + "values": { + "value": 50, + "min": 50, + "max": 50 + }, + "type": "gauge" + }, + "vus": { + "type": "gauge", + "contains": "default", + "values": { + "value": 12, + "min": 1, + "max": 49 + } + }, + "http_req_connecting": { + "type": "trend", + "contains": "time", + "values": { + "min": 0, + "med": 0, + "max": 2.895, + "p(90)": 0, + "p(95)": 0, + "avg": 0.016248484848484852 + } + }, + "http_req_duration{expected_response:true}": { + "type": "trend", + "contains": "time", + "values": { + "avg": 41.269027272727314, + "min": 9.816, + "med": 40.524, + "max": 118.789, + "p(90)": 58.074400000000004, + "p(95)": 64.67054999999999 + } + } + }, + "root_group": { + "name": "", + "path": "", + "id": "d41d8cd98f00b204e9800998ecf8427e", + "groups": [], + "checks": [ + { + "id": "4900f98b03ff58bb73677daf299fc44a", + "passes": 2310, + "fails": 0, + "name": "GET /api/courses/name/{name} returns 200", + "path": "::GET /api/courses/name/{name} returns 200" + } + ] + } +} \ No newline at end of file diff --git a/tests/performance/results/summary_spike_get_all_courses_2025-12-09T15-40-48-531Z.json b/tests/performance/results/summary_spike_get_all_courses_2025-12-09T15-40-48-531Z.json new file mode 100644 index 0000000..3a89f63 --- /dev/null +++ b/tests/performance/results/summary_spike_get_all_courses_2025-12-09T15-40-48-531Z.json @@ -0,0 +1,222 @@ +{ + "root_group": { + "groups": [], + "checks": [ + { + "name": "GET /api/courses status 200", + "path": "::GET /api/courses status 200", + "id": "f02acc4eeef4e7a41d7801aaf3fd8a9d", + "passes": 88957, + "fails": 0 + } + ], + "name": "", + "path": "", + "id": "d41d8cd98f00b204e9800998ecf8427e" + }, + "options": { + "summaryTrendStats": [ + "avg", + "min", + "med", + "max", + "p(90)", + "p(95)" + ], + "summaryTimeUnit": "", + "noColor": false + }, + "state": { + "testRunDurationMs": 17001.441, + "isStdOutTTY": true, + "isStdErrTTY": true + }, + "metrics": { + "http_reqs": { + "type": "counter", + "contains": "default", + "values": { + "rate": 5232.321189715624, + "count": 88957 + } + }, + "http_req_blocked": { + "type": "trend", + "contains": "time", + "values": { + "min": 0, + "med": 0.001, + "max": 77.459, + "p(90)": 0.002, + "p(95)": 0.004, + "avg": 0.08780094877315953 + } + }, + "iterations": { + "values": { + "rate": 5232.321189715624, + "count": 88957 + }, + "type": "counter", + "contains": "default" + }, + "http_req_failed": { + "type": "rate", + "contains": "default", + "values": { + "fails": 88957, + "rate": 0, + "passes": 0 + }, + "thresholds": { + "rate<0.2": { + "ok": true + } + } + }, + "iteration_duration": { + "type": "trend", + "contains": "time", + "values": { + "avg": 283.06402668955275, + "min": 0.892459, + "med": 296.839375, + "max": 912.909875, + "p(90)": 387.19262480000003, + "p(95)": 425.50211659999997 + } + }, + "vus_max": { + "type": "gauge", + "contains": "default", + "values": { + "min": 2000, + "max": 2000, + "value": 2000 + } + }, + "http_req_waiting": { + "type": "trend", + "contains": "time", + "values": { + "avg": 281.9709629034241, + "min": 0.818, + "med": 296.393, + "max": 912.853, + "p(90)": 384.4694, + "p(95)": 422.2887999999999 + } + }, + "http_req_receiving": { + "type": "trend", + "contains": "time", + "values": { + "avg": 0.8390701237676371, + "min": 0.005, + "med": 0.014, + "max": 105.463, + "p(90)": 0.157, + "p(95)": 0.288 + } + }, + "http_req_connecting": { + "type": "trend", + "contains": "time", + "values": { + "avg": 0.08498199129916698, + "min": 0, + "med": 0, + "max": 77.434, + "p(90)": 0, + "p(95)": 0 + } + }, + "vus": { + "values": { + "value": 105, + "min": 1, + "max": 2000 + }, + "type": "gauge", + "contains": "default" + }, + "data_sent": { + "type": "counter", + "contains": "data", + "values": { + "count": 97763743, + "rate": 5750320.987497471 + } + }, + "data_received": { + "type": "counter", + "contains": "data", + "values": { + "count": 106036934, + "rate": 6236938.033664323 + } + }, + "http_req_duration{expected_response:true}": { + "type": "trend", + "contains": "time", + "values": { + "med": 296.696, + "max": 912.867, + "p(90)": 387.0536, + "p(95)": 425.3459999999999, + "avg": 282.8300397270564, + "min": 0.862 + } + }, + "http_req_tls_handshaking": { + "type": "trend", + "contains": "time", + "values": { + "med": 0, + "max": 0, + "p(90)": 0, + "p(95)": 0, + "avg": 0, + "min": 0 + } + }, + "http_req_sending": { + "contains": "time", + "values": { + "p(90)": 0.008, + "p(95)": 0.015, + "avg": 0.02000669986619526, + "min": 0.002, + "med": 0.003, + "max": 107.079 + }, + "type": "trend" + }, + "checks": { + "type": "rate", + "contains": "default", + "values": { + "passes": 88957, + "fails": 0, + "rate": 1 + } + }, + "http_req_duration": { + "contains": "time", + "values": { + "avg": 282.8300397270564, + "min": 0.862, + "med": 296.696, + "max": 912.867, + "p(90)": 387.0536, + "p(95)": 425.3459999999999 + }, + "thresholds": { + "p(95)<1500": { + "ok": true + } + }, + "type": "trend" + } + } +} \ No newline at end of file diff --git a/tests/performance/results/summary_stress_get_all_courses_2025-12-08T17-52-37-938Z.json b/tests/performance/results/summary_stress_get_all_courses_2025-12-08T17-52-37-938Z.json new file mode 100644 index 0000000..715d20a --- /dev/null +++ b/tests/performance/results/summary_stress_get_all_courses_2025-12-08T17-52-37-938Z.json @@ -0,0 +1,210 @@ +{ + "root_group": { + "id": "d41d8cd98f00b204e9800998ecf8427e", + "groups": [], + "checks": [ + { + "id": "f02acc4eeef4e7a41d7801aaf3fd8a9d", + "passes": 0, + "fails": 2616135, + "name": "GET /api/courses status 200", + "path": "::GET /api/courses status 200" + } + ], + "name": "", + "path": "" + }, + "options": { + "summaryTimeUnit": "", + "noColor": false, + "summaryTrendStats": [ + "avg", + "min", + "med", + "max", + "p(90)", + "p(95)" + ] + }, + "state": { + "isStdOutTTY": true, + "isStdErrTTY": true, + "testRunDurationMs": 85001.406 + }, + "metrics": { + "vus_max": { + "type": "gauge", + "contains": "default", + "values": { + "value": 2000, + "min": 2000, + "max": 2000 + } + }, + "iterations": { + "contains": "default", + "values": { + "count": 2616135, + "rate": 30777.54972664805 + }, + "type": "counter" + }, + "http_req_sending": { + "values": { + "avg": 0.05843801600354774, + "min": 0.001, + "med": 0.003, + "max": 133.096, + "p(90)": 0.008, + "p(95)": 0.016 + }, + "type": "trend", + "contains": "time" + }, + "data_received": { + "contains": "data", + "values": { + "count": 1444585301, + "rate": 16994840.073586546 + }, + "type": "counter" + }, + "checks": { + "type": "rate", + "contains": "default", + "values": { + "rate": 0, + "passes": 0, + "fails": 2616135 + } + }, + "iteration_duration": { + "type": "trend", + "contains": "time", + "values": { + "avg": 26.15504649736695, + "min": 0.198542, + "med": 21.873667, + "max": 461.378875, + "p(90)": 53.584950000000006, + "p(95)": 64.65959559999999 + } + }, + "http_req_duration": { + "type": "trend", + "contains": "time", + "values": { + "avg": 18.474802442916353, + "min": 0.18, + "med": 13.522, + "max": 460.526, + "p(90)": 40.894, + "p(95)": 51.54 + }, + "thresholds": { + "p(95)<1000": { + "ok": true + } + } + }, + "http_req_tls_handshaking": { + "type": "trend", + "contains": "time", + "values": { + "med": 0, + "max": 0, + "p(90)": 0, + "p(95)": 0, + "avg": 0, + "min": 0 + } + }, + "http_reqs": { + "contains": "default", + "values": { + "count": 2616135, + "rate": 30777.54972664805 + }, + "type": "counter" + }, + "http_req_blocked": { + "type": "trend", + "contains": "time", + "values": { + "p(95)": 0.003, + "avg": 0.1207288645241876, + "min": 0, + "med": 0.001, + "max": 124.99, + "p(90)": 0.002 + } + }, + "http_req_connecting": { + "type": "trend", + "contains": "time", + "values": { + "avg": 0.11568039264028825, + "min": 0, + "med": 0, + "max": 124.938, + "p(90)": 0, + "p(95)": 0 + } + }, + "http_req_receiving": { + "type": "trend", + "contains": "time", + "values": { + "avg": 0.10442196255082742, + "min": 0.003, + "med": 0.007, + "max": 167.395, + "p(90)": 0.017, + "p(95)": 0.034 + } + }, + "data_sent": { + "values": { + "rate": 33824527.14958621, + "count": 2875132365 + }, + "type": "counter", + "contains": "data" + }, + "http_req_waiting": { + "type": "trend", + "contains": "time", + "values": { + "avg": 18.31194246436071, + "min": 0.171, + "med": 13.431, + "max": 460.468, + "p(90)": 40.676, + "p(95)": 51.21 + } + }, + "http_req_failed": { + "type": "rate", + "contains": "default", + "values": { + "passes": 2616135, + "fails": 0, + "rate": 1 + }, + "thresholds": { + "rate<0.2": { + "ok": false + } + } + }, + "vus": { + "contains": "default", + "values": { + "value": 18, + "min": 18, + "max": 1997 + }, + "type": "gauge" + } + } +} \ No newline at end of file