main.go 6.2 KB

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