vitess.io/vitess@v0.16.2/go/streamlog/streamlog.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 streamlog provides a non-blocking message broadcaster.
    18  package streamlog
    19  
    20  import (
    21  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"net/url"
    25  	"os"
    26  	"os/signal"
    27  	"strings"
    28  	"sync"
    29  	"syscall"
    30  
    31  	"github.com/spf13/pflag"
    32  
    33  	"vitess.io/vitess/go/acl"
    34  	"vitess.io/vitess/go/stats"
    35  	"vitess.io/vitess/go/vt/log"
    36  	"vitess.io/vitess/go/vt/servenv"
    37  )
    38  
    39  var (
    40  	sendCount      = stats.NewCountersWithSingleLabel("StreamlogSend", "stream log send count", "logger_names")
    41  	deliveredCount = stats.NewCountersWithMultiLabels(
    42  		"StreamlogDelivered",
    43  		"Stream log delivered",
    44  		[]string{"Log", "Subscriber"})
    45  	deliveryDropCount = stats.NewCountersWithMultiLabels(
    46  		"StreamlogDeliveryDroppedMessages",
    47  		"Dropped messages by streamlog delivery",
    48  		[]string{"Log", "Subscriber"})
    49  )
    50  
    51  var (
    52  	redactDebugUIQueries bool
    53  	queryLogFilterTag    string
    54  	queryLogRowThreshold uint64
    55  	queryLogFormat       = "text"
    56  )
    57  
    58  func GetRedactDebugUIQueries() bool {
    59  	return redactDebugUIQueries
    60  }
    61  
    62  func SetRedactDebugUIQueries(newRedactDebugUIQueries bool) {
    63  	redactDebugUIQueries = newRedactDebugUIQueries
    64  }
    65  
    66  func GetQueryLogFilterTag() string {
    67  	return queryLogFilterTag
    68  }
    69  
    70  func SetQueryLogFilterTag(newQueryLogFilterTag string) {
    71  	queryLogFilterTag = newQueryLogFilterTag
    72  }
    73  
    74  func GetQueryLogRowThreshold() uint64 {
    75  	return queryLogRowThreshold
    76  }
    77  
    78  func SetQueryLogRowThreshold(newQueryLogRowThreshold uint64) {
    79  	queryLogRowThreshold = newQueryLogRowThreshold
    80  }
    81  
    82  func GetQueryLogFormat() string {
    83  	return queryLogFormat
    84  }
    85  
    86  func SetQueryLogFormat(newQueryLogFormat string) {
    87  	queryLogFormat = newQueryLogFormat
    88  }
    89  
    90  func init() {
    91  	servenv.OnParseFor("vtcombo", registerStreamLogFlags)
    92  	servenv.OnParseFor("vttablet", registerStreamLogFlags)
    93  	servenv.OnParseFor("vtgate", registerStreamLogFlags)
    94  }
    95  
    96  func registerStreamLogFlags(fs *pflag.FlagSet) {
    97  	// RedactDebugUIQueries controls whether full queries and bind variables are suppressed from debug UIs.
    98  	fs.BoolVar(&redactDebugUIQueries, "redact-debug-ui-queries", redactDebugUIQueries, "redact full queries and bind variables from debug UI")
    99  
   100  	// QueryLogFormat controls the format of the query log (either text or json)
   101  	fs.StringVar(&queryLogFormat, "querylog-format", queryLogFormat, "format for query logs (\"text\" or \"json\")")
   102  
   103  	// QueryLogFilterTag contains an optional string that must be present in the query for it to be logged
   104  	fs.StringVar(&queryLogFilterTag, "querylog-filter-tag", queryLogFilterTag, "string that must be present in the query for it to be logged; if using a value as the tag, you need to disable query normalization")
   105  
   106  	// QueryLogRowThreshold only log queries returning or affecting this many rows
   107  	fs.Uint64Var(&queryLogRowThreshold, "querylog-row-threshold", queryLogRowThreshold, "Number of rows a query has to return or affect before being logged; not useful for streaming queries. 0 means all queries will be logged.")
   108  
   109  }
   110  
   111  const (
   112  	// QueryLogFormatText is the format specifier for text querylog output
   113  	QueryLogFormatText = "text"
   114  
   115  	// QueryLogFormatJSON is the format specifier for json querylog output
   116  	QueryLogFormatJSON = "json"
   117  )
   118  
   119  // StreamLogger is a non-blocking broadcaster of messages.
   120  // Subscribers can use channels or HTTP.
   121  type StreamLogger struct {
   122  	name       string
   123  	size       int
   124  	mu         sync.Mutex
   125  	subscribed map[chan any]string
   126  }
   127  
   128  // LogFormatter is the function signature used to format an arbitrary
   129  // message for the given output writer.
   130  type LogFormatter func(out io.Writer, params url.Values, message any) error
   131  
   132  // New returns a new StreamLogger that can stream events to subscribers.
   133  // The size parameter defines the channel size for the subscribers.
   134  func New(name string, size int) *StreamLogger {
   135  	return &StreamLogger{
   136  		name:       name,
   137  		size:       size,
   138  		subscribed: make(map[chan any]string),
   139  	}
   140  }
   141  
   142  // Send sends message to all the writers subscribed to logger. Calling
   143  // Send does not block.
   144  func (logger *StreamLogger) Send(message any) {
   145  	logger.mu.Lock()
   146  	defer logger.mu.Unlock()
   147  
   148  	for ch, name := range logger.subscribed {
   149  		select {
   150  		case ch <- message:
   151  			deliveredCount.Add([]string{logger.name, name}, 1)
   152  		default:
   153  			deliveryDropCount.Add([]string{logger.name, name}, 1)
   154  		}
   155  	}
   156  	sendCount.Add(logger.name, 1)
   157  }
   158  
   159  // Subscribe returns a channel which can be used to listen
   160  // for messages.
   161  func (logger *StreamLogger) Subscribe(name string) chan any {
   162  	logger.mu.Lock()
   163  	defer logger.mu.Unlock()
   164  
   165  	ch := make(chan any, logger.size)
   166  	logger.subscribed[ch] = name
   167  	return ch
   168  }
   169  
   170  // Unsubscribe removes the channel from the subscription.
   171  func (logger *StreamLogger) Unsubscribe(ch chan any) {
   172  	logger.mu.Lock()
   173  	defer logger.mu.Unlock()
   174  
   175  	delete(logger.subscribed, ch)
   176  }
   177  
   178  // Name returns the name of StreamLogger.
   179  func (logger *StreamLogger) Name() string {
   180  	return logger.name
   181  }
   182  
   183  // ServeLogs registers the URL on which messages will be broadcast.
   184  // It is safe to register multiple URLs for the same StreamLogger.
   185  func (logger *StreamLogger) ServeLogs(url string, logf LogFormatter) {
   186  	http.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
   187  		if err := acl.CheckAccessHTTP(r, acl.DEBUGGING); err != nil {
   188  			acl.SendError(w, err)
   189  			return
   190  		}
   191  		if err := r.ParseForm(); err != nil {
   192  			http.Error(w, err.Error(), http.StatusBadRequest)
   193  		}
   194  		ch := logger.Subscribe("ServeLogs")
   195  		defer logger.Unsubscribe(ch)
   196  
   197  		// Notify client that we're set up. Helpful to distinguish low-traffic streams from connection issues.
   198  		w.WriteHeader(http.StatusOK)
   199  		w.(http.Flusher).Flush()
   200  
   201  		for message := range ch {
   202  			if err := logf(w, r.Form, message); err != nil {
   203  				return
   204  			}
   205  			w.(http.Flusher).Flush()
   206  		}
   207  	})
   208  	log.Infof("Streaming logs from %s at %v.", logger.Name(), url)
   209  }
   210  
   211  // LogToFile starts logging to the specified file path and will reopen the
   212  // file in response to SIGUSR2.
   213  //
   214  // Returns the channel used for the subscription which can be used to close
   215  // it.
   216  func (logger *StreamLogger) LogToFile(path string, logf LogFormatter) (chan any, error) {
   217  	rotateChan := make(chan os.Signal, 1)
   218  	signal.Notify(rotateChan, syscall.SIGUSR2)
   219  
   220  	logChan := logger.Subscribe("FileLog")
   221  	formatParams := map[string][]string{"full": {}}
   222  
   223  	f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  
   228  	go func() {
   229  		for {
   230  			select {
   231  			case record := <-logChan:
   232  				logf(f, formatParams, record) // nolint:errcheck
   233  			case <-rotateChan:
   234  				f.Close()
   235  				f, _ = os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
   236  			}
   237  		}
   238  	}()
   239  
   240  	return logChan, nil
   241  }
   242  
   243  // Formatter is a simple interface for objects that expose a Format function
   244  // as needed for streamlog.
   245  type Formatter interface {
   246  	Logf(io.Writer, url.Values) error
   247  }
   248  
   249  // GetFormatter returns a formatter function for objects conforming to the
   250  // Formatter interface
   251  func GetFormatter(logger *StreamLogger) LogFormatter {
   252  	return func(w io.Writer, params url.Values, val any) error {
   253  		fmter, ok := val.(Formatter)
   254  		if !ok {
   255  			_, err := fmt.Fprintf(w, "Error: unexpected value of type %T in %s!", val, logger.Name())
   256  			return err
   257  		}
   258  		return fmter.Logf(w, params)
   259  	}
   260  }
   261  
   262  // ShouldEmitLog returns whether the log with the given SQL query
   263  // should be emitted or filtered
   264  func ShouldEmitLog(sql string, rowsAffected, rowsReturned uint64) bool {
   265  	if queryLogRowThreshold > maxUint64(rowsAffected, rowsReturned) && queryLogFilterTag == "" {
   266  		return false
   267  	}
   268  	if queryLogFilterTag != "" {
   269  		return strings.Contains(sql, queryLogFilterTag)
   270  	}
   271  	return true
   272  }
   273  
   274  func maxUint64(a, b uint64) uint64 {
   275  	if a < b {
   276  		return b
   277  	}
   278  	return a
   279  }