mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-12-02 12:32:04 +00:00
284 lines
7.5 KiB
Go
284 lines
7.5 KiB
Go
package terminal
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"github.com/ClickHouse/clickhouse-diagnostics/internal/outputs"
|
|
"github.com/ClickHouse/clickhouse-diagnostics/internal/platform/config"
|
|
"github.com/ClickHouse/clickhouse-diagnostics/internal/platform/data"
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/pkg/errors"
|
|
"os"
|
|
)
|
|
|
|
const OutputName = "report"
|
|
|
|
type ReportOutput struct {
|
|
}
|
|
|
|
func (r ReportOutput) Write(id string, bundles map[string]*data.DiagnosticBundle, conf config.Configuration) (data.FrameErrors, error) {
|
|
conf, err := conf.ValidateConfig(r.Configuration())
|
|
if err != nil {
|
|
return data.FrameErrors{}, err
|
|
}
|
|
format, err := config.ReadStringOptionsValue(conf, "format")
|
|
if err != nil {
|
|
return data.FrameErrors{}, err
|
|
}
|
|
nonInteractive, err := config.ReadBoolValue(conf, "continue")
|
|
if err != nil {
|
|
return data.FrameErrors{}, err
|
|
}
|
|
maxRows, err := config.ReadIntValue(conf, "row_limit")
|
|
if err != nil {
|
|
return data.FrameErrors{}, err
|
|
}
|
|
maxColumns, err := config.ReadIntValue(conf, "column_limit")
|
|
if err != nil {
|
|
return data.FrameErrors{}, err
|
|
}
|
|
frameErrors := data.FrameErrors{}
|
|
for name := range bundles {
|
|
frameError := printDiagnosticBundle(name, bundles[name], format, !nonInteractive, int(maxRows), int(maxColumns))
|
|
frameErrors.Errors = append(frameErrors.Errors, frameError.Errors...)
|
|
}
|
|
return data.FrameErrors{}, nil
|
|
}
|
|
|
|
func printDiagnosticBundle(name string, diag *data.DiagnosticBundle, format string, interactive bool, maxRows, maxColumns int) data.FrameErrors {
|
|
frameErrors := data.FrameErrors{}
|
|
for frameId, frame := range diag.Frames {
|
|
printFrameHeader(fmt.Sprintf("%s.%s", name, frameId))
|
|
err := printFrame(frame, format, maxRows, maxColumns)
|
|
if err != nil {
|
|
frameErrors.Errors = append(frameErrors.Errors, err)
|
|
}
|
|
if interactive {
|
|
err := waitForEnter()
|
|
if err != nil {
|
|
frameErrors.Errors = append(frameErrors.Errors, err)
|
|
}
|
|
}
|
|
}
|
|
return frameErrors
|
|
}
|
|
|
|
func waitForEnter() error {
|
|
fmt.Println("Press the Enter Key to view the next frame report")
|
|
for {
|
|
consoleReader := bufio.NewReaderSize(os.Stdin, 1)
|
|
input, err := consoleReader.ReadByte()
|
|
if err != nil {
|
|
return errors.New("Unable to read user input")
|
|
}
|
|
if input == 3 {
|
|
//ctl +c
|
|
fmt.Println("Exiting...")
|
|
os.Exit(0)
|
|
}
|
|
if input == 10 {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func printFrame(frame data.Frame, format string, maxRows, maxColumns int) error {
|
|
switch f := frame.(type) {
|
|
case data.DatabaseFrame:
|
|
return printDatabaseFrame(f, format, maxRows, maxColumns)
|
|
case data.ConfigFileFrame:
|
|
return printConfigFrame(f, format)
|
|
case data.DirectoryFileFrame:
|
|
return printDirectoryFileFrame(f, format, maxRows)
|
|
case data.HierarchicalFrame:
|
|
return printHierarchicalFrame(f, format, maxRows, maxColumns)
|
|
default:
|
|
// for now our data frame writer supports all frames
|
|
return printDatabaseFrame(f, format, maxRows, maxColumns)
|
|
}
|
|
}
|
|
|
|
func createTable(format string) *tablewriter.Table {
|
|
table := tablewriter.NewWriter(os.Stdout)
|
|
if format == "markdown" {
|
|
table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
|
|
table.SetCenterSeparator("|")
|
|
}
|
|
return table
|
|
}
|
|
|
|
func printFrameHeader(title string) {
|
|
titleTable := tablewriter.NewWriter(os.Stdout)
|
|
titleTable.SetHeader([]string{title})
|
|
titleTable.SetAutoWrapText(false)
|
|
titleTable.SetAutoFormatHeaders(true)
|
|
titleTable.SetHeaderAlignment(tablewriter.ALIGN_CENTER)
|
|
titleTable.SetRowSeparator("\n")
|
|
titleTable.SetHeaderLine(false)
|
|
titleTable.SetBorder(false)
|
|
titleTable.SetTablePadding("\t") // pad with tabs
|
|
titleTable.SetNoWhiteSpace(true)
|
|
titleTable.Render()
|
|
}
|
|
|
|
func printHierarchicalFrame(frame data.HierarchicalFrame, format string, maxRows, maxColumns int) error {
|
|
err := printDatabaseFrame(frame, format, maxRows, maxColumns)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, subFrame := range frame.SubFrames {
|
|
err = printHierarchicalFrame(subFrame, format, maxRows, maxColumns)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func printDatabaseFrame(frame data.Frame, format string, maxRows, maxColumns int) error {
|
|
table := createTable(format)
|
|
table.SetAutoWrapText(false)
|
|
columns := len(frame.Columns())
|
|
if maxColumns > 0 && maxColumns < columns {
|
|
columns = maxColumns
|
|
}
|
|
table.SetHeader(frame.Columns()[:columns])
|
|
r := 0
|
|
trunColumns := 0
|
|
for {
|
|
values, ok, err := frame.Next()
|
|
if !ok || r == maxRows {
|
|
table.Render()
|
|
if trunColumns > 0 {
|
|
warning(fmt.Sprintf("Truncated %d columns, more available...", trunColumns))
|
|
}
|
|
if r == maxRows {
|
|
warning("Truncated rows, more available...")
|
|
}
|
|
return err
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
columns := len(values)
|
|
// -1 means unlimited
|
|
if maxColumns > 0 && maxColumns < columns {
|
|
trunColumns = columns - maxColumns
|
|
columns = maxColumns
|
|
}
|
|
row := make([]string, columns)
|
|
for i, value := range values {
|
|
if i == columns {
|
|
break
|
|
}
|
|
row[i] = fmt.Sprintf("%v", value)
|
|
}
|
|
table.Append(row)
|
|
r++
|
|
}
|
|
}
|
|
|
|
// currently we dump the whole config - useless in parts
|
|
func printConfigFrame(frame data.Frame, format string) error {
|
|
for {
|
|
values, ok, err := frame.Next()
|
|
if !ok {
|
|
return err
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
configFile := values[0].(data.File)
|
|
dat, err := os.ReadFile(configFile.FilePath())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// create a table per row - as each will be a file
|
|
table := createTable(format)
|
|
table.SetAutoWrapText(false)
|
|
table.SetAutoFormatHeaders(false)
|
|
table.ClearRows()
|
|
table.SetHeader([]string{configFile.FilePath()})
|
|
table.Append([]string{string(dat)})
|
|
table.Render()
|
|
}
|
|
}
|
|
|
|
func printDirectoryFileFrame(frame data.Frame, format string, maxRows int) error {
|
|
for {
|
|
values, ok, err := frame.Next()
|
|
if !ok {
|
|
|
|
return err
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path := values[0].(data.SimpleFile)
|
|
file, err := os.Open(path.FilePath())
|
|
if err != nil {
|
|
// failure on one file causes rest to be ignored in frame...we could improve this
|
|
return errors.Wrapf(err, "Unable to read file %s", path.FilePath())
|
|
}
|
|
scanner := bufio.NewScanner(file)
|
|
i := 0
|
|
// create a table per row - as each will be a file
|
|
table := createTable(format)
|
|
table.SetAutoWrapText(false)
|
|
table.SetAutoFormatHeaders(false)
|
|
table.ClearRows()
|
|
table.SetHeader([]string{path.FilePath()})
|
|
for scanner.Scan() {
|
|
if i == maxRows {
|
|
fmt.Println()
|
|
table.Render()
|
|
warning("Truncated lines, more available...")
|
|
fmt.Print("\n")
|
|
break
|
|
}
|
|
table.Append([]string{scanner.Text()})
|
|
i++
|
|
}
|
|
}
|
|
}
|
|
|
|
// prints a warning
|
|
func warning(s string) {
|
|
fmt.Printf("\x1b[%dm%v\x1b[0m%s\n", 33, "WARNING: ", s)
|
|
}
|
|
|
|
func (r ReportOutput) Configuration() config.Configuration {
|
|
return config.Configuration{
|
|
Params: []config.ConfigParam{
|
|
config.StringOptions{
|
|
Value: "default",
|
|
Options: []string{"default", "markdown"},
|
|
Param: config.NewParam("format", "Format of tables. Default is terminal friendly.", false),
|
|
},
|
|
config.BoolParam{
|
|
Value: false,
|
|
Param: config.NewParam("continue", "Print report with no interaction", false),
|
|
},
|
|
config.IntParam{
|
|
Value: 10,
|
|
Param: config.NewParam("row_limit", "Max Rows to print per frame.", false),
|
|
},
|
|
config.IntParam{
|
|
Value: 8,
|
|
Param: config.NewParam("column_limit", "Max Columns to print per frame. Negative is unlimited.", false),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (r ReportOutput) Description() string {
|
|
return "Writes out the diagnostic bundle to the terminal as a simple report."
|
|
}
|
|
|
|
// here we register the output for use
|
|
func init() {
|
|
outputs.Register(OutputName, func() (outputs.Output, error) {
|
|
return ReportOutput{}, nil
|
|
})
|
|
}
|