blob: 2a30cb74dda23b7f43ba976360ffe6941bb0120b [file] [log] [blame]
package logparser
import (
"bufio"
"bytes"
"fmt"
"io"
"regexp"
"strings"
"time"
"go.skia.org/infra/go/sklog"
)
// A Parser is a function that takes the text contents of a log file and returns a ParsedLog.
// There is one Parser for any type of logs that we support. It typically uses regular
// expressions to perform any parsing. In the process of parsing the logs, it basically
// makes a copy of every line in the log, so be aware of that memory usage.
type Parser func(string) ParsedLog
// A ParsedLog is a view of lines in a log file. It implements methods to allow for
// random access as well as linear iterators.
type ParsedLog interface {
// Start sets the current line to view.
Start(int)
// CurrLine gets the current line.
CurrLine() int
// Len returns how many parsed lines there are.
Len() int
// ReadAndNext returns a parsed line or nil and advances to the next line.
ReadAndNext() *sklog.LogPayload
// ReadLine sets the line to the specified number and returns the parsed line at that location.
ReadLine(int) *sklog.LogPayload
}
type logParser struct {
Content []string
Line int
SplitLine *regexp.Regexp
ParseLine func(s string) *sklog.LogPayload
}
func (p *logParser) init(r io.Reader) {
s := bufio.NewScanner(r)
c := ""
for s.Scan() {
line := s.Text()
if p.SplitLine.MatchString(line) {
if c != "" {
p.Content = append(p.Content, c)
}
c = ""
}
c += line + "\n"
}
p.Content = append(p.Content, c)
}
func (p *logParser) Start(i int) {
p.Line = i
}
func (p *logParser) CurrLine() int {
return p.Line
}
func (p *logParser) Len() int {
return len(p.Content)
}
func (p *logParser) ReadAndNext() *sklog.LogPayload {
if r := p.ReadLine(p.Line); r != nil {
p.Line++
return r
}
return nil
}
func (p *logParser) ReadLine(i int) *sklog.LogPayload {
if i >= 0 && i < len(p.Content) {
p.Line = i
s := strings.TrimSpace(p.Content[i])
if s == "" {
return nil
}
return p.ParseLine(s)
}
return nil
}
// syslog doesn't have a year in its logs. We will interpret it as in "this year".
var currentYear int
func ParseSyslog(contents string) ParsedLog {
p := &logParser{
SplitLine: syslogLine,
ParseLine: parseSysLog,
Line: 0,
}
// Update current year any time a new parsed log is created.
// This will hopefully minimize problems around Dec 31.
currentYear = time.Now().Year()
p.init(bytes.NewBufferString(contents))
return p
}
// syslog has a format like [time(implicitly in Eastern and current year)] [hostname] [logmessage]
var syslogLine = regexp.MustCompile(`\S\S\S \d\d \d\d:\d\d:\d\d [a-z0-9\-]+ `)
var syslogParse = regexp.MustCompile(`(?s)(?P<time>\S\S\S \d\d \d\d:\d\d:\d\d) (?P<hostname>[a-z0-9\-]+) (?P<payload>.*)`)
const syslogRef = "Jan 2 15:04:05 2006"
func parseSysLog(line string) *sklog.LogPayload {
p := sklog.LogPayload{}
matches := syslogParse.FindStringSubmatch(line)
if len(matches) < 4 {
sklog.CloudLogError("syslogparser", fmt.Errorf("Problem parsing syslog line: \n%s\n", line))
return &p
}
// matches[1] is time, [3] is payload
t := fmt.Sprintf("%s %d", matches[1], currentYear)
if parsed, err := time.ParseInLocation(syslogRef, t, time.Local); err == nil {
p.Time = parsed
} else {
sklog.CloudLogError("syslogparser", fmt.Errorf("Problem parsing date: %s\n", err))
}
p.Payload = strings.TrimSpace(matches[3])
p.Severity = sklog.INFO
return &p
}
func ParsePythonLog(contents string) ParsedLog {
p := &logParser{
SplitLine: pythonLogLine,
ParseLine: parsePythonLog,
Line: 0,
}
p.init(bytes.NewBufferString(contents))
return p
}
// pythonLog has a format like [thread] [time(implicitly in UTC)] [severity] [payload]
var pythonLogLine = regexp.MustCompile(`\d+ \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d+ \w: `)
var pythonLogParse = regexp.MustCompile(`(?s)\d+ (?P<time>\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d+) (?P<severity>\w): (?P<payload>.*)`)
const pythonLogRef = "2006-01-02 15:04:05.000"
var pythonLogSeverity = map[string]string{
"D": sklog.DEBUG,
"I": sklog.INFO,
"W": sklog.WARNING,
"E": sklog.ERROR,
"C": sklog.CRITICAL,
}
func parsePythonLog(line string) *sklog.LogPayload {
p := sklog.LogPayload{}
matches := pythonLogParse.FindStringSubmatch(line)
if len(matches) < 4 {
sklog.CloudLogError("pyparser", fmt.Errorf("Problem parsing pythonLog line: \n%s\n", line))
return &p
}
// matches[1] is time, [2] is severity, [3] is payload
if parsed, err := time.ParseInLocation(pythonLogRef, matches[1], time.UTC); err == nil {
p.Time = parsed
} else {
sklog.CloudLogError("pyparser", fmt.Errorf("Problem parsing date: %s\n", err))
}
p.Payload = strings.TrimSpace(matches[3])
sev, found := pythonLogSeverity[matches[2]]
if !found {
sev = sklog.INFO
}
p.Severity = sev
return &p
}