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>
94 lines
2.4 KiB
Go
94 lines
2.4 KiB
Go
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)})
|
|
}
|