package reports import ( "fmt" "strings" "time" ) // ReportType ist die Art eines Berichts. type ReportType string const ( // TypeDailyBackup ist der Tagesbericht der Sicherungen. TypeDailyBackup ReportType = "daily_backup" // TypeWeeklyBackup ist der Wochenbericht der Sicherungen. TypeWeeklyBackup ReportType = "weekly_backup" // TypeMonthlyBackup ist der Monatsbericht der Sicherungen. TypeMonthlyBackup ReportType = "monthly_backup" // TypeFailedBackup listet die gescheiterten Laeufe. TypeFailedBackup ReportType = "failed_backup" // TypeRepositoryCapacity beschreibt Belegung und Zustand der Repositories. TypeRepositoryCapacity ReportType = "repository_capacity" // TypeRecovery beschreibt die Wiederherstellungen. TypeRecovery ReportType = "recovery" // TypeSecurity ist der Sicherheitsbericht. TypeSecurity ReportType = "security" // TypeCompliance liefert technische Angaben fuer Pruefungen. TypeCompliance ReportType = "compliance" // TypeRecoveryObjectives vergleicht RPO- und RTO-Vorgaben mit der Wirklichkeit. TypeRecoveryObjectives ReportType = "rpo_rto" ) // Format ist ein Ausgabeformat. type Format string const ( // FormatJSON ist die maschinenlesbare Ausgabe. FormatJSON Format = "json" // FormatCSV ist die Ausgabe fuer Tabellenkalkulationen. FormatCSV Format = "csv" // FormatPDF ist die Ausgabe zum Ablegen und Weiterreichen. FormatPDF Format = "pdf" ) // ContentType liefert den MIME-Typ eines Formats. func (format Format) ContentType() string { switch format { case FormatCSV: return "text/csv; charset=utf-8" case FormatPDF: return "application/pdf" default: return "application/json; charset=utf-8" } } // FileExtension liefert die Dateiendung eines Formats. func (format Format) FileExtension() string { return string(format) } // ParseFormat liest ein Ausgabeformat. func ParseFormat(rawFormat string) (Format, error) { switch Format(strings.ToLower(strings.TrimSpace(rawFormat))) { case FormatJSON, "": return FormatJSON, nil case FormatCSV: return FormatCSV, nil case FormatPDF: return FormatPDF, nil default: return "", fmt.Errorf("das format %q ist unbekannt (erlaubt: json, csv, pdf)", rawFormat) } } // PeriodKind beschreibt, wie der Zeitraum eines Berichts zustande kommt. type PeriodKind string const ( // PeriodRange verlangt einen vom Aufrufer gewaehlten Zeitraum. PeriodRange PeriodKind = "range" // PeriodPointInTime beschreibt den Zustand jetzt, nicht einen Verlauf. // // Der Unterschied ist keine Formalie: Ein Bericht ueber die Belegung der // Repositories „vom letzten Dienstag" kann es nicht geben — die Belegung // wird nicht historisiert. Wer nach einem Zeitraum fragt, bekaeme sonst // einen Zustandsbericht mit falscher Ueberschrift. PeriodPointInTime PeriodKind = "point_in_time" ) // Definition beschreibt eine Reportart. type Definition struct { // Type ist der maschinenlesbare Bezeichner. Type ReportType `json:"type"` // Title ist die Bezeichnung. Title string `json:"title"` // Description erklaert, welche Frage der Bericht beantwortet. Description string `json:"description"` // PeriodKind beschreibt die Art des Zeitbezugs. PeriodKind PeriodKind `json:"period_kind"` // DefaultPeriod ist der Zeitraum ohne ausdrueckliche Angabe. DefaultPeriod time.Duration `json:"-"` // DefaultPeriodLabel beschreibt den Standardzeitraum. DefaultPeriodLabel string `json:"default_period,omitempty"` // Formats sind die verfuegbaren Ausgabeformate. Formats []Format `json:"formats"` } // allFormats sind die drei Ausgaben, die jeder Bericht beherrscht. // // Es gibt keinen Bericht, der nur in einem Format existiert: Ein Format, das // bei sieben von neun Berichten fehlt, ist im Menue nicht erklaerbar. var allFormats = []Format{FormatJSON, FormatCSV, FormatPDF} // Definitions liefert alle Reportarten in Anzeigereihenfolge. func Definitions() []Definition { return []Definition{ { Type: TypeDailyBackup, Title: "Tagesbericht Sicherungen", Description: "Alle Sicherungsläufe eines Tages mit Ergebnis, Datenmenge und Dauer. Beantwortet die Frage, die morgens zuerst gestellt wird: Ist heute Nacht alles durchgelaufen?", PeriodKind: PeriodRange, DefaultPeriod: 24 * time.Hour, DefaultPeriodLabel: "letzte 24 Stunden", Formats: allFormats, }, { Type: TypeWeeklyBackup, Title: "Wochenbericht Sicherungen", Description: "Die Läufe einer Woche, zusätzlich nach Tagen aufgeschlüsselt. Zeigt Muster, die im Tagesbericht untergehen — etwa einen Auftrag, der immer freitags scheitert.", PeriodKind: PeriodRange, DefaultPeriod: 7 * 24 * time.Hour, DefaultPeriodLabel: "letzte 7 Tage", Formats: allFormats, }, { Type: TypeMonthlyBackup, Title: "Monatsbericht Sicherungen", Description: "Die Läufe eines Monats mit Aufschlüsselung je Auftrag. Die übliche Grundlage für eine Berichterstattung nach oben.", PeriodKind: PeriodRange, DefaultPeriod: 30 * 24 * time.Hour, DefaultPeriodLabel: "letzte 30 Tage", Formats: allFormats, }, { Type: TypeFailedBackup, Title: "Bericht gescheiterte Sicherungen", Description: "Jeder gescheiterte Lauf und jeder Teilfehler einzeln, mit Fehlerklasse und Fehlercode. Teilfehler stehen ausdrücklich mit darin: Sie werden nicht wiederholt und verschwinden sonst aus dem Blick.", PeriodKind: PeriodRange, DefaultPeriod: 7 * 24 * time.Hour, DefaultPeriodLabel: "letzte 7 Tage", Formats: allFormats, }, { Type: TypeRepositoryCapacity, Title: "Bericht Repository-Kapazität", Description: "Belegung, Zustand und Löschschutz aller Repositories. Ein Zustandsbericht, kein Verlauf — die Belegung wird nicht historisiert.", PeriodKind: PeriodPointInTime, Formats: allFormats, }, { Type: TypeRecovery, Title: "Wiederherstellungsbericht", Description: "Alle Wiederherstellungen eines Zeitraums mit Dauer, Datenmenge und Ergebnis. Die einzige Quelle für tatsächlich gemessene Wiederherstellungszeiten.", PeriodKind: PeriodRange, DefaultPeriod: 30 * 24 * time.Hour, DefaultPeriodLabel: "letzte 30 Tage", Formats: allFormats, }, { Type: TypeSecurity, Title: "Sicherheitsbericht", Description: "Die Sicherheitsbewertung mit allen Befunden aus dem Security Center. Ein Zustandsbericht: Er beschreibt die Lage jetzt.", PeriodKind: PeriodPointInTime, Formats: allFormats, }, { Type: TypeCompliance, Title: "Bericht für Prüfungen", Description: "Technische Angaben, die bei einer Prüfung verlangt werden: Verschlüsselungsgrad, Löschschutz, zweiter Faktor, Prüfquote, Protokollierung. Dies ist keine Zertifizierung und ersetzt keine.", PeriodKind: PeriodPointInTime, Formats: allFormats, }, { Type: TypeRecoveryObjectives, Title: "Bericht RPO und RTO", Description: "Je Auftrag die zugesagte Wiederherstellungslage gegen die gemessene. RPO wird laufend gemessen; RTO nur dort, wo eine Wiederherstellung tatsächlich stattgefunden hat.", PeriodKind: PeriodPointInTime, Formats: allFormats, }, } } // FindDefinition sucht eine Reportart. func FindDefinition(reportType ReportType) (Definition, bool) { for _, definition := range Definitions() { if definition.Type == reportType { return definition, true } } return Definition{}, false } // ParseType liest eine Reportart. func ParseType(rawType string) (ReportType, error) { reportType := ReportType(strings.ToLower(strings.TrimSpace(rawType))) if _, found := FindDefinition(reportType); !found { return "", fmt.Errorf("die reportart %q ist unbekannt", rawType) } return reportType, nil } // ResolvePeriod bestimmt den Zeitraum eines Berichts. // // Ein Zustandsbericht bekommt den Erzeugungszeitpunkt als beide Grenzen: Ihm // einen Zeitraum zu geben, den er nicht auswertet, waere die freundlichste Art // zu luegen — der Leser glaubte, eine Auswahl getroffen zu haben. func ResolvePeriod(definition Definition, requestedFrom, requestedTo *time.Time, now time.Time) (time.Time, time.Time, error) { if definition.PeriodKind == PeriodPointInTime { return now, now, nil } periodTo := now if requestedTo != nil { periodTo = *requestedTo } periodFrom := periodTo.Add(-definition.DefaultPeriod) if requestedFrom != nil { periodFrom = *requestedFrom } if !periodFrom.Before(periodTo) { return time.Time{}, time.Time{}, fmt.Errorf("der beginn des zeitraums liegt nicht vor seinem ende") } return periodFrom.UTC(), periodTo.UTC(), nil }