Skip to content

Commit 106c17e

Browse files
committed
PMM-15326 Add a Prometheus collector for OM collection health
OM registered no collector where agents, inventory, ha, advisor and backups all do, per 4nte's review: the per-run counters it already computes ended up only in om_topology_runs (pruned at runHistory) and a log line at Info, so a degraded pass was invisible on PMM Health. Adds MetricsCollector, following the ha/inventory/backup shape (manual prom.Desc + prom.MustNewConstMetric, Describe delegating to DescribeByCollect), registered next to omService's own construction in main.go: - pmm_managed_om_runs_total{status} -- completed runs by outcome, tallied by three atomic counters on Service and incremented once per run by the new recordRunOutcome. - pmm_managed_om_document_age_seconds -- age of the newest observation in the document currently served, deliberately the same age Snapshot.Stale is already computed from, so the existing staleness signal becomes alertable rather than only renderable. Unset until the estate has been observed at least once. - pmm_managed_om_services{state} -- estate size versus how much answered, from the last pass's Summary. Verified live against the running dev stack: runs_total advanced on the next collection tick, document_age_seconds tracked real elapsed time, and services{state} matched GetTopology's own summary (14/14). Signed-off-by: Pawel Lebioda <pawel.lebioda@percona.com>
1 parent 9ee1b42 commit 106c17e

3 files changed

Lines changed: 130 additions & 0 deletions

File tree

