Skip to content

Instantly share code, notes, and snippets.

@prohulaelk
Created March 20, 2017 21:21
Show Gist options
  • Save prohulaelk/204ebe69d65093064a7e2d13a70f7967 to your computer and use it in GitHub Desktop.
Save prohulaelk/204ebe69d65093064a7e2d13a70f7967 to your computer and use it in GitHub Desktop.
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