Skip to content

feat(coderd): export metric indicating each experiment's status #12657

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cli/cliui/provisionerjob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 10 additions & 0 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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))
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine, but optionally to keep it cleaner, you could move this to codersdk, something like codersdk.ExperimentsFromString(Value()...).


if err = prometheusmetrics.Experiments(options.PrometheusRegistry, active); err != nil {
return xerrors.Errorf("register experiments metric: %w", err)
}
}

client := codersdk.New(localURL)
Expand Down
26 changes: 26 additions & 0 deletions coderd/prometheusmetrics/prometheusmetrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
82 changes: 82 additions & 0 deletions coderd/prometheusmetrics/prometheusmetrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down