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  }