diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-20 21:06:35 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-20 21:06:35 +0200 |
| commit | 59e86c9fd39308bc6b632e02ecf4d37265dabc91 (patch) | |
| tree | 20e865fa680df4cff10056fa238ad07d7c58f458 /cmd | |
| parent | 0441f47fd585812b9c1f98b8a3dbeee03aa70b03 (diff) | |
Add Go implementation (goprecords)v0.1.0
- cmd/goprecords: CLI with import, query, report-from-files, test
- internal/goprecords: types, order, aggregate, db, report
- internal/version: version constant (0.1.0)
- SQLite import (repeatable: clears record table then inserts)
- Magefile: Build, Test, Install, Uninstall
- Table-driven unit tests; comparison script vs Raku guprecords
- .gitignore: *.db, /goprecords binary
Co-authored-by: Cursor <cursoragent@cursor.com>
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/goprecords/main.go | 312 |
1 files changed, 312 insertions, 0 deletions
diff --git a/cmd/goprecords/main.go b/cmd/goprecords/main.go new file mode 100644 index 0000000..7807d1f --- /dev/null +++ b/cmd/goprecords/main.go @@ -0,0 +1,312 @@ +// Program goprecords generates uptime reports from uptimed record files or a SQLite database. +package main + +import ( + "context" + "flag" + "fmt" + "os" + + "github.com/goprecords/internal/goprecords" + "github.com/goprecords/internal/version" +) + +const defaultDB = "goprecords.db" + +func main() { + for _, arg := range os.Args[1:] { + if arg == "-version" || arg == "--version" { + fmt.Println(version.Version) + os.Exit(0) + } + } + + if len(os.Args) >= 2 { + switch os.Args[1] { + case "import": + runImport(os.Args[2:]) + return + case "query": + runQuery(os.Args[2:]) + return + case "test": + runTests() + return + } + } + + runReportFromFiles(os.Args[1:]) +} + +func runImport(args []string) { + fs := flag.NewFlagSet("import", flag.ExitOnError) + statsDir := fs.String("stats-dir", "", "Directory containing .records files (required)") + dbPath := fs.String("db", defaultDB, "SQLite database path") + fs.Parse(args) + + if *statsDir == "" { + fmt.Fprintln(os.Stderr, "import: missing required flag: -stats-dir") + fs.Usage() + os.Exit(1) + } + db, err := goprecords.OpenDB(*dbPath) + if err != nil { + fmt.Fprintln(os.Stderr, "open db:", err) + os.Exit(1) + } + defer db.Close() + ctx := context.Background() + if err := goprecords.CreateSchema(ctx, db); err != nil { + fmt.Fprintln(os.Stderr, "schema:", err) + os.Exit(1) + } + if err := goprecords.ImportFromDir(ctx, db, *statsDir); err != nil { + fmt.Fprintln(os.Stderr, "import:", err) + os.Exit(1) + } + fmt.Fprintf(os.Stderr, "imported %s into %s\n", *statsDir, *dbPath) +} + +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") + fs.Parse(args) + + db, err := goprecords.OpenDB(*dbPath) + if err != nil { + fmt.Fprintln(os.Stderr, "open db:", err) + os.Exit(1) + } + defer db.Close() + + ctx := context.Background() + aggregates, err := goprecords.LoadAggregates(ctx, db) + if err != nil { + fmt.Fprintln(os.Stderr, "load:", err) + 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) + if 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) { + 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") + fs.Parse(args) + + if *statsDir == "" { + fmt.Fprintln(os.Stderr, "missing required flag: -stats-dir") + fs.Usage() + 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) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + ctx := context.Background() + aggr := goprecords.NewAggregator(*statsDir) + aggregates, err := aggr.Aggregate(ctx) + if 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 runTests() { + ctx := context.Background() + aggr := goprecords.NewAggregator("./fixtures") + aggregates, err := aggr.Aggregate(ctx) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + limit := uint(3) + categories := []goprecords.Category{goprecords.CategoryHost, goprecords.CategoryKernel, goprecords.CategoryKernelMajor, goprecords.CategoryKernelName} + metrics := []goprecords.Metric{goprecords.MetricBoots, goprecords.MetricUptime, goprecords.MetricScore, goprecords.MetricDowntime, goprecords.MetricLifespan} + formats := []goprecords.OutputFormat{goprecords.FormatPlaintext, goprecords.FormatMarkdown, goprecords.FormatGemtext} + failed := 0 + for _, cat := range categories { + for _, met := range metrics { + if cat != goprecords.CategoryHost && (met == goprecords.MetricDowntime || met == goprecords.MetricLifespan) { + continue + } + for _, outFmt := range formats { + var report string + if cat == goprecords.CategoryHost { + report = goprecords.NewHostReporter(aggregates, limit, met, outFmt, 1).Report() + } else { + report = goprecords.NewReporter(aggregates, cat, limit, met, outFmt, 1).Report() + } + expectedPath := fmt.Sprintf("./fixtures/%s.%s.%s.expected", cat, met, outFmt) + expected, err := os.ReadFile(expectedPath) + if err != nil { + fmt.Printf("FAIL: read %s: %v\n", expectedPath, err) + failed++ + continue + } + if report != string(expected) { + fmt.Printf("FAIL: %s\n--- got:\n%s--- expected:\n%s\n", expectedPath, report, string(expected)) + failed++ + } + } + } + } + if _, err := goprecords.ParseStatsOrder("Host:Uptime,Host:Boots"); err != nil { + fmt.Printf("FAIL: parse Host:Uptime,Host:Boots: %v\n", err) + failed++ + } + merged, _ := goprecords.StatsOrderList("Host:Uptime") + if len(merged) == 0 || merged[0].Category != goprecords.CategoryHost || merged[0].Metric != goprecords.MetricUptime { + fmt.Printf("FAIL: stats-order custom first entry\n") + failed++ + } + for _, bad := range []string{"Host", "Bad:Uptime", "Kernel:Downtime", "Host:Nope"} { + if _, err := goprecords.ParseStatsOrder(bad); err == nil { + fmt.Printf("FAIL: parse %q should error\n", bad) + failed++ + } + } + tmpDB := "./fixtures/test_import.db" + os.Remove(tmpDB) + db, err := goprecords.OpenDB(tmpDB) + if err != nil { + fmt.Printf("FAIL: open tmp db: %v\n", err) + failed++ + } else { + goprecords.CreateSchema(ctx, db) + if err := goprecords.ImportFromDir(ctx, db, "./fixtures"); err != nil { + fmt.Printf("FAIL: import: %v\n", err) + failed++ + } else { + aggFromDB, err := goprecords.LoadAggregates(ctx, db) + if err != nil { + fmt.Printf("FAIL: load: %v\n", err) + failed++ + } else { + reportFromDB := goprecords.NewHostReporter(aggFromDB, limit, goprecords.MetricUptime, goprecords.FormatPlaintext, 1).Report() + reportFromMem := goprecords.NewHostReporter(aggregates, limit, goprecords.MetricUptime, goprecords.FormatPlaintext, 1).Report() + if reportFromDB != reportFromMem { + fmt.Printf("FAIL: import/query report differs from in-memory\n--- from DB:\n%s--- from memory:\n%s\n", reportFromDB, reportFromMem) + failed++ + } + } + } + db.Close() + os.Remove(tmpDB) + } + if failed > 0 { + os.Exit(1) + } + fmt.Println("ok") +} |
