feat(logs): Phase 4 — zentrales Logsystem /api/v1/logs + /system/logs
Aggregierter Reader für alle EdgeGuard-Service-Journale + audit_log.
internal/services/syslogs/
- 9 Quellen: edgeguard-api, edgeguard-scheduler, haproxy, squid,
unbound, chrony, wg-quick@*, ulogd2, audit
- journalctl --output=json + parser für __REALTIME_TIMESTAMP,
PRIORITY (0-7 → debug/info/warn/error), MESSAGE, _HOSTNAME
- audit-Reader nutzt bestehende audit.Repo.ListRecent
- Concurrent fan-out über alle gewählten Quellen, dann merge-sort
by Timestamp DESC + cap auf Limit (max 1000)
- Client-Filter: Level, Grep (case-insensitive über message +
actor + action + subject)
internal/handlers/logs.go:
GET /api/v1/logs — Filter via Query-Params
GET /api/v1/logs/sources — statische Quellen-Liste fürs UI
postinst: edgeguard → systemd-journal + adm Gruppen, damit
journalctl ohne sudo lesen kann. Verifiziert auf der Box: id zeigt
`groups=adm,systemd-journal,haproxy,edgeguard`.
UI: management-ui/src/pages/Logs — Multi-Source-Select, Level-Color-
Tags, Time-Range-Picker, Volltext-Suche, Auto-Refresh 5s (Toggle),
CSV-Export. Sidebar-Eintrag "Logs" unter System (FileSearchOutlined).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
93
internal/handlers/logs.go
Normal file
93
internal/handlers/logs.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/syslogs"
|
||||
)
|
||||
|
||||
// LogsHandler exposes:
|
||||
//
|
||||
// GET /api/v1/logs?sources=edgeguard-api,haproxy,...
|
||||
// &levels=error,warn
|
||||
// &since=2026-05-12T20:00:00Z
|
||||
// &until=2026-05-12T21:00:00Z
|
||||
// &grep=503
|
||||
// &limit=500
|
||||
//
|
||||
// Aggregiert journalctl-Output für die EdgeGuard-Services + die
|
||||
// audit_log-Tabelle in ein einheitliches Entry-Format.
|
||||
type LogsHandler struct {
|
||||
Reader *syslogs.Reader
|
||||
}
|
||||
|
||||
func NewLogsHandler(r *syslogs.Reader) *LogsHandler {
|
||||
return &LogsHandler{Reader: r}
|
||||
}
|
||||
|
||||
func (h *LogsHandler) Register(rg *gin.RouterGroup) {
|
||||
g := rg.Group("/logs")
|
||||
g.GET("", h.List)
|
||||
g.GET("/sources", h.Sources)
|
||||
}
|
||||
|
||||
// Sources gibt die statische Quellen-Liste fürs UI-Dropdown zurück —
|
||||
// das UI muss die nicht hartcodieren.
|
||||
func (h *LogsHandler) Sources(c *gin.Context) {
|
||||
out := make([]string, 0, len(syslogs.AllSources))
|
||||
for _, s := range syslogs.AllSources {
|
||||
out = append(out, string(s))
|
||||
}
|
||||
response.OK(c, gin.H{"sources": out})
|
||||
}
|
||||
|
||||
// List baut den Filter aus den Query-Params und führt Reader.Query aus.
|
||||
// Errors aus einzelnen Sources sind nicht-fatal (Reader.Query logged
|
||||
// sie selbst); wir liefern was vorhanden ist.
|
||||
func (h *LogsHandler) List(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
f := syslogs.Filter{
|
||||
Grep: c.Query("grep"),
|
||||
}
|
||||
if s := c.Query("sources"); s != "" {
|
||||
for _, raw := range strings.Split(s, ",") {
|
||||
s := strings.TrimSpace(raw)
|
||||
if s != "" {
|
||||
f.Sources = append(f.Sources, syslogs.Source(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
if s := c.Query("levels"); s != "" {
|
||||
for _, raw := range strings.Split(s, ",") {
|
||||
s := strings.TrimSpace(raw)
|
||||
if s != "" {
|
||||
f.Levels = append(f.Levels, syslogs.Level(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
if s := c.Query("since"); s != "" {
|
||||
if t, err := time.Parse(time.RFC3339, s); err == nil {
|
||||
f.Since = t
|
||||
}
|
||||
}
|
||||
if s := c.Query("until"); s != "" {
|
||||
if t, err := time.Parse(time.RFC3339, s); err == nil {
|
||||
f.Until = t
|
||||
}
|
||||
}
|
||||
if s := c.Query("limit"); s != "" {
|
||||
if n, err := strconv.Atoi(s); err == nil {
|
||||
f.Limit = n
|
||||
}
|
||||
}
|
||||
entries, _ := h.Reader.Query(ctx, f)
|
||||
response.OK(c, gin.H{"entries": entries, "count": len(entries)})
|
||||
}
|
||||
Reference in New Issue
Block a user