Skip to content

Commit 03ad7d9

Browse files
authored
expose incident + add monitor-level metrics (#230)
* implement monitor stats + improve UI around metrics * add more StatCalculator tests * fix the representation of Duration on the API * improve & unify event duration calculation * expose incidents on UI & API
1 parent 250429d commit 03ad7d9

File tree

56 files changed

+2124
-376
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+2124
-376
lines changed

app/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/HttpMonitorControllerV1.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import com.kuvaszuptime.kuvasz.models.dto.HttpMonitorCreateDto
1212
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitorDetailsDto
1313
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitorDto
1414
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitorExportDto
15-
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitorStatsDto
1615
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitoringStatsDto
1716
import com.kuvaszuptime.kuvasz.models.dto.HttpUptimeEventDto
17+
import com.kuvaszuptime.kuvasz.models.dto.LegacyHttpMonitorStatsDto
1818
import com.kuvaszuptime.kuvasz.models.dto.SSLEventDto
1919
import com.kuvaszuptime.kuvasz.services.StatCalculator
2020
import com.kuvaszuptime.kuvasz.services.check.http.HttpMonitorCrudService
@@ -196,7 +196,7 @@ class HttpMonitorControllerV1(
196196
ApiResponse(
197197
responseCode = "200",
198198
description = "Successful query",
199-
content = [Content(schema = Schema(implementation = HttpMonitorStatsDto::class))]
199+
content = [Content(schema = Schema(implementation = LegacyHttpMonitorStatsDto::class))]
200200
),
201201
ApiResponse(
202202
responseCode = "404",
@@ -208,9 +208,9 @@ class HttpMonitorControllerV1(
208208
override fun getMonitorStats(
209209
monitorId: Long,
210210
@QueryValue period: Duration?,
211-
): HttpMonitorStatsDto {
211+
): LegacyHttpMonitorStatsDto {
212212
val effectivePeriod = period ?: Duration.ofDays(MONITOR_STATS_PERIOD_DEFAULT_DAYS)
213-
return monitorCrudService.getMonitorStats(
213+
return monitorCrudService.getLegacyMonitorStats(
214214
monitorId = monitorId,
215215
period = effectivePeriod,
216216
)

app/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/HttpMonitorOperationsV1.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import com.kuvaszuptime.kuvasz.jooq.enums.UptimeStatus
66
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitorCreateDto
77
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitorDetailsDto
88
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitorDto
9-
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitorStatsDto
109
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitorUpdateDto
1110
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitoringStatsDto
1211
import com.kuvaszuptime.kuvasz.models.dto.HttpUptimeEventDto
12+
import com.kuvaszuptime.kuvasz.models.dto.LegacyHttpMonitorStatsDto
1313
import com.kuvaszuptime.kuvasz.models.dto.SSLEventDto
1414
import io.micronaut.http.annotation.Body
1515
import io.micronaut.http.annotation.Delete
@@ -82,10 +82,13 @@ interface HttpMonitorOperationsV1 {
8282
@QueryValue
8383
@Parameter(
8484
required = false,
85-
schema = Schema(implementation = Duration::class, description = "A Java Duration string, default 1d")
85+
schema = Schema(
86+
implementation = Duration::class,
87+
description = "An ISO-8601 Duration string, default P1D",
88+
)
8689
)
8790
period: Duration?,
88-
): HttpMonitorStatsDto
91+
): LegacyHttpMonitorStatsDto
8992

9093
@Operation(summary = "Download the export of all monitors in YAML format", deprecated = true)
9194
@Get("/export/yaml")
@@ -97,7 +100,10 @@ interface HttpMonitorOperationsV1 {
97100
@QueryValue
98101
@Parameter(
99102
required = false,
100-
schema = Schema(implementation = Duration::class, description = "A Java Duration string, default 7d")
103+
schema = Schema(
104+
implementation = Duration::class,
105+
description = "An ISO-8601 Duration string, default P7D",
106+
)
101107
)
102108
period: Duration?,
103109
): HttpMonitoringStatsDto

app/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/HttpMonitorOperationsV2.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,10 @@ interface HttpMonitorOperationsV2 {
7979
@QueryValue
8080
@Parameter(
8181
required = false,
82-
schema = Schema(implementation = Duration::class, description = "A Java Duration string, default 1d")
82+
schema = Schema(
83+
implementation = Duration::class,
84+
description = "An ISO-8601 Duration string, default P1D",
85+
)
8386
)
8487
period: Duration?,
8588
): HttpMonitorStatsDto
@@ -90,7 +93,10 @@ interface HttpMonitorOperationsV2 {
9093
@QueryValue
9194
@Parameter(
9295
required = false,
93-
schema = Schema(implementation = Duration::class, description = "A Java Duration string, default 7d")
96+
schema = Schema(
97+
implementation = Duration::class,
98+
description = "An ISO-8601 Duration string, default P7D",
99+
)
94100
)
95101
period: Duration?,
96102
): HttpMonitoringStatsDto
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.kuvaszuptime.kuvasz.controllers
2+
3+
import com.kuvaszuptime.kuvasz.models.dto.IncidentDto
4+
import com.kuvaszuptime.kuvasz.repositories.IncidentRepository
5+
import io.micronaut.http.MediaType
6+
import io.micronaut.http.annotation.Controller
7+
import io.micronaut.http.annotation.QueryValue
8+
import io.micronaut.scheduling.TaskExecutors
9+
import io.micronaut.scheduling.annotation.ExecuteOn
10+
import io.micronaut.validation.Validated
11+
import io.swagger.v3.oas.annotations.media.ArraySchema
12+
import io.swagger.v3.oas.annotations.media.Content
13+
import io.swagger.v3.oas.annotations.media.Schema
14+
import io.swagger.v3.oas.annotations.responses.ApiResponse
15+
import io.swagger.v3.oas.annotations.responses.ApiResponses
16+
import io.swagger.v3.oas.annotations.security.SecurityRequirement
17+
import io.swagger.v3.oas.annotations.security.SecurityRequirements
18+
import io.swagger.v3.oas.annotations.tags.Tag
19+
import java.time.Duration
20+
21+
@Controller("$API_V2_PREFIX/incidents", produces = [MediaType.APPLICATION_JSON])
22+
@Validated
23+
@Tag(name = "Incidents")
24+
@SecurityRequirements(
25+
SecurityRequirement(name = "apiKey"),
26+
SecurityRequirement(name = "bearerAuth")
27+
)
28+
class IncidentController(
29+
private val incidentRepository: IncidentRepository,
30+
) : IncidentOperations {
31+
32+
@ApiResponses(
33+
ApiResponse(
34+
responseCode = "200",
35+
description = "Successful query",
36+
content = [Content(array = ArraySchema(schema = Schema(implementation = IncidentDto::class)))]
37+
)
38+
)
39+
@ExecuteOn(TaskExecutors.IO)
40+
override fun getIncidents(
41+
@QueryValue monitorId: Long?,
42+
@QueryValue period: Duration?,
43+
@QueryValue includeResolved: Boolean?
44+
): List<IncidentDto> =
45+
incidentRepository.getIncidents(
46+
monitorId = monitorId,
47+
period = period ?: Duration.ofDays(INCIDENTS_PERIOD_DEFAULT_DAYS),
48+
includeResolved = includeResolved ?: INCLUDE_RESOLVED_DEFAULT,
49+
)
50+
51+
companion object {
52+
private const val INCIDENTS_PERIOD_DEFAULT_DAYS = 7L
53+
private const val INCLUDE_RESOLVED_DEFAULT = true
54+
}
55+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.kuvaszuptime.kuvasz.controllers
2+
3+
import com.kuvaszuptime.kuvasz.models.dto.IncidentDto
4+
import io.micronaut.http.annotation.Get
5+
import io.micronaut.http.annotation.QueryValue
6+
import io.swagger.v3.oas.annotations.Operation
7+
import io.swagger.v3.oas.annotations.Parameter
8+
import io.swagger.v3.oas.annotations.media.Schema
9+
import java.time.Duration
10+
11+
interface IncidentOperations {
12+
13+
@Operation(summary = "Get all incidents")
14+
@Get("/")
15+
fun getIncidents(
16+
@QueryValue
17+
@Parameter(required = false)
18+
monitorId: Long?,
19+
@QueryValue
20+
@Parameter(
21+
required = false,
22+
schema = Schema(
23+
implementation = Duration::class,
24+
description = "An ISO-8601 Duration string, default P7D",
25+
)
26+
)
27+
period: Duration?,
28+
@QueryValue
29+
@Parameter(
30+
required = false,
31+
description = "If false, only ongoing incidents are returned, default true",
32+
)
33+
includeResolved: Boolean?,
34+
): List<IncidentDto>
35+
}

app/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/ui/WebUIController.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package com.kuvaszuptime.kuvasz.controllers.ui
22

33
import com.kuvaszuptime.kuvasz.AppGlobals
44
import com.kuvaszuptime.kuvasz.i18n.Messages
5+
import com.kuvaszuptime.kuvasz.repositories.IncidentRepository
56
import com.kuvaszuptime.kuvasz.repositories.SettingsRepository
67
import com.kuvaszuptime.kuvasz.security.ui.UnauthorizedOnly
78
import com.kuvaszuptime.kuvasz.security.ui.WebSecured
89
import com.kuvaszuptime.kuvasz.services.integrations.IntegrationRepository
910
import com.kuvaszuptime.kuvasz.ui.pages.*
11+
import com.kuvaszuptime.kuvasz.util.UIDefaults
1012
import io.micronaut.http.MediaType
1113
import io.micronaut.http.annotation.Controller
1214
import io.micronaut.http.annotation.Get
@@ -15,13 +17,15 @@ import io.micronaut.http.annotation.QueryValue
1517
import io.micronaut.scheduling.TaskExecutors
1618
import io.micronaut.scheduling.annotation.ExecuteOn
1719
import io.swagger.v3.oas.annotations.Hidden
20+
import java.time.Duration
1821

1922
@Controller("/")
2023
@Hidden
2124
class WebUIController(
2225
private val appGlobals: AppGlobals,
2326
private val settingsRepository: SettingsRepository,
2427
private val integrationsRepository: IntegrationRepository,
28+
private val incidentRepository: IncidentRepository,
2529
) {
2630

2731
companion object {
@@ -58,4 +62,21 @@ class WebUIController(
5862
integrations = integrationsRepository.getConfiguredIntegrationDtos().sortedBy { it.name },
5963
settings = settingsRepository.getSettings(),
6064
)
65+
66+
@Get("/incidents")
67+
@WebSecured
68+
@Produces(MediaType.TEXT_HTML)
69+
@ExecuteOn(TaskExecutors.IO)
70+
fun incidents(@QueryValue period: Duration?): String {
71+
val effectivePeriod = period ?: Duration.ofDays(UIDefaults.INCIDENTS_PERIOD_DAYS)
72+
return renderIncidents(
73+
globals = appGlobals,
74+
period = effectivePeriod,
75+
incidents = incidentRepository.getIncidents(
76+
monitorId = null,
77+
period = effectivePeriod,
78+
includeResolved = true,
79+
)
80+
)
81+
}
6182
}

app/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/ui/WebUIHttpMonitorController.kt

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import com.kuvaszuptime.kuvasz.AppGlobals
44
import com.kuvaszuptime.kuvasz.jooq.enums.SslStatus
55
import com.kuvaszuptime.kuvasz.jooq.enums.UptimeStatus
66
import com.kuvaszuptime.kuvasz.jooq.tables.HttpMonitor.HTTP_MONITOR
7+
import com.kuvaszuptime.kuvasz.repositories.HttpMonitorRepository
78
import com.kuvaszuptime.kuvasz.security.ui.WebSecured
89
import com.kuvaszuptime.kuvasz.services.StatCalculator
910
import com.kuvaszuptime.kuvasz.services.check.http.HttpMonitorCrudService
1011
import com.kuvaszuptime.kuvasz.ui.fragments.dashboard.*
1112
import com.kuvaszuptime.kuvasz.ui.fragments.monitor.http.*
1213
import com.kuvaszuptime.kuvasz.ui.pages.*
14+
import com.kuvaszuptime.kuvasz.util.UIDefaults
1315
import io.micronaut.http.MediaType
1416
import io.micronaut.http.annotation.Controller
1517
import io.micronaut.http.annotation.Get
@@ -26,20 +28,20 @@ class WebUIHttpMonitorController(
2628
private val monitorCrudService: HttpMonitorCrudService,
2729
private val appGlobals: AppGlobals,
2830
private val statCalculator: StatCalculator,
31+
private val monitorRepository: HttpMonitorRepository,
2932
) {
3033

3134
companion object {
3235
private const val SSL_EVENTS_COUNT = 5
3336
private const val UPTIME_EVENTS_COUNT = 5
34-
private const val DASHBOARD_STATS_PERIOD_DEFAULT_DAYS = 7L
3537
}
3638

3739
@Get("/http-monitors/fragments/stats")
3840
@WebSecured
3941
@ExecuteOn(TaskExecutors.IO)
4042
@Produces(MediaType.TEXT_HTML)
4143
fun httpMonitoringStats(): String {
42-
val period = Duration.ofDays(DASHBOARD_STATS_PERIOD_DEFAULT_DAYS)
44+
val period = Duration.ofDays(UIDefaults.DASHBOARD_MONITORING_STATS_PERIOD_DAYS)
4345

4446
return renderMonitoringStats(
4547
monitoringStats = statCalculator.calculateOverallHttpStats(period),
@@ -66,7 +68,14 @@ class WebUIHttpMonitorController(
6668
fun httpMonitorDetails(@PathVariable monitorId: Long): String {
6769
val monitor = monitorCrudService.getMonitorDetails(monitorId)
6870

69-
return renderHttpMonitorDetailsPage(appGlobals, monitor)
71+
return renderHttpMonitorDetailsPage(
72+
appGlobals,
73+
monitor,
74+
stats = statCalculator.calculateHistoricalHttpUptimeStats(
75+
period = Duration.ofDays(UIDefaults.HTTP_MONITOR_UPTIME_STATS_PERIOD_DAYS),
76+
monitorId = monitor.id,
77+
),
78+
)
7079
}
7180

7281
@Get("/http-monitors/fragments/list")
@@ -87,7 +96,15 @@ class WebUIHttpMonitorController(
8796
val monitor = monitorCrudService.getMonitorDetails(monitorId)
8897
return buildString {
8998
append(renderHttpMonitorDetailsHeading(monitor))
90-
append(renderUptimeSummary(monitor))
99+
append(
100+
renderUptimeSummary(
101+
monitor = monitor,
102+
stats = statCalculator.calculateHistoricalHttpUptimeStats(
103+
period = Duration.ofDays(UIDefaults.HTTP_MONITOR_UPTIME_STATS_PERIOD_DAYS),
104+
monitorId = monitor.id,
105+
)
106+
)
107+
)
91108
if (monitor.sslCheckEnabled) {
92109
append(renderSSLSummary(monitor))
93110
}
@@ -98,17 +115,23 @@ class WebUIHttpMonitorController(
98115
@WebSecured
99116
@ExecuteOn(TaskExecutors.IO)
100117
@Produces(MediaType.TEXT_HTML)
101-
fun httpMonitorUptimeEvents(@PathVariable monitorId: Long): String =
102-
renderHttpUptimeEvents(
103-
events = monitorCrudService.getUptimeEventsByMonitorId(monitorId, UPTIME_EVENTS_COUNT)
104-
)
118+
fun httpMonitorUptimeEvents(@PathVariable monitorId: Long) =
119+
monitorRepository.findById(monitorId)?.let { monitor ->
120+
renderHttpUptimeEvents(
121+
isMonitorEnabled = monitor.enabled,
122+
events = monitorCrudService.getUptimeEventsByMonitorId(monitorId, UPTIME_EVENTS_COUNT)
123+
)
124+
}
105125

106126
@Get("/http-monitors/fragments/details-ssl-events/{monitorId}")
107127
@WebSecured
108128
@ExecuteOn(TaskExecutors.IO)
109129
@Produces(MediaType.TEXT_HTML)
110130
fun httpMonitorSSLEvents(@PathVariable monitorId: Long) =
111-
renderSSLEvents(
112-
events = monitorCrudService.getSSLEventsByMonitorId(monitorId, SSL_EVENTS_COUNT)
113-
)
131+
monitorRepository.findById(monitorId)?.let { monitor ->
132+
renderSSLEvents(
133+
isSSLCheckEnabled = monitor.enabled && monitor.sslCheckEnabled,
134+
events = monitorCrudService.getSSLEventsByMonitorId(monitorId, SSL_EVENTS_COUNT)
135+
)
136+
}
114137
}

app/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/HttpUptimeEventRepository.kt

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import com.kuvaszuptime.kuvasz.jooq.tables.records.HttpUptimeEventRecord
77
import com.kuvaszuptime.kuvasz.models.dto.HttpUptimeEventDto
88
import com.kuvaszuptime.kuvasz.models.events.HttpMonitorDownEvent
99
import com.kuvaszuptime.kuvasz.models.events.HttpUptimeMonitorEvent
10+
import com.kuvaszuptime.kuvasz.services.UptimeEventCalculationContext
1011
import com.kuvaszuptime.kuvasz.util.fetchOneOrThrow
12+
import com.kuvaszuptime.kuvasz.util.getCurrentTimestamp
1113
import jakarta.inject.Singleton
1214
import org.jooq.DSLContext
1315
import org.jooq.impl.DSL
16+
import java.time.Duration
1417
import java.time.OffsetDateTime
1518

1619
@Singleton
@@ -95,16 +98,28 @@ class HttpUptimeEventRepository(private val dslContext: DSLContext) {
9598
.fetchInto(HttpUptimeEventDto::class.java)
9699

97100
/**
98-
* Fetches all uptime events that have ended or was open within the specified period and are associated with
99-
* enabled monitors.
101+
* Fetches all uptime events that have ended or was open within the specified period.
100102
*/
101-
fun fetchAllInPeriod(periodStart: OffsetDateTime): List<HttpUptimeEventRecord> = dslContext
102-
.select(HTTP_UPTIME_EVENT.asterisk())
103-
.from(HTTP_UPTIME_EVENT)
104-
.join(HTTP_MONITOR).on(HTTP_UPTIME_EVENT.MONITOR_ID.eq(HTTP_MONITOR.ID))
105-
.where(DSL.coalesce(HTTP_UPTIME_EVENT.ENDED_AT, DSL.now()).greaterThan(periodStart))
106-
.and(HTTP_MONITOR.ENABLED.isTrue)
107-
.fetchInto(HttpUptimeEventRecord::class.java)
103+
@Suppress("IgnoredReturnValue")
104+
fun fetchAllInPeriod(period: Duration, monitorId: Long? = null): List<UptimeEventCalculationContext> {
105+
val periodStart = getCurrentTimestamp().minus(period)
106+
return dslContext
107+
.select(
108+
HTTP_MONITOR.ID.`as`(UptimeEventCalculationContext::monitorId.name),
109+
HTTP_MONITOR.ENABLED.`as`(UptimeEventCalculationContext::isMonitorEnabled.name),
110+
HTTP_UPTIME_EVENT.STATUS.`as`(UptimeEventCalculationContext::status.name),
111+
HTTP_UPTIME_EVENT.STARTED_AT.`as`(UptimeEventCalculationContext::startedAt.name),
112+
HTTP_UPTIME_EVENT.ENDED_AT.`as`(UptimeEventCalculationContext::endedAt.name),
113+
HTTP_UPTIME_EVENT.UPDATED_AT.`as`(UptimeEventCalculationContext::updatedAt.name),
114+
)
115+
.from(HTTP_UPTIME_EVENT)
116+
.join(HTTP_MONITOR).on(HTTP_UPTIME_EVENT.MONITOR_ID.eq(HTTP_MONITOR.ID))
117+
.where(DSL.coalesce(HTTP_UPTIME_EVENT.ENDED_AT, DSL.now()).greaterThan(periodStart))
118+
.apply {
119+
monitorId?.let { and(HTTP_UPTIME_EVENT.MONITOR_ID.eq(it)) }
120+
}
121+
.fetchInto(UptimeEventCalculationContext::class.java)
122+
}
108123

109124
/**
110125
* Fetches the timestamp of the latest incident (DOWN status) for enabled monitors.

0 commit comments

Comments
 (0)