Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme. Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes geht in keine Bewertung als "gut" ein. Umfang (Phasen 0-23): - Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll, Katalogaufbau allein aus den Manifesten — ohne Datenbank - Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck - Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell) - Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive - Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit - Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center, Ransomware-Heuristik (meldet, handelt nie) - Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing - Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository - Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64 Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs. Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md und docs/release-candidate.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
174 lines
5.9 KiB
Go
174 lines
5.9 KiB
Go
// Package logging stellt das strukturierte Logging für alle Syncova-Dienste bereit.
|
|
//
|
|
// Jeder Eintrag trägt laut PROMPT.md §49 mindestens Zeitstempel, Level, Service,
|
|
// Komponente, Operation und Correlation ID. Secrets dürfen niemals im Log landen
|
|
// (PROMPT.md §12), weshalb dieses Paket bekannte Secret-Felder aktiv redigiert.
|
|
package logging
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log/slog"
|
|
"strings"
|
|
)
|
|
|
|
// Feldnamen des strukturierten Logs. Sie sind zentral definiert, damit alle
|
|
// Dienste dieselben Schlüssel verwenden und Logs auswertbar bleiben.
|
|
const (
|
|
// FieldService benennt den Dienst (z. B. syncova-api).
|
|
FieldService = "service"
|
|
// FieldComponent benennt die Komponente innerhalb des Dienstes (z. B. repository).
|
|
FieldComponent = "component"
|
|
// FieldOperation benennt die konkrete Operation (z. B. backup.commit).
|
|
FieldOperation = "operation"
|
|
// FieldCorrelationID verknüpft alle Logeinträge einer Ende-zu-Ende-Operation (PROMPT.md §50).
|
|
FieldCorrelationID = "correlation_id"
|
|
// FieldJobID benennt den zugehörigen Backup-Job.
|
|
FieldJobID = "job_id"
|
|
// FieldTaskID benennt die zugehörige Teilaufgabe.
|
|
FieldTaskID = "task_id"
|
|
// FieldErrorCode trägt den klassifizierten Fehlercode (PROMPT.md §51).
|
|
FieldErrorCode = "error_code"
|
|
)
|
|
|
|
// redactedPlaceholder ersetzt den Wert eines als geheim erkannten Feldes.
|
|
const redactedPlaceholder = "[REDACTED]"
|
|
|
|
// secretFieldNames listet Feldnamen, deren Werte niemals im Klartext geloggt
|
|
// werden dürfen. Der Abgleich erfolgt als Teilstring-Suche auf dem kleingeschriebenen
|
|
// Schlüssel, damit auch Varianten wie "db_password" oder "agentToken" erfasst werden.
|
|
var secretFieldNames = []string{
|
|
"password",
|
|
"passwort",
|
|
"secret",
|
|
"token",
|
|
"credential",
|
|
"authorization",
|
|
"api_key",
|
|
"apikey",
|
|
"private_key",
|
|
"privatekey",
|
|
"encryption_key",
|
|
"passphrase",
|
|
"dsn",
|
|
"connection_string",
|
|
"recovery_code",
|
|
}
|
|
|
|
// contextKey ist ein privater Typ für Context-Schlüssel dieses Pakets.
|
|
// Ein eigener Typ verhindert Kollisionen mit Schlüsseln anderer Pakete.
|
|
type contextKey string
|
|
|
|
// correlationIDContextKey speichert die Correlation ID im Request-Context.
|
|
const correlationIDContextKey contextKey = "syncova.correlation_id"
|
|
|
|
// Options steuert den Aufbau des Loggers.
|
|
type Options struct {
|
|
// ServiceName erscheint in jedem Logeintrag als Dienstkennung.
|
|
ServiceName string
|
|
// Level ist der minimale Log-Level (debug, info, warn, error).
|
|
Level string
|
|
// Format ist "json" oder "text".
|
|
Format string
|
|
}
|
|
|
|
// New erzeugt einen Logger, der in die übergebene Senke schreibt.
|
|
//
|
|
// Der zurückgegebene Logger redigiert Secret-Felder automatisch und ergänzt
|
|
// jeden Eintrag um den Dienstnamen.
|
|
func New(outputWriter io.Writer, loggerOptions Options) *slog.Logger {
|
|
handlerOptions := &slog.HandlerOptions{
|
|
Level: parseLevel(loggerOptions.Level),
|
|
// ReplaceAttr greift für jedes Attribut und ist damit der einzige Ort,
|
|
// an dem eine Redaction zuverlässig nicht vergessen werden kann.
|
|
ReplaceAttr: redactSecretAttribute,
|
|
}
|
|
|
|
var logHandler slog.Handler
|
|
if strings.EqualFold(loggerOptions.Format, "text") {
|
|
logHandler = slog.NewTextHandler(outputWriter, handlerOptions)
|
|
} else {
|
|
logHandler = slog.NewJSONHandler(outputWriter, handlerOptions)
|
|
}
|
|
|
|
return slog.New(logHandler).With(slog.String(FieldService, loggerOptions.ServiceName))
|
|
}
|
|
|
|
// parseLevel übersetzt den konfigurierten Level-Namen in einen slog.Level.
|
|
func parseLevel(levelName string) slog.Level {
|
|
switch strings.ToLower(strings.TrimSpace(levelName)) {
|
|
case "debug":
|
|
return slog.LevelDebug
|
|
case "warn":
|
|
return slog.LevelWarn
|
|
case "error":
|
|
return slog.LevelError
|
|
default:
|
|
// Unbekannte Werte werden von der Konfiguration bereits abgelehnt;
|
|
// hier ist Info der sichere Rückfall.
|
|
return slog.LevelInfo
|
|
}
|
|
}
|
|
|
|
// redactSecretAttribute ersetzt die Werte geheim benannter Attribute.
|
|
func redactSecretAttribute(_ []string, logAttribute slog.Attr) slog.Attr {
|
|
if IsSecretFieldName(logAttribute.Key) {
|
|
return slog.String(logAttribute.Key, redactedPlaceholder)
|
|
}
|
|
|
|
return logAttribute
|
|
}
|
|
|
|
// IsSecretFieldName meldet, ob ein Feldname auf einen geheimen Wert hindeutet.
|
|
// Die Funktion ist exportiert, damit auch API- und Audit-Schichten dieselbe
|
|
// Bewertung verwenden können.
|
|
func IsSecretFieldName(fieldName string) bool {
|
|
lowercaseFieldName := strings.ToLower(fieldName)
|
|
for _, secretFieldName := range secretFieldNames {
|
|
if strings.Contains(lowercaseFieldName, secretFieldName) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// ContextWithCorrelationID hinterlegt die Correlation ID im Context.
|
|
func ContextWithCorrelationID(parentContext context.Context, correlationID string) context.Context {
|
|
return context.WithValue(parentContext, correlationIDContextKey, correlationID)
|
|
}
|
|
|
|
// CorrelationIDFromContext liest die Correlation ID aus dem Context.
|
|
// Fehlt sie, ist der zweite Rückgabewert false.
|
|
func CorrelationIDFromContext(currentContext context.Context) (string, bool) {
|
|
correlationID, isPresent := currentContext.Value(correlationIDContextKey).(string)
|
|
if !isPresent || correlationID == "" {
|
|
return "", false
|
|
}
|
|
|
|
return correlationID, true
|
|
}
|
|
|
|
// WithContext ergänzt einen Logger um die Correlation ID des Contexts.
|
|
//
|
|
// So bleibt eine Operation über Dienstgrenzen hinweg nachvollziehbar, ohne dass
|
|
// jede Aufrufstelle die ID manuell durchreichen muss.
|
|
func WithContext(currentContext context.Context, baseLogger *slog.Logger) *slog.Logger {
|
|
correlationID, isPresent := CorrelationIDFromContext(currentContext)
|
|
if !isPresent {
|
|
return baseLogger
|
|
}
|
|
|
|
return baseLogger.With(slog.String(FieldCorrelationID, correlationID))
|
|
}
|
|
|
|
// WithComponent kennzeichnet alle folgenden Einträge mit einer Komponente.
|
|
func WithComponent(baseLogger *slog.Logger, componentName string) *slog.Logger {
|
|
return baseLogger.With(slog.String(FieldComponent, componentName))
|
|
}
|
|
|
|
// WithOperation kennzeichnet alle folgenden Einträge mit einer Operation.
|
|
func WithOperation(baseLogger *slog.Logger, operationName string) *slog.Logger {
|
|
return baseLogger.With(slog.String(FieldOperation, operationName))
|
|
}
|