Workspace
Parsers
Manage and monitor available log parsers
TOTAL PARSERS
12
ACTIVE PARSERS
11
LOGS PROCESSED
23,891
diff --git a/code/backend/cmd/server/main.go b/code/backend/cmd/server/main.go index 063fd11..becbeee 100644 --- a/code/backend/cmd/server/main.go +++ b/code/backend/cmd/server/main.go @@ -81,6 +81,10 @@ func main() { }) app.POST("/user", apiCfg.HandlerUser) app.POST("/log", auth.MiddlewareAuth(apiCfg.LogHandler)) + app.GET("/parsers", apiCfg.ListParsersHandler) + app.GET("/parsers/:id", apiCfg.GetParserHandler) + app.PATCH("/parsers/:id", apiCfg.UpdateParserHandler) + app.POST("/parsers", apiCfg.CreateParserHandler) // schedular init sched := schedular.NewSchedular(mongod, JobChan) diff --git a/code/backend/internal/api/parser.go b/code/backend/internal/api/parser.go new file mode 100644 index 0000000..e1ac36f --- /dev/null +++ b/code/backend/internal/api/parser.go @@ -0,0 +1,199 @@ +package api + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "net/http" + "strings" + + "github.com/EleventhHour-Projects/GoLogGo/code/backend/internal/database" + "github.com/EleventhHour-Projects/GoLogGo/code/backend/internal/parser" + "github.com/nottechdm/notnet/pkg/notnet" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" +) + +// ListParsersHandler returns all parsers from MongoDB along with calculated aggregate metrics. +func (api *Config) ListParsersHandler(req *notnet.Request, res *notnet.Response) error { + ctx := req.HTTPRequest.Context() + parsers, err := api.Reqs.FindAllParsers(ctx) + if err != nil { + return res.JSON(http.StatusInternalServerError, map[string]string{ + "error": "failed to fetch parsers from database", + }) + } + + var activeCount int + var totalLogsProcessed int64 + for _, p := range parsers { + if strings.EqualFold(p.Status, "active") { + activeCount++ + } + totalLogsProcessed += p.LogsProcessed + } + + return res.JSON(http.StatusOK, map[string]interface{}{ + "parsers": parsers, + "metrics": map[string]interface{}{ + "totalParsers": len(parsers), + "activeParsers": activeCount, + "totalLogsProcessed": totalLogsProcessed, + }, + }) +} + +// GetParserHandler returns a single parser by its ObjectID or hash. +func (api *Config) GetParserHandler(req *notnet.Request, res *notnet.Response) error { + ctx := req.HTTPRequest.Context() + idOrHash := req.Param("id") + if idOrHash == "" { + return res.JSON(http.StatusBadRequest, map[string]string{ + "error": "parser id or hash is required", + }) + } + + var p *database.ParserDoc + var err error + + if objID, parseErr := bson.ObjectIDFromHex(idOrHash); parseErr == nil { + p, err = api.Reqs.FindParserByID(ctx, objID) + } else { + p, err = api.Reqs.FindParserByHash(ctx, idOrHash) + } + + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return res.JSON(http.StatusNotFound, map[string]string{ + "error": "parser not found", + }) + } + return res.JSON(http.StatusInternalServerError, map[string]string{ + "error": "failed to retrieve parser", + }) + } + + return res.JSON(http.StatusOK, p) +} + +// UpdateParserHandler updates the name, format, or status of an existing parser. +func (api *Config) UpdateParserHandler(req *notnet.Request, res *notnet.Response) error { + ctx := req.HTTPRequest.Context() + idStr := req.Param("id") + if idStr == "" { + return res.JSON(http.StatusBadRequest, map[string]string{ + "error": "parser id is required", + }) + } + + objID, err := bson.ObjectIDFromHex(idStr) + if err != nil { + // If not ObjectID, check if it's a hash + doc, findErr := api.Reqs.FindParserByHash(ctx, idStr) + if findErr != nil { + return res.JSON(http.StatusBadRequest, map[string]string{ + "error": "invalid parser id or hash", + }) + } + objID = doc.ID + } + + type updatePayload struct { + Name string `json:"name"` + Format string `json:"format"` + Status string `json:"status"` + } + + var payload updatePayload + if err := json.NewDecoder(req.HTTPRequest.Body).Decode(&payload); err != nil { + return res.JSON(http.StatusBadRequest, map[string]string{ + "error": "invalid request body", + }) + } + + updated, err := api.Reqs.UpdateParser(ctx, objID, payload.Name, payload.Format, payload.Status) + if err != nil { + return res.JSON(http.StatusInternalServerError, map[string]string{ + "error": "failed to update parser", + }) + } + + return res.JSON(http.StatusOK, map[string]interface{}{ + "success": true, + "parser": updated, + }) +} + +// CreateParserHandler allows manual creation of a log parser. +func (api *Config) CreateParserHandler(req *notnet.Request, res *notnet.Response) error { + ctx := req.HTTPRequest.Context() + + type createPayload struct { + Name string `json:"name"` + Format string `json:"format"` + Pattern string `json:"pattern"` + Mapping map[string]string `json:"mapping"` + Transformations map[string]parser.Transformation `json:"transformations,omitempty"` + Status string `json:"status"` + SampleLog string `json:"sampleLog,omitempty"` + } + + var payload createPayload + if err := json.NewDecoder(req.HTTPRequest.Body).Decode(&payload); err != nil { + return res.JSON(http.StatusBadRequest, map[string]string{ + "error": "invalid request body", + }) + } + + if payload.Pattern == "" { + return res.JSON(http.StatusBadRequest, map[string]string{ + "error": "pattern is required", + }) + } + + if payload.Name == "" { + if payload.Format != "" { + payload.Name = payload.Format + "Parser" + } else { + payload.Name = "CustomParser" + } + } + if payload.Status == "" { + payload.Status = "Active" + } + if payload.Format == "" { + payload.Format = "Custom" + } + if payload.Mapping == nil { + payload.Mapping = make(map[string]string) + } + + // Generate deterministic hash from pattern + h := sha256.New() + h.Write([]byte(payload.Pattern)) + hash := hex.EncodeToString(h.Sum(nil))[:16] + + doc := database.ParserDoc{ + Name: payload.Name, + Hash: hash, + Pattern: payload.Pattern, + Mapping: payload.Mapping, + Transformations: payload.Transformations, + Status: payload.Status, + Format: payload.Format, + SampleLog: payload.SampleLog, + } + + saved, err := api.Reqs.SaveParser(ctx, &doc) + if err != nil { + return res.JSON(http.StatusInternalServerError, map[string]string{ + "error": "failed to save parser to database", + }) + } + + return res.JSON(http.StatusCreated, map[string]interface{}{ + "success": true, + "parser": saved, + }) +} diff --git a/code/backend/internal/database/config.go b/code/backend/internal/database/config.go index 9ee2874..94394db 100644 --- a/code/backend/internal/database/config.go +++ b/code/backend/internal/database/config.go @@ -20,6 +20,9 @@ const ( // CollectionLogs is the name of the logs collection in MongoDB. CollectionLogs MongoDBCollection = "logs" + + // CollectionParsers is the name of the parsers collection in MongoDB. + CollectionParsers MongoDBCollection = "parsers" ) type MongoDB struct { @@ -67,6 +70,7 @@ func New(ctx context.Context, config MongoDBOptions) (*MongoDB, error) { CollectionUsers: db.Collection(string(CollectionUsers)), CollectionRequests: db.Collection(string(CollectionRequests)), CollectionLogs: db.Collection(string(CollectionLogs)), + CollectionParsers: db.Collection(string(CollectionParsers)), }, }, nil } diff --git a/code/backend/internal/database/mongodb.go b/code/backend/internal/database/mongodb.go index 797fbe7..586e323 100644 --- a/code/backend/internal/database/mongodb.go +++ b/code/backend/internal/database/mongodb.go @@ -189,3 +189,217 @@ func (m *MongoDB) InsertLog(ctx context.Context, req Req, nlog NormalizedLog, is return nil } + +// ParserDoc represents a parser document stored in MongoDB. +type ParserDoc struct { + ID bson.ObjectID `bson:"_id,omitempty" json:"id"` + Name string `bson:"name" json:"name"` + Hash string `bson:"hash" json:"hash"` + Pattern string `bson:"pattern" json:"pattern"` + Mapping map[string]string `bson:"mapping" json:"mapping"` + Transformations map[string]parser.Transformation `bson:"transformations,omitempty" json:"transformations,omitempty"` + Status string `bson:"status" json:"status"` + Format string `bson:"format" json:"format"` + Vendor string `bson:"vendor,omitempty" json:"vendor,omitempty"` + Product string `bson:"product,omitempty" json:"product,omitempty"` + Template string `bson:"template,omitempty" json:"template,omitempty"` + Delimiter string `bson:"delimiter,omitempty" json:"delimiter,omitempty"` + ExtractedFields []string `bson:"extractedFields,omitempty" json:"extractedFields,omitempty"` + FieldSignatures []map[string]string `bson:"fieldSignatures,omitempty" json:"fieldSignatures,omitempty"` + SampleLog string `bson:"sampleLog,omitempty" json:"sampleLog,omitempty"` + LogsProcessed int64 `bson:"logsProcessed" json:"logsProcessed"` + LastUsed time.Time `bson:"lastUsed" json:"lastUsed"` + CreatedAt time.Time `bson:"createdAt" json:"createdAt"` + UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"` +} + +// SaveParser inserts or updates a parser in MongoDB by Hash. +func (m *MongoDB) SaveParser(ctx context.Context, p *ParserDoc) (*ParserDoc, error) { + collection := m.Collections[CollectionParsers] + now := time.Now() + + if p.Name == "" { + if p.Product != "" { + p.Name = p.Product + "Parser" + } else if p.Vendor != "" { + p.Name = p.Vendor + "Parser" + } else if p.Format != "" && p.Format != "unknown" { + p.Name = p.Format + "Parser" + } else if len(p.Hash) >= 8 { + p.Name = "Parser-" + p.Hash[:8] + } else { + p.Name = "CustomParser" + } + } + if p.Status == "" { + p.Status = "Active" + } + if p.Format == "" { + p.Format = "Custom" + } + + var existing ParserDoc + err := collection.FindOne(ctx, bson.M{"hash": p.Hash}).Decode(&existing) + if err == nil { + // Update existing + updateFields := bson.M{ + "pattern": p.Pattern, + "mapping": p.Mapping, + "transformations": p.Transformations, + "format": p.Format, + "updatedAt": now, + } + if p.Vendor != "" { + updateFields["vendor"] = p.Vendor + } + if p.Product != "" { + updateFields["product"] = p.Product + } + if p.Template != "" { + updateFields["template"] = p.Template + } + if p.Delimiter != "" { + updateFields["delimiter"] = p.Delimiter + } + if len(p.ExtractedFields) > 0 { + updateFields["extractedFields"] = p.ExtractedFields + } + if len(p.FieldSignatures) > 0 { + updateFields["fieldSignatures"] = p.FieldSignatures + } + if p.SampleLog != "" { + updateFields["sampleLog"] = p.SampleLog + } + // If existing parser had a custom name, preserve it unless explicitly overwritten + if existing.Name != "" && (p.Name == "" || p.Name == "CustomParser" || (len(p.Hash) >= 8 && p.Name == "Parser-"+p.Hash[:8])) { + p.Name = existing.Name + } else if p.Name != "" { + updateFields["name"] = p.Name + } + if p.Status != "" { + updateFields["status"] = p.Status + } + + _, err = collection.UpdateOne(ctx, bson.M{"_id": existing.ID}, bson.M{"$set": updateFields}) + if err != nil { + return nil, err + } + existing.Pattern = p.Pattern + existing.Mapping = p.Mapping + existing.Transformations = p.Transformations + existing.Format = p.Format + existing.Vendor = p.Vendor + existing.Product = p.Product + existing.Template = p.Template + existing.Delimiter = p.Delimiter + existing.ExtractedFields = p.ExtractedFields + existing.FieldSignatures = p.FieldSignatures + existing.UpdatedAt = now + if p.SampleLog != "" { + existing.SampleLog = p.SampleLog + } + return &existing, nil + } + + // Insert new + p.ID = bson.NewObjectID() + p.CreatedAt = now + p.UpdatedAt = now + p.LastUsed = now + + _, err = collection.InsertOne(ctx, p) + if err != nil { + return nil, err + } + + return p, nil +} + +// FindParserByHash finds a parser by its hash. +func (m *MongoDB) FindParserByHash(ctx context.Context, hash string) (*ParserDoc, error) { + collection := m.Collections[CollectionParsers] + var p ParserDoc + err := collection.FindOne(ctx, bson.M{"hash": hash}).Decode(&p) + if err != nil { + return nil, err + } + return &p, nil +} + +// FindParserByID finds a parser by its ObjectID. +func (m *MongoDB) FindParserByID(ctx context.Context, id bson.ObjectID) (*ParserDoc, error) { + collection := m.Collections[CollectionParsers] + var p ParserDoc + err := collection.FindOne(ctx, bson.M{"_id": id}).Decode(&p) + if err != nil { + return nil, err + } + return &p, nil +} + +// FindAllParsers retrieves all parsers. +func (m *MongoDB) FindAllParsers(ctx context.Context) ([]ParserDoc, error) { + collection := m.Collections[CollectionParsers] + cursor, err := collection.Find(ctx, bson.M{}) + if err != nil { + return nil, err + } + defer cursor.Close(ctx) + + var parsers []ParserDoc + for cursor.Next(ctx) { + var p ParserDoc + if err := cursor.Decode(&p); err != nil { + return nil, err + } + parsers = append(parsers, p) + } + + if err := cursor.Err(); err != nil { + return nil, err + } + + if parsers == nil { + parsers = []ParserDoc{} + } + + return parsers, nil +} + +// UpdateParser updates parser name, format, or status by ID. +func (m *MongoDB) UpdateParser(ctx context.Context, id bson.ObjectID, name, format, status string) (*ParserDoc, error) { + collection := m.Collections[CollectionParsers] + update := bson.M{"updatedAt": time.Now()} + + if name != "" { + update["name"] = name + } + if format != "" { + update["format"] = format + } + if status != "" { + update["status"] = status + } + + _, err := collection.UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$set": update}) + if err != nil { + return nil, err + } + + return m.FindParserByID(ctx, id) +} + +// IncrementParserLogsProcessed increments logsProcessed count and sets lastUsed timestamp. +func (m *MongoDB) IncrementParserLogsProcessed(ctx context.Context, hash string) error { + collection := m.Collections[CollectionParsers] + _, err := collection.UpdateOne( + ctx, + bson.M{"hash": hash}, + bson.M{ + "$inc": bson.M{"logsProcessed": 1}, + "$set": bson.M{"lastUsed": time.Now()}, + }, + ) + return err +} + diff --git a/code/backend/internal/parsergen/parser_generator.go b/code/backend/internal/parsergen/parser_generator.go index 0034f70..6a95445 100644 --- a/code/backend/internal/parsergen/parser_generator.go +++ b/code/backend/internal/parsergen/parser_generator.go @@ -116,7 +116,52 @@ func (pg *ParserGenerator) ProcessMessage(ctx context.Context, body []byte) erro return fmt.Errorf("failed to generate parser from ML service: %w", err) } - // 4. Save parser to Redis/storage + // 4. Save parser to MongoDB & Redis/storage + if newParser != nil && pg.Reqs != nil { + formatName := "Custom" + if msg.Features.Format != "" && msg.Features.Format != "unknown" { + formatName = string(msg.Features.Format) + } + + var signatures []map[string]string + for _, sig := range msg.Features.FieldSignatures { + signatures = append(signatures, map[string]string{ + "key": sig.Key, + "type": string(sig.Type), + }) + } + + var extracted []string + if len(msg.Features.Keys) > 0 { + extracted = msg.Features.Keys + } else { + for k := range newParser.Mapping { + extracted = append(extracted, k) + } + } + + doc := database.ParserDoc{ + Hash: msg.Hash, + Pattern: newParser.Pattern, + Mapping: newParser.Mapping, + Transformations: newParser.Transformations, + Status: "Active", + Format: formatName, + Vendor: msg.Features.VendorHint, + Product: msg.Features.ProductHint, + Template: msg.Features.Template, + Delimiter: msg.Features.Delimiter, + ExtractedFields: extracted, + FieldSignatures: signatures, + SampleLog: msg.RawLog, + } + if _, dbErr := pg.Reqs.SaveParser(ctx, &doc); dbErr != nil { + log.Printf("ParserGenerator: warning: failed to save parser to MongoDB for hash %s: %v", msg.Hash, dbErr) + } else { + log.Printf("ParserGenerator: successfully saved parser to MongoDB for hash %s", msg.Hash) + } + } + if pg.Redis != nil && pg.Redis.Client != nil && newParser != nil { parserJSON, err := json.Marshal(newParser) if err == nil { diff --git a/code/backend/internal/worker/worker.go b/code/backend/internal/worker/worker.go index e1c51d7..cb8bc87 100644 --- a/code/backend/internal/worker/worker.go +++ b/code/backend/internal/worker/worker.go @@ -100,6 +100,24 @@ func (w *Worker) processJob(ctx context.Context, jobID bson.ObjectID) error { } } + // 4b. If missing in Redis, check MongoDB parsers collection + if p == nil && w.Reqs != nil { + doc, err := w.Reqs.FindParserByHash(ctx, hash) + if err == nil && doc != nil && doc.Pattern != "" { + p = &parser.Parser{ + Pattern: doc.Pattern, + Mapping: doc.Mapping, + Transformations: doc.Transformations, + } + log.Printf("Parser found in MongoDB for hash %s, caching in Redis", hash) + if w.Redis != nil && w.Redis.Client != nil { + if parserJSON, err := json.Marshal(p); err == nil { + _ = w.Redis.Client.Set(ctx, redisKey, parserJSON, 0).Err() + } + } + } + } + // 5. If parser doesn't exist: // - mark fingerprint as PENDING atomically // - only the worker that successfully creates PENDING publishes a RabbitMQ parser-generation job @@ -177,6 +195,9 @@ func (w *Worker) processJob(ctx context.Context, jobID bson.ObjectID) error { return fmt.Errorf("failed to insert normalized log to database for %v: %w", jobID, err) } + // Increment processed logs count for this parser in MongoDB + _ = w.Reqs.IncrementParserLogsProcessed(ctx, hash) + // 8. Update request status to "completed" err = w.Reqs.UpdateReqStatus(ctx, jobID, database.StatusCompleted) if err != nil { diff --git a/code/frontend/gologgo-client/app/api/parsers/[id]/route.js b/code/frontend/gologgo-client/app/api/parsers/[id]/route.js new file mode 100644 index 0000000..78c0df9 --- /dev/null +++ b/code/frontend/gologgo-client/app/api/parsers/[id]/route.js @@ -0,0 +1,66 @@ +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 backendRes = await fetch(`${BACKEND_URL}/parsers/${id}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + cache: 'no-store', + }); + + if (!backendRes.ok) { + const errorText = await backendRes.text(); + return NextResponse.json( + { error: 'Failed to fetch parser', details: errorText }, + { status: backendRes.status } + ); + } + + const data = await backendRes.json(); + return NextResponse.json(data); + } catch (error) { + console.error('Error fetching parser:', error); + return NextResponse.json( + { error: 'Internal Server Error', details: error.message }, + { status: 500 } + ); + } +} + +export async function PATCH(request, { params }) { + try { + const { id } = await params; + const body = await request.json(); + + const backendRes = await fetch(`${BACKEND_URL}/parsers/${id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!backendRes.ok) { + const errorText = await backendRes.text(); + return NextResponse.json( + { error: 'Failed to update parser', details: errorText }, + { status: backendRes.status } + ); + } + + const data = await backendRes.json(); + return NextResponse.json(data); + } catch (error) { + console.error('Error updating parser:', error); + return NextResponse.json( + { error: 'Internal Server Error', details: error.message }, + { status: 500 } + ); + } +} diff --git a/code/frontend/gologgo-client/app/api/parsers/route.js b/code/frontend/gologgo-client/app/api/parsers/route.js new file mode 100644 index 0000000..d430a92 --- /dev/null +++ b/code/frontend/gologgo-client/app/api/parsers/route.js @@ -0,0 +1,63 @@ +import { NextResponse } from 'next/server'; + +const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8080'; + +export async function GET() { + try { + const backendRes = await fetch(`${BACKEND_URL}/parsers`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + cache: 'no-store', + }); + + if (!backendRes.ok) { + const errorText = await backendRes.text(); + return NextResponse.json( + { error: 'Failed to fetch parsers from backend', details: errorText }, + { status: backendRes.status } + ); + } + + const data = await backendRes.json(); + return NextResponse.json(data); + } catch (error) { + console.error('Error fetching parsers:', error); + return NextResponse.json( + { error: 'Internal Server Error', details: error.message }, + { status: 500 } + ); + } +} + +export async function POST(request) { + try { + const body = await request.json(); + + const backendRes = await fetch(`${BACKEND_URL}/parsers`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!backendRes.ok) { + const errorText = await backendRes.text(); + return NextResponse.json( + { error: 'Failed to create parser', details: errorText }, + { status: backendRes.status } + ); + } + + const data = await backendRes.json(); + return NextResponse.json(data, { status: 201 }); + } catch (error) { + console.error('Error creating parser:', error); + return NextResponse.json( + { error: 'Internal Server Error', details: error.message }, + { status: 500 } + ); + } +} diff --git a/code/frontend/gologgo-client/components/parsers-page.jsx b/code/frontend/gologgo-client/components/parsers-page.jsx index 288584d..d0fc9f8 100644 --- a/code/frontend/gologgo-client/components/parsers-page.jsx +++ b/code/frontend/gologgo-client/components/parsers-page.jsx @@ -1,9 +1,21 @@ 'use client' -import { useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { Sidebar } from '@/components/sidebar' -import { useRouter } from 'next/navigation' -import { ChevronDown, CircleHelp, FileJson, LayoutDashboard, ListFilter, Menu, Plus, Search, TerminalSquare, X } from 'lucide-react' +import { + ChevronDown, + ChevronLeft, + ChevronRight, + Check, + Copy, + Edit2, + FileJson, + Menu, + Plus, + RefreshCw, + Search, + X +} from 'lucide-react' import { ThemeToggle } from '@/components/theme-toggle' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' @@ -11,36 +23,885 @@ import { Card, CardContent } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +function formatTimeAgo(dateInput) { + if (!dateInput) return '—' + const date = new Date(dateInput) + if (isNaN(date.getTime())) return '—' + const now = new Date() + const diffSeconds = Math.floor((now - date) / 1000) -const parsers = [ - { name: 'AuthParser', format: 'Authentication', status: 'Active', logs: '5,241', lastUsed: 'Just now', created: 'Aug 20, 2026' }, - { name: 'NginxParser', format: 'Nginx', status: 'Active', logs: '8,421', lastUsed: '2 min ago', created: 'Aug 18, 2026' }, - { name: 'PaymentParser', format: 'Payment', status: 'Active', logs: '4,120', lastUsed: '5 min ago', created: 'Aug 22, 2026' }, - { name: 'PostgresParser', format: 'PostgreSQL', status: 'Active', logs: '3,241', lastUsed: '8 min ago', created: 'Aug 15, 2026' }, - { name: 'UserParser', format: 'User Activity', status: 'Active', logs: '1,698', lastUsed: '12 min ago', created: 'Aug 21, 2026' }, - { name: 'GatewayParser', format: 'Gateway Security', status: 'Active', logs: '780', lastUsed: '18 min ago', created: 'Aug 28, 2026' }, - { name: 'LegacyParser', format: 'Legacy Application', status: 'Inactive', logs: '391', lastUsed: '2 days ago', created: 'Aug 02, 2026' }, - { name: 'SecurityParser', format: 'Security Event', status: 'Active', logs: '612', lastUsed: '25 min ago', created: 'Aug 27, 2026' }, -] - + if (diffSeconds < 60) return 'Just now' + if (diffSeconds < 3600) return `${Math.floor(diffSeconds / 60)} min ago` + if (diffSeconds < 86400) return `${Math.floor(diffSeconds / 3600)} hr ago` + if (diffSeconds < 604800) return `${Math.floor(diffSeconds / 86400)} days ago` + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) +} +function formatDate(dateInput) { + if (!dateInput) return '—' + const date = new Date(dateInput) + if (isNaN(date.getTime())) return '—' + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) +} function SelectFilter({ label, value, onChange, options }) { - return + return ( + + ) } export function ParsersPage() { + const [parsersList, setParsersList] = useState([]) + const [metrics, setMetrics] = useState({ totalParsers: 0, activeParsers: 0, totalLogsProcessed: 0 }) + const [loading, setLoading] = useState(true) + const [refreshing, setRefreshing] = useState(false) + const [error, setError] = useState(null) + + // Filters & Search const [search, setSearch] = useState('') const [status, setStatus] = useState('All') const [format, setFormat] = useState('All Formats') const [mobileOpen, setMobileOpen] = useState(false) - const router = useRouter() - const filtered = useMemo(() => parsers.filter((parser) => `${parser.name} ${parser.format}`.toLowerCase().includes(search.toLowerCase()) && (status === 'All' || parser.status === status) && (format === 'All Formats' || parser.format === format)), [search, status, format]) - const clear = () => { setSearch(''); setStatus('All'); setFormat('All Formats') } - - return
Workspace
Manage and monitor available log parsers
TOTAL PARSERS
12
ACTIVE PARSERS
11
LOGS PROCESSED
23,891
+ Workspace +
++ Manage and inspect active log parsers and extraction schemas +
+Total Parsers
+{loading ? '—' : metrics.totalParsers || parsersList.length}
+Active Parsers
+{loading ? '—' : metrics.activeParsers || 0}
+Logs Processed
++ {loading ? '—' : Number(metrics.totalLogsProcessed || 0).toLocaleString()} +
++ {parsersList.length} total parsers in database +
++ Fingerprint Hash: {inspectParser.hash || '—'} +
+