123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- /*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
- package main
- import (
- "context"
- "errors"
- "flag"
- "fmt"
- "io"
- "log"
- "os"
- "os/signal"
- "regexp"
- "time"
- )
- // -3 silent
- // -2 fatal
- // -1 error
- //
- // 0 warn
- // 1 info
- // 2 debug
- var (
- LogLevel int
- Version, Build string
- )
- func debug(fmt string, msg ...any) {
- if LogLevel < 2 {
- return
- }
- fmt = "debug: " + fmt
- if len(msg) == 0 {
- log.Printf(fmt)
- return
- }
- log.Printf(fmt, msg...)
- }
- var Options = struct {
- pattern *regexp.Regexp
- command string
- jumpToEnd,
- matchOnce,
- printNonmatching,
- verbose,
- debug bool
- }{}
- func main() {
- ctx, cancel := context.WithCancel(context.Background())
- go func() {
- sig := make(chan os.Signal, 1) // catch signals
- signal.Notify(sig, os.Interrupt, os.Kill)
- for {
- switch <-sig {
- case os.Interrupt, os.Kill:
- log.Printf("Caught SIGINT/SIGTERM.")
- cancel()
- }
- }
- }()
- handlePatternFlag := func(p string) error {
- var err error
- Options.pattern, err = regexp.Compile(p)
- return err
- }
- flags := flag.NewFlagSet("watchlogs", flag.ExitOnError)
- flags.SetOutput(os.Stderr)
- flags.Func("pattern", "process lines matching pattern", handlePatternFlag)
- flags.Func("p", "process lines matching pattern", handlePatternFlag)
- flags.BoolVar(&Options.jumpToEnd, "jump", false, "jump to end of file before processing")
- flags.BoolVar(&Options.jumpToEnd, "j", false, "jump to end of file before processing")
- flags.BoolVar(&Options.matchOnce, "once", false, "stop after first matching line is processed")
- flags.BoolVar(&Options.matchOnce, "1", false, "stop after first matching line is processed")
- flags.BoolVar(&Options.printNonmatching, "n", false, "print all lines, not just matching ones")
- flags.BoolVar(&Options.verbose, "v", false, "increase verbosity")
- flags.BoolVar(&Options.debug, "debug", false, "print debugging information")
- flags.StringVar(&Options.command, "command", "", "execute command on match, passing the matched line as input")
- flags.Parse(os.Args[1:])
- logPath := flags.Arg(0)
- if len(flags.Args()) == 0 {
- fmt.Printf("%s %s\n", Version, Build)
- fmt.Printf("usage: %s <log_file> [<option> ...]\n", os.Args[0])
- flags.PrintDefaults()
- os.Exit(1)
- }
- // TODO option: execute command on match
- switch {
- case Options.debug:
- LogLevel = 2
- case Options.verbose:
- LogLevel = 1
- }
- if Options.pattern == nil {
- Options.pattern = regexp.MustCompile(``)
- }
- os.Exit(mainLoop(ctx, logPath))
- }
- func mainLoop(ctx context.Context, path string) int {
- var file *os.File
- var lineCnt uint64
- var err error
- if file, err = os.Open(path); err != nil {
- log.Printf("Could not open file at %#v: %v", path, err)
- return 1
- }
- if file == nil {
- panic("BUG: Received nil file pointer")
- }
- defer func() {
- if err := file.Close(); err != nil {
- log.Printf("Error while closing file: %v", err)
- }
- }()
- if Options.jumpToEnd {
- if _, err := file.Seek(0, 2); err != nil {
- log.Printf("Could not jump to end of file: %v", err)
- }
- }
- initBuf := make([]byte, 0, L1CacheSize)
- buf := initBuf
- for {
- if ctx.Err() != nil {
- return 2
- }
- i := 0
- for i < len(buf) {
- if buf[i] == '\n' {
- break
- }
- i++
- }
- if i != len(buf) {
- line := buf[:i]
- debug("Detected linefeed; processing line %#v", string(line))
- buf = buf[i+1:]
- lineCnt++
- // TODO Decide how to handle commands
- if processLine(line, Options.pattern) {
- fmt.Println(string(line))
- if Options.matchOnce {
- debug("Matched once, as requested; terminating...")
- return 0
- }
- } else if Options.printNonmatching {
- fmt.Println(string(line))
- }
- continue
- }
- if len(buf) == cap(buf) {
- debug("Reached end of buffer; resetting cursor to start of buffer")
- nextBuf := initBuf[0:len(buf)]
- copy(nextBuf, buf)
- buf = nextBuf
- }
- n, err := file.Read(buf[len(buf):cap(buf)])
- if errors.Is(err, io.EOF) {
- var rot bool
- rot, err = detectFileRotation(path, file)
- if rot {
- debug("File %#v was rotated or truncated; reopening...", path)
- if err := file.Close(); err != nil {
- log.Printf("Warning: error while closing file: %v", err)
- }
- if file, err = os.Open(path); err != nil {
- log.Printf("Could not reopen file: %v", err)
- return 1
- }
- continue
- }
- time.Sleep(100 * time.Millisecond)
- continue
- }
- if err != nil {
- log.Printf("Unexpected read error: %v", err)
- return 3
- }
- buf = buf[:len(buf)+n]
- }
- }
- func processLine(line []byte, pattern *regexp.Regexp) bool {
- // TODO Allow for more sophisticated processing.
- return pattern.Match(line)
- }
|