From e3139cd92492e738657e7a9f6bdd142c528f9adb Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Thu, 8 Jan 2026 20:09:38 +0000 Subject: [PATCH 1/2] feat(scripts/metricsdocgen): add prometheus.NewDesc() pattern to metrics scanner --- scripts/metricsdocgen/generated_metrics | 66 +++++++++ scripts/metricsdocgen/scanner/scanner.go | 175 ++++++++++++++++++++++- 2 files changed, 238 insertions(+), 3 deletions(-) diff --git a/scripts/metricsdocgen/generated_metrics b/scripts/metricsdocgen/generated_metrics index e69de29bb2d1d..965c5b2bf143b 100644 --- a/scripts/metricsdocgen/generated_metrics +++ b/scripts/metricsdocgen/generated_metrics @@ -0,0 +1,66 @@ +# HELP coder_pubsub_current_events The current number of pubsub event channels listened for +# TYPE coder_pubsub_current_events gauge +coder_pubsub_current_events 0 +# HELP coder_pubsub_current_subscribers The current number of active pubsub subscribers +# TYPE coder_pubsub_current_subscribers gauge +coder_pubsub_current_subscribers 0 +# HELP coder_pubsub_latency_measure_errs_total The number of pubsub latency measurement failures +# TYPE coder_pubsub_latency_measure_errs_total counter +coder_pubsub_latency_measure_errs_total 0 +# HELP coder_pubsub_latency_measures_total The number of pubsub latency measurements +# TYPE coder_pubsub_latency_measures_total counter +coder_pubsub_latency_measures_total 0 +# HELP coder_pubsub_receive_latency_seconds The time taken to receive a message from a pubsub event channel +# TYPE coder_pubsub_receive_latency_seconds gauge +coder_pubsub_receive_latency_seconds 0 +# HELP coder_pubsub_send_latency_seconds The time taken to send a message into a pubsub event channel +# TYPE coder_pubsub_send_latency_seconds gauge +coder_pubsub_send_latency_seconds 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 +# 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 +# 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 +# HELP coderd_license_active_users The number of active users. +# TYPE coderd_license_active_users gauge +coderd_license_active_users 0 +# HELP coderd_license_limit_users The user seats limit based on the active Coder license. +# TYPE coderd_license_limit_users gauge +coderd_license_limit_users 0 +# HELP coderd_license_user_limit_enabled Returns 1 if the current license enforces the user limit. +# TYPE coderd_license_user_limit_enabled gauge +coderd_license_user_limit_enabled 0 +# HELP coderd_prebuilt_workspaces_claimed_total Total number of prebuilt workspaces which were claimed by users. Claiming refers to creating a workspace with a preset selected for which eligible prebuilt workspaces are available and one is reassigned to a user. +# TYPE coderd_prebuilt_workspaces_claimed_total counter +coderd_prebuilt_workspaces_claimed_total{template_name="",preset_name="",organization_name=""} 0 +# HELP coderd_prebuilt_workspaces_created_total Total number of prebuilt workspaces that have been created to meet the desired instance count of each template preset. +# TYPE coderd_prebuilt_workspaces_created_total counter +coderd_prebuilt_workspaces_created_total{template_name="",preset_name="",organization_name=""} 0 +# HELP coderd_prebuilt_workspaces_desired Target number of prebuilt workspaces that should be available for each template preset. +# TYPE coderd_prebuilt_workspaces_desired gauge +coderd_prebuilt_workspaces_desired{template_name="",preset_name="",organization_name=""} 0 +# HELP coderd_prebuilt_workspaces_eligible Current number of prebuilt workspaces that are eligible to be claimed by users. These are workspaces that have completed their build process with their agent reporting 'ready' status. +# TYPE coderd_prebuilt_workspaces_eligible gauge +coderd_prebuilt_workspaces_eligible{template_name="",preset_name="",organization_name=""} 0 +# HELP coderd_prebuilt_workspaces_failed_total Total number of prebuilt workspaces that failed to build. +# TYPE coderd_prebuilt_workspaces_failed_total counter +coderd_prebuilt_workspaces_failed_total{template_name="",preset_name="",organization_name=""} 0 +# HELP coderd_prebuilt_workspaces_metrics_last_updated The unix timestamp when the metrics related to prebuilt workspaces were last updated; these metrics are cached. +# TYPE coderd_prebuilt_workspaces_metrics_last_updated gauge +coderd_prebuilt_workspaces_metrics_last_updated 0 +# HELP coderd_prebuilt_workspaces_preset_hard_limited Indicates whether a given preset has reached the hard failure limit (1 = hard-limited). Metric is omitted otherwise. +# TYPE coderd_prebuilt_workspaces_preset_hard_limited gauge +coderd_prebuilt_workspaces_preset_hard_limited{template_name="",preset_name="",organization_name=""} 0 +# HELP coderd_prebuilt_workspaces_reconciliation_paused Indicates whether prebuilds reconciliation is currently paused (1 = paused, 0 = not paused). +# TYPE coderd_prebuilt_workspaces_reconciliation_paused gauge +coderd_prebuilt_workspaces_reconciliation_paused 0 +# HELP coderd_prebuilt_workspaces_resource_replacements_total Total number of prebuilt workspaces whose resource(s) got replaced upon being claimed. In Terraform, drift on immutable attributes results in resource replacement. This represents a worst-case scenario for prebuilt workspaces because the pre-provisioned resource would have been recreated when claiming, thus obviating the point of pre-provisioning. See https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces#preventing-resource-replacement +# TYPE coderd_prebuilt_workspaces_resource_replacements_total counter +coderd_prebuilt_workspaces_resource_replacements_total{template_name="",preset_name="",organization_name=""} 0 +# HELP coderd_prebuilt_workspaces_running Current number of prebuilt workspaces that are in a running state. These workspaces have started successfully but may not yet be claimable by users (see coderd_prebuilt_workspaces_eligible). +# TYPE coderd_prebuilt_workspaces_running gauge +coderd_prebuilt_workspaces_running{template_name="",preset_name="",organization_name=""} 0 diff --git a/scripts/metricsdocgen/scanner/scanner.go b/scripts/metricsdocgen/scanner/scanner.go index 62a2d0bf7e611..1c04d6a012a27 100644 --- a/scripts/metricsdocgen/scanner/scanner.go +++ b/scripts/metricsdocgen/scanner/scanner.go @@ -50,6 +50,12 @@ type Metric struct { Labels []string // Label names for this metric } +// declarations holds const/var values collected from a file for resolving references. +type declarations struct { + strings map[string]string // string constants/variables + stringSlices map[string][]string // []string variables +} + func main() { metrics, err := scanAllDirs() if err != nil { @@ -136,6 +142,9 @@ func scanFile(path string) ([]Metric, error) { return nil, xerrors.Errorf("parsing file: %w", err) } + // First pass: collect const and var declarations for resolving references. + decls := collectDecls(file) + var metrics []Metric // Walk the AST looking for metric registration calls. @@ -145,7 +154,7 @@ func scanFile(path string) ([]Metric, error) { return true } - metric, ok := extractMetricFromCall(call) + metric, ok := extractMetricFromCall(call, decls) if ok { metrics = append(metrics, metric) } @@ -156,6 +165,162 @@ func scanFile(path string) ([]Metric, error) { return metrics, nil } +// resolveStringExpr attempts to resolve an expression to a string value. +// Examples: +// - "my_metric": "my_metric" (string literal) +// - metricName: resolved value of metricName constant (identifier) +func resolveStringExpr(expr ast.Expr, decls declarations) string { + switch e := expr.(type) { + case *ast.BasicLit: + return strings.Trim(e.Value, `"`) + case *ast.Ident: + return decls.strings[e.Name] + case *ast.BinaryExpr: + return resolveBinaryExpr(e, decls) + } + return "" +} + +// resolveBinaryExpr resolves a binary expression (string concatenation) to a string. +// It recursively resolves the left and right operands. +// Example: +// - "coderd_" + "api_" + "requests": "coderd_api_requests" +// - namespace + "_" + metricName: resolved concatenation +func resolveBinaryExpr(expr *ast.BinaryExpr, decls declarations) string { + left := resolveStringExpr(expr.X, decls) + right := resolveStringExpr(expr.Y, decls) + if left != "" && right != "" { + return left + right + } + return "" +} + +// extractStringSlice extracts a []string from a composite literal. +func extractStringSlice(lit *ast.CompositeLit, decls declarations) []string { + var labels []string + for _, elt := range lit.Elts { + if label := resolveStringExpr(elt, decls); label != "" { + labels = append(labels, label) + } + } + return labels +} + +// collectDecls collects const and var declarations from a file. +// This is used to resolve constant and variable references in metric definitions. +func collectDecls(file *ast.File) declarations { + decls := declarations{ + strings: make(map[string]string), + stringSlices: make(map[string][]string), + } + + for _, decl := range file.Decls { + genDecl, ok := decl.(*ast.GenDecl) + if !ok { + continue + } + + for _, spec := range genDecl.Specs { + valueSpec, ok := spec.(*ast.ValueSpec) + if !ok { + continue + } + + for i, name := range valueSpec.Names { + if i >= len(valueSpec.Values) { + continue + } + + switch v := valueSpec.Values[i].(type) { + case *ast.BasicLit: + // String literal: const name = "value" + decls.strings[name.Name] = strings.Trim(v.Value, `"`) + case *ast.BinaryExpr: + // Concatenation: const name = prefix + "suffix" + if resolved := resolveBinaryExpr(v, decls); resolved != "" { + decls.strings[name.Name] = resolved + } + case *ast.CompositeLit: + // Slice literal: var labels = []string{"a", "b"} + if labels := extractStringSlice(v, decls); labels != nil { + decls.stringSlices[name.Name] = labels + } + } + } + } + } + + return decls +} + +// extractLabels extracts label names from an expression. +// Handles []string{...} literals and variable references. +func extractLabels(expr ast.Expr, decls declarations) []string { + switch e := expr.(type) { + case *ast.CompositeLit: + // []string{"label1", "label2"} + return extractStringSlice(e, decls) + case *ast.Ident: + // Variable reference like 'labels'. + if labels, ok := decls.stringSlices[e.Name]; ok { + return labels + } + return nil + } + return nil +} + +// extractNewDescMetric extracts a metric from a prometheus.NewDesc() call. +// Pattern: prometheus.NewDesc(name, help, variableLabels, constLabels) +// Currently, coder only uses MustNewConstMetric with NewDesc. +// TODO(ssncferreira): Add support for other MustNewConst* functions if needed. +func extractNewDescMetric(call *ast.CallExpr, decls declarations) (Metric, bool) { + // Check if this is a prometheus.NewDesc call. + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return Metric{}, false + } + + ident, ok := sel.X.(*ast.Ident) + if !ok || ident.Name != "prometheus" || sel.Sel.Name != "NewDesc" { + return Metric{}, false + } + + // NewDesc requires at least 4 arguments: name, help, variableLabels, constLabels + if len(call.Args) < 4 { + return Metric{}, false + } + + // Extract name (first argument). + name := resolveStringExpr(call.Args[0], decls) + if name == "" { + return Metric{}, false + } + + // Extract help (second argument). + help := resolveStringExpr(call.Args[1], decls) + + // Extract labels (third argument). + labels := extractLabels(call.Args[2], decls) + + // Infer metric type from name suffix. + // TODO(ssncferreira): The actual type is determined by the MustNewConst* function + // that uses this descriptor (e.g., MustNewConstMetric with prometheus.CounterValue or + // prometheus.GaugeValue). Currently, coder only uses MustNewConstMetric, so we + // infer the type from naming conventions. + metricType := MetricTypeGauge + if strings.HasSuffix(name, "_total") || strings.HasSuffix(name, "_count") { + metricType = MetricTypeCounter + } + + return Metric{ + Name: name, + Type: metricType, + Help: help, + Labels: labels, + }, true +} + // extractMetricFromCall attempts to extract a Metric from a function call expression. // It returns the metric and true if successful, or an empty metric and false if // the call is not a metric registration. @@ -164,9 +329,13 @@ func scanFile(path string) ([]Metric, error) { // - prometheus.NewDesc() calls // - prometheus.New*() and prometheus.New*Vec() with *Opts{} // - promauto.With(reg).New*() and factory.New*() patterns -func extractMetricFromCall(_ *ast.CallExpr) (Metric, bool) { +func extractMetricFromCall(call *ast.CallExpr, decls declarations) (Metric, bool) { + // Check for prometheus.NewDesc() pattern. + if metric, ok := extractNewDescMetric(call, decls); ok { + return metric, true + } + // TODO(ssncferreira): Implement upstack. - // Handle prometheus.NewDesc() // Handle prometheus.New*Vec() and prometheus.New*() with *Opts{} // Handle promauto.With(reg).New*() pattern From f0cb7f37d9b7f4adc6268f867762f24ab19d1c64 Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Thu, 12 Feb 2026 17:52:41 +0000 Subject: [PATCH 2/2] chore: improve comments and add logging --- scripts/metricsdocgen/generated_metrics | 6 ++++++ scripts/metricsdocgen/scanner/scanner.go | 18 ++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/scripts/metricsdocgen/generated_metrics b/scripts/metricsdocgen/generated_metrics index 965c5b2bf143b..9696896bedda4 100644 --- a/scripts/metricsdocgen/generated_metrics +++ b/scripts/metricsdocgen/generated_metrics @@ -28,12 +28,18 @@ coderd_insights_templates_active_users{template_name=""} 0 # HELP coderd_license_active_users The number of active users. # TYPE coderd_license_active_users gauge coderd_license_active_users 0 +# HELP coderd_license_errors The number of active license errors. +# TYPE coderd_license_errors gauge +coderd_license_errors 0 # HELP coderd_license_limit_users The user seats limit based on the active Coder license. # TYPE coderd_license_limit_users gauge coderd_license_limit_users 0 # HELP coderd_license_user_limit_enabled Returns 1 if the current license enforces the user limit. # TYPE coderd_license_user_limit_enabled gauge coderd_license_user_limit_enabled 0 +# HELP coderd_license_warnings The number of active license warnings. +# TYPE coderd_license_warnings gauge +coderd_license_warnings 0 # HELP coderd_prebuilt_workspaces_claimed_total Total number of prebuilt workspaces which were claimed by users. Claiming refers to creating a workspace with a preset selected for which eligible prebuilt workspaces are available and one is reassigned to a user. # TYPE coderd_prebuilt_workspaces_claimed_total counter coderd_prebuilt_workspaces_claimed_total{template_name="",preset_name="",organization_name=""} 0 diff --git a/scripts/metricsdocgen/scanner/scanner.go b/scripts/metricsdocgen/scanner/scanner.go index 1c04d6a012a27..cba3f32b13d48 100644 --- a/scripts/metricsdocgen/scanner/scanner.go +++ b/scripts/metricsdocgen/scanner/scanner.go @@ -196,6 +196,8 @@ func resolveBinaryExpr(expr *ast.BinaryExpr, decls declarations) string { } // extractStringSlice extracts a []string from a composite literal. +// Example: +// - []string{"a", "b", myConst}: ["a", "b", ] func extractStringSlice(lit *ast.CompositeLit, decls declarations) []string { var labels []string for _, elt := range lit.Elts { @@ -242,8 +244,8 @@ func collectDecls(file *ast.File) declarations { } case *ast.CompositeLit: // Slice literal: var labels = []string{"a", "b"} - if labels := extractStringSlice(v, decls); labels != nil { - decls.stringSlices[name.Name] = labels + if resolved := extractStringSlice(v, decls); resolved != nil { + decls.stringSlices[name.Name] = resolved } } } @@ -253,8 +255,12 @@ func collectDecls(file *ast.File) declarations { return decls } -// extractLabels extracts label names from an expression. -// Handles []string{...} literals and variable references. +// extractLabels extracts label names from an expression passed as an argument +// to a metric constructor. Handles both inline []string literals and +// variable references from decls. +// Examples: +// - []string{"label1", "label2"}: ["label1", "label2"] (inline literal) +// - myLabels: resolved value of myLabels variable (variable reference) func extractLabels(expr ast.Expr, decls declarations) []string { switch e := expr.(type) { case *ast.CompositeLit: @@ -281,6 +287,9 @@ func extractNewDescMetric(call *ast.CallExpr, decls declarations) (Metric, bool) return Metric{}, false } + // Match calls that are exactly "prometheus.NewDesc()". This checks the local + // package identifier, not the resolved import path. If the prometheus package + // is imported with an alias, this will not match. ident, ok := sel.X.(*ast.Ident) if !ok || ident.Name != "prometheus" || sel.Sel.Name != "NewDesc" { return Metric{}, false @@ -294,6 +303,7 @@ func extractNewDescMetric(call *ast.CallExpr, decls declarations) (Metric, bool) // Extract name (first argument). name := resolveStringExpr(call.Args[0], decls) if name == "" { + log.Printf("extractNewDescMetric: skipping prometheus.NewDesc() call: could not resolve metric name") return Metric{}, false }