mirror of
https://github.com/percona/percona-toolkit.git
synced 2025-09-24 13:25:01 +00:00
353 lines
10 KiB
Go
353 lines
10 KiB
Go
package display
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
// regular tabwriter do not work with color, this is a forked versions that ignores color special characters
|
|
"github.com/Ladicle/tabwriter"
|
|
"github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/types"
|
|
"github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/utils"
|
|
)
|
|
|
|
// TimelineCLI print a timeline to the terminal using tabulated format
|
|
// It will print header and footers, and dequeue the timeline chronologically
|
|
func TimelineCLI(timeline types.Timeline, verbosity types.Verbosity) {
|
|
|
|
timeline = removeEmptyColumns(timeline, verbosity)
|
|
|
|
// to hold the current context for each node
|
|
// "keys" is needed, because iterating over a map must give a different order each time
|
|
// a slice keeps its order
|
|
keys, currentContext := initKeysContext(timeline) // currentcontext to follow when important thing changed
|
|
latestContext := timeline.GetLatestUpdatedContextsByNodes() // so that we have fully updated context when we print
|
|
lastContext := map[string]types.LogCtx{} // just to follow when important thing changed
|
|
|
|
w := tabwriter.NewWriter(os.Stdout, 8, 8, 3, ' ', tabwriter.DiscardEmptyColumns)
|
|
defer w.Flush()
|
|
|
|
// header
|
|
fmt.Fprintln(w, headerNodes(keys))
|
|
fmt.Fprintln(w, headerFilePath(keys, currentContext))
|
|
fmt.Fprintln(w, headerIP(keys, latestContext))
|
|
fmt.Fprintln(w, headerName(keys, latestContext))
|
|
fmt.Fprintln(w, headerVersion(keys, latestContext))
|
|
fmt.Fprintln(w, separator(keys))
|
|
|
|
var (
|
|
args []string // stuff to print
|
|
linecount int
|
|
)
|
|
|
|
// as long as there is a next event to print
|
|
for nextNodes := timeline.IterateNode(); len(nextNodes) != 0; nextNodes = timeline.IterateNode() {
|
|
|
|
// Date column
|
|
date := timeline[nextNodes[0]][0].Date
|
|
args = []string{""}
|
|
if date != nil {
|
|
args = []string{date.DisplayTime}
|
|
}
|
|
|
|
displayedValue := 0
|
|
|
|
// node values
|
|
for _, node := range keys {
|
|
|
|
if !utils.SliceContains(nextNodes, node) {
|
|
// if there are no events, having a | is needed for tabwriter
|
|
// A few color can also help highlighting how the node is doing
|
|
ctx := currentContext[node]
|
|
args = append(args, utils.PaintForState("| ", ctx.State()))
|
|
continue
|
|
}
|
|
loginfo := timeline[node][0]
|
|
lastContext[node] = currentContext[node]
|
|
currentContext[node] = loginfo.Ctx
|
|
|
|
timeline.Dequeue(node)
|
|
|
|
msg := loginfo.Msg(latestContext[node])
|
|
if verbosity > loginfo.Verbosity && msg != "" {
|
|
args = append(args, msg)
|
|
displayedValue++
|
|
} else {
|
|
args = append(args, utils.PaintForState("| ", loginfo.Ctx.State()))
|
|
}
|
|
}
|
|
|
|
if sep := transitionSeparator(keys, lastContext, currentContext); sep != "" {
|
|
// reset current context, so that we avoid duplicating transitions
|
|
// lastContext/currentContext is only useful for that anyway
|
|
lastContext = map[string]types.LogCtx{}
|
|
for k, v := range currentContext {
|
|
lastContext[k] = v
|
|
}
|
|
// print transition
|
|
fmt.Fprintln(w, sep)
|
|
}
|
|
|
|
// If line is not filled with default placeholder values
|
|
if displayedValue == 0 {
|
|
continue
|
|
|
|
}
|
|
|
|
// Print tabwriter line
|
|
_, err := fmt.Fprintln(w, strings.Join(args, "\t")+"\t")
|
|
if err != nil {
|
|
log.Println("Failed to write a line", err)
|
|
}
|
|
linecount++
|
|
}
|
|
|
|
// footer
|
|
// only having a header is not fast enough to read when there are too many lines
|
|
if linecount >= 50 {
|
|
fmt.Fprintln(w, separator(keys))
|
|
fmt.Fprintln(w, headerNodes(keys))
|
|
fmt.Fprintln(w, headerFilePath(keys, currentContext))
|
|
fmt.Fprintln(w, headerIP(keys, currentContext))
|
|
fmt.Fprintln(w, headerName(keys, currentContext))
|
|
fmt.Fprintln(w, headerVersion(keys, currentContext))
|
|
}
|
|
|
|
// TODO: where to print conflicts details ?
|
|
}
|
|
|
|
func initKeysContext(timeline types.Timeline) ([]string, map[string]types.LogCtx) {
|
|
currentContext := map[string]types.LogCtx{}
|
|
|
|
// keys will be used to access the timeline map with an ordered manner
|
|
// without this, we would not print on the correct column as the order of a map is guaranteed to be random each time
|
|
keys := make([]string, 0, len(timeline))
|
|
for node := range timeline {
|
|
keys = append(keys, node)
|
|
if len(timeline[node]) > 0 {
|
|
currentContext[node] = timeline[node][0].Ctx
|
|
} else {
|
|
// Avoid crashing, but not ideal: we could have a better default Ctx with filepath at least
|
|
currentContext[node] = types.NewLogCtx()
|
|
}
|
|
}
|
|
sort.Strings(keys)
|
|
return keys, currentContext
|
|
}
|
|
|
|
func separator(keys []string) string {
|
|
return " \t" + strings.Repeat(" \t", len(keys))
|
|
}
|
|
|
|
func headerNodes(keys []string) string {
|
|
return "identifier\t" + strings.Join(keys, "\t") + "\t"
|
|
}
|
|
|
|
func headerFilePath(keys []string, ctxs map[string]types.LogCtx) string {
|
|
header := "current path\t"
|
|
for _, node := range keys {
|
|
if ctx, ok := ctxs[node]; ok {
|
|
if len(ctx.FilePath) < 50 {
|
|
header += ctx.FilePath + "\t"
|
|
} else {
|
|
header += "..." + ctx.FilePath[len(ctx.FilePath)-50:] + "\t"
|
|
}
|
|
} else {
|
|
header += " \t"
|
|
}
|
|
}
|
|
return header
|
|
}
|
|
|
|
func headerIP(keys []string, ctxs map[string]types.LogCtx) string {
|
|
header := "last known ip\t"
|
|
for _, node := range keys {
|
|
if ctx, ok := ctxs[node]; ok && len(ctx.OwnIPs) > 0 {
|
|
header += ctx.OwnIPs[len(ctx.OwnIPs)-1] + "\t"
|
|
} else {
|
|
header += " \t"
|
|
}
|
|
}
|
|
return header
|
|
}
|
|
|
|
func headerVersion(keys []string, ctxs map[string]types.LogCtx) string {
|
|
header := "mysql version\t"
|
|
for _, node := range keys {
|
|
if ctx, ok := ctxs[node]; ok {
|
|
header += ctx.Version + "\t"
|
|
}
|
|
}
|
|
return header
|
|
}
|
|
|
|
func headerName(keys []string, ctxs map[string]types.LogCtx) string {
|
|
header := "last known name\t"
|
|
for _, node := range keys {
|
|
if ctx, ok := ctxs[node]; ok && len(ctx.OwnNames) > 0 {
|
|
header += ctx.OwnNames[len(ctx.OwnNames)-1] + "\t"
|
|
} else {
|
|
header += " \t"
|
|
}
|
|
}
|
|
return header
|
|
}
|
|
|
|
func removeEmptyColumns(timeline types.Timeline, verbosity types.Verbosity) types.Timeline {
|
|
|
|
for key := range timeline {
|
|
if !timeline[key][len(timeline[key])-1].Ctx.HasVisibleEvents(verbosity) {
|
|
delete(timeline, key)
|
|
}
|
|
}
|
|
return timeline
|
|
}
|
|
|
|
// transition is to builds the check+display of an important context transition
|
|
// like files, IP, name, anything
|
|
// summary will hold the whole multi-line report
|
|
type transition struct {
|
|
s1, s2, changeType string
|
|
ok bool
|
|
summary transitionSummary
|
|
}
|
|
|
|
// transitions will hold any number of transition to test
|
|
// transitionToPrint will hold whatever transition happened, but will also store empty transitions
|
|
// to ensure that every columns will have the same amount of rows to write: this is needed to maintain
|
|
// the columnar output
|
|
type transitions struct {
|
|
tests []*transition
|
|
transitionToPrint []*transition
|
|
numberFound int
|
|
}
|
|
|
|
// 4 here means there are 4 rows to store
|
|
// 0: base info, 1: type of info that changed, 2: just an arrow placeholder, 3: new info
|
|
const RowPerTransitions = 4
|
|
|
|
type transitionSummary [RowPerTransitions]string
|
|
|
|
// because only those transitions are implemented: file path, ip, node name, version
|
|
const NumberOfPossibleTransition = 4
|
|
|
|
// transactionSeparator is useful to highligh a change of context
|
|
// example, changing file
|
|
// mysqld.log.2
|
|
// (file path)
|
|
// V
|
|
// mysqld.log.1
|
|
// or a change of ip, node name, ...
|
|
// This feels complicated: it is
|
|
// It was made difficult because of how "tabwriter" works
|
|
// it needs an element on each columns so that we don't break columns
|
|
// The rows can't have a variable count of elements: it has to be strictly identical each time
|
|
// so the whole next functions are here to ensure it takes minimal spaces, while giving context and preserving columns
|
|
func transitionSeparator(keys []string, oldctxs, ctxs map[string]types.LogCtx) string {
|
|
|
|
ts := map[string]*transitions{}
|
|
|
|
// For each columns to print, we build tests
|
|
for _, node := range keys {
|
|
ctx, ok1 := ctxs[node]
|
|
oldctx, ok2 := oldctxs[node]
|
|
|
|
ts[node] = &transitions{tests: []*transition{}}
|
|
if ok1 && ok2 {
|
|
ts[node].tests = append(ts[node].tests, &transition{s1: oldctx.FilePath, s2: ctx.FilePath, changeType: "file path"})
|
|
|
|
if len(oldctx.OwnNames) > 0 && len(ctx.OwnNames) > 0 {
|
|
ts[node].tests = append(ts[node].tests, &transition{s1: oldctx.OwnNames[len(oldctx.OwnNames)-1], s2: ctx.OwnNames[len(ctx.OwnNames)-1], changeType: "node name"})
|
|
}
|
|
if len(oldctx.OwnIPs) > 0 && len(ctx.OwnIPs) > 0 {
|
|
ts[node].tests = append(ts[node].tests, &transition{s1: oldctx.OwnIPs[len(oldctx.OwnIPs)-1], s2: ctx.OwnIPs[len(ctx.OwnIPs)-1], changeType: "node ip"})
|
|
}
|
|
if oldctx.Version != "" && ctx.Version != "" {
|
|
ts[node].tests = append(ts[node].tests, &transition{s1: oldctx.Version, s2: ctx.Version, changeType: "version"})
|
|
}
|
|
|
|
}
|
|
|
|
// we resolve tests
|
|
ts[node].fillEmptyTransition()
|
|
ts[node].iterate()
|
|
}
|
|
|
|
highestStackOfTransitions := 0
|
|
|
|
// we need to know the maximum height to print
|
|
for _, node := range keys {
|
|
if ts[node].numberFound > highestStackOfTransitions {
|
|
highestStackOfTransitions = ts[node].numberFound
|
|
}
|
|
}
|
|
// now we have the height, we compile the stack to print (possibly empty placeholders for some columns)
|
|
for _, node := range keys {
|
|
ts[node].stackPrioritizeFound(highestStackOfTransitions)
|
|
}
|
|
|
|
out := "\t"
|
|
for i := 0; i < highestStackOfTransitions; i++ {
|
|
for row := 0; row < RowPerTransitions; row++ {
|
|
for _, node := range keys {
|
|
out += ts[node].transitionToPrint[i].summary[row]
|
|
}
|
|
if !(i == highestStackOfTransitions-1 && row == RowPerTransitions-1) { // unless last row
|
|
out += "\n\t"
|
|
}
|
|
}
|
|
}
|
|
|
|
if out == "\t" {
|
|
return ""
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (ts *transitions) iterate() {
|
|
|
|
for _, test := range ts.tests {
|
|
|
|
test.summarizeIfDifferent()
|
|
if test.ok {
|
|
ts.numberFound++
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func (ts *transitions) stackPrioritizeFound(height int) {
|
|
for i, test := range ts.tests {
|
|
// if at the right height
|
|
if len(ts.tests)-i+len(ts.transitionToPrint) == height {
|
|
ts.transitionToPrint = append(ts.transitionToPrint, ts.tests[i:]...)
|
|
}
|
|
if test.ok {
|
|
ts.transitionToPrint = append(ts.transitionToPrint, test)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ts *transitions) fillEmptyTransition() {
|
|
if len(ts.tests) == NumberOfPossibleTransition {
|
|
return
|
|
}
|
|
for i := len(ts.tests); i < NumberOfPossibleTransition; i++ {
|
|
ts.tests = append(ts.tests, &transition{s1: "", s2: "", changeType: ""})
|
|
}
|
|
|
|
}
|
|
|
|
func (t *transition) summarizeIfDifferent() {
|
|
if t.s1 != t.s2 {
|
|
t.summary = [RowPerTransitions]string{utils.Paint(utils.BrightBlueText, t.s1), utils.Paint(utils.BlueText, "("+t.changeType+")"), utils.Paint(utils.BrightBlueText, " V "), utils.Paint(utils.BrightBlueText, t.s2)}
|
|
t.ok = true
|
|
}
|
|
for i := range t.summary {
|
|
t.summary[i] = t.summary[i] + "\t"
|
|
}
|
|
return
|
|
}
|