package reports import ( "context" "fmt" "time" "github.com/jackc/pgx/v5/pgxpool" "github.com/syncova/syncova/packages/security" ) // Generator erzeugt Berichte aus dem Bestand der Control Plane. // // Berichte werden **nicht gespeichert**. Ein abgelegter Bericht veraltet mit // jedem Tag, ohne dass sich etwas an ihm aendert — dieselbe Ueberlegung wie bei // der Recovery Assurance (Phase 10) und der Sicherheitsbewertung (Phase 15). // Wer einen Bericht aufbewahren will, laedt ihn herunter; dann traegt die Datei // ihren Erzeugungszeitpunkt bei sich. type Generator struct { // connectionPool ist der Datenbankpool der Control Plane. connectionPool *pgxpool.Pool // securityInspector beurteilt die Sicherheitslage. // // Er darf nil sein; dann melden die beiden Berichte, die ihn brauchen, // einen Fehler, statt eine erfundene Lage zu beschreiben. securityInspector *security.Inspector } // NewGenerator erzeugt den Berichtsersteller. func NewGenerator(connectionPool *pgxpool.Pool, securityInspector *security.Inspector) *Generator { return &Generator{connectionPool: connectionPool, securityInspector: securityInspector} } // Generate erzeugt einen Bericht. func (generator *Generator) Generate(generateContext context.Context, reportType ReportType, periodFrom, periodTo time.Time, generatedBy string) (*Report, error) { definition, found := FindDefinition(reportType) if !found { return nil, fmt.Errorf("die reportart %q ist unbekannt", reportType) } report := newReport(definition, periodFrom, periodTo, generatedBy) var buildError error switch reportType { case TypeDailyBackup, TypeWeeklyBackup, TypeMonthlyBackup: buildError = generator.buildBackupReport(generateContext, report, reportType) case TypeFailedBackup: buildError = generator.buildFailedBackupReport(generateContext, report) case TypeRepositoryCapacity: buildError = generator.buildRepositoryReport(generateContext, report) case TypeRecovery: buildError = generator.buildRecoveryReport(generateContext, report) case TypeSecurity: buildError = generator.buildSecurityReport(generateContext, report) case TypeCompliance: buildError = generator.buildComplianceReport(generateContext, report) case TypeRecoveryObjectives: buildError = generator.buildObjectivesReport(generateContext, report) default: buildError = fmt.Errorf("fuer die reportart %q gibt es keine erzeugung", reportType) } if buildError != nil { return nil, buildError } return report, nil } // runStatistics fasst die Laeufe eines Zeitraums zusammen. type runStatistics struct { // TotalRuns ist die Zahl abgeschlossener Laeufe. TotalRuns int64 // SucceededRuns ist die Zahl erfolgreicher Laeufe. SucceededRuns int64 // PartialRuns ist die Zahl der Teilfehler. // // Getrennt gefuehrt, nicht zu den Erfolgen geschlagen: Ein Lauf mit // uebergangenen Objekten ist keine Sicherung, auf die man sich verlassen // kann (verbindliche Regel 1). PartialRuns int64 // FailedRuns ist die Zahl gescheiterter Laeufe. FailedRuns int64 // RunningRuns ist die Zahl noch laufender Vorgaenge. RunningRuns int64 // BytesProcessed ist die gelesene Datenmenge. BytesProcessed int64 // BytesWritten ist die abgelegte Datenmenge. BytesWritten int64 // FilesProcessed ist die Zahl erfasster Objekte. FilesProcessed int64 // FilesSkipped ist die Zahl uebergangener Objekte. FilesSkipped int64 // TotalDurationSeconds ist die Summe der Laufzeiten. TotalDurationSeconds float64 // LongestDurationSeconds ist die laengste Laufzeit. LongestDurationSeconds float64 } // FinishedRuns zaehlt die abgeschlossenen Laeufe. // // Laufende Vorgaenge zaehlen nicht mit: Sie haben noch kein Ergebnis, und sie in // den Nenner einer Erfolgsquote zu nehmen zoege diese ohne Grund nach unten. func (statistics runStatistics) FinishedRuns() int64 { return statistics.SucceededRuns + statistics.PartialRuns + statistics.FailedRuns } // loadRunStatistics liest die Kennzahlen der Laeufe eines Zeitraums. func (generator *Generator) loadRunStatistics(loadContext context.Context, periodFrom, periodTo time.Time) (runStatistics, error) { const selectStatement = ` SELECT count(*), count(*) FILTER (WHERE status = 'succeeded'), count(*) FILTER (WHERE status = 'partial_failure'), count(*) FILTER (WHERE status = 'failed'), count(*) FILTER (WHERE status = 'running'), COALESCE(sum(bytes_processed), 0), COALESCE(sum(bytes_written), 0), COALESCE(sum(files_processed), 0), COALESCE(sum(files_skipped), 0), COALESCE(sum(EXTRACT(EPOCH FROM (completed_at - started_at))), 0), COALESCE(max(EXTRACT(EPOCH FROM (completed_at - started_at))), 0) FROM backup_job_runs WHERE created_at >= $1 AND created_at < $2` var statistics runStatistics scanError := generator.connectionPool.QueryRow(loadContext, selectStatement, periodFrom, periodTo).Scan( &statistics.TotalRuns, &statistics.SucceededRuns, &statistics.PartialRuns, &statistics.FailedRuns, &statistics.RunningRuns, &statistics.BytesProcessed, &statistics.BytesWritten, &statistics.FilesProcessed, &statistics.FilesSkipped, &statistics.TotalDurationSeconds, &statistics.LongestDurationSeconds) if scanError != nil { return runStatistics{}, fmt.Errorf("die kennzahlen der laeufe konnten nicht gelesen werden: %w", scanError) } return statistics, nil } // successRateMetric bildet die Erfolgsquote eines Zeitraums. // // **Keine Laeufe sind nicht hundert Prozent.** Ohne einen einzigen // abgeschlossenen Lauf gibt es keine Quote — und ein Bericht, der in diesem Fall // „100 %" ausweist, meldet ausgerechnet bei ausgefallener Sicherung den besten // aller Zustaende. Dieselbe Regel wie in der Uebersicht (Phase 12). func successRateMetric(statistics runStatistics) Metric { finishedRuns := statistics.FinishedRuns() if finishedRuns == 0 { return UnknownMetric("Erfolgsquote", UnitPercent, "Im gewählten Zeitraum wurde kein Lauf abgeschlossen. Ohne Lauf gibt es keine Quote.") } return KnownMetric("Erfolgsquote", float64(statistics.SucceededRuns)*100/float64(finishedRuns), UnitPercent) } // averageDurationMetric bildet die mittlere Laufzeit. func averageDurationMetric(statistics runStatistics) Metric { finishedRuns := statistics.FinishedRuns() if finishedRuns == 0 { return UnknownMetric("Mittlere Laufzeit", UnitSeconds, "Im gewählten Zeitraum wurde kein Lauf abgeschlossen.") } return KnownMetric("Mittlere Laufzeit", statistics.TotalDurationSeconds/float64(finishedRuns), UnitSeconds) } // longestDurationMetric bildet die laengste Laufzeit. // // Ohne abgeschlossenen Lauf gibt es keine laengste Laufzeit. Die Datenbank // liefert dort dank COALESCE eine Null — und eine Null neben einer als // „nicht bestimmbar" ausgewiesenen Durchschnittszeit liest sich wie eine // **Messung**: als sei ein Lauf in null Sekunden durchgelaufen. Im Nachweis // aufgefallen. func longestDurationMetric(statistics runStatistics) Metric { if statistics.FinishedRuns() == 0 { return UnknownMetric("Längste Laufzeit", UnitSeconds, "Im gewählten Zeitraum wurde kein Lauf abgeschlossen.") } return KnownMetric("Längste Laufzeit", statistics.LongestDurationSeconds, UnitSeconds) } // runStatusLabel benennt den Zustand eines Laufs. // // Ein Teilfehler heisst „TEILWEISE FEHLGESCHLAGEN", nicht „erfolgreich mit // Anmerkungen" (verbindliche Regel 1). Der Bericht ist genau der Ort, an dem // diese Unterscheidung gelesen wird. func runStatusLabel(rawStatus string) string { switch rawStatus { case "succeeded": return "erfolgreich" case "partial_failure": return "TEILWEISE FEHLGESCHLAGEN" case "failed": return "GESCHEITERT" case "running": return "läuft" case "queued": return "eingereiht" case "cancelled": return "abgebrochen" default: return rawStatus } }