feat: Implement initial Go backend and React frontend project structure for the gotail application.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LogEntry representa uma linha de log com metadados extraídos
|
||||
type LogEntry struct {
|
||||
Filename string `json:"filename"`
|
||||
Line string `json:"line"`
|
||||
Timestamp time.Time `json:"timestamp,omitempty"`
|
||||
Level string `json:"level,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
// Regex para extrair timestamps em diferentes formatos
|
||||
timestampRegex1 = regexp.MustCompile(`\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}`)
|
||||
timestampRegex2 = regexp.MustCompile(`\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}`)
|
||||
|
||||
// Regex para extrair nível de log
|
||||
logLevelRegex = regexp.MustCompile(`(?i)\b(DEBUG|INFO|WARN|WARNING|ERROR|FATAL|TRACE)\b`)
|
||||
)
|
||||
|
||||
// NewLogEntry cria uma nova entrada de log com parsing automático de timestamp e nível
|
||||
func NewLogEntry(filename, line string) LogEntry {
|
||||
entry := LogEntry{
|
||||
Filename: filename,
|
||||
Line: strings.TrimRight(line, "\r\n"),
|
||||
}
|
||||
|
||||
// Extrair timestamp
|
||||
if ts := timestampRegex1.FindString(line); ts != "" {
|
||||
if parsed, err := time.Parse("2006-01-02T15:04:05", ts); err == nil {
|
||||
entry.Timestamp = parsed
|
||||
}
|
||||
} else if ts := timestampRegex2.FindString(line); ts != "" {
|
||||
if parsed, err := time.Parse("2006-01-02 15:04:05", ts); err == nil {
|
||||
entry.Timestamp = parsed
|
||||
}
|
||||
}
|
||||
|
||||
// Extrair nível de log
|
||||
if level := logLevelRegex.FindString(line); level != "" {
|
||||
entry.Level = strings.ToUpper(level)
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
// TimestampString retorna o timestamp como string no formato original
|
||||
// Retorna string vazia se não houver timestamp
|
||||
func (e LogEntry) TimestampString() string {
|
||||
if e.Timestamp.IsZero() {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Tentar detectar o formato original da linha
|
||||
if strings.Contains(e.Line, "T") {
|
||||
return e.Timestamp.Format("2006-01-02T15:04:05")
|
||||
}
|
||||
return e.Timestamp.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewLogEntry(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
filename string
|
||||
line string
|
||||
wantLevel string
|
||||
wantTimestamp bool
|
||||
}{
|
||||
{
|
||||
name: "Log with ISO8601 timestamp and INFO level",
|
||||
filename: "app.log",
|
||||
line: "2025-11-19T10:00:00 INFO Application started",
|
||||
wantLevel: "INFO",
|
||||
wantTimestamp: true,
|
||||
},
|
||||
{
|
||||
name: "Log with space-separated timestamp and ERROR level",
|
||||
filename: "app.log",
|
||||
line: "2025-11-19 10:00:00 ERROR Connection failed",
|
||||
wantLevel: "ERROR",
|
||||
wantTimestamp: true,
|
||||
},
|
||||
{
|
||||
name: "Log with DEBUG level",
|
||||
filename: "debug.log",
|
||||
line: "DEBUG: Entering function processRequest",
|
||||
wantLevel: "DEBUG",
|
||||
wantTimestamp: false,
|
||||
},
|
||||
{
|
||||
name: "Log with WARN level",
|
||||
filename: "app.log",
|
||||
line: "WARN: Memory usage high",
|
||||
wantLevel: "WARN",
|
||||
wantTimestamp: false,
|
||||
},
|
||||
{
|
||||
name: "Log without level or timestamp",
|
||||
filename: "simple.log",
|
||||
line: "Simple log message",
|
||||
wantLevel: "",
|
||||
wantTimestamp: false,
|
||||
},
|
||||
{
|
||||
name: "Log with FATAL level",
|
||||
filename: "app.log",
|
||||
line: "FATAL: System crash",
|
||||
wantLevel: "FATAL",
|
||||
wantTimestamp: false,
|
||||
},
|
||||
{
|
||||
name: "Log with carriage return",
|
||||
filename: "windows.log",
|
||||
line: "Log line with CR\r\n",
|
||||
wantLevel: "",
|
||||
wantTimestamp: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
entry := NewLogEntry(tt.filename, tt.line)
|
||||
|
||||
if entry.Filename != tt.filename {
|
||||
t.Errorf("Filename = %v, want %v", entry.Filename, tt.filename)
|
||||
}
|
||||
|
||||
if entry.Level != tt.wantLevel {
|
||||
t.Errorf("Level = %v, want %v", entry.Level, tt.wantLevel)
|
||||
}
|
||||
|
||||
if tt.wantTimestamp && entry.Timestamp.IsZero() {
|
||||
t.Error("Expected timestamp to be parsed, but got zero value")
|
||||
}
|
||||
|
||||
if !tt.wantTimestamp && !entry.Timestamp.IsZero() {
|
||||
t.Error("Expected no timestamp, but got one")
|
||||
}
|
||||
|
||||
// Line should have CR/LF trimmed
|
||||
if entry.Line != tt.line && entry.Line != tt.line[:len(tt.line)-2] {
|
||||
t.Errorf("Line not properly trimmed: %q", entry.Line)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogEntry_TimestampString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
timestamp time.Time
|
||||
line string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "ISO8601 format",
|
||||
timestamp: time.Date(2025, 11, 19, 10, 0, 0, 0, time.UTC),
|
||||
line: "2025-11-19T10:00:00 INFO Test",
|
||||
want: "2025-11-19T10:00:00",
|
||||
},
|
||||
{
|
||||
name: "Space-separated format",
|
||||
timestamp: time.Date(2025, 11, 19, 10, 0, 0, 0, time.UTC),
|
||||
line: "2025-11-19 10:00:00 INFO Test",
|
||||
want: "2025-11-19 10:00:00",
|
||||
},
|
||||
{
|
||||
name: "Zero timestamp",
|
||||
timestamp: time.Time{},
|
||||
line: "No timestamp",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
entry := LogEntry{
|
||||
Filename: "test.log",
|
||||
Line: tt.line,
|
||||
Timestamp: tt.timestamp,
|
||||
}
|
||||
|
||||
got := entry.TimestampString()
|
||||
if got != tt.want {
|
||||
t.Errorf("TimestampString() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNewLogEntry(b *testing.B) {
|
||||
line := "2025-11-19T10:00:00 INFO Application started successfully"
|
||||
filename := "app.log"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = NewLogEntry(filename, line)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user