211 lines
4.2 KiB
Go
211 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/hpcloud/tail"
|
|
)
|
|
|
|
type FileMonitor struct {
|
|
config ToolConfig
|
|
parser LogParser
|
|
}
|
|
|
|
type LogParser interface {
|
|
Parse(line string, toolName string) LogEntry
|
|
}
|
|
|
|
func NewFileMonitor(config ToolConfig) *FileMonitor {
|
|
var parser LogParser
|
|
|
|
if config.Format.Pattern != "" {
|
|
pattern, err := regexp.Compile(config.Format.Pattern)
|
|
if err != nil {
|
|
slog.Error("invalid regex pattern", "tool", config.Name, "error", err)
|
|
parser = &DefaultLogParser{}
|
|
} else {
|
|
parser = &RegexLogParser{
|
|
pattern: pattern,
|
|
fields: config.Format.Fields,
|
|
}
|
|
}
|
|
} else {
|
|
switch config.Name {
|
|
case "nginx-tjm":
|
|
parser = &NginxTJMLogParser{}
|
|
default:
|
|
parser = &DefaultLogParser{}
|
|
}
|
|
}
|
|
|
|
return &FileMonitor{
|
|
config: config,
|
|
parser: parser,
|
|
}
|
|
}
|
|
|
|
func (fm *FileMonitor) Start(ctx context.Context, out chan<- LogEntry) error {
|
|
t, err := tail.TailFile(fm.config.LogFile, tail.Config{
|
|
Follow: true,
|
|
ReOpen: true,
|
|
MustExist: false,
|
|
Poll: true,
|
|
Location: &tail.SeekInfo{Offset: 0, Whence: 2},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("tail.TailFile: %w", err)
|
|
}
|
|
defer t.Stop()
|
|
|
|
slog.Debug("Started tailing file", "file", fm.config.LogFile, "tool", fm.config.Name)
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
slog.Debug("File monitor stopped", "tool", fm.config.Name)
|
|
return nil
|
|
case line, ok := <-t.Lines:
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
if line.Err != nil {
|
|
slog.Error("error reading log file", "tool", fm.config.Name, "error", line.Err)
|
|
continue
|
|
}
|
|
|
|
if strings.TrimSpace(line.Text) == "" {
|
|
continue
|
|
}
|
|
|
|
entry := fm.parser.Parse(line.Text, fm.config.Name)
|
|
|
|
select {
|
|
case out <- entry:
|
|
case <-ctx.Done():
|
|
return nil
|
|
default:
|
|
slog.Warn("Log-Channel is full, entry dropped", "tool", fm.config.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type DefaultLogParser struct{}
|
|
|
|
func (p *DefaultLogParser) Parse(line string, toolName string) LogEntry {
|
|
entry := NewLogEntry("log_entry")
|
|
entry.Tool = toolName
|
|
entry.LogMessage = strings.TrimSpace(line)
|
|
entry.Raw = line
|
|
return entry
|
|
}
|
|
|
|
type RegexLogParser struct {
|
|
pattern *regexp.Regexp
|
|
fields map[string]string
|
|
}
|
|
|
|
func (p *RegexLogParser) Parse(line string, toolName string) LogEntry {
|
|
entry := NewLogEntry("log_entry")
|
|
entry.Tool = toolName
|
|
entry.Raw = line
|
|
|
|
fields := p.parseWithPattern(line)
|
|
if fields != nil {
|
|
entry.Fields = fields
|
|
} else {
|
|
entry.LogMessage = strings.TrimSpace(line)
|
|
}
|
|
|
|
return entry
|
|
}
|
|
|
|
func (p *RegexLogParser) parseWithPattern(text string) map[string]any {
|
|
matches := p.pattern.FindStringSubmatch(text)
|
|
if matches == nil {
|
|
return nil
|
|
}
|
|
|
|
fields := make(map[string]any)
|
|
subexpNames := p.pattern.SubexpNames()
|
|
|
|
for i, match := range matches {
|
|
if i == 0 {
|
|
continue
|
|
}
|
|
|
|
if i < len(subexpNames) && subexpNames[i] != "" {
|
|
fieldName := subexpNames[i]
|
|
|
|
if mappedName, exists := p.fields[fieldName]; exists {
|
|
fieldName = mappedName
|
|
}
|
|
|
|
fields[fieldName] = match
|
|
}
|
|
}
|
|
|
|
return fields
|
|
}
|
|
|
|
type NginxTJMLogParser struct{}
|
|
|
|
func (p *NginxTJMLogParser) Parse(line string, toolName string) LogEntry {
|
|
entry := NewLogEntry("log_entry")
|
|
entry.Tool = toolName
|
|
entry.Raw = line
|
|
entry = p.parseNginxTJM(entry)
|
|
return entry
|
|
}
|
|
|
|
func (p *NginxTJMLogParser) parseNginxTJM(entry LogEntry) LogEntry {
|
|
newEntry := entry
|
|
var nginxBase NGinXBaseInfo
|
|
parts := strings.Fields(entry.Raw)
|
|
if len(parts) < 10 {
|
|
return newEntry
|
|
}
|
|
|
|
if len(parts) > 0 {
|
|
timestampStr := strings.Trim(parts[0], "[]")
|
|
timestamp, err := parseRFC3339WithOptionalZ(timestampStr)
|
|
if err != nil {
|
|
slog.Error("unable to parse time", "error", err)
|
|
}
|
|
newEntry.Timestamp = timestamp
|
|
}
|
|
|
|
if len(parts) > 2 {
|
|
nginxBase.ClientIP = parts[2]
|
|
}
|
|
|
|
for i, part := range parts {
|
|
if strings.HasPrefix(part, "\"") {
|
|
if i+1 < len(parts) {
|
|
nginxBase.HTTPMethod = strings.Trim(part, "\"")
|
|
nginxBase.Route = parts[i+1]
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
for _, part := range parts {
|
|
if after, ok := strings.CutPrefix(part, "status="); ok {
|
|
statusCode, err := strconv.ParseInt(after, 10, 64)
|
|
if err != nil {
|
|
slog.Error("cant convert statuscode", "error", err)
|
|
}
|
|
nginxBase.StatusCode = int(statusCode)
|
|
break
|
|
}
|
|
}
|
|
newEntry.BaseInformation = nginxBase
|
|
|
|
return newEntry
|
|
}
|