diff --git a/src/main/java/de/tum/in/www1/artemis/config/MetricsBean.java b/src/main/java/de/tum/in/www1/artemis/config/MetricsBean.java index 47e02b7a6233..31a23f52c6e6 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/MetricsBean.java +++ b/src/main/java/de/tum/in/www1/artemis/config/MetricsBean.java @@ -41,10 +41,7 @@ import de.tum.in.www1.artemis.repository.StudentExamRepository; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.security.SecurityUtils; -import io.micrometer.core.instrument.Gauge; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.MultiGauge; -import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.*; @Component public class MetricsBean { @@ -57,6 +54,8 @@ public class MetricsBean { private static final String ARTEMIS_HEALTH_TAG = "healthindicator"; + private static final String NO_SEMESTER_TAG = "No semester"; + private static final int LOGGING_DELAY_SECONDS = 10; /** @@ -331,7 +330,7 @@ interface NowEndDateActiveUserNamesMetricsEntryFunction { * - the current date * - an end date (which is the current date + one of the values of MINUTE_RANGES_LOOKAHEAD), and * - a list of active users (that is, users that created a submission within the last 14 days). - * + *

* The implementing method may decide to ignore certain arguments. * The method returns a list of ExerciseTypeMetricsEntries, that each correspond to an exercise type and a value. * @@ -350,7 +349,7 @@ interface NowEndDateActiveUserNamesIntegerFunction { * This interface is used to calculate and expose metrics that are based on * - the current date, and * - an end date (which is the current date + one of the values of MINUTE_RANGES_LOOKAHEAD). - * + *

* The implementing method may decide to ignore certain arguments. * The method returns an integer, representing the corresponding value. * @@ -471,32 +470,57 @@ public void updatePublicArtemisMetrics() { } private void updateActiveUserMultiGauge(ZonedDateTime now) { - var activeUserPeriodsInDays = new Integer[] { 1, 7, 14, 30 }; - activeUserMultiGauge.register(Stream.of(activeUserPeriodsInDays) - .map(periodInDays -> MultiGauge.Row.of(Tags.of("period", periodInDays.toString()), statisticsRepository.countActiveUsers(now.minusDays(periodInDays), now))) - // A mutable list is required here because otherwise the values can not be updated correctly - .collect(Collectors.toCollection(ArrayList::new)), true); + final Integer[] activeUserPeriodsInDays = new Integer[] { 1, 7, 14, 30 }; + + // A mutable list is required here because otherwise the values can not be updated correctly + final List> gauges = Stream.of(activeUserPeriodsInDays).map(periodInDays -> { + final Tags tags = Tags.of("period", periodInDays.toString()); + final long activeUsers = statisticsRepository.countActiveUsers(now.minusDays(periodInDays), now); + return MultiGauge.Row.of(tags, activeUsers); + }).collect(Collectors.toCollection(ArrayList::new)); + + activeUserMultiGauge.register(gauges, true); } private void updateStudentsCourseMultiGauge(List activeCourses) { - studentsCourseGauge.register( - activeCourses.stream().map(course -> MultiGauge.Row.of(Tags.of("courseName", course.getTitle(), "semester", course.getSemester()), course.getNumberOfStudents())) - // A mutable list is required here because otherwise the values can not be updated correctly - .collect(Collectors.toCollection(ArrayList::new)), - true); + // A mutable list is required here because otherwise the values can not be updated correctly + final List> gauges = activeCourses.stream().map(course -> { + final Tags tags = Tags.of("courseId", Long.toString(course.getId()), "courseName", course.getTitle(), "semester", course.getSemester()); + final long studentCount = course.getNumberOfStudents(); + return MultiGauge.Row.of(tags, studentCount); + }).collect(Collectors.toCollection(ArrayList::new)); + + studentsCourseGauge.register(gauges, true); } private void updateStudentsExamMultiGauge(List examsInActiveCourses, List courses) { - studentsExamGauge.register(examsInActiveCourses.stream() - .map(exam -> MultiGauge.Row.of(Tags.of("examName", exam.getTitle(), "semester", getExamSemester(courses, exam)), - studentExamRepository.findByExamId(exam.getId()).size())) - // A mutable list is required here because otherwise the values can not be updated correctly - .collect(Collectors.toCollection(ArrayList::new)), true); + // A mutable list is required here because otherwise the values can not be updated correctly + final List> gauges = examsInActiveCourses.stream().map(exam -> { + final Tags tags = getExamMetricTags(courses, exam); + final long studentCount = studentExamRepository.countByExamId(exam.getId()); + return MultiGauge.Row.of(tags, studentCount); + }).collect(Collectors.toCollection(ArrayList::new)); + + studentsExamGauge.register(gauges, true); + } + + private Tags getExamMetricTags(final List courses, final Exam exam) { + final Optional examCourse = findExamCourse(courses, exam); + + final List tags = new ArrayList<>(); + examCourse.ifPresent(course -> { + tags.add(Tag.of("courseId", Long.toString(course.getId()))); + tags.add(Tag.of("courseName", course.getTitle())); + }); + tags.add(Tag.of("examId", Long.toString(exam.getId()))); + tags.add(Tag.of("examName", exam.getTitle())); + tags.add(Tag.of("semester", examCourse.map(Course::getSemester).orElse(NO_SEMESTER_TAG))); + + return Tags.of(tags); } - private String getExamSemester(final List courses, final Exam exam) { - // The exam.getCourse() is not populated (the semester property is not set) -> Use course from the courses list, which contains the semester - return courses.stream().filter(course -> Objects.equals(course.getId(), exam.getCourse().getId())).findAny().map(Course::getSemester).orElse("No semester"); + private Optional findExamCourse(final List courses, final Exam exam) { + return courses.stream().filter(course -> Objects.equals(course.getId(), exam.getCourse().getId())).findAny(); } private void updateActiveExerciseMultiGauge() { @@ -530,7 +554,7 @@ private void extractExerciseTypeMetricsAndAddToMetricsResults(List courses) { courses.forEach(course -> { if (course.getSemester() == null) { - course.setSemester("No semester"); + course.setSemester(NO_SEMESTER_TAG); } if (course.getTitle() == null) { if (course.getShortName() != null) { diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java index b880005a94d2..8f91a855d803 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java @@ -71,10 +71,18 @@ public interface StudentExamRepository extends JpaRepository SELECT se FROM StudentExam se WHERE se.exam.id = :examId - AND se.testRun = FALSE + AND se.testRun = FALSE """) Set findByExamId(@Param("examId") long examId); + @Query(""" + SELECT COUNT(DISTINCT se) + FROM StudentExam se + WHERE se.exam.id = :examId + AND se.testRun = FALSE + """) + long countByExamId(@Param("examId") long examId); + @Query(""" SELECT se FROM StudentExam se diff --git a/src/test/java/de/tum/in/www1/artemis/config/MetricsBeanTest.java b/src/test/java/de/tum/in/www1/artemis/config/MetricsBeanTest.java index 452203bea36c..f4074f1ad64b 100644 --- a/src/test/java/de/tum/in/www1/artemis/config/MetricsBeanTest.java +++ b/src/test/java/de/tum/in/www1/artemis/config/MetricsBeanTest.java @@ -225,11 +225,11 @@ void testPublicMetricsExams() { metricsBean.updatePublicArtemisMetrics(); - var totalNumberOfExams = examRepository.count(); - var numberOfActiveExams = examRepository.countAllActiveExams(ZonedDateTime.now()); + long totalNumberOfExams = examRepository.count(); + long numberOfActiveExams = examRepository.countAllActiveExams(ZonedDateTime.now()); // Assert that there is at least one non-active exam in the database so that the values returned from the metrics are different - assertThat(numberOfActiveExams.intValue()).isNotEqualTo(totalNumberOfExams); + assertThat(numberOfActiveExams).isNotEqualTo(totalNumberOfExams); assertMetricEquals(totalNumberOfExams, "artemis.statistics.public.exams"); assertMetricEquals(numberOfActiveExams, "artemis.statistics.public.active_exams"); @@ -240,9 +240,10 @@ void testPublicMetricsCourseAndExamStudents() { var users = userUtilService.addUsers(TEST_PREFIX, 3, 0, 0, 0); var course1 = courseUtilService.createCourse(); + course1.setTitle("Course 1"); course1.setSemester(null); course1.setStudentGroupName(TEST_PREFIX + "course1Students"); - courseRepository.save(course1); + course1 = courseRepository.save(course1); var exam1 = examUtilService.addExamWithModellingAndTextAndFileUploadAndQuizAndEmptyGroup(course1); exam1.setStartDate(ZonedDateTime.now().minusMinutes(1)); exam1.setTitle("exam" + UUID.randomUUID()); @@ -253,7 +254,7 @@ void testPublicMetricsCourseAndExamStudents() { var course2 = courseUtilService.createCourse(); course2.setSemester("WS 2023/24"); course2.setStudentGroupName(TEST_PREFIX + "course2Students"); - courseRepository.save(course2); + course2 = courseRepository.save(course2); var exam2 = examUtilService.addExamWithModellingAndTextAndFileUploadAndQuizAndEmptyGroup(course2); exam2.setTitle("exam" + UUID.randomUUID()); exam2.setStartDate(ZonedDateTime.now().minusMinutes(1)); @@ -269,11 +270,16 @@ void testPublicMetricsCourseAndExamStudents() { metricsBean.updatePublicArtemisMetrics(); - assertMetricEquals(3, "artemis.statistics.public.course_students", "courseName", course1.getTitle(), "semester", "No semester"); - assertMetricEquals(1, "artemis.statistics.public.course_students", "courseName", course2.getTitle(), "semester", "WS 2023/24"); + String course1Id = Long.toString(course1.getId()); + String course2Id = Long.toString(course2.getId()); + + assertMetricEquals(3, "artemis.statistics.public.course_students", "courseId", course1Id, "courseName", course1.getTitle(), "semester", "No semester"); + assertMetricEquals(1, "artemis.statistics.public.course_students", "courseId", course2Id, "courseName", course2.getTitle(), "semester", "WS 2023/24"); - assertMetricEquals(2, "artemis.statistics.public.exam_students", "examName", exam1.getTitle(), "semester", "No semester"); - assertMetricEquals(1, "artemis.statistics.public.exam_students", "examName", exam2.getTitle(), "semester", "WS 2023/24"); + assertMetricEquals(2, "artemis.statistics.public.exam_students", "courseId", course1Id, "courseName", course1.getTitle(), "examId", Long.toString(exam1.getId()), + "examName", exam1.getTitle(), "semester", "No semester"); + assertMetricEquals(1, "artemis.statistics.public.exam_students", "courseId", course2Id, "examId", Long.toString(exam2.getId()), "examName", exam2.getTitle(), "semester", + "WS 2023/24"); } @Test