Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions code/backend/internal/ml/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func GenerateDummyParser(rawLog string, features fingerprint.FingerprintFeatures
pattern5 := `^(?P<timestamp>\S+ \S+) (?P<severity>[A-Za-z]+) (?P<service>\S+) (?P<host>\S+) (?P<message>.*)$`
if re, err := regexp.Compile(pattern5); err == nil && re.MatchString(trimmed) {
return &parser.Parser{
Name: "Generic Structured Log Parser",
Pattern: pattern5,
Mapping: map[string]string{
"timestamp": "timestamp",
Expand All @@ -127,6 +128,7 @@ func GenerateDummyParser(rawLog string, features fingerprint.FingerprintFeatures
pattern4 := `^(?P<timestamp>\S+) \[(?P<severity>[A-Za-z]+)\] \[(?P<service>\S+)\] (?P<message>.*)$`
if re, err := regexp.Compile(pattern4); err == nil && re.MatchString(trimmed) {
return &parser.Parser{
Name: "Bracketed Service Log Parser",
Pattern: pattern4,
Mapping: map[string]string{
"timestamp": "timestamp",
Expand All @@ -150,6 +152,7 @@ func GenerateDummyParser(rawLog string, features fingerprint.FingerprintFeatures
pattern3 := `^(?P<timestamp>\S+ \S+) (?P<severity>[A-Za-z]+) (?P<message>.*)$`
if re, err := regexp.Compile(pattern3); err == nil && re.MatchString(trimmed) {
return &parser.Parser{
Name: "Timestamp Severity Log Parser",
Pattern: pattern3,
Mapping: map[string]string{
"timestamp": "timestamp",
Expand Down
14 changes: 14 additions & 0 deletions code/backend/internal/parser/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,20 @@ func applyTransformation(value string, t Transformation) (string, error) {
case "trim", "trim_space":
return strings.TrimSpace(value), nil

case "integer", "int":
number, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
if err != nil {
return "", fmt.Errorf("invalid integer value %q: %w", value, err)
}
return strconv.FormatInt(number, 10), nil

case "float", "decimal", "number":
number, err := strconv.ParseFloat(strings.TrimSpace(value), 64)
if err != nil {
return "", fmt.Errorf("invalid float value %q: %w", value, err)
}
return strconv.FormatFloat(number, 'f', -1, 64), nil

case "datetime", "date", "timestamp", "time":
return parseAndFormatDateTime(value, t.Format)

Expand Down
14 changes: 14 additions & 0 deletions code/backend/internal/parser/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,20 @@ func TestParseLog_Transformations(t *testing.T) {
expected: "padded message",
expectError: false,
},
{
name: "integer",
input: "001204",
trans: Transformation{Type: "integer"},
expected: "1204",
expectError: false,
},
{
name: "float",
input: "249.990",
trans: Transformation{Type: "float"},
expected: "249.99",
expectError: false,
},
{
name: "datetime with Go layout",
input: "2026-09-05 18:30:21",
Expand Down
1 change: 1 addition & 0 deletions code/backend/internal/parser/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type NormalizedLog struct {
}

type Parser struct {
Name string `json:"name,omitempty" bson:"name,omitempty"`
Pattern string `json:"pattern" bson:"pattern"`
Mapping map[string]string `json:"mapping" bson:"mapping"`
Transformations map[string]Transformation `json:"transformations,omitempty" bson:"transformations,omitempty"`
Expand Down
1 change: 1 addition & 0 deletions code/backend/internal/parsergen/parser_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ func (pg *ParserGenerator) ProcessMessage(ctx context.Context, body []byte) erro
}

doc := database.ParserDoc{
Name: newParser.Name,
Hash: msg.Hash,
Pattern: newParser.Pattern,
Mapping: newParser.Mapping,
Expand Down
24 changes: 16 additions & 8 deletions code/frontend/gologgo-client/components/log-dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,30 @@ export function LogDashboard() {

const handleClearLogs = () => setRawLogs('')
const handleProcessLogs = async () => {
if (!rawLogs.trim()) return;
const logLines = rawLogs
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)

if (logLines.length === 0) return
setIsProcessing(true);
try {
const res = await fetch('/api/logs', {
const responses = await Promise.all(logLines.map((line) => fetch('/api/logs', {
method: 'POST',
headers: {
'Content-Type': 'text/plain'
},
body: rawLogs
});
if (res.ok) {
body: line
})))

const failedResponse = responses.find((response) => !response.ok)
if (!failedResponse) {
setRawLogs('');
toast.success('Logs successfully sent for processing!');
toast.success(`${logLines.length} log${logLines.length === 1 ? '' : 's'} sent for processing!`);
} else {
const errorData = await res.json().catch(() => ({}));
toast.error(`Failed to process logs: ${errorData.error || 'Unknown error'}`);
const errorData = await failedResponse.json().catch(() => ({}))
const successfulCount = responses.length - responses.filter((response) => !response.ok).length
toast.error(`Processed ${successfulCount}/${logLines.length} logs. ${errorData.error || 'Some logs failed.'}`)
}
} catch (err) {
console.error(err);
Expand Down
37 changes: 33 additions & 4 deletions code/frontend/gologgo-client/components/logs-explorer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export function LogsExplorer() {
// Filter / pagination state
const [search, setSearch] = useState('')
const [statusFilter, setStatusFilter] = useState('all')
const [parserFilter, setParserFilter] = useState('all')
const [parserOptions, setParserOptions] = useState([{ value: 'all', label: 'All Parsers' }])
const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(10)

Expand Down Expand Up @@ -138,6 +140,24 @@ export function LogsExplorer() {
// Initial load
useEffect(() => { fetchLogs() }, []) // eslint-disable-line react-hooks/exhaustive-deps

useEffect(() => {
fetch('/api/parsers', { cache: 'no-store' })
.then((response) => response.ok ? response.json() : null)
.then((data) => {
const parsers = data?.parsers ?? data ?? []
setParserOptions([
{ value: 'all', label: 'All Parsers' },
...parsers
.map((parser) => ({
value: parser.hash || parser.name,
label: parser.name || parser.hash,
}))
.filter((parser) => parser.value),
])
})
.catch(() => setParserOptions([{ value: 'all', label: 'All Parsers' }]))
}, [])

// Re-fetch on filter/page change (skip on mount, handled above)
useEffect(() => {
fetchLogs({ page: currentPage, limit: pageSize, status: statusFilter })
Expand All @@ -150,21 +170,24 @@ export function LogsExplorer() {
const clearFilters = () => {
setSearch('')
setStatusFilter('all')
setParserFilter('all')
setCurrentPage(1)
}

// Client-side search on the current page results (search within payload text)
const filtered = useMemo(() => {
if (!search.trim()) return requests
if (!search.trim() && parserFilter === 'all') return requests
const q = search.toLowerCase()
return requests.filter((r) => {
const payloadMatch = r.payload?.toLowerCase().includes(q)
const idMatch = r.id?.toLowerCase().includes(q)
const statusMatch = r.status?.toLowerCase().includes(q)
const logsMatch = r.logs?.some?.((l) => JSON.stringify(l.normalizedLog).toLowerCase().includes(q))
return payloadMatch || idMatch || statusMatch || logsMatch
const parserMatch = parserFilter === 'all' || r.logs?.some?.((l) => l.hash === parserFilter)
const textMatch = !search.trim() || payloadMatch || idMatch || statusMatch || logsMatch
return textMatch && parserMatch
})
}, [requests, search])
}, [requests, search, parserFilter])

// ── Format helpers ─────────────────────────────────────────────────────────

Expand Down Expand Up @@ -299,6 +322,12 @@ export function LogsExplorer() {
onChange={(v) => { setStatusFilter(v); setCurrentPage(1) }}
options={statusOptions}
/>
<FilterSelect
label="Parser"
value={parserFilter}
onChange={(v) => { setParserFilter(v); setCurrentPage(1) }}
options={parserOptions}
/>
<button
onClick={clearFilters}
className="px-2 text-xs text-muted-foreground underline-offset-4 hover:text-foreground hover:underline"
Expand Down Expand Up @@ -336,7 +365,7 @@ export function LogsExplorer() {
) : filtered.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-2 py-16 text-center text-sm text-muted-foreground">
<p>No logs found.</p>
{(search || statusFilter !== 'all') && (
{(search || statusFilter !== 'all' || parserFilter !== 'all') && (
<button onClick={clearFilters} className="text-xs underline">Clear filters</button>
)}
</div>
Expand Down
8 changes: 5 additions & 3 deletions code/ml/app/models/rule_synthesizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"transformations": {
"timestamp": { "type": "datetime", "format": "2006-01-02 15:04:05" },
"severity": { "type": "uppercase" }
}
},
"name": "Nginx Access Log Parser"
}

CRITICAL RULES:
Expand All @@ -35,8 +36,9 @@
- If a field is a timestamp/date/time, set `type` to "datetime" and provide a Go layout format string in `format` (e.g. "2006-01-02 15:04:05", "Jan _2 15:04:05", RFC3339).
- If a field is severity or level, set `type` to "uppercase".
- If a field is numeric, set `type` to "integer" or "float".
5. Do NOT hardcode dynamic values inside the regex; capture them generically.
6. Return ONLY raw JSON. No markdown backticks (no ```json).
5. "name" MUST be concise, human-readable, and describe the log source or format. Do not use generic names like "Parser" or hash values.
6. Do NOT hardcode dynamic values inside the regex; capture them generically.
7. Return ONLY raw JSON. No markdown backticks (no ```json).
"""

class RuleSynthesizer:
Expand Down
3 changes: 3 additions & 0 deletions code/ml/app/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class Transformation(BaseModel):
class RuleGeneratorOutput(BaseModel):
model_config = ConfigDict(extra="forbid")

name: str = Field(
description="Concise human-readable name for the parser, describing the log source or format"
)
pattern: str = Field(
description="Named capture group regex matching the whole log string, e.g. ^(?<timestamp>\\S+ \\S+) (?<severity>\\w+) ...$"
)
Expand Down
Loading