cloud/bmaas/bmdb/scruffy: initialize, implement BMDB metrics

This creates a new BMaaS component, Scruffy the Janitor.

Scruffy will run a bunch of housekeeping jobs that aren't tied to a
particular provider or even region. Currently Scruffy just collects BMDB
metrics by periodically polling the BMDB SQL database.

Change-Id: Icafa714811757eaaf31fed43184ded8512bde067
Reviewed-on: https://review.monogon.dev/c/monogon/+/1819
Tested-by: Jenkins CI
Reviewed-by: Tim Windelschmidt <tim@monogon.tech>
diff --git a/cloud/bmaas/bmdb/model/interfaces.go b/cloud/bmaas/bmdb/model/interfaces.go
new file mode 100644
index 0000000..9c8f601
--- /dev/null
+++ b/cloud/bmaas/bmdb/model/interfaces.go
@@ -0,0 +1,73 @@
+package model
+
+import "context"
+
+// MetricValue is a prometheus-style labeled numerical metric value. In other
+// words, it's a number accompanied by string key/value pairs.
+type MetricValue struct {
+	Count  int64
+	Labels map[string]string
+}
+
+// WrapSimpleMetric turns a SQL model function which returns a single number into
+// a function which returns one-length MetricValue list with no labels.
+func WrapSimpleMetric(fn func(*Queries, context.Context) (int64, error)) func(*Queries, context.Context) ([]MetricValue, error) {
+	return func(q *Queries, ctx context.Context) ([]MetricValue, error) {
+		v, err := fn(q, ctx)
+		if err != nil {
+			return nil, err
+		}
+		return []MetricValue{
+			{
+				Count:  v,
+				Labels: nil,
+			},
+		}, nil
+	}
+}
+
+// A SQLMetricRow is a row that is the result of some kind of SQL 'metric query'.
+// For each such query we define in our *.sql files, a corresponding
+// implementation exists here.
+type SQLMetricRow interface {
+	Value() MetricValue
+}
+
+// Value implements SQLMetricRow for a row of the result of the
+// CountActiveBackoffs SQL metric query.
+func (c CountActiveBackoffsRow) Value() MetricValue {
+	return MetricValue{
+		Count: c.Count,
+		Labels: map[string]string{
+			"process": string(c.Process),
+		},
+	}
+}
+
+// Value implements SQLMetricRow for a row of the result of the
+// CountActiveWork SQL metric query.
+func (c CountActiveWorkRow) Value() MetricValue {
+	return MetricValue{
+		Count: c.Count,
+		Labels: map[string]string{
+			"process": string(c.Process),
+		},
+	}
+}
+
+// WrapLabeledMetric turns a SQL model function which returns a list of rows
+// implementing SQLMetricRow into a function which returns a list of MetricValues
+// with labels corresponding to the data returned in the rows.
+func WrapLabeledMetric[M SQLMetricRow](fn func(*Queries, context.Context) ([]M, error)) func(*Queries, context.Context) ([]MetricValue, error) {
+	return func(q *Queries, ctx context.Context) ([]MetricValue, error) {
+		v, err := fn(q, ctx)
+		if err != nil {
+			return nil, err
+		}
+		res := make([]MetricValue, len(v))
+		for i, vv := range v {
+			res[i] = vv.Value()
+		}
+		return res, nil
+	}
+}