Skip to content

Commit

Permalink
Add utils package with pretty table formatting. Eliminate external de…
Browse files Browse the repository at this point in the history
…pendency
  • Loading branch information
IsThisEvenCode committed Jun 7, 2024
1 parent 2f117e1 commit 02abb15
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 13 deletions.
4 changes: 2 additions & 2 deletions cmd/gearman_top/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package main
import (
"flag"
"fmt"
"os"
"github.com/consol-monitoring/mod-gearman-worker-go/pkg/modgearman"
"os"
)

// Build contains the current git commit id
Expand All @@ -13,7 +13,7 @@ var Build string

func main() {
args := modgearman.Args{}
// Define a new FlagSet for avoiding collions with other flags
// Define a new FlagSet for avoiding collisions with other flags
var fs = flag.NewFlagSet("gearman_top", flag.ExitOnError)

fs.BoolVar(&args.Usage, "h", false, "Print usage")
Expand Down
35 changes: 24 additions & 11 deletions pkg/modgearman/gearman_top.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"sync"
"time"

"github.com/consol-monitoring/snclient/pkg/utils"
"github.com/consol-monitoring/mod-gearman-worker-go/pkg/utils"
"github.com/nsf/termbox-go"
)

Expand Down Expand Up @@ -102,7 +102,7 @@ func runInteractiveMode(args *Args, hostList []string, connectionMap map[string]
printMap[host] = fmt.Sprintf("---- %s ----\nNot data yet...\n\n\n", host)
}

// Execute getStats() for all hosts in parallel in order to prevent a program block
// Get and print stats for all hosts in parallel in order to prevent a program block
// when a connection to a host runs into a timeout
for _, host := range hostList {
go func(host string) {
Expand Down Expand Up @@ -253,29 +253,42 @@ func createTable(queueList []queue) (string, error) {
if err != nil {
return "", err
}

tableSize := calcTableSize(tableHeaders)
tableHorizontalBorder := strings.Repeat("-", tableSize+1) // Add one for an additional pipe symbol at the end of each row
table = fmt.Sprintf("%s\n%s%s\n\n", tableHorizontalBorder, table, tableHorizontalBorder)
return table, nil
}

func calcTableSize(tableHeaders []utils.ASCIITableHeader) int {
tableSize := 0
for _, header := range tableHeaders {
tableSize += header.Size
tableSize += 3
}
return tableSize
}

func createTableHeaders() []utils.ASCIITableHeader {
tableHeaders := []utils.ASCIITableHeader{
{
Name: "Queue Name",
Field: "queueName",
},
{
Name: "Worker Available",
Field: "workerAvailable",
RightAligned: true,
Name: "Worker Available",
Field: "workerAvailable",
Alignment: "right",
},
{
Name: "Jobs Waiting",
Field: "jobsWaiting",
RightAligned: true,
Name: "Jobs Waiting",
Field: "jobsWaiting",
Alignment: "right",
},
{
Name: "Jobs running",
Field: "jobsRunning",
RightAligned: true,
Name: "Jobs running",
Field: "jobsRunning",
Alignment: "right",
},
}
return tableHeaders
Expand Down
140 changes: 140 additions & 0 deletions pkg/utils/asciiTable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package utils

import (
"fmt"
"reflect"
"strings"
)

const maxLineLength = 120

type ASCIITableHeader struct {
Name string // name in table header
Field string // attribute name in data row
Alignment string // flag whether column is aligned to the right
Size int // calculated max Size of column

}

// ASCIITable creates an ascii table from columns and data rows
func ASCIITable(header []ASCIITableHeader, rows interface{}, escapePipes bool) (string, error) {
dataRows := reflect.ValueOf(rows)
if dataRows.Kind() != reflect.Slice {
return "", fmt.Errorf("rows is not a slice")
}

err := calculateHeaderSize(header, dataRows, escapePipes)
if err != nil {
return "", err
}

// output header
out := ""
for _, head := range header {
out += fmt.Sprintf(fmt.Sprintf("| %%-%ds ", head.Size), head.Name)
}
out += "|\n"

// output separator
for _, head := range header {
padding := " "
out += fmt.Sprintf("|%s%s%s", padding, strings.Repeat("-", head.Size), padding)
}
out += "|\n"

// output data
for i := range dataRows.Len() {
rowVal := dataRows.Index(i)
for _, head := range header {
value, _ := asciiTableRowValue(escapePipes, rowVal, head)
if head.Alignment == "right" {
out += fmt.Sprintf(fmt.Sprintf("| %%%ds ", head.Size), value)
} else if head.Alignment == "left" || head.Alignment == "" {
out += fmt.Sprintf(fmt.Sprintf("| %%-%ds ", head.Size), value)
} else if head.Alignment == "centered" {
padding := (head.Size - len(value)) / 2
out += fmt.Sprintf("| %*s%-*s ", padding, "", head.Size-padding, value)
} else {
err := fmt.Errorf("unsupported alignment '%s' in table", head.Alignment)
return "", err
}
}
out += "|\n"
}

return out, nil
}

func asciiTableRowValue(escape bool, rowVal reflect.Value, head ASCIITableHeader) (string, error) {
value := ""
field := rowVal.FieldByName(head.Field)
if field.IsValid() {
t := field.Type().String()
switch t {
case "string":
value = field.String()
default:
return "", fmt.Errorf("unsupported struct attribute type for field %s: %s", head.Field, t)
}
}

if escape {
value = strings.ReplaceAll(value, "\n", `\n`)
value = strings.ReplaceAll(value, "|", "\\|")
value = strings.ReplaceAll(value, "$", "\\$")
value = strings.ReplaceAll(value, "*", "\\*")
}

return value, nil
}

func calculateHeaderSize(header []ASCIITableHeader, dataRows reflect.Value, escapePipes bool) error {
// set headers as minimum Size
for i, head := range header {
header[i].Size = len(head.Name)
}

// adjust column Size from max row data
for i := range dataRows.Len() {
rowVal := dataRows.Index(i)
if rowVal.Kind() != reflect.Struct {
return fmt.Errorf("row %d is not a struct", i)
}
for num, head := range header {
value, err := asciiTableRowValue(escapePipes, rowVal, head)
if err != nil {
return err
}
length := len(value)
if length > header[num].Size {
header[num].Size = length
}
}
}

// calculate total line length
total := 0
for i := range header {
total += header[i].Size + 3 // add padding
}

if total < maxLineLength {
return nil
}

avgAvail := maxLineLength / len(header)
tooWide := []int{}
sumTooWide := 0
for i := range header {
if header[i].Size > avgAvail {
tooWide = append(tooWide, i)
sumTooWide += header[i].Size
}
}
avgLargeCol := (maxLineLength - (total - sumTooWide)) / len(tooWide)
for _, i := range tooWide {
header[i].Size = avgLargeCol
}

return nil
}

0 comments on commit 02abb15

Please sign in to comment.