mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
fix(coderd): add organization_name label to insights Prometheus metrics (#22296)
## 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
This commit is contained in:
@@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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. | |
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user