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
1 change: 1 addition & 0 deletions code/backend/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func main() {
app.POST("/user", apiCfg.HandlerUser)
app.POST("/log", auth.MiddlewareAuth(apiCfg.LogHandler))
app.GET("/logs", auth.MiddlewareAuth(apiCfg.GetUserLogsHandler))
app.GET("/logs/:id", auth.MiddlewareAuth(apiCfg.GetUserLogHandler))
app.GET("/parsers", apiCfg.ListParsersHandler)
app.GET("/parsers/:id", apiCfg.GetParserHandler)
app.PATCH("/parsers/:id", apiCfg.UpdateParserHandler)
Expand Down
33 changes: 33 additions & 0 deletions code/backend/internal/api/get_user_logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/EleventhHour-Projects/GoLogGo/code/backend/internal/auth"
"github.com/nottechdm/notnet/pkg/notnet"
"go.mongodb.org/mongo-driver/v2/bson"
)

// GetUserLogsHandler returns paginated requests + stats for the authenticated user.
Expand Down Expand Up @@ -82,3 +83,35 @@ func (api *Config) GetUserLogsHandler(req *notnet.Request, res *notnet.Response,
"totalPages": (total + limit - 1) / limit,
})
}

// GetUserLogHandler returns the original request and its parsed log entries.
// GET /logs/:id
func (api *Config) GetUserLogHandler(req *notnet.Request, res *notnet.Response, claims *auth.CustomClaims) error {
requestID, err := bson.ObjectIDFromHex(req.Param("id"))
if err != nil {
return res.JSON(http.StatusBadRequest, map[string]string{"error": "invalid log id"})
}

ctx := req.HTTPRequest.Context()
storedReq, err := api.Reqs.GetUserRequestByID(ctx, claims.UserID, requestID)
if err != nil {
return res.JSON(http.StatusNotFound, map[string]string{"error": "log not found"})
}

logs, err := api.Reqs.GetLogsByReqID(ctx, requestID)
if err != nil {
return res.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to fetch log details"})
}

return res.JSON(http.StatusOK, map[string]interface{}{
"request": map[string]interface{}{
"id": storedReq.ID.Hex(),
"payload": string(storedReq.Payload),
"status": storedReq.Status,
"attempts": storedReq.Attempts,
"createdAt": storedReq.CreatedAt,
"updatedAt": storedReq.UpdatedAt,
},
"logs": logs,
})
}
13 changes: 13 additions & 0 deletions code/backend/internal/database/get_user_logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ func (m *MongoDB) GetUserRequests(
return reqs, total, cursor.Err()
}

// GetUserRequestByID returns one request only when it belongs to the user.
func (m *MongoDB) GetUserRequestByID(ctx context.Context, userID, reqID bson.ObjectID) (*Req, error) {
var req Req
err := m.Collections[CollectionRequests].FindOne(ctx, bson.M{
"_id": reqID,
"userId": userID,
}).Decode(&req)
if err != nil {
return nil, err
}
return &req, nil
}

// GetLogsByReqID returns all parsed log entries for a given request ID.
func (m *MongoDB) GetLogsByReqID(ctx context.Context, reqID bson.ObjectID) ([]Log, error) {
col := m.Collections[CollectionLogs]
Expand Down
14 changes: 14 additions & 0 deletions code/backend/internal/fingerprint/fingerprint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,20 @@ func TestKVEquivalence(t *testing.T) {
}
}

func TestKVFormattingEquivalence(t *testing.T) {
log1 := `user=alice action=login source=10.0.0.1`
log2 := `user="bob", action="login", source="192.168.1.2"`

features1 := ExtractFingerprintFeatures(log1)
features2 := ExtractFingerprintFeatures(log2)
hash1 := GenerateFingerprintHash(features1)
hash2 := GenerateFingerprintHash(features2)

if hash1 != hash2 {
t.Fatalf("expected equivalent KV formatting to produce the same hash, got %s and %s (%s vs %s)", hash1, hash2, CanonicalRepresentation(features1), CanonicalRepresentation(features2))
}
}

