summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-28 09:38:15 +0200
committerPaul Buetow <paul@buetow.org>2026-02-28 09:38:15 +0200
commit4521606d7b64234eb8377c3edb8b15fbc4ed97d7 (patch)
tree30e7991012dd14a0c316df712ed35307eb813f4a /internal
parenta4b234587cdeed5e1a71c3ee42d5fe35bb7b8a32 (diff)
refactor: extract report config, flags, and rendering into internal package
Add ReportConfig, ReportFlags, RegisterReportFlags, and WriteReports to the internal goprecords package. Both runQuery and runReportFromFiles now use these shared functions, eliminating duplicated flag definitions and report-rendering loops. main.go reduced from 313 to 220 lines. Task: d8f7af80-1aca-4dea-9a20-b8f95640acb7 Amp-Thread-ID: https://ampcode.com/threads/T-019ca323-dde1-73ac-97f0-cebfae5922a5 Co-authored-by: Amp <amp@ampcode.com>
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: