Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tiup: supprot record tiup execution history #1808

Merged
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions cmd/history.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright 2022 PingCAP, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
"encoding/json"
"fmt"
"strconv"

"github.com/pingcap/errors"
"github.com/pingcap/tiup/pkg/environment"
"github.com/pingcap/tiup/pkg/tui"
"github.com/spf13/cobra"
)

// newHistoryCmd history
func newHistoryCmd() *cobra.Command {
rows := 100
var displayMode string
var all bool
cmd := &cobra.Command{
Use: "history <rows>",
Short: "Display the historical execution record of TiUP, displays 100 lines by default",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
r, err := strconv.Atoi(args[0])
if err == nil {
rows = r
} else {
return fmt.Errorf("%s: numeric argument required", args[0])
}
}

env := environment.GlobalEnv()
rows, err := env.GetHistory(rows, all)
if err != nil {
return err
}

if displayMode == "json" {
for _, r := range rows {
rBytes, err := json.Marshal(r)
if err != nil {
continue
}
fmt.Println(string(rBytes))
}
return nil
}
var table [][]string
table = append(table, []string{"Date", "Command", "Code"})

for _, r := range rows {
table = append(table, []string{
r.Date.Format("2006-01-02T15:04:05"),
r.Command,
strconv.Itoa(r.Code),
})
}
tui.PrintTable(table, true)
fmt.Printf("history log save path: %s\n", env.LocalPath(environment.HistoryDir))
return nil
},
}
cmd.Flags().StringVar(&displayMode, "format", "default", "The format of output, available values are [default, json]")
cmd.Flags().BoolVar(&all, "all", false, "Display all execution history")
cmd.AddCommand(newHistoryCleanupCmd())
return cmd
}

