find-metrics.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. // Copyright (C) 2023 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. // Usage: go run script/find-metrics.go > metrics.md
  7. //
  8. // This script finds all of the metrics in the Syncthing codebase and prints
  9. // them in Markdown format. It's used to generate the metrics documentation
  10. // for the Syncthing docs.
  11. package main
  12. import (
  13. "fmt"
  14. "go/ast"
  15. "go/token"
  16. "log"
  17. "strconv"
  18. "strings"
  19. "golang.org/x/exp/slices"
  20. "golang.org/x/tools/go/packages"
  21. )
  22. type metric struct {
  23. subsystem string
  24. name string
  25. help string
  26. kind string
  27. }
  28. func main() {
  29. opts := &packages.Config{
  30. Mode: packages.NeedSyntax | packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedImports | packages.NeedDeps,
  31. }
  32. pkgs, err := packages.Load(opts, "github.com/syncthing/syncthing/...")
  33. if err != nil {
  34. log.Fatalln(err)
  35. }
  36. var coll metricCollector
  37. for _, pkg := range pkgs {
  38. for _, file := range pkg.Syntax {
  39. ast.Inspect(file, coll.Visit)
  40. }
  41. }
  42. coll.print()
  43. }
  44. type metricCollector struct {
  45. metrics []metric
  46. }
  47. func (c *metricCollector) Visit(n ast.Node) bool {
  48. if gen, ok := n.(*ast.GenDecl); ok {
  49. // We're only interested in var declarations (var metricWhatever =
  50. // promauto.NewCounter(...) etc).
  51. if gen.Tok != token.VAR {
  52. return false
  53. }
  54. for _, spec := range gen.Specs {
  55. // We want to look at the value given to a var (the NewCounter()
  56. // etc call).
  57. if vsp, ok := spec.(*ast.ValueSpec); ok {
  58. // There should be only one value.
  59. if len(vsp.Values) != 1 {
  60. continue
  61. }
  62. // The value should be a function call.
  63. call, ok := vsp.Values[0].(*ast.CallExpr)
  64. if !ok {
  65. continue
  66. }
  67. // The call should be a selector expression
  68. // (package.Identifer).
  69. sel, ok := call.Fun.(*ast.SelectorExpr)
  70. if !ok {
  71. continue
  72. }
  73. // The package selector should be `promauto`.
  74. selID, ok := sel.X.(*ast.Ident)
  75. if !ok || selID.Name != "promauto" {
  76. continue
  77. }
  78. // The function should be one of the New* functions.
  79. var kind string
  80. switch sel.Sel.Name {
  81. case "NewCounter":
  82. kind = "counter"
  83. case "NewGauge":
  84. kind = "gauge"
  85. case "NewCounterVec":
  86. kind = "counter vector"
  87. case "NewGaugeVec":
  88. kind = "gauge vector"
  89. default:
  90. continue
  91. }
  92. // The arguments to the function should be a single
  93. // composite (struct literal). Grab all of the fields in the
  94. // declaration into a map so we can easily access them.
  95. args := make(map[string]string)
  96. for _, el := range call.Args[0].(*ast.CompositeLit).Elts {
  97. kv := el.(*ast.KeyValueExpr)
  98. key := kv.Key.(*ast.Ident).Name // e.g., "Name"
  99. val := kv.Value.(*ast.BasicLit).Value // e.g., `"foo"`
  100. args[key], _ = strconv.Unquote(val)
  101. }
  102. // Build the full name of the metric from the namespace +
  103. // subsystem + name, like Prometheus does.
  104. var parts []string
  105. if v := args["Namespace"]; v != "" {
  106. parts = append(parts, v)
  107. }
  108. if v := args["Subsystem"]; v != "" {
  109. parts = append(parts, v)
  110. }
  111. if v := args["Name"]; v != "" {
  112. parts = append(parts, v)
  113. }
  114. fullName := strings.Join(parts, "_")
  115. // Add the metric to the list.
  116. c.metrics = append(c.metrics, metric{
  117. subsystem: args["Subsystem"],
  118. name: fullName,
  119. help: args["Help"],
  120. kind: kind,
  121. })
  122. }
  123. }
  124. }
  125. return true
  126. }
  127. func (c *metricCollector) print() {
  128. slices.SortFunc(c.metrics, func(a, b metric) bool {
  129. if a.subsystem != b.subsystem {
  130. return a.subsystem < b.subsystem
  131. }
  132. return a.name < b.name
  133. })
  134. var prevSubsystem string
  135. for _, m := range c.metrics {
  136. if m.subsystem != prevSubsystem {
  137. fmt.Printf("## Package `%s`\n\n", m.subsystem)
  138. prevSubsystem = m.subsystem
  139. }
  140. fmt.Printf("### `%v` (%s)\n\n%s\n\n", m.name, m.kind, wordwrap(sentenceize(m.help), 72))
  141. }
  142. }
  143. func sentenceize(s string) string {
  144. if s == "" {
  145. return ""
  146. }
  147. if !strings.HasSuffix(s, ".") {
  148. return s + "."
  149. }
  150. return s
  151. }
  152. func wordwrap(s string, width int) string {
  153. var lines []string
  154. for _, line := range strings.Split(s, "\n") {
  155. for len(line) > width {
  156. i := strings.LastIndex(line[:width], " ")
  157. if i == -1 {
  158. i = width
  159. }
  160. lines = append(lines, line[:i])
  161. line = line[i+1:]
  162. }
  163. lines = append(lines, line)
  164. }
  165. return strings.Join(lines, "\n")
  166. }