diff --git a/scripts/metricsdocgen/generated_metrics b/scripts/metricsdocgen/generated_metrics index e69de29bb2d1d..9696896bedda4 100644 --- a/scripts/metricsdocgen/generated_metrics +++ b/scripts/metricsdocgen/generated_metrics @@ -0,0 +1,72 @@ +# 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_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 +# 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..cba3f32b13d48 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,172 @@ 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. +// Example: +// - []string{"a", "b", myConst}: ["a", "b", ] +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 resolved := extractStringSlice(v, decls); resolved != nil { + decls.stringSlices[name.Name] = resolved + } + } + } + } + } + + return decls +} + +// 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: + // []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 + } + + // 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 + } + + // 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 == "" { + log.Printf("extractNewDescMetric: skipping prometheus.NewDesc() call: could not resolve metric 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 +339,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