Skip to content

Instantly share code, notes, and snippets.

@butlerwang
Forked from prohulaelk/phetail.go
Created February 17, 2018 20:23
Show Gist options
  • Save butlerwang/27a6566a6a57f482ca2d1c052248d6f3 to your computer and use it in GitHub Desktop.
Save butlerwang/27a6566a6a57f482ca2d1c052248d6f3 to your computer and use it in GitHub Desktop.

Revisions

  1. @prohulaelk prohulaelk created this gist Mar 20, 2017.
    187 changes: 187 additions & 0 deletions phetail.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,187 @@
    package main

    import (
    "bufio"
    "flag"
    "fmt"
    "io"
    "os"
    "path/filepath"
    "regexp"
    "time"
    )

    var (
    rootdir string
    files []*tailer
    filename string
    filetype string
    numfiles int
    lines llist
    searchpattern string
    searchregex *regexp.Regexp
    )

    var (
    reTimeStamp = regexp.MustCompile(`(?im)^\d+[\/\-\\]\d+[\/\-\\]\d+\s+\d+\:\d+\:\d+\s+([AP]M)?`)
    reEntryTypes = regexp.MustCompile(`FATAL|INFO|WARN|ERROR|DEBUG`)
    )

    // linked list configuration
    type litem struct {
    data string
    prev, next *litem
    }

    type llist struct {
    head *litem
    size int
    }

    func (L *llist) insert(s string) {
    i := litem{data: s}
    i.next = L.head
    if L.head != nil {
    L.head.prev = &i
    }
    L.head = &i
    L.size += 1
    }

    func (L *llist) pop() *litem {
    k := L.head
    if k == nil {
    return k
    }
    for k.next != nil {
    k = k.next
    }
    if k.prev != nil {
    k.prev.next = k.next
    } else {
    L.head = k.next
    }
    L.size--
    return k
    }

    func (L *llist) dump() {
    var pop *litem
    pop = L.pop()
    for pop != nil {
    fmt.Print(pop.data)
    pop = L.pop()
    }
    }

    // tailer struct and logic

    type tailer struct {
    path string
    size int64
    }

    func (t *tailer) init() {
    stats, _ := os.Stat(t.path)
    t.size = stats.Size()
    }

    func (t *tailer) update() {
    stats, _ := os.Stat(t.path)
    cursize := stats.Size()
    if cursize != t.size {
    var readfrom int64
    if cursize > t.size {
    readfrom = t.size
    }
    t.queuelogs(readfrom)
    t.size = cursize
    }
    }

    func (t *tailer) queuelogs(readfrom int64) {
    file, _ := os.Open(t.path)
    file.Seek(readfrom, 0)
    reader := bufio.NewReader(file)
    text, err := reader.ReadString('\n')
    var line string
    for err != io.EOF {
    line, err = reader.ReadString('\n')
    text += line
    }
    timestamps := reTimeStamp.FindAllString(text, -1)
    logs := reTimeStamp.Split(text, -1)
    for i, log := range logs {
    // skip the first log entry, which will be blank because of how Split() works.
    if i != 0 {
    if searchregex.MatchString(log) {
    /* timestamps will be one entry shorter than the logs because of how
    * FindAllString() and Split() work */
    lines.insert(timestamps[i-1] + log)
    }
    }
    }
    }

    func newtailer(path string) *tailer {
    t := tailer{path: path}
    t.init()
    return &t
    }

    // main logic

    func walkfunc(path string, info os.FileInfo, err error) error {
    // create loggers for all files in a folder
    if filepath.Ext(path) == filetype && filepath.Dir(path) == rootdir {
    files = append(files, newtailer(path))
    }
    return err
    }

    func init() {
    // initialize command line arguments for parsing
    const (
    filedefault = ""
    fileusage = "Specify a file to tail. Otherwise, attempt to tail all files in the current directory."
    ftdefault = ".txt"
    ftusage = "Specify a filetype to read."
    searchdefault = ""
    searchusage = "Only shows lines that match the supplied regular expression. This will be case insensitive."
    )
    flag.StringVar(&filename, "filename", filedefault, fileusage)
    flag.StringVar(&filename, "f", filedefault, fileusage+" (shorthand)")
    flag.StringVar(&filetype, "filetype", ftdefault, ftusage)
    flag.StringVar(&filetype, "ft", ftdefault, ftusage+" (shorthand)")
    flag.StringVar(&searchpattern, "search", searchdefault, searchusage)
    flag.StringVar(&searchpattern, "s", searchdefault, searchusage+" (shorthand)")
    }

    func main() {
    flag.Parse()
    if searchpattern == "" {
    searchpattern = ".*"
    }
    var err error
    // ims flags mean 'case-insensitive', 'multiline', and '. matches \n' respectively
    searchregex, err = regexp.Compile("(?ims)" + searchpattern)
    if err != nil {
    panic(searchpattern + " is not a valid regular expression.")
    }
    files = make([]*tailer, 0, 1000)
    if filename == "" {
    dir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
    rootdir = dir
    filepath.Walk(dir, walkfunc)
    } else {
    files = append(files, newtailer(filename))
    }

    for true {
    for _, f := range files {
    f.update()
    }
    lines.dump()
    time.Sleep(25 * time.Millisecond)
    }
    }