summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/goprecords/report.go98
-rw-r--r--internal/goprecords/report_test.go79
2 files changed, 177 insertions, 0 deletions
diff --git a/internal/goprecords/report.go b/internal/goprecords/report.go
index 01ca570..0b4454d 100644
--- a/internal/goprecords/report.go
+++ b/internal/goprecords/report.go
@@ -1,11 +1,109 @@
package goprecords
import (
+ "flag"
"fmt"
+ "io"
"sort"
"strings"
)
+// ReportConfig holds parsed report configuration.
+type ReportConfig struct {
+ Category Category
+ Metric Metric
+ Limit uint
+ OutputFormat OutputFormat
+ All bool
+ IncludeKernel bool
+ StatsOrder string
+}
+
+// ReportFlags holds flag pointers registered on a FlagSet.
+type ReportFlags struct {
+ category *string
+ metric *string
+ limit *uint
+ outputFormat *string
+ all *bool
+ includeKernel *bool
+ statsOrder *string
+}
+
+// RegisterReportFlags registers common report flags on the given FlagSet.
+func RegisterReportFlags(fs *flag.FlagSet) *ReportFlags {
+ return &ReportFlags{
+ category: fs.String("category", "Host", "Category: Host, Kernel, KernelMajor, KernelName"),
+ metric: fs.String("metric", "Uptime", "Metric: Boots, Uptime, Score, Downtime, Lifespan"),
+ limit: fs.Uint("limit", 20, "Limit output to num of entries"),
+ outputFormat: fs.String("output-format", "Plaintext", "Output format: Plaintext, Markdown, Gemtext"),
+ all: fs.Bool("all", false, "Generate all possible stats but Kernel"),
+ includeKernel: fs.Bool("include-kernel", false, "Also include Kernel when using -all"),
+ statsOrder: fs.String("stats-order", "", "Comma-separated Category:Metric order for -all"),
+ }
+}
+
+// Parse converts flag values into a ReportConfig.
+func (rf *ReportFlags) Parse() (ReportConfig, error) {
+ cat, err := ParseCategory(*rf.category)
+ if err != nil {
+ return ReportConfig{}, err
+ }
+ met, err := ParseMetric(*rf.metric)
+ if err != nil {
+ return ReportConfig{}, err
+ }
+ outFmt, err := ParseOutputFormat(*rf.outputFormat)
+ if err != nil {
+ return ReportConfig{}, err
+ }
+ return ReportConfig{
+ Category: cat,
+ Metric: met,
+ Limit: *rf.limit,
+ OutputFormat: outFmt,
+ All: *rf.all,
+ IncludeKernel: *rf.includeKernel,
+ StatsOrder: *rf.statsOrder,
+ }, nil
+}
+
+// WriteReports renders reports to w based on the given config.
+func WriteReports(w io.Writer, aggregates *Aggregates, cfg ReportConfig) error {
+ if !cfg.All {
+ if cfg.Category != CategoryHost && (cfg.Metric == MetricDowntime || cfg.Metric == MetricLifespan) {
+ return fmt.Errorf("Category %s only supports: Boots, Uptime, Score", cfg.Category)
+ }
+ if cfg.Category == CategoryHost {
+ io.WriteString(w, NewHostReporter(aggregates, cfg.Limit, cfg.Metric, cfg.OutputFormat, 1).Report())
+ } else {
+ io.WriteString(w, NewReporter(aggregates, cfg.Category, cfg.Limit, cfg.Metric, cfg.OutputFormat, 1).Report())
+ }
+ return nil
+ }
+ order, err := StatsOrderList(cfg.StatsOrder)
+ if err != nil {
+ return err
+ }
+ headerIndent := uint(2)
+ for _, pair := range order {
+ c, m := pair.Category, pair.Metric
+ if !cfg.IncludeKernel && c == CategoryKernel {
+ continue
+ }
+ if c != CategoryHost && (m == MetricDowntime || m == MetricLifespan) {
+ continue
+ }
+ if c == CategoryHost {
+ io.WriteString(w, NewHostReporter(aggregates, cfg.Limit, m, cfg.OutputFormat, headerIndent).Report())
+ } else {
+ io.WriteString(w, NewReporter(aggregates, c, cfg.Limit, m, cfg.OutputFormat, headerIndent).Report())
+ }
+ io.WriteString(w, "\n")
+ }
+ return nil
+}
+
// Reporter builds a single report (category + metric + format).
type Reporter struct {
aggregates *Aggregates
diff --git a/internal/goprecords/report_test.go b/internal/goprecords/report_test.go
index 651e887..f6bdab9 100644
--- a/internal/goprecords/report_test.go
+++ b/internal/goprecords/report_test.go
@@ -1,6 +1,7 @@
package goprecords
import (
+ "bytes"
"strings"
"testing"
)
@@ -217,6 +218,84 @@ func TestReportLimit(t *testing.T) {
}
}
+func TestWriteReportsSingle(t *testing.T) {
+ aggs := testAggregates()
+ var buf bytes.Buffer
+ cfg := ReportConfig{
+ Category: CategoryHost,
+ Metric: MetricUptime,
+ Limit: 20,
+ OutputFormat: FormatPlaintext,
+ }
+ if err := WriteReports(&buf, aggs, cfg); err != nil {
+ t.Fatalf("WriteReports: %v", err)
+ }
+ if !strings.Contains(buf.String(), "host1") {
+ t.Error("expected output to contain host1")
+ }
+}
+
+func TestWriteReportsAll(t *testing.T) {
+ aggs := testAggregates()
+ var buf bytes.Buffer
+ cfg := ReportConfig{
+ Category: CategoryHost,
+ Metric: MetricUptime,
+ Limit: 20,
+ OutputFormat: FormatPlaintext,
+ All: true,
+ IncludeKernel: true,
+ }
+ if err := WriteReports(&buf, aggs, cfg); err != nil {
+ t.Fatalf("WriteReports: %v", err)
+ }
+ out := buf.String()
+ if !strings.Contains(out, "Uptime") {
+ t.Error("expected output to contain Uptime report")
+ }
+ if !strings.Contains(out, "Boots") {
+ t.Error("expected output to contain Boots report")
+ }
+}
+
+func TestWriteReportsInvalidMetricForCategory(t *testing.T) {
+ aggs := testAggregates()
+ var buf bytes.Buffer
+ cfg := ReportConfig{
+ Category: CategoryKernel,
+ Metric: MetricDowntime,
+ Limit: 20,
+ OutputFormat: FormatPlaintext,
+ }
+ err := WriteReports(&buf, aggs, cfg)
+ if err == nil {
+ t.Fatal("expected error for Downtime on Kernel category")
+ }
+ if !strings.Contains(err.Error(), "only supports") {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
+
+func testAggregates() *Aggregates {
+ aggs := &Aggregates{
+ Host: make(map[string]*HostAggregate),
+ Kernel: make(map[string]*Aggregate),
+ KernelMajor: make(map[string]*Aggregate),
+ KernelName: make(map[string]*Aggregate),
+ }
+ hagg := NewHostAggregate("host1", "Linux 5.10")
+ hagg.Uptime = 86400000
+ hagg.Boots = 10
+ hagg.FirstBoot = 1000
+ hagg.LastSeen = 86401000
+ aggs.Host["host1"] = hagg
+ kernel := NewAggregate("Linux 5.10")
+ kernel.Uptime = 86400000
+ kernel.Boots = 10
+ aggs.Kernel["Linux 5.10"] = kernel
+ return aggs
+}
+
func hostName(i int) string {
switch i {
case 0: