github.com/containers/podman/v4@v4.9.4/libpod/events/logfile.go (about) 1 //go:build linux || freebsd 2 // +build linux freebsd 3 4 package events 5 6 import ( 7 "bufio" 8 "context" 9 "errors" 10 "fmt" 11 "io" 12 "os" 13 "path" 14 "path/filepath" 15 "time" 16 17 "github.com/containers/podman/v4/pkg/util" 18 "github.com/containers/storage/pkg/lockfile" 19 "github.com/nxadm/tail" 20 "github.com/sirupsen/logrus" 21 "golang.org/x/sys/unix" 22 ) 23 24 // EventLogFile is the structure for event writing to a logfile. It contains the eventer 25 // options and the event itself. Methods for reading and writing are also defined from it. 26 type EventLogFile struct { 27 options EventerOptions 28 } 29 30 // newLogFileEventer creates a new EventLogFile eventer 31 func newLogFileEventer(options EventerOptions) (*EventLogFile, error) { 32 // Create events log dir 33 if err := os.MkdirAll(filepath.Dir(options.LogFilePath), 0700); err != nil { 34 return nil, fmt.Errorf("creating events dirs: %w", err) 35 } 36 // We have to make sure the file is created otherwise reading events will hang. 37 // https://github.com/containers/podman/issues/15688 38 fd, err := os.OpenFile(options.LogFilePath, os.O_RDONLY|os.O_CREATE, 0700) 39 if err != nil { 40 return nil, fmt.Errorf("failed to create event log file: %w", err) 41 } 42 return &EventLogFile{options: options}, fd.Close() 43 } 44 45 // Writes to the log file 46 func (e EventLogFile) Write(ee Event) error { 47 // We need to lock events file 48 lock, err := lockfile.GetLockFile(e.options.LogFilePath + ".lock") 49 if err != nil { 50 return err 51 } 52 lock.Lock() 53 defer lock.Unlock() 54 55 eventJSONString, err := ee.ToJSONString() 56 if err != nil { 57 return err 58 } 59 60 if _, err := rotateLog(e.options.LogFilePath, eventJSONString, e.options.LogFileMaxSize); err != nil { 61 return err 62 } 63 64 return e.writeString(eventJSONString) 65 } 66 67 func (e EventLogFile) writeString(s string) error { 68 f, err := os.OpenFile(e.options.LogFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0700) 69 if err != nil { 70 return err 71 } 72 return writeToFile(s, f) 73 } 74 75 func writeToFile(s string, f *os.File) error { 76 _, err := f.WriteString(s + "\n") 77 return err 78 } 79 80 func (e EventLogFile) getTail(options ReadOptions) (*tail.Tail, error) { 81 seek := tail.SeekInfo{Offset: 0, Whence: io.SeekEnd} 82 if options.FromStart || !options.Stream { 83 seek.Whence = 0 84 } 85 stream := options.Stream 86 return tail.TailFile(e.options.LogFilePath, tail.Config{ReOpen: stream, Follow: stream, Location: &seek, Logger: tail.DiscardingLogger, Poll: true}) 87 } 88 89 func (e EventLogFile) readRotateEvent(event *Event) (begin bool, end bool, err error) { 90 if event.Status != Rotate { 91 return 92 } 93 if event.Details.Attributes == nil { 94 // may be an old event before storing attributes in the rotate event 95 return 96 } 97 switch event.Details.Attributes[rotateEventAttribute] { 98 case rotateEventBegin: 99 begin = true 100 return 101 case rotateEventEnd: 102 end = true 103 return 104 default: 105 err = fmt.Errorf("unknown rotate-event attribute %q", event.Details.Attributes[rotateEventAttribute]) 106 return 107 } 108 } 109 110 // Reads from the log file 111 func (e EventLogFile) Read(ctx context.Context, options ReadOptions) error { 112 defer close(options.EventChannel) 113 filterMap, err := generateEventFilters(options.Filters, options.Since, options.Until) 114 if err != nil { 115 return fmt.Errorf("failed to parse event filters: %w", err) 116 } 117 t, err := e.getTail(options) 118 if err != nil { 119 return err 120 } 121 if len(options.Until) > 0 { 122 untilTime, err := util.ParseInputTime(options.Until, false) 123 if err != nil { 124 return err 125 } 126 go func() { 127 time.Sleep(time.Until(untilTime)) 128 if err := t.Stop(); err != nil { 129 logrus.Errorf("Stopping logger: %v", err) 130 } 131 }() 132 } 133 logrus.Debugf("Reading events from file %q", e.options.LogFilePath) 134 135 // Get the time *before* starting to read. Comparing the timestamps 136 // with events avoids returning events more than once after a log-file 137 // rotation. 138 readTime, err := func() (time.Time, error) { 139 // We need to lock events file 140 lock, err := lockfile.GetLockFile(e.options.LogFilePath + ".lock") 141 if err != nil { 142 return time.Time{}, err 143 } 144 lock.Lock() 145 defer lock.Unlock() 146 return time.Now(), nil 147 }() 148 if err != nil { 149 return err 150 } 151 152 var line *tail.Line 153 var ok bool 154 var skipRotate bool 155 for { 156 select { 157 case <-ctx.Done(): 158 // the consumer has cancelled 159 t.Kill(errors.New("hangup by client")) 160 return nil 161 case line, ok = <-t.Lines: 162 if !ok { 163 // channel was closed 164 return nil 165 } 166 // fallthrough 167 } 168 169 event, err := newEventFromJSONString(line.Text) 170 if err != nil { 171 return err 172 } 173 switch event.Type { 174 case Image, Volume, Pod, Container, Network: 175 // no-op 176 case System: 177 begin, end, err := e.readRotateEvent(event) 178 if err != nil { 179 return err 180 } 181 if begin && event.Time.After(readTime) { 182 // If the rotation event happened _after_ we 183 // started reading, we need to ignore/skip 184 // subsequent event until the end of the 185 // rotation. 186 skipRotate = true 187 logrus.Debugf("Skipping already read events after log-file rotation: %v", event) 188 } else if end { 189 // This rotate event 190 skipRotate = false 191 } 192 default: 193 return fmt.Errorf("event type %s is not valid in %s", event.Type.String(), e.options.LogFilePath) 194 } 195 if skipRotate { 196 continue 197 } 198 if applyFilters(event, filterMap) { 199 options.EventChannel <- event 200 } 201 } 202 } 203 204 // String returns a string representation of the logger 205 func (e EventLogFile) String() string { 206 return LogFile.String() 207 } 208 209 const ( 210 rotateEventAttribute = "io.podman.event.rotate" 211 rotateEventBegin = "begin" 212 rotateEventEnd = "end" 213 ) 214 215 func writeRotateEvent(f *os.File, logFilePath string, begin bool) error { 216 rEvent := NewEvent(Rotate) 217 rEvent.Type = System 218 rEvent.Name = logFilePath 219 rEvent.Attributes = make(map[string]string) 220 if begin { 221 rEvent.Attributes[rotateEventAttribute] = rotateEventBegin 222 } else { 223 rEvent.Attributes[rotateEventAttribute] = rotateEventEnd 224 } 225 rotateJSONString, err := rEvent.ToJSONString() 226 if err != nil { 227 return err 228 } 229 return writeToFile(rotateJSONString, f) 230 } 231 232 // Rotates the log file if the log file size and content exceeds limit 233 func rotateLog(logfile string, content string, limit uint64) (bool, error) { 234 needsRotation, err := logNeedsRotation(logfile, content, limit) 235 if err != nil || !needsRotation { 236 return false, err 237 } 238 if err := truncate(logfile); err != nil { 239 return false, err 240 } 241 return true, nil 242 } 243 244 // logNeedsRotation return true if the log file needs to be rotated. 245 func logNeedsRotation(logfile string, content string, limit uint64) (bool, error) { 246 if limit == 0 { 247 return false, nil 248 } 249 file, err := os.Stat(logfile) 250 if err != nil { 251 if errors.Is(err, os.ErrNotExist) { 252 // The logfile does not exist yet. 253 return false, nil 254 } 255 return false, err 256 } 257 var filesize = uint64(file.Size()) 258 var contentsize = uint64(len([]rune(content))) 259 if filesize+contentsize < limit { 260 return false, nil 261 } 262 263 return true, nil 264 } 265 266 // Truncates the log file and saves 50% of content to new log file 267 func truncate(filePath string) error { 268 orig, err := os.Open(filePath) 269 if err != nil { 270 return err 271 } 272 defer orig.Close() 273 274 origFinfo, err := orig.Stat() 275 if err != nil { 276 return err 277 } 278 279 size := origFinfo.Size() 280 threshold := size / 2 281 282 tmp, err := os.CreateTemp(path.Dir(filePath), "") 283 if err != nil { 284 // Retry in /tmp in case creating a tmp file in the same 285 // directory has failed. 286 tmp, err = os.CreateTemp("", "") 287 if err != nil { 288 return err 289 } 290 } 291 defer tmp.Close() 292 293 // Jump directly to the threshold, drop the first line and copy the remainder 294 if _, err := orig.Seek(threshold, 0); err != nil { 295 return err 296 } 297 reader := bufio.NewReader(orig) 298 if _, err := reader.ReadString('\n'); err != nil { 299 if !errors.Is(err, io.EOF) { 300 return err 301 } 302 } 303 304 if err := writeRotateEvent(tmp, filePath, true); err != nil { 305 return fmt.Errorf("writing rotation event begin marker: %w", err) 306 } 307 if _, err := reader.WriteTo(tmp); err != nil { 308 return fmt.Errorf("writing truncated contents: %w", err) 309 } 310 if err := writeRotateEvent(tmp, filePath, false); err != nil { 311 return fmt.Errorf("writing rotation event end marker: %w", err) 312 } 313 314 if err := renameLog(tmp.Name(), filePath); err != nil { 315 return fmt.Errorf("writing back %s to %s: %w", tmp.Name(), filePath, err) 316 } 317 318 return nil 319 } 320 321 // Renames from, to 322 func renameLog(from, to string) error { 323 err := os.Rename(from, to) 324 if err == nil { 325 return nil 326 } 327 328 if !errors.Is(err, unix.EXDEV) { 329 return err 330 } 331 332 // Files are not on the same partition, so we need to copy them the 333 // hard way. 334 fFrom, err := os.Open(from) 335 if err != nil { 336 return err 337 } 338 defer fFrom.Close() 339 340 // Remove the old file to make sure we're not truncating current 341 // readers. 342 if err := os.Remove(to); err != nil { 343 return fmt.Errorf("recreating file %s: %w", to, err) 344 } 345 346 fTo, err := os.Create(to) 347 if err != nil { 348 return err 349 } 350 defer fTo.Close() 351 352 if _, err := io.Copy(fTo, fFrom); err != nil { 353 return fmt.Errorf("writing back from temporary file: %w", err) 354 } 355 356 if err := os.Remove(from); err != nil { 357 return fmt.Errorf("removing temporary file: %w", err) 358 } 359 360 return nil 361 }