From d0923b96823903745c9b9e7d7ccfe81a7661e2a8 Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Thu, 8 Jan 2026 20:29:07 +0000 Subject: [PATCH 1/3] feat(scripts/metricsdocgen): add prometheus.New*() and New*Vec() patterns to metrics scanner --- scripts/metricsdocgen/generated_metrics | 183 +++++++++++++++++++++++ scripts/metricsdocgen/scanner/scanner.go | 155 ++++++++++++++++++- 2 files changed, 337 insertions(+), 1 deletion(-) diff --git a/scripts/metricsdocgen/generated_metrics b/scripts/metricsdocgen/generated_metrics index 9696896bedd..a941c87bf4a 100644 --- a/scripts/metricsdocgen/generated_metrics +++ b/scripts/metricsdocgen/generated_metrics @@ -1,21 +1,168 @@ +# HELP agent_reconnecting_pty_connections_total +# TYPE agent_reconnecting_pty_connections_total counter +agent_reconnecting_pty_connections_total 0 +# HELP agent_reconnecting_pty_errors_total +# TYPE agent_reconnecting_pty_errors_total counter +agent_reconnecting_pty_errors_total{error_type=""} 0 +# HELP agent_scripts_executed_total +# TYPE agent_scripts_executed_total counter +agent_scripts_executed_total{success=""} 0 +# HELP agent_sessions_errors_total +# TYPE agent_sessions_errors_total counter +agent_sessions_errors_total{magic_type="",pty="",error_type=""} 0 +# HELP agent_sessions_total +# TYPE agent_sessions_total counter +agent_sessions_total{magic_type="",pty=""} 0 +# HELP agent_ssh_server_failed_connections_total +# TYPE agent_ssh_server_failed_connections_total counter +agent_ssh_server_failed_connections_total 0 +# HELP agent_ssh_server_sftp_connections_total +# TYPE agent_ssh_server_sftp_connections_total counter +agent_ssh_server_sftp_connections_total 0 +# HELP agent_ssh_server_sftp_server_errors_total +# TYPE agent_ssh_server_sftp_server_errors_total counter +agent_ssh_server_sftp_server_errors_total 0 +# HELP agent_x11_handler_errors_total +# TYPE agent_x11_handler_errors_total counter +agent_x11_handler_errors_total{error_type=""} 0 +# HELP coder_pubsub_connected Whether we are connected (1) or not connected (0) to postgres +# TYPE coder_pubsub_connected gauge +coder_pubsub_connected 0 # 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_disconnections_total Total number of times we disconnected unexpectedly from postgres +# TYPE coder_pubsub_disconnections_total counter +coder_pubsub_disconnections_total 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_messages_total Total number of messages received from postgres +# TYPE coder_pubsub_messages_total counter +coder_pubsub_messages_total{size=""} 0 +# HELP coder_pubsub_published_bytes_total Total number of bytes successfully published across all publishes +# TYPE coder_pubsub_published_bytes_total counter +coder_pubsub_published_bytes_total 0 +# HELP coder_pubsub_publishes_total Total number of calls to Publish +# TYPE coder_pubsub_publishes_total counter +coder_pubsub_publishes_total{success=""} 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_received_bytes_total Total number of bytes received across all messages +# TYPE coder_pubsub_received_bytes_total counter +coder_pubsub_received_bytes_total 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 coder_pubsub_subscribes_total Total number of calls to Subscribe/SubscribeWithErr +# TYPE coder_pubsub_subscribes_total counter +coder_pubsub_subscribes_total{success=""} 0 +# HELP coder_servertailnet_connections_total Total number of TCP connections made to workspace agents. +# TYPE coder_servertailnet_connections_total counter +coder_servertailnet_connections_total{network=""} 0 +# HELP coder_servertailnet_open_connections Total number of TCP connections currently open to workspace agents. +# TYPE coder_servertailnet_open_connections gauge +coder_servertailnet_open_connections{network=""} 0 +# HELP coderd_agentapi_metadata_batch_size Total number of metadata entries in each batch, updated before flushes. +# TYPE coderd_agentapi_metadata_batch_size histogram +coderd_agentapi_metadata_batch_size 0 +# HELP coderd_agentapi_metadata_batch_utilization Number of metadata keys per agent in each batch, updated before flushes. +# TYPE coderd_agentapi_metadata_batch_utilization histogram +coderd_agentapi_metadata_batch_utilization 0 +# HELP coderd_agentapi_metadata_batches_total Total number of metadata batches flushed. +# TYPE coderd_agentapi_metadata_batches_total counter +coderd_agentapi_metadata_batches_total{reason=""} 0 +# HELP coderd_agentapi_metadata_dropped_keys_total Total number of metadata keys dropped due to capacity limits. +# TYPE coderd_agentapi_metadata_dropped_keys_total counter +coderd_agentapi_metadata_dropped_keys_total 0 +# HELP coderd_agentapi_metadata_flush_duration_seconds Time taken to flush metadata batch to database and pubsub. +# TYPE coderd_agentapi_metadata_flush_duration_seconds histogram +coderd_agentapi_metadata_flush_duration_seconds{reason=""} 0 +# HELP coderd_agentapi_metadata_flushed_total Total number of unique metadatas flushed. +# TYPE coderd_agentapi_metadata_flushed_total counter +coderd_agentapi_metadata_flushed_total 0 +# HELP coderd_agentapi_metadata_publish_errors_total Total number of metadata batch pubsub publish calls that have resulted in an error. +# TYPE coderd_agentapi_metadata_publish_errors_total counter +coderd_agentapi_metadata_publish_errors_total 0 +# HELP coderd_agents_apps Agent applications with statuses. +# TYPE coderd_agents_apps gauge +coderd_agents_apps{app_name="",health=""} 0 +# HELP coderd_agents_connection_latencies_seconds Agent connection latencies in seconds. +# TYPE coderd_agents_connection_latencies_seconds gauge +coderd_agents_connection_latencies_seconds{derp_region="",preferred=""} 0 +# HELP coderd_agents_connections Agent connections with statuses. +# TYPE coderd_agents_connections gauge +coderd_agents_connections{status="",lifecycle_state="",tailnet_node=""} 0 +# HELP coderd_agents_up The number of active agents per workspace. +# TYPE coderd_agents_up gauge +coderd_agents_up{template_version=""} 0 +# HELP coderd_agentstats_connection_count The number of established connections by agent +# TYPE coderd_agentstats_connection_count gauge +coderd_agentstats_connection_count 0 +# HELP coderd_agentstats_connection_median_latency_seconds The median agent connection latency in seconds +# TYPE coderd_agentstats_connection_median_latency_seconds gauge +coderd_agentstats_connection_median_latency_seconds 0 +# HELP coderd_agentstats_currently_reachable_peers The number of peers (e.g. clients) that are currently reachable over the encrypted network. +# TYPE coderd_agentstats_currently_reachable_peers gauge +coderd_agentstats_currently_reachable_peers{connection_type=""} 0 +# HELP coderd_agentstats_rx_bytes Agent Rx bytes +# TYPE coderd_agentstats_rx_bytes gauge +coderd_agentstats_rx_bytes 0 +# HELP coderd_agentstats_session_count_jetbrains The number of session established by JetBrains +# TYPE coderd_agentstats_session_count_jetbrains gauge +coderd_agentstats_session_count_jetbrains 0 +# HELP coderd_agentstats_session_count_reconnecting_pty The number of session established by reconnecting PTY +# TYPE coderd_agentstats_session_count_reconnecting_pty gauge +coderd_agentstats_session_count_reconnecting_pty 0 +# HELP coderd_agentstats_session_count_ssh The number of session established by SSH +# TYPE coderd_agentstats_session_count_ssh gauge +coderd_agentstats_session_count_ssh 0 +# HELP coderd_agentstats_session_count_vscode The number of session established by VSCode +# TYPE coderd_agentstats_session_count_vscode gauge +coderd_agentstats_session_count_vscode 0 +# HELP coderd_agentstats_startup_script_seconds Amount of time taken to run the startup script in seconds. +# TYPE coderd_agentstats_startup_script_seconds gauge +coderd_agentstats_startup_script_seconds{success=""} 0 +# HELP coderd_agentstats_tx_bytes Agent Tx bytes +# TYPE coderd_agentstats_tx_bytes gauge +coderd_agentstats_tx_bytes 0 +# HELP coderd_api_active_users_duration_hour The number of users that have been active within the last hour. +# TYPE coderd_api_active_users_duration_hour gauge +coderd_api_active_users_duration_hour 0 +# HELP coderd_api_total_user_count The total number of registered users, partitioned by status. +# TYPE coderd_api_total_user_count gauge +coderd_api_total_user_count{status=""} 0 +# HELP coderd_api_workspace_latest_build The current number of workspace builds by status for all non-deleted workspaces. +# TYPE coderd_api_workspace_latest_build gauge +coderd_api_workspace_latest_build{status=""} 0 +# HELP coderd_db_query_counts_total Total number of queries labelled by HTTP route, method, and query name. +# TYPE coderd_db_query_counts_total counter +coderd_db_query_counts_total{route="",method="",query=""} 0 +# HELP coderd_db_query_latencies_seconds Latency distribution of queries in seconds. +# TYPE coderd_db_query_latencies_seconds histogram +coderd_db_query_latencies_seconds{query=""} 0 +# HELP coderd_db_tx_duration_seconds Duration of transactions in seconds. +# TYPE coderd_db_tx_duration_seconds histogram +coderd_db_tx_duration_seconds{success="",tx_id=""} 0 +# HELP coderd_db_tx_executions_count Total count of transactions executed. 'retries' is expected to be 0 for a successful transaction. +# TYPE coderd_db_tx_executions_count counter +coderd_db_tx_executions_count{success="",retries="",tx_id=""} 0 +# HELP coderd_dbpurge_iteration_duration_seconds Duration of each dbpurge iteration in seconds. +# TYPE coderd_dbpurge_iteration_duration_seconds histogram +coderd_dbpurge_iteration_duration_seconds{success=""} 0 +# HELP coderd_dbpurge_records_purged_total Total number of records purged by type. +# TYPE coderd_dbpurge_records_purged_total counter +coderd_dbpurge_records_purged_total{record_type=""} 0 +# HELP coderd_experiments Indicates whether each experiment is enabled (1) or not (0) +# TYPE coderd_experiments gauge +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 @@ -40,6 +187,9 @@ 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_workspace_claim_duration_seconds Time to claim a prebuilt workspace by organization, template, and preset. +# TYPE coderd_prebuilt_workspace_claim_duration_seconds histogram +coderd_prebuilt_workspace_claim_duration_seconds{organization_name="",template_name="",preset_name=""} 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 @@ -70,3 +220,36 @@ coderd_prebuilt_workspaces_resource_replacements_total{template_name="",preset_n # 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 +# HELP coderd_prometheusmetrics_agents_execution_seconds Histogram for duration of agents metrics collection in seconds. +# TYPE coderd_prometheusmetrics_agents_execution_seconds histogram +coderd_prometheusmetrics_agents_execution_seconds 0 +# HELP coderd_prometheusmetrics_agentstats_execution_seconds Histogram for duration of agent stats metrics collection in seconds. +# TYPE coderd_prometheusmetrics_agentstats_execution_seconds histogram +coderd_prometheusmetrics_agentstats_execution_seconds 0 +# HELP coderd_prometheusmetrics_metrics_aggregator_execution_cleanup_seconds Histogram for duration of metrics aggregator cleanup in seconds. +# TYPE coderd_prometheusmetrics_metrics_aggregator_execution_cleanup_seconds histogram +coderd_prometheusmetrics_metrics_aggregator_execution_cleanup_seconds 0 +# HELP coderd_prometheusmetrics_metrics_aggregator_execution_update_seconds Histogram for duration of metrics aggregator update in seconds. +# TYPE coderd_prometheusmetrics_metrics_aggregator_execution_update_seconds histogram +coderd_prometheusmetrics_metrics_aggregator_execution_update_seconds 0 +# HELP coderd_prometheusmetrics_metrics_aggregator_store_size The number of metrics stored in the aggregator +# TYPE coderd_prometheusmetrics_metrics_aggregator_store_size gauge +coderd_prometheusmetrics_metrics_aggregator_store_size 0 +# HELP coderd_proxyhealth_health_check_duration_seconds Histogram for duration of proxy health collection in seconds. +# TYPE coderd_proxyhealth_health_check_duration_seconds histogram +coderd_proxyhealth_health_check_duration_seconds 0 +# HELP coderd_proxyhealth_health_check_results This endpoint returns a number to indicate the health status. -3 (unknown), -2 (Unreachable), -1 (Unhealthy), 0 (Unregistered), 1 (Healthy) +# TYPE coderd_proxyhealth_health_check_results gauge +coderd_proxyhealth_health_check_results{proxy_id=""} 0 +# HELP coderd_template_workspace_build_duration_seconds Duration from workspace build creation to agent ready, by template. +# TYPE coderd_template_workspace_build_duration_seconds histogram +coderd_template_workspace_build_duration_seconds{template_name="",organization_name="",transition="",status="",is_prebuild=""} 0 +# HELP coderd_workspace_creation_duration_seconds Time to create a workspace by organization, template, preset, and type (regular or prebuild). +# TYPE coderd_workspace_creation_duration_seconds histogram +coderd_workspace_creation_duration_seconds{organization_name="",template_name="",preset_name="",type=""} 0 +# HELP coderd_workspace_creation_total Total regular (non-prebuilt) workspace creations by organization, template, and preset. +# TYPE coderd_workspace_creation_total counter +coderd_workspace_creation_total{organization_name="",template_name="",preset_name=""} 0 +# HELP coderd_workspace_latest_build_status The current workspace statuses by template, transition, and owner for all non-deleted workspaces. +# TYPE coderd_workspace_latest_build_status gauge +coderd_workspace_latest_build_status{status="",template_name="",template_version="",workspace_owner="",workspace_transition=""} 0 diff --git a/scripts/metricsdocgen/scanner/scanner.go b/scripts/metricsdocgen/scanner/scanner.go index cba3f32b13d..d3630fa3372 100644 --- a/scripts/metricsdocgen/scanner/scanner.go +++ b/scripts/metricsdocgen/scanner/scanner.go @@ -50,6 +50,14 @@ type Metric struct { Labels []string // Label names for this metric } +// metricOpts holds the fields extracted from a prometheus.*Opts struct. +type metricOpts struct { + Namespace string + Subsystem string + Name string + Help string +} + // declarations holds const/var values collected from a file for resolving references. type declarations struct { strings map[string]string // string constants/variables @@ -331,6 +339,147 @@ func extractNewDescMetric(call *ast.CallExpr, decls declarations) (Metric, bool) }, true } +// parseMetricFuncName parses a prometheus function name and returns the metric type +// and whether it's a Vec type. Returns empty string if not a recognized metric function. +func parseMetricFuncName(funcName string) (MetricType, bool) { + isVec := strings.HasSuffix(funcName, "Vec") + baseName := strings.TrimSuffix(funcName, "Vec") + + switch baseName { + case "NewGauge": + return MetricTypeGauge, isVec + case "NewCounter": + return MetricTypeCounter, isVec + case "NewHistogram": + return MetricTypeHistogram, isVec + case "NewSummary": + return MetricTypeSummary, isVec + } + return "", false +} + +// extractOpts extracts fields from a prometheus.*Opts composite literal. +func extractOpts(expr ast.Expr, decls declarations) (metricOpts, bool) { + // Handle both direct composite literals and calls that return opts. + var lit *ast.CompositeLit + + switch e := expr.(type) { + case *ast.CompositeLit: + lit = e + case *ast.UnaryExpr: + // Handle &prometheus.GaugeOpts{...} + if l, ok := e.X.(*ast.CompositeLit); ok { + lit = l + } + } + + if lit == nil { + return metricOpts{}, false + } + + var opts metricOpts + for _, elt := range lit.Elts { + kv, ok := elt.(*ast.KeyValueExpr) + if !ok { + continue + } + + key, ok := kv.Key.(*ast.Ident) + if !ok { + continue + } + + value := resolveStringExpr(kv.Value, decls) + + switch key.Name { + case "Namespace": + opts.Namespace = value + case "Subsystem": + opts.Subsystem = value + case "Name": + opts.Name = value + case "Help": + opts.Help = value + } + } + + return opts, opts.Name != "" +} + +// buildMetricName constructs the full metric name from namespace, subsystem, and name. +func buildMetricName(namespace, subsystem, name string) string { + parts := make([]string, 0, 3) + if namespace != "" { + parts = append(parts, namespace) + } + if subsystem != "" { + parts = append(parts, subsystem) + } + if name != "" { + parts = append(parts, name) + } + return strings.Join(parts, "_") +} + +// extractOptsMetric extracts a metric from prometheus.New*() or prometheus.New*Vec() calls. +// Supported patterns: +// - prometheus.NewGauge(prometheus.GaugeOpts{...}) +// - prometheus.NewCounter(prometheus.CounterOpts{...}) +// - prometheus.NewHistogram(prometheus.HistogramOpts{...}) +// - prometheus.NewSummary(prometheus.SummaryOpts{...}) +// - prometheus.NewGaugeVec(prometheus.GaugeOpts{...}, labels) +// - prometheus.NewCounterVec(prometheus.CounterOpts{...}, labels) +// - prometheus.NewHistogramVec(prometheus.HistogramOpts{...}, labels) +// - prometheus.NewSummaryVec(prometheus.SummaryOpts{...}, labels) +func extractOptsMetric(call *ast.CallExpr, decls declarations) (Metric, bool) { + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return Metric{}, false + } + + // Check for prometheus.New* pattern. + ident, ok := sel.X.(*ast.Ident) + if !ok || ident.Name != "prometheus" { + return Metric{}, false + } + + funcName := sel.Sel.Name + metricType, isVec := parseMetricFuncName(funcName) + if metricType == "" { + return Metric{}, false + } + + // Need at least one argument (the Opts struct). + if len(call.Args) < 1 { + return Metric{}, false + } + + // Extract metric info from the Opts struct. + opts, ok := extractOpts(call.Args[0], decls) + if !ok { + return Metric{}, false + } + + // Extract labels for Vec types. + var labels []string + if isVec && len(call.Args) >= 2 { + labels = extractLabels(call.Args[1], decls) + } + + // Build the full metric name. + name := buildMetricName(opts.Namespace, opts.Subsystem, opts.Name) + if name == "" { + return Metric{}, false + } + + return Metric{ + Name: name, + Type: metricType, + Help: opts.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. @@ -345,8 +494,12 @@ func extractMetricFromCall(call *ast.CallExpr, decls declarations) (Metric, bool return metric, true } + // Check for prometheus.New*() and prometheus.New*Vec() patterns. + if metric, ok := extractOptsMetric(call, decls); ok { + return metric, true + } + // TODO(ssncferreira): Implement upstack. - // Handle prometheus.New*Vec() and prometheus.New*() with *Opts{} // Handle promauto.With(reg).New*() pattern return Metric{}, false From b8f07e755d2a0a81fe3ef814df7954ddbdfb6b32 Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Fri, 9 Jan 2026 12:29:02 +0000 Subject: [PATCH 2/3] fix: resolve cross-package label constants in metrics scanner --- scripts/metricsdocgen/generated_metrics | 8 ++-- scripts/metricsdocgen/scanner/scanner.go | 58 +++++++++++++++++++++++- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/scripts/metricsdocgen/generated_metrics b/scripts/metricsdocgen/generated_metrics index a941c87bf4a..d3ef452499d 100644 --- a/scripts/metricsdocgen/generated_metrics +++ b/scripts/metricsdocgen/generated_metrics @@ -93,16 +93,16 @@ coderd_agentapi_metadata_flushed_total 0 coderd_agentapi_metadata_publish_errors_total 0 # HELP coderd_agents_apps Agent applications with statuses. # TYPE coderd_agents_apps gauge -coderd_agents_apps{app_name="",health=""} 0 +coderd_agents_apps{agent_name="",username="",workspace_name="",app_name="",health=""} 0 # HELP coderd_agents_connection_latencies_seconds Agent connection latencies in seconds. # TYPE coderd_agents_connection_latencies_seconds gauge -coderd_agents_connection_latencies_seconds{derp_region="",preferred=""} 0 +coderd_agents_connection_latencies_seconds{agent_name="",username="",workspace_name="",derp_region="",preferred=""} 0 # HELP coderd_agents_connections Agent connections with statuses. # TYPE coderd_agents_connections gauge -coderd_agents_connections{status="",lifecycle_state="",tailnet_node=""} 0 +coderd_agents_connections{agent_name="",username="",workspace_name="",status="",lifecycle_state="",tailnet_node=""} 0 # HELP coderd_agents_up The number of active agents per workspace. # TYPE coderd_agents_up gauge -coderd_agents_up{template_version=""} 0 +coderd_agents_up{username="",workspace_name="",template_name="",template_version=""} 0 # HELP coderd_agentstats_connection_count The number of established connections by agent # TYPE coderd_agentstats_connection_count gauge coderd_agentstats_connection_count 0 diff --git a/scripts/metricsdocgen/scanner/scanner.go b/scripts/metricsdocgen/scanner/scanner.go index d3630fa3372..1a4202de4f1 100644 --- a/scripts/metricsdocgen/scanner/scanner.go +++ b/scripts/metricsdocgen/scanner/scanner.go @@ -64,6 +64,10 @@ type declarations struct { stringSlices map[string][]string // []string variables } +// packageDeclarations holds exported string constants collected from all scanned files, +// keyed by package name. This allows resolving cross-file references. +var packageDeclarations = make(map[string]map[string]string) + func main() { metrics, err := scanAllDirs() if err != nil { @@ -150,7 +154,10 @@ func scanFile(path string) ([]Metric, error) { return nil, xerrors.Errorf("parsing file: %w", err) } - // First pass: collect const and var declarations for resolving references. + // Collect exported constants into the global package declarations map. + collectPackageConsts(file) + + // Collect file-local const and var declarations for resolving references. decls := collectDecls(file) var metrics []Metric @@ -173,10 +180,51 @@ func scanFile(path string) ([]Metric, error) { return metrics, nil } +// collectPackageConsts collects exported string constants from a file into +// the global packageDeclarations map, keyed by package name. +func collectPackageConsts(file *ast.File) { + pkgName := file.Name.Name + + if packageDeclarations[pkgName] == nil { + packageDeclarations[pkgName] = make(map[string]string) + } + + for _, decl := range file.Decls { + genDecl, ok := decl.(*ast.GenDecl) + if !ok || genDecl.Tok != token.CONST { + continue + } + + for _, spec := range genDecl.Specs { + valueSpec, ok := spec.(*ast.ValueSpec) + if !ok { + continue + } + + for i, name := range valueSpec.Names { + if !ast.IsExported(name.Name) { + continue + } + + if i >= len(valueSpec.Values) { + continue + } + + if lit, ok := valueSpec.Values[i].(*ast.BasicLit); ok { + if lit.Kind == token.STRING { + packageDeclarations[pkgName][name.Name] = strings.Trim(lit.Value, `"`) + } + } + } + } + } +} + // resolveStringExpr attempts to resolve an expression to a string value. // Examples: // - "my_metric": "my_metric" (string literal) // - metricName: resolved value of metricName constant (identifier) +// - agentmetrics.LabelUsername: resolved from package constants (selector) func resolveStringExpr(expr ast.Expr, decls declarations) string { switch e := expr.(type) { case *ast.BasicLit: @@ -185,7 +233,15 @@ func resolveStringExpr(expr ast.Expr, decls declarations) string { return decls.strings[e.Name] case *ast.BinaryExpr: return resolveBinaryExpr(e, decls) + case *ast.SelectorExpr: + // Handle pkg.Const syntax. + if ident, ok := e.X.(*ast.Ident); ok { + if pkgConsts, ok := packageDeclarations[ident.Name]; ok { + return pkgConsts[e.Sel.Name] + } + } } + return "" } From 777de5c3900819c706bf61cdb176b4d9f65e9a67 Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Thu, 12 Feb 2026 18:33:09 +0000 Subject: [PATCH 3/3] chore: improve comments and add logging --- scripts/metricsdocgen/scanner/scanner.go | 25 ++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/scripts/metricsdocgen/scanner/scanner.go b/scripts/metricsdocgen/scanner/scanner.go index 1a4202de4f1..e2ce64cb0ff 100644 --- a/scripts/metricsdocgen/scanner/scanner.go +++ b/scripts/metricsdocgen/scanner/scanner.go @@ -66,6 +66,8 @@ type declarations struct { // packageDeclarations holds exported string constants collected from all scanned files, // keyed by package name. This allows resolving cross-file references. +// Note: resolution depends on directory scan order in scanDirs, i.e., +// constants from later directories won't be available when scanning earlier ones. var packageDeclarations = make(map[string]map[string]string) func main() { @@ -171,6 +173,11 @@ func scanFile(path string) ([]Metric, error) { metric, ok := extractMetricFromCall(call, decls) if ok { + // TODO(ssncferreira): Consider filtering out metrics with empty Help descriptions. + // These indicate missing documentation in the source code. + if metric.Help == "" { + log.Printf("WARNING: metric %q has no HELP description, consider updating the source code", metric.Name) + } metrics = append(metrics, metric) } @@ -464,17 +471,19 @@ func extractOpts(expr ast.Expr, decls declarations) (metricOpts, bool) { // buildMetricName constructs the full metric name from namespace, subsystem, and name. func buildMetricName(namespace, subsystem, name string) string { - parts := make([]string, 0, 3) + metricNameParts := make([]string, 0, 3) if namespace != "" { - parts = append(parts, namespace) + metricNameParts = append(metricNameParts, namespace) } if subsystem != "" { - parts = append(parts, subsystem) + metricNameParts = append(metricNameParts, subsystem) } if name != "" { - parts = append(parts, name) + metricNameParts = append(metricNameParts, name) } - return strings.Join(parts, "_") + // Join non-empty parts with "_" to handle optional namespace/subsystem. + // e.g., ("coderd", "", "agents_up"): "coderd_agents_up" + return strings.Join(metricNameParts, "_") } // extractOptsMetric extracts a metric from prometheus.New*() or prometheus.New*Vec() calls. @@ -493,7 +502,9 @@ func extractOptsMetric(call *ast.CallExpr, decls declarations) (Metric, bool) { return Metric{}, false } - // Check for prometheus.New* pattern. + // Match calls that are exactly "prometheus.New*(...)". 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" { return Metric{}, false @@ -513,6 +524,7 @@ func extractOptsMetric(call *ast.CallExpr, decls declarations) (Metric, bool) { // Extract metric info from the Opts struct. opts, ok := extractOpts(call.Args[0], decls) if !ok { + log.Printf("extractOptsMetric: skipping prometheus.%s() call: could not extract opts", funcName) return Metric{}, false } @@ -525,6 +537,7 @@ func extractOptsMetric(call *ast.CallExpr, decls declarations) (Metric, bool) { // Build the full metric name. name := buildMetricName(opts.Namespace, opts.Subsystem, opts.Name) if name == "" { + log.Printf("extractOptsMetric: skipping prometheus.%s() call: could not build metric name", funcName) return Metric{}, false }