11// Package main provides a tool to scan Go source files and extract Prometheus
2- // metric definitions. It outputs metrics in Prometheus text format for use
3- // by the documentation generator.
2+ // metric definitions. It outputs metrics in Prometheus text exposition format
3+ // to stdout for use by the documentation generator.
44//
55// Usage:
66//
7- // go run ./scripts/metricsdocgen/scanner [-output FILE]
7+ // go run ./scripts/metricsdocgen/scanner > scripts/metricsdocgen/generated_metrics
88package main
99
1010import (
11- "flag"
1211 "fmt"
1312 "go/ast"
1413 "go/parser"
1514 "go/token"
15+ "io"
1616 "io/fs"
1717 "log"
1818 "os"
@@ -24,14 +24,15 @@ import (
2424)
2525
2626// Directories to scan for metric definitions, relative to the repository root.
27+ // Add or remove directories here to control the scanner's scope.
2728var scanDirs = []string {
2829 "agent" ,
2930 "coderd" ,
3031 "enterprise" ,
3132 "provisionerd" ,
3233}
3334
34- // MetricType represents the type of a Prometheus metric.
35+ // MetricType represents the type of Prometheus metric.
3536type MetricType string
3637
3738const (
@@ -49,27 +50,30 @@ type Metric struct {
4950 Labels []string // Label names for this metric
5051}
5152
52- var outputFile string
53-
5453func main () {
55- flag .StringVar (& outputFile , "output" , "scripts/metricsdocgen/generated_metrics" , "Output file path" )
56- flag .Parse ()
57-
5854 metrics , err := scanAllDirs ()
5955 if err != nil {
6056 log .Fatalf ("Failed to scan directories: %v" , err )
6157 }
6258
59+ // Duplicates are not expected since Prometheus enforces unique metric names at registration.
60+ uniqueMetrics := make (map [string ]Metric )
61+ for _ , m := range metrics {
62+ uniqueMetrics [m .Name ] = m
63+ }
64+ metrics = make ([]Metric , 0 , len (uniqueMetrics ))
65+ for _ , m := range uniqueMetrics {
66+ metrics = append (metrics , m )
67+ }
68+
6369 // Sort metrics by name for consistent output across runs.
6470 sort .Slice (metrics , func (i , j int ) bool {
6571 return metrics [i ].Name < metrics [j ].Name
6672 })
6773
68- if err := writeMetrics (metrics , outputFile ); err != nil {
69- log .Fatalf ("Failed to write metrics: %v" , err )
70- }
74+ writeMetrics (metrics , os .Stdout )
7175
72- _ , _ = fmt . Fprintf ( os . Stderr , "Wrote %d metrics to %s \n " , len (metrics ), outputFile )
76+ log . Printf ( "Successfully parsed %d metrics" , len (metrics ))
7377}
7478
7579// scanAllDirs scans all configured directories for metric definitions.
@@ -81,6 +85,8 @@ func scanAllDirs() ([]Metric, error) {
8185 if err != nil {
8286 return nil , xerrors .Errorf ("scanning %s: %w" , dir , err )
8387 }
88+
89+ log .Printf ("scanning %s: found %d metrics" , dir , len (metrics ))
8490 allMetrics = append (allMetrics , metrics ... )
8591 }
8692
@@ -110,6 +116,10 @@ func scanDirectory(root string) ([]Metric, error) {
110116 if err != nil {
111117 return xerrors .Errorf ("scanning %s: %w" , path , err )
112118 }
119+
120+ if len (fileMetrics ) > 0 {
121+ log .Printf ("scanning %s: found %d metrics" , path , len (fileMetrics ))
122+ }
113123 metrics = append (metrics , fileMetrics ... )
114124
115125 return nil
@@ -121,7 +131,7 @@ func scanDirectory(root string) ([]Metric, error) {
121131// scanFile parses a single Go file and extracts all Prometheus metric definitions.
122132func scanFile (path string ) ([]Metric , error ) {
123133 fset := token .NewFileSet ()
124- file , err := parser .ParseFile (fset , path , nil , parser .ParseComments )
134+ file , err := parser .ParseFile (fset , path , nil , parser .SkipObjectResolution )
125135 if err != nil {
126136 return nil , xerrors .Errorf ("parsing file: %w" , err )
127137 }
@@ -130,12 +140,12 @@ func scanFile(path string) ([]Metric, error) {
130140
131141 // Walk the AST looking for metric registration calls.
132142 ast .Inspect (file , func (n ast.Node ) bool {
133- _ , ok := n .(* ast.CallExpr )
143+ call , ok := n .(* ast.CallExpr )
134144 if ! ok {
135145 return true
136146 }
137147
138- metric , ok := extractMetricFromCall ()
148+ metric , ok := extractMetricFromCall (call )
139149 if ok {
140150 metrics = append (metrics , metric )
141151 }
@@ -154,7 +164,7 @@ func scanFile(path string) ([]Metric, error) {
154164// - prometheus.NewDesc() calls
155165// - prometheus.New*() and prometheus.New*Vec() with *Opts{}
156166// - promauto.With(reg).New*() and factory.New*() patterns
157- func extractMetricFromCall () (Metric , bool ) {
167+ func extractMetricFromCall (_ * ast. CallExpr ) (Metric , bool ) {
158168 // TODO(ssncferreira): Implement upstack.
159169 // Handle prometheus.NewDesc()
160170 // Handle prometheus.New*Vec() and prometheus.New*() with *Opts{}
@@ -163,31 +173,35 @@ func extractMetricFromCall() (Metric, bool) {
163173 return Metric {}, false
164174}
165175
166- // writeMetrics writes the metrics in Prometheus text exposition format.
176+ // String returns the metric in Prometheus text exposition format.
167177// Label values are empty strings and metric values are 0 since only
168178// metadata (name, type, help, label names) is used for documentation generation.
169- func writeMetrics ( metrics [] Metric , path string ) error {
179+ func ( m Metric ) String () string {
170180 var buf strings.Builder
171181
172- for _ , m := range metrics {
173- // Write HELP line.
174- _ , _ = buf .WriteString (fmt .Sprintf ("# HELP %s %s\n " , m .Name , m .Help ))
175-
176- // Write TYPE line.
177- _ , _ = buf .WriteString (fmt .Sprintf ("# TYPE %s %s\n " , m .Name , m .Type ))
178-
179- // Write a sample metric line with empty label values and zero metric value.
180- if len (m .Labels ) > 0 {
181- labelPairs := make ([]string , len (m .Labels ))
182- for i , l := range m .Labels {
183- labelPairs [i ] = fmt .Sprintf ("%s=\" \" " , l )
184- }
185- _ , _ = buf .WriteString (fmt .Sprintf ("%s{%s} 0\n " , m .Name , strings .Join (labelPairs , "," )))
186- } else {
187- _ , _ = buf .WriteString (fmt .Sprintf ("%s 0\n " , m .Name ))
182+ // Write HELP line.
183+ _ , _ = fmt .Fprintf (& buf , "# HELP %s %s\n " , m .Name , m .Help )
184+
185+ // Write TYPE line.
186+ _ , _ = fmt .Fprintf (& buf , "# TYPE %s %s\n " , m .Name , m .Type )
187+
188+ // Write a sample metric line with empty label values and zero metric value.
189+ if len (m .Labels ) > 0 {
190+ labelPairs := make ([]string , len (m .Labels ))
191+ for i , l := range m .Labels {
192+ labelPairs [i ] = fmt .Sprintf ("%s=\" \" " , l )
188193 }
194+ _ , _ = fmt .Fprintf (& buf , "%s{%s} 0\n " , m .Name , strings .Join (labelPairs , "," ))
195+ } else {
196+ _ , _ = fmt .Fprintf (& buf , "%s 0\n " , m .Name )
189197 }
190198
191- // #nosec G306 - metrics file needs to be readable
192- return os .WriteFile (path , []byte (buf .String ()), 0o644 )
199+ return buf .String ()
200+ }
201+
202+ // writeMetrics writes all metrics in Prometheus text exposition format.
203+ func writeMetrics (metrics []Metric , w io.Writer ) {
204+ for _ , m := range metrics {
205+ _ , _ = fmt .Fprint (w , m .String ())
206+ }
193207}
0 commit comments