@@ -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 )
}
}