// TestSyslogRFC5424Equivalence verifies RFC 5424 syslog logs.
func TestSyslogRFC5424Equivalence(t *testing.T) {
log1 := `<165>1 2026-08-30T13:02:11.003Z myhost.example.com myapp 1234 ID47 [exampleSDID@32473 iut="3" eventSource="Application"] User 10.0.0.1 authenticated`
Expand Down
4 changes: 0 additions & 4 deletions code/backend/internal/fingerprint/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,6 @@ func CanonicalRepresentation(feat FingerprintFeatures) string {

case FormatKeyValue:
if feat.KV != nil {
sb.WriteString("|sep=")
sb.WriteString(feat.KV.Separator)
sb.WriteString("|delim=")
sb.WriteString(feat.KV.Delimiter)
sb.WriteString("|fields=")
sb.WriteString(canonicalFieldSignatures(feat.KV.Fields))
} else {
Expand Down
3 changes: 3 additions & 0 deletions code/backend/internal/parser/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ func ParseLog(rawLog string, p *Parser) (*NormalizedLog, error) {
if match == nil {
return nil, fmt.Errorf("log does not match parser pattern")
}
if match[0] != rawLog {
return nil, fmt.Errorf("log does not fully match parser pattern")
}

fields := extractFields(re, match)

Expand Down
16 changes: 16 additions & 0 deletions code/backend/internal/parser/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ func TestParseLog_BasicExample(t *testing.T) {
}
}

func TestParseLog_RejectsTrailingInput(t *testing.T) {
p := &Parser{Pattern: `^(?P<severity>\w+) (?P<message>.*)$`, Mapping: map[string]string{
"severity": "severity",
"message": "message",
}}

if _, err := ParseLog("INFO message trailing", p); err != nil {
t.Fatalf("expected a valid full-line match, got %v", err)
}

anchoredParser := &Parser{Pattern: `^INFO$`, Mapping: map[string]string{}}
if _, err := ParseLog("INFO unexpected", anchoredParser); err == nil {
t.Fatal("expected trailing input to be rejected")
}
}

func TestParseLog_ExtractedFieldTransformationKey(t *testing.T) {
// Raw log with lowercase severity and raw_time field
rawLog := "raw_time=2026-09-05 18:30:21 raw_sev=warn msg=something happened"
Expand Down
22 changes: 22 additions & 0 deletions code/frontend/gologgo-client/app/api/logs/[id]/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'

const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8080'

export async function GET(request, { params }) {
try {
const { id } = await params
const token = (await cookies()).get('tokenString')?.value
if (!token) return NextResponse.json({ error: 'Unauthorized: No token found' }, { status: 401 })

const backendRes = await fetch(`${BACKEND_URL}/logs/${id}`, {
headers: { Authorization: `Bearer ${token}` },
cache: 'no-store',
})
const body = await backendRes.json().catch(() => ({ error: 'Invalid backend response' }))
return NextResponse.json(body, { status: backendRes.status })
} catch (error) {
console.error('Error in log detail API:', error)
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
}
}
4 changes: 4 additions & 0 deletions code/frontend/gologgo-client/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
@custom-variant dark (&:is(.dark *));

@theme inline {
--font-sans: "Avenir Next", "Segoe UI", sans-serif;
--font-mono: "SF Mono", "Cascadia Code", Menlo, monospace;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
Expand Down Expand Up @@ -159,5 +161,7 @@
}
body {
@apply bg-background text-foreground;
font-family: "Avenir Next", "Segoe UI", sans-serif;
letter-spacing: 0;
}
}
5 changes: 0 additions & 5 deletions code/frontend/gologgo-client/app/unknown-logs/page.jsx

This file was deleted.

84 changes: 49 additions & 35 deletions code/frontend/gologgo-client/components/log-dashboard.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
'use client'

import { useState } from 'react'
import { useEffect, useState } from 'react'
import { Sidebar } from '@/components/sidebar'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import {
BarChart3,
ChevronDown,
CircleHelp,
Database,
FileJson,
LayoutDashboard,
ListFilter,
Menu,
Search,
TerminalSquare,
X,
} from 'lucide-react'
import { ThemeToggle } from '@/components/theme-toggle'
import { toast } from 'sonner'
Expand All @@ -25,33 +17,55 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'


const parserData = [
{ name: 'Nginx', count: 8421 },
{ name: 'Apache', count: 5921 },
{ name: 'Auth', count: 5241 },
{ name: 'PostgreSQL', count: 3241 },
{ name: 'Docker', count: 1698 },
{ name: 'Custom', count: 780 },
]

const logs = [
{ id: 'log-2241', time: '22:41:03', level: 'ERROR', service: 'auth-service', message: 'User authentication failed', status: 'Parsed' },
{ id: 'log-2240', time: '22:40:15', level: 'INFO', service: 'payment-service', message: 'Payment completed', status: 'Parsed' },
{ id: 'log-2239', time: '22:39:42', level: 'WARN', service: 'nginx', message: 'Invalid request', status: 'Unknown' },
{ id: 'log-2238', time: '22:38:51', level: 'INFO', service: 'user-service', message: 'User registered', status: 'Parsed' },
{ id: 'log-2237', time: '22:37:22', level: 'ERROR', service: 'database', message: 'Connection timeout', status: 'Parsed' },
]



export function LogDashboard() {
const router = useRouter()
const [range, setRange] = useState('Last 24 hours')
const [menuOpen, setMenuOpen] = useState(false)
const [mobileOpen, setMobileOpen] = useState(false)
const max = parserData[0].count
const [rawLogs, setRawLogs] = useState('')
const [isProcessing, setIsProcessing] = useState(false)
const [stats, setStats] = useState(null)
const [parserData, setParserData] = useState([])
const [recentLogs, setRecentLogs] = useState([])

useEffect(() => {
const loadDashboard = async () => {
try {
const [logsResponse, parsersResponse] = await Promise.all([
fetch('/api/logs?limit=5&status=all', { cache: 'no-store' }),
fetch('/api/parsers', { cache: 'no-store' }),
])

if (logsResponse.ok) {
const logsData = await logsResponse.json()
setStats(logsData.stats)
setRecentLogs((logsData.requests ?? []).map((request) => {
const normalized = request.logs?.[0]?.normalizedLog ?? {}
return {
id: request.id,
time: request.createdAt,
level: normalized.severity || '—',
service: normalized.service || '—',
message: normalized.message || request.payload || '—',
status: request.status,
}
}))
}

if (parsersResponse.ok) {
const parsersData = await parsersResponse.json()
const parsers = parsersData?.parsers ?? parsersData ?? []
setParserData(parsers
.map((parser) => ({ name: parser.name || parser.format || 'Unnamed', count: Number(parser.logsProcessed || 0) }))
.filter((parser) => parser.count > 0)
.sort((a, b) => b.count - a.count)
.slice(0, 6))
}
} catch (error) {
console.error('Failed to load dashboard data:', error)
}
}

loadDashboard()
}, [])

const handleClearLogs = () => setRawLogs('')
const handleProcessLogs = async () => {
Expand Down Expand Up @@ -96,11 +110,11 @@ export function LogDashboard() {
<div className="mx-auto max-w-[1400px] px-5 py-5 sm:px-8 sm:py-7">
<header className="mb-7 flex items-start justify-between gap-4">
<div className="flex items-start gap-3"><Button variant="ghost" size="icon" className="mt-0.5 size-9 md:hidden" onClick={() => setMobileOpen(true)} aria-label="Open menu"><Menu /></Button><div><p className="font-mono text-xs uppercase tracking-widest text-muted-foreground">Overview</p><h1 className="mt-1 text-2xl font-semibold tracking-tight">Dashboard</h1><p className="mt-1 text-sm text-muted-foreground">Overview of your GoLogGo workspace</p></div></div>
<div className="flex items-center gap-2"><ThemeToggle /><Button variant="outline" size="sm" className="hidden gap-2 sm:flex" onClick={() => setMenuOpen(!menuOpen)}>{range}<ChevronDown className="size-3.5" />{menuOpen && <span className="absolute right-8 top-16 z-10 flex w-36 flex-col rounded-md border border-border bg-popover p-1 text-left shadow-lg"><button className="rounded px-2 py-1.5 text-left text-xs hover:bg-accent" onClick={() => { setRange('Last 24 hours'); setMenuOpen(false) }}>Last 24 hours</button><button className="rounded px-2 py-1.5 text-left text-xs hover:bg-accent" onClick={() => { setRange('Last 7 days'); setMenuOpen(false) }}>Last 7 days</button></span>}</Button></div>
<div className="flex items-center gap-2"><ThemeToggle /></div>
</header>

<section aria-label="Log metrics" className="grid grid-cols-2 gap-3 lg:grid-cols-4">
{[['Total Logs', '24,521', '+12.4%', Database], ['Parsed Logs', '23,891', '+8.2%', BarChart3], ['Unknown Logs', '630', '-3.1%', CircleHelp], ['Error Logs', '42', '+2.4%', CircleHelp]].map(([label, value, trend, Icon]) => <Card key={label} className="gap-3 py-4"><CardContent className="px-4"><div className="flex items-center justify-between"><p className="text-xs text-muted-foreground">{label}</p><Icon className="size-4 text-muted-foreground/70" /></div><div className="mt-2 flex items-end justify-between gap-2"><p className="font-mono text-xl font-medium tracking-tight">{value}</p><span className="text-xs text-muted-foreground">{trend}</span></div></CardContent></Card>)}
{[['Total Requests', stats?.total ?? '—', Database], ['Completed', stats?.completed ?? '—', BarChart3], ['Waiting Parser', stats?.waitingParser ?? '—', CircleHelp], ['Failed', stats?.failed ?? '—', CircleHelp]].map(([label, value, Icon]) => <Card key={label} className="gap-3 py-4"><CardContent className="px-4"><div className="flex items-center justify-between"><p className="text-xs text-muted-foreground">{label}</p><Icon className="size-4 text-muted-foreground/70" /></div><div className="mt-2 flex items-end justify-between gap-2"><p className="font-mono text-xl font-medium tracking-tight">{value}</p></div></CardContent></Card>)}
</section>

<Card className="mt-5">
Expand Down Expand Up @@ -129,9 +143,9 @@ export function LogDashboard() {
</CardContent>
</Card>

<Card className="mt-5"><CardHeader className="flex-row items-center justify-between border-b border-border px-5 py-4"><div><CardTitle className="text-sm font-medium">Log Parsing Overview</CardTitle><p className="mt-1 text-xs text-muted-foreground">Successfully processed logs by parser</p></div><div className="relative"><Button variant="outline" size="sm" className="gap-2 sm:hidden" onClick={() => setMenuOpen(!menuOpen)}>{range}<ChevronDown className="size-3.5" /></Button><Button variant="outline" size="sm" className="hidden gap-2 sm:flex" onClick={() => setMenuOpen(!menuOpen)}>{range}<ChevronDown className="size-3.5" /></Button></div></CardHeader><CardContent className="px-5 py-6"><div className="flex flex-col gap-4">{parserData.map((item) => <div key={item.name} className="grid grid-cols-[80px_1fr_52px] items-center gap-3 text-sm sm:grid-cols-[110px_1fr_64px]"><span className="text-muted-foreground">{item.name}</span><div className="h-2 overflow-hidden rounded-sm bg-accent"><div className="h-full rounded-sm bg-primary/75" style={{ width: `${(item.count / max) * 100}%` }} /></div><span className="text-right font-mono text-xs text-muted-foreground">{item.count.toLocaleString()}</span></div>)}</div></CardContent></Card>
<Card className="mt-5"><CardHeader className="border-b border-border px-5 py-4"><div><CardTitle className="text-sm font-medium">Log Parsing Overview</CardTitle><p className="mt-1 text-xs text-muted-foreground">Successfully processed logs by parser</p></div></CardHeader><CardContent className="px-5 py-6">{parserData.length > 0 ? <div className="flex flex-col gap-4">{parserData.map((item) => <div key={item.name} className="grid grid-cols-[100px_1fr_64px] items-center gap-3 text-sm sm:grid-cols-[140px_1fr_76px]"><span className="truncate text-muted-foreground">{item.name}</span><div className="h-2 overflow-hidden rounded-sm bg-accent"><div className="h-full rounded-sm bg-primary/75" style={{ width: `${(item.count / parserData[0].count) * 100}%` }} /></div><span className="text-right font-mono text-xs text-muted-foreground">{item.count.toLocaleString()}</span></div>)}</div> : <p className="text-sm text-muted-foreground">No parser activity yet.</p>}</CardContent></Card>

<Card className="mt-5"><CardHeader className="flex-row items-center justify-between px-5 py-4"><div><CardTitle className="text-sm font-medium">Recent Logs</CardTitle><p className="mt-1 text-xs text-muted-foreground">Latest events received by GoLogGo</p></div><Button variant="ghost" size="sm" className="text-xs text-muted-foreground hover:text-foreground" onClick={() => router.push('/logs')}>View all logs <span aria-hidden="true">→</span></Button></CardHeader><CardContent className="p-0"><div className="overflow-x-auto"><Table><TableHeader><TableRow><TableHead>Time</TableHead><TableHead>Level</TableHead><TableHead>Service</TableHead><TableHead>Message</TableHead><TableHead>Status</TableHead></TableRow></TableHeader><TableBody>{logs.map((log) => <TableRow key={log.id} tabIndex={0} className="cursor-pointer" onClick={() => router.push(`/logs/${log.id}`)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') router.push(`/logs/${log.id}`) }}><TableCell className="font-mono text-xs text-muted-foreground">{log.time}</TableCell><TableCell><Badge variant={log.level === 'ERROR' ? 'destructive' : 'secondary'} className={log.level === 'WARN' ? 'border-amber-500/30 bg-amber-500/10 text-amber-400' : 'font-mono text-[10px]'}>{log.level}</Badge></TableCell><TableCell className="font-mono text-xs">{log.service}</TableCell><TableCell className="min-w-48 text-sm">{log.message}</TableCell><TableCell><Badge variant="outline" className={log.status === 'Unknown' ? 'border-amber-500/30 text-amber-400' : 'border-emerald-500/30 text-emerald-400'}>{log.status}</Badge></TableCell></TableRow>)}</TableBody></Table></div></CardContent></Card>
<Card className="mt-5"><CardHeader className="flex-row items-center justify-between px-5 py-4"><div><CardTitle className="text-sm font-medium">Recent Logs</CardTitle><p className="mt-1 text-xs text-muted-foreground">Latest events received by GoLogGo</p></div><Button variant="ghost" size="sm" className="text-xs text-muted-foreground hover:text-foreground" onClick={() => router.push('/logs')}>View all logs <span aria-hidden="true">→</span></Button></CardHeader><CardContent className="p-0"><div className="overflow-x-auto">{recentLogs.length > 0 ? <Table><TableHeader><TableRow><TableHead>Time</TableHead><TableHead>Level</TableHead><TableHead>Service</TableHead><TableHead>Message</TableHead><TableHead>Status</TableHead></TableRow></TableHeader><TableBody>{recentLogs.map((log) => <TableRow key={log.id} tabIndex={0} className="cursor-pointer" onClick={() => router.push(`/logs/${log.id}`)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') router.push(`/logs/${log.id}`) }}><TableCell className="font-mono text-xs text-muted-foreground">{log.time ? new Date(log.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '—'}</TableCell><TableCell><Badge variant={log.level === 'ERROR' ? 'destructive' : 'secondary'}>{log.level}</Badge></TableCell><TableCell className="font-mono text-xs">{log.service}</TableCell><TableCell className="min-w-48 max-w-[560px] truncate text-sm">{log.message}</TableCell><TableCell><Badge variant="outline" className={log.status === 'failed' ? 'border-destructive/40 text-destructive' : 'border-emerald-500/30 text-emerald-400'}>{log.status}</Badge></TableCell></TableRow>)}</TableBody></Table> : <p className="px-5 py-8 text-sm text-muted-foreground">No recent logs yet.</p>}</div></CardContent></Card>
</div>
</main>
</div>
Expand Down
Loading
Loading