main.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. /*
  2. * This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  5. */
  6. package main
  7. import (
  8. "context"
  9. "errors"
  10. "flag"
  11. "fmt"
  12. "io"
  13. "log"
  14. "os"
  15. "os/signal"
  16. "regexp"
  17. "time"
  18. )
  19. // -3 silent
  20. // -2 fatal
  21. // -1 error
  22. //
  23. // 0 warn
  24. // 1 info
  25. // 2 debug
  26. var (
  27. LogLevel int
  28. Version, Build string
  29. )
  30. func debug(fmt string, msg ...any) {
  31. if LogLevel < 2 {
  32. return
  33. }
  34. fmt = "debug: " + fmt
  35. if len(msg) == 0 {
  36. log.Printf(fmt)
  37. return
  38. }
  39. log.Printf(fmt, msg...)
  40. }
  41. var Options = struct {
  42. pattern *regexp.Regexp
  43. command string
  44. jumpToEnd,
  45. matchOnce,
  46. printNonmatching,
  47. verbose,
  48. debug bool
  49. }{}
  50. func main() {
  51. ctx, cancel := context.WithCancel(context.Background())
  52. go func() {
  53. sig := make(chan os.Signal, 1) // catch signals
  54. signal.Notify(sig, os.Interrupt, os.Kill)
  55. for {
  56. switch <-sig {
  57. case os.Interrupt, os.Kill:
  58. log.Printf("Caught SIGINT/SIGTERM.")
  59. cancel()
  60. }
  61. }
  62. }()
  63. handlePatternFlag := func(p string) error {
  64. var err error
  65. Options.pattern, err = regexp.Compile(p)
  66. return err
  67. }
  68. flags := flag.NewFlagSet("watchlogs", flag.ExitOnError)
  69. flags.SetOutput(os.Stderr)
  70. flags.Func("pattern", "process lines matching pattern", handlePatternFlag)
  71. flags.Func("p", "process lines matching pattern", handlePatternFlag)
  72. flags.BoolVar(&Options.jumpToEnd, "jump", false, "jump to end of file before processing")
  73. flags.BoolVar(&Options.jumpToEnd, "j", false, "jump to end of file before processing")
  74. flags.BoolVar(&Options.matchOnce, "once", false, "stop after first matching line is processed")
  75. flags.BoolVar(&Options.matchOnce, "1", false, "stop after first matching line is processed")
  76. flags.BoolVar(&Options.printNonmatching, "n", false, "print all lines, not just matching ones")
  77. flags.BoolVar(&Options.verbose, "v", false, "increase verbosity")
  78. flags.BoolVar(&Options.debug, "debug", false, "print debugging information")
  79. flags.StringVar(&Options.command, "command", "", "execute command on match, passing the matched line as input")
  80. flags.Parse(os.Args[1:])
  81. logPath := flags.Arg(0)
  82. if len(flags.Args()) == 0 {
  83. fmt.Printf("%s %s\n", Version, Build)
  84. fmt.Printf("usage: %s <log_file> [<option> ...]\n", os.Args[0])
  85. flags.PrintDefaults()
  86. os.Exit(1)
  87. }
  88. // TODO option: execute command on match
  89. switch {
  90. case Options.debug:
  91. LogLevel = 2
  92. case Options.verbose:
  93. LogLevel = 1
  94. }
  95. if Options.pattern == nil {
  96. Options.pattern = regexp.MustCompile(``)
  97. }
  98. os.Exit(mainLoop(ctx, logPath))
  99. }
  100. func mainLoop(ctx context.Context, path string) int {
  101. var file *os.File
  102. var lineCnt uint64
  103. var err error
  104. if file, err = os.Open(path); err != nil {
  105. log.Printf("Could not open file at %#v: %v", path, err)
  106. return 1
  107. }
  108. if file == nil {
  109. panic("BUG: Received nil file pointer")
  110. }
  111. defer func() {
  112. if err := file.Close(); err != nil {
  113. log.Printf("Error while closing file: %v", err)
  114. }
  115. }()
  116. if Options.jumpToEnd {
  117. if _, err := file.Seek(0, 2); err != nil {
  118. log.Printf("Could not jump to end of file: %v", err)
  119. }
  120. }
  121. initBuf := make([]byte, 0, L1CacheSize)
  122. buf := initBuf
  123. for {
  124. if ctx.Err() != nil {
  125. return 2
  126. }
  127. i := 0
  128. for i < len(buf) {
  129. if buf[i] == '\n' {
  130. break
  131. }
  132. i++
  133. }
  134. if i != len(buf) {
  135. line := buf[:i]
  136. debug("Detected linefeed; processing line %#v", string(line))
  137. buf = buf[i+1:]
  138. lineCnt++
  139. // TODO Decide how to handle commands
  140. if processLine(line, Options.pattern) {
  141. fmt.Println(string(line))
  142. if Options.matchOnce {
  143. debug("Matched once, as requested; terminating...")
  144. return 0
  145. }
  146. } else if Options.printNonmatching {
  147. fmt.Println(string(line))
  148. }
  149. continue
  150. }
  151. if len(buf) == cap(buf) {
  152. debug("Reached end of buffer; resetting cursor to start of buffer")
  153. nextBuf := initBuf[0:len(buf)]
  154. copy(nextBuf, buf)
  155. buf = nextBuf
  156. }
  157. n, err := file.Read(buf[len(buf):cap(buf)])
  158. if errors.Is(err, io.EOF) {
  159. var rot bool
  160. rot, err = detectFileRotation(path, file)
  161. if rot {
  162. debug("File %#v was rotated or truncated; reopening...", path)
  163. if err := file.Close(); err != nil {
  164. log.Printf("Warning: error while closing file: %v", err)
  165. }
  166. if file, err = os.Open(path); err != nil {
  167. log.Printf("Could not reopen file: %v", err)
  168. return 1
  169. }
  170. continue
  171. }
  172. time.Sleep(100 * time.Millisecond)
  173. continue
  174. }
  175. if err != nil {
  176. log.Printf("Unexpected read error: %v", err)
  177. return 3
  178. }
  179. buf = buf[:len(buf)+n]
  180. }
  181. }
  182. func processLine(line []byte, pattern *regexp.Regexp) bool {
  183. // TODO Allow for more sophisticated processing.
  184. return pattern.Match(line)
  185. }