managed/cmd/pmm-managed/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,6 +1201,7 @@ func main() { //nolint:gocognit,maintidx,cyclop
12011201
// and metrics alone and records the probe source as disabled.
12021202
omService := om.New(db, v1.NewAPI(vmClient), haService, logrus.WithField("component", "om"))
12031203
omService.WithProbeSource(*sepURLF, *sepTokenF)
1204+
prom.MustRegister(om.NewMetricsCollector(omService))
12041205

12051206
// Leader-only, like every other periodic writer here. A collection persists a run and
12061207
// its snapshot and then prunes the shared history, so running it on every node of an

managed/services/om/om_metrics.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright (C) 2023 Percona LLC
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU Affero General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU Affero General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU Affero General Public License
14+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
15+
16+
package om
17+
18+
import (
19+
"time"
20+
21+
prom "github.com/prometheus/client_golang/prometheus"
22+
)
23+
24+
const (
25+
prometheusNamespace = "pmm_managed"
26+
prometheusSubsystem = "om"
27+
)
28+
29+
// MetricsCollector exposes OM's own collection health to Prometheus -- otherwise a
30+
// degraded pass is visible only in the run history, which prunes at runHistory, and in a
31+
// log line at Info.
32+
//
33+
// The following metrics are exposed:
34+
//
35+
// - pmm_managed_om_runs_total{status="success|partial|failed"} -- completed collection
36+
// runs so far, by outcome. A rising "partial" or "failed" rate says collection itself
37+
// is unhealthy, before any caller notices the document is stale.
38+
//
39+
// - pmm_managed_om_document_age_seconds -- how old the newest observation in the
40+
// document PMM currently serves is. This is the same age Snapshot.Stale is computed
41+
// from, exposed as a number so it can be alerted on rather than only rendered. Unset
42+
// until the estate has been observed at least once.
43+
//
44+
// - pmm_managed_om_services{state="total|up|down"} -- how much of the estate the last
45+
// collection pass saw, and how much of it answered.
46+
type MetricsCollector struct {
47+
svc *Service
48+
49+
mRunsTotal *prom.Desc
50+
mDocumentAge *prom.Desc
51+
mServices *prom.Desc
52+
}
53+
54+
// NewMetricsCollector creates a new MetricsCollector backed by the given OM service.
55+
func NewMetricsCollector(svc *Service) *MetricsCollector {
56+
return &MetricsCollector{
57+
svc: svc,
58+
mRunsTotal: prom.NewDesc(
59+
prom.BuildFQName(prometheusNamespace, prometheusSubsystem, "runs_total"),
60+
"Total number of OM topology collection runs, by outcome status.",
61+
[]string{"status"},
62+
nil,
63+
),
64+
mDocumentAge: prom.NewDesc(
65+
prom.BuildFQName(prometheusNamespace, prometheusSubsystem, "document_age_seconds"),
66+
"Age, in seconds, of the newest observation in the topology document PMM currently serves.",
67+
nil,
68+
nil,
69+
),
70+
mServices: prom.NewDesc(
71+
prom.BuildFQName(prometheusNamespace, prometheusSubsystem, "services"),
72+
"Services in the topology document PMM currently serves, by state.",
73+
[]string{"state"},
74+
nil,
75+
),
76+
}
77+
}
78+
79+
// Describe implements prom.Collector.
80+
func (c *MetricsCollector) Describe(ch chan<- *prom.Desc) {
81+
prom.DescribeByCollect(c, ch)
82+
}
83+
84+
// Collect implements prom.Collector.
85+
func (c *MetricsCollector) Collect(ch chan<- prom.Metric) {
86+
ch <- prom.MustNewConstMetric(c.mRunsTotal, prom.CounterValue, float64(c.svc.runsSuccess.Load()), runStatusSuccess)
87+
ch <- prom.MustNewConstMetric(c.mRunsTotal, prom.CounterValue, float64(c.svc.runsPartial.Load()), runStatusPartial)
88+
ch <- prom.MustNewConstMetric(c.mRunsTotal, prom.CounterValue, float64(c.svc.runsFailed.Load()), runStatusFailed)
89+
90+
snap := c.svc.snapshot()
91+
if snap.GetSnapshot().GetObservedAt() == nil {
92+
// Nothing has been observed yet -- a fresh estate whose leader has not completed
93+
// its first pass. Reporting age 0 would read as "current" rather than "no data".
94+
return
95+
}
96+
age := time.Since(snap.Snapshot.ObservedAt.AsTime()).Seconds()
97+
ch <- prom.MustNewConstMetric(c.mDocumentAge, prom.GaugeValue, age)
98+
99+
summary := snap.Summary
100+
ch <- prom.MustNewConstMetric(c.mServices, prom.GaugeValue, float64(summary.GetTotalServices()), "total")
101+
ch <- prom.MustNewConstMetric(c.mServices, prom.GaugeValue, float64(summary.GetUpServices()), "up")
102+
ch <- prom.MustNewConstMetric(c.mServices, prom.GaugeValue, float64(summary.GetDownServices()), "down")
103+
}
104+
105+
var _ prom.Collector = (*MetricsCollector)(nil)

managed/services/om/service.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"fmt"
3838
"net/http"
3939
"sync"
40+
"sync/atomic"
4041
"time"
4142

4243
"github.com/google/uuid"
@@ -110,6 +111,13 @@ type Service struct {
110111

111112
mu sync.Mutex
112113
latest *omv1.GetTopologyResponse
114+
115+
// Completed-run tally by status, read by MetricsCollector on every Prometheus scrape
116+
// and written once per run by recordRunOutcome. Atomic because a scrape can land while
117+
// a collection is in flight.
118+
runsSuccess atomic.Int64
119+
runsPartial atomic.Int64
120+
runsFailed atomic.Int64
113121
}
114122

115123
// New returns a new OM service.
@@ -314,6 +322,7 @@ func (s *Service) collect(ctx context.Context) (*omv1.GetTopologyResponse, *omv1
314322
Environments: doc.environments,
315323
}
316324
run := buildRun(runID, startedAt, generatedAt, services, merged, doc, results)
325+
s.recordRunOutcome(run.Status)
317326

318327
s.mu.Lock()
319328
s.latest = response
@@ -539,6 +548,21 @@ func buildRun(
539548
return run
540549
}
541550

551+
// recordRunOutcome tallies one completed run by status, for MetricsCollector's
552+
// pmm_managed_om_runs_total counter.
553+
func (s *Service) recordRunOutcome(status omv1.RunStatus) {
554+
switch status {
555+
case omv1.RunStatus_RUN_STATUS_SUCCESS:
556+
s.runsSuccess.Add(1)
557+
case omv1.RunStatus_RUN_STATUS_PARTIAL:
558+
s.runsPartial.Add(1)
559+
case omv1.RunStatus_RUN_STATUS_FAILED:
560+
s.runsFailed.Add(1)
561+
default:
562+
// buildRun only ever sets one of the three above.
563+
}
564+
}
565+
542566
// detailStrings renders a source's counters for the wire, which carries them as strings
543567
// so a source can report whatever it has without the contract naming every counter.
544568
func detailStrings(detail map[string]any) map[string]string {

0 commit comments

Comments
 (0)