implemented 09-system-metrics.md: added system metrics: introduced system monitoring with a metrics collector, REST API endpoint, Thymeleaf UI rendering, and lifecycle management
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
package de.hoennig.gittally.server
|
||||
|
||||
import de.hoennig.gittally.metrics.SystemMetricsCollector
|
||||
import jakarta.annotation.PreDestroy
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
/**
|
||||
* Starts the system-metrics sampling loop once the server context is ready and
|
||||
* stops it on shutdown. Only in the `server` profile, like [ServerWatcherLifecycle].
|
||||
*/
|
||||
@Component
|
||||
@Profile("server")
|
||||
class ServerMetricsLifecycle(
|
||||
private val collector: SystemMetricsCollector,
|
||||
) {
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
collector.start()
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
fun onShutdown() {
|
||||
collector.stop()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.hoennig.gittally.server
|
||||
|
||||
import de.hoennig.gittally.metrics.SystemMetrics
|
||||
import de.hoennig.gittally.metrics.SystemMetricsCollector
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
class SystemApiController(
|
||||
private val collector: SystemMetricsCollector,
|
||||
) {
|
||||
/**
|
||||
* The current system snapshot plus min/max/avg aggregates (the legacy
|
||||
* `system.json`). Unavailable metrics are null, never an error.
|
||||
*/
|
||||
@GetMapping("/api/system")
|
||||
fun system(): SystemMetrics = collector.snapshot()
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import de.hoennig.gittally.build.BuildExecutor
|
||||
import de.hoennig.gittally.build.BuildResultRepository
|
||||
import de.hoennig.gittally.build.BuildStatus
|
||||
import de.hoennig.gittally.config.ConfigLoader
|
||||
import de.hoennig.gittally.metrics.SystemMetricsCollector
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.boot.info.BuildProperties
|
||||
import org.springframework.http.HttpStatus
|
||||
@@ -32,6 +33,7 @@ class UiController(
|
||||
private val artifactStore: ArtifactStore,
|
||||
private val controlTokens: ControlTokenService,
|
||||
private val configLoader: ConfigLoader,
|
||||
private val metricsCollector: SystemMetricsCollector,
|
||||
private val buildProperties: ObjectProvider<BuildProperties>,
|
||||
) {
|
||||
var workingDir: Path = Paths.get(".")
|
||||
@@ -80,6 +82,13 @@ class UiController(
|
||||
return "current"
|
||||
}
|
||||
|
||||
@GetMapping("/system")
|
||||
fun system(model: Model): String {
|
||||
baseModel(model, view = "system", pageTitle = "System Metrics")
|
||||
model.addAttribute("metrics", SystemMetricsView.from(metricsCollector.snapshot()))
|
||||
return "system"
|
||||
}
|
||||
|
||||
/** Artifact index rendered from the artifact store — legacy pre-generated this page as static HTML. */
|
||||
@GetMapping("/builds/{artifactKey}")
|
||||
fun artifactIndex(
|
||||
|
||||
@@ -2,12 +2,15 @@ package de.hoennig.gittally.server
|
||||
|
||||
import de.hoennig.gittally.build.BuildResult
|
||||
import de.hoennig.gittally.config.GiteaConfig
|
||||
import de.hoennig.gittally.metrics.MetricAggregate
|
||||
import de.hoennig.gittally.metrics.SystemMetrics
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
|
||||
/** Links into the Gitea web UI, like legacy `gitea_branch_web_url`; null when Gitea is not configured. */
|
||||
class GiteaWebLinks(
|
||||
@@ -33,6 +36,8 @@ class GiteaWebLinks(
|
||||
object UiFormats {
|
||||
private val timestampFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.systemDefault())
|
||||
|
||||
private val timeOfDayFormat = DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault())
|
||||
|
||||
fun timestamp(instant: Instant): String = timestampFormat.format(instant)
|
||||
|
||||
/** `m:ss`, or `h:mm:ss` from one hour — like the legacy `MM:SS` duration column. */
|
||||
@@ -50,6 +55,16 @@ object UiFormats {
|
||||
"%d:%02d".format(minutes, rest)
|
||||
}
|
||||
}
|
||||
|
||||
/** Two-decimal metric value with a dot like JS `toFixed(2)`; `n/a` when the source is unavailable. */
|
||||
fun metric(value: Double?): String =
|
||||
if (value == null || !value.isFinite()) {
|
||||
"n/a"
|
||||
} else {
|
||||
String.format(Locale.ROOT, "%.2f", value)
|
||||
}
|
||||
|
||||
fun timeOfDay(instant: Instant): String = timeOfDayFormat.format(instant)
|
||||
}
|
||||
|
||||
/** One row of the latest/history build tables. */
|
||||
@@ -96,3 +111,60 @@ data class CurrentBuildView(
|
||||
val branchUrl: String?,
|
||||
val commitUrl: String?,
|
||||
)
|
||||
|
||||
/**
|
||||
* One row of the system-metrics table. The [key] matches the JSON field of
|
||||
* `GET /api/system`, so `gittally.js` can update the cells in place.
|
||||
*/
|
||||
data class MetricRowView(
|
||||
val key: String,
|
||||
val label: String,
|
||||
val current: String,
|
||||
val min: String,
|
||||
val max: String,
|
||||
val avg: String,
|
||||
) {
|
||||
companion object {
|
||||
fun from(
|
||||
key: String,
|
||||
label: String,
|
||||
aggregate: MetricAggregate?,
|
||||
) = MetricRowView(
|
||||
key = key,
|
||||
label = label,
|
||||
current = UiFormats.metric(aggregate?.current),
|
||||
min = UiFormats.metric(aggregate?.min),
|
||||
max = UiFormats.metric(aggregate?.max),
|
||||
avg = UiFormats.metric(aggregate?.avg),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** The system view: the metric rows plus the totals/updated info line, like the legacy system page. */
|
||||
data class SystemMetricsView(
|
||||
val rows: List<MetricRowView>,
|
||||
val cpuCount: String,
|
||||
val ramTotal: String,
|
||||
val diskTotal: String,
|
||||
val updated: String,
|
||||
) {
|
||||
companion object {
|
||||
fun from(metrics: SystemMetrics) =
|
||||
SystemMetricsView(
|
||||
rows =
|
||||
listOf(
|
||||
MetricRowView.from("cpuUsed", "CPU used (cores)", metrics.cpuUsed),
|
||||
MetricRowView.from("cpuIdle", "CPU idle (cores)", metrics.cpuIdle),
|
||||
MetricRowView.from("ramUsedGib", "RAM used (GiB)", metrics.ramUsedGib),
|
||||
MetricRowView.from("ramFreeGib", "RAM free (GiB)", metrics.ramFreeGib),
|
||||
MetricRowView.from("diskUsedGib", "Disk used (GiB)", metrics.diskUsedGib),
|
||||
MetricRowView.from("diskFreeGib", "Disk free (GiB)", metrics.diskFreeGib),
|
||||
MetricRowView.from("repoSizeGib", "Repo size (GiB)", metrics.repoSizeGib),
|
||||
),
|
||||
cpuCount = "${metrics.cpuCount} cores",
|
||||
ramTotal = metrics.ramTotalGib?.let { "${UiFormats.metric(it)} GiB" } ?: "n/a",
|
||||
diskTotal = metrics.diskTotalGib?.let { "${UiFormats.metric(it)} GiB" } ?: "n/a",
|
||||
updated = metrics.timestamp?.let { UiFormats.timeOfDay(it) } ?: "n/a",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user