summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/goprecords/main.go105
-rw-r--r--internal/goprecords/report.go98
-rw-r--r--internal/goprecords/report_test.go79
3 files changed, 183 insertions, 99 deletions
diff --git a/cmd/goprecords/main.go b/cmd/goprecords/main.go
index c608f78..1bec690 100644
--- a/cmd/goprecords/main.go
+++ b/cmd/goprecords/main.go
@@ -68,13 +68,7 @@ func runImport(args []string) {
func runQuery(args []string) {
fs := flag.NewFlagSet("query", flag.ExitOnError)
dbPath := fs.String("db", defaultDB, "SQLite database path")
- 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")
+ rf := goprecords.RegisterReportFlags(fs)
fs.Parse(args)
db, err := goprecords.OpenDB(*dbPath)
@@ -91,56 +85,15 @@ func runQuery(args []string) {
os.Exit(1)
}
- cat, err := goprecords.ParseCategory(*category)
+ cfg, err := rf.Parse()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
- met, err := goprecords.ParseMetric(*metric)
- if err != nil {
- fmt.Fprintln(os.Stderr, err)
- os.Exit(1)
- }
- outFmt, err := goprecords.ParseOutputFormat(*outputFormat)
- if err != nil {
+ if err := goprecords.WriteReports(os.Stdout, aggregates, cfg); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
-
- if !*all {
- if cat != goprecords.CategoryHost && (met == goprecords.MetricDowntime || met == goprecords.MetricLifespan) {
- fmt.Fprintf(os.Stderr, "Category %s only supports: Boots, Uptime, Score\n", *category)
- os.Exit(1)
- }
- if cat == goprecords.CategoryHost {
- os.Stdout.WriteString(goprecords.NewHostReporter(aggregates, *limit, met, outFmt, 1).Report())
- } else {
- os.Stdout.WriteString(goprecords.NewReporter(aggregates, cat, *limit, met, outFmt, 1).Report())
- }
- return
- }
-
- order, err := goprecords.StatsOrderList(*statsOrder)
- if err != nil {
- fmt.Fprintln(os.Stderr, err)
- os.Exit(1)
- }
- headerIndent := uint(2)
- for _, pair := range order {
- c, m := pair.Category, pair.Metric
- if !*includeKernel && c == goprecords.CategoryKernel {
- continue
- }
- if c != goprecords.CategoryHost && (m == goprecords.MetricDowntime || m == goprecords.MetricLifespan) {
- continue
- }
- if c == goprecords.CategoryHost {
- os.Stdout.WriteString(goprecords.NewHostReporter(aggregates, *limit, m, outFmt, headerIndent).Report())
- } else {
- os.Stdout.WriteString(goprecords.NewReporter(aggregates, c, *limit, m, outFmt, headerIndent).Report())
- }
- os.Stdout.WriteString("\n")
- }
}
func runReportFromFiles(args []string) {
@@ -149,13 +102,7 @@ func runReportFromFiles(args []string) {
}
fs := flag.NewFlagSet("goprecords", flag.ExitOnError)
statsDir := fs.String("stats-dir", "", "The uptimed raw record input dir (required)")
- 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")
+ rf := goprecords.RegisterReportFlags(fs)
fs.Parse(args)
if *statsDir == "" {
@@ -164,17 +111,7 @@ func runReportFromFiles(args []string) {
os.Exit(1)
}
- cat, err := goprecords.ParseCategory(*category)
- if err != nil {
- fmt.Fprintln(os.Stderr, err)
- os.Exit(1)
- }
- met, err := goprecords.ParseMetric(*metric)
- if err != nil {
- fmt.Fprintln(os.Stderr, err)
- os.Exit(1)
- }
- outFmt, err := goprecords.ParseOutputFormat(*outputFormat)
+ cfg, err := rf.Parse()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
@@ -188,40 +125,10 @@ func runReportFromFiles(args []string) {
os.Exit(1)
}
- if !*all {
- if cat != goprecords.CategoryHost && (met == goprecords.MetricDowntime || met == goprecords.MetricLifespan) {
- fmt.Fprintf(os.Stderr, "Category %s only supports: Boots, Uptime, Score\n", *category)
- os.Exit(1)
- }
- if cat == goprecords.CategoryHost {
- os.Stdout.WriteString(goprecords.NewHostReporter(aggregates, *limit, met, outFmt, 1).Report())
- } else {
- os.Stdout.WriteString(goprecords.NewReporter(aggregates, cat, *limit, met, outFmt, 1).Report())
- }
- return
- }
-
- order, err := goprecords.StatsOrderList(*statsOrder)
- if err != nil {
+ if err := goprecords.WriteReports(os.Stdout, aggregates, cfg); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
- headerIndent := uint(2)
- for _, pair := range order {
- c, m := pair.Category, pair.Metric
- if !*includeKernel && c == goprecords.CategoryKernel {
- continue
- }
- if c != goprecords.CategoryHost && (m == goprecords.MetricDowntime || m == goprecords.MetricLifespan) {
- continue
- }
- if c == goprecords.CategoryHost {
- os.Stdout.WriteString(goprecords.NewHostReporter(aggregates, *limit, m, outFmt, headerIndent).Report())
- } else {
- os.Stdout.WriteString(goprecords.NewReporter(aggregates, c, *limit, m, outFmt, headerIndent).Report())
- }
- os.Stdout.WriteString("\n")
- }
}
func runTests() {
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: