Skip to content

feat: expose parameter insights as Prometheus metrics #10574

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 4 commits into from
Nov 9, 2023
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
93 changes: 90 additions & 3 deletions coderd/prometheusmetrics/insights/metricscollector.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,21 @@ import (

"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/exp/slices"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"

"cdr.dev/slog"

"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
)

var (
templatesActiveUsersDesc = prometheus.NewDesc("coderd_insights_templates_active_users", "The number of active users of the template.", []string{"template_name"}, nil)
applicationsUsageSecondsDesc = prometheus.NewDesc("coderd_insights_applications_usage_seconds", "The application usage per template.", []string{"template_name", "application_name", "slug"}, nil)
parametersDesc = prometheus.NewDesc("coderd_insights_parameters", "The parameter usage per template.", []string{"template_name", "parameter_name", "parameter_type", "parameter_value"}, nil)
)

type MetricsCollector struct {
Expand All @@ -33,10 +36,20 @@ type MetricsCollector struct {
type insightsData struct {
templates []database.GetTemplateInsightsByTemplateRow
apps []database.GetTemplateAppInsightsByTemplateRow
params []parameterRow

templateNames map[uuid.UUID]string
}

type parameterRow struct {
templateID uuid.UUID
name string
aType string
value string

count int64
}

var _ prometheus.Collector = new(MetricsCollector)

func NewMetricsCollector(db database.Store, logger slog.Logger, timeWindow time.Duration, tickInterval time.Duration) (*MetricsCollector, error) {
Expand Down Expand Up @@ -75,10 +88,11 @@ func (mc *MetricsCollector) Run(ctx context.Context) (func(), error) {
// Phase 1: Fetch insights from database
// FIXME errorGroup will be used to fetch insights for apps and parameters
eg, egCtx := errgroup.WithContext(ctx)
eg.SetLimit(2)
eg.SetLimit(3)

var templateInsights []database.GetTemplateInsightsByTemplateRow
var appInsights []database.GetTemplateAppInsightsByTemplateRow
var paramInsights []parameterRow

eg.Go(func() error {
var err error
Expand All @@ -102,13 +116,25 @@ func (mc *MetricsCollector) Run(ctx context.Context) (func(), error) {
}
return err
})
eg.Go(func() error {
var err error
rows, err := mc.database.GetTemplateParameterInsights(egCtx, database.GetTemplateParameterInsightsParams{
StartTime: startTime,
EndTime: endTime,
})
if err != nil {
mc.logger.Error(ctx, "unable to fetch parameter insights from database", slog.Error(err))
}
paramInsights = convertParameterInsights(rows)
return err
})
err := eg.Wait()
if err != nil {
return
}

// Phase 2: Collect template IDs, and fetch relevant details
templateIDs := uniqueTemplateIDs(templateInsights, appInsights)
templateIDs := uniqueTemplateIDs(templateInsights, appInsights, paramInsights)

templateNames := make(map[uuid.UUID]string, len(templateIDs))
if len(templateIDs) > 0 {
Expand All @@ -126,6 +152,7 @@ func (mc *MetricsCollector) Run(ctx context.Context) (func(), error) {
mc.data.Store(&insightsData{
templates: templateInsights,
apps: appInsights,
params: paramInsights,

templateNames: templateNames,
})
Expand Down Expand Up @@ -153,6 +180,7 @@ func (mc *MetricsCollector) Run(ctx context.Context) (func(), error) {
func (*MetricsCollector) Describe(descCh chan<- *prometheus.Desc) {
descCh <- templatesActiveUsersDesc
descCh <- applicationsUsageSecondsDesc
descCh <- parametersDesc
}

func (mc *MetricsCollector) Collect(metricsCh chan<- prometheus.Metric) {
Expand Down Expand Up @@ -200,18 +228,26 @@ func (mc *MetricsCollector) Collect(metricsCh chan<- prometheus.Metric) {
for _, templateRow := range data.templates {
metricsCh <- prometheus.MustNewConstMetric(templatesActiveUsersDesc, prometheus.GaugeValue, float64(templateRow.ActiveUsers), data.templateNames[templateRow.TemplateID])
}

// Parameters
for _, parameterRow := range data.params {
metricsCh <- prometheus.MustNewConstMetric(parametersDesc, prometheus.GaugeValue, float64(parameterRow.count), data.templateNames[parameterRow.templateID], parameterRow.name, parameterRow.aType, parameterRow.value)
}
}

// Helper functions below.

func uniqueTemplateIDs(templateInsights []database.GetTemplateInsightsByTemplateRow, appInsights []database.GetTemplateAppInsightsByTemplateRow) []uuid.UUID {
func uniqueTemplateIDs(templateInsights []database.GetTemplateInsightsByTemplateRow, appInsights []database.GetTemplateAppInsightsByTemplateRow, paramInsights []parameterRow) []uuid.UUID {
tids := map[uuid.UUID]bool{}
for _, t := range templateInsights {
tids[t.TemplateID] = true
}
for _, t := range appInsights {
tids[t.TemplateID] = true
}
for _, t := range paramInsights {
tids[t.templateID] = true
}

uniqueUUIDs := make([]uuid.UUID, len(tids))
var i int
Expand All @@ -229,3 +265,54 @@ func onlyTemplateNames(templates []database.Template) map[uuid.UUID]string {
}
return m
}

func convertParameterInsights(rows []database.GetTemplateParameterInsightsRow) []parameterRow {
type uniqueKey struct {
templateID uuid.UUID
parameterName string
parameterType string
parameterValue string
}

m := map[uniqueKey]int64{}
for _, r := range rows {
for _, t := range r.TemplateIDs {
key := uniqueKey{
templateID: t,
parameterName: r.Name,
parameterType: r.Type,
parameterValue: r.Value,
}

if _, ok := m[key]; !ok {
m[key] = 0
}
m[key] = m[key] + r.Count
}
}

converted := make([]parameterRow, len(m))
var i int
for k, c := range m {
converted[i] = parameterRow{
templateID: k.templateID,
name: k.parameterName,
aType: k.parameterType,
value: k.parameterValue,
count: c,
}
i++
}

slices.SortFunc(converted, func(a, b parameterRow) int {
if a.templateID != b.templateID {
return slice.Ascending(a.templateID.String(), b.templateID.String())
}
if a.name != b.name {
return slice.Ascending(a.name, b.name)
}
return slice.Ascending(a.value, b.value)
})

return converted
}
28 changes: 25 additions & 3 deletions coderd/prometheusmetrics/insights/metricscollector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func TestCollectInsights(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionPlan: provisionPlanWithParameters(),
ProvisionApply: provisionApplyWithAgentAndApp(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
Expand All @@ -66,7 +66,13 @@ func TestCollectInsights(t *testing.T) {
require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart])

coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{
{Name: "first_parameter", Value: "Foobar"},
{Name: "second_parameter", Value: "true"},
{Name: "third_parameter", Value: "789"},
}
})
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

// Start an agent so that we can generate stats.
Expand Down Expand Up @@ -142,7 +148,7 @@ func TestCollectInsights(t *testing.T) {
// Then
for _, metric := range metrics {
switch metric.GetName() {
case "coderd_insights_applications_usage_seconds", "coderd_insights_templates_active_users":
case "coderd_insights_applications_usage_seconds", "coderd_insights_templates_active_users", "coderd_insights_parameters":
for _, m := range metric.Metric {
key := metric.GetName()
if len(m.Label) > 0 {
Expand All @@ -167,6 +173,22 @@ func metricLabelAsString(m *io_prometheus_client.Metric) string {
return strings.Join(labels, ",")
}

func provisionPlanWithParameters() []*proto.Response {
return []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{
{Name: "first_parameter", Type: "string", Mutable: true},
{Name: "second_parameter", Type: "bool", Mutable: true},
{Name: "third_parameter", Type: "number", Mutable: true},
},
},
},
},
}
}

func provisionApplyWithAgentAndApp(authToken string) []*proto.Response {
return []*proto.Response{{
Type: &proto.Response_Apply{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@
"coderd_insights_applications_usage_seconds[application_name=Web Terminal,slug=,template_name=golden-template]": 0,
"coderd_insights_applications_usage_seconds[application_name=SSH,slug=,template_name=golden-template]": 60,
"coderd_insights_applications_usage_seconds[application_name=Golden Slug,slug=golden-slug,template_name=golden-template]": 180,
"coderd_insights_parameters[parameter_name=first_parameter,parameter_type=string,parameter_value=Foobar,template_name=golden-template]": 1,
"coderd_insights_parameters[parameter_name=second_parameter,parameter_type=bool,parameter_value=true,template_name=golden-template]": 1,
"coderd_insights_parameters[parameter_name=third_parameter,parameter_type=number,parameter_value=789,template_name=golden-template]": 1,
"coderd_insights_templates_active_users[template_name=golden-template]": 1
}