From b4cad31e7782fbc03382ae0642995d305f4bad58 Mon Sep 17 00:00:00 2001 From: Yoann La Cancellera Date: Wed, 30 Aug 2023 19:32:50 +0200 Subject: [PATCH] Add pt-galera-log-explainer --- go.mod | 8 +- go.sum | 13 + src/go/pt-galera-log-explainer/README.md | 120 ++ src/go/pt-galera-log-explainer/conflicts.go | 72 + src/go/pt-galera-log-explainer/ctx.go | 39 + .../display/timelinecli.go | 352 +++++ .../display/timelinecli_test.go | 186 +++ src/go/pt-galera-log-explainer/example.png | Bin 0 -> 196876 bytes .../example_conflicts.png | Bin 0 -> 60861 bytes src/go/pt-galera-log-explainer/list.go | 80 + src/go/pt-galera-log-explainer/main.go | 283 ++++ .../regex/applicative.go | 192 +++ src/go/pt-galera-log-explainer/regex/date.go | 117 ++ .../regex/date_test.go | 44 + .../pt-galera-log-explainer/regex/events.go | 179 +++ src/go/pt-galera-log-explainer/regex/file.go | 33 + .../regex/file_test.go | 47 + .../pt-galera-log-explainer/regex/idents.go | 212 +++ .../pt-galera-log-explainer/regex/operator.go | 55 + src/go/pt-galera-log-explainer/regex/regex.go | 72 + .../regex/regex_test.go | 1286 +++++++++++++++++ src/go/pt-galera-log-explainer/regex/sst.go | 310 ++++ .../pt-galera-log-explainer/regex/states.go | 44 + src/go/pt-galera-log-explainer/regex/views.go | 257 ++++ src/go/pt-galera-log-explainer/regexList.go | 39 + src/go/pt-galera-log-explainer/sed.go | 110 ++ .../types/conflicts.go | 57 + src/go/pt-galera-log-explainer/types/ctx.go | 281 ++++ .../pt-galera-log-explainer/types/loginfo.go | 97 ++ .../types/loginfo_test.go | 53 + .../pt-galera-log-explainer/types/nodeinfo.go | 12 + src/go/pt-galera-log-explainer/types/regex.go | 91 ++ src/go/pt-galera-log-explainer/types/sst.go | 15 + .../pt-galera-log-explainer/types/timeline.go | 178 +++ .../types/timeline_test.go | 261 ++++ src/go/pt-galera-log-explainer/types/utils.go | 56 + src/go/pt-galera-log-explainer/utils/utils.go | 123 ++ .../utils/utils_test.go | 42 + src/go/pt-galera-log-explainer/whois.go | 102 ++ 39 files changed, 5517 insertions(+), 1 deletion(-) create mode 100644 src/go/pt-galera-log-explainer/README.md create mode 100644 src/go/pt-galera-log-explainer/conflicts.go create mode 100644 src/go/pt-galera-log-explainer/ctx.go create mode 100644 src/go/pt-galera-log-explainer/display/timelinecli.go create mode 100644 src/go/pt-galera-log-explainer/display/timelinecli_test.go create mode 100644 src/go/pt-galera-log-explainer/example.png create mode 100644 src/go/pt-galera-log-explainer/example_conflicts.png create mode 100644 src/go/pt-galera-log-explainer/list.go create mode 100644 src/go/pt-galera-log-explainer/main.go create mode 100644 src/go/pt-galera-log-explainer/regex/applicative.go create mode 100644 src/go/pt-galera-log-explainer/regex/date.go create mode 100644 src/go/pt-galera-log-explainer/regex/date_test.go create mode 100644 src/go/pt-galera-log-explainer/regex/events.go create mode 100644 src/go/pt-galera-log-explainer/regex/file.go create mode 100644 src/go/pt-galera-log-explainer/regex/file_test.go create mode 100644 src/go/pt-galera-log-explainer/regex/idents.go create mode 100644 src/go/pt-galera-log-explainer/regex/operator.go create mode 100644 src/go/pt-galera-log-explainer/regex/regex.go create mode 100644 src/go/pt-galera-log-explainer/regex/regex_test.go create mode 100644 src/go/pt-galera-log-explainer/regex/sst.go create mode 100644 src/go/pt-galera-log-explainer/regex/states.go create mode 100644 src/go/pt-galera-log-explainer/regex/views.go create mode 100644 src/go/pt-galera-log-explainer/regexList.go create mode 100644 src/go/pt-galera-log-explainer/sed.go create mode 100644 src/go/pt-galera-log-explainer/types/conflicts.go create mode 100644 src/go/pt-galera-log-explainer/types/ctx.go create mode 100644 src/go/pt-galera-log-explainer/types/loginfo.go create mode 100644 src/go/pt-galera-log-explainer/types/loginfo_test.go create mode 100644 src/go/pt-galera-log-explainer/types/nodeinfo.go create mode 100644 src/go/pt-galera-log-explainer/types/regex.go create mode 100644 src/go/pt-galera-log-explainer/types/sst.go create mode 100644 src/go/pt-galera-log-explainer/types/timeline.go create mode 100644 src/go/pt-galera-log-explainer/types/timeline_test.go create mode 100644 src/go/pt-galera-log-explainer/types/utils.go create mode 100644 src/go/pt-galera-log-explainer/utils/utils.go create mode 100644 src/go/pt-galera-log-explainer/utils/utils_test.go create mode 100644 src/go/pt-galera-log-explainer/whois.go diff --git a/go.mod b/go.mod index c6040c31..b28d5927 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,14 @@ go 1.21 require ( github.com/AlekSi/pointer v1.2.0 + github.com/Ladicle/tabwriter v1.0.0 github.com/Masterminds/semver v1.5.0 github.com/alecthomas/kingpin v2.2.6+incompatible github.com/alecthomas/kong v0.8.0 + github.com/davecgh/go-spew v1.1.1 github.com/go-ini/ini v1.67.0 github.com/golang/mock v1.6.0 + github.com/google/go-cmp v0.5.9 github.com/google/uuid v1.3.1 github.com/hashicorp/go-version v1.6.0 github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef @@ -18,6 +21,7 @@ require ( github.com/pborman/getopt v1.1.0 github.com/percona/go-mysql v0.0.0-20210427141028-73d29c6da78c github.com/pkg/errors v0.9.1 + github.com/rs/zerolog v1.30.0 github.com/shirou/gopsutil v3.21.11+incompatible github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 @@ -25,6 +29,7 @@ require ( golang.org/x/crypto v0.13.0 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 + gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.28.2 ) @@ -39,6 +44,8 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.16.3 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -55,7 +62,6 @@ require ( golang.org/x/term v0.12.0 // indirect golang.org/x/text v0.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apimachinery v0.28.2 // indirect k8s.io/klog/v2 v2.100.1 // indirect diff --git a/go.sum b/go.sum index 12e99dfa..03030eee 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= +github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg= +github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0= @@ -14,6 +16,7 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -24,6 +27,7 @@ github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= @@ -58,6 +62,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -78,6 +86,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= +github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -151,6 +162,8 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/src/go/pt-galera-log-explainer/README.md b/src/go/pt-galera-log-explainer/README.md new file mode 100644 index 00000000..a17b5821 --- /dev/null +++ b/src/go/pt-galera-log-explainer/README.md @@ -0,0 +1,120 @@ +[![Go Report Card](https://goreportcard.com/badge/github.com/ylacancellera/galera-log-explainer)](https://goreportcard.com/report/github.com/ylacancellera/galera-log-explainer) + +# pt-galera-log-explainer + +Filter, aggregate and summarize multiple galera logs together. + + +## Features + +* List events in chronological order from any number of nodes +* List key points of information from logs (sst, view changes, general errors, maintenance operations) +* Translate advanced Galera information to a easily readable counterpart +* Filter on dates with --since, --until +* Filter on type of events +* Aggregates rotated logs together, even when there are logs from multiple nodes + +

+Get the latest cluster changes on a local server +```sh +pt-galera-log-explainer list --all --since 2023-01-05T03:24:26.000000Z /var/log/mysql/*.log +``` + +Or gather every log files and compile them +```sh +pt-galera-log-explainer list --all *.log +``` +![example](example.png) + +

+Find out information about nodes, using any type of info +```sh +pt-galera-log-explainer whois '218469b2' mysql.log +{ + "input": "218469b2", + "IPs": [ + "172.17.0.3" + ], + "nodeNames": [ + "galera-node2" + ], + "hostname": "", + "nodeUUIDs:": [ + "218469b2", + "259b78a0", + "fa81213d", + ] +} +``` + +You can find information from UUIDs, IPs, node names +``` +pt-galera-log-explainer whois '172.17.0.3' mysql.log + +pt-galera-log-explainer whois 'galera-node2' mysql.log +``` +

+List every replication failures (Galera 4) +```sh +pt-galera-log-explainer conflicts [--json|--yaml] *.log +``` +![conflicts example](example_conflicts.png) + +

+ +Automatically translate every information (IP, UUID) from a log +``` +pt-galera-log-explainer sed some/log.log another/one.log to_translate.log < to_translate.log | less + +cat to_translate.log | pt-galera-log-explainer sed some/log.log another/one.log to_translate.log | less +``` +Or get the raw `sed` command to do it yourself +``` +pt-galera-log-explainer sed some/log.log another/one.log to_translate.log +``` +

+Usage: +``` +Usage: pt-galera-log-explainer + +An utility to merge and help analyzing Galera logs + +Flags: + -h, --help Show context-sensitive help. + --no-color + --since=SINCE Only list events after this date, format: 2023-01-23T03:53:40Z (RFC3339) + --until=UNTIL Only list events before this date + -v, --verbosity=1 -v: Detailed (default), -vv: DebugMySQL (add every mysql info the tool used), + -vvv: Debug (internal tool debug) + --pxc-operator Analyze logs from Percona PXC operator. Off by default because it negatively + impacts performance for non-k8s setups + --exclude-regexes=EXCLUDE-REGEXES,... + Remove regexes from analysis. List regexes using 'pt-galera-log-explainer + regex-list' + --grep-cmd="grep" 'grep' command path. Could need to be set to 'ggrep' for darwin systems + --grep-args="-P" 'grep' arguments. perl regexp (-P) is necessary. -o will break the tool + +Commands: + list ... + + whois ... + + sed ... + + ctx ... + + regex-list + + version + + conflicts ... + +Run "pt-galera-log-explainer --help" for more information on a command. +``` + + +## Compatibility + +* Percona XtraDB Cluster: 5.5 to 8.0 +* MariaDB Galera Cluster: 10.0 to 10.6 +* Galera logs from K8s pods diff --git a/src/go/pt-galera-log-explainer/conflicts.go b/src/go/pt-galera-log-explainer/conflicts.go new file mode 100644 index 00000000..310d10c8 --- /dev/null +++ b/src/go/pt-galera-log-explainer/conflicts.go @@ -0,0 +1,72 @@ +package main + +import ( + "encoding/json" + "fmt" + + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/regex" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/utils" + "gopkg.in/yaml.v2" +) + +type conflicts struct { + Paths []string `arg:"" name:"paths" help:"paths of the log to use"` + Yaml bool `xor:"format"` + Json bool `xor:"format"` +} + +func (c *conflicts) Help() string { + return "Summarize every replication conflicts, from every node's point of view" +} + +func (c *conflicts) Run() error { + + regexes := regex.IdentsMap.Merge(regex.ApplicativeMap) + timeline, err := timelineFromPaths(c.Paths, regexes, CLI.Since, CLI.Until) + if err != nil { + return err + } + + ctxs := timeline.GetLatestUpdatedContextsByNodes() + for _, ctx := range ctxs { + if len(ctx.Conflicts) == 0 { + continue + } + var out string + + if c.Yaml { + tmp, err := yaml.Marshal(ctx.Conflicts) + if err != nil { + return err + } + out = string(tmp) + } else if c.Json { + tmp, err := json.Marshal(ctx.Conflicts) + if err != nil { + return err + } + out = string(tmp) + } else { + + for _, conflict := range ctx.Conflicts { + out += "\n" + out += "\n" + utils.Paint(utils.BlueText, "seqno: ") + conflict.Seqno + out += "\n\t" + utils.Paint(utils.BlueText, "winner: ") + conflict.Winner + out += "\n\t" + utils.Paint(utils.BlueText, "votes per nodes:") + for node, vote := range conflict.VotePerNode { + displayVote := utils.Paint(utils.RedText, vote.MD5) + if vote.MD5 == conflict.Winner { + displayVote = utils.Paint(utils.GreenText, vote.MD5) + } + out += "\n\t\t" + utils.Paint(utils.BlueText, node) + ": (" + displayVote + ") " + vote.Error + } + out += "\n\t" + utils.Paint(utils.BlueText, "initiated by: ") + fmt.Sprintf("%v", conflict.InitiatedBy) + } + + } + fmt.Println(out) + return nil + } + + return nil +} diff --git a/src/go/pt-galera-log-explainer/ctx.go b/src/go/pt-galera-log-explainer/ctx.go new file mode 100644 index 00000000..3cf4ab1a --- /dev/null +++ b/src/go/pt-galera-log-explainer/ctx.go @@ -0,0 +1,39 @@ +package main + +import ( + "encoding/json" + "fmt" + + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/regex" + "github.com/pkg/errors" +) + +type ctx struct { + Paths []string `arg:"" name:"paths" help:"paths of the log to use"` +} + +func (c *ctx) Help() string { + return "Dump the context derived from the log" +} + +func (c *ctx) Run() error { + + if len(c.Paths) != 1 { + return errors.New("Can only use 1 path at a time for ctx subcommand") + } + + timeline, err := timelineFromPaths(c.Paths, regex.AllRegexes(), CLI.Since, CLI.Until) + if err != nil { + return err + } + + for _, t := range timeline { + out, err := json.MarshalIndent(t[len(t)-1].Ctx, "", "\t") + if err != nil { + return err + } + fmt.Println(string(out)) + } + + return nil +} diff --git a/src/go/pt-galera-log-explainer/display/timelinecli.go b/src/go/pt-galera-log-explainer/display/timelinecli.go new file mode 100644 index 00000000..6d6f92c1 --- /dev/null +++ b/src/go/pt-galera-log-explainer/display/timelinecli.go @@ -0,0 +1,352 @@ +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 +} diff --git a/src/go/pt-galera-log-explainer/display/timelinecli_test.go b/src/go/pt-galera-log-explainer/display/timelinecli_test.go new file mode 100644 index 00000000..4c37dcde --- /dev/null +++ b/src/go/pt-galera-log-explainer/display/timelinecli_test.go @@ -0,0 +1,186 @@ +package display + +import ( + "testing" + + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/types" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/utils" +) + +func TestTransitionSeparator(t *testing.T) { + tests := []struct { + keys []string + oldctxs, ctxs map[string]types.LogCtx + expectedOut string + name string + }{ + { + name: "no changes", + keys: []string{"node0", "node1"}, + oldctxs: map[string]types.LogCtx{ + "node0": {}, + "node1": {}, + }, + ctxs: map[string]types.LogCtx{ + + "node0": {}, + "node1": {}, + }, + expectedOut: "", + }, + { + name: "filepath changed on node0", + keys: []string{"node0", "node1"}, + oldctxs: map[string]types.LogCtx{ + "node0": {FilePath: "path1"}, + "node1": {}, + }, + ctxs: map[string]types.LogCtx{ + + "node0": {FilePath: "path2"}, + "node1": {}, + }, + /* + path1 + (file path) + V + path2 + */ + expectedOut: "\tpath1\t\t\n\t(file path)\t\t\n\t V \t\t\n\tpath2\t\t", + }, + { + name: "filepath changed on node1", + keys: []string{"node0", "node1"}, + oldctxs: map[string]types.LogCtx{ + "node0": {}, + "node1": {FilePath: "path1"}, + }, + ctxs: map[string]types.LogCtx{ + + "node0": {}, + "node1": {FilePath: "path2"}, + }, + expectedOut: "\t\tpath1\t\n\t\t(file path)\t\n\t\t V \t\n\t\tpath2\t", + }, + { + name: "filepath changed on both", + keys: []string{"node0", "node1"}, + oldctxs: map[string]types.LogCtx{ + "node0": {FilePath: "path1_0"}, + "node1": {FilePath: "path1_1"}, + }, + ctxs: map[string]types.LogCtx{ + "node0": {FilePath: "path2_0"}, + "node1": {FilePath: "path2_1"}, + }, + expectedOut: "\tpath1_0\tpath1_1\t\n\t(file path)\t(file path)\t\n\t V \t V \t\n\tpath2_0\tpath2_1\t", + }, + { + name: "node name changed on node1", + keys: []string{"node0", "node1"}, + oldctxs: map[string]types.LogCtx{ + "node0": {}, + "node1": {OwnNames: []string{"name1"}}, + }, + ctxs: map[string]types.LogCtx{ + "node0": {}, + "node1": {OwnNames: []string{"name1", "name2"}}, + }, + expectedOut: "\t\tname1\t\n\t\t(node name)\t\n\t\t V \t\n\t\tname2\t", + }, + { + name: "node ip changed on node1", + keys: []string{"node0", "node1"}, + oldctxs: map[string]types.LogCtx{ + "node0": {}, + "node1": {OwnIPs: []string{"ip1"}}, + }, + ctxs: map[string]types.LogCtx{ + "node0": {}, + "node1": {OwnIPs: []string{"ip1", "ip2"}}, + }, + expectedOut: "\t\tip1\t\n\t\t(node ip)\t\n\t\t V \t\n\t\tip2\t", + }, + { + name: "version changed on node1", + keys: []string{"node0", "node1"}, + oldctxs: map[string]types.LogCtx{ + "node0": {}, + "node1": {Version: "8.0.28"}, + }, + ctxs: map[string]types.LogCtx{ + "node0": {}, + "node1": {Version: "8.0.30"}, + }, + expectedOut: "\t\t8.0.28\t\n\t\t(version)\t\n\t\t V \t\n\t\t8.0.30\t", + }, + { + name: "node ip, node name and filepath changed on node1", // very possible with operators + keys: []string{"node0", "node1"}, + oldctxs: map[string]types.LogCtx{ + "node0": {}, + "node1": {OwnIPs: []string{"ip1"}, OwnNames: []string{"name1"}, FilePath: "path1"}, + }, + ctxs: map[string]types.LogCtx{ + "node0": {}, + "node1": {OwnIPs: []string{"ip1", "ip2"}, OwnNames: []string{"name1", "name2"}, FilePath: "path2"}, + }, + /* + (timestamp) (node0) (node1) + \t \t path1 \t\n + \t \t (file path) \t\n + \t \t V \t\n + \t \t path2 \t\n + \t \t name1 \t\n + \t \t (node name) \t\n + \t \t V \t\n + \t \t name2 \t\n + \t \t ip2 \t\n + \t \t (node ip) \t\n + \t \t V \t\n + \t \t ip2 \t --only one without \n + + */ + expectedOut: "\t\tpath1\t\n\t\t(file path)\t\n\t\t V \t\n\t\tpath2\t\n\t\tname1\t\n\t\t(node name)\t\n\t\t V \t\n\t\tname2\t\n\t\tip1\t\n\t\t(node ip)\t\n\t\t V \t\n\t\tip2\t", + }, + + { + name: "node ip, node name and filepath changed on node1, nodename changed on node2", // very possible with operators + keys: []string{"node0", "node1"}, + oldctxs: map[string]types.LogCtx{ + "node0": {OwnNames: []string{"name1_0"}}, + "node1": {OwnIPs: []string{"ip1"}, OwnNames: []string{"name1_1"}, FilePath: "path1"}, + }, + ctxs: map[string]types.LogCtx{ + "node0": {OwnNames: []string{"name1_0", "name2_0"}}, + "node1": {OwnIPs: []string{"ip1", "ip2"}, OwnNames: []string{"name1_1", "name2_1"}, FilePath: "path2"}, + }, + /* + (timestamp) (node0) (node1) + \t name1_0\t path1 \t\n + \t (node name)\t (file path) \t\n + \t V \t V \t\n + \t name2_0\t path2 \t\n + \t \t name1_1 \t\n + \t \t (node name) \t\n + \t \t V \t\n + \t \t name2_1 \t\n + \t \t ip2 \t\n + \t \t (node ip) \t\n + \t \t V \t\n + \t \t ip2 \t --only one without \n + + */ + expectedOut: "\tname1_0\tpath1\t\n\t(node name)\t(file path)\t\n\t V \t V \t\n\tname2_0\tpath2\t\n\t\tname1_1\t\n\t\t(node name)\t\n\t\t V \t\n\t\tname2_1\t\n\t\tip1\t\n\t\t(node ip)\t\n\t\t V \t\n\t\tip2\t", + }, + } + + utils.SkipColor = true + for _, test := range tests { + out := transitionSeparator(test.keys, test.oldctxs, test.ctxs) + if out != test.expectedOut { + t.Errorf("testname: %s, expected: \n%#v\n got: \n%#v", test.name, test.expectedOut, out) + } + + } +} diff --git a/src/go/pt-galera-log-explainer/example.png b/src/go/pt-galera-log-explainer/example.png new file mode 100644 index 0000000000000000000000000000000000000000..50672e5282874c66428a9dbd73cdf0e4fd31cabc GIT binary patch literal 196876 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∨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

  • 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 literal 0 HcmV?d00001 diff --git a/src/go/pt-galera-log-explainer/example_conflicts.png b/src/go/pt-galera-log-explainer/example_conflicts.png new file mode 100644 index 0000000000000000000000000000000000000000..0b437a750530d1a74421006b1f5e501aee3a64df GIT binary patch literal 60861 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 S`r#+YOIA`*qWpu=*Z&6%JC*JL literal 0 HcmV?d00001 diff --git a/src/go/pt-galera-log-explainer/list.go b/src/go/pt-galera-log-explainer/list.go new file mode 100644 index 00000000..3e29015f --- /dev/null +++ b/src/go/pt-galera-log-explainer/list.go @@ -0,0 +1,80 @@ +package main + +import ( + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/display" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/regex" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/types" + "github.com/pkg/errors" +) + +type list struct { + // Paths is duplicated because it could not work as variadic with kong cli if I set it as CLI object + Paths []string `arg:"" name:"paths" help:"paths of the log to use"` + SkipStateColoredColumn bool `help:"avoid having the placeholder colored with mysql state, which is guessed using several regexes that will not be displayed"` + All bool `help:"List everything" xor:"states,views,events,sst,applicative"` + States bool `help:"List WSREP state changes(SYNCED, DONOR, ...)" xor:"states"` + Views bool `help:"List how Galera views evolved (who joined, who left)" xor:"views"` + Events bool `help:"List generic mysql events (start, shutdown, assertion failures)" xor:"events"` + SST bool `help:"List Galera synchronization event" xor:"sst"` + Applicative bool `help:"List applicative events (resyncs, desyncs, conflicts). Events tied to one's usage of Galera" xor:"applicative"` +} + +func (l *list) Help() string { + return `List events for each nodes in a columnar output + It will merge logs between themselves + + "identifier" is an internal metadata, this is used to merge logs. + +Usage: + galera-log-explainer list --all + galera-log-explainer list --all *.log + galera-log-explainer list --sst --views --states + galera-log-explainer list --events --views *.log + ` +} + +func (l *list) Run() error { + + if !(l.All || l.Events || l.States || l.SST || l.Views || l.Applicative) { + return errors.New("Please select a type of logs to search: --all, or any parameters from: --sst --views --events --states") + } + + toCheck := l.regexesToUse() + + timeline, err := timelineFromPaths(CLI.List.Paths, toCheck, CLI.Since, CLI.Until) + if err != nil { + return errors.Wrap(err, "Could not list events") + } + + display.TimelineCLI(timeline, CLI.Verbosity) + + return nil +} + +func (l *list) regexesToUse() types.RegexMap { + + // IdentRegexes is always needed: we would not be able to identify the node where the file come from + toCheck := regex.IdentsMap + if l.States || l.All { + toCheck.Merge(regex.StatesMap) + } else if !l.SkipStateColoredColumn { + regex.SetVerbosity(types.DebugMySQL, regex.StatesMap) + toCheck.Merge(regex.StatesMap) + } + if l.Views || l.All { + toCheck.Merge(regex.ViewsMap) + } + if l.SST || l.All { + toCheck.Merge(regex.SSTMap) + } + if l.Applicative || l.All { + toCheck.Merge(regex.ApplicativeMap) + } + if l.Events || l.All { + toCheck.Merge(regex.EventsMap) + } else if !l.SkipStateColoredColumn { + regex.SetVerbosity(types.DebugMySQL, regex.EventsMap) + toCheck.Merge(regex.EventsMap) + } + return toCheck +} diff --git a/src/go/pt-galera-log-explainer/main.go b/src/go/pt-galera-log-explainer/main.go new file mode 100644 index 00000000..de5834c0 --- /dev/null +++ b/src/go/pt-galera-log-explainer/main.go @@ -0,0 +1,283 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "runtime" + "strings" + "time" + + "github.com/alecthomas/kong" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/regex" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/types" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/utils" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +// ldflags +var ( + version string + commit string + date string +) + +var CLI struct { + NoColor bool + Since *time.Time `help:"Only list events after this date, format: 2023-01-23T03:53:40Z (RFC3339)"` + Until *time.Time `help:"Only list events before this date"` + Verbosity types.Verbosity `type:"counter" short:"v" default:"1" help:"-v: Detailed (default), -vv: DebugMySQL (add every mysql info the tool used), -vvv: Debug (internal tool debug)"` + PxcOperator bool `default:"false" help:"Analyze logs from Percona PXC operator. Off by default because it negatively impacts performance for non-k8s setups"` + ExcludeRegexes []string `help:"Remove regexes from analysis. List regexes using 'pt-galera-log-explainer regex-list'"` + + List list `cmd:""` + Whois whois `cmd:""` + Sed sed `cmd:""` + Ctx ctx `cmd:""` + RegexList regexList `cmd:""` + Version versioncmd `cmd:""` + Conflicts conflicts `cmd:""` + + GrepCmd string `help:"'grep' command path. Could need to be set to 'ggrep' for darwin systems" default:"grep"` + GrepArgs string `help:"'grep' arguments. perl regexp (-P) is necessary. -o will break the tool" default:"-P"` +} + +func main() { + ctx := kong.Parse(&CLI, + kong.Name("pt-galera-log-explainer"), + kong.Description("An utility to merge and help analyzing Galera logs"), + kong.UsageOnError(), + ) + + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + zerolog.SetGlobalLevel(zerolog.WarnLevel) + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + if CLI.Verbosity == types.Debug { + zerolog.SetGlobalLevel(zerolog.DebugLevel) + } + + utils.SkipColor = CLI.NoColor + err := ctx.Run() + ctx.FatalIfErrorf(err) +} + +type versioncmd struct{} + +func (v *versioncmd) Help() string { + return "" +} +func (v *versioncmd) Run() error { + fmt.Printf("version: %s, commit:%s, built at %s\n", version, commit, date) + return nil +} + +// timelineFromPaths takes every path, search them using a list of regexes +// and organize them in a timeline that will be ready to aggregate or read +func timelineFromPaths(paths []string, toCheck types.RegexMap, since, until *time.Time) (types.Timeline, error) { + timeline := make(types.Timeline) + found := false + + for _, path := range paths { + + extr := newExtractor(path, toCheck, since, until) + + localTimeline, err := extr.search() + if err != nil { + extr.logger.Warn().Err(err).Msg("Search failed") + continue + } + found = true + extr.logger.Debug().Str("path", path).Msg("Finished searching") + + // identify the node with the easiest to read information + // this is critical part to aggregate logs: this is what enable to merge logs + // ultimately the "identifier" will be used for columns header + var node string + if CLI.PxcOperator { + node = path + + } else { + + // Why it should not just identify using the file path: + // so that we are able to merge files that belong to the same nodes + // we wouldn't want them to be shown as from different nodes + node = types.Identifier(localTimeline[len(localTimeline)-1].Ctx) + if t, ok := timeline[node]; ok { + + extr.logger.Debug().Str("path", path).Str("node", node).Msg("Merging with existing timeline") + localTimeline = types.MergeTimeline(t, localTimeline) + } + } + extr.logger.Debug().Str("path", path).Str("node", node).Msg("Storing timeline") + timeline[node] = localTimeline + + } + if !found { + return nil, errors.New("Could not find data") + } + return timeline, nil +} + +// extractor is an utility struct to store what needs to be done +type extractor struct { + regexes types.RegexMap + path string + since, until *time.Time + logger zerolog.Logger +} + +func newExtractor(path string, toCheck types.RegexMap, since, until *time.Time) extractor { + e := extractor{regexes: toCheck, path: path, since: since, until: until} + e.logger = log.With().Str("component", "extractor").Str("path", e.path).Logger() + if since != nil { + e.logger = e.logger.With().Time("since", *e.since).Logger() + } + if until != nil { + e.logger = e.logger.With().Time("until", *e.until).Logger() + } + e.logger.Debug().Msg("new extractor") + + return e +} + +func (e *extractor) grepArgument() string { + + regexToSendSlice := e.regexes.Compile() + + grepRegex := "^" + if CLI.PxcOperator { + // special case + // I'm not adding pxcoperator map the same way others are used, because they do not have the same formats and same place + // it needs to be put on the front so that it's not 'merged' with the '{"log":"' json prefix + // this is to keep things as close as '^' as possible to keep doing prefix searches + grepRegex += "((" + strings.Join(regex.PXCOperatorMap.Compile(), "|") + ")|^{\"log\":\"" + e.regexes.Merge(regex.PXCOperatorMap) + } + if e.since != nil { + grepRegex += "(" + regex.BetweenDateRegex(e.since, CLI.PxcOperator) + "|" + regex.NoDatesRegex(CLI.PxcOperator) + ")" + } + grepRegex += ".*" + grepRegex += "(" + strings.Join(regexToSendSlice, "|") + ")" + if CLI.PxcOperator { + grepRegex += ")" + } + e.logger.Debug().Str("grepArg", grepRegex).Msg("Compiled grep arguments") + return grepRegex +} + +// search is the main function to search what we want in a file +func (e *extractor) search() (types.LocalTimeline, error) { + + // A first pass is done, with every regexes we want compiled in a single one. + grepRegex := e.grepArgument() + + /* + Regular grep is actually used + + There are no great alternatives, even less as golang libraries. + grep itself do not have great alternatives: they are less performant for common use-cases, or are not easily portable, or are costlier to execute. + grep is everywhere, grep is good enough, it even enable to use the stdout pipe. + + The usual bottleneck with grep is that it is single-threaded, but we actually benefit + from a sequential scan here as we will rely on the log order. + + Also, being sequential also ensure this program is light enough to run without too much impacts + It also helps to be transparent and not provide an obscure tool that work as a blackbox + */ + if runtime.GOOS == "darwin" && CLI.GrepCmd == "grep" { + e.logger.Warn().Msg("On Darwin systems, use 'pt-galera-log-explainer --grep-cmd=ggrep' as it requires grep v3") + } + + cmd := exec.Command(CLI.GrepCmd, CLI.GrepArgs, grepRegex, e.path) + + out, _ := cmd.StdoutPipe() + defer out.Close() + + err := cmd.Start() + if err != nil { + return nil, errors.Wrapf(err, "failed to search in %s", e.path) + } + + // grep treatment + s := bufio.NewScanner(out) + + // it will iterate on stdout pipe results + lt, err := e.iterateOnResults(s) + if err != nil { + e.logger.Warn().Err(err).Msg("Failed to iterate on results") + } + + // double-check it stopped correctly + if err = cmd.Wait(); err != nil { + return nil, errors.Wrap(err, "grep subprocess error") + } + + if len(lt) == 0 { + return nil, errors.New("Found nothing") + } + + return lt, nil +} + +func (e *extractor) sanitizeLine(s string) string { + if len(s) > 0 && s[0] == '\t' { + return s[1:] + } + return s +} + +// iterateOnResults will take line by line each logs that matched regex +// it will iterate on every regexes in slice, and apply the handler for each +// it also filters out --since and --until rows +func (e *extractor) iterateOnResults(s *bufio.Scanner) ([]types.LogInfo, error) { + + var ( + line string + lt types.LocalTimeline + recentEnough bool + displayer types.LogDisplayer + ) + ctx := types.NewLogCtx() + ctx.FilePath = e.path + + for s.Scan() { + line = e.sanitizeLine(s.Text()) + + var date *types.Date + t, layout, ok := regex.SearchDateFromLog(line) + if ok { + d := types.NewDate(t, layout) + date = &d + } + + // If it's recentEnough, it means we already validated a log: every next logs necessarily happened later + // this is useful because not every logs have a date attached, and some without date are very useful + if !recentEnough && e.since != nil && (date == nil || (date != nil && e.since.After(date.Time))) { + continue + } + if e.until != nil && date != nil && e.until.Before(date.Time) { + return lt, nil + } + recentEnough = true + + filetype := regex.FileType(line, CLI.PxcOperator) + ctx.FileType = filetype + + // We have to find again what regex worked to get this log line + // it can match multiple regexes + for key, regex := range e.regexes { + if !regex.Regex.MatchString(line) || utils.SliceContains(CLI.ExcludeRegexes, key) { + continue + } + ctx, displayer = regex.Handle(ctx, line) + li := types.NewLogInfo(date, displayer, line, regex, key, ctx, filetype) + + lt = lt.Add(li) + } + + } + return lt, nil +} diff --git a/src/go/pt-galera-log-explainer/regex/applicative.go b/src/go/pt-galera-log-explainer/regex/applicative.go new file mode 100644 index 00000000..8f476c6f --- /dev/null +++ b/src/go/pt-galera-log-explainer/regex/applicative.go @@ -0,0 +1,192 @@ +package regex + +import ( + "regexp" + + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/types" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/utils" +) + +func init() { + setType(types.ApplicativeRegexType, ApplicativeMap) +} + +var ApplicativeMap = types.RegexMap{ + + "RegexDesync": &types.LogRegex{ + Regex: regexp.MustCompile("desyncs itself from group"), + InternalRegex: regexp.MustCompile("\\(" + regexNodeName + "\\) desyncs"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.Desynced = true + + node := submatches[groupNodeName] + return ctx, func(ctx types.LogCtx) string { + if utils.SliceContains(ctx.OwnNames, node) { + return utils.Paint(utils.YellowText, "desyncs itself from group") + } + return node + utils.Paint(utils.YellowText, " desyncs itself from group") + } + }, + }, + + "RegexResync": &types.LogRegex{ + Regex: regexp.MustCompile("resyncs itself to group"), + InternalRegex: regexp.MustCompile("\\(" + regexNodeName + "\\) resyncs"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.Desynced = false + node := submatches[groupNodeName] + return ctx, func(ctx types.LogCtx) string { + if utils.SliceContains(ctx.OwnNames, node) { + return utils.Paint(utils.YellowText, "resyncs itself to group") + } + return node + utils.Paint(utils.YellowText, " resyncs itself to group") + } + }, + }, + + "RegexInconsistencyVoteInit": &types.LogRegex{ + Regex: regexp.MustCompile("initiates vote on"), + InternalRegex: regexp.MustCompile("Member " + regexIdx + "\\(" + regexNodeName + "\\) initiates vote on " + regexUUID + ":" + regexSeqno + "," + regexErrorMD5 + ": (?P.*), Error_code:"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + node := submatches[groupNodeName] + seqno := submatches[groupSeqno] + errormd5 := submatches[groupErrorMD5] + errorstring := submatches["error"] + + c := types.Conflict{ + InitiatedBy: []string{node}, + Seqno: seqno, + VotePerNode: map[string]types.ConflictVote{node: types.ConflictVote{MD5: errormd5, Error: errorstring}}, + } + + ctx.Conflicts = ctx.Conflicts.Merge(c) + + return ctx, func(ctx types.LogCtx) string { + + if utils.SliceContains(ctx.OwnNames, node) { + return utils.Paint(utils.YellowText, "inconsistency vote started") + "(seqno:" + seqno + ")" + } + + return utils.Paint(utils.YellowText, "inconsistency vote started by "+node) + "(seqno:" + seqno + ")" + } + }, + }, + + "RegexInconsistencyVoteRespond": &types.LogRegex{ + Regex: regexp.MustCompile("responds to vote on "), + InternalRegex: regexp.MustCompile("Member " + regexIdx + "\\(" + regexNodeName + "\\) responds to vote on " + regexUUID + ":" + regexSeqno + "," + regexErrorMD5 + ": (?P.*)"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + node := submatches[groupNodeName] + seqno := submatches[groupSeqno] + errormd5 := submatches[groupErrorMD5] + errorstring := submatches["error"] + + latestConflict := ctx.Conflicts.ConflictWithSeqno(seqno) + if latestConflict == nil { + return ctx, nil + } + latestConflict.VotePerNode[node] = types.ConflictVote{MD5: errormd5, Error: errorstring} + + return ctx, func(ctx types.LogCtx) string { + + for _, name := range ctx.OwnNames { + vote, ok := latestConflict.VotePerNode[name] + if !ok { + continue + } + + return voteResponse(vote, *latestConflict) + } + + return "" + } + }, + }, + + "RegexInconsistencyVoted": &types.LogRegex{ + Regex: regexp.MustCompile("Inconsistency detected: Inconsistent by consensus"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "found inconsistent by vote")) + }, + }, + + "RegexInconsistencyWinner": &types.LogRegex{ + Regex: regexp.MustCompile("Winner: "), + InternalRegex: regexp.MustCompile("Winner: " + regexErrorMD5), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + errormd5 := submatches[groupErrorMD5] + + if len(ctx.Conflicts) == 0 { + return ctx, nil // nothing to guess + } + + c := ctx.Conflicts.ConflictFromMD5(errormd5) + if c == nil { + // some votes have been observed to be logged again + // sometimes days after the initial one + // the winner outcomes is not even always the initial one + + // as they don't add any helpful context, we should ignore + // plus, it would need multiline regexes, which is not supported here + return ctx, nil + } + c.Winner = errormd5 + + return ctx, func(ctx types.LogCtx) string { + out := "consistency vote(seqno:" + c.Seqno + "): " + for _, name := range ctx.OwnNames { + + vote, ok := c.VotePerNode[name] + if !ok { + continue + } + + if vote.MD5 == c.Winner { + return out + utils.Paint(utils.GreenText, "won") + } + return out + utils.Paint(utils.RedText, "lost") + } + return "" + } + }, + }, + + "RegexInconsistencyRecovery": &types.LogRegex{ + Regex: regexp.MustCompile("Recovering vote result from history"), + InternalRegex: regexp.MustCompile("Recovering vote result from history: " + regexUUID + ":" + regexSeqno + "," + regexErrorMD5), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + if len(ctx.OwnNames) == 0 { + return ctx, nil + } + + errormd5 := submatches[groupErrorMD5] + seqno := submatches[groupSeqno] + c := ctx.Conflicts.ConflictWithSeqno(seqno) + vote := types.ConflictVote{MD5: errormd5} + c.VotePerNode[ctx.OwnNames[len(ctx.OwnNames)-1]] = vote + + return ctx, types.SimpleDisplayer(voteResponse(vote, *c)) + }, + Verbosity: types.DebugMySQL, + }, +} + +func voteResponse(vote types.ConflictVote, conflict types.Conflict) string { + out := "consistency vote(seqno:" + conflict.Seqno + "): voted " + + initError := conflict.VotePerNode[conflict.InitiatedBy[0]] + switch vote.MD5 { + case "0000000000000000": + out += "Success" + case initError.MD5: + out += "same error" + default: + out += "different error" + } + + return out + +} diff --git a/src/go/pt-galera-log-explainer/regex/date.go b/src/go/pt-galera-log-explainer/regex/date.go new file mode 100644 index 00000000..973d37a5 --- /dev/null +++ b/src/go/pt-galera-log-explainer/regex/date.go @@ -0,0 +1,117 @@ +package regex + +import ( + "fmt" + "strconv" + "time" + + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/utils" + "github.com/rs/zerolog/log" +) + +// 5.5 date : 151027 6:02:49 +// 5.6 date : 2019-07-17 07:16:37 +//5.7 date : 2019-07-17T15:16:37.123456Z +//5.7 date : 2019-07-17T15:16:37.123456+01:00 +// 10.3 date: 2019-07-15 7:32:25 +var DateLayouts = []string{ + "2006-01-02T15:04:05.000000Z", // 5.7 + "2006-01-02T15:04:05.000000-07:00", // 5.7 + "060102 15:04:05", // 5.5 + "2006-01-02 15:04:05", // 5.6 + "2006-01-02 15:04:05", // 10.3, yes the extra space is needed + "2006/01/02 15:04:05", // sometimes found in socat errors +} + +// BetweenDateRegex generate a regex to filter mysql error log dates to just get +// events between 2 dates +// Currently limited to filter by day to produce "short" regexes. Finer events will be filtered later in code +// Trying to filter hours, minutes using regexes would produce regexes even harder to read +// while not really adding huge benefit as we do not expect so many events of interets +func BetweenDateRegex(since *time.Time, skipLeadingCircumflex bool) string { + /* + "2006-01-02 + "2006-01-0[3-9] + "2006-01-[1-9][0-9] + "2006-0[2-9]-[0-9]{2} + "2006-[1-9][0-9]-[0-9]{2} + "200[7-9]-[0-9]{2}-[0-9]{2} + "20[1-9][0-9]-[0-9]{2}-[0-9]{2} + */ + + separator := "|^" + if skipLeadingCircumflex { + separator = "|" + } + + regexConstructor := []struct { + unit int + unitToStr string + }{ + { + unit: since.Day(), + unitToStr: fmt.Sprintf("%02d", since.Day()), + }, + { + unit: int(since.Month()), + unitToStr: fmt.Sprintf("%02d", since.Month()), + }, + { + unit: since.Year(), + unitToStr: fmt.Sprintf("%d", since.Year())[2:], + }, + } + s := "" + for _, layout := range []string{"2006-01-02", "060102"} { + // base complete date + lastTransformed := since.Format(layout) + s += separator + lastTransformed + + for _, construct := range regexConstructor { + if construct.unit != 9 { + s += separator + utils.StringsReplaceReversed(lastTransformed, construct.unitToStr, string(construct.unitToStr[0])+"["+strconv.Itoa(construct.unit%10+1)+"-9]", 1) + } + // %1000 here is to cover the transformation of 2022 => 22 + s += separator + utils.StringsReplaceReversed(lastTransformed, construct.unitToStr, "["+strconv.Itoa((construct.unit%1000/10)+1)+"-9][0-9]", 1) + + lastTransformed = utils.StringsReplaceReversed(lastTransformed, construct.unitToStr, "[0-9][0-9]", 1) + + } + } + s += ")" + return "(" + s[1:] +} + +// basically capturing anything that does not have a date +// needed, else we would miss some logs, like wsrep recovery +func NoDatesRegex(skipLeadingCircumflex bool) string { + //return "((?![0-9]{4}-[0-9]{2}-[0-9]{2})|(?![0-9]{6}))" + if skipLeadingCircumflex { + return "(?![0-9]{4})" + } + return "^(?![0-9]{4})" +} + +/* +SYSLOG_DATE="\(Jan\|Feb\|Mar\|Apr\|May\|Jun\|Jul\|Aug\|Sep\|Oct\|Nov\|Dec\) \( \|[0-9]\)[0-9] [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}" +REGEX_LOG_PREFIX="$REGEX_DATE \?[0-9]* " +*/ + +const k8sprefix = `{"log":"` + +func SearchDateFromLog(logline string) (time.Time, string, bool) { + if logline[:len(k8sprefix)] == k8sprefix { + logline = logline[len(k8sprefix):] + } + for _, layout := range DateLayouts { + if len(logline) < len(layout) { + continue + } + t, err := time.Parse(layout, logline[:len(layout)]) + if err == nil { + return t, layout, true + } + } + log.Debug().Str("log", logline).Msg("could not find date from log") + return time.Time{}, "", false +} diff --git a/src/go/pt-galera-log-explainer/regex/date_test.go b/src/go/pt-galera-log-explainer/regex/date_test.go new file mode 100644 index 00000000..0778e315 --- /dev/null +++ b/src/go/pt-galera-log-explainer/regex/date_test.go @@ -0,0 +1,44 @@ +package regex + +import ( + "testing" + "time" +) + +func TestDateAfter(t *testing.T) { + tests := []struct { + input string + format string + expected string + }{ + { + input: "2006-01-02T15:04:05Z", + format: time.RFC3339, + expected: "(^2006-01-02|^2006-01-0[3-9]|^2006-01-[1-9][0-9]|^2006-0[2-9]-[0-9][0-9]|^2006-[1-9][0-9]-[0-9][0-9]|^200[7-9]-[0-9][0-9]-[0-9][0-9]|^20[1-9][0-9]-[0-9][0-9]-[0-9][0-9]|^060102|^06010[3-9]|^0601[1-9][0-9]|^060[2-9][0-9][0-9]|^06[1-9][0-9][0-9][0-9]|^0[7-9][0-9][0-9][0-9][0-9]|^[1-9][0-9][0-9][0-9][0-9][0-9])", + }, + { + input: "2006-01-02", + format: "2006-01-02", + expected: "(^2006-01-02|^2006-01-0[3-9]|^2006-01-[1-9][0-9]|^2006-0[2-9]-[0-9][0-9]|^2006-[1-9][0-9]-[0-9][0-9]|^200[7-9]-[0-9][0-9]-[0-9][0-9]|^20[1-9][0-9]-[0-9][0-9]-[0-9][0-9]|^060102|^06010[3-9]|^0601[1-9][0-9]|^060[2-9][0-9][0-9]|^06[1-9][0-9][0-9][0-9]|^0[7-9][0-9][0-9][0-9][0-9]|^[1-9][0-9][0-9][0-9][0-9][0-9])", + }, + { + input: "060102", + format: "060102", + expected: "(^2006-01-02|^2006-01-0[3-9]|^2006-01-[1-9][0-9]|^2006-0[2-9]-[0-9][0-9]|^2006-[1-9][0-9]-[0-9][0-9]|^200[7-9]-[0-9][0-9]-[0-9][0-9]|^20[1-9][0-9]-[0-9][0-9]-[0-9][0-9]|^060102|^06010[3-9]|^0601[1-9][0-9]|^060[2-9][0-9][0-9]|^06[1-9][0-9][0-9][0-9]|^0[7-9][0-9][0-9][0-9][0-9]|^[1-9][0-9][0-9][0-9][0-9][0-9])", + }, + { + input: "2022-01-22", + format: "2006-01-02", + expected: "(^2022-01-22|^2022-01-2[3-9]|^2022-01-[3-9][0-9]|^2022-0[2-9]-[0-9][0-9]|^2022-[1-9][0-9]-[0-9][0-9]|^202[3-9]-[0-9][0-9]-[0-9][0-9]|^20[3-9][0-9]-[0-9][0-9]-[0-9][0-9]|^220122|^22012[3-9]|^2201[3-9][0-9]|^220[2-9][0-9][0-9]|^22[1-9][0-9][0-9][0-9]|^2[3-9][0-9][0-9][0-9][0-9]|^[3-9][0-9][0-9][0-9][0-9][0-9])", + }, + } + + for _, test := range tests { + d, _ := time.Parse(test.format, test.input) + s := BetweenDateRegex(&d, false) + if s != test.expected { + t.Log("wrong date regex:", s) + t.Fail() + } + } +} diff --git a/src/go/pt-galera-log-explainer/regex/events.go b/src/go/pt-galera-log-explainer/regex/events.go new file mode 100644 index 00000000..a0c80ba7 --- /dev/null +++ b/src/go/pt-galera-log-explainer/regex/events.go @@ -0,0 +1,179 @@ +package regex + +import ( + "regexp" + "strings" + + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/types" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/utils" +) + +func init() { + setType(types.EventsRegexType, EventsMap) +} + +var EventsMap = types.RegexMap{ + "RegexStarting": &types.LogRegex{ + Regex: regexp.MustCompile("starting as process"), + InternalRegex: regexp.MustCompile("\\(mysqld " + regexVersion + ".*\\)"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.Version = submatches[groupVersion] + + msg := "starting(" + ctx.Version + if isShutdownReasonMissing(ctx) { + msg += ", " + utils.Paint(utils.YellowText, "could not catch how/when it stopped") + } + msg += ")" + ctx.SetState("OPEN") + + return ctx, types.SimpleDisplayer(msg) + }, + }, + "RegexShutdownComplete": &types.LogRegex{ + Regex: regexp.MustCompile("mysqld: Shutdown complete"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.SetState("CLOSED") + + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "shutdown complete")) + }, + }, + "RegexTerminated": &types.LogRegex{ + Regex: regexp.MustCompile("mysqld: Terminated"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.SetState("CLOSED") + + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "terminated")) + }, + }, + "RegexGotSignal6": &types.LogRegex{ + Regex: regexp.MustCompile("mysqld got signal 6"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.SetState("CLOSED") + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "crash: got signal 6")) + }, + }, + "RegexGotSignal11": &types.LogRegex{ + Regex: regexp.MustCompile("mysqld got signal 11"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.SetState("CLOSED") + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "crash: got signal 11")) + }, + }, + "RegexShutdownSignal": &types.LogRegex{ + Regex: regexp.MustCompile("Normal|Received shutdown"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.SetState("CLOSED") + + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "received shutdown")) + }, + }, + + // 2023-06-12T07:51:38.135646Z 0 [Warning] [MY-000000] [Galera] Exception while mapping writeset addr: 0x7fb668d4e568, seqno: 2770385572449823232, size: 73316, ctx: 0x56128412e0c0, flags: 1. store: 1, type: 32 into [555, 998): 'deque::_M_new_elements_at_back'. Aborting GCache recovery. + + "RegexAborting": &types.LogRegex{ + Regex: regexp.MustCompile("Aborting"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.SetState("CLOSED") + + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "ABORTING")) + }, + }, + + "RegexWsrepLoad": &types.LogRegex{ + Regex: regexp.MustCompile("wsrep_load\\(\\): loading provider library"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.SetState("OPEN") + if regexWsrepLoadNone.MatchString(log) { + return ctx, types.SimpleDisplayer(utils.Paint(utils.GreenText, "started(standalone)")) + } + return ctx, types.SimpleDisplayer(utils.Paint(utils.GreenText, "started(cluster)")) + }, + }, + "RegexWsrepRecovery": &types.LogRegex{ + // INFO: WSREP: Recovered position 00000000-0000-0000-0000-000000000000:-1 + Regex: regexp.MustCompile("Recovered position"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + msg := "wsrep recovery" + // if state is joiner, it can be due to sst + // if state is open, it is just a start sequence depending on platform + if isShutdownReasonMissing(ctx) && ctx.State() != "JOINER" && ctx.State() != "OPEN" { + msg += "(" + utils.Paint(utils.YellowText, "could not catch how/when it stopped") + ")" + } + ctx.SetState("RECOVERY") + + return ctx, types.SimpleDisplayer(msg) + }, + }, + + "RegexUnknownConf": &types.LogRegex{ + Regex: regexp.MustCompile("unknown variable"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + split := strings.Split(log, "'") + v := "?" + if len(split) > 0 { + v = split[1] + } + if len(v) > 20 { + v = v[:20] + "..." + } + return ctx, types.SimpleDisplayer(utils.Paint(utils.YellowText, "unknown variable") + ": " + v) + }, + }, + + "RegexAssertionFailure": &types.LogRegex{ + Regex: regexp.MustCompile("Assertion failure"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.SetState("CLOSED") + + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "ASSERTION FAILURE")) + }, + }, + "RegexBindAddressAlreadyUsed": &types.LogRegex{ + Regex: regexp.MustCompile("asio error .bind: Address already in use"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.SetState("CLOSED") + + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "bind address already used")) + }, + }, + "RegexTooManyConnections": &types.LogRegex{ + Regex: regexp.MustCompile("Too many connections"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "too many connections")) + }, + }, + + "RegexReversingHistory": &types.LogRegex{ + Regex: regexp.MustCompile("Reversing history"), + InternalRegex: regexp.MustCompile("Reversing history: " + regexSeqno + " -> [0-9]*, this member has applied (?P[0-9]*) more events than the primary component"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + return ctx, types.SimpleDisplayer(utils.Paint(utils.BrightRedText, "having "+submatches["diff"]+" more events than the other nodes, data loss possible")) + }, + }, +} +var regexWsrepLoadNone = regexp.MustCompile("none") + +// isShutdownReasonMissing is returning true if the latest wsrep state indicated a "working" node +func isShutdownReasonMissing(ctx types.LogCtx) bool { + return ctx.State() != "DESTROYED" && ctx.State() != "CLOSED" && ctx.State() != "RECOVERY" && ctx.State() != "" +} + +/* + + +2023-05-09T17:39:19.955040Z 51 [Warning] [MY-000000] [Galera] failed to replay trx: source: fb9d6310-ee8b-11ed-8aee-f7542ad73e53 version: 5 local: 1 flags: 1 conn_id: 48 trx_id: 2696 tstamp: 1683653959142522853; state: EXECUTING:0->REPLICATING:782->CERTIFYING:3509->APPLYING:3748->COMMITTING:1343->COMMITTED:-1 +2023-05-09T17:39:19.955085Z 51 [Warning] [MY-000000] [Galera] Invalid state in replay for trx source: fb9d6310-ee8b-11ed-8aee-f7542ad73e53 version: 5 local: 1 flags: 1 conn_id: 48 trx_id: 2696 tstamp: 1683653959142522853; state: EXECUTING:0->REPLICATING:782->CERTIFYING:3509->APPLYING:3748->COMMITTING:1343->COMMITTED:-1 (FATAL) + at galera/src/replicator_smm.cpp:replay_trx():1247 + + +2001-01-01T01:01:01.000000Z 0 [ERROR] [MY-000000] [Galera] gcs/src/gcs_group.cpp:group_post_state_exchange():431: Reversing history: 312312 -> 20121, this member has applied 12345 more events than the primary component.Data loss is possible. Must abort. + +2023-06-07T02:50:17.288285-06:00 0 [ERROR] WSREP: Requested size 114209078 for '/var/lib/mysql//galera.cache' exceeds available storage space 1: 28 (No space left on device) + +2023-01-01 11:33:15 2101097 [ERROR] mariadbd: Disk full (/tmp/#sql-temptable-.....MAI); waiting for someone to free some space... (errno: 28 "No space left on device") + +2023-06-13 1:15:27 35 [Note] WSREP: MDL BF-BF conflict + +*/ diff --git a/src/go/pt-galera-log-explainer/regex/file.go b/src/go/pt-galera-log-explainer/regex/file.go new file mode 100644 index 00000000..d2a01e3e --- /dev/null +++ b/src/go/pt-galera-log-explainer/regex/file.go @@ -0,0 +1,33 @@ +package regex + +import "regexp" + +var RegexOperatorFileType = regexp.MustCompile(`\"file\":\"/([a-z]+/)+(?P[a-z._-]+.log)\"}$`) +var RegexOperatorShellDebugFileType = regexp.MustCompile(`^\+`) + +func FileType(line string, operator bool) string { + if !operator { + // if not operator, we can't really guess + return "error.log" + } + r, err := internalRegexSubmatch(RegexOperatorFileType, line) + if err != nil { + if RegexOperatorShellDebugFileType.MatchString(line) { + return "operator shell" + } + return "" + } + t := r[RegexOperatorFileType.SubexpIndex("filetype")] + switch t { + case "mysqld.post.processing.log": + return "post.processing.log" + case "wsrep_recovery_verbose.log": + return "recovery.log" + case "mysqld-error.log": + return "error.log" + case "innobackup.backup.log": + return "backup.log" + default: + return t + } +} diff --git a/src/go/pt-galera-log-explainer/regex/file_test.go b/src/go/pt-galera-log-explainer/regex/file_test.go new file mode 100644 index 00000000..ce72fecf --- /dev/null +++ b/src/go/pt-galera-log-explainer/regex/file_test.go @@ -0,0 +1,47 @@ +package regex + +import ( + "testing" +) + +func TestFileType(t *testing.T) { + tests := []struct { + inputline string + inputoperator bool + expected string + }{ + { + inputline: `{"log":"2023-07-11T06:03:51.109165Z 0 [Note] [MY-010747] [Server] Plugin 'FEDERATED' is disabled.\n","file":"/var/lib/mysql/wsrep_recovery_verbose.log"}`, + inputoperator: true, + expected: "recovery.log", + }, + { + inputline: `{"log":"2023-07-11T06:03:51.109165Z 0 [Note] [MY-010747] [Server] Plugin 'FEDERATED' is disabled.\n","file":"/var/lib/mysql/mysqld-error.log"}`, + inputoperator: true, + expected: "error.log", + }, + { + inputline: `{"log":"2023-07-11T06:03:51.109165Z 0 [Note] [MY-010747] [Server] Plugin 'FEDERATED' is disabled.\n","file":"/var/lib/mysql/mysqld.post.processing.log"}`, + inputoperator: true, + expected: "post.processing.log", + }, + { + inputline: `+ NODE_PORT=3306`, + inputoperator: true, + expected: "operator shell", + }, + { + inputline: `++ hostname -f`, + inputoperator: true, + expected: "operator shell", + }, + } + + for _, test := range tests { + + out := FileType(test.inputline, test.inputoperator) + if out != test.expected { + t.Errorf("expected: %s, got: %s", test.expected, out) + } + } +} diff --git a/src/go/pt-galera-log-explainer/regex/idents.go b/src/go/pt-galera-log-explainer/regex/idents.go new file mode 100644 index 00000000..ae77fb58 --- /dev/null +++ b/src/go/pt-galera-log-explainer/regex/idents.go @@ -0,0 +1,212 @@ +package regex + +import ( + "regexp" + "strconv" + + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/types" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/utils" +) + +func init() { + init_add_regexes() + setType(types.IdentRegexType, IdentsMap) +} + +var IdentsMap = types.RegexMap{ + // sourceNode is to identify from which node this log was taken + "RegexSourceNode": &types.LogRegex{ + Regex: regexp.MustCompile("(local endpoint for a connection, blacklisting address)|(points to own listening address, blacklisting)"), + InternalRegex: regexp.MustCompile("\\(" + regexNodeHash + ", '.+'\\).+" + regexNodeIPMethod), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + ip := submatches[groupNodeIP] + ctx.AddOwnIP(ip) + return ctx, types.SimpleDisplayer(ip + " is local") + }, + Verbosity: types.DebugMySQL, + }, + + // 2022-12-18T01:03:17.950545Z 0 [Note] [MY-000000] [Galera] Passing config to GCS: base_dir = /var/lib/mysql/; base_host = 127.0.0.1; + "RegexBaseHost": &types.LogRegex{ + Regex: regexp.MustCompile("base_host"), + InternalRegex: regexp.MustCompile("base_host = " + regexNodeIP), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + ip := submatches[groupNodeIP] + ctx.AddOwnIP(ip) + return ctx, types.SimpleDisplayer(ctx.OwnIPs[len(ctx.OwnIPs)-1] + " is local") + }, + Verbosity: types.DebugMySQL, + }, + + // 0: 015702fc-32f5-11ed-a4ca-267f97316394, node-1 + // 1: 08dd5580-32f7-11ed-a9eb-af5e3d01519e, garb + // TO *never* DO: store indexes to later search for them using SST infos and STATES EXCHANGES logs. EDIT: is definitely NOT reliable + "RegexMemberAssociations": &types.LogRegex{ + Regex: regexp.MustCompile("[0-9]: [a-z0-9]+-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]+, [a-zA-Z0-9-_]+"), + InternalRegex: regexp.MustCompile(regexIdx + ": " + regexUUID + ", " + regexNodeName), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + idx := submatches[groupIdx] + hash := submatches[groupUUID] + nodename := utils.ShortNodeName(submatches[groupNodeName]) + + // nodenames are truncated after 32 characters ... + if len(nodename) == 31 { + return ctx, nil + } + shorthash := utils.UUIDToShortUUID(hash) + ctx.HashToNodeName[shorthash] = nodename + + if ctx.MyIdx == idx && (ctx.IsPrimary() || ctx.MemberCount == 1) { + ctx.AddOwnHash(shorthash) + ctx.AddOwnName(nodename) + } + + return ctx, types.SimpleDisplayer(shorthash + " is " + nodename) + }, + Verbosity: types.DebugMySQL, + }, + + "RegexMemberCount": &types.LogRegex{ + Regex: regexp.MustCompile("members.[0-9]+.:"), + InternalRegex: regexp.MustCompile(regexMembers), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + members := submatches[groupMembers] + + membercount, err := strconv.Atoi(members) + if err != nil { + return ctx, nil + } + ctx.MemberCount = membercount + + return ctx, types.SimpleDisplayer("view member count: " + members) + }, + Verbosity: types.DebugMySQL, + }, + + // My UUID: 6938f4ae-32f4-11ed-be8d-8a0f53f88872 + "RegexOwnUUID": &types.LogRegex{ + Regex: regexp.MustCompile("My UUID"), + InternalRegex: regexp.MustCompile("My UUID: " + regexUUID), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + shorthash := utils.UUIDToShortUUID(submatches[groupUUID]) + + ctx.AddOwnHash(shorthash) + + return ctx, types.SimpleDisplayer(shorthash + " is local") + }, + Verbosity: types.DebugMySQL, + }, + + // 2023-01-06T06:59:26.527748Z 0 [Note] WSREP: (9509c194, 'tcp://0.0.0.0:4567') turning message relay requesting on, nonlive peers: + "RegexOwnUUIDFromMessageRelay": &types.LogRegex{ + Regex: regexp.MustCompile("turning message relay requesting"), + InternalRegex: regexp.MustCompile("\\(" + regexNodeHash + ", '" + regexNodeIPMethod + "'\\)"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + hash := submatches[groupNodeHash] + ctx.AddOwnHash(hash) + + return ctx, types.SimpleDisplayer(hash + " is local") + }, + Verbosity: types.DebugMySQL, + }, + + // 2023-01-06T07:05:35.693861Z 0 [Note] WSREP: New COMPONENT: primary = yes, bootstrap = no, my_idx = 0, memb_num = 2 + "RegexMyIDXFromComponent": &types.LogRegex{ + Regex: regexp.MustCompile("New COMPONENT:"), + InternalRegex: regexp.MustCompile("New COMPONENT:.*my_idx = " + regexIdx), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + idx := submatches[groupIdx] + ctx.MyIdx = idx + return ctx, types.SimpleDisplayer("my_idx=" + idx) + }, + Verbosity: types.DebugMySQL, + }, + + /* + + can't be trusted, from actual log: + View: + id: : + status: primary + protocol_version: 4 + capabilities: MULTI-MASTER, CERTIFICATION, PARALLEL_APPLYING, REPLAY, ISOLATION, PAUSE, CAUSAL_READ, INCREMENTAL_WS, UNORDERED, PREORDERED, STREAMING, NBO + final: no + own_index: 1 + members(3): + 0: , node0 + 1: , node1 + 2: , node2 + ================================================= + 2023-05-28T21:18:23.184707-05:00 2 [Note] [MY-000000] [WSREP] wsrep_notify_cmd is not defined, skipping notification. + 2023-05-28T21:18:23.193459-05:00 0 [Note] [MY-000000] [Galera] STATE EXCHANGE: sent state msg: + 2023-05-28T21:18:23.195777-05:00 0 [Note] [MY-000000] [Galera] STATE EXCHANGE: got state msg: from 0 (node1) + 2023-05-28T21:18:23.195805-05:00 0 [Note] [MY-000000] [Galera] STATE EXCHANGE: got state msg: from 1 (node2) + + + "RegexOwnNameFromStateExchange": &types.LogRegex{ + Regex: regexp.MustCompile("STATE EXCHANGE: got state msg"), + InternalRegex: regexp.MustCompile("STATE EXCHANGE:.* from " + regexIdx + " \\(" + regexNodeName + "\\)"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + r, err := internalRegexSubmatch(internalRegex, log) + if err != nil { + return ctx, nil + } + + idx := submatches[groupIdx] + name := submatches[groupNodeName] + if idx != ctx.MyIdx { + return ctx, types.SimpleDisplayer("name(" + name + ") from unknown idx") + } + + if ctx.State == "NON-PRIMARY" { + return ctx, types.SimpleDisplayer("name(" + name + ") can't be trusted as it's non-primary") + } + + ctx.AddOwnName(name) + return ctx, types.SimpleDisplayer("local name:" + name) + }, + Verbosity: types.DebugMySQL, + }, + */ +} + +func init_add_regexes() { + // 2023-01-06T07:05:34.035959Z 0 [Note] WSREP: (9509c194, 'tcp://0.0.0.0:4567') connection established to 838ebd6d tcp://ip:4567 + IdentsMap["RegexOwnUUIDFromEstablished"] = &types.LogRegex{ + Regex: regexp.MustCompile("connection established to"), + InternalRegex: IdentsMap["RegexOwnUUIDFromMessageRelay"].InternalRegex, + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + return IdentsMap["RegexOwnUUIDFromMessageRelay"].Handler(submatches, ctx, log) + }, + Verbosity: types.DebugMySQL, + } + + IdentsMap["RegexOwnIndexFromView"] = &types.LogRegex{ + Regex: regexp.MustCompile("own_index:"), + InternalRegex: regexp.MustCompile("own_index: " + regexIdx), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + return IdentsMap["RegexMyIDXFromComponent"].Handler(submatches, ctx, log) + }, + Verbosity: types.DebugMySQL, + } + + // 2023-01-06T07:05:35.698869Z 7 [Note] WSREP: New cluster view: global state: 00000000-0000-0000-0000-000000000000:0, view# 10: Primary, number of nodes: 2, my index: 0, protocol version 3 + // WARN: my index seems to always be 0 on this log on certain version. It had broken some nodenames + /* + IdentsMap["RegexMyIDXFromClusterView"] = &types.LogRegex{ + Regex: regexp.MustCompile("New cluster view:"), + InternalRegex: regexp.MustCompile("New cluster view:.*my index: " + regexIdx + ","), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + return IdentsMap["RegexMyIDXFromComponent"].Handler(internalRegex, ctx, log) + }, + Verbosity: types.DebugMySQL, + } + */ +} diff --git a/src/go/pt-galera-log-explainer/regex/operator.go b/src/go/pt-galera-log-explainer/regex/operator.go new file mode 100644 index 00000000..480b0337 --- /dev/null +++ b/src/go/pt-galera-log-explainer/regex/operator.go @@ -0,0 +1,55 @@ +package regex + +import ( + "regexp" + "strings" + + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/types" +) + +func init() { + setType(types.PXCOperatorRegexType, PXCOperatorMap) +} + +// Regexes from this type should only be about operator extra logs +// it should not contain Galera logs +// Specifically operators are dumping configuration files, recoveries, script outputs, ... +// only those should be handled here, they are specific to pxc operator but still very insightful +var PXCOperatorMap = types.RegexMap{ + "RegexNodeNameFromEnv": &types.LogRegex{ + Regex: regexp.MustCompile(". NODE_NAME="), + InternalRegex: regexp.MustCompile("NODE_NAME=" + regexNodeName), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + nodename := submatches[groupNodeName] + nodename, _, _ = strings.Cut(nodename, ".") + ctx.AddOwnName(nodename) + return ctx, types.SimpleDisplayer("local name:" + nodename) + }, + Verbosity: types.DebugMySQL, + }, + + "RegexNodeIPFromEnv": &types.LogRegex{ + Regex: regexp.MustCompile(". NODE_IP="), + InternalRegex: regexp.MustCompile("NODE_IP=" + regexNodeIP), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + ip := submatches[groupNodeIP] + ctx.AddOwnIP(ip) + return ctx, types.SimpleDisplayer("local ip:" + ip) + }, + Verbosity: types.DebugMySQL, + }, + + // Why is it not in regular "views" regexes: + // it could have been useful as an "verbosity=types.Detailed" regexes, very rarely + // but in context of operators, it is actually a very important information + "RegexGcacheScan": &types.LogRegex{ + // those "operators" regexes do not have the log prefix added implicitely. It's not strictly needed, but + // it will help to avoid catching random piece of log out of order + Regex: regexp.MustCompile("^{\"log\":\".*GCache::RingBuffer initial scan"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + return ctx, types.SimpleDisplayer("recovering gcache") + }, + }, +} diff --git a/src/go/pt-galera-log-explainer/regex/regex.go b/src/go/pt-galera-log-explainer/regex/regex.go new file mode 100644 index 00000000..2a1c6a03 --- /dev/null +++ b/src/go/pt-galera-log-explainer/regex/regex.go @@ -0,0 +1,72 @@ +package regex + +import ( + "errors" + "fmt" + "regexp" + "strings" + + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/types" +) + +func internalRegexSubmatch(regex *regexp.Regexp, log string) ([]string, error) { + slice := regex.FindStringSubmatch(log) + if len(slice) == 0 { + return nil, errors.New(fmt.Sprintf("Could not find submatch from log \"%s\" using pattern \"%s\"", log, regex.String())) + } + return slice, nil +} + +func setType(t types.RegexType, regexes types.RegexMap) { + for _, regex := range regexes { + regex.Type = t + } + return +} + +// SetVerbosity accepts any LogRegex +// Some can be useful to construct context, but we can choose not to display them +func SetVerbosity(verbosity types.Verbosity, regexes types.RegexMap) { + for _, regex := range regexes { + regex.Verbosity = verbosity + } + return +} + +func AllRegexes() types.RegexMap { + IdentsMap.Merge(ViewsMap).Merge(SSTMap).Merge(EventsMap).Merge(StatesMap).Merge(ApplicativeMap) + return IdentsMap +} + +// general building block wsrep regexes +// It's later used to identify subgroups easier +var ( + groupMethod = "ssltcp" + groupNodeIP = "nodeip" + groupNodeHash = "uuid" + groupUUID = "uuid" // same value as groupnodehash, because both are used in same context + groupNodeName = "nodename" + groupNodeName2 = "nodename2" + groupIdx = "idx" + groupSeqno = "seqno" + groupMembers = "members" + groupVersion = "version" + groupErrorMD5 = "errormd5" + regexMembers = "(?P<" + groupMembers + ">[0-9]{1,2})" + regexNodeHash = "(?P<" + groupNodeHash + ">[a-zA-Z0-9-_]+)" + regexNodeName = "(?P<" + groupNodeName + `>[a-zA-Z0-9-_\.]+)` + regexNodeName2 = strings.Replace(regexNodeName, groupNodeName, groupNodeName2, 1) + regexUUID = "(?P<" + groupUUID + ">[a-z0-9]+-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]+)" // eg ed97c863-d5c9-11ec-8ab7-671bbd2d70ef + regexNodeHash1Dash = "(?P<" + groupNodeHash + ">[a-z0-9]+-[a-z0-9]{4})" // eg ed97c863-8ab7 + regexSeqno = "(?P<" + groupSeqno + ">[0-9]+)" + regexNodeIP = "(?P<" + groupNodeIP + ">[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})" + regexNodeIPMethod = "(?P<" + groupMethod + ">.+)://" + regexNodeIP + ":[0-9]{1,6}" + regexIdx = "(?P<" + groupIdx + ">-?[0-9]{1,2})" + regexVersion = "(?P<" + groupVersion + ">(5|8|10|11)\\.[0-9]\\.[0-9]{1,2})" + regexErrorMD5 = "(?P<" + groupErrorMD5 + ">[a-z0-9]*)" +) + +func IsNodeUUID(s string) bool { + b, _ := regexp.MatchString(regexUUID, s) + return b +} diff --git a/src/go/pt-galera-log-explainer/regex/regex_test.go b/src/go/pt-galera-log-explainer/regex/regex_test.go new file mode 100644 index 00000000..c9a675fa --- /dev/null +++ b/src/go/pt-galera-log-explainer/regex/regex_test.go @@ -0,0 +1,1286 @@ +package regex + +import ( + "io/ioutil" + "os/exec" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/types" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/utils" + "github.com/pkg/errors" +) + +func TestRegexes(t *testing.T) { + utils.SkipColor = true + tests := []struct { + name string + log, expectedOut string + inputCtx types.LogCtx + inputState string + expectedState string + expectedCtx types.LogCtx + displayerExpectedNil bool + expectedErr bool + mapToTest types.RegexMap + key string + }{ + { + name: "8.0.30-22", + log: "2001-01-01T01:01:01.000000Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.30-22) starting as process 1", + expectedCtx: types.LogCtx{Version: "8.0.30"}, + expectedState: "OPEN", + expectedOut: "starting(8.0.30)", + mapToTest: EventsMap, + key: "RegexStarting", + }, + { + name: "8.0.2-22", + log: "2001-01-01T01:01:01.000000Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.2-22) starting as process 1", + expectedCtx: types.LogCtx{Version: "8.0.2"}, + expectedState: "OPEN", + expectedOut: "starting(8.0.2)", + mapToTest: EventsMap, + key: "RegexStarting", + }, + { + name: "5.7.31-34-log", + log: "2001-01-01T01:01:01.000000Z 0 [Note] /usr/sbin/mysqld (mysqld 5.7.31-34-log) starting as process 2 ...", + expectedCtx: types.LogCtx{Version: "5.7.31"}, + expectedState: "OPEN", + expectedOut: "starting(5.7.31)", + mapToTest: EventsMap, + key: "RegexStarting", + }, + { + name: "10.4.25-MariaDB-log", + log: "2001-01-01 01:01:01 0 [Note] /usr/sbin/mysqld (mysqld 10.4.25-MariaDB-log) starting as process 2 ...", + expectedCtx: types.LogCtx{Version: "10.4.25"}, + expectedState: "OPEN", + expectedOut: "starting(10.4.25)", + mapToTest: EventsMap, + key: "RegexStarting", + }, + { + name: "10.2.31-MariaDB-1:10.2.31+maria~bionic-log", + log: "2001-01-01 01:01:01 0 [Note] /usr/sbin/mysqld (mysqld 10.2.31-MariaDB-1:10.2.31+maria~bionic-log) starting as process 2 ...", + expectedCtx: types.LogCtx{Version: "10.2.31"}, + expectedState: "OPEN", + expectedOut: "starting(10.2.31)", + mapToTest: EventsMap, + key: "RegexStarting", + }, + { + name: "5.7.28-enterprise-commercial-advanced-log", + log: "2001-01-01T01:01:01.000000Z 0 [Note] /usr/sbin/mysqld (mysqld 5.7.28-enterprise-commercial-advanced-log) starting as process 2 ...", + expectedCtx: types.LogCtx{Version: "5.7.28"}, + expectedState: "OPEN", + expectedOut: "starting(5.7.28)", + mapToTest: EventsMap, + key: "RegexStarting", + }, + { + name: "8.0.30 operator", + log: "{\"log\":\"2001-01-01T01:01:01.000000Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.30-22.1) starting as process 1\n\",\"file\":\"/var/lib/mysql/mysqld-error.log\"}", + expectedCtx: types.LogCtx{Version: "8.0.30"}, + expectedState: "OPEN", + expectedOut: "starting(8.0.30)", + mapToTest: EventsMap, + key: "RegexStarting", + }, + { + name: "wrong version 7.0.0", + log: "2001-01-01T01:01:01.000000Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 7.0.0-22) starting as process 1", + displayerExpectedNil: true, + mapToTest: EventsMap, + key: "RegexStarting", + }, + { + name: "wrong version 8.12.0", + log: "2001-01-01T01:01:01.000000Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.12.0-22) starting as process 1", + displayerExpectedNil: true, + mapToTest: EventsMap, + key: "RegexStarting", + }, + { + name: "could not catch how it stopped", + log: "{\"log\":\"2001-01-01T01:01:01.000000Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.30-22.1) starting as process 1\n\",\"file\":\"/var/lib/mysql/mysqld-error.log\"}", + expectedCtx: types.LogCtx{Version: "8.0.30"}, + expectedState: "OPEN", + inputState: "OPEN", + expectedOut: "starting(8.0.30, could not catch how/when it stopped)", + mapToTest: EventsMap, + key: "RegexStarting", + }, + + { + + log: "2001-01-01T01:01:01.000000Z 0 [System] [MY-010910] [Server] /usr/sbin/mysqld: Shutdown complete (mysqld 8.0.23-14.1) Percona XtraDB Cluster (GPL), Release rel14, Revision d3b9a1d, WSREP version 26.4.3.", + expectedState: "CLOSED", + expectedOut: "shutdown complete", + mapToTest: EventsMap, + key: "RegexShutdownComplete", + }, + + { + log: "2001-01-01 01:01:01 140430087788288 [Note] WSREP: /opt/rh-mariadb102/root/usr/libexec/mysqld: Terminated.", + expectedState: "CLOSED", + expectedOut: "terminated", + mapToTest: EventsMap, + key: "RegexTerminated", + }, + { + log: "2001-01-01T01:01:01.000000Z 8 [Note] WSREP: /usr/sbin/mysqld: Terminated.", + expectedState: "CLOSED", + expectedOut: "terminated", + mapToTest: EventsMap, + key: "RegexTerminated", + }, + + { + log: "01:01:01 UTC - mysqld got signal 6 ;", + expectedState: "CLOSED", + expectedOut: "crash: got signal 6", + mapToTest: EventsMap, + key: "RegexGotSignal6", + }, + { + log: "01:01:01 UTC - mysqld got signal 11 ;", + expectedState: "CLOSED", + expectedOut: "crash: got signal 11", + mapToTest: EventsMap, + key: "RegexGotSignal11", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [WSREP] Received shutdown signal. Will sleep for 10 secs before initiating shutdown. pxc_maint_mode switched to SHUTDOWN", + expectedState: "CLOSED", + expectedOut: "received shutdown", + mapToTest: EventsMap, + key: "RegexShutdownSignal", + }, + { + log: "2001-01-01 01:01:01 139688443508480 [Note] /opt/rh-mariadb102/root/usr/libexec/mysqld (unknown): Normal shutdown", + expectedState: "CLOSED", + expectedOut: "received shutdown", + mapToTest: EventsMap, + key: "RegexShutdownSignal", + }, + { + log: "2001-01-01 1:01:01 0 [Note] /usr/sbin/mariadbd (initiated by: unknown): Normal shutdown", + expectedState: "CLOSED", + expectedOut: "received shutdown", + mapToTest: EventsMap, + key: "RegexShutdownSignal", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [ERROR] [MY-010119] [Server] Aborting", + expectedState: "CLOSED", + expectedOut: "ABORTING", + mapToTest: EventsMap, + key: "RegexAborting", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [Galera] wsrep_load(): loading provider library '/usr/lib64/galera4/libgalera_smm.so'", + expectedState: "OPEN", + expectedOut: "started(cluster)", + mapToTest: EventsMap, + key: "RegexWsrepLoad", + }, + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [Galera] wsrep_load(): loading provider library 'none'", + expectedState: "OPEN", + expectedOut: "started(standalone)", + mapToTest: EventsMap, + key: "RegexWsrepLoad", + }, + + { + log: "2001-01-01 01:01:01 140557650536640 [Note] WSREP: wsrep_load(): loading provider library '/opt/rh-mariadb102/root/usr/lib64/galera/libgalera_smm.so'", + expectedState: "OPEN", + expectedOut: "started(cluster)", + mapToTest: EventsMap, + key: "RegexWsrepLoad", + }, + + { + log: "2001-01-01T01:01:01.000000Z 3 [Note] [MY-000000] [Galera] Recovered position from storage: 7780bb61-87cf-11eb-b53b-6a7c64b0fee3:23506640", + expectedState: "RECOVERY", + expectedOut: "wsrep recovery", + mapToTest: EventsMap, + key: "RegexWsrepRecovery", + }, + { + log: " INFO: WSREP: Recovered position 9a4db4a5-5cf1-11ec-940d-6ba8c5905c02:30", + expectedState: "RECOVERY", + expectedOut: "wsrep recovery", + mapToTest: EventsMap, + key: "RegexWsrepRecovery", + }, + { + log: " INFO: WSREP: Recovered position 00000000-0000-0000-0000-000000000000:-1", + expectedState: "RECOVERY", + expectedOut: "wsrep recovery", + mapToTest: EventsMap, + key: "RegexWsrepRecovery", + }, + { + name: "not unknown", + log: " INFO: WSREP: Recovered position 00000000-0000-0000-0000-000000000000:-1", + expectedState: "RECOVERY", + inputState: "OPEN", + expectedOut: "wsrep recovery", + mapToTest: EventsMap, + key: "RegexWsrepRecovery", + }, + { + name: "could not catch how it stopped", + log: " INFO: WSREP: Recovered position 00000000-0000-0000-0000-000000000000:-1", + expectedState: "RECOVERY", + inputState: "SYNCED", + expectedOut: "wsrep recovery(could not catch how/when it stopped)", + mapToTest: EventsMap, + key: "RegexWsrepRecovery", + }, + + { + log: "2001-01-01T01:01:01.045425-05:00 0 [ERROR] unknown variable 'validate_password_length=8'", + expectedOut: "unknown variable: validate_password_le...", + mapToTest: EventsMap, + key: "RegexUnknownConf", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [ERROR] [MY-013183] [InnoDB] Assertion failure: btr0cur.cc:296:btr_page_get_prev(get_block->frame, mtr) == page_get_page_no(page) thread 139538894652992", + expectedState: "CLOSED", + expectedOut: "ASSERTION FAILURE", + mapToTest: EventsMap, + key: "RegexAssertionFailure", + }, + + { + log: "2001-01-01 5:06:12 47285568576576 [ERROR] WSREP: failed to open gcomm backend connection: 98: error while trying to listen 'tcp://0.0.0.0:4567?socket.non_blocking=1', asio error 'bind: Address already in use': 98 (Address already in use)", + expectedState: "CLOSED", + expectedOut: "bind address already used", + mapToTest: EventsMap, + key: "RegexBindAddressAlreadyUsed", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [ERROR] [MY-000000] [Galera] gcs/src/gcs_group.cpp:group_post_state_exchange():431: Reversing history: 150 -> 10, this member has applied 140 more events than the primary component.Data loss is possible. Must abort.", + expectedOut: "having 140 more events than the other nodes, data loss possible", + mapToTest: EventsMap, + key: "RegexReversingHistory", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [Galera] (90002222-1111, 'ssl://0.0.0.0:4567') Found matching local endpoint for a connection, blacklisting address ssl://127.0.0.1:4567", + expectedCtx: types.LogCtx{OwnIPs: []string{"127.0.0.1"}}, + expectedOut: "127.0.0.1 is local", + mapToTest: IdentsMap, + key: "RegexSourceNode", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [Galera] Passing config to GCS: base_dir = /var/lib/mysql/; base_host = 127.0.0.1; base_port = 4567; cert.log_conflicts = no; cert.optimistic_pa = no; debug = no; evs.auto_evict = 0; evs.delay_margin = PT1S; evs.delayed_keep_period = PT30S; evs.inactive_check_period = PT0.5S; evs.inactive_timeout = PT15S; evs.join_retrans_period = PT1S; evs.max_install_timeouts = 3; evs.send_window = 10; evs.stats_report_period = PT1M; evs.suspect_timeout = PT5S; evs.user_send_window = 4; evs.view_forget_timeout = PT24H; gcache.dir = /data/mysql/; gcache.freeze_purge_at_seqno = -1; gcache.keep_pages_count = 0; gcache.keep_pages_size = 0; gcache.mem_size = 0; gcache.name = galera.cache; gcache.page_size = 128M; gcache.recover = yes; gcache.size = 128M; gcomm.thread_prio = ; gcs.fc_debug = 0; gcs.fc_factor = 1.0; gcs.fc_limit = 100; gcs.fc_master_slave = no; gcs.max_packet_size = 64500; gcs.max_throttle = 0.25; gcs.recv_q_hard_limit = 9223372036854775807; gcs.recv_q_soft_limit = 0.25; gcs.sync_donor = no; gmcast.segment = 0; gmcast.version = 0; pc.announce_timeout = PT3S; pc.checksum = false; pc.ignore_quorum = false; pc.ignore_sb = false; pc.npvo = false; pc.recovery = true; pc.version = 0; pc.wait_prim = true; pc.wait_prim_timeout = PT30S; pc.weight = 1; protonet.backend = asio; protonet.version = 0; repl.causal_read_timeout = PT30S; repl.commit_order = 3; repl.key_format = FLAT8; repl.max_ws_size = 2147483647; repl.proto_max = 10; socket.checksum = 2; socket.recv_buf_size = auto; socket.send_buf_size = auto; socket.ssl_ca = ca.pem; socket.ssl_cert = server-cert.pem; socket.ssl_cipher = ; socket.ssl_compression = YES; socket.ssl_key = server-key.pem;", + expectedCtx: types.LogCtx{OwnIPs: []string{"127.0.0.1"}}, + expectedOut: "127.0.0.1 is local", + mapToTest: IdentsMap, + key: "RegexBaseHost", + }, + + { + log: " 0: 015702fc-32f5-11ed-a4ca-267f97316394, node1", + inputCtx: types.LogCtx{ + MyIdx: "0", + MemberCount: 1, + OwnHashes: []string{}, + OwnNames: []string{}, + HashToNodeName: map[string]string{}, + }, + inputState: "PRIMARY", + expectedCtx: types.LogCtx{ + MyIdx: "0", + MemberCount: 1, + OwnHashes: []string{"015702fc-a4ca"}, + OwnNames: []string{"node1"}, + HashToNodeName: map[string]string{"015702fc-a4ca": "node1"}, + }, + expectedState: "PRIMARY", + expectedOut: "015702fc-a4ca is node1", + mapToTest: IdentsMap, + key: "RegexMemberAssociations", + }, + { + log: " 0: 015702fc-32f5-11ed-a4ca-267f97316394, node1", + inputCtx: types.LogCtx{ + MyIdx: "0", + MemberCount: 1, + OwnHashes: []string{}, + OwnNames: []string{}, + HashToNodeName: map[string]string{}, + }, + inputState: "NON-PRIMARY", + expectedCtx: types.LogCtx{ + MyIdx: "0", + MemberCount: 1, + OwnHashes: []string{"015702fc-a4ca"}, + OwnNames: []string{"node1"}, + HashToNodeName: map[string]string{"015702fc-a4ca": "node1"}, + }, + expectedState: "NON-PRIMARY", + expectedOut: "015702fc-a4ca is node1", + mapToTest: IdentsMap, + key: "RegexMemberAssociations", + }, + { + log: " 0: 015702fc-32f5-11ed-a4ca-267f97316394, node1", + inputCtx: types.LogCtx{ + MyIdx: "0", + MemberCount: 2, + HashToNodeName: map[string]string{}, + }, + inputState: "NON-PRIMARY", + expectedCtx: types.LogCtx{ + MyIdx: "0", + MemberCount: 2, + HashToNodeName: map[string]string{"015702fc-a4ca": "node1"}, + }, + expectedState: "NON-PRIMARY", + expectedOut: "015702fc-a4ca is node1", + mapToTest: IdentsMap, + key: "RegexMemberAssociations", + }, + { + log: " 1: 015702fc-32f5-11ed-a4ca-267f97316394, node1", + inputCtx: types.LogCtx{ + MyIdx: "1", + MemberCount: 1, + OwnHashes: []string{}, + OwnNames: []string{}, + HashToNodeName: map[string]string{}, + }, + inputState: "PRIMARY", + expectedCtx: types.LogCtx{ + MyIdx: "1", + MemberCount: 1, + OwnHashes: []string{"015702fc-a4ca"}, + OwnNames: []string{"node1"}, + HashToNodeName: map[string]string{"015702fc-a4ca": "node1"}, + }, + expectedState: "PRIMARY", + expectedOut: "015702fc-a4ca is node1", + mapToTest: IdentsMap, + key: "RegexMemberAssociations", + }, + { + log: " 0: 015702fc-32f5-11ed-a4ca-267f97316394, node1", + inputCtx: types.LogCtx{ + MyIdx: "1", + MemberCount: 1, + HashToNodeName: map[string]string{}, + }, + inputState: "PRIMARY", + expectedCtx: types.LogCtx{ + MyIdx: "1", + MemberCount: 1, + HashToNodeName: map[string]string{"015702fc-a4ca": "node1"}, + }, + expectedState: "PRIMARY", + expectedOut: "015702fc-a4ca is node1", + mapToTest: IdentsMap, + key: "RegexMemberAssociations", + }, + { + log: " 0: 015702fc-32f5-11ed-a4ca-267f97316394, node1.with.complete.fqdn", + inputCtx: types.LogCtx{ + MyIdx: "1", + MemberCount: 1, + HashToNodeName: map[string]string{}, + }, + inputState: "PRIMARY", + expectedCtx: types.LogCtx{ + MyIdx: "1", + MemberCount: 1, + HashToNodeName: map[string]string{"015702fc-a4ca": "node1"}, + }, + expectedState: "PRIMARY", + expectedOut: "015702fc-a4ca is node1", + mapToTest: IdentsMap, + key: "RegexMemberAssociations", + }, + { + name: "name too long and truncated", + log: " 0: 015702fc-32f5-11ed-a4ca-267f97316394, name_so_long_it_will_get_trunca", + inputCtx: types.LogCtx{ + MyIdx: "1", + MemberCount: 1, + }, + inputState: "PRIMARY", + expectedCtx: types.LogCtx{ + MyIdx: "1", + MemberCount: 1, + }, + expectedState: "PRIMARY", + expectedOut: "", + displayerExpectedNil: true, + mapToTest: IdentsMap, + key: "RegexMemberAssociations", + }, + + { + log: " members(1):", + expectedOut: "view member count: 1", + expectedCtx: types.LogCtx{MemberCount: 1}, + mapToTest: IdentsMap, + key: "RegexMemberCount", + }, + + { + log: "2001-01-01T01:01:01.000000Z 1 [Note] [MY-000000] [Galera] ####### My UUID: 60205de0-5cf6-11ec-8884-3a01908be11a", + inputCtx: types.LogCtx{}, + expectedCtx: types.LogCtx{ + OwnHashes: []string{"60205de0-8884"}, + }, + expectedOut: "60205de0-8884 is local", + mapToTest: IdentsMap, + key: "RegexOwnUUID", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: (9509c194, 'tcp://0.0.0.0:4567') turning message relay requesting on, nonlive peers:", + inputCtx: types.LogCtx{}, + expectedCtx: types.LogCtx{ + OwnHashes: []string{"9509c194"}, + }, + expectedOut: "9509c194 is local", + mapToTest: IdentsMap, + key: "RegexOwnUUIDFromMessageRelay", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: New COMPONENT: primary = yes, bootstrap = no, my_idx = 0, memb_num = 2", + inputCtx: types.LogCtx{}, + expectedCtx: types.LogCtx{ + MyIdx: "0", + }, + expectedOut: "my_idx=0", + mapToTest: IdentsMap, + key: "RegexMyIDXFromComponent", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: (9509c194, 'tcp://0.0.0.0:4567') connection established to 838ebd6d tcp://172.17.0.2:4567", + inputCtx: types.LogCtx{}, + expectedCtx: types.LogCtx{ + OwnHashes: []string{"9509c194"}, + }, + expectedOut: "9509c194 is local", + mapToTest: IdentsMap, + key: "RegexOwnUUIDFromEstablished", + }, + + { + log: " own_index: 1", + inputCtx: types.LogCtx{}, + expectedCtx: types.LogCtx{ + MyIdx: "1", + }, + expectedOut: "my_idx=1", + mapToTest: IdentsMap, + key: "RegexOwnIndexFromView", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [Galera] (60205de0-8884, 'ssl://0.0.0.0:4567') connection established to 5873acd0-baa8 ssl://172.17.0.2:4567", + inputCtx: types.LogCtx{ + HashToIP: map[string]string{}, + }, + expectedCtx: types.LogCtx{ + HashToIP: map[string]string{"5873acd0-baa8": "172.17.0.2"}, + }, + expectedOut: "172.17.0.2 established", + mapToTest: ViewsMap, + key: "RegexNodeEstablished", + }, + { + name: "established to node's own ip", + log: "2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [Galera] (60205de0-8884, 'ssl://0.0.0.0:4567') connection established to 5873acd0-baa8 ssl://172.17.0.2:4567", + inputCtx: types.LogCtx{ + OwnIPs: []string{"172.17.0.2"}, + HashToIP: map[string]string{}, + }, + expectedCtx: types.LogCtx{ + OwnIPs: []string{"172.17.0.2"}, + HashToIP: map[string]string{"5873acd0-baa8": "172.17.0.2"}, + }, + expectedOut: "", + displayerExpectedNil: true, + mapToTest: ViewsMap, + key: "RegexNodeEstablished", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [Galera] declaring 5873acd0-baa8 at ssl://172.17.0.2:4567 stable", + inputCtx: types.LogCtx{ + HashToIP: map[string]string{}, + IPToMethod: map[string]string{}, + }, + expectedCtx: types.LogCtx{ + HashToIP: map[string]string{"5873acd0-baa8": "172.17.0.2"}, + IPToMethod: map[string]string{"172.17.0.2": "ssl"}, + }, + expectedOut: "172.17.0.2 joined", + mapToTest: ViewsMap, + key: "RegexNodeJoined", + }, + { + name: "mariadb variation", + log: "2001-01-01 1:01:30 0 [Note] WSREP: declaring 5873acd0-baa8 at tcp://172.17.0.2:4567 stable", + inputCtx: types.LogCtx{ + HashToIP: map[string]string{}, + IPToMethod: map[string]string{}, + }, + expectedCtx: types.LogCtx{ + HashToIP: map[string]string{"5873acd0-baa8": "172.17.0.2"}, + IPToMethod: map[string]string{"172.17.0.2": "tcp"}, + }, + expectedOut: "172.17.0.2 joined", + mapToTest: ViewsMap, + key: "RegexNodeJoined", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [Galera] forgetting 871c35de-99ae (ssl://172.17.0.2:4567)", + expectedOut: "172.17.0.2 left", + mapToTest: ViewsMap, + key: "RegexNodeLeft", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: New COMPONENT: primary = yes, bootstrap = no, my_idx = 0, memb_num = 2", + expectedCtx: types.LogCtx{MemberCount: 2}, + expectedState: "PRIMARY", + expectedOut: "PRIMARY(n=2)", + mapToTest: ViewsMap, + key: "RegexNewComponent", + }, + { + name: "bootstrap", + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: New COMPONENT: primary = yes, bootstrap = yes, my_idx = 0, memb_num = 2", + expectedCtx: types.LogCtx{MemberCount: 2}, + expectedState: "PRIMARY", + expectedOut: "PRIMARY(n=2),bootstrap", + mapToTest: ViewsMap, + key: "RegexNewComponent", + }, + { + name: "don't set primary", + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: New COMPONENT: primary = yes, bootstrap = no, my_idx = 0, memb_num = 2", + inputState: "JOINER", + expectedCtx: types.LogCtx{MemberCount: 2}, + expectedState: "JOINER", + expectedOut: "PRIMARY(n=2)", + mapToTest: ViewsMap, + key: "RegexNewComponent", + }, + { + name: "non-primary", + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: New COMPONENT: primary = no, bootstrap = no, my_idx = 0, memb_num = 2", + expectedCtx: types.LogCtx{MemberCount: 2}, + expectedState: "NON-PRIMARY", + expectedOut: "NON-PRIMARY(n=2)", + mapToTest: ViewsMap, + key: "RegexNewComponent", + }, + + { + log: "2001-01-01T01:01:01.000000Z 84580 [Note] [MY-000000] [Galera] evs::proto(9a826787-9e98, LEAVING, view_id(REG,4971d113-87b0,22)) suspecting node: 4971d113-87b0", + inputCtx: types.LogCtx{ + HashToIP: map[string]string{}, + }, + expectedCtx: types.LogCtx{ + HashToIP: map[string]string{}, + }, + expectedOut: "4971d113-87b0 suspected to be down", + mapToTest: ViewsMap, + key: "RegexNodeSuspect", + }, + { + name: "with known ip", + log: "2001-01-01T01:01:01.000000Z 84580 [Note] [MY-000000] [Galera] evs::proto(9a826787-9e98, LEAVING, view_id(REG,4971d113-87b0,22)) suspecting node: 4971d113-87b0", + inputCtx: types.LogCtx{ + HashToIP: map[string]string{"4971d113-87b0": "172.17.0.2"}, + }, + expectedCtx: types.LogCtx{ + HashToIP: map[string]string{"4971d113-87b0": "172.17.0.2"}, + }, + expectedOut: "172.17.0.2 suspected to be down", + mapToTest: ViewsMap, + key: "RegexNodeSuspect", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: remote endpoint tcp://172.17.0.2:4567 changed identity 84953af9 -> 5a478da2", + inputCtx: types.LogCtx{ + HashToIP: map[string]string{"84953af9": "172.17.0.2"}, + }, + expectedCtx: types.LogCtx{ + HashToIP: map[string]string{"84953af9": "172.17.0.2", "5a478da2": "172.17.0.2"}, + }, + expectedOut: "172.17.0.2 changed identity", + mapToTest: ViewsMap, + key: "RegexNodeChangedIdentity", + }, + { + name: "with complete uuid", + log: "2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [Galera] remote endpoint ssl://172.17.0.2:4567 changed identity 595812bc-9c79-11ec-ad3f-3a7953bcc2fc -> 595812bc-9c79-11ec-ad40-3a7953bcc2fc", + inputCtx: types.LogCtx{ + HashToIP: map[string]string{"595812bc-ad3f": "172.17.0.2"}, + }, + expectedCtx: types.LogCtx{ + HashToIP: map[string]string{"595812bc-ad3f": "172.17.0.2", "595812bc-ad40": "172.17.0.2"}, + }, + expectedOut: "172.17.0.2 changed identity", + mapToTest: ViewsMap, + key: "RegexNodeChangedIdentity", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [ERROR] [MY-000000] [Galera] It may not be safe to bootstrap the cluster from this node. It was not the last one to leave the cluster and may not contain all the updates. To force cluster bootstrap with this node, edit the grastate.dat file manually and set safe_to_bootstrap to 1 .", + expectedState: "CLOSED", + expectedOut: "not safe to bootstrap", + mapToTest: ViewsMap, + key: "RegexWsrepUnsafeBootstrap", + }, + + { + log: "2001-01-01T01:01:01.481967+09:00 4 [ERROR] WSREP: Node consistency compromised, aborting...", + expectedState: "CLOSED", + expectedOut: "consistency compromised", + mapToTest: ViewsMap, + key: "RegexWsrepConsistenctyCompromised", + }, + { + log: "2001-01-01T01:01:01.000000Z 86 [ERROR] WSREP: Node consistency compromized, aborting...", + expectedState: "CLOSED", + expectedOut: "consistency compromised", + mapToTest: ViewsMap, + key: "RegexWsrepConsistenctyCompromised", + }, + + { + log: "2001-01-01 5:06:12 47285568576576 [Note] WSREP: gcomm: bootstrapping new group 'cluster'", + expectedOut: "bootstrapping", + mapToTest: ViewsMap, + key: "RegexBootstrap", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: Found saved state: 8e862473-455e-11e8-a0ca-3fcd8faf3209:-1, safe_to_bootstrap: 1", + expectedOut: "safe_to_bootstrap: 1", + mapToTest: ViewsMap, + key: "RegexSafeToBoostrapSet", + }, + { + name: "should not match", + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: Found saved state: 8e862473-455e-11e8-a0ca-3fcd8faf3209:-1, safe_to_bootstrap: 0", + expectedErr: true, + mapToTest: ViewsMap, + key: "RegexSafeToBoostrapSet", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Warning] [MY-000000] [Galera] Could not open state file for reading: '/var/lib/mysql//grastate.dat'", + expectedOut: "no grastate.dat file", + mapToTest: ViewsMap, + key: "RegexNoGrastate", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Warning] [MY-000000] [Galera] No persistent state found. Bootstraping with default state", + expectedOut: "bootstrapping(empty grastate)", + mapToTest: ViewsMap, + key: "RegexBootstrapingDefaultState", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: Shifting OPEN -> CLOSED (TO: 1922878)", + expectedState: "CLOSED", + expectedOut: "OPEN -> CLOSED", + mapToTest: StatesMap, + key: "RegexShift", + }, + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: Shifting SYNCED -> DONOR/DESYNCED (TO: 21582507)", + expectedState: "DONOR", + expectedOut: "SYNCED -> DONOR", + mapToTest: StatesMap, + key: "RegexShift", + }, + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: Shifting DONOR/DESYNCED -> JOINED (TO: 21582507)", + expectedState: "JOINED", + expectedOut: "DESYNCED -> JOINED", + mapToTest: StatesMap, + key: "RegexShift", + }, + + { + log: "2001-01-01 01:01:01 140446385440512 [Note] WSREP: Restored state OPEN -> SYNCED (72438094)", + expectedState: "SYNCED", + expectedOut: "(restored)OPEN -> SYNCED", + mapToTest: StatesMap, + key: "RegexRestoredState", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: Member 2.0 (node2) requested state transfer from '*any*'. Selected 0.0 (node1)(SYNCED) as donor.", + inputCtx: types.LogCtx{}, + expectedCtx: types.LogCtx{}, + expectedOut: "node1 will resync node2", + mapToTest: SSTMap, + key: "RegexSSTRequestSuccess", + }, + { + name: "with fqdn", + log: "2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [Galera] Member 2.0 (node2.host.com) requested state transfer from '*any*'. Selected 0.0 (node1.host.com)(SYNCED) as donor.", + inputCtx: types.LogCtx{}, + expectedCtx: types.LogCtx{}, + expectedOut: "node1 will resync node2", + mapToTest: SSTMap, + key: "RegexSSTRequestSuccess", + }, + { + name: "joining", + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: Member 2.0 (node2) requested state transfer from '*any*'. Selected 0.0 (node1)(SYNCED) as donor.", + inputCtx: types.LogCtx{ + OwnNames: []string{"node2"}, + }, + expectedCtx: types.LogCtx{ + OwnNames: []string{"node2"}, + SST: types.SST{ResyncedFromNode: "node1"}, + }, + expectedOut: "node1 will resync local node", + mapToTest: SSTMap, + key: "RegexSSTRequestSuccess", + }, + { + name: "donor", + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: Member 2.0 (node2) requested state transfer from '*any*'. Selected 0.0 (node1)(SYNCED) as donor.", + inputCtx: types.LogCtx{ + OwnNames: []string{"node1"}, + }, + expectedCtx: types.LogCtx{ + OwnNames: []string{"node1"}, + SST: types.SST{ResyncingNode: "node2"}, + }, + expectedOut: "local node will resync node2", + mapToTest: SSTMap, + key: "RegexSSTRequestSuccess", + }, + + { + log: "2001-01-01 01:01:01.164 WARN: Member 1.0 (node2) requested state transfer from 'node1', but it is impossible to select State Transfer donor: Resource temporarily unavailable", + inputCtx: types.LogCtx{}, + expectedCtx: types.LogCtx{}, + expectedOut: "node2 cannot find donor", + mapToTest: SSTMap, + key: "RegexSSTResourceUnavailable", + }, + { + name: "local", + log: "2001-01-01 01:01:01.164 WARN: Member 1.0 (node2) requested state transfer from 'node1', but it is impossible to select State Transfer donor: Resource temporarily unavailable", + inputCtx: types.LogCtx{ + OwnNames: []string{"node2"}, + }, + expectedCtx: types.LogCtx{ + OwnNames: []string{"node2"}, + }, + expectedOut: "cannot find donor", + mapToTest: SSTMap, + key: "RegexSSTResourceUnavailable", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: 0.0 (node1): State transfer to 2.0 (node2) complete.", + inputCtx: types.LogCtx{}, + expectedCtx: types.LogCtx{}, + expectedOut: "node1 synced node2", + mapToTest: SSTMap, + key: "RegexSSTComplete", + }, + { + name: "joiner", + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: 0.0 (node1): State transfer to 2.0 (node2) complete.", + inputCtx: types.LogCtx{ + OwnNames: []string{"node2"}, + SST: types.SST{ResyncedFromNode: "node1"}, + }, + expectedCtx: types.LogCtx{ + SST: types.SST{ResyncedFromNode: ""}, + OwnNames: []string{"node2"}, + }, + expectedOut: "got SST from node1", + mapToTest: SSTMap, + key: "RegexSSTComplete", + }, + { + name: "joiner ist", + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: 0.0 (node1): State transfer to 2.0 (node2) complete.", + inputCtx: types.LogCtx{ + OwnNames: []string{"node2"}, + SST: types.SST{ResyncedFromNode: "node1", Type: "IST"}, + }, + expectedCtx: types.LogCtx{ + SST: types.SST{ResyncedFromNode: "", Type: ""}, + OwnNames: []string{"node2"}, + }, + expectedOut: "got IST from node1", + mapToTest: SSTMap, + key: "RegexSSTComplete", + }, + { + name: "donor", + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: 0.0 (node1): State transfer to 2.0 (node2) complete.", + inputCtx: types.LogCtx{ + OwnNames: []string{"node1"}, + SST: types.SST{ResyncingNode: "node2"}, + }, + expectedCtx: types.LogCtx{ + SST: types.SST{ResyncingNode: ""}, + OwnNames: []string{"node1"}, + }, + expectedOut: "finished sending SST to node2", + mapToTest: SSTMap, + key: "RegexSSTComplete", + }, + { + name: "donor ist", + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: 0.0 (node1): State transfer to 2.0 (node2) complete.", + inputCtx: types.LogCtx{ + OwnNames: []string{"node1"}, + SST: types.SST{ResyncingNode: "node2", Type: "IST"}, + }, + expectedCtx: types.LogCtx{ + SST: types.SST{ResyncingNode: "", Type: ""}, + OwnNames: []string{"node1"}, + }, + expectedOut: "finished sending IST to node2", + mapToTest: SSTMap, + key: "RegexSSTComplete", + }, + { + name: "with donor name", + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: 0.0 (node1): State transfer to 2.0 (node2) complete.", + inputCtx: types.LogCtx{ + SST: types.SST{ResyncingNode: "node2", Type: "IST"}, + }, + expectedCtx: types.LogCtx{ + SST: types.SST{ResyncingNode: "", Type: ""}, + OwnNames: []string{"node1"}, + }, + inputState: "DONOR", + expectedState: "DONOR", + expectedOut: "finished sending IST to node2", + mapToTest: SSTMap, + key: "RegexSSTComplete", + }, + { + name: "with joiner name", + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: 0.0 (node1): State transfer to 2.0 (node2) complete.", + inputCtx: types.LogCtx{ + SST: types.SST{ResyncingNode: "node2", Type: "IST"}, + }, + expectedCtx: types.LogCtx{ + SST: types.SST{ResyncingNode: "", Type: ""}, + OwnNames: []string{"node2"}, + }, + inputState: "JOINER", + expectedState: "JOINER", + expectedOut: "got IST from node1", + mapToTest: SSTMap, + key: "RegexSSTComplete", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: 0.0 (node1): State transfer to -1.-1 (left the group) complete.", + inputCtx: types.LogCtx{}, + expectedCtx: types.LogCtx{}, + expectedOut: "node1 synced ??(node left)", + mapToTest: SSTMap, + key: "RegexSSTCompleteUnknown", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [ERROR] [MY-000000] [WSREP] Process completed with error: wsrep_sst_xtrabackup-v2 --role 'donor' --address '172.17.0.2:4444/xtrabackup_sst//1' --socket '/var/lib/mysql/mysql.sock' --datadir '/var/lib/mysql/' --basedir '/usr/' --plugindir '/usr/lib64/mysql/plugin/' --defaults-file '/etc/my.cnf' --defaults-group-suffix '' --mysqld-version '8.0.28-19.1' '' --gtid '9db0bcdf-b31a-11ed-a398-2a4cfdd82049:1' : 22 (Invalid argument)", + expectedOut: "SST error", + mapToTest: SSTMap, + key: "RegexSSTError", + }, + + { + log: "2001-01-01T01:01:01.000000Z 1328586 [Note] [MY-000000] [WSREP] Initiating SST cancellation", + expectedOut: "former SST cancelled", + mapToTest: SSTMap, + key: "RegexSSTCancellation", + }, + + { + log: "2001-01-01T01:01:01.000000Z WSREP_SST: [INFO] Proceeding with SST.........", + expectedCtx: types.LogCtx{SST: types.SST{Type: "SST"}}, + expectedState: "JOINER", + expectedOut: "receiving SST", + mapToTest: SSTMap, + key: "RegexSSTProceeding", + }, + + { + log: "2001-01-01T01:01:01.000000Z WSREP_SST: [INFO] Streaming the backup to joiner at 172.17.0.2 4444", + expectedCtx: types.LogCtx{SST: types.SST{ResyncingNode: "172.17.0.2"}}, + expectedState: "DONOR", + expectedOut: "SST to 172.17.0.2", + mapToTest: SSTMap, + key: "RegexSSTStreamingTo", + }, + + { + log: "2001-01-01 01:01:01 140446376740608 [Note] WSREP: IST received: e00c4fff-c4b0-11e9-96a8-0f9789de42ad:69472531", + expectedCtx: types.LogCtx{}, + expectedOut: "IST received(seqno:69472531)", + mapToTest: SSTMap, + key: "RegexISTReceived", + }, + + { + log: "2001-01-01 1:01:01 140433613571840 [Note] WSREP: async IST sender starting to serve tcp://172.17.0.2:4568 sending 2-116", + expectedCtx: types.LogCtx{SST: types.SST{Type: "IST"}}, + expectedState: "DONOR", + expectedOut: "IST to 172.17.0.2(seqno:116)", + mapToTest: SSTMap, + key: "RegexISTSender", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [Galera] Prepared IST receiver for 114-116, listening at: ssl://172.17.0.2:4568", + expectedCtx: types.LogCtx{SST: types.SST{Type: "IST"}}, + expectedState: "JOINER", + expectedOut: "will receive IST(seqno:116)", + mapToTest: SSTMap, + key: "RegexISTReceiver", + }, + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [Galera] Prepared IST receiver for 0-116, listening at: ssl://172.17.0.2:4568", + expectedCtx: types.LogCtx{SST: types.SST{Type: "SST"}}, + expectedState: "JOINER", + expectedOut: "will receive SST", + mapToTest: SSTMap, + key: "RegexISTReceiver", + }, + { + name: "mdb variant", + log: "2001-01-01T01:01:01.000000Z 0 [Note] WSREP: Prepared IST receiver, listening at: ssl://172.17.0.2:4568", + expectedCtx: types.LogCtx{SST: types.SST{Type: "IST"}}, + expectedState: "JOINER", + expectedOut: "will receive IST", + mapToTest: SSTMap, + key: "RegexISTReceiver", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Warning] [MY-000000] [Galera] 0.1 (node): State transfer to -1.-1 (left the group) failed: -111 (Connection refused)", + expectedOut: "node failed to sync ??(node left)", + mapToTest: SSTMap, + key: "RegexSSTFailedUnknown", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Warning] [MY-000000] [Galera] 0.1 (node): State transfer to 0.2 (node2) failed: -111 (Connection refused)", + expectedOut: "node failed to sync node2", + mapToTest: SSTMap, + key: "RegexSSTStateTransferFailed", + }, + { + log: "2001-01-01T01:01:01.000000Z 0 [Warning] [MY-000000] [Galera] 0.1 (node): State transfer to -1.-1 (left the group) failed: -111 (Connection refused)", + displayerExpectedNil: true, + mapToTest: SSTMap, + key: "RegexSSTStateTransferFailed", + }, + + { + log: "2001-01-01T01:01:01.000000Z 1 [Note] WSREP: Failed to prepare for incremental state transfer: Local state UUID (00000000-0000-0000-0000-000000000000) does not match group state UUID (ed16c932-84b3-11ed-998c-8e3ae5bc328f): 1 (Operation not permitted)", + expectedCtx: types.LogCtx{SST: types.SST{Type: "SST"}}, + expectedOut: "IST is not applicable", + mapToTest: SSTMap, + key: "RegexFailedToPrepareIST", + }, + { + log: "2001-01-01T01:01:01.000000Z 1 [Warning] WSREP: Failed to prepare for incremental state transfer: Local state seqno is undefined: 1 (Operation not permitted)", + expectedCtx: types.LogCtx{SST: types.SST{Type: "SST"}}, + expectedOut: "IST is not applicable", + mapToTest: SSTMap, + key: "RegexFailedToPrepareIST", + }, + + { + log: "2001-01-01T01:01:01.000000Z WSREP_SST: [INFO] Bypassing SST. Can work it through IST", + expectedCtx: types.LogCtx{SST: types.SST{Type: "IST"}}, + expectedOut: "IST will be used", + mapToTest: SSTMap, + key: "RegexBypassSST", + }, + + { + log: "2001/01/01 01:01:01 socat[23579] E connect(62, AF=2 172.17.0.20:4444, 16): Connection refused", + expectedOut: "socat: connection refused", + mapToTest: SSTMap, + key: "RegexSocatConnRefused", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [WSREP-SST] Preparing the backup at /var/lib/mysql/sst-xb-tmpdir", + expectedOut: "preparing SST backup", + mapToTest: SSTMap, + key: "RegexPreparingBackup", + }, + + { + log: "2001-01-01T01:01:01.000000Z WSREP_SST: [ERROR] Possible timeout in receving first data from donor in gtid/keyring stage", + expectedOut: "timeout from donor in gtid/keyring stage", + mapToTest: SSTMap, + key: "RegexTimeoutReceivingFirstData", + }, + + { + log: "2001-01-01 01:01:01 140666176771840 [ERROR] WSREP: gcs/src/gcs_group.cpp:gcs_group_handle_join_msg():736: Will never receive state. Need to abort.", + expectedOut: "will never receive SST, aborting", + mapToTest: SSTMap, + key: "RegexWillNeverReceive", + }, + + { + log: "2001-01-01T01:01:01.000000Z 0 [ERROR] WSREP: async IST sender failed to serve tcp://172.17.0.2:4568: ist send failed: asio.system:32', asio error 'write: Broken pipe': 32 (Broken pipe)", + expectedOut: "IST to 172.17.0.2 failed: Broken pipe", + mapToTest: SSTMap, + key: "RegexISTFailed", + }, + { + log: "2001-01-01 01:10:01 28949 [ERROR] WSREP: async IST sender failed to serve tcp://172.17.0.2:4568: ist send failed: asio.system:104', asio error 'write: Connection reset by peer': 104 (Connection reset by peer)", + expectedOut: "IST to 172.17.0.2 failed: Connection reset by peer", + mapToTest: SSTMap, + key: "RegexISTFailed", + }, + { + log: "2001-01-01T01:01:01.000000Z 0 [ERROR] [MY-000000] [Galera] async IST sender failed to serve ssl://172.17.0.2:4568: ist send failed: ', asio error 'Got unexpected return from write: eof: 71 (Protocol error)", + expectedOut: "IST to 172.17.0.2 failed: Protocol error", + mapToTest: SSTMap, + key: "RegexISTFailed", + }, + { + log: `{\"log\":\"2001-01-01T01:01:01.000000Z 0 [ERROR] [MY-000000] [Galera] async IST sender failed to serve ssl://172.17.0.2:4568: ist send failed: ', asio error 'Got unexpected return from write: eof: 71 (Protocol error)\n\t at galerautils/src/gu_asio_stream_react.cpp:write():195': 71 (Protocol error)\n\t at galera/src/ist.cpp:send():856\n\",\"file\":\"/var/lib/mysql/mysqld-error.log\"}`, + expectedOut: "IST to 172.17.0.2 failed: Protocol error", + mapToTest: SSTMap, + key: "RegexISTFailed", + }, + + { + log: "+ NODE_NAME=cluster1-pxc-0.cluster1-pxc.test-percona.svc.cluster.local", + expectedCtx: types.LogCtx{OwnNames: []string{"cluster1-pxc-0"}}, + expectedOut: "local name:cluster1-pxc-0", + mapToTest: PXCOperatorMap, + key: "RegexNodeNameFromEnv", + }, + + { + log: "+ NODE_IP=172.17.0.2", + expectedCtx: types.LogCtx{OwnIPs: []string{"172.17.0.2"}}, + expectedOut: "local ip:172.17.0.2", + mapToTest: PXCOperatorMap, + key: "RegexNodeIPFromEnv", + }, + + { + log: "{\"log\":\"2023-07-05T08:17:23.447015Z 0 [Note] [MY-000000] [Galera] GCache::RingBuffer initial scan... 0.0% ( 0/1073741848 bytes) complete.\n\",\"file\":\"/var/lib/mysql/mysqld-error.log\"}", + expectedOut: "recovering gcache", + mapToTest: PXCOperatorMap, + key: "RegexGcacheScan", + }, + + { + log: "2001-01-01 1:01:01 0 [Note] WSREP: Member 0.0 (node) desyncs itself from group", + expectedCtx: types.LogCtx{Desynced: true}, + expectedOut: "node desyncs itself from group", + mapToTest: ApplicativeMap, + key: "RegexDesync", + }, + + { + log: "2001-01-01 1:01:01 0 [Note] WSREP: Member 0.0 (node) resyncs itself to group", + expectedCtx: types.LogCtx{Desynced: false}, + inputCtx: types.LogCtx{Desynced: true}, + expectedOut: "node resyncs itself to group", + mapToTest: ApplicativeMap, + key: "RegexResync", + }, + + { + log: "{\"log\":\"2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [Galera] Member 1(node1) initiates vote on 8c9b5610-e020-11ed-a5ea-e253cc5f629d:20,bdb2b9234ae75cb3: some error, Error_code: 123;\n\",\"file\":\"/var/lib/mysql/mysqld-error.log\"}", + expectedOut: "inconsistency vote started by node1(seqno:20)", + expectedCtx: types.LogCtx{Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}}}}}, + mapToTest: ApplicativeMap, + key: "RegexInconsistencyVoteInit", + }, + { + log: "{\"log\":\"2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [Galera] Member 1(node1) initiates vote on 8c9b5610-e020-11ed-a5ea-e253cc5f629d:20,bdb2b9234ae75cb3: some error, Error_code: 123;\n\",\"file\":\"/var/lib/mysql/mysqld-error.log\"}", + inputCtx: types.LogCtx{OwnNames: []string{"node1"}}, + expectedCtx: types.LogCtx{OwnNames: []string{"node1"}, Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}}}}}, + expectedOut: "inconsistency vote started(seqno:20)", + mapToTest: ApplicativeMap, + key: "RegexInconsistencyVoteInit", + }, + + { + log: "{\"log\":\"2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [Galera] Member 2(node2) responds to vote on 8c9b5610-e020-11ed-a5ea-e253cc5f629d:20,0000000000000000: Success\n\",\"file\":\"/var/lib/mysql/mysqld-error.log\"}", + inputCtx: types.LogCtx{OwnNames: []string{"node2"}, Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}}}}}, + expectedCtx: types.LogCtx{OwnNames: []string{"node2"}, Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}, "node2": types.ConflictVote{MD5: "0000000000000000", Error: "Success"}}}}}, + expectedOut: "consistency vote(seqno:20): voted Success", + mapToTest: ApplicativeMap, + key: "RegexInconsistencyVoteRespond", + }, + { + log: "{\"log\":\"2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [Galera] Member 2(node2) responds to vote on 8c9b5610-e020-11ed-a5ea-e253cc5f629d:20,bdb2b9234ae75cb3: some error\n\",\"file\":\"/var/lib/mysql/mysqld-error.log\"}", + inputCtx: types.LogCtx{OwnNames: []string{"node2"}, Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}}}}}, + expectedCtx: types.LogCtx{OwnNames: []string{"node2"}, Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}, "node2": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}}}}}, + expectedOut: "consistency vote(seqno:20): voted same error", + mapToTest: ApplicativeMap, + key: "RegexInconsistencyVoteRespond", + }, + { + // could not actually find a "responds to" with any error for now + log: "{\"log\":\"2001-01-01T01:01:01.000000Z 0 [Note] [MY-000000] [Galera] Member 2(node2) responds to vote on 8c9b5610-e020-11ed-a5ea-e253cc5f629d:20,ed9774a3cad44656: some different error\n\",\"file\":\"/var/lib/mysql/mysqld-error.log\"}", + inputCtx: types.LogCtx{OwnNames: []string{"node2"}, Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}}}}}, + expectedCtx: types.LogCtx{OwnNames: []string{"node2"}, Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}, "node2": types.ConflictVote{MD5: "ed9774a3cad44656", Error: "some different error"}}}}}, + expectedOut: "consistency vote(seqno:20): voted different error", + mapToTest: ApplicativeMap, + key: "RegexInconsistencyVoteRespond", + }, + + { + log: "{\"log\":\"2001-01-01T01:01:01.000000Z 1 [ERROR] [MY-000000] [Galera] Inconsistency detected: Inconsistent by consensus on 8c9b5610-e020-11ed-a5ea-e253cc5f629d:127\n\t at galera/src/replicator_smm.cpp:process_apply_error():1469\n\",\"file\":\"/var/lib/mysql/mysqld-error.log\"}", + expectedOut: "found inconsistent by vote", + mapToTest: ApplicativeMap, + key: "RegexInconsistencyVoted", + }, + + { + log: "Winner: bdb2b9234ae75cb3", + inputCtx: types.LogCtx{OwnNames: []string{"node1"}, Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}}}}}, + expectedCtx: types.LogCtx{OwnNames: []string{"node1"}, Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Winner: "bdb2b9234ae75cb3", Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}}}}}, + expectedOut: "consistency vote(seqno:20): won", + mapToTest: ApplicativeMap, + key: "RegexInconsistencyWinner", + }, + { + log: "Winner: 0000000000000000", + inputCtx: types.LogCtx{OwnNames: []string{"node1"}, Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}, "node2": types.ConflictVote{MD5: "0000000000000000", Error: "Success"}}}}}, + expectedCtx: types.LogCtx{OwnNames: []string{"node1"}, Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Winner: "0000000000000000", Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}, "node2": types.ConflictVote{MD5: "0000000000000000", Error: "Success"}}}}}, + expectedOut: "consistency vote(seqno:20): lost", + mapToTest: ApplicativeMap, + key: "RegexInconsistencyWinner", + }, + { + name: "already voted conflict, should not print anything", + log: "Winner: 0000000000000000", + inputCtx: types.LogCtx{OwnNames: []string{"node1"}, Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Winner: "bdb2b9234ae75cb3", Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}}}}}, + expectedCtx: types.LogCtx{OwnNames: []string{"node1"}, Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Winner: "bdb2b9234ae75cb3", Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}}}}}, + displayerExpectedNil: true, + mapToTest: ApplicativeMap, + key: "RegexInconsistencyWinner", + }, + + { + log: "{\"log\":\"2001-01-01T01:01:01.000000Z 1 [ERROR] [MY-000000] [Galera] Recovering vote result from history: 8c9b5610-e020-11ed-a5ea-e253cc5f629d:20,bdb2b9234ae75cb3\n\",\"file\":\"/var/lib/mysql/mysqld-error.log\"}", + inputCtx: types.LogCtx{OwnNames: []string{"node2"}, Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Winner: "bdb2b9234ae75cb3", Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}}}}}, + expectedCtx: types.LogCtx{OwnNames: []string{"node2"}, Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Winner: "bdb2b9234ae75cb3", Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}, "node2": types.ConflictVote{MD5: "bdb2b9234ae75cb3"}}}}}, + expectedOut: "consistency vote(seqno:20): voted same error", + mapToTest: ApplicativeMap, + key: "RegexInconsistencyRecovery", + }, + { + log: "{\"log\":\"2001-01-01T01:01:01.000000Z 1 [ERROR] [MY-000000] [Galera] Recovering vote result from history: 8c9b5610-e020-11ed-a5ea-e253cc5f629d:20,0000000000000000\n\",\"file\":\"/var/lib/mysql/mysqld-error.log\"}", + inputCtx: types.LogCtx{OwnNames: []string{"node2"}, Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Winner: "bdb2b9234ae75cb3", Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}}}}}, + expectedCtx: types.LogCtx{OwnNames: []string{"node2"}, Conflicts: types.Conflicts{&types.Conflict{InitiatedBy: []string{"node1"}, Winner: "bdb2b9234ae75cb3", Seqno: "20", VotePerNode: map[string]types.ConflictVote{"node1": types.ConflictVote{MD5: "bdb2b9234ae75cb3", Error: "some error"}, "node2": types.ConflictVote{MD5: "0000000000000000"}}}}}, + expectedOut: "consistency vote(seqno:20): voted Success", + mapToTest: ApplicativeMap, + key: "RegexInconsistencyRecovery", + }, + } + + for _, test := range tests { + if test.name == "" { + test.name = "default" + } + if _, ok := test.mapToTest[test.key]; !ok { + t.Fatalf("key %s does not exist in maps", test.key) + } + err := testRegexFromMap(t, test.log, test.mapToTest[test.key]) + if err != nil { + if test.expectedErr { + continue + } + t.Fatalf("key: %s\ntestname: %s\nregex string: \"%s\"\nlog: %s\n", test.key, test.name, test.mapToTest[test.key].Regex.String(), err) + } + + if test.inputState != "" { + test.inputCtx.SetState(test.inputState) + } + + ctx, displayer := test.mapToTest[test.key].Handle(test.inputCtx, test.log) + msg := "" + if displayer != nil { + msg = displayer(ctx) + } else if !test.displayerExpectedNil { + t.Errorf("key: %s\ntestname: %s\ndisplayer is nil\nexpected: not nil", test.key, test.name) + } + + // alternative to reflect.deepequal, it enables to avoid comparing "states" map + res := cmp.Equal(ctx, test.expectedCtx, cmpopts.IgnoreUnexported(types.LogCtx{})) + if !res || msg != test.expectedOut || ctx.State() != test.expectedState { + t.Errorf("\nkey: %s\ntestname: %s\nctx: %+v\nexpected ctx: %+v\nout: %s\nexpected out: %s\nstate: %s\nexpected state: %s", test.key, test.name, spew.Sdump(ctx), spew.Sdump(test.expectedCtx), msg, test.expectedOut, ctx.State(), test.expectedState) + t.Fail() + } + } +} + +func testRegexFromMap(t *testing.T, log string, regex *types.LogRegex) error { + m := types.RegexMap{"test": regex} + + return testActualGrepOnLog(t, log, m.Compile()[0]) +} + +func testActualGrepOnLog(t *testing.T, log, regex string) error { + + f, err := ioutil.TempFile(t.TempDir(), "test_log") + if err != nil { + return errors.Wrap(err, "failed to create tmp file") + } + defer f.Sync() + + _, err = f.WriteString(log) + if err != nil { + return errors.Wrap(err, "failed to write in tmp file") + } + + out, err := exec.Command("grep", "-P", regex, f.Name()).Output() + if err != nil { + return errors.Wrap(err, "failed to grep in tmp file") + } + if string(out) == "" { + return errors.Wrap(err, "empty results when grepping in tmp file") + } + return nil +} diff --git a/src/go/pt-galera-log-explainer/regex/sst.go b/src/go/pt-galera-log-explainer/regex/sst.go new file mode 100644 index 00000000..6e57e0bb --- /dev/null +++ b/src/go/pt-galera-log-explainer/regex/sst.go @@ -0,0 +1,310 @@ +package regex + +import ( + "regexp" + + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/types" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/utils" +) + +func init() { + setType(types.SSTRegexType, SSTMap) +} + +var SSTMap = types.RegexMap{ + // TODO: requested state from unknown node + "RegexSSTRequestSuccess": &types.LogRegex{ + Regex: regexp.MustCompile("requested state transfer.*Selected"), + InternalRegex: regexp.MustCompile("Member .* \\(" + regexNodeName + "\\) requested state transfer.*Selected .* \\(" + regexNodeName2 + "\\)\\("), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + joiner := utils.ShortNodeName(submatches[groupNodeName]) + donor := utils.ShortNodeName(submatches[groupNodeName2]) + if utils.SliceContains(ctx.OwnNames, joiner) { + ctx.SST.ResyncedFromNode = donor + } + if utils.SliceContains(ctx.OwnNames, donor) { + ctx.SST.ResyncingNode = joiner + } + + return ctx, func(ctx types.LogCtx) string { + if utils.SliceContains(ctx.OwnNames, joiner) { + return donor + utils.Paint(utils.GreenText, " will resync local node") + } + if utils.SliceContains(ctx.OwnNames, donor) { + return utils.Paint(utils.GreenText, "local node will resync ") + joiner + } + + return donor + utils.Paint(utils.GreenText, " will resync ") + joiner + } + }, + Verbosity: types.Detailed, + }, + + "RegexSSTResourceUnavailable": &types.LogRegex{ + Regex: regexp.MustCompile("requested state transfer.*Resource temporarily unavailable"), + InternalRegex: regexp.MustCompile("Member .* \\(" + regexNodeName + "\\) requested state transfer"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + joiner := submatches[groupNodeName] + if utils.SliceContains(ctx.OwnNames, joiner) { + + return ctx, types.SimpleDisplayer(utils.Paint(utils.YellowText, "cannot find donor")) + } + + return ctx, types.SimpleDisplayer(joiner + utils.Paint(utils.YellowText, " cannot find donor")) + }, + }, + + // 2022-12-24T03:28:22.444125Z 0 [Note] WSREP: 0.0 (name): State transfer to 2.0 (name2) complete. + "RegexSSTComplete": &types.LogRegex{ + Regex: regexp.MustCompile("State transfer to.*complete"), + InternalRegex: regexp.MustCompile("\\(" + regexNodeName + "\\): State transfer.*\\(" + regexNodeName2 + "\\) complete"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + donor := utils.ShortNodeName(submatches[groupNodeName]) + joiner := utils.ShortNodeName(submatches[groupNodeName2]) + displayType := "SST" + if ctx.SST.Type != "" { + displayType = ctx.SST.Type + } + ctx.SST.Reset() + + ctx = addOwnNameWithSSTMetadata(ctx, joiner, donor) + + return ctx, func(ctx types.LogCtx) string { + if utils.SliceContains(ctx.OwnNames, joiner) { + return utils.Paint(utils.GreenText, "got "+displayType+" from ") + donor + } + if utils.SliceContains(ctx.OwnNames, donor) { + return utils.Paint(utils.GreenText, "finished sending "+displayType+" to ") + joiner + } + + return donor + utils.Paint(utils.GreenText, " synced ") + joiner + } + }, + }, + + // some weird ones: + // 2022-12-24T03:27:41.966118Z 0 [Note] WSREP: 0.0 (name): State transfer to -1.-1 (left the group) complete. + "RegexSSTCompleteUnknown": &types.LogRegex{ + Regex: regexp.MustCompile("State transfer to.*left the group.*complete"), + InternalRegex: regexp.MustCompile("\\(" + regexNodeName + "\\): State transfer.*\\(left the group\\) complete"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + donor := utils.ShortNodeName(submatches[groupNodeName]) + ctx = addOwnNameWithSSTMetadata(ctx, "", donor) + return ctx, types.SimpleDisplayer(donor + utils.Paint(utils.RedText, " synced ??(node left)")) + }, + }, + + "RegexSSTFailedUnknown": &types.LogRegex{ + Regex: regexp.MustCompile("State transfer to.*left the group.*failed"), + InternalRegex: regexp.MustCompile("\\(" + regexNodeName + "\\): State transfer.*\\(left the group\\) failed"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + donor := utils.ShortNodeName(submatches[groupNodeName]) + ctx = addOwnNameWithSSTMetadata(ctx, "", donor) + return ctx, types.SimpleDisplayer(donor + utils.Paint(utils.RedText, " failed to sync ??(node left)")) + }, + }, + + "RegexSSTStateTransferFailed": &types.LogRegex{ + Regex: regexp.MustCompile("State transfer to.*failed:"), + InternalRegex: regexp.MustCompile("\\(" + regexNodeName + "\\): State transfer.*\\(" + regexNodeName2 + "\\) failed"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + donor := utils.ShortNodeName(submatches[groupNodeName]) + joiner := utils.ShortNodeName(submatches[groupNodeName2]) + ctx = addOwnNameWithSSTMetadata(ctx, joiner, donor) + return ctx, types.SimpleDisplayer(donor + utils.Paint(utils.RedText, " failed to sync ") + joiner) + }, + }, + + "RegexSSTError": &types.LogRegex{ + Regex: regexp.MustCompile("Process completed with error: wsrep_sst"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "SST error")) + }, + }, + + "RegexSSTCancellation": &types.LogRegex{ + Regex: regexp.MustCompile("Initiating SST cancellation"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "former SST cancelled")) + }, + }, + + "RegexSSTProceeding": &types.LogRegex{ + Regex: regexp.MustCompile("Proceeding with SST"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.SetState("JOINER") + ctx.SST.Type = "SST" + + return ctx, types.SimpleDisplayer(utils.Paint(utils.YellowText, "receiving SST")) + }, + }, + + "RegexSSTStreamingTo": &types.LogRegex{ + Regex: regexp.MustCompile("Streaming the backup to"), + InternalRegex: regexp.MustCompile("Streaming the backup to joiner at " + regexNodeIP), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + ctx.SetState("DONOR") + node := submatches[groupNodeIP] + if ctx.SST.ResyncingNode == "" { // we should already have something at this point + ctx.SST.ResyncingNode = node + } + + return ctx, func(ctx types.LogCtx) string { + return utils.Paint(utils.YellowText, "SST to ") + types.DisplayNodeSimplestForm(ctx, node) + } + }, + }, + + "RegexISTReceived": &types.LogRegex{ + Regex: regexp.MustCompile("IST received"), + + // the UUID here is not from a node, it's a cluster state UUID, this is only used to ensure it's correctly parsed + InternalRegex: regexp.MustCompile("IST received: " + regexUUID + ":" + regexSeqno), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + seqno := submatches[groupSeqno] + return ctx, types.SimpleDisplayer(utils.Paint(utils.GreenText, "IST received") + "(seqno:" + seqno + ")") + }, + }, + + "RegexISTSender": &types.LogRegex{ + Regex: regexp.MustCompile("IST sender starting"), + + // TODO: sometimes, it's a hostname here + InternalRegex: regexp.MustCompile("IST sender starting to serve " + regexNodeIPMethod + " sending [0-9]+-" + regexSeqno), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.SST.Type = "IST" + ctx.SetState("DONOR") + + seqno := submatches[groupSeqno] + node := submatches[groupNodeIP] + + return ctx, func(ctx types.LogCtx) string { + return utils.Paint(utils.YellowText, "IST to ") + types.DisplayNodeSimplestForm(ctx, node) + "(seqno:" + seqno + ")" + } + }, + }, + + "RegexISTReceiver": &types.LogRegex{ + Regex: regexp.MustCompile("Prepared IST receiver"), + + InternalRegex: regexp.MustCompile("Prepared IST receiver( for (?P[0-9]+)-" + regexSeqno + ")?"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.SetState("JOINER") + + seqno := submatches[groupSeqno] + msg := utils.Paint(utils.YellowText, "will receive ") + + startingseqno := submatches["startingseqno"] + // if it's 0, it will go to SST without a doubt + if startingseqno == "0" { + ctx.SST.Type = "SST" + msg += "SST" + + // not totally correct, but need more logs to get proper pattern + // in some cases it does IST before going with SST + } else { + ctx.SST.Type = "IST" + msg += "IST" + if seqno != "" { + msg += "(seqno:" + seqno + ")" + } + } + return ctx, types.SimpleDisplayer(msg) + }, + }, + + "RegexFailedToPrepareIST": &types.LogRegex{ + Regex: regexp.MustCompile("Failed to prepare for incremental state transfer"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.SST.Type = "SST" + return ctx, types.SimpleDisplayer("IST is not applicable") + }, + }, + + // could not find production examples yet, but it did exist in older version there also was "Bypassing state dump" + "RegexBypassSST": &types.LogRegex{ + Regex: regexp.MustCompile("Bypassing SST"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.SST.Type = "IST" + return ctx, types.SimpleDisplayer("IST will be used") + }, + }, + + "RegexSocatConnRefused": &types.LogRegex{ + Regex: regexp.MustCompile("E connect.*Connection refused"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "socat: connection refused")) + }, + }, + + // 2023-05-12T02:52:33.767132Z 0 [Note] [MY-000000] [WSREP-SST] Preparing the backup at /var/lib/mysql/sst-xb-tmpdir + "RegexPreparingBackup": &types.LogRegex{ + Regex: regexp.MustCompile("Preparing the backup at"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + return ctx, types.SimpleDisplayer("preparing SST backup") + }, + Verbosity: types.Detailed, + }, + + "RegexTimeoutReceivingFirstData": &types.LogRegex{ + Regex: regexp.MustCompile("Possible timeout in receving first data from donor in gtid/keyring stage"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "timeout from donor in gtid/keyring stage")) + }, + }, + + "RegexWillNeverReceive": &types.LogRegex{ + Regex: regexp.MustCompile("Will never receive state. Need to abort"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "will never receive SST, aborting")) + }, + }, + + "RegexISTFailed": &types.LogRegex{ + Regex: regexp.MustCompile("async IST sender failed to serve"), + InternalRegex: regexp.MustCompile("IST sender failed to serve " + regexNodeIPMethod + ":.*asio error '.*: [0-9]+ \\((?P[\\w\\s]+)\\)"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + node := submatches[groupNodeIP] + istError := submatches["error"] + + return ctx, func(ctx types.LogCtx) string { + return "IST to " + types.DisplayNodeSimplestForm(ctx, node) + utils.Paint(utils.RedText, " failed: ") + istError + } + }, + }, +} + +func addOwnNameWithSSTMetadata(ctx types.LogCtx, joiner, donor string) types.LogCtx { + + var nameToAdd string + + if ctx.State() == "JOINER" && joiner != "" { + nameToAdd = joiner + } + if ctx.State() == "DONOR" && donor != "" { + nameToAdd = donor + } + if nameToAdd != "" { + ctx.AddOwnName(nameToAdd) + } + return ctx +} + +/* + +2023-06-07T02:42:29.734960-06:00 0 [ERROR] WSREP: sst sent called when not SST donor, state SYNCED +2023-06-07T02:42:00.234711-06:00 0 [Warning] WSREP: Protocol violation. JOIN message sender 0.0 (node1) is not in state transfer (SYNCED). Message ignored. + +) +*/ diff --git a/src/go/pt-galera-log-explainer/regex/states.go b/src/go/pt-galera-log-explainer/regex/states.go new file mode 100644 index 00000000..1f1075a9 --- /dev/null +++ b/src/go/pt-galera-log-explainer/regex/states.go @@ -0,0 +1,44 @@ +package regex + +import ( + "regexp" + + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/types" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/utils" +) + +func init() { + setType(types.StatesRegexType, StatesMap) +} + +var ( + shiftFunc = func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + ctx.SetState(submatches["state2"]) + log = utils.PaintForState(submatches["state1"], submatches["state1"]) + " -> " + utils.PaintForState(submatches["state2"], submatches["state2"]) + + return ctx, types.SimpleDisplayer(log) + } + shiftRegex = regexp.MustCompile("(?P[A-Z]+) -> (?P[A-Z]+)") +) + +var StatesMap = types.RegexMap{ + "RegexShift": &types.LogRegex{ + Regex: regexp.MustCompile("Shifting"), + InternalRegex: shiftRegex, + Handler: shiftFunc, + }, + + "RegexRestoredState": &types.LogRegex{ + Regex: regexp.MustCompile("Restored state"), + InternalRegex: shiftRegex, + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + var displayer types.LogDisplayer + ctx, displayer = shiftFunc(submatches, ctx, log) + + return ctx, types.SimpleDisplayer("(restored)" + displayer(ctx)) + }, + }, +} + +// [Note] [MY-000000] [WSREP] Server status change connected -> joiner diff --git a/src/go/pt-galera-log-explainer/regex/views.go b/src/go/pt-galera-log-explainer/regex/views.go new file mode 100644 index 00000000..692db875 --- /dev/null +++ b/src/go/pt-galera-log-explainer/regex/views.go @@ -0,0 +1,257 @@ +package regex + +import ( + "regexp" + "strconv" + "strings" + + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/types" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/utils" +) + +func init() { + setType(types.ViewsRegexType, ViewsMap) +} + +// "galera views" regexes +var ViewsMap = types.RegexMap{ + "RegexNodeEstablished": &types.LogRegex{ + Regex: regexp.MustCompile("connection established"), + InternalRegex: regexp.MustCompile("established to " + regexNodeHash + " " + regexNodeIPMethod), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + ip := submatches[groupNodeIP] + ctx.HashToIP[submatches[groupNodeHash]] = ip + if utils.SliceContains(ctx.OwnIPs, ip) { + return ctx, nil + } + return ctx, func(ctx types.LogCtx) string { return types.DisplayNodeSimplestForm(ctx, ip) + " established" } + }, + Verbosity: types.DebugMySQL, + }, + + "RegexNodeJoined": &types.LogRegex{ + Regex: regexp.MustCompile("declaring .* stable"), + InternalRegex: regexp.MustCompile("declaring " + regexNodeHash + " at " + regexNodeIPMethod), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + ip := submatches[groupNodeIP] + ctx.HashToIP[submatches[groupNodeHash]] = ip + ctx.IPToMethod[ip] = submatches[groupMethod] + return ctx, func(ctx types.LogCtx) string { + return types.DisplayNodeSimplestForm(ctx, ip) + utils.Paint(utils.GreenText, " joined") + } + }, + }, + + "RegexNodeLeft": &types.LogRegex{ + Regex: regexp.MustCompile("forgetting"), + InternalRegex: regexp.MustCompile("forgetting " + regexNodeHash + " \\(" + regexNodeIPMethod), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + ip := submatches[groupNodeIP] + return ctx, func(ctx types.LogCtx) string { + return types.DisplayNodeSimplestForm(ctx, ip) + utils.Paint(utils.RedText, " left") + } + }, + }, + + // New COMPONENT: primary = yes, bootstrap = no, my_idx = 1, memb_num = 5 + "RegexNewComponent": &types.LogRegex{ + Regex: regexp.MustCompile("New COMPONENT:"), + InternalRegex: regexp.MustCompile("New COMPONENT: primary = (?P.+), bootstrap = (?P.*), my_idx = .*, memb_num = (?P[0-9]{1,2})"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + primary := submatches["primary"] == "yes" + membNum := submatches["memb_num"] + bootstrap := submatches["bootstrap"] == "yes" + memberCount, err := strconv.Atoi(membNum) + if err != nil { + return ctx, nil + } + + ctx.MemberCount = memberCount + if primary { + // we don't always store PRIMARY because we could have found DONOR/JOINER/SYNCED/DESYNCED just earlier + // and we do not want to override these as they have more value + if !ctx.IsPrimary() { + ctx.SetState("PRIMARY") + } + msg := utils.Paint(utils.GreenText, "PRIMARY") + "(n=" + membNum + ")" + if bootstrap { + msg += ",bootstrap" + } + return ctx, types.SimpleDisplayer(msg) + } + + ctx.SetState("NON-PRIMARY") + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "NON-PRIMARY") + "(n=" + membNum + ")") + }, + }, + + "RegexNodeSuspect": &types.LogRegex{ + Regex: regexp.MustCompile("suspecting node"), + InternalRegex: regexp.MustCompile("suspecting node: " + regexNodeHash), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + hash := submatches[groupNodeHash] + ip, ok := ctx.HashToIP[hash] + if ok { + return ctx, func(ctx types.LogCtx) string { + return types.DisplayNodeSimplestForm(ctx, ip) + utils.Paint(utils.YellowText, " suspected to be down") + } + } + return ctx, types.SimpleDisplayer(hash + utils.Paint(utils.YellowText, " suspected to be down")) + }, + Verbosity: types.Detailed, + }, + + "RegexNodeChangedIdentity": &types.LogRegex{ + Regex: regexp.MustCompile("remote endpoint.*changed identity"), + InternalRegex: regexp.MustCompile("remote endpoint " + regexNodeIPMethod + " changed identity " + regexNodeHash + " -> " + strings.Replace(regexNodeHash, groupNodeHash, groupNodeHash+"2", -1)), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + + hash := submatches[groupNodeHash] + hash2 := submatches[groupNodeHash+"2"] + ip, ok := ctx.HashToIP[hash] + if !ok && IsNodeUUID(hash) { + ip, ok = ctx.HashToIP[utils.UUIDToShortUUID(hash)] + + // there could have additional corner case to discover yet + if !ok { + return ctx, types.SimpleDisplayer(hash + utils.Paint(utils.YellowText, " changed identity")) + } + hash2 = utils.UUIDToShortUUID(hash2) + } + ctx.HashToIP[hash2] = ip + return ctx, func(ctx types.LogCtx) string { + return types.DisplayNodeSimplestForm(ctx, ip) + utils.Paint(utils.YellowText, " changed identity") + } + }, + Verbosity: types.Detailed, + }, + + "RegexWsrepUnsafeBootstrap": &types.LogRegex{ + Regex: regexp.MustCompile("ERROR.*not be safe to bootstrap"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.SetState("CLOSED") + + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "not safe to bootstrap")) + }, + }, + "RegexWsrepConsistenctyCompromised": &types.LogRegex{ + Regex: regexp.MustCompile(".ode consistency compromi.ed"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + ctx.SetState("CLOSED") + + return ctx, types.SimpleDisplayer(utils.Paint(utils.RedText, "consistency compromised")) + }, + }, + "RegexWsrepNonPrimary": &types.LogRegex{ + Regex: regexp.MustCompile("failed to reach primary view"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + return ctx, types.SimpleDisplayer("received " + utils.Paint(utils.RedText, "non primary")) + }, + }, + + "RegexBootstrap": &types.LogRegex{ + Regex: regexp.MustCompile("gcomm: bootstrapping new group"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + return ctx, types.SimpleDisplayer(utils.Paint(utils.YellowText, "bootstrapping")) + }, + }, + + "RegexSafeToBoostrapSet": &types.LogRegex{ + Regex: regexp.MustCompile("safe_to_bootstrap: 1"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + return ctx, types.SimpleDisplayer(utils.Paint(utils.YellowText, "safe_to_bootstrap: 1")) + }, + }, + "RegexNoGrastate": &types.LogRegex{ + Regex: regexp.MustCompile("Could not open state file for reading.*grastate.dat"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + return ctx, types.SimpleDisplayer(utils.Paint(utils.YellowText, "no grastate.dat file")) + }, + Verbosity: types.Detailed, + }, + "RegexBootstrapingDefaultState": &types.LogRegex{ + Regex: regexp.MustCompile("Bootstraping with default state"), + Handler: func(submatches map[string]string, ctx types.LogCtx, log string) (types.LogCtx, types.LogDisplayer) { + return ctx, types.SimpleDisplayer(utils.Paint(utils.YellowText, "bootstrapping(empty grastate)")) + }, + }, +} + +/* + +2022-11-29T23:34:51.820009-05:00 0 [Warning] [MY-000000] [Galera] Could not find peer: c0ff4085-5ad7-11ed-8b74-cfeec74147fe + +2022-12-07 1:00:06 0 [Note] WSREP: Member 0.0 (node) synced with group. + + +2021-03-25T21:58:13.570928Z 0 [Warning] WSREP: no nodes coming from prim view, prim not possible +2021-03-25T21:58:13.855983Z 0 [Warning] WSREP: Quorum: No node with complete state: + + + +2021-04-22T08:01:05.000581Z 0 [Warning] WSREP: Failed to report last committed 66328091, -110 (Connection timed out) + + +input_map=evs::input_map: {aru_seq=8,safe_seq=8,node_index=node: {idx=0,range=[9,8],safe_seq=8} node: {idx=1,range=[9,8],safe_seq=8} }, +fifo_seq=4829086170, +last_sent=8, +known: +17a2e064 at tcp://ip:4567 +{o=0,s=1,i=0,fs=-1,} +470a6438 at tcp://ip:4567 +{o=1,s=0,i=0,fs=4829091361,jm= +{v=0,t=4,ut=255,o=1,s=8,sr=-1,as=8,f=4,src=470a6438,srcvid=view_id(REG,470a6438,24),insvid=view_id(UNKNOWN,00000000,0),ru=00000000,r=[-1,-1],fs=4829091361,nl=( + 17a2e064, {o=0,s=1,e=0,ls=-1,vid=view_id(REG,00000000,0),ss=-1,ir=[-1,-1],} + 470a6438, {o=1,s=0,e=0,ls=-1,vid=view_id(REG,470a6438,24),ss=8,ir=[9,8],} + 6548cf50, {o=1,s=1,e=0,ls=-1,vid=view_id(REG,17a2e064,24),ss=12,ir=[13,12],} + 8b0c0f77, {o=1,s=0,e=0,ls=-1,vid=view_id(REG,470a6438,24),ss=8,ir=[9,8],} + d4397932, {o=0,s=1,e=0,ls=-1,vid=view_id(REG,00000000,0),ss=-1,ir=[-1,-1],} +) +}, +} +6548cf50 at tcp://ip:4567 +{o=1,s=1,i=0,fs=-1,jm= +{v=0,t=4,ut=255,o=1,s=12,sr=-1,as=12,f=4,src=6548cf50,srcvid=view_id(REG,17a2e064,24),insvid=view_id(UNKNOWN,00000000,0),ru=00000000,r=[-1,-1],fs=4829165031,nl=( + 17a2e064, {o=1,s=0,e=0,ls=-1,vid=view_id(REG,17a2e064,24),ss=12,ir=[13,12],} + 470a6438, {o=1,s=0,e=0,ls=-1,vid=view_id(REG,00000000,0),ss=-1,ir=[-1,-1],} + 6548cf50, {o=1,s=0,e=0,ls=-1,vid=view_id(REG,17a2e064,24),ss=12,ir=[13,12],} + 8b0c0f77, {o=1,s=0,e=0,ls=-1,vid=view_id(REG,00000000,0),ss=-1,ir=[-1,-1],} + d4397932, {o=1,s=0,e=0,ls=-1,vid=view_id(REG,17a2e064,24),ss=12,ir=[13,12],} +) +}, +} +8b0c0f77 at +{o=1,s=0,i=0,fs=-1,jm= +{v=0,t=4,ut=255,o=1,s=8,sr=-1,as=8,f=0,src=8b0c0f77,srcvid=view_id(REG,470a6438,24),insvid=view_id(UNKNOWN,00000000,0),ru=00000000,r=[-1,-1],fs=4829086170,nl=( + 17a2e064, {o=0,s=1,e=0,ls=-1,vid=view_id(REG,00000000,0),ss=-1,ir=[-1,-1],} + 470a6438, {o=1,s=0,e=0,ls=-1,vid=view_id(REG,470a6438,24),ss=8,ir=[9,8],} + 6548cf50, {o=1,s=1,e=0,ls=-1,vid=view_id(REG,17a2e064,24),ss=12,ir=[13,12],} + 8b0c0f77, {o=1,s=0,e=0,ls=-1,vid=view_id(REG,470a6438,24),ss=8,ir=[9,8],} + d4397932, {o=0,s=1,e=0,ls=-1,vid=view_id(REG,00000000,0),ss=-1,ir=[-1,-1],} +) +}, +} +d4397932 at tcp://ip:4567 +{o=0,s=1,i=0,fs=4685894552,} + } + + Transport endpoint is not connected + + + 2023-03-31T08:05:57.964535Z 0 [Note] WSREP: handshake failed, my group: '', peer group: '' + + 2023-04-04T22:35:23.487304Z 0 [Warning] [MY-000000] [Galera] Handshake failed: tlsv1 alert decrypt error + + 2023-04-16T19:35:06.875877Z 0 [Warning] [MY-000000] [Galera] Action message in non-primary configuration from member 0 + +{"log":"2023-06-10T04:50:46.835491Z 0 [Note] [MY-000000] [Galera] going to give up, state dump for diagnosis:\nevs::proto(evs::proto(6d0345f5-bcc0, GATHER, view_id(REG,02e369be-8363,1046)), GATHER) {\ncurrent_view=Current view of cluster as seen by this node\nview (view_id(REG,02e369be-8363,1046)\nmemb {\n\t02e369be-8363,0\n\t49761f3d-bd34,0\n\t6d0345f5-bcc0,0\n\tb05443d1-96bf,0\n\tb05443d1-96c0,0\n\t}\njoined {\n\t}\nleft {\n\t}\npartitioned {\n\t}\n),\ninput_map=evs::input_map: {aru_seq=461,safe_seq=461,node_index=node: {idx=0,range=[462,461],safe_seq=461} node: {idx=1,range=[462,461],safe_seq=461} node: {idx=2,range=[462,461],safe_seq=461} node: {idx=3,range=[462,461],safe_seq=461} node: {idx=4,range=[462,461],safe_seq=461} },\nfifo_seq=221418422,\nlast_sent=461,\nknown:\n","file":"/var/lib/mysql/mysqld-error.log"} + + +[Warning] WSREP: FLOW message from member -12921743687968 in non-primary configuration. Ignored. + +*/ diff --git a/src/go/pt-galera-log-explainer/regexList.go b/src/go/pt-galera-log-explainer/regexList.go new file mode 100644 index 00000000..2b7bbd1c --- /dev/null +++ b/src/go/pt-galera-log-explainer/regexList.go @@ -0,0 +1,39 @@ +package main + +import ( + "encoding/json" + "fmt" + + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/regex" + "github.com/pkg/errors" +) + +type regexList struct { + Json bool +} + +func (l *regexList) Help() string { + return "List available regexes. Can be used to exclude them later" +} + +func (l *regexList) Run() error { + + allregexes := regex.AllRegexes() + allregexes.Merge(regex.PXCOperatorMap) + + if l.Json { + out, err := json.Marshal(&allregexes) + if err != nil { + return errors.Wrap(err, "could not marshal regexes") + } + fmt.Println(string(out)) + return nil + } + keys := []string{} + for k := range allregexes { + keys = append(keys, k) + + } + fmt.Println(keys) + return nil +} diff --git a/src/go/pt-galera-log-explainer/sed.go b/src/go/pt-galera-log-explainer/sed.go new file mode 100644 index 00000000..13453bcf --- /dev/null +++ b/src/go/pt-galera-log-explainer/sed.go @@ -0,0 +1,110 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/regex" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/types" + "github.com/pkg/errors" +) + +type sed struct { + Paths []string `arg:"" name:"paths" help:"paths of the log to use"` + ByIP bool `help:"Replace by IP instead of name"` +} + +func (s *sed) Help() string { + return `sed translates a log, replacing node UUID, IPS, names with either name or IP everywhere. By default it replaces by name. + +Use like so: + cat node1.log | galera-log-explainer sed *.log | less + galera-log-explainer sed *.log < node1.log | less + +You can also simply call the command to get a generated sed command to review and apply yourself + galera-log-explainer sed *.log` +} + +func (s *sed) Run() error { + toCheck := regex.AllRegexes() + timeline, err := timelineFromPaths(s.Paths, toCheck, CLI.Since, CLI.Until) + if err != nil { + return errors.Wrap(err, "Found nothing worth replacing") + } + ctxs := timeline.GetLatestUpdatedContextsByNodes() + + args := []string{} + for key, ctx := range ctxs { + + tosearchs := []string{key} + tosearchs = append(tosearchs, ctx.OwnHashes...) + tosearchs = append(tosearchs, ctx.OwnIPs...) + tosearchs = append(tosearchs, ctx.OwnNames...) + for _, tosearch := range tosearchs { + ni := whoIs(ctxs, tosearch) + + fmt.Println(ni) + switch { + case CLI.Sed.ByIP: + args = append(args, sedByIP(ni)...) + default: + args = append(args, sedByName(ni)...) + } + } + + } + if len(args) == 0 { + return errors.New("Could not find informations to replace") + } + + fstat, err := os.Stdin.Stat() + if err != nil { + return err + } + if fstat.Size() == 0 { + fmt.Println("No files found in stdin, returning the sed command instead:") + fmt.Println("sed", strings.Join(args, " ")) + return nil + } + + cmd := exec.Command("sed", args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Start() + if err != nil { + return err + } + return cmd.Wait() +} + +func sedByName(ni types.NodeInfo) []string { + if len(ni.NodeNames) == 0 { + return nil + } + elem := ni.NodeNames[0] + args := sedSliceWith(ni.NodeUUIDs, elem) + args = append(args, sedSliceWith(ni.IPs, elem)...) + return args +} + +func sedByIP(ni types.NodeInfo) []string { + if len(ni.IPs) == 0 { + return nil + } + elem := ni.IPs[0] + args := sedSliceWith(ni.NodeUUIDs, elem) + args = append(args, sedSliceWith(ni.NodeNames, elem)...) + return args +} + +func sedSliceWith(elems []string, replace string) []string { + args := []string{} + for _, elem := range elems { + args = append(args, "-e") + args = append(args, "s/"+elem+"/"+replace+"/g") + } + return args +} diff --git a/src/go/pt-galera-log-explainer/types/conflicts.go b/src/go/pt-galera-log-explainer/types/conflicts.go new file mode 100644 index 00000000..4f3bf455 --- /dev/null +++ b/src/go/pt-galera-log-explainer/types/conflicts.go @@ -0,0 +1,57 @@ +package types + +type Conflicts []*Conflict + +type Conflict struct { + Seqno string + InitiatedBy []string + Winner string // winner will help the winning md5sum + VotePerNode map[string]ConflictVote +} + +type ConflictVote struct { + MD5 string + Error string +} + +func (cs Conflicts) Merge(c Conflict) Conflicts { + for i := range cs { + if c.Seqno == cs[i].Seqno { + for node, vote := range c.VotePerNode { + cs[i].VotePerNode[node] = vote + } + return cs + } + } + + return append(cs, &c) +} + +func (cs Conflicts) ConflictWithSeqno(seqno string) *Conflict { + // technically could make it a binary search, seqno should be ever increasing + for _, c := range cs { + if seqno == c.Seqno { + return c + } + } + return nil +} + +func (cs Conflicts) OldestUnresolved() *Conflict { + for _, c := range cs { + if c.Winner == "" { + return c + } + } + return nil +} +func (cs Conflicts) ConflictFromMD5(md5 string) *Conflict { + for _, c := range cs { + for _, vote := range c.VotePerNode { + if vote.MD5 == md5 { + return c + } + } + } + return nil +} diff --git a/src/go/pt-galera-log-explainer/types/ctx.go b/src/go/pt-galera-log-explainer/types/ctx.go new file mode 100644 index 00000000..5042161e --- /dev/null +++ b/src/go/pt-galera-log-explainer/types/ctx.go @@ -0,0 +1,281 @@ +package types + +import ( + "encoding/json" + + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/utils" +) + +// LogCtx is a context for a given file. +// It is the principal storage of this tool +// Everything relevant will be stored here +type LogCtx struct { + FilePath string + FileType string + OwnIPs []string + OwnHashes []string + OwnNames []string + stateErrorLog string + stateRecoveryLog string + statePostProcessingLog string + stateBackupLog string + Version string + SST SST + MyIdx string + MemberCount int + Desynced bool + HashToIP map[string]string + HashToNodeName map[string]string + IPToHostname map[string]string + IPToMethod map[string]string + IPToNodeName map[string]string + minVerbosity Verbosity + Conflicts Conflicts +} + +func NewLogCtx() LogCtx { + return LogCtx{minVerbosity: Debug, HashToIP: map[string]string{}, IPToHostname: map[string]string{}, IPToMethod: map[string]string{}, IPToNodeName: map[string]string{}, HashToNodeName: map[string]string{}} +} + +// State will return the wsrep state of the current file type +// That is because for operator related logs, we have every type of files +// Not tracking and differenciating by file types led to confusions in most subcommands +// as it would seem that sometimes mysql is restarting after a crash, while actually +// the operator was simply launching a "wsrep-recover" instance while mysql was still running +func (ctx LogCtx) State() string { + switch ctx.FileType { + case "post.processing.log": + return ctx.statePostProcessingLog + case "recovery.log": + return ctx.stateRecoveryLog + case "backup.log": + return ctx.stateBackupLog + case "error.log": + fallthrough + default: + return ctx.stateErrorLog + } +} + +// SetState will double-check if the STATE exists, and also store it on the correct status +func (ctx *LogCtx) SetState(s string) { + + // NON-PRIMARY and RECOVERY are not a real wsrep state, but it's helpful here + // DONOR and DESYNCED are merged in wsrep, but we are able to distinguish here + // list at gcs/src/gcs.cpp, gcs_conn_state_str + if !utils.SliceContains([]string{"SYNCED", "JOINED", "DONOR", "DESYNCED", "JOINER", "PRIMARY", "NON-PRIMARY", "OPEN", "CLOSED", "DESTROYED", "ERROR", "RECOVERY"}, s) { + return + } + //ctx.state[ctx.FileType] = append(ctx.state[ctx.FileType], s) + switch ctx.FileType { + case "post.processing.log": + ctx.statePostProcessingLog = s + case "recovery.log": + ctx.stateRecoveryLog = s + case "backup.log": + ctx.stateBackupLog = s + case "error.log": + fallthrough + default: + ctx.stateErrorLog = s + } +} + +func (ctx *LogCtx) HasVisibleEvents(level Verbosity) bool { + return level >= ctx.minVerbosity +} + +func (ctx *LogCtx) IsPrimary() bool { + return utils.SliceContains([]string{"SYNCED", "DONOR", "DESYNCED", "JOINER", "PRIMARY"}, ctx.State()) +} + +func (ctx *LogCtx) OwnHostname() string { + for _, ip := range ctx.OwnIPs { + if hn, ok := ctx.IPToHostname[ip]; ok { + return hn + } + } + for _, hash := range ctx.OwnHashes { + if hn, ok := ctx.IPToHostname[ctx.HashToIP[hash]]; ok { + return hn + } + } + return "" +} + +func (ctx *LogCtx) HashesFromIP(ip string) []string { + hashes := []string{} + for hash, ip2 := range ctx.HashToIP { + if ip == ip2 { + hashes = append(hashes, hash) + } + } + return hashes +} + +func (ctx *LogCtx) HashesFromNodeName(nodename string) []string { + hashes := []string{} + for hash, nodename2 := range ctx.HashToNodeName { + if nodename == nodename2 { + hashes = append(hashes, hash) + } + } + return hashes +} + +func (ctx *LogCtx) IPsFromNodeName(nodename string) []string { + ips := []string{} + for ip, nodename2 := range ctx.IPToNodeName { + if nodename == nodename2 { + ips = append(ips, ip) + } + } + return ips +} + +func (ctx *LogCtx) AllNodeNames() []string { + nodenames := ctx.OwnNames + for _, nn := range ctx.HashToNodeName { + if !utils.SliceContains(nodenames, nn) { + nodenames = append(nodenames, nn) + } + } + for _, nn := range ctx.IPToNodeName { + if !utils.SliceContains(nodenames, nn) { + nodenames = append(nodenames, nn) + } + } + return nodenames +} + +// AddOwnName propagates a name into the translation maps using the trusted node's known own hashes and ips +func (ctx *LogCtx) AddOwnName(name string) { + // used to be a simple "if utils.SliceContains", changed to "is it the last known name?" + // because somes names/ips come back and forth, we should keep track of that + name = utils.ShortNodeName(name) + if len(ctx.OwnNames) > 0 && ctx.OwnNames[len(ctx.OwnNames)-1] == name { + return + } + ctx.OwnNames = append(ctx.OwnNames, name) + for _, hash := range ctx.OwnHashes { + + ctx.HashToNodeName[hash] = name + } + for _, ip := range ctx.OwnIPs { + ctx.IPToNodeName[ip] = name + } +} + +// AddOwnHash propagates a hash into the translation maps +func (ctx *LogCtx) AddOwnHash(hash string) { + if utils.SliceContains(ctx.OwnHashes, hash) { + return + } + ctx.OwnHashes = append(ctx.OwnHashes, hash) + + for _, ip := range ctx.OwnIPs { + ctx.HashToIP[hash] = ip + } + for _, name := range ctx.OwnNames { + ctx.HashToNodeName[hash] = name + } +} + +// AddOwnIP propagates a ip into the translation maps +func (ctx *LogCtx) AddOwnIP(ip string) { + // see AddOwnName comment + if len(ctx.OwnIPs) > 0 && ctx.OwnIPs[len(ctx.OwnIPs)-1] == ip { + return + } + ctx.OwnIPs = append(ctx.OwnIPs, ip) + for _, hash := range ctx.OwnHashes { + ctx.HashToIP[hash] = ip + } + for _, name := range ctx.OwnNames { + ctx.IPToNodeName[ip] = name + } +} + +// MergeMapsWith will take a slice of contexts and merge every translation maps +// into the base context. It won't touch "local" infos such as "ownNames" +func (base *LogCtx) MergeMapsWith(ctxs []LogCtx) { + for _, ctx := range ctxs { + for hash, ip := range ctx.HashToIP { + base.HashToIP[hash] = ip + } + for hash, nodename := range ctx.HashToNodeName { + + base.HashToNodeName[hash] = nodename + } + for ip, hostname := range ctx.IPToHostname { + base.IPToHostname[ip] = hostname + } + for ip, nodename := range ctx.IPToNodeName { + base.IPToNodeName[ip] = nodename + } + for ip, method := range ctx.IPToMethod { + base.IPToMethod[ip] = method + } + } +} + +// Inherit will fill the local information from given context +// into the base +// It is used when merging, so that we do not start from nothing +// It helps when dealing with many small files +func (base *LogCtx) Inherit(ctx LogCtx) { + base.OwnHashes = append(ctx.OwnHashes, base.OwnHashes...) + base.OwnNames = append(ctx.OwnNames, base.OwnNames...) + base.OwnIPs = append(ctx.OwnIPs, base.OwnIPs...) + if base.Version == "" { + base.Version = ctx.Version + } + base.MergeMapsWith([]LogCtx{ctx}) +} + +func (l LogCtx) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + FilePath string + FileType string + OwnIPs []string + OwnHashes []string + OwnNames []string + StateErrorLog string + StateRecoveryLog string + StatePostProcessingLog string + StateBackupLog string + Version string + SST SST + MyIdx string + MemberCount int + Desynced bool + HashToIP map[string]string + HashToNodeName map[string]string + IPToHostname map[string]string + IPToMethod map[string]string + IPToNodeName map[string]string + MinVerbosity Verbosity + Conflicts Conflicts + }{ + FilePath: l.FilePath, + FileType: l.FileType, + OwnIPs: l.OwnIPs, + OwnHashes: l.OwnHashes, + StateErrorLog: l.stateErrorLog, + StateRecoveryLog: l.stateRecoveryLog, + StatePostProcessingLog: l.statePostProcessingLog, + StateBackupLog: l.stateBackupLog, + Version: l.Version, + SST: l.SST, + MyIdx: l.MyIdx, + MemberCount: l.MemberCount, + Desynced: l.Desynced, + HashToIP: l.HashToIP, + HashToNodeName: l.HashToNodeName, + IPToHostname: l.IPToHostname, + IPToMethod: l.IPToMethod, + IPToNodeName: l.IPToNodeName, + MinVerbosity: l.minVerbosity, + Conflicts: l.Conflicts, + }) +} diff --git a/src/go/pt-galera-log-explainer/types/loginfo.go b/src/go/pt-galera-log-explainer/types/loginfo.go new file mode 100644 index 00000000..9cd923f5 --- /dev/null +++ b/src/go/pt-galera-log-explainer/types/loginfo.go @@ -0,0 +1,97 @@ +package types + +import ( + "fmt" + "time" + + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/utils" +) + +type Verbosity int + +const ( + Info Verbosity = iota + // Detailed is having every suspect/warn + Detailed + // DebugMySQL only includes finding that are usually not relevant to show but useful to create the log context (eg: how we found the local address) + DebugMySQL + Debug +) + +// LogInfo is to store a single event in log. This is something that should be displayed ultimately, this is what we want when we launch this tool +type LogInfo struct { + Date *Date + displayer LogDisplayer // what to show + Log string // the raw log + RegexType RegexType + RegexUsed string + Ctx LogCtx // the context is copied for each logInfo, so that it is easier to handle some info (current state), and this is also interesting to check how it evolved + Verbosity Verbosity + RepetitionCount int + extraNotes map[string]string +} + +func NewLogInfo(date *Date, displayer LogDisplayer, log string, regex *LogRegex, regexkey string, ctx LogCtx, filetype string) LogInfo { + li := LogInfo{ + Date: date, + Log: log, + displayer: displayer, + Ctx: ctx, + RegexType: regex.Type, + RegexUsed: regexkey, + Verbosity: regex.Verbosity, + extraNotes: map[string]string{}, + } + if filetype != "error.log" && filetype != "" { + li.extraNotes["filetype"] = filetype + } + return li +} + +func (li *LogInfo) Msg(ctx LogCtx) string { + if li.displayer == nil { + return "" + } + msg := "" + if li.RepetitionCount > 0 { + msg += utils.Paint(utils.BlueText, fmt.Sprintf("(repeated x%d)", li.RepetitionCount)) + } + msg += li.displayer(ctx) + for _, note := range li.extraNotes { + msg += utils.Paint(utils.BlueText, fmt.Sprintf("(%s)", note)) + } + return msg +} + +// IsDuplicatedEvent will aim to keep 2 occurences of the same event +// To be considered duplicated, they must be from the same regexes and have the same message +func (current *LogInfo) IsDuplicatedEvent(base, previous LogInfo) bool { + return base.RegexUsed == previous.RegexUsed && + base.displayer != nil && previous.displayer != nil && current.displayer != nil && + base.displayer(base.Ctx) == previous.displayer(previous.Ctx) && + previous.RegexUsed == current.RegexUsed && + previous.displayer(previous.Ctx) == current.displayer(current.Ctx) +} + +type Date struct { + Time time.Time + DisplayTime string + Layout string +} + +func NewDate(t time.Time, layout string) Date { + return Date{ + Time: t, + Layout: layout, + DisplayTime: t.Format(layout), + } +} + +// LogDisplayer is the handler to generate messages thanks to a context +// The context in parameters should be as updated as possible +type LogDisplayer func(LogCtx) string + +// SimpleDisplayer satisfies LogDisplayer and ignores any context received +func SimpleDisplayer(s string) LogDisplayer { + return func(_ LogCtx) string { return s } +} diff --git a/src/go/pt-galera-log-explainer/types/loginfo_test.go b/src/go/pt-galera-log-explainer/types/loginfo_test.go new file mode 100644 index 00000000..407cfc12 --- /dev/null +++ b/src/go/pt-galera-log-explainer/types/loginfo_test.go @@ -0,0 +1,53 @@ +package types + +import ( + "testing" +) + +func TestIsDuplicatedEvent(t *testing.T) { + + tests := []struct { + name string + inputbase LogInfo + inputprevious LogInfo + inputcurrent LogInfo + expected bool + }{ + { + name: "different regex, same output", + inputbase: LogInfo{RegexUsed: "some regex", displayer: SimpleDisplayer("")}, + inputprevious: LogInfo{RegexUsed: "some other regex", displayer: SimpleDisplayer("")}, + inputcurrent: LogInfo{RegexUsed: "yet another regex", displayer: SimpleDisplayer("")}, + expected: false, + }, + { + name: "same regex, different output", + inputbase: LogInfo{RegexUsed: "same regex", displayer: SimpleDisplayer("out1")}, + inputprevious: LogInfo{RegexUsed: "same regex", displayer: SimpleDisplayer("out2")}, + inputcurrent: LogInfo{RegexUsed: "same regex", displayer: SimpleDisplayer("out3")}, + expected: false, + }, + { + name: "not enough duplication yet", + inputbase: LogInfo{RegexUsed: "another regex", displayer: SimpleDisplayer("")}, + inputprevious: LogInfo{RegexUsed: "same regex", displayer: SimpleDisplayer("same")}, + inputcurrent: LogInfo{RegexUsed: "same regex", displayer: SimpleDisplayer("same")}, + expected: false, + }, + { + name: "duplicated", + inputbase: LogInfo{RegexUsed: "same regex", displayer: SimpleDisplayer("same")}, + inputprevious: LogInfo{RegexUsed: "same regex", displayer: SimpleDisplayer("same")}, + inputcurrent: LogInfo{RegexUsed: "same regex", displayer: SimpleDisplayer("same")}, + expected: true, + }, + } + + for _, test := range tests { + out := test.inputcurrent.IsDuplicatedEvent(test.inputbase, test.inputprevious) + if out != test.expected { + t.Fatalf("%s failed: expected %v, got %v", test.name, test.expected, out) + } + } + +} diff --git a/src/go/pt-galera-log-explainer/types/nodeinfo.go b/src/go/pt-galera-log-explainer/types/nodeinfo.go new file mode 100644 index 00000000..d1229050 --- /dev/null +++ b/src/go/pt-galera-log-explainer/types/nodeinfo.go @@ -0,0 +1,12 @@ +package types + +// NodeInfo is mainly used by "whois" subcommand +// This is to display its result +// As it's the base work for "sed" subcommand, it's in types package +type NodeInfo struct { + Input string `json:"input"` + IPs []string `json:"IPs"` + NodeNames []string `json:"nodeNames"` + Hostname string `json:"hostname"` + NodeUUIDs []string `json:"nodeUUIDs:"` +} diff --git a/src/go/pt-galera-log-explainer/types/regex.go b/src/go/pt-galera-log-explainer/types/regex.go new file mode 100644 index 00000000..1a3092a0 --- /dev/null +++ b/src/go/pt-galera-log-explainer/types/regex.go @@ -0,0 +1,91 @@ +package types + +import ( + "encoding/json" + "regexp" +) + +// LogRegex is the work struct to work on lines that were sent by "grep" + +type LogRegex struct { + Regex *regexp.Regexp // to send to grep, should be as simple as possible but without collisions + InternalRegex *regexp.Regexp // for internal usage in handler func + Type RegexType + + // Taking into arguments the current context and log line, returning an updated context and a closure to get the msg to display + // Why a closure: to later inject an updated context instead of the current partial context + // This ensure every hash/ip/nodenames are already known when crafting the message + Handler func(map[string]string, LogCtx, string) (LogCtx, LogDisplayer) + Verbosity Verbosity // To be able to hide details from summaries +} + +func (l *LogRegex) Handle(ctx LogCtx, line string) (LogCtx, LogDisplayer) { + if ctx.minVerbosity > l.Verbosity { + ctx.minVerbosity = l.Verbosity + } + mergedResults := map[string]string{} + if l.InternalRegex == nil { + return l.Handler(mergedResults, ctx, line) + } + slice := l.InternalRegex.FindStringSubmatch(line) + if len(slice) == 0 { + return ctx, nil + } + for _, subexpname := range l.InternalRegex.SubexpNames() { + if subexpname == "" { // 1st element is always empty for the complete regex + continue + } + mergedResults[subexpname] = slice[l.InternalRegex.SubexpIndex(subexpname)] + } + return l.Handler(mergedResults, ctx, line) +} + +func (l *LogRegex) MarshalJSON() ([]byte, error) { + out := &struct { + Regex string `json:"regex"` + InternalRegex string `json:"internalRegex"` + Type RegexType `json:"type"` + Verbosity Verbosity `json:"verbosity"` + }{ + Type: l.Type, + Verbosity: l.Verbosity, + } + if l.Regex != nil { + out.Regex = l.Regex.String() + } + if l.InternalRegex != nil { + out.InternalRegex = l.InternalRegex.String() + } + + return json.Marshal(out) +} + +type RegexType string + +var ( + EventsRegexType RegexType = "events" + SSTRegexType RegexType = "sst" + ViewsRegexType RegexType = "views" + IdentRegexType RegexType = "identity" + StatesRegexType RegexType = "states" + PXCOperatorRegexType RegexType = "pxc-operator" + ApplicativeRegexType RegexType = "applicative" +) + +type RegexMap map[string]*LogRegex + +func (r RegexMap) Merge(r2 RegexMap) RegexMap { + for key, value := range r2 { + r[key] = value + } + return r +} + +func (r RegexMap) Compile() []string { + + arr := []string{} + for _, regex := range r { + arr = append(arr, regex.Regex.String()) + } + return arr +} diff --git a/src/go/pt-galera-log-explainer/types/sst.go b/src/go/pt-galera-log-explainer/types/sst.go new file mode 100644 index 00000000..97986812 --- /dev/null +++ b/src/go/pt-galera-log-explainer/types/sst.go @@ -0,0 +1,15 @@ +package types + +type SST struct { + Method string + Type string + ResyncingNode string + ResyncedFromNode string +} + +func (s *SST) Reset() { + s.Method = "" + s.Type = "" + s.ResyncedFromNode = "" + s.ResyncingNode = "" +} diff --git a/src/go/pt-galera-log-explainer/types/timeline.go b/src/go/pt-galera-log-explainer/types/timeline.go new file mode 100644 index 00000000..5dbd07e7 --- /dev/null +++ b/src/go/pt-galera-log-explainer/types/timeline.go @@ -0,0 +1,178 @@ +package types + +import ( + "math" + "time" +) + +// It should be kept already sorted by timestamp +type LocalTimeline []LogInfo + +func (lt LocalTimeline) Add(li LogInfo) LocalTimeline { + + // to deduplicate, it will keep 2 loginfo occurences + // 1st one for the 1st timestamp found, it will also show the number of repetition + // 2nd loginfo the keep the last timestamp found, so that we don't loose track + // so there will be a corner case if the first ever event is repeated, but that is acceptable + if len(lt) > 1 && li.IsDuplicatedEvent(lt[len(lt)-2], lt[len(lt)-1]) { + lt[len(lt)-2].RepetitionCount++ + lt[len(lt)-1] = li + } else { + lt = append(lt, li) + } + return lt +} + +// "string" key is a node IP +type Timeline map[string]LocalTimeline + +// MergeTimeline is helpful when log files are split by date, it can be useful to be able to merge content +// a "timeline" come from a log file. Log files that came from some node should not never have overlapping dates +func MergeTimeline(t1, t2 LocalTimeline) LocalTimeline { + if len(t1) == 0 { + return t2 + } + if len(t2) == 0 { + return t1 + } + + startt1 := getfirsttime(t1) + startt2 := getfirsttime(t2) + + // just flip them, easier than adding too many nested conditions + // t1: ---O----?-- + // t2: --O-----?-- + if startt1.After(startt2) { + return MergeTimeline(t2, t1) + } + + endt1 := getlasttime(t1) + endt2 := getlasttime(t2) + + // if t2 is an updated version of t1, or t1 an updated of t2, or t1=t2 + // t1: --O-----?-- + // t2: --O-----?-- + if startt1.Equal(startt2) { + // t2 > t1 + // t1: ---O---O---- + // t2: ---O-----O-- + if endt1.Before(endt2) { + return t2 + } + // t1: ---O-----O-- + // t2: ---O-----O-- + // or + // t1: ---O-----O-- + // t2: ---O---O---- + return t1 + } + + // if t1 superseds t2 + // t1: --O-----O-- + // t2: ---O---O--- + // or + // t1: --O-----O-- + // t2: ---O----O-- + if endt1.After(endt2) || endt1.Equal(endt2) { + return t1 + } + //return append(t1, t2...) + + // t1: --O----O---- + // t2: ----O----O-- + if endt1.After(startt2) { + // t1: --O----O---- + // t2: ----OO--OO-- + //>t : --O----OOO-- won't try to get things between t1.end and t2.start + // we assume they're identical, they're supposed to be from the same server + t2 = CutTimelineAt(t2, endt1) + // no return here, to avoid repeating the ctx.inherit + } + + // t1: --O--O------ + // t2: ------O--O-- + t2[len(t2)-1].Ctx.Inherit(t1[len(t1)-1].Ctx) + return append(t1, t2...) +} + +func getfirsttime(l LocalTimeline) time.Time { + for _, event := range l { + if event.Date != nil && (event.Ctx.FileType == "error.log" || event.Ctx.FileType == "") { + return event.Date.Time + } + } + return time.Time{} +} +func getlasttime(l LocalTimeline) time.Time { + for i := len(l) - 1; i >= 0; i-- { + if l[i].Date != nil && (l[i].Ctx.FileType == "error.log" || l[i].Ctx.FileType == "") { + return l[i].Date.Time + } + } + return time.Time{} +} + +// CutTimelineAt returns a localtimeline with the 1st event starting +// right after the time sent as parameter +func CutTimelineAt(t LocalTimeline, at time.Time) LocalTimeline { + var i int + for i = 0; i < len(t); i++ { + if t[i].Date.Time.After(at) { + break + } + } + + return t[i:] +} + +func (t *Timeline) GetLatestUpdatedContextsByNodes() map[string]LogCtx { + updatedCtxs := map[string]LogCtx{} + latestctxs := []LogCtx{} + + for key, localtimeline := range *t { + if len(localtimeline) == 0 { + updatedCtxs[key] = NewLogCtx() + continue + } + latestctx := localtimeline[len(localtimeline)-1].Ctx + latestctxs = append(latestctxs, latestctx) + updatedCtxs[key] = latestctx + } + + for _, ctx := range updatedCtxs { + ctx.MergeMapsWith(latestctxs) + } + return updatedCtxs +} + +// iterateNode is used to search the source node(s) that contains the next chronological events +// it returns a slice in case 2 nodes have their next event precisely at the same time, which +// happens a lot on some versions +func (t Timeline) IterateNode() []string { + var ( + nextDate time.Time + nextNodes []string + ) + nextDate = time.Unix(math.MaxInt32, 0) + for node := range t { + if len(t[node]) == 0 { + continue + } + curDate := getfirsttime(t[node]) + if curDate.Before(nextDate) { + nextDate = curDate + nextNodes = []string{node} + } else if curDate.Equal(nextDate) { + nextNodes = append(nextNodes, node) + } + } + return nextNodes +} + +func (t Timeline) Dequeue(node string) { + + // dequeue the events + if len(t[node]) > 0 { + t[node] = t[node][1:] + } +} diff --git a/src/go/pt-galera-log-explainer/types/timeline_test.go b/src/go/pt-galera-log-explainer/types/timeline_test.go new file mode 100644 index 00000000..7b6dd659 --- /dev/null +++ b/src/go/pt-galera-log-explainer/types/timeline_test.go @@ -0,0 +1,261 @@ +package types + +import ( + "reflect" + "testing" + "time" +) + +func TestMergeTimeline(t *testing.T) { + + tests := []struct { + name string + input1 LocalTimeline + input2 LocalTimeline + expected LocalTimeline + }{ + { + name: "t1 is completely before the t2", + input1: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 1, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 1, time.UTC)}, + }, + }, + input2: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 2, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 3, 1, 1, 1, 1, time.UTC)}, + }, + }, + expected: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 1, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 2, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 3, 1, 1, 1, 1, time.UTC)}, + }, + }, + }, + { + name: "t1 is completely after the t2", + input1: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 2, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 3, 1, 1, 1, 1, time.UTC)}, + }, + }, + input2: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 1, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 1, time.UTC)}, + }, + }, + expected: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 1, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 2, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 3, 1, 1, 1, 1, time.UTC)}, + }, + }, + }, + { + name: "t1 is a superset of t2, with same start time", + input1: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 1, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 3, 1, 1, 1, 1, time.UTC)}, + }, + }, + input2: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 1, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 1, time.UTC)}, + }, + }, + // actually what is expected, as we don't expect logs to be different as we already merge them when they are with an identical identifier + expected: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 1, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 3, 1, 1, 1, 1, time.UTC)}, + }, + }, + }, + { + name: "t1 overlap with t2, sharing events", + input1: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 1, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 3, 1, 1, 1, 1, time.UTC)}, + }, + }, + input2: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 4, 1, 1, 1, 1, time.UTC)}, + }, + }, + expected: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 1, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 3, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 4, 1, 1, 1, 1, time.UTC)}, + }, + }, + }, + + { + name: "t1 is completely before the t2, but t2 has null trailing dates", + input1: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 1, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 1, time.UTC)}, + }, + }, + input2: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 2, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 3, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{}, + }, + expected: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 1, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 2, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 3, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{}, + }, + }, + + { + name: "t1 is completely before the t2, but t1 has null leading dates", + input1: LocalTimeline{ + LogInfo{}, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 1, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 1, time.UTC)}, + }, + }, + input2: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 2, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 3, 1, 1, 1, 1, time.UTC)}, + }, + }, + expected: LocalTimeline{ + LogInfo{}, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 1, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 2, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 3, 1, 1, 1, 1, time.UTC)}, + }, + }, + }, + } + + for _, test := range tests { + out := MergeTimeline(test.input1, test.input2) + if !reflect.DeepEqual(out, test.expected) { + t.Fatalf("%s failed: expected %v, got %v", test.name, test.expected, out) + } + } + +} + +func TestCutTimelineAt(t *testing.T) { + + tests := []struct { + name string + input1 LocalTimeline + input2 time.Time + expected LocalTimeline + }{ + { + name: "simple cut", + input1: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 1, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 2, 1, 1, 1, 1, time.UTC)}, + }, + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 3, 1, 1, 1, 1, time.UTC)}, + }, + }, + input2: time.Date(2023, time.January, 2, 1, 1, 1, 1, time.UTC), + expected: LocalTimeline{ + LogInfo{ + Date: &Date{Time: time.Date(2023, time.January, 3, 1, 1, 1, 1, time.UTC)}, + }, + }, + }, + } + + for _, test := range tests { + out := CutTimelineAt(test.input1, test.input2) + if !reflect.DeepEqual(out, test.expected) { + t.Fatalf("%s failed: expected %v, got %v", test.name, test.expected, out) + } + } + +} diff --git a/src/go/pt-galera-log-explainer/types/utils.go b/src/go/pt-galera-log-explainer/types/utils.go new file mode 100644 index 00000000..201b2d21 --- /dev/null +++ b/src/go/pt-galera-log-explainer/types/utils.go @@ -0,0 +1,56 @@ +package types + +import ( + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/utils" + "github.com/rs/zerolog/log" +) + +// Identifier is used to identify a node timeline. +// It will the column headers +// It will also impacts how logs are merged if we have multiple logs per nodes +// +// In order of preference: wsrep_node_name (or galera "node" name), hostname, ip, filepath +func Identifier(ctx LogCtx) string { + if len(ctx.OwnNames) > 0 { + return ctx.OwnNames[len(ctx.OwnNames)-1] + } + if len(ctx.OwnIPs) > 0 { + return DisplayNodeSimplestForm(ctx, ctx.OwnIPs[len(ctx.OwnIPs)-1]) + } + if len(ctx.OwnHashes) > 0 { + if name, ok := ctx.HashToNodeName[ctx.OwnHashes[0]]; ok { + return name + } + if ip, ok := ctx.HashToIP[ctx.OwnHashes[0]]; ok { + return DisplayNodeSimplestForm(ctx, ip) + } + } + return ctx.FilePath +} + +// DisplayNodeSimplestForm is useful to get the most easily to read string for a given IP +// This only has impacts on display +// In order of preference: wsrep_node_name (or galera "node" name), hostname, ip +func DisplayNodeSimplestForm(ctx LogCtx, ip string) string { + if nodename, ok := ctx.IPToNodeName[ip]; ok { + s := utils.ShortNodeName(nodename) + log.Debug().Str("ip", ip).Str("simplestform", s).Str("from", "IPToNodeName").Msg("nodeSimplestForm") + return s + } + + for hash, storedip := range ctx.HashToIP { + if ip == storedip { + if nodename, ok := ctx.HashToNodeName[hash]; ok { + s := utils.ShortNodeName(nodename) + log.Debug().Str("ip", ip).Str("simplestform", s).Str("from", "HashToNodeName").Msg("nodeSimplestForm") + return s + } + } + } + if hostname, ok := ctx.IPToHostname[ip]; ok { + log.Debug().Str("ip", ip).Str("simplestform", hostname).Str("from", "IPToHostname").Msg("nodeSimplestForm") + return hostname + } + log.Debug().Str("ip", ip).Str("simplestform", ip).Str("from", "default").Msg("nodeSimplestForm") + return ip +} diff --git a/src/go/pt-galera-log-explainer/utils/utils.go b/src/go/pt-galera-log-explainer/utils/utils.go new file mode 100644 index 00000000..900acf7d --- /dev/null +++ b/src/go/pt-galera-log-explainer/utils/utils.go @@ -0,0 +1,123 @@ +package utils + +import ( + "fmt" + "strings" +) + +// Color is given its own type for safe function signatures +type Color string + +// Color codes interpretted by the terminal +// NOTE: all codes must be of the same length or they will throw off the field alignment of tabwriter +const ( + ResetText Color = "\x1b[0000m" + BrightText = "\x1b[0001m" + RedText = "\x1b[0031m" + GreenText = "\x1b[0032m" + YellowText = "\x1b[0033m" + BlueText = "\x1b[0034m" + MagentaText = "\x1b[0035m" + CyanText = "\x1b[0036m" + WhiteText = "\x1b[0037m" + DefaultText = "\x1b[0039m" + BrightRedText = "\x1b[1;31m" + BrightGreenText = "\x1b[1;32m" + BrightYellowText = "\x1b[1;33m" + BrightBlueText = "\x1b[1;34m" + BrightMagentaText = "\x1b[1;35m" + BrightCyanText = "\x1b[1;36m" + BrightWhiteText = "\x1b[1;37m" +) + +var colorsToTextColor = map[string]Color{ + "yellow": YellowText, + "green": GreenText, + "red": RedText, +} + +var SkipColor bool + +// Color implements the Stringer interface for interoperability with string +func (c *Color) String() string { + return string(*c) +} + +func Paint(color Color, value string) string { + if SkipColor { + return value + } + return fmt.Sprintf("%v%v%v", color, value, ResetText) +} + +func PaintForState(text, state string) string { + + c := ColorForState(state) + if c != "" { + return Paint(colorsToTextColor[c], text) + } + + return text +} + +func ColorForState(state string) string { + switch state { + case "DONOR", "JOINER", "DESYNCED": + return "yellow" + case "SYNCED": + return "green" + case "CLOSED", "NON-PRIMARY": + return "red" + default: + return "" + } +} + +func SliceContains(s []string, str string) bool { + for _, v := range s { + if v == str { + return true + } + } + return false +} + +func SliceMergeDeduplicate(s, s2 []string) []string { + for _, str := range s2 { + if !SliceContains(s, str) { + s = append(s, str) + } + } + return s +} + +// StringsReplaceReversed is similar to strings.Replace, but replacing the +// right-most elements instead of left-most +func StringsReplaceReversed(s, old, new string, n int) string { + + s2 := s + stop := len(s) + + for i := 0; i < n; i++ { + stop = strings.LastIndex(s[:stop], old) + + s2 = (s[:stop]) + new + s2[stop+len(old):] + } + return s2 +} + +func UUIDToShortUUID(uuid string) string { + splitted := strings.Split(uuid, "-") + return splitted[0] + "-" + splitted[3] +} + +// ShortNodeName helps reducing the node name when it is the default value (node hostname) +// It only keeps the top-level domain +func ShortNodeName(s string) string { + // short enough + if len(s) < 10 { + return s + } + before, _, _ := strings.Cut(s, ".") + return before +} diff --git a/src/go/pt-galera-log-explainer/utils/utils_test.go b/src/go/pt-galera-log-explainer/utils/utils_test.go new file mode 100644 index 00000000..dbc3fe60 --- /dev/null +++ b/src/go/pt-galera-log-explainer/utils/utils_test.go @@ -0,0 +1,42 @@ +package utils + +import "testing" + +func TestStringsReplaceReverse(t *testing.T) { + + tests := []struct { + inputS string + inputOld string + inputNew string + inputCount int + expected string + }{ + { + inputS: "2022-22-22", + inputOld: "22", + inputNew: "XX", + inputCount: 1, + expected: "2022-22-XX", + }, + { + inputS: "2022-22-22", + inputOld: "22", + inputNew: "XX", + inputCount: 2, + expected: "2022-XX-XX", + }, + { + inputS: "2022-22-22", + inputOld: "22", + inputNew: "XX", + inputCount: 3, + expected: "20XX-XX-XX", + }, + } + for _, test := range tests { + if s := StringsReplaceReversed(test.inputS, test.inputOld, test.inputNew, test.inputCount); s != test.expected { + t.Log("Expected", test.expected, "got", s) + t.Fail() + } + } +} diff --git a/src/go/pt-galera-log-explainer/whois.go b/src/go/pt-galera-log-explainer/whois.go new file mode 100644 index 00000000..f8a7b500 --- /dev/null +++ b/src/go/pt-galera-log-explainer/whois.go @@ -0,0 +1,102 @@ +package main + +import ( + "encoding/json" + "fmt" + + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/regex" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/types" + "github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/utils" + "github.com/pkg/errors" +) + +type whois struct { + Search string `arg:"" name:"search" help:"the identifier (node name, ip, uuid, hash) to search"` + Paths []string `arg:"" name:"paths" help:"paths of the log to use"` +} + +func (w *whois) Help() string { + return `Take any type of info pasted from error logs and find out about it. +It will list known node name(s), IP(s), hostname(s), and other known node's UUIDs. +` +} + +func (w *whois) Run() error { + + toCheck := regex.AllRegexes() + timeline, err := timelineFromPaths(CLI.Whois.Paths, toCheck, CLI.Since, CLI.Until) + if err != nil { + return errors.Wrap(err, "Found nothing to translate") + } + ctxs := timeline.GetLatestUpdatedContextsByNodes() + + ni := whoIs(ctxs, CLI.Whois.Search) + + json, err := json.MarshalIndent(ni, "", "\t") + if err != nil { + return err + } + fmt.Println(string(json)) + return nil +} + +func whoIs(ctxs map[string]types.LogCtx, search string) types.NodeInfo { + ni := types.NodeInfo{Input: search} + if regex.IsNodeUUID(search) { + search = utils.UUIDToShortUUID(search) + } + var ( + ips []string + hashes []string + nodenames []string + ) + + for _, ctx := range ctxs { + if utils.SliceContains(ctx.OwnNames, search) || utils.SliceContains(ctx.OwnHashes, search) || utils.SliceContains(ctx.OwnIPs, search) { + ni.NodeNames = ctx.OwnNames + ni.NodeUUIDs = ctx.OwnHashes + ni.IPs = ctx.OwnIPs + ni.Hostname = ctx.OwnHostname() + } + + if nodename, ok := ctx.HashToNodeName[search]; ok { + nodenames = utils.SliceMergeDeduplicate(nodenames, []string{nodename}) + hashes = utils.SliceMergeDeduplicate(hashes, []string{search}) + } + + if ip, ok := ctx.HashToIP[search]; ok { + ips = utils.SliceMergeDeduplicate(ips, []string{ip}) + hashes = utils.SliceMergeDeduplicate(hashes, []string{search}) + + } else if nodename, ok := ctx.IPToNodeName[search]; ok { + nodenames = utils.SliceMergeDeduplicate(nodenames, []string{nodename}) + ips = utils.SliceMergeDeduplicate(ips, []string{search}) + + } else if utils.SliceContains(ctx.AllNodeNames(), search) { + nodenames = utils.SliceMergeDeduplicate(nodenames, []string{search}) + } + + for _, nodename := range nodenames { + hashes = utils.SliceMergeDeduplicate(hashes, ctx.HashesFromNodeName(nodename)) + ips = utils.SliceMergeDeduplicate(ips, ctx.IPsFromNodeName(nodename)) + } + + for _, ip := range ips { + hashes = utils.SliceMergeDeduplicate(hashes, ctx.HashesFromIP(ip)) + nodename, ok := ctx.IPToNodeName[ip] + if ok { + nodenames = utils.SliceMergeDeduplicate(nodenames, []string{nodename}) + } + } + for _, hash := range hashes { + nodename, ok := ctx.HashToNodeName[hash] + if ok { + nodenames = utils.SliceMergeDeduplicate(nodenames, []string{nodename}) + } + } + } + ni.NodeNames = nodenames + ni.NodeUUIDs = hashes + ni.IPs = ips + return ni +}