373 lines
8.7 KiB
Go
373 lines
8.7 KiB
Go
/*
|
|
Copyright IBM Corp. All Rights Reserved.
|
|
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package gendoc
|
|
|
|
import (
|
|
"bytes"
|
|
"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
|
|
Split int
|
|
Width int
|
|
}
|
|
|
|
// NewPrometheusTable creates a table that can be used to document Prometheus
|
|
// metrics maintained by Fabric.
|
|
func NewPrometheusTable(cells Cells) Table {
|
|
labelSplit := 0
|
|
for _, cell := range cells {
|
|
for _, label := range cell.labels {
|
|
labelSplit = max(labelSplit, len(label)+2)
|
|
}
|
|
}
|
|
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: 80, Split: labelSplit},
|
|
},
|
|
}
|
|
}
|
|
|
|
// 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("-", false) }
|
|
func (t Table) headerSeparator() string { return t.separator("=", false) }
|
|
|
|
func (t Table) separator(delim string, firstLine bool) string {
|
|
var buf bytes.Buffer
|
|
for _, c := range t.Columns {
|
|
buf.WriteString("+")
|
|
if !firstLine && c.Split != 0 {
|
|
buf.WriteString(strings.Repeat(delim, c.Split))
|
|
buf.WriteString("+")
|
|
buf.WriteString(strings.Repeat(delim, c.Width-c.Split-1))
|
|
} else {
|
|
buf.WriteString(strings.Repeat(delim, c.Width))
|
|
}
|
|
}
|
|
buf.WriteString("+\n")
|
|
return buf.String()
|
|
}
|
|
|
|
func (t Table) header() string {
|
|
var h string
|
|
h += t.separator("-", true)
|
|
for _, c := range t.Columns {
|
|
h += "| " + printWidth(c.Name, c.Width-2, 0) + " "
|
|
}
|
|
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 {
|
|
if c.Split != 0 {
|
|
lines := formSubtableCell(cell.labels, cell.labelHelp, c.Split, c.Width)
|
|
if l := len(lines); l > lineCount {
|
|
lineCount = l
|
|
}
|
|
contents[c.Field] = lines
|
|
} else {
|
|
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, col.Split-1)
|
|
}
|
|
|
|
var c string
|
|
for i := 0; i < lineCount; i++ {
|
|
endSplit := "|"
|
|
endPadding := " "
|
|
for _, col := range t.Columns {
|
|
frontSplit := "| "
|
|
if contents[col.Field][i][0] == '+' {
|
|
frontSplit = ""
|
|
endSplit = ""
|
|
endPadding = ""
|
|
}
|
|
c += frontSplit + contents[col.Field][i] + endPadding
|
|
}
|
|
c += endSplit + "\n"
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
func formSubtableCell(keys []string, m map[string]string, leftWidth, cellWidth int) []string {
|
|
var result []string
|
|
for i, key := range keys {
|
|
// build subtable separator
|
|
if i != 0 {
|
|
var buf bytes.Buffer
|
|
buf.WriteString("+")
|
|
buf.WriteString(strings.Repeat("-", leftWidth))
|
|
buf.WriteString("+")
|
|
buf.WriteString(strings.Repeat("-", cellWidth-leftWidth-1))
|
|
buf.WriteString("+")
|
|
result = append(result, buf.String())
|
|
}
|
|
|
|
// populate subtable
|
|
rightLines := wrapWidths(m[key], cellWidth-leftWidth-1)
|
|
leftLines := padLines([]string{key}, leftWidth-2, max(1, len(rightLines)), 0)
|
|
for i := 0; i < len(leftLines); i++ {
|
|
text := fmt.Sprintf("%s | %s", leftLines[i], rightLines[i])
|
|
result = append(result, text)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
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, split int) []string {
|
|
for len(lines) < h {
|
|
lines = append(lines, "")
|
|
}
|
|
for idx, line := range lines {
|
|
lines[idx] = printWidth(line, w, split)
|
|
}
|
|
|
|
return lines
|
|
}
|
|
|
|
func printWidth(s string, w, split int) string {
|
|
if len(s) < w {
|
|
var buf bytes.Buffer
|
|
buf.WriteString(s)
|
|
if split <= len(s) {
|
|
buf.WriteString(strings.Repeat(" ", w-len(s)))
|
|
} else {
|
|
buf.WriteString(strings.Repeat(" ", split-len(s)))
|
|
buf.WriteString("|")
|
|
buf.WriteString(strings.Repeat(" ", w-split-1))
|
|
}
|
|
s = buf.String()
|
|
}
|
|
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
|
|
labelHelp map[string]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()
|
|
case Bucket:
|
|
return c.BucketFormat()
|
|
default:
|
|
panic(fmt.Sprintf("unknown field type: %d", f))
|
|
}
|
|
}
|
|
|
|
func (c Cell) Name() string { return strings.Replace(c.namer.FullyQualifiedName(), ".", "_", -1) }
|
|
func (c Cell) Type() string { return c.meterType }
|
|
func (c Cell) Description() string { return c.description }
|
|
|
|
func (c Cell) Labels() string {
|
|
buf := &strings.Builder{}
|
|
for _, label := range c.labels {
|
|
fmt.Fprintf(buf, " | %s\n", label)
|
|
}
|
|
return strings.TrimRight(buf.String(), "\n")
|
|
}
|
|
|
|
func (c Cell) BucketFormat() string {
|
|
var lvs []string
|
|
for _, label := range c.labels {
|
|
lvs = append(lvs, label, asBucketVar(label))
|
|
}
|
|
return c.namer.Format(lvs...)
|
|
}
|
|
|
|
func asBucketVar(s string) string { return "%{" + s + "}" }
|
|
|
|
type Cells []Cell
|
|
|
|
func (c Cells) Len() int { return len(c) }
|
|
func (c Cells) Less(i, j int) bool { return c[i].Name() < c[j].Name() }
|
|
func (c Cells) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
|
|
|
|
func (c Cells) MaxLen(f Field) int {
|
|
var maxlen int
|
|
for _, c := range c {
|
|
if l := len(c.Field(f)); l > maxlen {
|
|
maxlen = l
|
|
}
|
|
}
|
|
return maxlen
|
|
}
|
|
|
|
// NewCells transforms metrics options to cells that can be used for doc
|
|
// generation.
|
|
func NewCells(options []interface{}) (Cells, error) {
|
|
var cells Cells
|
|
for _, o := range options {
|
|
switch m := o.(type) {
|
|
case metrics.CounterOpts:
|
|
cells = append(cells, counterCell(m))
|
|
case metrics.GaugeOpts:
|
|
cells = append(cells, gaugeCell(m))
|
|
case metrics.HistogramOpts:
|
|
cells = append(cells, histogramCell(m))
|
|
default:
|
|
return nil, fmt.Errorf("unknown option type: %t", o)
|
|
}
|
|
}
|
|
sort.Sort(cells)
|
|
return cells, nil
|
|
}
|
|
|
|
func counterCell(c metrics.CounterOpts) Cell {
|
|
if c.StatsdFormat == "" {
|
|
c.StatsdFormat = "%{#fqname}"
|
|
}
|
|
return Cell{
|
|
namer: namer.NewCounterNamer(c),
|
|
meterType: "counter",
|
|
description: c.Help,
|
|
labels: c.LabelNames,
|
|
labelHelp: c.LabelHelp,
|
|
}
|
|
}
|
|
|
|
func gaugeCell(g metrics.GaugeOpts) Cell {
|
|
if g.StatsdFormat == "" {
|
|
g.StatsdFormat = "%{#fqname}"
|
|
}
|
|
return Cell{
|
|
namer: namer.NewGaugeNamer(g),
|
|
meterType: "gauge",
|
|
description: g.Help,
|
|
labels: g.LabelNames,
|
|
labelHelp: g.LabelHelp,
|
|
}
|
|
}
|
|
|
|
func histogramCell(h metrics.HistogramOpts) Cell {
|
|
if h.StatsdFormat == "" {
|
|
h.StatsdFormat = "%{#fqname}"
|
|
}
|
|
return Cell{
|
|
namer: namer.NewHistogramNamer(h),
|
|
meterType: "histogram",
|
|
description: h.Help,
|
|
labels: h.LabelNames,
|
|
labelHelp: h.LabelHelp,
|
|
}
|
|
}
|