diff --git a/cli/cliui/provisionerjob_test.go b/cli/cliui/provisionerjob_test.go index f3661ca8d1597..f75a8bc53f12a 100644 --- a/cli/cliui/provisionerjob_test.go +++ b/cli/cliui/provisionerjob_test.go @@ -11,9 +11,10 @@ import ( "testing" "time" - "github.com/coder/coder/v2/testutil" "github.com/stretchr/testify/assert" + "github.com/coder/coder/v2/testutil" + "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/codersdk" diff --git a/cli/server.go b/cli/server.go index f57d761da7481..78f7270074f81 100644 --- a/cli/server.go +++ b/cli/server.go @@ -258,6 +258,7 @@ func enablePrometheus( ), nil } +//nolint:gocognit // TODO(dannyk): reduce complexity of this function func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, io.Closer, error)) *serpent.Command { if newAPI == nil { newAPI = func(_ context.Context, o *coderd.Options) (*coderd.API, io.Closer, error) { @@ -892,6 +893,15 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. return xerrors.Errorf("register agents prometheus metric: %w", err) } defer closeAgentsFunc() + + var active codersdk.Experiments + for _, exp := range options.DeploymentValues.Experiments.Value() { + active = append(active, codersdk.Experiment(exp)) + } + + if err = prometheusmetrics.Experiments(options.PrometheusRegistry, active); err != nil { + return xerrors.Errorf("register experiments metric: %w", err) + } } client := codersdk.New(localURL) diff --git a/coderd/prometheusmetrics/prometheusmetrics.go b/coderd/prometheusmetrics/prometheusmetrics.go index 36bf9b887d2c0..b2c4b46677eb6 100644 --- a/coderd/prometheusmetrics/prometheusmetrics.go +++ b/coderd/prometheusmetrics/prometheusmetrics.go @@ -516,6 +516,32 @@ func AgentStats(ctx context.Context, logger slog.Logger, registerer prometheus.R }, nil } +// Experiments registers a metric which indicates whether each experiment is enabled or not. +func Experiments(registerer prometheus.Registerer, active codersdk.Experiments) error { + experimentsGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "coderd", + Name: "experiments", + Help: "Indicates whether each experiment is enabled (1) or not (0)", + }, []string{"experiment"}) + if err := registerer.Register(experimentsGauge); err != nil { + return err + } + + for _, exp := range codersdk.ExperimentsAll { + var val float64 + for _, enabled := range active { + if exp == enabled { + val = 1 + break + } + } + + experimentsGauge.WithLabelValues(string(exp)).Set(val) + } + + return nil +} + // filterAcceptableAgentLabels handles a slightly messy situation whereby `prometheus-aggregate-agent-stats-by` can control on // which labels agent stats are aggregated, but for these specific metrics in this file there is no `template` label value, // and therefore we have to exclude it from the list of acceptable labels. diff --git a/coderd/prometheusmetrics/prometheusmetrics_test.go b/coderd/prometheusmetrics/prometheusmetrics_test.go index 3992792266c65..32e97f84c32b1 100644 --- a/coderd/prometheusmetrics/prometheusmetrics_test.go +++ b/coderd/prometheusmetrics/prometheusmetrics_test.go @@ -500,6 +500,88 @@ func TestAgentStats(t *testing.T) { assert.EqualValues(t, golden, collected) } +func TestExperimentsMetric(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + experiments codersdk.Experiments + expected map[codersdk.Experiment]float64 + }{ + { + name: "Enabled experiment is exported in metrics", + experiments: codersdk.Experiments{codersdk.ExperimentSharedPorts}, + expected: map[codersdk.Experiment]float64{ + codersdk.ExperimentSharedPorts: 1, + }, + }, + { + name: "Disabled experiment is exported in metrics", + experiments: codersdk.Experiments{}, + expected: map[codersdk.Experiment]float64{ + codersdk.ExperimentSharedPorts: 0, + }, + }, + { + name: "Unknown experiment is not exported in metrics", + experiments: codersdk.Experiments{codersdk.Experiment("bob")}, + expected: map[codersdk.Experiment]float64{}, + }, + } + + for _, tc := range tests { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + reg := prometheus.NewRegistry() + + require.NoError(t, prometheusmetrics.Experiments(reg, tc.experiments)) + + out, err := reg.Gather() + require.NoError(t, err) + require.Lenf(t, out, 1, "unexpected number of registered metrics") + + seen := make(map[codersdk.Experiment]float64) + + for _, metric := range out[0].GetMetric() { + require.Equal(t, "coderd_experiments", out[0].GetName()) + + labels := metric.GetLabel() + require.Lenf(t, labels, 1, "unexpected number of labels") + + experiment := codersdk.Experiment(labels[0].GetValue()) + value := metric.GetGauge().GetValue() + + seen[experiment] = value + + expectedValue := 0 + + // Find experiment we expect to be enabled. + for _, exp := range tc.experiments { + if experiment == exp { + expectedValue = 1 + break + } + } + + require.EqualValuesf(t, expectedValue, value, "expected %d value for experiment %q", expectedValue, experiment) + } + + // We don't want to define the state of all experiments because codersdk.ExperimentAll will change at some + // point and break these tests; so we only validate the experiments we know about. + for exp, val := range seen { + expectedVal, found := tc.expected[exp] + if !found { + t.Logf("ignoring experiment %q; it is not listed in expectations", exp) + continue + } + require.Equalf(t, expectedVal, val, "experiment %q did not match expected value %v", exp, expectedVal) + } + }) + } +} + func prepareWorkspaceAndAgent(t *testing.T, client *codersdk.Client, user codersdk.CreateFirstUserResponse, workspaceNum int) *agentsdk.Client { authToken := uuid.NewString()