github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/querylogger/worker.go (about)

     1  // Copyright 2023 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package querylogger
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"time"
    11  
    12  	"github.com/juju/clock"
    13  	"github.com/juju/errors"
    14  	"gopkg.in/tomb.v2"
    15  )
    16  
    17  const (
    18  	filename = "slow-query.log"
    19  
    20  	PollInterval = time.Second
    21  )
    22  
    23  // WorkerConfig encapsulates the configuration options for the
    24  // dbaccessor worker.
    25  type WorkerConfig struct {
    26  	LogDir        string
    27  	Clock         clock.Clock
    28  	Logger        Logger
    29  	StackGatherer func() []byte
    30  }
    31  
    32  // Validate ensures that the config values are valid.
    33  func (c *WorkerConfig) Validate() error {
    34  	if c.LogDir == "" {
    35  		return errors.NotValidf("missing LogDir")
    36  	}
    37  	if c.Clock == nil {
    38  		return errors.NotValidf("missing Clock")
    39  	}
    40  	if c.Logger == nil {
    41  		return errors.NotValidf("missing Logger")
    42  	}
    43  	if c.StackGatherer == nil {
    44  		return errors.NotValidf("missing StackGatherer")
    45  	}
    46  	return nil
    47  }
    48  
    49  // loggerWorker is a logger that can be used to log slow operations at the
    50  // database level. It will print out debug messages for slow queries.
    51  type loggerWorker struct {
    52  	tomb tomb.Tomb
    53  
    54  	clock         clock.Clock
    55  	logger        Logger
    56  	stackGatherer func() []byte
    57  
    58  	logDir string
    59  	logs   chan payload
    60  }
    61  
    62  // newWorker creates a new Worker, which can be used to log
    63  // slow queries.
    64  func newWorker(cfg *WorkerConfig) (*loggerWorker, error) {
    65  	var err error
    66  	if err = cfg.Validate(); err != nil {
    67  		return nil, errors.Trace(err)
    68  	}
    69  
    70  	l := &loggerWorker{
    71  		logDir:        cfg.LogDir,
    72  		clock:         cfg.Clock,
    73  		logger:        cfg.Logger,
    74  		stackGatherer: cfg.StackGatherer,
    75  
    76  		logs: make(chan payload),
    77  	}
    78  	l.tomb.Go(l.loop)
    79  	return l, nil
    80  }
    81  
    82  // RecordSlowQuery the slow query, with the given arguments.
    83  func (l *loggerWorker) RecordSlowQuery(msg, stmt string, args []any, duration float64) {
    84  	// Record the stack.
    85  	// TODO (stickupkid): Prune the stack to remove the first few frames.
    86  	stack := l.stackGatherer()
    87  
    88  	done := make(chan error)
    89  	select {
    90  	case l.logs <- payload{
    91  		log: log{
    92  			duration: duration,
    93  			stmt:     stmt,
    94  			stack:    stack,
    95  		},
    96  		done: done,
    97  	}:
    98  	case <-l.tomb.Dying():
    99  		return
   100  	}
   101  
   102  	var err error
   103  	select {
   104  	case err = <-done:
   105  	case <-l.tomb.Dying():
   106  		return
   107  	}
   108  
   109  	if err != nil {
   110  		// Failed to log the slow query, log it to the main logger.
   111  		l.logger.Warningf("failed to log slow query: %v", err)
   112  		l.logger.Warningf("slow query: "+msg+"\n%s", append(args, stack)...)
   113  		return
   114  	}
   115  
   116  	l.logger.Warningf("slow query: "+msg, args...)
   117  }
   118  
   119  // Kill is part of the worker.Worker interface.
   120  func (w *loggerWorker) Kill() {
   121  	w.tomb.Kill(nil)
   122  }
   123  
   124  // Wait is part of the worker.Worker interface.
   125  func (w *loggerWorker) Wait() error {
   126  	return w.tomb.Wait()
   127  }
   128  
   129  func (l *loggerWorker) loop() error {
   130  	// Open the log file.
   131  	file, err := os.OpenFile(filepath.Join(l.logDir, filename), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
   132  	if err != nil {
   133  		return errors.Trace(err)
   134  	}
   135  	// Ensure we sync and then close it when we're done, so we don't loose any
   136  	// potential writes.
   137  	defer func() {
   138  		// Don't return early, just log and continue.
   139  		if err := file.Sync(); err != nil {
   140  			l.logger.Errorf("failed to sync slow query log: %v", err)
   141  		}
   142  		if err := file.Close(); err != nil {
   143  			l.logger.Errorf("failed to close slow query log: %v", err)
   144  		}
   145  	}()
   146  
   147  	timer := l.clock.NewTimer(PollInterval)
   148  	defer timer.Stop()
   149  
   150  	var syncRequired bool
   151  	for {
   152  		select {
   153  		case <-l.tomb.Dying():
   154  			return tomb.ErrDying
   155  
   156  		case payload := <-l.logs:
   157  			_, err := file.WriteString(payload.log.String())
   158  			select {
   159  			case payload.done <- err:
   160  				syncRequired = err == nil
   161  			case <-l.tomb.Dying():
   162  				return tomb.ErrDying
   163  			}
   164  
   165  		case <-timer.Chan():
   166  			if syncRequired {
   167  				if err := file.Sync(); err != nil {
   168  					return errors.Annotatef(err, "failed to sync slow query log")
   169  				}
   170  			}
   171  			syncRequired = false
   172  
   173  			timer.Reset(PollInterval)
   174  		}
   175  	}
   176  }
   177  
   178  type payload struct {
   179  	log  log
   180  	done chan<- error
   181  }
   182  
   183  type log struct {
   184  	duration float64
   185  	stmt     string
   186  	stack    []byte
   187  }
   188  
   189  func (l log) String() string {
   190  	return fmt.Sprintf(`slow query took %0.3fs for statement: %s
   191  stack trace:
   192  %s
   193  
   194  `, l.duration, l.stmt, string(l.stack))
   195  }