diff options
author | Jakob Borg <jakob@kastelo.net> | 2023-08-04 19:57:30 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-08-04 19:57:30 +0200 |
commit | b9c08d3814685bc8c4a322756e042b9852e321b4 (patch) | |
tree | 101808c3cb8a9cfcfae374c600189c3c4d96ac6d /script | |
parent | 58042b3129a8918ca4001b455c3aa12d48c45eb0 (diff) | |
download | syncthing-b9c08d3814685bc8c4a322756e042b9852e321b4.tar.gz syncthing-b9c08d3814685bc8c4a322756e042b9852e321b4.zip |
all: Add Prometheus-style metrics to expose some internal performance counters (fixes #5175) (#9003)
Diffstat (limited to 'script')
-rw-r--r-- | script/find-metrics.go | 187 |
1 files changed, 187 insertions, 0 deletions
diff --git a/script/find-metrics.go b/script/find-metrics.go new file mode 100644 index 000000000..dbb6549b0 --- /dev/null +++ b/script/find-metrics.go @@ -0,0 +1,187 @@ +// Copyright (C) 2023 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +// Usage: go run script/find-metrics.go > metrics.md +// +// This script finds all of the metrics in the Syncthing codebase and prints +// them in Markdown format. It's used to generate the metrics documentation +// for the Syncthing docs. +package main + +import ( + "fmt" + "go/ast" + "go/token" + "log" + "strconv" + "strings" + + "golang.org/x/exp/slices" + "golang.org/x/tools/go/packages" +) + +type metric struct { + subsystem string + name string + help string + kind string +} + +func main() { + opts := &packages.Config{ + Mode: packages.NeedSyntax | packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedImports | packages.NeedDeps, + } + + pkgs, err := packages.Load(opts, "github.com/syncthing/syncthing/...") + if err != nil { + log.Fatalln(err) + } + + var coll metricCollector + for _, pkg := range pkgs { + for _, file := range pkg.Syntax { + ast.Inspect(file, coll.Visit) + } + } + coll.print() +} + +type metricCollector struct { + metrics []metric +} + +func (c *metricCollector) Visit(n ast.Node) bool { + if gen, ok := n.(*ast.GenDecl); ok { + // We're only interested in var declarations (var metricWhatever = + // promauto.NewCounter(...) etc). + if gen.Tok != token.VAR { + return false + } + + for _, spec := range gen.Specs { + // We want to look at the value given to a var (the NewCounter() + // etc call). + if vsp, ok := spec.(*ast.ValueSpec); ok { + // There should be only one value. + if len(vsp.Values) != 1 { + continue + } + + // The value should be a function call. + call, ok := vsp.Values[0].(*ast.CallExpr) + if !ok { + continue + } + + // The call should be a selector expression + // (package.Identifer). + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + continue + } + + // The package selector should be `promauto`. + selID, ok := sel.X.(*ast.Ident) + if !ok || selID.Name != "promauto" { + continue + } + + // The function should be one of the New* functions. + var kind string + switch sel.Sel.Name { + case "NewCounter": + kind = "counter" + case "NewGauge": + kind = "gauge" + case "NewCounterVec": + kind = "counter vector" + case "NewGaugeVec": + kind = "gauge vector" + default: + continue + } + + // The arguments to the function should be a single + // composite (struct literal). Grab all of the fields in the + // declaration into a map so we can easily access them. + args := make(map[string]string) + for _, el := range call.Args[0].(*ast.CompositeLit).Elts { + kv := el.(*ast.KeyValueExpr) + key := kv.Key.(*ast.Ident).Name // e.g., "Name" + val := kv.Value.(*ast.BasicLit).Value // e.g., `"foo"` + args[key], _ = strconv.Unquote(val) + } + + // Build the full name of the metric from the namespace + + // subsystem + name, like Prometheus does. + var parts []string + if v := args["Namespace"]; v != "" { + parts = append(parts, v) + } + if v := args["Subsystem"]; v != "" { + parts = append(parts, v) + } + if v := args["Name"]; v != "" { + parts = append(parts, v) + } + fullName := strings.Join(parts, "_") + + // Add the metric to the list. + c.metrics = append(c.metrics, metric{ + subsystem: args["Subsystem"], + name: fullName, + help: args["Help"], + kind: kind, + }) + } + } + } + return true +} + +func (c *metricCollector) print() { + slices.SortFunc(c.metrics, func(a, b metric) bool { + if a.subsystem != b.subsystem { + return a.subsystem < b.subsystem + } + return a.name < b.name + }) + + var prevSubsystem string + for _, m := range c.metrics { + if m.subsystem != prevSubsystem { + fmt.Printf("## Package `%s`\n\n", m.subsystem) + prevSubsystem = m.subsystem + } + fmt.Printf("### `%v` (%s)\n\n%s\n\n", m.name, m.kind, wordwrap(sentenceize(m.help), 72)) + } +} + +func sentenceize(s string) string { + if s == "" { + return "" + } + if !strings.HasSuffix(s, ".") { + return s + "." + } + return s +} + +func wordwrap(s string, width int) string { + var lines []string + for _, line := range strings.Split(s, "\n") { + for len(line) > width { + i := strings.LastIndex(line[:width], " ") + if i == -1 { + i = width + } + lines = append(lines, line[:i]) + line = line[i+1:] + } + lines = append(lines, line) + } + return strings.Join(lines, "\n") +} |