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/scruffy/labels.go b/cloud/bmaas/scruffy/labels.go
new file mode 100644
index 0000000..b88f799
--- /dev/null
+++ b/cloud/bmaas/scruffy/labels.go
@@ -0,0 +1,94 @@
+package scruffy
+
+import (
+	"github.com/prometheus/client_golang/prometheus"
+
+	"source.monogon.dev/go/algorithm/cartesian"
+)
+
+// A labelDefinition describes a key/value pair that's a metric dimension. It
+// consists of the label key/name (a string), and a list of possible values of
+// this key. The list of values will be used to initialize the metrics at startup
+// with zero values.
+//
+// The initialValues system is intended to be used with labels that are
+// low-cardinality enums, e.g. the name of a subsystem.
+//
+// All labelDefinitions for a single metric will then create a cartesian product
+// of all initialValues.
+type labelDefinition struct {
+	// name/key of the label.
+	name string
+	// initialValues defines the default values for this label key/name that will be
+	// used to generate a list of initial zero-filled metrics.
+	initialValues []string
+}
+
+// labelDefinitions is a list of labelDefinition which define the label
+// dimensions of a metric. All the initialValues of the respective
+// labelDefinitions will create a cartesian set of default zero-filled metric
+// values when the metric susbsystem gets initialized. These zero values will
+// then get overridden by real data as it is collected.
+type labelDefinitions []labelDefinition
+
+// initialLabels generates the list of initial labels key/values that should be
+// used to generate zero-filled metrics on startup. This is a cartesian product
+// of all initialValues of all labelDefinitions.
+func (l labelDefinitions) initialLabels() []prometheus.Labels {
+	// Nothing to do if this is an empty labelDefinitions.
+	if len(l) == 0 {
+		return nil
+	}
+
+	// Given:
+	//
+	// labelDefinitions := []labelDefinition{
+	//    { name: "a", initialValues: []string{"foo", "bar"}},
+	//    { name: "b", initialValues: []string{"baz", "barf"}},
+	// }
+	//
+	// This creates:
+	//
+	// values := []string{
+	//    { "foo", "bar" }, // label 'a'
+	//    { "baz", "barf" }, // label 'b'
+	// }
+	var values [][]string
+	for _, ld := range l {
+		values = append(values, ld.initialValues)
+	}
+
+	// Given the above:
+	//
+	// valuesProduct := []string{
+	//    //  a      b
+	//    { "foo", "baz" },
+	//    { "foo", "barf" },
+	//    { "bar", "baz" },
+	//    { "bar", "barf" },
+	// }
+	valuesProduct := cartesian.Product[string](values...)
+
+	// This converts valuesProduct into an actual prometheus-compatible type,
+	// re-attaching the label names back into the columns as seen above.
+	var res []prometheus.Labels
+	for _, vp := range valuesProduct {
+		labels := make(prometheus.Labels)
+		for i, lv := range vp {
+			labelDef := l[i]
+			labels[labelDef.name] = lv
+		}
+		res = append(res, labels)
+	}
+	return res
+}
+
+// names returns the keys/names of all the metric labels from these
+// labelDefinitions.
+func (l labelDefinitions) names() []string {
+	var res []string
+	for _, ld := range l {
+		res = append(res, ld.name)
+	}
+	return res
+}