You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

642 lines
18 KiB
Go

1 year ago
package main
import (
"b612.me/mysql/binlog"
"b612.me/mysql/gtid"
"b612.me/starlog"
"b612.me/staros"
10 months ago
"b612.me/startext"
10 months ago
"errors"
1 year ago
"fmt"
"github.com/spf13/cobra"
"github.com/tealeg/xlsx"
"os"
"path/filepath"
"regexp"
"strings"
12 months ago
"time"
1 year ago
)
var (
10 months ago
bigThan int
smallThan int
startPos int
endPos int
startTime int64
endTime int64
includeDTs []string
excludeDTs []string
includeGtid string
excludeGtid string
outPath string
vbo bool
skipquery bool
pos int64
prefix string
outasRow bool
counts int
fourceParse bool
timeBigThan int
timeSmallThan int
outType string
10 months ago
showBasic uint8
1 year ago
)
func init() {
10 months ago
cmd.Flags().IntVar(&timeBigThan, "cost-after", 0, "show transactions cost big than x seconds")
cmd.Flags().IntVar(&timeSmallThan, "cost-less", 0, "show transactions cost below x seconds")
10 months ago
cmd.Flags().IntVarP(&counts, "count", "c", 0, "counts of binlog transaction")
12 months ago
cmd.Flags().IntVarP(&endPos, "pos", "P", 0, "skipPos of binlog")
1 year ago
cmd.Flags().IntVarP(&startPos, "start-pos", "S", 0, "startPos of binlog")
10 months ago
cmd.Flags().IntVarP(&endPos, "end-pos", "F", 0, "endPos of binlog")
1 year ago
cmd.Flags().StringVarP(&includeGtid, "include-gtid", "i", "", "include gtid")
cmd.Flags().StringVarP(&excludeGtid, "exclude-gtid", "e", "", "exclude gtid")
cmd.Flags().StringVarP(&outPath, "savepath", "o", "", "output excel path")
cmd.Flags().Int64Var(&startTime, "starttime", 0, "start unix timestamp")
cmd.Flags().Int64Var(&endTime, "endtime", 0, "end unix timestamp")
cmd.Flags().BoolVarP(&vbo, "verbose", "v", false, "show the detail verbose")
cmd.Flags().BoolVarP(&skipquery, "skip-query", "s", true, "skip query write to xlsx like BEGIN COMMIT")
12 months ago
cmd.Flags().IntVar(&bigThan, "big", 0, "show tx big than x bytes")
cmd.Flags().IntVar(&smallThan, "small", 0, "show tx small than x bytes")
cmd.Flags().StringVar(&prefix, "prefix", "mysql-bin", "mysql binlog prefix")
10 months ago
cmd.Flags().BoolVarP(&outasRow, "row-mode", "r", false, "output as row")
10 months ago
cmd.Flags().StringSliceVarP(&includeDTs, "include-tables", "I", []string{}, "whitelist schemas and tables,eg: schema.table")
cmd.Flags().StringSliceVarP(&excludeDTs, "exclude-tables", "E", []string{}, "blacklist schemas and tables,eg: schema.table")
cmd.Flags().BoolVar(&fourceParse, "force-parse-all", false, "force parse all events in binlog")
cmd.Flags().StringVarP(&outType, "output-type", "t", "xlsx", "output file type,now support xlsx,txt")
10 months ago
cmd.Flags().Uint8Var(&showBasic, "show-level", 255, "show level,value range 0-255")
1 year ago
}
var cmd = &cobra.Command{
10 months ago
Use: "",
Short: "binlog parser by i@b612.me",
Long: "binlog解析工具支持离线binlog解析",
Version: "0.1.0",
1 year ago
Run: func(cmd *cobra.Command, args []string) {
10 months ago
// for test
// cc05c88c-0683-11ee-8149-fa163e8c148c:44969805
if len(args) == 0 {
1 year ago
starlog.Warningln("Please enter a binlog path or folder")
10 months ago
cmd.Help()
1 year ago
return
}
11 months ago
now := time.Now()
10 months ago
ParseBinlog(args)
11 months ago
cost := time.Now().Sub(now).Seconds()
fmt.Println("")
fmt.Printf("Time Cost:%.2fs", cost)
1 year ago
},
}
func main() {
cmd.Execute()
}
10 months ago
func ParseBinlog(filePath []string) {
1 year ago
var err error
foundCount := 0
var totalGtid *gtid.Gtid
10 months ago
var out Outfile
1 year ago
if outPath != "" {
10 months ago
out, err = InitOutput(outType, outPath)
1 year ago
if err != nil {
10 months ago
starlog.Errorln(err)
1 year ago
return
}
}
12 months ago
getParser := func(fpath string) string {
var sTime, eTime time.Time
if startTime != 0 {
sTime = time.Unix(startTime, 0)
}
if endTime != 0 {
eTime = time.Unix(endTime, 0)
}
11 months ago
onlyGtid := false
10 months ago
if !vbo && outPath == "" && !fourceParse {
11 months ago
onlyGtid = true
}
12 months ago
var filter = binlog.BinlogFilter{
10 months ago
IncludeTables: includeDTs,
ExcludeTables: excludeDTs,
IncludeGtid: includeGtid,
ExcludeGtid: excludeGtid,
StartPos: startPos,
EndPos: endPos,
StartDate: sTime,
EndDate: eTime,
BigThan: bigThan,
SmallThan: smallThan,
OnlyShowGtid: onlyGtid,
12 months ago
}
var cGtid *gtid.Gtid
11 months ago
proc := make(chan string, 1000)
if !vbo {
go func() {
var latest string
var count = uint16(0)
for {
select {
case tmp := <-proc:
if tmp == "end" {
fmt.Println(latest)
return
}
latest = tmp
if count%10 == 0 {
fmt.Print(latest)
}
count++
10 months ago
case <-time.After(time.Millisecond * 200):
11 months ago
fmt.Print(latest)
}
}
}()
}
10 months ago
err = binlog.ParseBinlogWithFilter(fpath, pos, filter, func(tx binlog.Transaction) bool {
10 months ago
if timeBigThan != 0 {
if tx.TxEndTime-tx.TxStartTime < int64(timeBigThan) {
return true
}
}
if timeSmallThan != 0 {
if tx.TxEndTime-tx.TxStartTime > int64(timeSmallThan) {
return true
}
}
1 year ago
foundCount++
12 months ago
if cGtid == nil {
cGtid, _ = gtid.Parse(tx.GTID)
} else {
cGtid.Add(tx.GTID)
}
1 year ago
if totalGtid == nil {
totalGtid, _ = gtid.Parse(tx.GTID)
} else {
totalGtid.Add(tx.GTID)
}
if !vbo {
11 months ago
proc <- fmt.Sprintf("已找到%d个合法GTID\r", foundCount)
1 year ago
} else {
10 months ago
if !outasRow {
10 months ago
switch showBasic {
case 0:
fmt.Printf("GTID:%s\nTime:%s\nSQL:%v\n\n",
tx.GTID, tx.Time, tx.GetSqlOrigin())
default:
fmt.Printf("GTID:%s Time:%s StartPos:%v EndPos:%v RowsCount:%v Size:%v Detail:%+v\n",
tx.GTID, tx.Time, tx.StartPos, tx.EndPos, tx.RowsCount, tx.Size, tx.Txs)
}
10 months ago
} else {
fmt.Printf("-------------------------\nGTID:%s Time:%s StartPos:%v EndPos:%v RowsCount:%v Size:%v\n\n",
tx.GTID, tx.Time, tx.StartPos, tx.EndPos, tx.RowsCount, tx.Size)
for _, t := range tx.Txs {
if skipquery && (strings.ToLower(t.Sql) == "begin" || strings.ToLower(t.Sql) == "commit") {
continue
}
10 months ago
sqls := generateOfflineRowSql(t)
for _, sql := range sqls {
10 months ago
switch showBasic {
case 0:
fmt.Printf("GTID:\t%s\nTime:\t%s\nSQL:\t%+v\n\n",
tx.GTID, t.Time, sql.SQL)
case 1:
fmt.Printf("GTID:\t%s\nTime:\t%s\nSQLOrigin:\t%+v\nSQL:\t%+v\n\n",
tx.GTID, t.Time, t.Sql, sql.SQL)
default:
fmt.Printf("GTID:\t%s\nTime:\t%s\nStartPos:\t%v\nEndPos:\t%v\nCost\t%v\nSchema:%v\nRowsCount:\t%v\nSQLOrigin:\t%v\nSQL:\t%+v\n\n",
tx.GTID, t.Time, t.StartPos, t.EndPos, tx.TxEndTime-tx.TxStartTime, t.Db+"."+t.Table, t.RowCount, t.Sql, sql.SQL)
}
10 months ago
}
10 months ago
}
}
1 year ago
}
if outPath != "" {
10 months ago
AddOutfile(out, tx, foundCount)
1 year ago
}
10 months ago
if counts > 0 && foundCount >= counts {
10 months ago
return false
}
return true
1 year ago
})
11 months ago
if !vbo {
time.Sleep(time.Millisecond * 500)
}
12 months ago
var cGtidStr string
if cGtid != nil {
cGtidStr = cGtid.String()
}
10 months ago
if outPath != "" && out.Type == "xlsx" {
err = FinishOutfile(out, outPath)
1 year ago
if err != nil {
starlog.Errorln(err)
12 months ago
return cGtidStr
1 year ago
}
}
if err != nil {
starlog.Errorln(err)
12 months ago
return cGtidStr
1 year ago
}
12 months ago
return cGtidStr
1 year ago
}
12 months ago
var gtidRes [][]string
1 year ago
for _, fp := range filePath {
if staros.IsFolder(fp) {
files, err := os.ReadDir(fp)
if err != nil {
starlog.Errorln("read folder failed:", err)
return
}
12 months ago
rgp := regexp.MustCompile(`^` + prefix + `\.\d+$`)
1 year ago
for _, v := range files {
if !v.IsDir() && rgp.MatchString(v.Name()) {
12 months ago
gtidRes = append(gtidRes, []string{v.Name(), getParser(filepath.Join(fp, v.Name()))})
1 year ago
}
}
} else {
getParser(fp)
}
}
if outPath != "" {
10 months ago
err := FinishOutfile(out, outPath)
if err != nil {
starlog.Errorln(err)
}
1 year ago
}
fmt.Println("")
12 months ago
if len(gtidRes) != 0 {
for _, v := range gtidRes {
fmt.Printf("%s:%s\n", v[0], v[1])
}
}
fmt.Println("")
1 year ago
allGtid := ""
if totalGtid != nil {
allGtid = totalGtid.String()
}
10 months ago
fmt.Printf("Total Gtid:%v\nTotal SQL Number:%v\n", allGtid, foundCount)
1 year ago
}
10 months ago
10 months ago
func prepareXlsx() (Outfile, error) {
10 months ago
owrt := xlsx.NewFile()
res, err := owrt.AddSheet("结果")
if err != nil {
starlog.Errorln(err)
10 months ago
return Outfile{}, err
10 months ago
}
title := res.AddRow()
title.AddCell().SetValue("序号")
title.AddCell().SetValue("GTID")
title.AddCell().SetValue("时间")
10 months ago
if showBasic > 1 {
title.AddCell().SetValue("时间戳")
title.AddCell().SetValue("StartPos")
title.AddCell().SetValue("EndPos")
title.AddCell().SetValue("事务大小")
title.AddCell().SetValue("影响行数")
title.AddCell().SetValue("事务状态")
title.AddCell().SetValue("事务耗时")
title.AddCell().SetValue("压缩类型")
title.AddCell().SetValue("单语句StartPos")
title.AddCell().SetValue("单语句EndPos")
title.AddCell().SetValue("单语句时间")
title.AddCell().SetValue("单语句影响行数")
title.AddCell().SetValue("单语句影响库")
title.AddCell().SetValue("单语句影响表")
}
10 months ago
title.AddCell().SetValue("SQL类型")
title.AddCell().SetValue("具体SQL")
title.AddCell().SetValue("从属事务编号")
title.AddCell().SetValue("同事务行编号")
title.AddCell().SetValue("行变更内容")
res.SetColWidth(0, 0, 5)
res.SetColWidth(1, 1, 40)
res.SetColWidth(3, 6, 6)
res.SetColWidth(7, 7, 5)
10 months ago
res.SetColWidth(18, 18, 40)
return Outfile{
Type: "xlsx",
File: owrt,
}, owrt.Save(outPath)
}
type Outfile struct {
Type string
File interface{}
10 months ago
}
10 months ago
func add2Xlsx(owrt *xlsx.File, tx binlog.Transaction, foundCount int) error {
10 months ago
for k, t := range tx.Txs {
if skipquery && (strings.ToLower(t.Sql) == "begin" || strings.ToLower(t.Sql) == "commit") {
continue
}
addRow := func() *xlsx.Row {
r := owrt.Sheets[0].AddRow()
r.AddCell().SetValue(foundCount)
r.AddCell().SetValue(tx.GTID)
r.AddCell().SetValue(tx.Time.String())
10 months ago
if showBasic > 1 {
r.AddCell().SetValue(tx.Timestamp)
r.AddCell().SetValue(tx.StartPos)
r.AddCell().SetValue(tx.EndPos)
r.AddCell().SetValue(tx.Size)
r.AddCell().SetValue(tx.RowsCount)
status := "PREPARE"
switch tx.Status {
case binlog.STATUS_BEGIN:
status = "BEGIN"
case binlog.STATUS_COMMIT:
status = "COMMIT"
case binlog.STATUS_ROLLBACK:
status = "ROLLBACK"
}
r.AddCell().SetValue(status)
r.AddCell().SetValue(tx.TxEndTime - tx.TxStartTime)
if t.CompressionType == "" {
r.AddCell().SetValue("NONE")
} else {
r.AddCell().SetValue(t.CompressionType)
}
r.AddCell().SetValue(t.StartPos)
r.AddCell().SetValue(t.EndPos)
r.AddCell().SetValue(t.Time.String())
r.AddCell().SetValue(t.RowCount)
r.AddCell().SetValue(t.Db)
r.AddCell().SetValue(t.Table)
10 months ago
}
r.AddCell().SetValue(t.SqlType)
r.AddCell().SetValue(t.Sql)
return r
}
if !outasRow {
r := addRow()
r.AddCell().SetValue(k + 1)
r.AddCell().SetValue(1)
r.AddCell().SetValue(t.Rows)
continue
}
10 months ago
sqls := generateOfflineRowSql(t)
for _, sql := range sqls {
r := addRow()
r.AddCell().SetValue(k + 1)
r.AddCell().SetValue(sql.ID)
r.AddCell().SetValue(sql.SQL)
}
}
return nil
}
func InitOutput(types string, outPath string) (Outfile, error) {
switch types {
case "xlsx":
return prepareXlsx()
case "txt":
return prepareTxtFile(outPath)
default:
return Outfile{}, errors.New("Not Support This Outfile Format:" + types)
}
}
func AddOutfile(of Outfile, tx binlog.Transaction, foundCount int) error {
switch of.Type {
case "xlsx":
owrt, ok := of.File.(*xlsx.File)
if !ok {
return errors.New("failed!,not a valid xlsx pointer")
}
return add2Xlsx(owrt, tx, foundCount)
case "txt":
f, ok := of.File.(*os.File)
if !ok {
return errors.New("failed!,not a valid txtfile pointer")
}
return addTxtFile(f, tx, foundCount)
default:
return errors.New("Not Support This Outfile Format:" + of.Type)
}
}
func FinishOutfile(of Outfile, path string) error {
switch of.Type {
case "xlsx":
owrt, ok := of.File.(*xlsx.File)
if !ok {
return errors.New("failed!,not a valid xlsx pointer")
}
return owrt.Save(path)
case "txt":
f, ok := of.File.(*os.File)
if !ok {
return errors.New("failed!,not a valid txtfile pointer")
}
f.Sync()
return f.Close()
default:
return errors.New("Not Support This Outfile Format:" + of.Type)
}
}
func prepareTxtFile(path string) (Outfile, error) {
txtFile, err := os.OpenFile(path, os.O_TRUNC|os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return Outfile{}, err
}
return Outfile{
Type: "txt",
File: txtFile,
}, err
}
func addTxtFile(f *os.File, tx binlog.Transaction, foundCount int) error {
for _, t := range tx.Txs {
if skipquery && (strings.ToLower(t.Sql) == "begin" || strings.ToLower(t.Sql) == "commit") {
continue
}
addRow := func() error {
status := "PREPARE"
switch tx.Status {
case binlog.STATUS_BEGIN:
status = "BEGIN"
case binlog.STATUS_COMMIT:
status = "COMMIT"
case binlog.STATUS_ROLLBACK:
status = "ROLLBACK"
}
if t.CompressionType == "" {
t.CompressionType = "NONE"
}
10 months ago
var err error
switch showBasic {
case 0, 1:
_, err = f.WriteString(fmt.Sprintf("\n\nGTID:%s\nTime:%v\nSQLOrigin:%v\n",
tx.GTID, tx.Time, strings.ReplaceAll(t.Sql, "\n", " ")))
default:
_, err = f.WriteString(fmt.Sprintf("\n\nNumber:%v\nGTID:%s\nTime%s\nTotalSize:%v TotalCost:%v\n"+
"StartPos:%v EndPos:%v RowsCount:%v\nStatus:%v\n"+
"TxStartPos:%v TxEndPos:%v TxRowsCount:%v CompressionType:%s\n"+
"Schema:%v Type:%s TxTime:%v\nSQLOrigin:%v\n",
foundCount, tx.GTID, tx.Time, tx.Size, tx.TxEndTime-tx.TxStartTime,
tx.StartPos, tx.EndPos, tx.RowsCount, status,
t.StartPos, t.EndPos, t.RowCount, t.CompressionType,
t.Db+"."+t.Table, t.SqlType, t.Time, strings.ReplaceAll(t.Sql, "\n", " ")))
}
10 months ago
if err != nil {
return err
}
return nil
}
if !outasRow {
return addRow()
}
sqls := generateOfflineRowSql(t)
for _, sql := range sqls {
err := addRow()
if err != nil {
return err
}
if sql.SQL == "" {
9 months ago
continue
10 months ago
}
f.WriteString("SQL:" + sql.SQL + "\n")
}
10 months ago
}
10 months ago
return nil
10 months ago
}
10 months ago
type SQLInfo struct {
ID int
SQL string
Origin [][]interface{}
}
func generateOfflineRowSql(t binlog.TxDetail) []SQLInfo {
var sqlList = make([]SQLInfo, 0, len(t.Rows))
10 months ago
switch t.SqlType {
case "insert":
for idx, rows := range t.Rows {
setence := ""
for _, row := range rows {
switch row.(type) {
case uint, uint64, uint32, uint16, uint8, int, int64, int32, int16, int8, float64, float32:
setence += fmt.Sprintf("%v, ", row)
case string:
setence += fmt.Sprintf("'%v', ", strings.ReplaceAll(row.(string), "'", "''"))
case []byte:
10 months ago
if startext.IsUtf8(row.([]byte)) {
setence += fmt.Sprintf("'%v', ", strings.ReplaceAll(string(row.([]byte)), "'", "''"))
} else if startext.IsGBK(row.([]byte)) {
data, _ := startext.GBK2UTF8(row.([]byte))
setence += fmt.Sprintf("'%v', ", strings.ReplaceAll(string(data), "'", "''"))
} else {
setence += fmt.Sprintf("X'%X', ", row.([]byte))
}
10 months ago
case nil:
setence += fmt.Sprintf("%v, ", "NULL")
default:
setence += fmt.Sprintf("%v, ", row)
}
}
if setence != "" && len(setence) > 2 {
setence = setence[:len(setence)-2]
}
10 months ago
sqlList = append(sqlList,
SQLInfo{
ID: idx + 1,
SQL: fmt.Sprintf(`INSERT INTO %s.%s VALUES(%v)`, t.Db, t.Table, setence),
},
)
10 months ago
}
case "update":
var sql string
var where string
for idxc, rows := range t.Rows {
setence := ""
spec := ", "
if idxc%2 == 0 {
spec = " AND "
}
for idxf, row := range rows {
switch row.(type) {
case uint, uint64, uint32, uint16, uint8, int, int64, int32, int16, int8, float64, float32:
10 months ago
setence += fmt.Sprintf("@%d=%v%s", idxf, row, spec)
10 months ago
case string:
10 months ago
setence += fmt.Sprintf("@%d='%v'%s", idxf, strings.ReplaceAll(row.(string), "'", "''"), spec)
10 months ago
case []byte:
10 months ago
if startext.IsUtf8(row.([]byte)) {
setence += fmt.Sprintf("@%d='%v'%s", idxf, strings.ReplaceAll(string(row.([]byte)), "'", "''"), spec)
} else if startext.IsGBK(row.([]byte)) {
data, _ := startext.GBK2UTF8(row.([]byte))
setence += fmt.Sprintf("@%d='%v'%s", idxf, strings.ReplaceAll(string(data), "'", "''"), spec)
} else {
setence += fmt.Sprintf("@%d=X'%X'%s", idxf, row.([]byte), spec)
}
10 months ago
case nil:
10 months ago
setence += fmt.Sprintf("@%d=%v%s", idxf, "NULL", spec)
10 months ago
default:
10 months ago
setence += fmt.Sprintf("@%d=%v%s", idxf, row, spec)
10 months ago
}
}
if setence != "" && len(setence) > 2 {
setence = setence[:len(setence)-len(spec)]
}
if idxc%2 == 0 {
where = setence
continue
}
sql = fmt.Sprintf("UPDATE %s.%s SET (%v) WHERE %v", t.Db, t.Table, setence, where)
10 months ago
sqlList = append(sqlList,
SQLInfo{
ID: (idxc + 1) / 2,
SQL: sql,
},
)
10 months ago
}
case "delete":
for idx, rows := range t.Rows {
setence := ""
spec := " AND "
for idxf, row := range rows {
switch row.(type) {
case uint, uint64, uint32, uint16, uint8, int, int64, int32, int16, int8, float64, float32:
10 months ago
setence += fmt.Sprintf("@%d=%v%s", idxf, row, spec)
10 months ago
case string:
10 months ago
setence += fmt.Sprintf("@%d='%v'%s", idxf, strings.ReplaceAll(row.(string), "'", "''"), spec)
10 months ago
case []byte:
10 months ago
if startext.IsUtf8(row.([]byte)) {
setence += fmt.Sprintf("@%d='%v'%s", idxf, strings.ReplaceAll(string(row.([]byte)), "'", "''"), spec)
} else if startext.IsGBK(row.([]byte)) {
data, _ := startext.GBK2UTF8(row.([]byte))
setence += fmt.Sprintf("@%d='%v'%s", idxf, strings.ReplaceAll(string(data), "'", "''"), spec)
} else {
setence += fmt.Sprintf("@%d=X'%X'%s", idxf, row.([]byte), spec)
}
10 months ago
case nil:
10 months ago
setence += fmt.Sprintf("@%d=%v%s", idxf, "NULL", spec)
10 months ago
default:
10 months ago
setence += fmt.Sprintf("@%d=%v%s", idxf, row, spec)
10 months ago
}
}
if setence != "" && len(setence) > 2 {
setence = setence[:len(setence)-len(spec)]
}
sql := fmt.Sprintf("DELETE FROM %s.%s WHERE %v", t.Db, t.Table, setence)
10 months ago
sqlList = append(sqlList,
SQLInfo{
ID: idx + 1,
SQL: sql,
},
)
10 months ago
}
default:
10 months ago
sqlList = append(sqlList, SQLInfo{Origin: t.Rows})
return sqlList
10 months ago
}
10 months ago
return sqlList
10 months ago
}