vitess.io/vitess@v0.16.2/go/vt/vttablet/sysloglogger/sysloglogger_test.go (about) 1 /* 2 Copyright 2019 The Vitess Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package sysloglogger 18 19 import ( 20 "errors" 21 "fmt" 22 "log/syslog" 23 "strconv" 24 "strings" 25 "testing" 26 "time" 27 28 "context" 29 30 "vitess.io/vitess/go/streamlog" 31 "vitess.io/vitess/go/vt/vttablet/tabletserver/tabletenv" 32 ) 33 34 // fakeWriter is a mock of the real syslog writer, to enable capturing and playing back of log messages in unit testing. 35 type fakeWriter struct { 36 messages map[string]bool 37 } 38 39 func newFakeWriter() *fakeWriter { 40 return &fakeWriter{ 41 messages: make(map[string]bool), 42 } 43 } 44 func (fw *fakeWriter) write(pri syslog.Priority, msg string) error { 45 fw.messages[strings.TrimSpace(msg)] = true 46 return nil 47 } 48 func (fw *fakeWriter) Info(msg string) error { return fw.write(syslog.LOG_INFO, msg) } 49 func (fw *fakeWriter) Close() error { return nil } 50 51 // mockLogStats generates a dummy tabletserver.LogStats message for testing. 52 func mockLogStats(originalSQL string) *tabletenv.LogStats { 53 logstats := tabletenv.NewLogStats(context.Background(), "Execute") 54 logstats.StartTime = time.Time{} 55 logstats.PlanType = "PASS_SELECT" 56 logstats.OriginalSQL = originalSQL 57 logstats.AddRewrittenSQL(originalSQL, time.Now()) 58 logstats.MysqlResponseTime = 0 59 return logstats 60 } 61 62 // failingFakeWriter is a mock syslog writer that deliberately simulates an intermittent syslog outage, which causes 63 // every 4th message log message to be dropped. 64 type failingFakeWriter struct { 65 messages map[string]bool 66 numberProcessed int 67 } 68 69 func newFailingFakeWriter() *failingFakeWriter { 70 return &failingFakeWriter{ 71 messages: make(map[string]bool), 72 numberProcessed: 0, 73 } 74 } 75 func (fw *failingFakeWriter) write(pri syslog.Priority, msg string) error { 76 fw.numberProcessed++ 77 if fw.numberProcessed%4 == 0 { 78 return errors.New("Cannot connect to syslog") 79 } 80 fw.messages[strings.TrimSpace(msg)] = true 81 return nil 82 } 83 func (fw *failingFakeWriter) Info(msg string) error { return fw.write(syslog.LOG_INFO, msg) } 84 func (fw *failingFakeWriter) Close() error { return nil } 85 86 // expectedLogStatsText returns the results expected from the plugin processing a dummy message generated by mockLogStats(...). 87 func expectedLogStatsText(originalSQL string) string { 88 return fmt.Sprintf("Execute\t\t\t''\t''\t0001-01-01 00:00:00.000000\t0001-01-01 00:00:00.000000\t0.000000\tPASS_SELECT\t"+ 89 "\"%s\"\t%s\t1\t\"%s\"\tmysql\t0.000000\t0.000000\t0\t0\t0\t\"\"", originalSQL, "map[]", originalSQL) 90 } 91 92 // expectedRedactedLogStatsText returns the results expected from the plugin processing a dummy message generated by mockLogStats(...) 93 // when redaction is enabled. 94 func expectedRedactedLogStatsText(originalSQL string) string { 95 return fmt.Sprintf("Execute\t\t\t''\t''\t0001-01-01 00:00:00.000000\t0001-01-01 00:00:00.000000\t0.000000\tPASS_SELECT\t"+ 96 "\"%s\"\t%q\t1\t\"%s\"\tmysql\t0.000000\t0.000000\t0\t0\t0\t\"\"", originalSQL, "[REDACTED]", "[REDACTED]") 97 } 98 99 // TestSyslog sends a stream of five query records to the plugin, and verifies that they are logged. 100 func TestSyslog(t *testing.T) { 101 // Overwrite the usual syslog writer and StatsLogger subscription channel with mocks 102 mock := newFakeWriter() 103 writer = mock 104 ch = make(chan any, 10) 105 106 // Start running the plugin loop 107 syncChannel := make(chan bool) 108 go func() { 109 run() 110 close(syncChannel) 111 }() 112 113 // Send fake messages to the mock channel, and then close the channel to end the plugin loop 114 ch <- mockLogStats("select 1") 115 ch <- mockLogStats("select 2") 116 ch <- mockLogStats("select 3") 117 ch <- mockLogStats("select 4") 118 ch <- mockLogStats("select 5") 119 close(ch) 120 <-syncChannel 121 122 // Collect everything that the plugin logged 123 queriesLogged := make(map[string]bool) 124 for received := range mock.messages { 125 t.Logf("Logged query \"%s\"", received) 126 queriesLogged[received] = true 127 } 128 129 // Verify the count and contents 130 if len(queriesLogged) != 5 { 131 t.Fatalf("Expected 5 queries to be logged, but found %d", len(queriesLogged)) 132 } 133 for i := 1; i <= 5; i++ { 134 if _, ok := queriesLogged[expectedLogStatsText("select "+strconv.Itoa(i))]; !ok { 135 t.Fatalf("Expected query \"%s\" was not logged", expectedLogStatsText("select "+strconv.Itoa(i))) 136 } 137 } 138 } 139 140 // TestSyslogRedacted sends a stream of five query records to the plugin, and verifies that they are logged 141 // when redaction is enabled. 142 func TestSyslogRedacted(t *testing.T) { 143 // Overwrite the usual syslog writer and StatsLogger subscription channel with mocks 144 streamlog.SetRedactDebugUIQueries(true) 145 defer func() { 146 streamlog.SetRedactDebugUIQueries(false) 147 }() 148 mock := newFakeWriter() 149 writer = mock 150 ch = make(chan any, 10) 151 152 // Start running the plugin loop 153 syncChannel := make(chan bool) 154 go func() { 155 run() 156 close(syncChannel) 157 }() 158 159 // Send fake messages to the mock channel, and then close the channel to end the plugin loop 160 ch <- mockLogStats("select 1") 161 ch <- mockLogStats("select 2") 162 ch <- mockLogStats("select 3") 163 ch <- mockLogStats("select 4") 164 ch <- mockLogStats("select 5") 165 close(ch) 166 <-syncChannel 167 168 // Collect everything that the plugin logged 169 queriesLogged := make(map[string]bool) 170 for received := range mock.messages { 171 t.Logf("Logged query \"%s\"", received) 172 queriesLogged[received] = true 173 } 174 175 // Verify the count and contents 176 if len(queriesLogged) != 5 { 177 t.Fatalf("Expected 5 queries to be logged, but found %d", len(queriesLogged)) 178 } 179 for i := 1; i <= 5; i++ { 180 if _, ok := queriesLogged[expectedRedactedLogStatsText("select "+strconv.Itoa(i))]; !ok { 181 t.Fatalf("Expected query \"%s\" was not logged", expectedRedactedLogStatsText("select "+strconv.Itoa(i))) 182 } 183 } 184 } 185 186 // TestSyslogWithBadData sends a stream of query records, including one that doesn't fit the type specification... 187 // verifying that the bad record is gracefully skipped and the others are still logged successfully. 188 func TestSyslogWithBadData(t *testing.T) { 189 mock := newFakeWriter() 190 writer = mock 191 ch = make(chan any, 10) 192 193 syncChannel := make(chan bool) 194 go func() { 195 run() 196 close(syncChannel) 197 }() 198 199 // Send 5 records for logging, one of which is bad 200 ch <- mockLogStats("select 1") 201 ch <- mockLogStats("select 2") 202 ch <- mockLogStats("select 3") 203 ch <- "Wait... this is just a garbage 'string', not of type '*tabletserver.LogStats'!" 204 ch <- mockLogStats("select 5") 205 close(ch) 206 <-syncChannel 207 208 // Collect everything that the plugin logged 209 queriesLogged := make(map[string]bool) 210 for received := range mock.messages { 211 queriesLogged[received] = true 212 } 213 214 // Verify the count and contents 215 if len(queriesLogged) != 4 { 216 t.Fatalf("Expected 4 queries to be logged, but found %d", len(queriesLogged)) 217 } 218 validNums := []int{1, 2, 3, 5} 219 for _, num := range validNums { 220 if _, ok := queriesLogged[expectedLogStatsText("select "+strconv.Itoa(num))]; !ok { 221 t.Fatalf("Expected query \"%s\" was not logged", expectedLogStatsText("select "+strconv.Itoa(num))) 222 } 223 } 224 } 225 226 // TestSyslogWithInterruptedConnection sends a stream of query records, simulating temporary syslog outage 227 // while they're processing. Verifies that the plugin gracefully handles and recovers from the broken connectivity, 228 // and that all messages received while the connection is alive are logged successfully. 229 func TestSyslogWithInterruptedConnection(t *testing.T) { 230 231 // This mock will simulate a broken syslog connection when processing every 4th record 232 mock := newFailingFakeWriter() 233 writer = mock 234 ch = make(chan any, 10) 235 236 syncChannel := make(chan bool) 237 go func() { 238 run() 239 close(syncChannel) 240 }() 241 242 ch <- mockLogStats("select 1") 243 ch <- mockLogStats("select 2") 244 ch <- mockLogStats("select 3") 245 ch <- mockLogStats("select 4") // This record will get dropped due to a syslog outage 246 ch <- mockLogStats("select 5") 247 close(ch) 248 <-syncChannel 249 250 queriesLogged := make(map[string]bool) 251 for received := range mock.messages { 252 queriesLogged[received] = true 253 } 254 if len(queriesLogged) != 4 { 255 t.Fatalf("Expected 4 queries to be logged, but found %d", len(queriesLogged)) 256 } 257 expectedLogs := []int{1, 2, 3, 5} 258 for _, num := range expectedLogs { 259 if _, ok := queriesLogged[expectedLogStatsText("select "+strconv.Itoa(num))]; !ok { 260 t.Fatalf("Expected query \"%s\" was not logged", expectedLogStatsText("select "+strconv.Itoa(num))) 261 } 262 } 263 }