mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat(scripts/metricsdocgen): add prometheus.NewDesc() pattern to metrics scanner (#21461)
## Description This PR implements extraction of metrics defined using the `prometheus.NewDesc()` pattern. ## Changes * Add `extractNewDescMetric()` to extract metrics from `prometheus.NewDesc()` calls * Script generates an updated `scripts/metricsdocgen/generated_metrics` file Related to: https://github.com/coder/coder/issues/13223 **Disclosure:** This PR was mainly developed with Claude Sonnet 4, with iterative review and refinement by @ssncferreira
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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", <resolved value of myConst>]
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user