syncova-backup/packages/platform/logging/logging.go
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
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>
2026-08-17 09:10:54 +02:00

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))
}