func newHistoryCleanupCmd() *cobra.Command {
var retainDays int
var all bool
var skipConfirm bool
cmd := &cobra.Command{
Use: "cleanup",
Short: "delete all execution history",
RunE: func(cmd *cobra.Command, args []string) error {
if retainDays < 0 {
return errors.Errorf("retain-days cannot be less than 0")
}

if all {
retainDays = 0
}

env := environment.GlobalEnv()
return env.DeleteHistory(retainDays, skipConfirm)
},
}

cmd.Flags().IntVar(&retainDays, "retain-days", 60, "Number of days to keep history for deletion")
cmd.Flags().BoolVar(&all, "all", false, "Delete all history")
cmd.Flags().BoolVarP(&skipConfirm, "yes", "y", false, "Skip all confirmations and assumes 'yes'")
return cmd
}
7 changes: 7 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ the latest stable version will be downloaded from the repository.`,
newMirrorCmd(),
newTelemetryCmd(),
newEnvCmd(),
newHistoryCmd(),
)

originHelpFunc := rootCmd.HelpFunc()
Expand Down Expand Up @@ -248,6 +249,12 @@ func Execute() {
// us a dedicated package for that
reportEnabled = false
} else {
// record TiUP execution history
err := environment.HistoryRecord(env, os.Args, start, code)
if err != nil {
log.Warnf("Record TiUP execution history log failed: %v", err)
}

teleMeta, _, err := telemetry.GetMeta(env)
if err == nil {
reportEnabled = teleMeta.Status == telemetry.EnableStatus
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ require (
github.com/pingcap/kvproto v0.0.0-20220125073028-58f2ac94aa38
github.com/pingcap/log v0.0.0-20211215031037-e024ba4eb0ee // indirect
github.com/pingcap/tidb-insight/collector v0.0.0-20220111101533-227008e9835b
github.com/pkg/errors v0.9.1
github.com/prometheus/client_model v0.2.0
github.com/prometheus/common v0.32.1
github.com/prometheus/prom2json v1.3.0
Expand Down
244 changes: 244 additions & 0 deletions pkg/environment/history.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
// Copyright 2022 PingCAP, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.

package environment

import (
"bufio"
"encoding/json"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"

"github.com/fatih/color"
"github.com/pingcap/tiup/pkg/tui"
"github.com/pingcap/tiup/pkg/utils"
"github.com/pkg/errors"
)

const (
// HistoryDir history save path
HistoryDir = "history"
historyPrefix = "tiup-history-"
historySize int64 = 1024 * 64 // history file default size is 64k
)

// commandRow type of command history row
type historyRow struct {
Date time.Time `json:"time"`
Command string `json:"command"`
Code int `json:"code"`
srstack marked this conversation as resolved.
Show resolved Hide resolved
}

// historyItem record history row file item
type historyItem struct {
path string
info fs.FileInfo
index int
}

// HistoryRecord record tiup exec cmd
func HistoryRecord(env *Environment, command []string, date time.Time, code int) error {
if env == nil {
return nil
}

historyPath := env.LocalPath(HistoryDir)
if utils.IsNotExist(historyPath) {
err := os.MkdirAll(historyPath, 0755)
if err != nil {
return err
}
}

h := &historyRow{
Command: strings.Join(command, " "),
Date: date,
Code: code,
}

return h.save(historyPath)
}

// save save commandRow to file
func (r *historyRow) save(dir string) error {
rBytes, err := json.Marshal(r)
if err != nil {
return err
}

historyFile := getLatestHistoryFile(dir)

f, err := os.OpenFile(historyFile.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(append(rBytes, []byte("\n")...))
return err
}

// GetHistory get tiup history
func (env *Environment) GetHistory(count int, all bool) ([]*historyRow, error) {
fList, err := getHistoryFileList(env.LocalPath(HistoryDir))
if err != nil {
return nil, err
}
rows := []*historyRow{}
for _, f := range fList {
rs, err := f.getHistory()
if err != nil {
return rows, err
}
if (len(rows)+len(rs)) > count && !all {
i := len(rows) + len(rs) - count
rows = append(rs[i:], rows...)
break
}

rows = append(rs, rows...)
}
return rows, nil
}

// DeleteHistory delete history file
func (env *Environment) DeleteHistory(retainDays int, skipConfirm bool) error {
if retainDays < 0 {
return errors.Errorf("retainDays cannot be less than 0")
}

// history file before `DelBeforeTime` will be deleted
oneDayDuration, _ := time.ParseDuration("-24h")
delBeforeTime := time.Now().Add(oneDayDuration * time.Duration(retainDays))

if !skipConfirm {
fmt.Printf("History logs before %s will be %s!\n",
color.HiYellowString(delBeforeTime.Format("2006-01-02T15:04:05")),
color.HiYellowString("deleted"),
)
if err := tui.PromptForConfirmOrAbortError("Do you want to continue? [y/N]:"); err != nil {
return err
}
}

fList, err := getHistoryFileList(env.LocalPath(HistoryDir))
if err != nil {
return err
}

if len(fList) == 0 {
return nil
}

for _, f := range fList {
if f.info.ModTime().Before(delBeforeTime) {
err := os.Remove(f.path)
if err != nil {
return err
}
continue
}
}
return nil
}

// getHistory get tiup history execution row
func (i *historyItem) getHistory() ([]*historyRow, error) {
rows := []*historyRow{}

fi, err := os.Open(i.path)
if err != nil {
return rows, err
}
defer fi.Close()

br := bufio.NewReader(fi)
for {
a, _, c := br.ReadLine()
if c == io.EOF {
break
}
r := &historyRow{}
// ignore
err := json.Unmarshal(a, r)
if err != nil {
continue
}
rows = append(rows, r)
}

return rows, nil
}

// getHistoryFileList get the history file list
func getHistoryFileList(dir string) ([]historyItem, error) {
fileInfos, err := os.ReadDir(dir)
if err != nil {
return nil, err
}

hfileList := []historyItem{}
for _, fi := range fileInfos {
if fi.IsDir() {
continue
}

// another suffix
// ex: tiup-history-0.bak
i, err := strconv.Atoi((strings.TrimPrefix(fi.Name(), historyPrefix)))
if err != nil {
continue
}

fInfo, _ := fi.Info()
hfileList = append(hfileList, historyItem{
path: filepath.Join(dir, fi.Name()),
index: i,
info: fInfo,
})
}

sort.Slice(hfileList, func(i, j int) bool {
return hfileList[i].index > hfileList[j].index
})

return hfileList, nil
}

// getLatestHistoryFile get the latest history file, use index 0 if it doesn't exist
func getLatestHistoryFile(dir string) (item historyItem) {
fileList, err := getHistoryFileList(dir)
// start from 0
if len(fileList) == 0 || err != nil {
item.index = 0
item.path = filepath.Join(dir, fmt.Sprintf("%s%s", historyPrefix, strconv.Itoa(item.index)))
return
}

latestItem := fileList[0]

if latestItem.info.Size() >= historySize {
item.index = latestItem.index + 1
item.path = filepath.Join(dir, fmt.Sprintf("%s%s", historyPrefix, strconv.Itoa(item.index)))
} else {
item = latestItem
}

return
}