Files
coder/scripts/metricsdocgen/main.go
T
Mathias Fredriksson a6a8fd94d7 build(Makefile): enable parallel make -j gen with correct dependency graph (#22612)
`make gen` could not run with `-j` because inter-target dependency edges
were missing. Multiple recipes compile `coderd/rbac` (which includes
generated files like `object_gen.go`), and without explicit ordering,
parallel runs produced syntax errors from mid-write reads.

Three main changes:

**Dependency graph fixes** declare the compile-time chain through
`coderd/rbac` so that `object_gen.go` is written before anything that
imports it is compiled. The DB generation targets use a GNU Make 4.3+
grouped target (`&:`) so Make knows `generate.sh` co-produces
`querier.go`, `unique_constraint.go`, `dbmetrics`, and `dbauthz` in a
single invocation. `SKIP_DUMP_SQL=1` avoids re-entrant `make` inside
`generate.sh` when the Makefile already guarantees `dump.sql` is fresh.

**`scripts/atomicwrite` package** replaces `os.WriteFile` in all gen
scripts with a temp-file-in-same-dir + rename pattern, preventing
interrupted runs from leaving partial files.

**`.PRECIOUS` and shell atomic writes** protect git-tracked generated
files from Make's default delete-on-error behavior. Since these files
are committed, deletion is worse than staleness -- `git restore` is the
recovery path.

CI now runs `make -j --output-sync -B gen` (~32s, down from ~85s
serial).

| Scenario                          | Before             | After    |
|-----------------------------------|--------------------|----------|
| `make gen` (serial)               | 95s                | 95s      |
| `make -j gen` (parallel)          | race error         | **22s**  |
| CI `make -j --output-sync -B gen` | forced serial ~85s | **~32s** |
2026-03-05 11:58:10 +00:00

202 lines
5.4 KiB
Go

package main
import (
"bytes"
"errors"
"flag"
"io"
"log"
"os"
"sort"
"strings"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/scripts/atomicwrite"
)
var (
staticMetricsFile string
prometheusDocFile string
generatedMetricsFile string
dryRun bool
generatorPrefix = []byte("<!-- Code generated by 'make docs/admin/integrations/prometheus.md'. DO NOT EDIT -->")
generatorSuffix = []byte("<!-- End generated by 'make docs/admin/integrations/prometheus.md'. -->")
)
func main() {
flag.StringVar(&staticMetricsFile, "static-metrics", "scripts/metricsdocgen/metrics", "Path to static metrics file (manually maintained)")
flag.StringVar(&generatedMetricsFile, "generated-metrics", "scripts/metricsdocgen/generated_metrics", "Path to generated metrics file (from scanner)")
flag.StringVar(&prometheusDocFile, "prometheus-doc-file", "docs/admin/integrations/prometheus.md", "Path to Prometheus doc file")
flag.BoolVar(&dryRun, "dry-run", false, "Dry run")
flag.Parse()
metrics, err := readAndMergeMetrics()
if err != nil {
log.Fatal("can't read metrics: ", err)
}
doc, err := readPrometheusDoc()
if err != nil {
log.Fatal("can't read Prometheus doc: ", err)
}
doc, err = updatePrometheusDoc(doc, metrics)
if err != nil {
log.Fatal("can't update Prometheus doc: ", err)
}
if dryRun {
log.Println(string(doc))
return
}
err = writePrometheusDoc(doc)
if err != nil {
log.Fatal("can't write updated Prometheus doc: ", err)
}
}
// readMetricsFromFile reads metrics from a single Prometheus text format file.
func readMetricsFromFile(path string) ([]*dto.MetricFamily, error) {
f, err := os.Open(path)
if err != nil {
return nil, xerrors.Errorf("can't open metrics file %s: %w", path, err)
}
defer f.Close()
var metrics []*dto.MetricFamily
decoder := expfmt.NewDecoder(f, expfmt.NewFormat(expfmt.TypeTextPlain))
for {
var m dto.MetricFamily
err = decoder.Decode(&m)
if errors.Is(err, io.EOF) {
break
} else if err != nil {
return nil, xerrors.Errorf("decoding metrics from %s: %w", path, err)
}
metrics = append(metrics, &m)
}
return metrics, nil
}
// readAndMergeMetrics reads metrics from both generated and static files,
// merges them, and returns a sorted list. Generated metrics are produced
// by the AST scanner that extracts metric definitions from the coder source
// code while static metrics are manually maintained (e.g., go_*, process_*,
// external dependencies).
// Note: Static metrics take priority over generated metrics, allowing manual
// overrides for metrics that can't be accurately extracted by the scanner.
func readAndMergeMetrics() ([]*dto.MetricFamily, error) {
generatedMetrics, err := readMetricsFromFile(generatedMetricsFile)
if err != nil {
return nil, xerrors.Errorf("reading generated metrics: %w", err)
}
staticMetrics, err := readMetricsFromFile(staticMetricsFile)
if err != nil {
return nil, xerrors.Errorf("reading static metrics: %w", err)
}
// Merge metrics, using a map to deduplicate by name.
metricsByName := make(map[string]*dto.MetricFamily)
// Add generated metrics first.
for _, m := range generatedMetrics {
metricsByName[*m.Name] = m
}
// Static metrics overwrite generated metrics if they exist.
for _, m := range staticMetrics {
metricsByName[*m.Name] = m
}
// Convert back to slice and sort.
var metrics []*dto.MetricFamily
for _, m := range metricsByName {
metrics = append(metrics, m)
}
sort.Slice(metrics, func(i, j int) bool {
return *metrics[i].Name < *metrics[j].Name
})
return metrics, nil
}
func readPrometheusDoc() ([]byte, error) {
doc, err := os.ReadFile(prometheusDocFile)
if err != nil {
return nil, err
}
return doc, nil
}
func updatePrometheusDoc(doc []byte, metricFamilies []*dto.MetricFamily) ([]byte, error) {
i := bytes.Index(doc, generatorPrefix)
if i < 0 {
return nil, xerrors.New("generator prefix tag not found")
}
tableStartIndex := i + len(generatorPrefix) + 1
j := bytes.Index(doc[tableStartIndex:], generatorSuffix)
if j < 0 {
return nil, xerrors.New("generator suffix tag not found")
}
tableEndIndex := tableStartIndex + j
var buffer bytes.Buffer
_, _ = buffer.Write(doc[:tableStartIndex])
_ = buffer.WriteByte('\n')
_, _ = buffer.WriteString("| Name | Type | Description | Labels |\n")
_, _ = buffer.WriteString("| - | - | - | - |\n")
for _, mf := range metricFamilies {
_, _ = buffer.WriteString("| ")
_, _ = buffer.Write([]byte("`" + *mf.Name + "`"))
_, _ = buffer.WriteString(" | ")
_, _ = buffer.Write([]byte(strings.ToLower(mf.Type.String())))
_, _ = buffer.WriteString(" | ")
if mf.Help != nil {
_, _ = buffer.Write([]byte(*mf.Help))
}
_, _ = buffer.WriteString(" | ")
labels := map[string]struct{}{}
metrics := mf.GetMetric()
for _, m := range metrics {
for _, label := range m.Label {
labels["`"+*label.Name+"`"] = struct{}{}
}
}
if len(labels) > 0 {
_, _ = buffer.WriteString(strings.Join(sortedKeys(labels), " "))
}
_, _ = buffer.WriteString(" |\n")
}
_ = buffer.WriteByte('\n')
_, _ = buffer.Write(doc[tableEndIndex:])
return buffer.Bytes(), nil
}
func writePrometheusDoc(doc []byte) error {
return atomicwrite.File(prometheusDocFile, doc)
}
func sortedKeys(m map[string]struct{}) []string {
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}