Skip to content

Commit 9cfd5ba

Browse files
authored
feat(coderd): export metric indicating each experiment's status (#12657)
1 parent 1a9f7e7 commit 9cfd5ba

File tree

3 files changed

+118
-0
lines changed

3 files changed

+118
-0
lines changed

cli/server.go

+10
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ func enablePrometheus(
258258
), nil
259259
}
260260

261+
//nolint:gocognit // TODO(dannyk): reduce complexity of this function
261262
func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, io.Closer, error)) *serpent.Command {
262263
if newAPI == nil {
263264
newAPI = func(_ context.Context, o *coderd.Options) (*coderd.API, io.Closer, error) {
@@ -893,6 +894,15 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
893894
return xerrors.Errorf("register agents prometheus metric: %w", err)
894895
}
895896
defer closeAgentsFunc()
897+
898+
var active codersdk.Experiments
899+
for _, exp := range options.DeploymentValues.Experiments.Value() {
900+
active = append(active, codersdk.Experiment(exp))
901+
}
902+
903+
if err = prometheusmetrics.Experiments(options.PrometheusRegistry, active); err != nil {
904+
return xerrors.Errorf("register experiments metric: %w", err)
905+
}
896906
}
897907

898908
client := codersdk.New(localURL)

coderd/prometheusmetrics/prometheusmetrics.go

+26
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,32 @@ func AgentStats(ctx context.Context, logger slog.Logger, registerer prometheus.R
516516
}, nil
517517
}
518518

519+
// Experiments registers a metric which indicates whether each experiment is enabled or not.
520+
func Experiments(registerer prometheus.Registerer, active codersdk.Experiments) error {
521+
experimentsGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
522+
Namespace: "coderd",
523+
Name: "experiments",
524+
Help: "Indicates whether each experiment is enabled (1) or not (0)",
525+
}, []string{"experiment"})
526+
if err := registerer.Register(experimentsGauge); err != nil {
527+
return err
528+
}
529+
530+
for _, exp := range codersdk.ExperimentsAll {
531+
var val float64
532+
for _, enabled := range active {
533+
if exp == enabled {
534+
val = 1
535+
break
536+
}
537+
}
538+
539+
experimentsGauge.WithLabelValues(string(exp)).Set(val)
540+
}
541+
542+
return nil
543+
}
544+
519545
// filterAcceptableAgentLabels handles a slightly messy situation whereby `prometheus-aggregate-agent-stats-by` can control on
520546
// which labels agent stats are aggregated, but for these specific metrics in this file there is no `template` label value,
521547
// and therefore we have to exclude it from the list of acceptable labels.

coderd/prometheusmetrics/prometheusmetrics_test.go

+82
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,88 @@ func TestAgentStats(t *testing.T) {
500500
assert.EqualValues(t, golden, collected)
501501
}
502502

503+
func TestExperimentsMetric(t *testing.T) {
504+
t.Parallel()
505+
506+
tests := []struct {
507+
name string
508+
experiments codersdk.Experiments
509+
expected map[codersdk.Experiment]float64
510+
}{
511+
{
512+
name: "Enabled experiment is exported in metrics",
513+
experiments: codersdk.Experiments{codersdk.ExperimentSharedPorts},
514+
expected: map[codersdk.Experiment]float64{
515+
codersdk.ExperimentSharedPorts: 1,
516+
},
517+
},
518+
{
519+
name: "Disabled experiment is exported in metrics",
520+
experiments: codersdk.Experiments{},
521+
expected: map[codersdk.Experiment]float64{
522+
codersdk.ExperimentSharedPorts: 0,
523+
},
524+
},
525+
{
526+
name: "Unknown experiment is not exported in metrics",
527+
experiments: codersdk.Experiments{codersdk.Experiment("bob")},
528+
expected: map[codersdk.Experiment]float64{},
529+
},
530+
}
531+
532+
for _, tc := range tests {
533+
tc := tc
534+
535+
t.Run(tc.name, func(t *testing.T) {
536+
t.Parallel()
537+
reg := prometheus.NewRegistry()
538+
539+
require.NoError(t, prometheusmetrics.Experiments(reg, tc.experiments))
540+
541+
out, err := reg.Gather()
542+
require.NoError(t, err)
543+
require.Lenf(t, out, 1, "unexpected number of registered metrics")
544+
545+
seen := make(map[codersdk.Experiment]float64)
546+
547+
for _, metric := range out[0].GetMetric() {
548+
require.Equal(t, "coderd_experiments", out[0].GetName())
549+
550+
labels := metric.GetLabel()
551+
require.Lenf(t, labels, 1, "unexpected number of labels")
552+
553+
experiment := codersdk.Experiment(labels[0].GetValue())
554+
value := metric.GetGauge().GetValue()
555+
556+
seen[experiment] = value
557+
558+
expectedValue := 0
559+
560+
// Find experiment we expect to be enabled.
561+
for _, exp := range tc.experiments {
562+
if experiment == exp {
563+
expectedValue = 1
564+
break
565+
}
566+
}
567+
568+
require.EqualValuesf(t, expectedValue, value, "expected %d value for experiment %q", expectedValue, experiment)
569+
}
570+
571+
// We don't want to define the state of all experiments because codersdk.ExperimentAll will change at some
572+
// point and break these tests; so we only validate the experiments we know about.
573+
for exp, val := range seen {
574+
expectedVal, found := tc.expected[exp]
575+
if !found {
576+
t.Logf("ignoring experiment %q; it is not listed in expectations", exp)
577+
continue
578+
}
579+
require.Equalf(t, expectedVal, val, "experiment %q did not match expected value %v", exp, expectedVal)
580+
}
581+
})
582+
}
583+
}
584+
503585
func prepareWorkspaceAndAgent(t *testing.T, client *codersdk.Client, user codersdk.CreateFirstUserResponse, workspaceNum int) *agentsdk.Client {
504586
authToken := uuid.NewString()
505587

0 commit comments

Comments
 (0)