From 4057363f781dd2b480936d9836540866ca08080d Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 25 Feb 2026 00:58:50 -0800 Subject: [PATCH] fix(coderd): add organization_name label to insights Prometheus metrics (#22296) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description When multiple organizations have templates with the same name, the Prometheus `/metrics` endpoint returns HTTP 500 because Prometheus rejects duplicate label combinations. The three `coderd_insights_*` metrics (`coderd_insights_templates_active_users`, `coderd_insights_applications_usage_seconds`, `coderd_insights_parameters`) used only `template_name` as a distinguishing label, so two templates named e.g. `"openstack-v1"` in different orgs would produce duplicate metric series. This adds `organization_name` as a label to all three insight metric descriptors to disambiguate templates across organizations. ## Changes **`coderd/prometheusmetrics/insights/metricscollector.go`**: - Added `organization_name` label to all three metric descriptors - Added `organizationNames` field (template ID → org name) to the `insightsData` struct - In `doTick`: after fetching templates, collect unique org IDs, fetch organizations via `GetOrganizations`, and build a template-ID-to-org-name mapping - In `Collect()`: pass the organization name as an additional label value in every `MustNewConstMetric` call **`coderd/prometheusmetrics/insights/testdata/insights-metrics.json`**: Updated golden file to include `organization_name=coder` in all metric label keys. Fixes #21748 --- .../insights/metricscollector.go | 54 ++++++++++++++----- .../insights/testdata/insights-metrics.json | 22 ++++---- docs/admin/integrations/prometheus.md | 6 +-- scripts/metricsdocgen/generated_metrics | 6 +-- 4 files changed, 59 insertions(+), 29 deletions(-) diff --git a/coderd/prometheusmetrics/insights/metricscollector.go b/coderd/prometheusmetrics/insights/metricscollector.go index dc1d3e5363..207541fc09 100644 --- a/coderd/prometheusmetrics/insights/metricscollector.go +++ b/coderd/prometheusmetrics/insights/metricscollector.go @@ -19,9 +19,9 @@ import ( ) 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) + templatesActiveUsersDesc = prometheus.NewDesc("coderd_insights_templates_active_users", "The number of active users of the template.", []string{"template_name", "organization_name"}, nil) + applicationsUsageSecondsDesc = prometheus.NewDesc("coderd_insights_applications_usage_seconds", "The application usage per template.", []string{"template_name", "application_name", "slug", "organization_name"}, nil) + parametersDesc = prometheus.NewDesc("coderd_insights_parameters", "The parameter usage per template.", []string{"template_name", "parameter_name", "parameter_type", "parameter_value", "organization_name"}, nil) ) type MetricsCollector struct { @@ -38,7 +38,8 @@ type insightsData struct { apps []database.GetTemplateAppInsightsByTemplateRow params []parameterRow - templateNames map[uuid.UUID]string + templateNames map[uuid.UUID]string + organizationNames map[uuid.UUID]string // template ID → org name } type parameterRow struct { @@ -137,6 +138,7 @@ func (mc *MetricsCollector) Run(ctx context.Context) (func(), error) { templateIDs := uniqueTemplateIDs(templateInsights, appInsights, paramInsights) templateNames := make(map[uuid.UUID]string, len(templateIDs)) + organizationNames := make(map[uuid.UUID]string, len(templateIDs)) if len(templateIDs) > 0 { templates, err := mc.database.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{ IDs: templateIDs, @@ -146,6 +148,31 @@ func (mc *MetricsCollector) Run(ctx context.Context) (func(), error) { return } templateNames = onlyTemplateNames(templates) + + // Build org name lookup so that metrics can + // distinguish templates with the same name across + // different organizations. + orgIDs := make([]uuid.UUID, 0, len(templates)) + for _, t := range templates { + orgIDs = append(orgIDs, t.OrganizationID) + } + orgIDs = slice.Unique(orgIDs) + + orgs, err := mc.database.GetOrganizations(ctx, database.GetOrganizationsParams{ + IDs: orgIDs, + }) + if err != nil { + mc.logger.Error(ctx, "unable to fetch organizations from database", slog.Error(err)) + return + } + orgNameByID := make(map[uuid.UUID]string, len(orgs)) + for _, o := range orgs { + orgNameByID[o.ID] = o.Name + } + organizationNames = make(map[uuid.UUID]string, len(templates)) + for _, t := range templates { + organizationNames[t.ID] = orgNameByID[t.OrganizationID] + } } // Refresh the collector state @@ -154,7 +181,8 @@ func (mc *MetricsCollector) Run(ctx context.Context) (func(), error) { apps: appInsights, params: paramInsights, - templateNames: templateNames, + templateNames: templateNames, + organizationNames: organizationNames, }) } @@ -194,44 +222,46 @@ func (mc *MetricsCollector) Collect(metricsCh chan<- prometheus.Metric) { // Custom apps for _, appRow := range data.apps { metricsCh <- prometheus.MustNewConstMetric(applicationsUsageSecondsDesc, prometheus.GaugeValue, float64(appRow.UsageSeconds), data.templateNames[appRow.TemplateID], - appRow.DisplayName, appRow.SlugOrPort) + appRow.DisplayName, appRow.SlugOrPort, data.organizationNames[appRow.TemplateID]) } // Built-in apps for _, templateRow := range data.templates { + orgName := data.organizationNames[templateRow.TemplateID] + metricsCh <- prometheus.MustNewConstMetric(applicationsUsageSecondsDesc, prometheus.GaugeValue, float64(templateRow.UsageVscodeSeconds), data.templateNames[templateRow.TemplateID], codersdk.TemplateBuiltinAppDisplayNameVSCode, - "") + "", orgName) metricsCh <- prometheus.MustNewConstMetric(applicationsUsageSecondsDesc, prometheus.GaugeValue, float64(templateRow.UsageJetbrainsSeconds), data.templateNames[templateRow.TemplateID], codersdk.TemplateBuiltinAppDisplayNameJetBrains, - "") + "", orgName) metricsCh <- prometheus.MustNewConstMetric(applicationsUsageSecondsDesc, prometheus.GaugeValue, float64(templateRow.UsageReconnectingPtySeconds), data.templateNames[templateRow.TemplateID], codersdk.TemplateBuiltinAppDisplayNameWebTerminal, - "") + "", orgName) metricsCh <- prometheus.MustNewConstMetric(applicationsUsageSecondsDesc, prometheus.GaugeValue, float64(templateRow.UsageSshSeconds), data.templateNames[templateRow.TemplateID], codersdk.TemplateBuiltinAppDisplayNameSSH, - "") + "", orgName) } // Templates for _, templateRow := range data.templates { - metricsCh <- prometheus.MustNewConstMetric(templatesActiveUsersDesc, prometheus.GaugeValue, float64(templateRow.ActiveUsers), data.templateNames[templateRow.TemplateID]) + metricsCh <- prometheus.MustNewConstMetric(templatesActiveUsersDesc, prometheus.GaugeValue, float64(templateRow.ActiveUsers), data.templateNames[templateRow.TemplateID], data.organizationNames[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) + metricsCh <- prometheus.MustNewConstMetric(parametersDesc, prometheus.GaugeValue, float64(parameterRow.count), data.templateNames[parameterRow.templateID], parameterRow.name, parameterRow.aType, parameterRow.value, data.organizationNames[parameterRow.templateID]) } } diff --git a/coderd/prometheusmetrics/insights/testdata/insights-metrics.json b/coderd/prometheusmetrics/insights/testdata/insights-metrics.json index e672ed304a..6acfb61dd0 100644 --- a/coderd/prometheusmetrics/insights/testdata/insights-metrics.json +++ b/coderd/prometheusmetrics/insights/testdata/insights-metrics.json @@ -1,13 +1,13 @@ { - "coderd_insights_applications_usage_seconds[application_name=JetBrains,slug=,template_name=golden-template]": 60, - "coderd_insights_applications_usage_seconds[application_name=Visual Studio Code,slug=,template_name=golden-template]": 60, - "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=first_parameter,parameter_type=string,parameter_value=Baz,template_name=golden-template]": 1, - "coderd_insights_parameters[parameter_name=second_parameter,parameter_type=bool,parameter_value=true,template_name=golden-template]": 2, - "coderd_insights_parameters[parameter_name=third_parameter,parameter_type=number,parameter_value=789,template_name=golden-template]": 1, - "coderd_insights_parameters[parameter_name=third_parameter,parameter_type=number,parameter_value=999,template_name=golden-template]": 1, - "coderd_insights_templates_active_users[template_name=golden-template]": 1 + "coderd_insights_applications_usage_seconds[application_name=JetBrains,organization_name=coder,slug=,template_name=golden-template]": 60, + "coderd_insights_applications_usage_seconds[application_name=Visual Studio Code,organization_name=coder,slug=,template_name=golden-template]": 60, + "coderd_insights_applications_usage_seconds[application_name=Web Terminal,organization_name=coder,slug=,template_name=golden-template]": 0, + "coderd_insights_applications_usage_seconds[application_name=SSH,organization_name=coder,slug=,template_name=golden-template]": 60, + "coderd_insights_applications_usage_seconds[application_name=Golden Slug,organization_name=coder,slug=golden-slug,template_name=golden-template]": 180, + "coderd_insights_parameters[organization_name=coder,parameter_name=first_parameter,parameter_type=string,parameter_value=Foobar,template_name=golden-template]": 1, + "coderd_insights_parameters[organization_name=coder,parameter_name=first_parameter,parameter_type=string,parameter_value=Baz,template_name=golden-template]": 1, + "coderd_insights_parameters[organization_name=coder,parameter_name=second_parameter,parameter_type=bool,parameter_value=true,template_name=golden-template]": 2, + "coderd_insights_parameters[organization_name=coder,parameter_name=third_parameter,parameter_type=number,parameter_value=789,template_name=golden-template]": 1, + "coderd_insights_parameters[organization_name=coder,parameter_name=third_parameter,parameter_type=number,parameter_value=999,template_name=golden-template]": 1, + "coderd_insights_templates_active_users[organization_name=coder,template_name=golden-template]": 1 } diff --git a/docs/admin/integrations/prometheus.md b/docs/admin/integrations/prometheus.md index 3779f961f7..ace326019f 100644 --- a/docs/admin/integrations/prometheus.md +++ b/docs/admin/integrations/prometheus.md @@ -175,9 +175,9 @@ deployment. They will always be available from the agent. | `coderd_dbpurge_iteration_duration_seconds` | histogram | Duration of each dbpurge iteration in seconds. | `success` | | `coderd_dbpurge_records_purged_total` | counter | Total number of records purged by type. | `record_type` | | `coderd_experiments` | gauge | Indicates whether each experiment is enabled (1) or not (0) | `experiment` | -| `coderd_insights_applications_usage_seconds` | gauge | The application usage per template. | `application_name` `slug` `template_name` | -| `coderd_insights_parameters` | gauge | The parameter usage per template. | `parameter_name` `parameter_type` `parameter_value` `template_name` | -| `coderd_insights_templates_active_users` | gauge | The number of active users of the template. | `template_name` | +| `coderd_insights_applications_usage_seconds` | gauge | The application usage per template. | `application_name` `organization_name` `slug` `template_name` | +| `coderd_insights_parameters` | gauge | The parameter usage per template. | `organization_name` `parameter_name` `parameter_type` `parameter_value` `template_name` | +| `coderd_insights_templates_active_users` | gauge | The number of active users of the template. | `organization_name` `template_name` | | `coderd_license_active_users` | gauge | The number of active users. | | | `coderd_license_errors` | gauge | The number of active license errors. | | | `coderd_license_limit_users` | gauge | The user seats limit based on the active Coder license. | | diff --git a/scripts/metricsdocgen/generated_metrics b/scripts/metricsdocgen/generated_metrics index c3a38fc620..887f8f469c 100644 --- a/scripts/metricsdocgen/generated_metrics +++ b/scripts/metricsdocgen/generated_metrics @@ -159,13 +159,13 @@ coderd_dbpurge_records_purged_total{record_type=""} 0 coderd_experiments{experiment=""} 0 # HELP coderd_insights_applications_usage_seconds The application usage per template. # TYPE coderd_insights_applications_usage_seconds gauge -coderd_insights_applications_usage_seconds{template_name="",application_name="",slug=""} 0 +coderd_insights_applications_usage_seconds{template_name="",application_name="",slug="",organization_name=""} 0 # HELP coderd_insights_parameters The parameter usage per template. # TYPE coderd_insights_parameters gauge -coderd_insights_parameters{template_name="",parameter_name="",parameter_type="",parameter_value=""} 0 +coderd_insights_parameters{template_name="",parameter_name="",parameter_type="",parameter_value="",organization_name=""} 0 # HELP coderd_insights_templates_active_users The number of active users of the template. # TYPE coderd_insights_templates_active_users gauge -coderd_insights_templates_active_users{template_name=""} 0 +coderd_insights_templates_active_users{template_name="",organization_name=""} 0 # HELP coderd_license_active_users The number of active users. # TYPE coderd_license_active_users gauge coderd_license_active_users 0