| 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) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment