feat(metrics): observe felis_start_duration_seconds across the start lifecycle

Wire the fourth mandated §23 metric to a real producer. The histogram
spans two reconcile passes, so anchor and observation must persist in
status:

- Add status.startRequestedAt, set once on the first Starting reconcile
  of a start attempt and cleared on Stopped so the next start re-anchors.
- Observe felis_start_duration_seconds exactly when readiness is first
  reached (ReadySignalAt - StartRequestedAt), guarded so a server that
  reaches ready without a Starting pass records nothing.
- Mirror the field into the deepcopy and the structural CRD schema so the
  apiserver does not prune it on patchStatus round-trips.
- Promote prometheus/client_golang and client_model to direct deps now
  that the operator and its tests import them.

Tests drive a step clock through Starting -> Running asserting the exact
observed duration, and through Running -> Stopped asserting the metric is
observed once and the anchor clears.
This commit is contained in:
flyemoji committed 2026-06-30 13:11:49 +09:00
1 parent 79eae7f669
commit 8ac5e64d8a
6 files changed
+145 -2

No files matched your search

@@ -234,6 +234,13 @@ type MinecraftServerStatus struct {
LiveMotd string `json:"liveMotd,omitempty"`
// ReadySignalAt is when the first RCON probe succeeded.
ReadySignalAt *metav1.Time `json:"readySignalAt,omitempty"`
// StartRequestedAt is when the current start attempt was first observed
// (the first Starting reconcile after desiredState=Running). It anchors the
// felis_start_duration_seconds histogram (spec §23): the operator observes
// ReadySignalAt-StartRequestedAt the moment readiness is first reached, then
// clears this on stop so the next start re-anchors. Persisted in status
// because the two endpoints fall in different reconcile passes.
StartRequestedAt *metav1.Time `json:"startRequestedAt,omitempty"`
// ObservedGeneration is the spec generation this status reflects.
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Conditions are the standard metav1 conditions (Ready, RconReached, ...).
@@ -115,6 +115,9 @@ func (in *MinecraftServerStatus) DeepCopyInto(out *MinecraftServerStatus) {
if in.ReadySignalAt != nil {
out.ReadySignalAt = in.ReadySignalAt.DeepCopy()
}
if in.StartRequestedAt != nil {
out.StartRequestedAt = in.StartRequestedAt.DeepCopy()
}
if in.Conditions != nil {
l := make([]metav1.Condition, len(in.Conditions))
for i := range in.Conditions {