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 }