Commit e9e5ca6d authored by Matthew Sykes's avatar Matthew Sykes
Browse files

[FAB-13025] generate rst metric tables



Use option definitions in the source tree to generate restructured text
tables to be included in doc.

Change-Id: I0243581ce10875cf1bbebe2b3e6765f2b91150e6
Signed-off-by: default avatarMatthew Sykes <sykesmat@us.ibm.com>
parent f91e87c9
......@@ -849,17 +849,20 @@
version = "v0.3.0"
[[projects]]
digest = "1:99677abdd7b156ae0ab7f0eba2f7b9257214e9959dd506d5daa423cdafb470fb"
digest = "1:0a25e2d158566f94027c0eeb72e1d34b0cfba5b8b51dc7c5b1bb04c2d437898b"
name = "golang.org/x/tools"
packages = [
"cmd/goimports",
"go/ast/astutil",
"go/gcexportdata",
"go/internal/cgo",
"go/internal/gcimporter",
"go/packages",
"go/types/typeutil",
"imports",
"internal/fastwalk",
"internal/gopathwalk",
"internal/semver",
]
pruneopts = "NUT"
revision = "f60e5f99f0816fc2d9ecb338008ea420248d2943"
......@@ -1002,6 +1005,7 @@
"golang.org/x/net/context",
"golang.org/x/sync/semaphore",
"golang.org/x/tools/cmd/goimports",
"golang.org/x/tools/go/packages",
"google.golang.org/grpc",
"google.golang.org/grpc/balancer",
"google.golang.org/grpc/balancer/roundrobin",
......
......@@ -115,7 +115,7 @@ all: native docker checks
checks: basic-checks unit-test integration-test
basic-checks: license spelling trailing-spaces linter
basic-checks: license spelling trailing-spaces linter check-metrics-doc
desk-check: checks verify
......@@ -212,6 +212,14 @@ check-deps: buildenv
@echo "DEP: Checking for dependency issues.."
@$(DRUN) $(DOCKER_NS)/fabric-buildenv:$(DOCKER_TAG) ./scripts/check_deps.sh
check-metrics-doc: buildenv
@echo "METRICS: Checking for outdated reference documentation.."
@$(DRUN) $(DOCKER_NS)/fabric-buildenv:$(DOCKER_TAG) ./scripts/metrics_doc.sh check
generate-metrics-doc: buildenv
@echo "Generating metrics reference documentation..."
@$(DRUN) $(DOCKER_NS)/fabric-buildenv:$(DOCKER_TAG) ./scripts/metrics_doc.sh generate
$(BUILD_DIR)/%/chaintool: Makefile
@echo "Installing chaintool"
@mkdir -p $(@D)
......
#SPDX-License-Identifier: Apache-2.0
gendoc
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package main
import (
"bytes"
"flag"
"io/ioutil"
"os"
"text/template"
"github.com/hyperledger/fabric/common/metrics/gendoc"
"golang.org/x/tools/go/packages"
)
// Gendoc can be used used to discover the metrics options declared at the
// package level in the fabric tree and output a table that can be used in the
// documentation.
var templatePath = flag.String(
"template",
"docs/source/metrics_reference.rst.tmpl",
"The documentation template.",
)
func main() {
flag.Parse()
patterns := flag.Args()
if len(patterns) == 0 {
patterns = []string{"github.com/hyperledger/fabric/..."}
}
pkgs, err := packages.Load(&packages.Config{Mode: packages.LoadSyntax}, patterns...)
if err != nil {
panic(err)
}
options, err := gendoc.Options(pkgs)
if err != nil {
panic(err)
}
cells, err := gendoc.NewCells(options)
if err != nil {
panic(err)
}
funcMap := template.FuncMap{
"PrometheusTable": func() string {
buf := &bytes.Buffer{}
gendoc.NewPrometheusTable(cells).Generate(buf)
return buf.String()
},
"StatsdTable": func() string {
buf := &bytes.Buffer{}
gendoc.NewStatsdTable(cells).Generate(buf)
return buf.String()
},
}
docTemplate, err := ioutil.ReadFile(*templatePath)
if err != nil {
panic(err)
}
tmpl, err := template.New("metrics_reference").Funcs(funcMap).Parse(string(docTemplate))
if err != nil {
panic(err)
}
if err := tmpl.Execute(os.Stdout, ""); err != nil {
panic(err)
}
}
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package gendoc_test
import (
"go/ast"
"go/parser"
"go/token"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestGendoc(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Gendoc Suite")
}
func ParseFile(filename string) (*ast.File, error) {
fs := token.NewFileSet()
f, err := parser.ParseFile(fs, filename, nil, parser.ParseComments)
if err != nil {
return nil, err
}
return f, nil
}
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package gendoc
import (
"fmt"
"go/ast"
"path"
"reflect"
"strconv"
"strings"
"github.com/hyperledger/fabric/common/metrics"
"golang.org/x/tools/go/packages"
)
// Options scans the provided list of packages for options structs used when
// creating metrics and returns instances that are recreated from the source
// tree.
func Options(pkgs []*packages.Package) ([]interface{}, error) {
var options []interface{}
for _, p := range pkgs {
for _, f := range p.Syntax {
opts, err := FileOptions(f)
if err != nil {
return nil, err
}
options = append(options, opts...)
}
}
return options, nil
}
// FileOptions walks the specified ast.File for options structs used when
// creating metrics and returns instances that are recreated from the source.
func FileOptions(f *ast.File) ([]interface{}, error) {
var imports = walkImports(f)
var options []interface{}
var errors []error
// If the file contains gendoc:ignore, ignore the file
for _, c := range f.Comments {
if strings.Index(c.Text(), "gendoc:ignore") != -1 {
return nil, nil
}
}
// Iterate over declarations
for i := range f.Decls {
ast.Inspect(f.Decls[i], func(x ast.Node) bool {
node, ok := x.(*ast.ValueSpec)
if !ok {
return true
}
for _, v := range node.Values {
value, ok := v.(*ast.CompositeLit)
if !ok {
continue
}
literalType, ok := value.Type.(*ast.SelectorExpr)
if !ok {
continue
}
ident, ok := literalType.X.(*ast.Ident)
if !ok {
continue
}
if imports[ident.Name] != "github.com/hyperledger/fabric/common/metrics" {
continue
}
option, err := createOption(literalType)
if err != nil {
errors = append(errors, err)
break
}
option, err = populateOption(value, option)
if err != nil {
errors = append(errors, err)
break
}
options = append(options, option)
}
return false
})
}
if len(errors) != 0 {
return nil, errors[0]
}
return options, nil
}
func walkImports(f *ast.File) map[string]string {
imports := map[string]string{}
for i := range f.Imports {
ast.Inspect(f.Imports[i], func(x ast.Node) bool {
switch node := x.(type) {
case *ast.ImportSpec:
importPath, err := strconv.Unquote(node.Path.Value)
if err != nil {
panic(err)
}
importName := path.Base(importPath)
if node.Name != nil {
importName = node.Name.Name
}
imports[importName] = importPath
return false
default:
return true
}
})
}
return imports
}
func createOption(lit *ast.SelectorExpr) (interface{}, error) {
optionName := lit.Sel.Name
switch optionName {
case "CounterOpts":
return &metrics.CounterOpts{}, nil
case "GaugeOpts":
return &metrics.GaugeOpts{}, nil
case "HistogramOpts":
return &metrics.HistogramOpts{}, nil
default:
return nil, fmt.Errorf("unknown object type: %s", optionName)
}
}
func populateOption(lit *ast.CompositeLit, target interface{}) (interface{}, error) {
val := reflect.ValueOf(target).Elem()
for _, elem := range lit.Elts {
if kv, ok := elem.(*ast.KeyValueExpr); ok {
name := kv.Key.(*ast.Ident).Name
field := val.FieldByName(name)
switch name {
// ignored
case "Buckets":
// slice of strings
case "LabelNames":
labelNames, err := stringSlice(kv.Value.(*ast.CompositeLit))
if err != nil {
return nil, err
}
labelNamesValue := reflect.ValueOf(labelNames)
field.Set(labelNamesValue)
// simple strings
case "Namespace", "Subsystem", "Name", "Help", "StatsdFormat":
basicVal := kv.Value.(*ast.BasicLit)
val, err := strconv.Unquote(basicVal.Value)
if err != nil {
return nil, err
}
field.SetString(val)
default:
return nil, fmt.Errorf("unknown field name: %s", name)
}
}
}
return val.Interface(), nil
}
func stringSlice(lit *ast.CompositeLit) ([]string, error) {
var slice []string
for _, elem := range lit.Elts {
val, err := strconv.Unquote(elem.(*ast.BasicLit).Value)
if err != nil {
return nil, err
}
slice = append(slice, val)
}
return slice, nil
}
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package gendoc_test
import (
"github.com/hyperledger/fabric/common/metrics/gendoc"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Options", func() {
It("finds standard options", func() {
f, err := ParseFile("testdata/basic.go")
Expect(err).NotTo(HaveOccurred())
Expect(f).NotTo(BeNil())
options, err := gendoc.FileOptions(f)
Expect(err).NotTo(HaveOccurred())
Expect(options).To(HaveLen(3))
})
It("finds options that use named imports", func() {
f, err := ParseFile("testdata/named_import.go")
Expect(err).NotTo(HaveOccurred())
Expect(f).NotTo(BeNil())
options, err := gendoc.FileOptions(f)
Expect(err).NotTo(HaveOccurred())
Expect(options).To(HaveLen(3))
})
It("ignores variables that are tagged", func() {
f, err := ParseFile("testdata/ignored.go")
Expect(err).NotTo(HaveOccurred())
Expect(f).NotTo(BeNil())
options, err := gendoc.FileOptions(f)
Expect(err).NotTo(HaveOccurred())
Expect(options).To(BeEmpty())
})
})
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package gendoc
import (
"fmt"
"io"
"sort"
"strings"
"github.com/hyperledger/fabric/common/metrics"
"github.com/hyperledger/fabric/common/metrics/internal/namer"
)
// A Field represents data that is included in the reference table for metrics.
type Field uint8
const (
Name Field = iota // Name is the meter name.
Type // Type is the type of meter option.
Description // Description is the help text from the meter option.
Labels // Labels is the meter's label information.
Bucket // Bucket is the statsd bucket format
)
// A Column represents a column of data in the reference table.
type Column struct {
Field Field
Name string
Width int
}
// NewPrometheusTable creates a table that can be used to document Prometheus
// metrics maintained by Fabric.
func NewPrometheusTable(cells Cells) Table {
return Table{
Cells: cells,
Columns: []Column{
{Field: Name, Name: "Name", Width: max(cells.MaxLen(Name)+2, 20)},
{Field: Type, Name: "Type", Width: 11},
{Field: Description, Name: "Description", Width: 60},
{Field: Labels, Name: "Labels", Width: 20},
},
}
}
// NewStatsdTable creates a table that can be used to document StatsD metrics
// maintained by Fabric.
func NewStatsdTable(cells Cells) Table {
return Table{
Cells: cells,
Columns: []Column{
{Field: Bucket, Name: "Bucket", Width: max(cells.MaxLen(Bucket)+2, 20)},
{Field: Type, Name: "Type", Width: 11},
{Field: Description, Name: "Description", Width: 60},
},
}
}
// A Table maintains the cells and columns used to generate the restructured text
// formatted reference documentation.
type Table struct {
Columns []Column
Cells Cells
}
// Generate generates a restructured text formatted table from the cells and
// columns contained in the table.
func (t Table) Generate(w io.Writer) {
fmt.Fprint(w, t.header())
for _, c := range t.Cells {
fmt.Fprint(w, t.formatCell(c))
fmt.Fprint(w, t.rowSeparator())
}
}
func (t Table) rowSeparator() string { return t.separator("-") }
func (t Table) headerSeparator() string { return t.separator("=") }
func (t Table) separator(delim string) string {
var s string
for _, c := range t.Columns {
s += "+" + strings.Repeat(delim, c.Width)
}
return s + "+\n"
}
func (t Table) header() string {
var h string
h += t.rowSeparator()
for _, c := range t.Columns {
h += "| " + printWidth(c.Name, c.Width-2) + " "
}
h += "|\n"
h += t.headerSeparator()
return h
}
func (t Table) formatCell(cell Cell) string {
contents := map[Field][]string{}
lineCount := 0
// wrap lines
for _, c := range t.Columns {
lines := wrapWidths(cell.Field(c.Field), c.Width-2)
if l := len(lines); l > lineCount {
lineCount = l
}
contents[c.Field] = lines
}
// add extra lines
for _, col := range t.Columns {
lines := contents[col.Field]
contents[col.Field] = padLines(lines, col.Width-2, lineCount)
}
var c string
for i := 0; i < lineCount; i++ {
for _, col := range t.Columns {
c += "| " + contents[col.Field][i] + " "
}
c += "|\n"
}
return c
}
func wrapWidths(s string, width int) []string {
var result []string
for _, s := range strings.Split(s, "\n") {
result = append(result, wrapWidth(s, width)...)
}
return result
}
func wrapWidth(s string, width int) []string {
words := strings.Fields(strings.TrimSpace(s))
if len(words) == 0 { // only white space
return []string{s}
}
result := words[0]
remaining := width - len(words[0])
for _, w := range words[1:] {
if len(w)+1 > remaining {
result += "\n" + w
remaining = width - len(w) - 1
} else {
result += " " + w
remaining -= len(w) + 1
}
}
return strings.Split(result, "\n")
}
func padLines(lines []string, w, h int) []string {
for len(lines) < h {
lines = append(lines, "")
}
for idx, line := range lines {
lines[idx] = printWidth(line, w)
}
return lines
}
func printWidth(s string, w int) string {
s = strings.TrimSpace(s)
if len(s) < w {
return s + strings.Repeat(" ", w-len(s))
}
return s
}
func max(x, y int) int {
if x > y {
return x
}
return y
}
type Cell struct {
meterType string
namer *namer.Namer
description string
labels []string
}
func (c Cell) Field(f Field) string {
switch f {
case Name:
return c.Name()
case Type:
return c.Type()
case Description:
return c.Description()
case Labels:
return c.Labels<