refactor: use parser package for file monitor

This commit is contained in:
Patryk Hegenberg 2025-09-25 00:18:20 +02:00
parent e468b3a0e3
commit 72b6ad88c7
2 changed files with 81 additions and 133 deletions

View file

@ -5,49 +5,44 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"regexp" "regexp"
"strconv"
"strings" "strings"
"tixel_watch/helpers"
"tixel_watch/models" "tixel_watch/models"
"tixel_watch/parser"
"github.com/hpcloud/tail" "github.com/hpcloud/tail"
) )
type FileMonitor struct { type FileMonitor struct {
config ToolConfig config ToolConfig
parser LogParser parser parser.Parser
}
type LogParser interface {
Parse(line string, toolName string) models.LogMessage
} }
func NewFileMonitor(config ToolConfig) *FileMonitor { func NewFileMonitor(config ToolConfig) *FileMonitor {
var parser LogParser var logParser parser.Parser
if config.Format.Pattern != "" { if config.Format.Pattern != "" {
pattern, err := regexp.Compile(config.Format.Pattern) pattern, err := regexp.Compile(config.Format.Pattern)
if err != nil { if err != nil {
slog.Error("invalid regex pattern", "tool", config.Name, "error", err) slog.Error("invalid regex pattern", "tool", config.Name, "error", err)
parser = &DefaultLogParser{} logParser = &parser.DefaultParser{}
} else { } else {
parser = &RegexLogParser{ logParser = &parser.RegexLogParser{
pattern: pattern, Pattern: pattern,
fields: config.Format.Fields, Fields: config.Format.Fields,
Toolname: config.Name,
} }
} }
} else { } else {
switch config.Name { var err error
case "nginx-tjm": logParser, err = parser.New(config.Name, "custom")
parser = &NginxTJMLogParser{} if err != nil {
default: slog.Error("cannot get tool specific parser", "error", err)
parser = &DefaultLogParser{}
} }
} }
return &FileMonitor{ return &FileMonitor{
config: config, config: config,
parser: parser, parser: logParser,
} }
} }
@ -85,7 +80,10 @@ func (fm *FileMonitor) Start(ctx context.Context, out chan<- models.LogMessage)
continue continue
} }
entry := fm.parser.Parse(line.Text, fm.config.Name) entry, err := fm.parser.Parse(line.Text)
if err != nil {
slog.Error("error parsing log line", "error", err)
}
select { select {
case out <- entry: case out <- entry:
@ -97,117 +95,3 @@ func (fm *FileMonitor) Start(ctx context.Context, out chan<- models.LogMessage)
} }
} }
} }
type DefaultLogParser struct{}
func (p *DefaultLogParser) Parse(line string, toolName string) models.LogMessage {
entry := models.NewLogMessage("log_entry", hostname)
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) models.LogMessage {
entry := models.NewLogMessage("log_entry", hostname)
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) models.LogMessage {
entry := models.NewLogMessage("log_entry", hostname)
entry.Tool = toolName
entry.Raw = line
entry = p.parseNginxTJM(entry)
return entry
}
func (p *NginxTJMLogParser) parseNginxTJM(entry models.LogMessage) models.LogMessage {
newEntry := entry
var nginxBase models.NGinXBaseInfo
parts := strings.Fields(entry.Raw)
if len(parts) < 10 {
return newEntry
}
if len(parts) > 0 {
timestampStr := strings.Trim(parts[0], "[]")
timestamp, err := helpers.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.ServiceInformation = nginxBase
return newEntry
}

64
parser/regex_parser.go Normal file
View file

@ -0,0 +1,64 @@
package parser
import (
"log/slog"
"regexp"
"strings"
"tixel_watch/helpers"
"tixel_watch/models"
)
type RegexLogParser struct {
Pattern *regexp.Regexp
Fields map[string]string
Toolname string
}
func (p *RegexLogParser) Parse(line string) (models.LogMessage, error) {
entry := models.LogMessage{Type: "log_entry"}
entry.Tool = p.Toolname
entry.Raw = line
hostname, err := helpers.GetHostname()
if err != nil {
slog.Warn("cannot get hostname")
return entry, err
}
entry.Host = hostname
fields := p.parseWithPattern(line)
if fields != nil {
entry.Fields = fields
} else {
entry.LogMessage = strings.TrimSpace(line)
}
return entry, nil
}
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
}