polardbxoperator/pkg/binlogtool/cmd/show.go

599 lines
19 KiB
Go

/*
Copyright 2022 Alibaba Group Holding Limited.
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,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"bufio"
"errors"
"fmt"
"github.com/alibaba/polardbx-operator/pkg/binlogtool/binlog"
"io"
"os"
"regexp"
"strconv"
"strings"
"github.com/spf13/cobra"
"golang.org/x/exp/slices"
"github.com/alibaba/polardbx-operator/pkg/binlogtool/binlog/event"
"github.com/alibaba/polardbx-operator/pkg/binlogtool/binlog/rows"
"github.com/alibaba/polardbx-operator/pkg/binlogtool/binlog/spec"
"github.com/alibaba/polardbx-operator/pkg/binlogtool/binlog/str"
"github.com/alibaba/polardbx-operator/pkg/binlogtool/bitmap"
"github.com/alibaba/polardbx-operator/pkg/binlogtool/utils"
)
var (
showBinlogFile string
showBinlogStartOffset uint64
showBinlogEndOffset uint64
showBinlogInterestEventTypes []string
showBinlogIncludeHeader bool
showBinlogScanHeaderOnly bool
showBinlogPrintHeaderOnly bool
showBinlogSkip int
showBinlogPrecede int
showBinlogCount int
showBinlogCharset string
showBinlogChecksum string
showBinlogValidateChecksum bool
showOutputFile string
showNoOutput bool
showDecodeRows bool
showRowsTargetSchema string
showRowsTargetTable string
showDecodeBlob bool
showStartTimestamp uint32
showEndTimestamp uint32
showRowsPredicate string
showBinlogTailCount int
showQuery string
)
func init() {
showCmd.Flags().Uint64Var(&showBinlogStartOffset, "start-offset", 0, "start offset in bytes")
showCmd.Flags().Uint64Var(&showBinlogEndOffset, "end-offset", 0, "end offset in bytes")
showCmd.Flags().StringSliceVarP(&showBinlogInterestEventTypes, "event", "e", nil, "interested event types")
showCmd.Flags().BoolVar(&showBinlogIncludeHeader, "header", false, "show headers")
showCmd.Flags().BoolVar(&showBinlogScanHeaderOnly, "header-only", false, "show headers only (scan only headers)")
showCmd.Flags().BoolVar(&showBinlogPrintHeaderOnly, "print-header-only", false, "show headers only (print only headers)")
showCmd.Flags().IntVarP(&showBinlogSkip, "skip", "s", 0, "skip the first s events")
showCmd.Flags().IntVarP(&showBinlogCount, "count", "n", -1, "show n events (negative means no limit)")
showCmd.Flags().StringVar(&showBinlogCharset, "charset", "utf-8", "charset to decode strings")
showCmd.Flags().StringVar(&showBinlogChecksum, "checksum", "crc32", "binary log checksum (ignored for binary log version v1, v3 and v4 after 3.6.1)")
showCmd.Flags().BoolVar(&showBinlogValidateChecksum, "validate-checksum", false, "turn on checksum validation")
showCmd.Flags().StringVarP(&showOutputFile, "output", "o", "", "output file")
showCmd.Flags().BoolVar(&showNoOutput, "no-output", false, "no output")
showCmd.Flags().IntVarP(&showBinlogPrecede, "precede", "b", 0, "print n events before the start offset (only probe 64K bytes ahead)")
showCmd.Flags().BoolVar(&showDecodeRows, "decode-rows", false, "print decoded rows (must allow table map event)")
showCmd.Flags().StringVar(&showRowsTargetTable, "table", "", "filter table map event and rows event with matched table (regex)")
showCmd.Flags().StringVar(&showRowsTargetSchema, "schema", "", "filter table map event and rows event with matched schema (regex)")
showCmd.Flags().BoolVar(&showDecodeBlob, "decode-blobs", false, "print decoded blobs (change charset with --charset)")
showCmd.Flags().Uint32Var(&showStartTimestamp, "start-ts", 0, "start timestamp in seconds (compared with event header)")
showCmd.Flags().Uint32Var(&showEndTimestamp, "end-ts", 0, "end timestamp in seconds (compared with event header)")
showCmd.Flags().StringVarP(&showRowsPredicate, "predicate", "p", "", `predicate to filter the rows event by values (must be specified with --decode-rows),
in the format of @i=value[,@j=value,...], where i is the column index start with 1`)
showCmd.Flags().IntVarP(&showBinlogTailCount, "tail", "t", -1, "show tail n events")
showCmd.Flags().StringVar(&showQuery, "query", "", "filter query event and rows query event with regex")
rootCmd.AddCommand(showCmd)
}
const showProbeSize = 65536
var (
showRowsTableRe *regexp.Regexp
showRowsSchemaRe *regexp.Regexp
showQueryRe *regexp.Regexp
showRowsPredicateKeyValues map[int]string
)
func wrapRegex(s string) string {
if !strings.HasPrefix(s, "^") {
s = "^" + s
}
if !strings.HasSuffix(s, "$") {
s += "$"
}
return s
}
var showCmd = &cobra.Command{
Use: "show [flags] file",
Short: "Show binlog events",
Long: "Show binlog events",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("please specify a binlog file")
}
showBinlogFile = args[0]
return nil
},
PreRun: wrap(func(cmd *cobra.Command, args []string) error {
var err error
if showBinlogScanHeaderOnly {
if showDecodeRows {
return errors.New("--header-only and --decode-rows are not compatible")
}
if showRowsTargetSchema != "" {
_, _ = fmt.Fprintln(os.Stderr, "WARN: ignore --schema values because --header-only is specified")
}
if showRowsTargetTable != "" {
_, _ = fmt.Fprintln(os.Stderr, "WARN: ignore --table values because --header-only is specified")
}
if showQuery != "" {
_, _ = fmt.Fprintln(os.Stderr, "WARN: ignore --query values because --header-only is specified")
}
} else {
if showRowsPredicate != "" {
if !showDecodeRows {
return errors.New("--predicate must be applied with --decode-rows")
}
showRowsPredicateKeyValues, err = parsePredicate(showRowsPredicate)
if err != nil {
return fmt.Errorf("failed to parse predicates: %w", err)
}
}
if showRowsTargetTable != "" {
if showRowsTableRe, err = regexp.Compile(wrapRegex(showRowsTargetTable)); err != nil {
return fmt.Errorf("failed to parse regex: %w", err)
}
}
if showRowsTargetSchema != "" {
if showRowsSchemaRe, err = regexp.Compile(wrapRegex(showRowsTargetSchema)); err != nil {
return fmt.Errorf("failed to parse regex: %w", err)
}
}
if showQuery != "" {
if showQueryRe, err = regexp.Compile(showQuery); err != nil {
return fmt.Errorf("faield to parse regex: %w", err)
}
}
if showBinlogCharset != "utf-8" {
enc, err := lookupEncoding(showBinlogCharset)
if err != nil {
return err
}
str.DefaultEncoding = enc
}
if showDecodeBlob {
str.DecodeBlob = true
}
}
return nil
}),
Run: wrap(func(cmd *cobra.Command, args []string) error {
opts := []binlog.LogEventScannerOption{
binlog.WithBinlogFile(showBinlogFile),
binlog.WithChecksumAlgorithm(showBinlogChecksum),
}
if showBinlogStartOffset > 0 {
// Enable probe when start offset specified.
opts = append(opts, binlog.WithProbeFirstHeader(showProbeSize))
}
if showBinlogEndOffset > 0 {
opts = append(opts, binlog.WithEndPos(showBinlogEndOffset))
}
// Control if table map events are displayed when decoded rows.
var showTableMapEvents = true
if showBinlogInterestEventTypes != nil {
eventTypeCodes := utils.ConvertAndConcatenateSlice(showBinlogInterestEventTypes, guessEventType)
eventTypeCodes = utils.SortedSlice(utils.DistinctSlice(eventTypeCodes))
if showDecodeRows && !slices.Contains(eventTypeCodes, spec.TABLE_MAP_EVENT) {
eventTypeCodes = append(eventTypeCodes, spec.TABLE_MAP_EVENT)
showTableMapEvents = false
}
if (showRowsTargetSchema != "" || showRowsTargetTable != "") &&
!slices.Contains(eventTypeCodes, spec.TABLE_MAP_EVENT) {
return errors.New("enable table map / rows event filter but table map event is not in the interested types")
}
opts = append(opts, binlog.WithInterestedLogEventTypes(eventTypeCodes...))
}
if showStartTimestamp > 0 {
opts = append(opts, binlog.WithLogEventHeaderFilter(func(header event.LogEventHeader) bool {
return header.EventTimestamp() >= showStartTimestamp
}))
}
if showBinlogValidateChecksum {
opts = append(opts, binlog.EnableChecksumValidation)
}
if showBinlogScanHeaderOnly {
opts = append(opts, binlog.WithScanMode(binlog.ScanModeHeaderOnly))
}
startOffset := showBinlogStartOffset
if showBinlogPrecede > 0 {
// Backoff probe size bytes.
if startOffset >= showProbeSize {
startOffset -= showProbeSize
} else {
startOffset = 0
}
}
lazyScanner := binlog.NewLazyLogEventScanCloser(
func() (io.ReadCloser, error) {
f, err := os.Open(showBinlogFile)
if err != nil {
return nil, err
}
return utils.NewSeekableBufferReader(f), nil
},
startOffset,
opts...,
)
defer lazyScanner.Close()
var w *bufio.Writer
if !showNoOutput {
var of io.Writer
if showOutputFile != "" {
f, err := os.OpenFile(showOutputFile, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0644)
if err != nil {
return err
}
defer f.Close()
of = f
} else {
of = os.Stdout
}
w = bufio.NewWriter(of)
defer w.Flush()
}
predicates := make([]binlog.LogEventFilterFunc, 0)
if showQueryRe != nil {
predicates = append(predicates, func(off binlog.EventOffset, ev event.LogEvent) bool {
if ev.EventHeader().EventTypeCode() == spec.QUERY_EVENT {
queryEv := ev.EventData().(*event.QueryEvent)
return showQueryRe.MatchString(queryEv.Query.String())
} else if ev.EventHeader().EventTypeCode() == spec.ROWS_QUERY_LOG_EVENT {
queryEv := ev.EventData().(*event.RowsQueryEvent)
return showQueryRe.MatchString(queryEv.Query.String())
}
return true
})
}
tables := make(map[uint64]*event.TableMapEvent)
recordTables := showRowsSchemaRe != nil || showRowsTableRe != nil || showDecodeRows
if !showBinlogScanHeaderOnly {
if recordTables {
predicates = append(predicates,
// Filter and record table.
func(off binlog.EventOffset, ev event.LogEvent) bool {
if ev.EventHeader().EventTypeCode() == spec.TABLE_MAP_EVENT {
if tableMapEvent, ok := ev.EventData().(*event.TableMapEvent); ok {
if showRowsSchemaRe != nil && !showRowsSchemaRe.MatchString(tableMapEvent.Schema.String()) {
tables[tableMapEvent.TableID] = nil
return false
} else if showRowsTableRe != nil && !showRowsTableRe.MatchString(tableMapEvent.Table.String()) {
tables[tableMapEvent.TableID] = nil
return false
} else {
tables[tableMapEvent.TableID] = tableMapEvent
return true
}
}
}
return true
},
// Filter rows event.
func(off binlog.EventOffset, ev event.LogEvent) bool {
if rowsEvent, ok := ev.EventData().(event.RowsEventVariants); ok {
table, ok := tables[rowsEvent.AsRowsEvent().TableID]
// Table not recorded or table not filtered.
return !ok || table != nil
}
return true
},
)
}
}
var s binlog.LogEventScanner = lazyScanner
if len(predicates) > 0 {
s = binlog.NewFilterLogEventScanner(s, predicates...)
}
if !showBinlogScanHeaderOnly {
if showDecodeRows {
s = binlog.NewMutateLogEventScanner(s, func(off binlog.EventOffset, ev event.LogEvent) (event.LogEvent, error) {
// errTableFilteredOut should never happen
return decodeRowsEvents(tables, ev)
})
}
if showRowsPredicate != "" {
s = binlog.NewFilterLogEventScanner(s, func(off binlog.EventOffset, ev event.LogEvent) bool {
if rowsEvent, ok := ev.EventData().(rows.DecodedRowsEvent); ok {
rows := rowsEvent.GetDecodedRows()
for _, row := range rows {
ok, err := matchesRow(row, rowsEvent.AsRowsEvent().Columns, showRowsPredicateKeyValues)
if err == nil && ok {
return true
}
}
return false
}
return true
})
}
var afterPredicates []binlog.LogEventFilterFunc
if recordTables {
afterPredicates = append(afterPredicates, // Clean table if it's an end statement.
func(off binlog.EventOffset, ev event.LogEvent) bool {
if rowsEvent, ok := ev.EventData().(event.RowsEventVariants); ok {
if rowsEvent.AsRowsEvent().Flags&spec.STMT_END_F > 0 {
tables = make(map[uint64]*event.TableMapEvent)
}
}
return true
})
}
if !showTableMapEvents {
afterPredicates = append(afterPredicates,
func(off binlog.EventOffset, ev event.LogEvent) bool {
return ev.EventHeader().EventTypeCode() != spec.TABLE_MAP_EVENT
})
}
if len(afterPredicates) > 0 {
s = binlog.NewFilterLogEventScanner(s, afterPredicates...)
}
}
if showBinlogPrecede > 0 {
var pivotOff binlog.EventOffset
var pivotEv event.LogEvent
s = binlog.NewMultiLogEventScanner(
// Events before start offset.
binlog.NewTailLogEventScanner(
binlog.NewMutateLogEventScanner(s, func(off binlog.EventOffset, ev event.LogEvent) (event.LogEvent, error) {
if off.Offset-uint64(ev.EventHeader().TotalEventLength()) >= showBinlogStartOffset {
pivotOff, pivotEv = off, ev
return nil, binlog.EOF
}
return ev, nil
}),
showBinlogPrecede,
),
// Captured pivot event.
binlog.NewOneEventScanner(&pivotOff, &pivotEv),
// Events afterwards.
s,
)
}
if showBinlogSkip > 0 || showBinlogCount >= 0 {
s = binlog.NewLimitedLogEventScanner(s, showBinlogSkip, showBinlogCount)
}
if showBinlogTailCount >= 0 {
s = binlog.NewTailLogEventScanner(s, showBinlogTailCount)
}
for {
off, ev, err := s.Next()
if err != nil {
if err == binlog.EOF {
break
}
return err
}
if showEndTimestamp > 0 && ev.EventHeader().EventTimestamp() > showEndTimestamp {
return nil
}
// Print event.
if !showNoOutput {
if err = showPrintEvent(w, off, ev); err != nil {
return err
}
}
}
return nil
}),
}
func parsePredicate(predicate string) (map[int]string, error) {
m := make(map[int]string)
for _, s := range strings.Split(predicate, ",") {
s = strings.TrimSpace(s)
kv := strings.SplitN(s, "=", 2)
if len(kv) != 2 {
return nil, errors.New("invalid predicate \"" + s + "\"")
}
if !strings.HasPrefix(kv[0], "@") {
return nil, errors.New("invalid predicate \"" + s + "\"")
}
i, err := strconv.Atoi(kv[0][1:])
if err != nil {
return nil, fmt.Errorf("invalid predicate \"%s\": %w", s, err)
}
if _, ok := m[i-1]; ok {
return nil, fmt.Errorf("invalid predicate \"%s\": duplicate column index", s)
}
m[i-1] = kv[1]
}
return m, nil
}
func matchesColumn(value any, expect string) (bool, error) {
if value == nil {
return expect == "NULL", nil
}
switch v := value.(type) {
case uint8:
e, err := strconv.ParseInt(expect, 10, 64)
if err != nil {
return false, fmt.Errorf("not parsable value: %w", err)
}
return uint64(v) == uint64(e), nil
case uint16:
e, err := strconv.ParseInt(expect, 10, 64)
if err != nil {
return false, fmt.Errorf("not parsable value: %w", err)
}
return uint64(v) == uint64(e), nil
case uint32:
e, err := strconv.ParseInt(expect, 10, 64)
if err != nil {
return false, fmt.Errorf("not parsable value: %w", err)
}
return uint64(v) == uint64(e), nil
case uint64:
e, err := strconv.ParseInt(expect, 10, 64)
if err != nil {
return false, fmt.Errorf("not parsable value: %w", err)
}
return v == uint64(e), nil
case str.Str:
return v.String() == expect, nil
case float32:
e, err := strconv.ParseFloat(expect, 64)
if err != nil {
return false, fmt.Errorf("not parsable value: %w", err)
}
return float64(v) == e, nil
case float64:
e, err := strconv.ParseFloat(expect, 64)
if err != nil {
return false, fmt.Errorf("not parsable value: %w", err)
}
return v == e, nil
default:
return false, fmt.Errorf("unsupported column value type: %t", v)
}
}
func matchesRow(row rows.Row, columns bitmap.Bitmap, expect map[int]string) (bool, error) {
j := 0
for i := 0; i < columns.Len(); i++ {
var value any
if !columns.Get(i) {
value = nil
} else {
value = row[j].Value
j++
}
v, ok := expect[i]
if !ok {
continue
}
ok, err := matchesColumn(value, v)
if err != nil {
return false, err
}
if !ok {
return false, nil
}
}
return true, nil
}
func showPrintEvent(w io.Writer, off binlog.EventOffset, ev event.LogEvent) error {
var err error
if showBinlogIncludeHeader {
_, err = fmt.Fprintf(w, "[%s] Length=%d StartPos=%d EndPos=%d ServerID=%d Timestamp=%d %s\n",
ev.EventHeader().EventType(),
ev.EventHeader().TotalEventLength(),
off.Offset-uint64(ev.EventHeader().TotalEventLength()),
off.Offset,
ev.EventHeader().EventServerID(),
ev.EventHeader().EventTimestamp(),
utils.JsonPrettyFormat(ev))
} else if showBinlogScanHeaderOnly || showBinlogPrintHeaderOnly {
_, err = fmt.Fprintf(w, "[%s] Length=%d StartPos=%d EndPos=%d ServerID=%d Timestamp=%d\n",
ev.EventHeader().EventType(),
ev.EventHeader().TotalEventLength(),
off.Offset-uint64(ev.EventHeader().TotalEventLength()),
off.Offset,
ev.EventHeader().EventServerID(),
ev.EventHeader().EventTimestamp(),
)
} else {
_, err = fmt.Fprintf(w, "[%s] Length=%d StartPos=%d EndPos=%d ServerID=%d Timestamp=%d %s\n",
ev.EventHeader().EventType(),
ev.EventHeader().TotalEventLength(),
off.Offset-uint64(ev.EventHeader().TotalEventLength()),
off.Offset,
ev.EventHeader().EventServerID(),
ev.EventHeader().EventTimestamp(),
utils.JsonPrettyFormat(ev.EventData()))
}
return err
}
var (
errTableNotFound = errors.New("table not found")
errTableFilteredOut = errors.New("table filtered out")
)
func decodeRowsEvents(tables map[uint64]*event.TableMapEvent, ev event.LogEvent) (event.LogEvent, error) {
var table *event.TableMapEvent
if rowsEvent, ok := ev.EventData().(event.RowsEventVariants); ok {
table, ok = tables[rowsEvent.AsRowsEvent().TableID]
if !ok {
return nil, errTableNotFound
}
if table == nil {
return ev, errTableFilteredOut
}
} else {
return ev, nil
}
switch ev.EventHeader().EventTypeCode() {
case spec.PRE_GA_DELETE_ROWS_EVENT, spec.DELETE_ROWS_EVENT_V1, spec.DELETE_ROWS_EVENT_V2:
rowsEvent := ev.EventData().(*event.DeleteRowsEvent)
body, err := rows.DecodeDeleteRowsEvent(rowsEvent, table)
if err != nil {
return nil, fmt.Errorf("decode rows error: %w", err)
}
ev, _ = event.NewLogEvent(ev.EventHeader(), *body)
return ev, nil
case spec.PRE_GA_WRITE_ROWS_EVENT, spec.WRITE_ROWS_EVENT_V1, spec.WRITE_ROWS_EVENT_V2:
rowsEvent := ev.EventData().(*event.WriteRowsEvent)
body, err := rows.DecodeWriteRowsEvent(rowsEvent, table)
if err != nil {
return nil, fmt.Errorf("decode rows error: %w", err)
}
ev, _ = event.NewLogEvent(ev.EventHeader(), *body)
return ev, nil
case spec.PRE_GA_UPDATE_ROWS_EVENT, spec.UPDATE_ROWS_EVENT_V1, spec.UPDATE_ROWS_EVENT_V2:
rowsEvent := ev.EventData().(*event.UpdateRowsEvent)
body, err := rows.DecodeUpdateRowsEvent(rowsEvent, table)
if err != nil {
return nil, fmt.Errorf("decode rows error: %w", err)
}
ev, _ = event.NewLogEvent(ev.EventHeader(), *body)
return ev, nil
default:
return ev, nil
}
}