Skip to content

Commit eb9d42e

Browse files
authored
fix stats related bugs of paused monitors (#236)
1 parent f537dc6 commit eb9d42e

File tree

6 files changed

+148
-7
lines changed

6 files changed

+148
-7
lines changed

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,13 @@ class IncidentRepository(private val dslContext: DSLContext) {
6666
.from(HTTP_UPTIME_EVENT)
6767
.join(HTTP_MONITOR).on(HTTP_UPTIME_EVENT.MONITOR_ID.eq(HTTP_MONITOR.ID))
6868
.where(HTTP_UPTIME_EVENT.STATUS.eq(UptimeStatus.DOWN))
69-
.and(HTTP_MONITOR.ENABLED.isTrue)
7069
.apply {
7170
// Filter for monitors
72-
monitorId?.let { and(HTTP_MONITOR.ID.eq(it)) }
71+
if (monitorId != null) {
72+
and(HTTP_MONITOR.ID.eq(monitorId))
73+
} else {
74+
and(HTTP_MONITOR.ENABLED.isTrue)
75+
}
7376
// Filter for events that were open at any point during the specified period
7477
period?.let {
7578
val periodStart = getCurrentTimestamp().minus(period)
@@ -103,10 +106,13 @@ class IncidentRepository(private val dslContext: DSLContext) {
103106
.from(SSL_EVENT)
104107
.join(HTTP_MONITOR).on(SSL_EVENT.MONITOR_ID.eq(HTTP_MONITOR.ID))
105108
.where(SSL_EVENT.STATUS.eq(SslStatus.INVALID))
106-
.and(HTTP_MONITOR.ENABLED.isTrue)
107109
.apply {
108110
// Filter for monitors
109-
monitorId?.let { and(HTTP_MONITOR.ID.eq(it)) }
111+
if (monitorId != null) {
112+
and(HTTP_MONITOR.ID.eq(monitorId))
113+
} else {
114+
and(HTTP_MONITOR.ENABLED.isTrue).and(HTTP_MONITOR.SSL_CHECK_ENABLED.isTrue)
115+
}
110116
// Filter for events that were open at any point during the specified period
111117
period?.let {
112118
val periodStart = getCurrentTimestamp().minus(period)

app/src/main/kotlin/com/kuvaszuptime/kuvasz/services/StatCalculator.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ class StatCalculator(
102102
var historicalDowntimeSeconds = 0L
103103

104104
uptimeEvents.forEach { uptimeEvent ->
105+
if (!uptimeEvent.isMonitorEnabled && uptimeEvent.updatedAt.isBefore(periodStart)) {
106+
// If the monitor was disabled and the last update was before the period then we skip this event
107+
return@forEach
108+
}
105109
val effectiveStartDate = maxOf(uptimeEvent.startedAt, periodStart)
106110
val duration = getDurationOfEvent(
107111
isMonitorEnabled = uptimeEvent.isMonitorEnabled,

app/src/test/kotlin/com/kuvaszuptime/kuvasz/repositories/IncidentRepositoryTest.kt

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ class IncidentRepositoryTest(
379379
then("it should return the incident of that monitor only") {
380380

381381
val monitor1 = createMonitor(httpMonitorRepository)
382-
val monitor2 = createMonitor(httpMonitorRepository)
382+
val monitor2 = createMonitor(httpMonitorRepository, enabled = false)
383383

384384
val openDownMonitor1 = createUptimeEventRecord(
385385
dslContext,
@@ -410,7 +410,7 @@ class IncidentRepositoryTest(
410410
endedAt = getCurrentTimestamp().minusDays(5),
411411
)
412412

413-
createUptimeEventRecord(
413+
val openDownMonitor2 = createUptimeEventRecord(
414414
dslContext,
415415
monitorId = monitor2.id,
416416
status = UptimeStatus.DOWN,
@@ -425,7 +425,7 @@ class IncidentRepositoryTest(
425425
startedAt = getCurrentTimestamp().minusDays(6),
426426
endedAt = getCurrentTimestamp().minusDays(4),
427427
)
428-
createSSLEventRecord(
428+
val openInvalidMonitor2 = createSSLEventRecord(
429429
dslContext,
430430
monitorId = monitor2.id,
431431
status = SslStatus.INVALID,
@@ -458,6 +458,26 @@ class IncidentRepositoryTest(
458458
openInvalidSSLMonitor1.status shouldBe IncidentStatus.ONGOING
459459
openInvalidSSLMonitor1.incidentType shouldBe IncidentType.SSL
460460
}
461+
462+
// Should return events for the disabled monitor as well
463+
val incidentsOfMonitor2 = incidentRepository.getIncidents(
464+
includeResolved = false,
465+
monitorId = monitor2.id,
466+
)
467+
468+
incidentsOfMonitor2 shouldHaveSize 2
469+
470+
incidentsOfMonitor2.forOne { openMonitor2 ->
471+
openMonitor2.monitorId shouldBe openDownMonitor2.monitorId
472+
openMonitor2.status shouldBe IncidentStatus.ONGOING
473+
openMonitor2.incidentType shouldBe IncidentType.HTTP
474+
}
475+
476+
incidentsOfMonitor2.forOne { openInvalidSSLMonitor2 ->
477+
openInvalidSSLMonitor2.monitorId shouldBe openInvalidMonitor2.monitorId
478+
openInvalidSSLMonitor2.status shouldBe IncidentStatus.ONGOING
479+
openInvalidSSLMonitor2.incidentType shouldBe IncidentType.SSL
480+
}
461481
}
462482
}
463483
}

app/src/test/kotlin/com/kuvaszuptime/kuvasz/services/StatCalculatorTest.kt

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,90 @@ class StatCalculatorTest(
106106
}
107107
}
108108

109+
`when`("there is a paused monitor - last update before the period") {
110+
111+
val enabledUpMonitor = createMonitor(monitorRepository, enabled = true)
112+
val enabledDownMonitor = createMonitor(monitorRepository, enabled = true)
113+
val pausedMonitor = createMonitor(monitorRepository, enabled = false)
114+
val now = getCurrentTimestamp()
115+
116+
// enabledUpMonitor's incidents
117+
createUptimeEventRecord(
118+
dslContext = dslContext,
119+
monitorId = enabledUpMonitor.id,
120+
startedAt = now.minusDays(10),
121+
status = UptimeStatus.DOWN,
122+
endedAt = now.minusDays(5), // 5 days DOWN, 1 day in the period
123+
)
124+
createUptimeEventRecord(
125+
dslContext = dslContext,
126+
monitorId = enabledUpMonitor.id,
127+
startedAt = now.minusDays(5),
128+
status = UptimeStatus.UP,
129+
endedAt = null, // 5 days UP
130+
)
131+
createSSLEventRecord(
132+
dslContext = dslContext,
133+
monitorId = enabledUpMonitor.id,
134+
status = SslStatus.INVALID,
135+
startedAt = now.minusDays(10),
136+
endedAt = null,
137+
)
138+
// enabledDownMonitor's incidents
139+
createUptimeEventRecord(
140+
dslContext = dslContext,
141+
monitorId = enabledDownMonitor.id,
142+
startedAt = now.minusHours(12),
143+
status = UptimeStatus.DOWN,
144+
endedAt = null, // 0.5 day DOWN
145+
)
146+
createSSLEventRecord(
147+
dslContext = dslContext,
148+
monitorId = enabledDownMonitor.id,
149+
status = SslStatus.VALID,
150+
startedAt = now.minusDays(10),
151+
endedAt = null,
152+
)
153+
// pausedMonitor's incidents
154+
createUptimeEventRecord(
155+
dslContext = dslContext,
156+
monitorId = pausedMonitor.id,
157+
startedAt = now.minusDays(12),
158+
status = UptimeStatus.DOWN,
159+
endedAt = null,
160+
updatedAt = now.minusDays(8),
161+
)
162+
createSSLEventRecord(
163+
dslContext = dslContext,
164+
monitorId = pausedMonitor.id,
165+
status = SslStatus.VALID,
166+
startedAt = now.minusDays(10),
167+
endedAt = null,
168+
updatedAt = now.minusDays(7)
169+
)
170+
171+
then("it should not count the obsolete events from the paused monitor") {
172+
val stats = statCalculator.calculateOverallHttpStats(Duration.ofDays(6))
173+
stats.actual.uptimeStats.total shouldBe 3 // 2 enabled monitors + 1 paused monitor
174+
stats.actual.uptimeStats.down shouldBe 1
175+
stats.actual.uptimeStats.up shouldBe 1
176+
stats.actual.uptimeStats.paused shouldBe 1
177+
stats.actual.uptimeStats.inProgress shouldBe 0
178+
179+
stats.actual.sslStats.valid shouldBe 1
180+
stats.actual.sslStats.invalid shouldBe 1
181+
stats.actual.sslStats.willExpire shouldBe 0
182+
stats.actual.sslStats.inProgress shouldBe 0
183+
184+
stats.history.uptimeStats.incidents shouldBe 2
185+
stats.history.uptimeStats.affectedMonitors shouldBe 2
186+
// 1.5 days DOWN inside the period
187+
stats.history.uptimeStats.totalDowntimeSeconds shouldBe 36 * 60 * 60
188+
// 5 days UP, 1.5 days DOWN
189+
stats.history.uptimeStats.uptimeRatio shouldBe 5.toDouble() / 6.5
190+
}
191+
}
192+
109193
`when`("there is a monitor that was just created") {
110194

111195
createMonitor(monitorRepository, enabled = true, sslCheckEnabled = true)
@@ -410,6 +494,7 @@ class StatCalculatorTest(
410494
val upMonitor = createMonitor(monitorRepository, enabled = true)
411495
val downMonitor = createMonitor(monitorRepository, enabled = true)
412496
val pausedMonitor = createMonitor(monitorRepository, enabled = false)
497+
val pausedMonitor2 = createMonitor(monitorRepository, enabled = false)
413498

414499
// upMonitor's events: UP
415500
createUptimeEventRecord(
@@ -447,6 +532,16 @@ class StatCalculatorTest(
447532
endedAt = now.minusDays(2),
448533
)
449534

535+
// pausedMonitor2's events: DOWN, but update date is before the period, so it should not be counted
536+
createUptimeEventRecord(
537+
dslContext = dslContext,
538+
monitorId = pausedMonitor2.id,
539+
startedAt = now.minusDays(10),
540+
status = UptimeStatus.DOWN,
541+
endedAt = null,
542+
updatedAt = now.minusDays(7),
543+
)
544+
450545
then("it should correctly calculate the stats for all statuses") {
451546
val statsOfInProgressUpMonitor = statCalculator.calculateHistoricalHttpUptimeStats(
452547
period = Duration.ofDays(6),
@@ -483,6 +578,15 @@ class StatCalculatorTest(
483578
statsOfPausedMonitor.affectedMonitors shouldBe 1
484579
statsOfPausedMonitor.totalDowntimeSeconds shouldBe 24 * 60 * 60 // 1 day
485580
statsOfPausedMonitor.uptimeRatio shouldBe 0.5
581+
582+
val statsOfPausedMonitor2 = statCalculator.calculateHistoricalHttpUptimeStats(
583+
period = Duration.ofDays(6),
584+
monitorId = pausedMonitor2.id,
585+
)
586+
statsOfPausedMonitor2.incidents shouldBe 0
587+
statsOfPausedMonitor2.affectedMonitors shouldBe 0
588+
statsOfPausedMonitor2.totalDowntimeSeconds shouldBe 0
589+
statsOfPausedMonitor2.uptimeRatio shouldBe null
486590
}
487591
}
488592
}

docs/docs/changelog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 3.0.2 <small>2025-08-31</small> { id="3.0.2" data-toc-label="3.0.2" }
2+
3+
### Fixes
4+
5+
- Fixed a bug where the historical HTTP uptime stats could contain incorrect values in case a paused monitor's ongoing incident was updated before the requested time range of stats
6+
- Fixed a bug where the monitor-specific incidents were not returned in case of a paused monitor
7+
18
## 3.0.1 <small>2025-08-31</small> { id="3.0.1" data-toc-label="3.0.1" }
29

310
### Breaking changes

0 commit comments

Comments
 (0)