-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathparser.go
More file actions
221 lines (190 loc) · 6.51 KB
/
parser.go
File metadata and controls
221 lines (190 loc) · 6.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
package main
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
)
// Pre-compiled regex patterns for better performance
var (
// Common Log Format: IP - - [timestamp] "METHOD path protocol" status size "referer" "user-agent"
accessLogRegex = regexp.MustCompile(`^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) ([^"]*) ([^"]*)" (\d+) (\S+) "([^"]*)" "([^"]*)"`)
// JSON object detection within log messages - simplified to avoid catastrophic backtracking
jsonRegex = regexp.MustCompile(`\{[^{}]{0,1000}\}`)
)
// AccessLogEntry represents a parsed access log entry
type AccessLogEntry struct {
IP string
Timestamp string
Method string
Path string
Protocol string
Status string
Size string
Referer string
UserAgent string
}
// logEntry represents a log entry with original and formatted versions
type logEntry struct {
Timestamp time.Time
OriginalMessage string // Store the original unformatted message
Message string // Store the formatted message
Raw string // Store the complete display line
}
// isJSON checks if a string is valid JSON with safety checks
func isJSON(s string) bool {
// Quick checks to avoid expensive JSON parsing
if len(s) == 0 {
return false
}
s = strings.TrimSpace(s)
if len(s) == 0 {
return false
}
// Avoid parsing very large JSON to prevent performance issues
if len(s) > 5000 {
return false
}
// Quick check for obvious JSON patterns
first := s[0]
if first == '{' || first == '[' || first == '"' ||
s == "null" || s == "true" || s == "false" ||
(first >= '0' && first <= '9') || first == '-' {
var js json.RawMessage
return json.Unmarshal([]byte(s), &js) == nil
}
return false
}
// formatJSON pretty-prints JSON with syntax highlighting
func formatJSON(jsonStr string, indent string) string {
var obj interface{}
if err := json.Unmarshal([]byte(jsonStr), &obj); err != nil {
return jsonStr // Return original if not valid JSON
}
formatted, err := json.MarshalIndent(obj, "", indent)
if err != nil {
return jsonStr // Return original if formatting fails
}
return string(formatted)
}
// parseAccessLog attempts to parse common access log formats (Apache/Nginx)
func parseAccessLog(logLine string) *AccessLogEntry {
matches := accessLogRegex.FindStringSubmatch(logLine)
if len(matches) >= 10 {
return &AccessLogEntry{
IP: matches[1],
Timestamp: matches[2],
Method: matches[3],
Path: matches[4],
Protocol: matches[5],
Status: matches[6],
Size: matches[7],
Referer: matches[8],
UserAgent: matches[9],
}
}
return nil
}
// formatAccessLog formats an access log entry with colors and structure
func formatAccessLog(entry *AccessLogEntry, config *UIConfig) string {
if !config.ColorizeFields {
return fmt.Sprintf("%s %s %s %s %s",
entry.IP, entry.Method, entry.Path, entry.Status, entry.Size)
}
// Color coding based on HTTP status
var statusStyle lipgloss.Style
switch {
case strings.HasPrefix(entry.Status, "2"):
statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true) // Green for 2xx
case strings.HasPrefix(entry.Status, "3"):
statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("11")).Bold(true) // Yellow for 3xx
case strings.HasPrefix(entry.Status, "4"):
statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9")).Bold(true) // Red for 4xx
case strings.HasPrefix(entry.Status, "5"):
statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Bold(true) // Dark red for 5xx
default:
statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("15")) // White for others
}
// Method colors with better distinction
var methodStyle lipgloss.Style
switch entry.Method {
case "GET":
methodStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("12")).Bold(true) // Blue
case "POST":
methodStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("13")).Bold(true) // Magenta
case "PUT":
methodStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("14")).Bold(true) // Cyan
case "DELETE":
methodStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9")).Bold(true) // Red
default:
methodStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("15")) // White
}
// Better styling for different elements
ipStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("6")) // Cyan for IP
pathStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("15")) // White for path
sizeStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // Gray for size
// Simple, clean single-line format
return fmt.Sprintf("%s %s %s %s %s",
ipStyle.Render(entry.IP),
methodStyle.Render(entry.Method),
pathStyle.Render(entry.Path),
statusStyle.Render(entry.Status),
sizeStyle.Render(entry.Size))
}
// formatLogMessage formats a log message, applying appropriate formatting
func formatLogMessage(message string, config *UIConfig) string {
// Fast path: if no formatting is enabled, return as-is
if !config.ParseAccessLogs && !config.PrettyPrintJSON {
return strings.TrimSpace(message)
}
message = strings.TrimSpace(message)
// Skip formatting for very long messages to prevent performance issues
const maxMessageLength = 10000
if len(message) > maxMessageLength {
return message
}
// Try access log parsing first
if config.ParseAccessLogs {
if accessEntry := parseAccessLog(message); accessEntry != nil {
return formatAccessLog(accessEntry, config)
}
}
// Fall back to JSON formatting
if config.PrettyPrintJSON {
// Check if the entire message is JSON (but only for reasonable sizes)
if len(message) < 5000 && isJSON(message) {
return formatJSON(message, config.JSONIndent)
}
// Look for JSON objects within the message (with safety limits)
if len(message) < 5000 {
// Use a defer/recover to catch any potential panics from regex
func() {
defer func() {
if recover() != nil {
// If regex panics, just return the original message
return
}
}()
message = jsonRegex.ReplaceAllStringFunc(message, func(match string) string {
if len(match) < 2000 && isJSON(match) {
return formatJSON(match, config.JSONIndent)
}
return match
})
}()
}
}
return message
}
// makeLogEntry creates log entries consistently
func makeLogEntry(ts time.Time, originalMsg string, cfg *UIConfig) logEntry {
formatted := formatLogMessage(originalMsg, cfg)
return logEntry{
Timestamp: ts,
OriginalMessage: originalMsg,
Message: formatted,
Raw: fmt.Sprintf("[%s] %s", ts.Format("15:04:05"), formatted),
}
}