main.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  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. "os"
  14. "os/signal"
  15. "regexp"
  16. "time"
  17. "golang.org/x/term"
  18. logger "idio.link/go/logger/v3"
  19. )
  20. var (
  21. Version, Build string
  22. )
  23. var Options = struct {
  24. pattern *regexp.Regexp
  25. command string
  26. jumpToEnd,
  27. matchOnce,
  28. printNonmatching,
  29. verbose,
  30. debug,
  31. quiet bool
  32. }{}
  33. func main() {
  34. ctx, cancel := context.WithCancel(context.Background())
  35. ctx = context.WithValue(ctx, "log", logger.NewLogger())
  36. log := ctx.Value("log").(*logger.Logger)
  37. done := make(chan struct{})
  38. go func() {
  39. sig := make(chan os.Signal, 1) // catch signals
  40. signal.Notify(sig, os.Interrupt, os.Kill)
  41. for {
  42. select {
  43. case <-sig:
  44. log.Info("Caught SIGINT/SIGTERM.")
  45. cancel()
  46. return
  47. case <-done:
  48. log.Info("Caught SIGINT/SIGTERM.")
  49. return
  50. case <-time.After(100 * time.Millisecond):
  51. // do nothing; just throttle the cpu
  52. }
  53. }
  54. }()
  55. handlePatternFlag := func(p string) error {
  56. var err error
  57. Options.pattern, err = regexp.Compile(p)
  58. return err
  59. }
  60. flags := flag.NewFlagSet("watchlogs", flag.ExitOnError)
  61. flags.SetOutput(os.Stderr)
  62. flags.Func("pattern", "process lines matching pattern", handlePatternFlag)
  63. flags.Func("p", "process lines matching pattern", handlePatternFlag)
  64. flags.BoolVar(&Options.jumpToEnd, "jump", false, "jump to end of file before processing")
  65. flags.BoolVar(&Options.jumpToEnd, "j", false, "jump to end of file before processing")
  66. flags.BoolVar(&Options.matchOnce, "once", false, "stop after first matching line is processed")
  67. flags.BoolVar(&Options.matchOnce, "1", false, "stop after first matching line is processed")
  68. flags.BoolVar(&Options.printNonmatching, "n", false, "print all lines, not just matching ones")
  69. flags.BoolVar(&Options.verbose, "v", false, "increase verbosity")
  70. flags.BoolVar(&Options.debug, "debug", false, "print debugging information")
  71. flags.BoolVar(&Options.debug, "d", false, "print debugging information")
  72. flags.BoolVar(&Options.quiet, "quiet", false, "do not print lines")
  73. flags.BoolVar(&Options.quiet, "q", false, "do not print lines")
  74. flags.StringVar(&Options.command, "command", "", "execute command on match, passing the matched line as input")
  75. flags.Parse(os.Args[1:])
  76. logPath := flags.Arg(0)
  77. if len(flags.Args()) == 0 {
  78. fmt.Fprintf(os.Stderr, "%s %s\n", Version, Build)
  79. fmt.Fprintf(os.Stderr, "usage: %s <log_file> [<option> ...]\n", os.Args[0])
  80. flags.PrintDefaults()
  81. os.Exit(1)
  82. }
  83. // TODO option: execute command on match
  84. switch {
  85. case Options.debug:
  86. log.SetLevel(logger.LogLevelDebug)
  87. case Options.verbose:
  88. log.SetLevel(logger.LogLevelInfo)
  89. case Options.quiet:
  90. log.SetLevel(logger.LogLevelError)
  91. }
  92. if Options.pattern == nil {
  93. Options.pattern = regexp.MustCompile(``)
  94. }
  95. ret := mainLoop(ctx, logPath)
  96. close(done)
  97. os.Exit(ret)
  98. }
  99. func printStatus(n uint64) {
  100. if term.IsTerminal(int(os.Stderr.Fd())) &&
  101. !Options.quiet {
  102. fmt.Fprintf(os.Stderr, "\x1b[%dG", 1)
  103. fmt.Printf("Read %d lines.", n)
  104. }
  105. }
  106. func mainLoop(ctx context.Context, path string) int {
  107. log := ctx.Value("log").(*logger.Logger)
  108. var file *os.File
  109. var lineCnt uint64
  110. var err error
  111. if file, err = os.Open(path); err != nil {
  112. log.Error("Could not open file at %#v: %v", path, err)
  113. return 1
  114. }
  115. if file == nil {
  116. panic("BUG: Received nil file pointer")
  117. }
  118. defer func() {
  119. if err := file.Close(); err != nil {
  120. log.Error("Error while closing file: %v", err)
  121. }
  122. }()
  123. if Options.jumpToEnd {
  124. if _, err := file.Seek(0, 2); err != nil {
  125. log.Error("Could not jump to end of file: %v", err)
  126. }
  127. }
  128. initBuf := make([]byte, 0, L1CacheSize)
  129. buf := initBuf
  130. for {
  131. if ctx.Err() != nil {
  132. return 2
  133. }
  134. i := 0
  135. for i < len(buf) {
  136. if buf[i] == '\n' {
  137. break
  138. }
  139. i++
  140. }
  141. if i != len(buf) {
  142. line := buf[:i]
  143. log.Debug("Detected linefeed; processing line %#v", string(line))
  144. buf = buf[i+1:]
  145. lineCnt++
  146. if lineCnt&1023 == 0 {
  147. printStatus(lineCnt)
  148. }
  149. // TODO Decide how to handle commands
  150. if processLine(line, Options.pattern) {
  151. if !Options.quiet {
  152. fmt.Println()
  153. fmt.Println(string(line))
  154. }
  155. if Options.matchOnce {
  156. log.Info("Matched once, as requested; terminating...")
  157. return 0
  158. }
  159. } else if Options.printNonmatching &&
  160. !Options.quiet {
  161. fmt.Println(string(line))
  162. }
  163. continue
  164. }
  165. if len(buf) == cap(initBuf) {
  166. log.Debug("Line exceeds allocated buffer of %d bytes", cap(initBuf))
  167. initBuf = make([]byte, 0, 2*cap(initBuf))
  168. continue
  169. }
  170. if len(buf) == cap(buf) {
  171. log.Debug("Reached end of buffer; resetting cursor to start of buffer")
  172. nextBuf := initBuf[0:len(buf)]
  173. copy(nextBuf, buf)
  174. buf = nextBuf
  175. continue
  176. }
  177. // TODO Note the difference between extension of
  178. // existing lines and buffering new lines, and make sure
  179. // new line slices are cache aligned.
  180. log.Debug("Attempting to read %d bytes into buffer...", cap(buf)-len(buf))
  181. n, err := file.Read(buf[len(buf):cap(buf)])
  182. if errors.Is(err, io.EOF) {
  183. log.Debug("Reached end of file")
  184. var rot bool
  185. rot, err = detectFileRotation(path, file)
  186. if rot {
  187. log.Info("File %#v was rotated or truncated; reopening...", path)
  188. if err := file.Close(); err != nil {
  189. log.Warn("Error while closing file: %v", err)
  190. }
  191. if file, err = os.Open(path); err != nil {
  192. log.Error("Could not reopen file: %v", err)
  193. return 1
  194. }
  195. continue
  196. }
  197. time.Sleep(100 * time.Millisecond)
  198. continue
  199. }
  200. if err != nil {
  201. log.Error("Unexpected read error: %v", err)
  202. return 3
  203. }
  204. buf = buf[:len(buf)+n]
  205. }
  206. }
  207. func processLine(line []byte, pattern *regexp.Regexp) bool {
  208. // TODO Allow for more sophisticated processing.
  209. return pattern.Match(line)
  210. }