github.com/nsqio/nsq@v1.3.0/apps/nsq_to_file/file_logger.go (about) 1 package main 2 3 import ( 4 "compress/gzip" 5 "errors" 6 "fmt" 7 "io" 8 "os" 9 "path" 10 "path/filepath" 11 "strings" 12 "time" 13 14 "github.com/nsqio/go-nsq" 15 "github.com/nsqio/nsq/internal/lg" 16 ) 17 18 type FileLogger struct { 19 logf lg.AppLogFunc 20 opts *Options 21 topic string 22 consumer *nsq.Consumer 23 24 out *os.File 25 writer io.Writer 26 gzipWriter *gzip.Writer 27 logChan chan *nsq.Message 28 filenameFormat string 29 30 termChan chan bool 31 hupChan chan bool 32 33 // for rotation 34 filename string 35 openTime time.Time 36 filesize int64 37 rev uint 38 } 39 40 func NewFileLogger(logf lg.AppLogFunc, opts *Options, topic string, cfg *nsq.Config) (*FileLogger, error) { 41 computedFilenameFormat, err := computeFilenameFormat(opts, topic) 42 if err != nil { 43 return nil, err 44 } 45 46 consumer, err := nsq.NewConsumer(topic, opts.Channel, cfg) 47 if err != nil { 48 return nil, err 49 } 50 51 f := &FileLogger{ 52 logf: logf, 53 opts: opts, 54 topic: topic, 55 consumer: consumer, 56 logChan: make(chan *nsq.Message, 1), 57 filenameFormat: computedFilenameFormat, 58 termChan: make(chan bool), 59 hupChan: make(chan bool), 60 } 61 consumer.AddHandler(f) 62 63 err = consumer.ConnectToNSQDs(opts.NSQDTCPAddrs) 64 if err != nil { 65 return nil, err 66 } 67 68 err = consumer.ConnectToNSQLookupds(opts.NSQLookupdHTTPAddrs) 69 if err != nil { 70 return nil, err 71 } 72 73 return f, nil 74 } 75 76 func (f *FileLogger) HandleMessage(m *nsq.Message) error { 77 m.DisableAutoResponse() 78 f.logChan <- m 79 return nil 80 } 81 82 func (f *FileLogger) router() { 83 pos := 0 84 output := make([]*nsq.Message, f.opts.MaxInFlight) 85 sync := false 86 ticker := time.NewTicker(f.opts.SyncInterval) 87 closeFile := false 88 exit := false 89 90 for { 91 select { 92 case <-f.consumer.StopChan: 93 sync = true 94 closeFile = true 95 exit = true 96 case <-f.termChan: 97 ticker.Stop() 98 f.consumer.Stop() 99 sync = true 100 case <-f.hupChan: 101 sync = true 102 closeFile = true 103 case <-ticker.C: 104 if f.needsRotation() { 105 if f.opts.SkipEmptyFiles { 106 closeFile = true 107 } else { 108 f.updateFile() 109 } 110 } 111 sync = true 112 case m := <-f.logChan: 113 if f.needsRotation() { 114 f.updateFile() 115 sync = true 116 } 117 _, err := f.Write(m.Body) 118 if err != nil { 119 f.logf(lg.FATAL, "[%s/%s] writing message to disk: %s", f.topic, f.opts.Channel, err) 120 os.Exit(1) 121 } 122 _, err = f.Write([]byte("\n")) 123 if err != nil { 124 f.logf(lg.FATAL, "[%s/%s] writing newline to disk: %s", f.topic, f.opts.Channel, err) 125 os.Exit(1) 126 } 127 output[pos] = m 128 pos++ 129 if pos == cap(output) { 130 sync = true 131 } 132 } 133 134 if sync || f.consumer.IsStarved() { 135 if pos > 0 { 136 f.logf(lg.INFO, "[%s/%s] syncing %d records to disk", f.topic, f.opts.Channel, pos) 137 err := f.Sync() 138 if err != nil { 139 f.logf(lg.FATAL, "[%s/%s] failed syncing messages: %s", f.topic, f.opts.Channel, err) 140 os.Exit(1) 141 } 142 for pos > 0 { 143 pos-- 144 m := output[pos] 145 m.Finish() 146 output[pos] = nil 147 } 148 } 149 sync = false 150 } 151 152 if closeFile { 153 f.Close() 154 closeFile = false 155 } 156 157 if exit { 158 break 159 } 160 } 161 } 162 163 func (f *FileLogger) Close() { 164 if f.out == nil { 165 return 166 } 167 168 if f.gzipWriter != nil { 169 err := f.gzipWriter.Close() 170 if err != nil { 171 f.logf(lg.FATAL, "[%s/%s] failed to close GZIP writer: %s", f.topic, f.opts.Channel, err) 172 os.Exit(1) 173 } 174 } 175 err := f.out.Sync() 176 if err != nil { 177 f.logf(lg.FATAL, "[%s/%s] failed to fsync output file: %s", f.topic, f.opts.Channel, err) 178 os.Exit(1) 179 } 180 err = f.out.Close() 181 if err != nil { 182 f.logf(lg.FATAL, "[%s/%s] failed to close output file: %s", f.topic, f.opts.Channel, err) 183 os.Exit(1) 184 } 185 186 // Move file from work dir to output dir if necessary, taking care not 187 // to overwrite existing files 188 if f.opts.WorkDir != f.opts.OutputDir { 189 src := f.out.Name() 190 dst := filepath.Join(f.opts.OutputDir, strings.TrimPrefix(src, f.opts.WorkDir)) 191 192 // Optimistic rename 193 f.logf(lg.INFO, "[%s/%s] moving finished file %s to %s", f.topic, f.opts.Channel, src, dst) 194 err := exclusiveRename(src, dst) 195 if err == nil { 196 return 197 } else if !os.IsExist(err) { 198 f.logf(lg.FATAL, "[%s/%s] unable to move file from %s to %s: %s", f.topic, f.opts.Channel, src, dst, err) 199 os.Exit(1) 200 } 201 202 // Optimistic rename failed, so we need to generate a new 203 // destination file name by bumping the revision number. 204 _, filenameTmpl := filepath.Split(f.filename) 205 dstDir, _ := filepath.Split(dst) 206 dstTmpl := filepath.Join(dstDir, filenameTmpl) 207 208 for i := f.rev + 1; ; i++ { 209 f.logf(lg.WARN, "[%s/%s] destination file already exists: %s", f.topic, f.opts.Channel, dst) 210 dst := strings.Replace(dstTmpl, "<REV>", fmt.Sprintf("-%06d", i), -1) 211 err := exclusiveRename(src, dst) 212 if err != nil { 213 if os.IsExist(err) { 214 continue // next rev 215 } 216 f.logf(lg.FATAL, "[%s/%s] unable to rename file from %s to %s: %s", f.topic, f.opts.Channel, src, dst, err) 217 os.Exit(1) 218 } 219 f.logf(lg.INFO, "[%s/%s] renamed finished file %s to %s to avoid overwrite", f.topic, f.opts.Channel, src, dst) 220 break 221 } 222 } 223 224 f.out = nil 225 } 226 227 func (f *FileLogger) Write(p []byte) (int, error) { 228 n, err := f.writer.Write(p) 229 f.filesize += int64(n) 230 return n, err 231 } 232 233 func (f *FileLogger) Sync() error { 234 var err error 235 if f.gzipWriter != nil { 236 // finish current gzip stream and start a new one (concatenated) 237 // gzip stream trailer has checksum, and can indicate which messages were ACKed 238 err = f.gzipWriter.Close() 239 if err != nil { 240 return err 241 } 242 err = f.out.Sync() 243 f.gzipWriter, _ = gzip.NewWriterLevel(f.out, f.opts.GZIPLevel) 244 f.writer = f.gzipWriter 245 } else { 246 err = f.out.Sync() 247 } 248 return err 249 } 250 251 func (f *FileLogger) currentFilename() string { 252 t := time.Now() 253 datetime := strftime(f.opts.DatetimeFormat, t) 254 return strings.Replace(f.filenameFormat, "<DATETIME>", datetime, -1) 255 } 256 257 func (f *FileLogger) needsRotation() bool { 258 if f.out == nil { 259 return true 260 } 261 262 filename := f.currentFilename() 263 if filename != f.filename { 264 f.logf(lg.INFO, "[%s/%s] new filename %s, rotating...", f.topic, f.opts.Channel, filename) 265 return true // rotate by filename 266 } 267 268 if f.opts.RotateInterval > 0 { 269 if s := time.Since(f.openTime); s > f.opts.RotateInterval { 270 f.logf(lg.INFO, "[%s/%s] %s since last open, rotating...", f.topic, f.opts.Channel, s) 271 return true // rotate by interval 272 } 273 } 274 275 if f.opts.RotateSize > 0 && f.filesize > f.opts.RotateSize { 276 f.logf(lg.INFO, "[%s/%s] %s currently %d bytes (> %d), rotating...", 277 f.topic, f.opts.Channel, f.out.Name(), f.filesize, f.opts.RotateSize) 278 return true // rotate by size 279 } 280 281 return false 282 } 283 284 func (f *FileLogger) updateFile() { 285 f.Close() // uses current f.filename and f.rev to resolve rename dst conflict 286 287 filename := f.currentFilename() 288 if filename != f.filename { 289 f.rev = 0 // reset revision to 0 if it is a new filename 290 } else { 291 f.rev++ 292 } 293 f.filename = filename 294 f.openTime = time.Now() 295 296 fullPath := path.Join(f.opts.WorkDir, filename) 297 err := makeDirFromPath(f.logf, fullPath) 298 if err != nil { 299 f.logf(lg.FATAL, "[%s/%s] unable to create dir: %s", f.topic, f.opts.Channel, err) 300 os.Exit(1) 301 } 302 303 var fi os.FileInfo 304 for ; ; f.rev++ { 305 absFilename := strings.Replace(fullPath, "<REV>", fmt.Sprintf("-%06d", f.rev), -1) 306 307 // If we're using a working directory for in-progress files, 308 // proactively check for duplicate file names in the output dir to 309 // prevent conflicts on rename in the normal case 310 if f.opts.WorkDir != f.opts.OutputDir { 311 outputFileName := filepath.Join(f.opts.OutputDir, strings.TrimPrefix(absFilename, f.opts.WorkDir)) 312 err := makeDirFromPath(f.logf, outputFileName) 313 if err != nil { 314 f.logf(lg.FATAL, "[%s/%s] unable to create dir: %s", f.topic, f.opts.Channel, err) 315 os.Exit(1) 316 } 317 318 _, err = os.Stat(outputFileName) 319 if err == nil { 320 f.logf(lg.WARN, "[%s/%s] output file already exists: %s", f.topic, f.opts.Channel, outputFileName) 321 continue // next rev 322 } else if !os.IsNotExist(err) { 323 f.logf(lg.FATAL, "[%s/%s] unable to stat output file %s: %s", f.topic, f.opts.Channel, outputFileName, err) 324 os.Exit(1) 325 } 326 } 327 328 openFlag := os.O_WRONLY | os.O_CREATE 329 if f.opts.GZIP || f.opts.RotateInterval > 0 { 330 openFlag |= os.O_EXCL 331 } else { 332 openFlag |= os.O_APPEND 333 } 334 f.out, err = os.OpenFile(absFilename, openFlag, 0666) 335 if err != nil { 336 if os.IsExist(err) { 337 f.logf(lg.WARN, "[%s/%s] working file already exists: %s", f.topic, f.opts.Channel, absFilename) 338 continue // next rev 339 } 340 f.logf(lg.FATAL, "[%s/%s] unable to open %s: %s", f.topic, f.opts.Channel, absFilename, err) 341 os.Exit(1) 342 } 343 344 f.logf(lg.INFO, "[%s/%s] opening %s", f.topic, f.opts.Channel, absFilename) 345 346 fi, err = f.out.Stat() 347 if err != nil { 348 f.logf(lg.FATAL, "[%s/%s] unable to stat file %s: %s", f.topic, f.opts.Channel, f.out.Name(), err) 349 } 350 f.filesize = fi.Size() 351 352 if f.opts.RotateSize > 0 && f.filesize > f.opts.RotateSize { 353 f.logf(lg.INFO, "[%s/%s] %s currently %d bytes (> %d), rotating...", 354 f.topic, f.opts.Channel, f.out.Name(), f.filesize, f.opts.RotateSize) 355 continue // next rev 356 } 357 358 break // good file 359 } 360 361 if f.opts.GZIP { 362 f.gzipWriter, _ = gzip.NewWriterLevel(f.out, f.opts.GZIPLevel) 363 f.writer = f.gzipWriter 364 } else { 365 f.writer = f.out 366 } 367 } 368 369 func makeDirFromPath(logf lg.AppLogFunc, path string) error { 370 dir, _ := filepath.Split(path) 371 if dir != "" { 372 return os.MkdirAll(dir, 0770) 373 } 374 return nil 375 } 376 377 func exclusiveRename(src, dst string) error { 378 err := os.Link(src, dst) 379 if err != nil { 380 return err 381 } 382 383 err = os.Remove(src) 384 if err != nil { 385 return err 386 } 387 388 return nil 389 } 390 391 func computeFilenameFormat(opts *Options, topic string) (string, error) { 392 hostname, err := os.Hostname() 393 if err != nil { 394 return "", err 395 } 396 shortHostname := strings.Split(hostname, ".")[0] 397 398 identifier := shortHostname 399 if len(opts.HostIdentifier) != 0 { 400 identifier = strings.Replace(opts.HostIdentifier, "<SHORT_HOST>", shortHostname, -1) 401 identifier = strings.Replace(identifier, "<HOSTNAME>", hostname, -1) 402 } 403 404 cff := opts.FilenameFormat 405 if opts.GZIP || opts.RotateSize > 0 || opts.RotateInterval > 0 || opts.WorkDir != opts.OutputDir { 406 if !strings.Contains(cff, "<REV>") { 407 return "", errors.New("missing <REV> in --filename-format when gzip, rotation, or work dir enabled") 408 } 409 } else { 410 // remove <REV> as we don't need it 411 cff = strings.Replace(cff, "<REV>", "", -1) 412 } 413 cff = strings.Replace(cff, "<TOPIC>", topic, -1) 414 cff = strings.Replace(cff, "<HOST>", identifier, -1) 415 cff = strings.Replace(cff, "<PID>", fmt.Sprintf("%d", os.Getpid()), -1) 416 if opts.GZIP && !strings.HasSuffix(cff, ".gz") { 417 cff = cff + ".gz" 418 } 419 420 return cff, nil 421 }