docs(runtime-logs): implement runtime log management features
Добавлены новые возможности для работы с файловыми логами Docker-сервисов: - Эндпоинты для получения списка логов и хвоста лог-файла. - Очистка лог-файлов с возможностью выбора режима (truncate или delete) и запись в аудит очистки. - Обновлена документация и конфигурация для поддержки новых функций. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -125,6 +125,10 @@ type Backend interface {
|
||||
TouchMaintenancePolicyRun(id, status, errMsg string) error
|
||||
AppendMaintenancePolicyConfigAudit(actor, policyID, action string, before, after map[string]any) error
|
||||
ListMaintenancePolicyConfigAudit(cursor string, limit int) ([]*MaintenancePolicyConfigAudit, string, bool, error)
|
||||
|
||||
// Runtime log cleanup audit (filesystem ops logged per tenant).
|
||||
AppendRuntimeLogCleanupAudit(tenantID, actor, filename, action string, sizeBefore int64, sizeAfter *int64, detail map[string]any) (string, error)
|
||||
ListRuntimeLogCleanupAudit(tenantID, cursor string, limit int) ([]*RuntimeLogCleanupAudit, string, bool, error)
|
||||
}
|
||||
|
||||
// ASNPrefixCacheEntry is a cached RIPEstat response for one ASN.
|
||||
|
||||
+34
-32
@@ -33,19 +33,20 @@ type Memory struct {
|
||||
|
||||
peers map[string]*BGPPeer
|
||||
|
||||
dohProfiles map[string]*DohProfile
|
||||
communities map[string]*Community
|
||||
cdnSources map[string]*CDNSource
|
||||
asEntries map[string]*ASEntry
|
||||
domainEnt map[string]*DomainEntry
|
||||
ipRanges map[string]*IPRangeEntry
|
||||
settings map[string]map[string]any // tenantID -> key -> JSON-compatible value
|
||||
revPrefixes map[string][]PrefixRow
|
||||
moduleSnapshots map[string]*moduleSnapshotRec
|
||||
asnPrefixCache map[int64]*ASNPrefixCacheEntry
|
||||
apiKeys map[string]*apiKeyRec
|
||||
maintenancePolicies map[string]*MaintenancePolicy
|
||||
maintConfigAudit []*MaintenancePolicyConfigAudit
|
||||
dohProfiles map[string]*DohProfile
|
||||
communities map[string]*Community
|
||||
cdnSources map[string]*CDNSource
|
||||
asEntries map[string]*ASEntry
|
||||
domainEnt map[string]*DomainEntry
|
||||
ipRanges map[string]*IPRangeEntry
|
||||
settings map[string]map[string]any // tenantID -> key -> JSON-compatible value
|
||||
revPrefixes map[string][]PrefixRow
|
||||
moduleSnapshots map[string]*moduleSnapshotRec
|
||||
asnPrefixCache map[int64]*ASNPrefixCacheEntry
|
||||
apiKeys map[string]*apiKeyRec
|
||||
maintenancePolicies map[string]*MaintenancePolicy
|
||||
maintConfigAudit []*MaintenancePolicyConfigAudit
|
||||
runtimeLogCleanupAudit []*RuntimeLogCleanupAudit
|
||||
|
||||
// DemoIDs valid after SeedDemo()
|
||||
demoTenantID string
|
||||
@@ -125,25 +126,26 @@ type Speaker struct {
|
||||
|
||||
func NewMemory() *Memory {
|
||||
return &Memory{
|
||||
tenants: make(map[string]*Tenant),
|
||||
modules: make(map[string]*Module),
|
||||
revisions: make(map[string]*Revision),
|
||||
speakers: make(map[string]*Speaker),
|
||||
publishedRevision: make(map[string]publishedInfo),
|
||||
peers: make(map[string]*BGPPeer),
|
||||
dohProfiles: make(map[string]*DohProfile),
|
||||
communities: make(map[string]*Community),
|
||||
cdnSources: make(map[string]*CDNSource),
|
||||
asEntries: make(map[string]*ASEntry),
|
||||
domainEnt: make(map[string]*DomainEntry),
|
||||
ipRanges: make(map[string]*IPRangeEntry),
|
||||
settings: make(map[string]map[string]any),
|
||||
revPrefixes: make(map[string][]PrefixRow),
|
||||
moduleSnapshots: make(map[string]*moduleSnapshotRec),
|
||||
asnPrefixCache: make(map[int64]*ASNPrefixCacheEntry),
|
||||
apiKeys: make(map[string]*apiKeyRec),
|
||||
maintenancePolicies: make(map[string]*MaintenancePolicy),
|
||||
maintConfigAudit: nil,
|
||||
tenants: make(map[string]*Tenant),
|
||||
modules: make(map[string]*Module),
|
||||
revisions: make(map[string]*Revision),
|
||||
speakers: make(map[string]*Speaker),
|
||||
publishedRevision: make(map[string]publishedInfo),
|
||||
peers: make(map[string]*BGPPeer),
|
||||
dohProfiles: make(map[string]*DohProfile),
|
||||
communities: make(map[string]*Community),
|
||||
cdnSources: make(map[string]*CDNSource),
|
||||
asEntries: make(map[string]*ASEntry),
|
||||
domainEnt: make(map[string]*DomainEntry),
|
||||
ipRanges: make(map[string]*IPRangeEntry),
|
||||
settings: make(map[string]map[string]any),
|
||||
revPrefixes: make(map[string][]PrefixRow),
|
||||
moduleSnapshots: make(map[string]*moduleSnapshotRec),
|
||||
asnPrefixCache: make(map[int64]*ASNPrefixCacheEntry),
|
||||
apiKeys: make(map[string]*apiKeyRec),
|
||||
maintenancePolicies: make(map[string]*MaintenancePolicy),
|
||||
maintConfigAudit: nil,
|
||||
runtimeLogCleanupAudit: nil,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (m *Memory) AppendRuntimeLogCleanupAudit(tenantID, actor, filename, action string, sizeBefore int64, sizeAfter *int64, detail map[string]any) (string, error) {
|
||||
if strings.TrimSpace(tenantID) == "" {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
if !ValidRuntimeLogCleanupAction(action) {
|
||||
return "", ErrInvalidInput
|
||||
}
|
||||
name := strings.TrimSpace(filename)
|
||||
if name == "" {
|
||||
return "", ErrInvalidInput
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
id := uuid.NewString()
|
||||
row := &RuntimeLogCleanupAudit{
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
ActorPrefix: strings.TrimSpace(actor),
|
||||
Filename: name,
|
||||
Action: action,
|
||||
SizeBefore: sizeBefore,
|
||||
SizeAfter: sizeAfter,
|
||||
Detail: detail,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
}
|
||||
m.runtimeLogCleanupAudit = append(m.runtimeLogCleanupAudit, row)
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (m *Memory) ListRuntimeLogCleanupAudit(tenantID, cursor string, limit int) ([]*RuntimeLogCleanupAudit, string, bool, error) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
var filtered []*RuntimeLogCleanupAudit
|
||||
for _, row := range m.runtimeLogCleanupAudit {
|
||||
if row.TenantID == tenantID {
|
||||
filtered = append(filtered, row)
|
||||
}
|
||||
}
|
||||
sort.Slice(filtered, func(i, j int) bool {
|
||||
if filtered[i].CreatedAt.Equal(filtered[j].CreatedAt) {
|
||||
return filtered[i].ID > filtered[j].ID
|
||||
}
|
||||
return filtered[i].CreatedAt.After(filtered[j].CreatedAt)
|
||||
})
|
||||
off := parseMaintCursor(cursor)
|
||||
end := off + limit
|
||||
next := ""
|
||||
hasMore := false
|
||||
if end > len(filtered) {
|
||||
end = len(filtered)
|
||||
} else if end < len(filtered) {
|
||||
hasMore = true
|
||||
next = formatMaintCursor(end)
|
||||
}
|
||||
if off >= len(filtered) {
|
||||
return nil, "", false, nil
|
||||
}
|
||||
out := make([]*RuntimeLogCleanupAudit, end-off)
|
||||
copy(out, filtered[off:end])
|
||||
return out, next, hasMore, nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package store
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMemoryRuntimeLogCleanupAudit(t *testing.T) {
|
||||
m := NewMemory()
|
||||
tenantA := "tenant-a"
|
||||
tenantB := "tenant-b"
|
||||
after := int64(0)
|
||||
|
||||
id, err := m.AppendRuntimeLogCleanupAudit(tenantA, "op:alice", "evobgp-all.log", RuntimeLogCleanupTruncate, 1024, &after, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if id == "" {
|
||||
t.Fatal("expected audit id")
|
||||
}
|
||||
|
||||
if _, err := m.AppendRuntimeLogCleanupAudit(tenantA, "op:alice", "postgres.log", RuntimeLogCleanupDelete, 512, nil, map[string]any{"note": "removed"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := m.AppendRuntimeLogCleanupAudit(tenantB, "op:bob", "bird2.log", RuntimeLogCleanupTruncate, 256, &after, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
items, next, hasMore, err := m.ListRuntimeLogCleanupAudit(tenantA, "", 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(items) != 2 || hasMore || next != "" {
|
||||
t.Fatalf("tenantA list: len=%d hasMore=%v next=%q", len(items), hasMore, next)
|
||||
}
|
||||
if items[0].Filename == items[1].Filename {
|
||||
t.Fatalf("expected desc order by created_at: %+v", items)
|
||||
}
|
||||
|
||||
page, next, hasMore, err := m.ListRuntimeLogCleanupAudit(tenantA, "", 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(page) != 1 || !hasMore || next == "" {
|
||||
t.Fatalf("page1: len=%d hasMore=%v next=%q", len(page), hasMore, next)
|
||||
}
|
||||
|
||||
page2, next2, hasMore2, err := m.ListRuntimeLogCleanupAudit(tenantA, next, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(page2) != 1 || hasMore2 || next2 != "" {
|
||||
t.Fatalf("page2: len=%d hasMore=%v next=%q", len(page2), hasMore2, next2)
|
||||
}
|
||||
if page[0].ID == page2[0].ID {
|
||||
t.Fatal("expected different audit rows across pages")
|
||||
}
|
||||
|
||||
if _, err := m.AppendRuntimeLogCleanupAudit(tenantA, "op:x", "bad.log", "wipe", 1, nil, nil); err != ErrInvalidInput {
|
||||
t.Fatalf("invalid action: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidRuntimeLogCleanupAction(t *testing.T) {
|
||||
if !ValidRuntimeLogCleanupAction(RuntimeLogCleanupTruncate) {
|
||||
t.Fatal("truncate")
|
||||
}
|
||||
if ValidRuntimeLogCleanupAction("rotate") {
|
||||
t.Fatal("unexpected valid")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Runtime log cleanup actions (runtime_log_cleanup_audit.action).
|
||||
const (
|
||||
RuntimeLogCleanupTruncate = "truncate"
|
||||
RuntimeLogCleanupDelete = "delete"
|
||||
)
|
||||
|
||||
// RuntimeLogFile describes a file in EVOBGP_RUNTIME_LOGS_DIR (API DTO).
|
||||
type RuntimeLogFile struct {
|
||||
Name string `json:"name"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
ModifiedAt time.Time `json:"modified_at"`
|
||||
}
|
||||
|
||||
// RuntimeLogTail is a tail/preview fragment of a runtime log file.
|
||||
type RuntimeLogTail struct {
|
||||
Filename string `json:"filename"`
|
||||
Content string `json:"content"`
|
||||
Truncated bool `json:"truncated"`
|
||||
LinesReturned int `json:"lines_returned"`
|
||||
}
|
||||
|
||||
// RuntimeLogCleanupAudit is a persisted cleanup operation log entry.
|
||||
type RuntimeLogCleanupAudit struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
ActorPrefix string `json:"actor_prefix"`
|
||||
Filename string `json:"filename"`
|
||||
Action string `json:"action"`
|
||||
SizeBefore int64 `json:"size_before"`
|
||||
SizeAfter *int64 `json:"size_after,omitempty"`
|
||||
Detail map[string]any `json:"detail,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ValidRuntimeLogCleanupAction reports whether action is truncate or delete.
|
||||
func ValidRuntimeLogCleanupAction(action string) bool {
|
||||
switch strings.TrimSpace(action) {
|
||||
case RuntimeLogCleanupTruncate, RuntimeLogCleanupDelete:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user