node build fixed
This commit is contained in:
381
seanime-2.9.10/internal/troubleshooter/logs.go
Normal file
381
seanime-2.9.10/internal/troubleshooter/logs.go
Normal file
@@ -0,0 +1,381 @@
|
||||
package troubleshooter
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"seanime/internal/util"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AnalysisResult struct {
|
||||
Items []AnalysisResultItem `json:"items"`
|
||||
}
|
||||
|
||||
type AnalysisResultItem struct {
|
||||
Observation string `json:"observation"`
|
||||
Recommendation string `json:"recommendation"`
|
||||
Severity string `json:"severity"`
|
||||
Errors []string `json:"errors"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Logs []string `json:"logs"`
|
||||
}
|
||||
|
||||
// RuleBuilder provides a fluent interface for building rules
|
||||
type RuleBuilder struct {
|
||||
name string
|
||||
description string
|
||||
conditions []condition
|
||||
platforms []string
|
||||
branches []branch
|
||||
defaultBranch *branch
|
||||
state *AppState
|
||||
}
|
||||
|
||||
type condition struct {
|
||||
check func(LogLine) bool
|
||||
message string // For debugging/logging
|
||||
}
|
||||
|
||||
type branch struct {
|
||||
conditions []condition
|
||||
observation string
|
||||
recommendation string
|
||||
severity string
|
||||
}
|
||||
|
||||
// NewRule starts building a new rule
|
||||
func NewRule(name string) *RuleBuilder {
|
||||
return &RuleBuilder{
|
||||
name: name,
|
||||
conditions: []condition{},
|
||||
branches: []branch{},
|
||||
defaultBranch: &branch{
|
||||
severity: "info",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Desc adds a description to the rule
|
||||
func (r *RuleBuilder) Desc(desc string) *RuleBuilder {
|
||||
r.description = desc
|
||||
return r
|
||||
}
|
||||
|
||||
// When adds a base condition that must be met
|
||||
func (r *RuleBuilder) When(check func(LogLine) bool, message string) *RuleBuilder {
|
||||
r.conditions = append(r.conditions, condition{check: check, message: message})
|
||||
return r
|
||||
}
|
||||
|
||||
// ModuleIs adds a module condition
|
||||
func (r *RuleBuilder) ModuleIs(module Module) *RuleBuilder {
|
||||
return r.When(func(l LogLine) bool {
|
||||
return l.Module == string(module)
|
||||
}, "module is "+string(module))
|
||||
}
|
||||
|
||||
// LevelIs adds a level condition
|
||||
func (r *RuleBuilder) LevelIs(level Level) *RuleBuilder {
|
||||
return r.When(func(l LogLine) bool {
|
||||
return l.Level == string(level)
|
||||
}, "level is "+string(level))
|
||||
}
|
||||
|
||||
// MessageContains adds a message contains condition
|
||||
func (r *RuleBuilder) MessageContains(substr string) *RuleBuilder {
|
||||
return r.When(func(l LogLine) bool {
|
||||
return strings.Contains(l.Message, substr)
|
||||
}, "message contains "+substr)
|
||||
}
|
||||
|
||||
// MessageMatches adds a message regex condition
|
||||
func (r *RuleBuilder) MessageMatches(pattern string) *RuleBuilder {
|
||||
return r.When(func(l LogLine) bool {
|
||||
matched, err := util.MatchesRegex(l.Message, pattern)
|
||||
return err == nil && matched
|
||||
}, "message matches "+pattern)
|
||||
}
|
||||
|
||||
// OnPlatforms restricts the rule to specific platforms
|
||||
func (r *RuleBuilder) OnPlatforms(platforms ...string) *RuleBuilder {
|
||||
r.platforms = platforms
|
||||
return r
|
||||
}
|
||||
|
||||
// Branch adds a new branch with additional conditions
|
||||
func (r *RuleBuilder) Branch() *BranchBuilder {
|
||||
return &BranchBuilder{
|
||||
rule: r,
|
||||
branch: branch{
|
||||
conditions: []condition{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Then sets the default observation and recommendation
|
||||
func (r *RuleBuilder) Then(observation, recommendation string) *RuleBuilder {
|
||||
r.defaultBranch.observation = observation
|
||||
r.defaultBranch.recommendation = recommendation
|
||||
return r
|
||||
}
|
||||
|
||||
// WithSeverity sets the default severity
|
||||
func (r *RuleBuilder) WithSeverity(severity string) *RuleBuilder {
|
||||
r.defaultBranch.severity = severity
|
||||
return r
|
||||
}
|
||||
|
||||
// BranchBuilder helps build conditional branches
|
||||
type BranchBuilder struct {
|
||||
rule *RuleBuilder
|
||||
branch branch
|
||||
}
|
||||
|
||||
// When adds a condition to the branch
|
||||
func (b *BranchBuilder) When(check func(LogLine) bool, message string) *BranchBuilder {
|
||||
b.branch.conditions = append(b.branch.conditions, condition{check: check, message: message})
|
||||
return b
|
||||
}
|
||||
|
||||
// Then sets the branch observation and recommendation
|
||||
func (b *BranchBuilder) Then(observation, recommendation string) *RuleBuilder {
|
||||
b.branch.observation = observation
|
||||
b.branch.recommendation = recommendation
|
||||
b.rule.branches = append(b.rule.branches, b.branch)
|
||||
return b.rule
|
||||
}
|
||||
|
||||
// WithSeverity sets the branch severity
|
||||
func (b *BranchBuilder) WithSeverity(severity string) *BranchBuilder {
|
||||
b.branch.severity = severity
|
||||
return b
|
||||
}
|
||||
|
||||
// matches checks if a log line matches the rule and returns the matching branch
|
||||
func (r *RuleBuilder) matches(line LogLine, platform string) (bool, *branch) {
|
||||
// Check platform restrictions
|
||||
if len(r.platforms) > 0 && !util.Contains(r.platforms, platform) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Check base conditions
|
||||
for _, cond := range r.conditions {
|
||||
if !cond.check(line) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check branches in order
|
||||
for _, branch := range r.branches {
|
||||
matches := true
|
||||
for _, cond := range branch.conditions {
|
||||
if !cond.check(line) {
|
||||
matches = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if matches {
|
||||
return true, &branch
|
||||
}
|
||||
}
|
||||
|
||||
// If no branches match but base conditions do, use default branch
|
||||
return true, r.defaultBranch
|
||||
}
|
||||
|
||||
// NewAnalyzer creates a new analyzer with the default rule groups
|
||||
func NewAnalyzer(opts NewTroubleshooterOptions) *Troubleshooter {
|
||||
a := &Troubleshooter{
|
||||
logsDir: opts.LogsDir,
|
||||
logger: opts.Logger,
|
||||
state: opts.State,
|
||||
rules: defaultRules(),
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// defaultRules returns the default set of rules
|
||||
func defaultRules() []RuleBuilder {
|
||||
return []RuleBuilder{
|
||||
*mpvRules(),
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze analyzes the logs in the logs directory and returns an AnalysisResult
|
||||
// App.OnFlushLogs should be called before this function
|
||||
func (t *Troubleshooter) Analyze() (AnalysisResult, error) {
|
||||
|
||||
files, err := os.ReadDir(t.logsDir)
|
||||
if err != nil {
|
||||
return AnalysisResult{}, err
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return AnalysisResult{}, errors.New("no logs found")
|
||||
}
|
||||
|
||||
// Get the latest server log file
|
||||
// name: seanime-<timestamp>.log
|
||||
// e.g., seanime-2025-01-21-12-00-00.log
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return files[i].Name() > files[j].Name()
|
||||
})
|
||||
|
||||
latestFile := files[0]
|
||||
|
||||
return analyzeLogFile(filepath.Join(t.logsDir, latestFile.Name()))
|
||||
}
|
||||
|
||||
// LogLine represents a parsed log line
|
||||
type LogLine struct {
|
||||
Timestamp time.Time
|
||||
Line string
|
||||
Module string
|
||||
Level string
|
||||
Message string
|
||||
}
|
||||
|
||||
// analyzeLogFile analyzes a log file and returns an AnalysisResult
|
||||
func analyzeLogFile(filepath string) (res AnalysisResult, err error) {
|
||||
platform := runtime.GOOS
|
||||
|
||||
// Read the log file
|
||||
content, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
|
||||
// Get lines no older than 1 hour
|
||||
_lines := []string{}
|
||||
for _, line := range lines {
|
||||
timestamp, ok := util.SliceStrTo(line, len(time.DateTime))
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
timestampTime, err := time.Parse(time.DateTime, timestamp)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if time.Since(timestampTime) < 1*time.Hour {
|
||||
_lines = append(_lines, line)
|
||||
}
|
||||
}
|
||||
lines = _lines
|
||||
|
||||
// Parse lines into LogLine
|
||||
logLines := []LogLine{}
|
||||
for _, line := range lines {
|
||||
logLine, err := parseLogLine(line)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
logLines = append(logLines, logLine)
|
||||
}
|
||||
|
||||
// Group log lines by rule group
|
||||
type matchGroup struct {
|
||||
ruleGroup *RuleBuilder
|
||||
branch *branch
|
||||
logLines []LogLine
|
||||
}
|
||||
matches := make(map[string]*matchGroup) // key is rule group name
|
||||
|
||||
// For each log line, check against all rules
|
||||
for _, logLine := range logLines {
|
||||
for _, ruleGroup := range defaultRules() {
|
||||
if matched, branch := ruleGroup.matches(logLine, platform); matched {
|
||||
if _, ok := matches[ruleGroup.name]; !ok {
|
||||
matches[ruleGroup.name] = &matchGroup{
|
||||
ruleGroup: &ruleGroup,
|
||||
branch: branch,
|
||||
logLines: []LogLine{},
|
||||
}
|
||||
}
|
||||
matches[ruleGroup.name].logLines = append(matches[ruleGroup.name].logLines, logLine)
|
||||
break // Stop checking other rules in this group once we find a match
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert matches to analysis result items
|
||||
for _, group := range matches {
|
||||
item := AnalysisResultItem{
|
||||
Observation: group.branch.observation,
|
||||
Recommendation: group.branch.recommendation,
|
||||
Severity: group.branch.severity,
|
||||
}
|
||||
|
||||
// Add log lines based on their level
|
||||
for _, logLine := range group.logLines {
|
||||
switch logLine.Level {
|
||||
case "error":
|
||||
item.Errors = append(item.Errors, logLine.Line)
|
||||
case "warning":
|
||||
item.Warnings = append(item.Warnings, logLine.Line)
|
||||
default:
|
||||
item.Logs = append(item.Logs, logLine.Line)
|
||||
}
|
||||
}
|
||||
|
||||
res.Items = append(res.Items, item)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func parseLogLine(line string) (ret LogLine, err error) {
|
||||
|
||||
ret.Line = line
|
||||
|
||||
timestamp, ok := util.SliceStrTo(line, len(time.DateTime))
|
||||
if !ok {
|
||||
return LogLine{}, errors.New("failed to parse timestamp")
|
||||
}
|
||||
timestampTime, err := time.Parse(time.DateTime, timestamp)
|
||||
if err != nil {
|
||||
return LogLine{}, errors.New("failed to parse timestamp")
|
||||
}
|
||||
ret.Timestamp = timestampTime
|
||||
|
||||
rest, ok := util.SliceStrFrom(line, len(timestamp))
|
||||
if !ok {
|
||||
return LogLine{}, errors.New("failed to parse rest")
|
||||
}
|
||||
rest = strings.TrimSpace(rest)
|
||||
if strings.HasPrefix(rest, "|ERR|") {
|
||||
ret.Level = "error"
|
||||
} else if strings.HasPrefix(rest, "|WRN|") {
|
||||
ret.Level = "warning"
|
||||
} else if strings.HasPrefix(rest, "|INF|") {
|
||||
ret.Level = "info"
|
||||
} else if strings.HasPrefix(rest, "|DBG|") {
|
||||
ret.Level = "debug"
|
||||
} else if strings.HasPrefix(rest, "|TRC|") {
|
||||
ret.Level = "trace"
|
||||
} else if strings.HasPrefix(rest, "|PNC|") {
|
||||
ret.Level = "panic"
|
||||
}
|
||||
|
||||
// Remove the level prefix
|
||||
rest, ok = util.SliceStrFrom(rest, 6)
|
||||
if !ok {
|
||||
return LogLine{}, errors.New("failed to parse rest")
|
||||
}
|
||||
|
||||
// Get the module (string before `>`)
|
||||
moduleCaretIndex := strings.Index(rest, ">")
|
||||
if moduleCaretIndex != -1 {
|
||||
ret.Module = strings.TrimSpace(rest[:moduleCaretIndex])
|
||||
rest = strings.TrimSpace(rest[moduleCaretIndex+1:])
|
||||
}
|
||||
|
||||
ret.Message = rest
|
||||
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user