summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/goprecords/report.go285
-rw-r--r--internal/goprecords/report_config.go151
-rw-r--r--internal/goprecords/report_format.go146
3 files changed, 297 insertions, 285 deletions
diff --git a/internal/goprecords/report.go b/internal/goprecords/report.go
index cd86fcd..7d04ce3 100644
--- a/internal/goprecords/report.go
+++ b/internal/goprecords/report.go
@@ -1,158 +1,11 @@
package goprecords
import (
- "flag"
"fmt"
- "html/template"
"io"
- "net/url"
"sort"
- "strconv"
- "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, HTML"),
- 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
-}
-
-// ParseReportQuery builds a ReportConfig from URL query parameters using the
-// same names and defaults as RegisterReportFlags (category, metric, limit,
-// output-format, all, include-kernel, stats-order). It also accepts Category,
-// Metric, and OutputFormat as alternate keys (same values as the CLI).
-func ParseReportQuery(q url.Values) (ReportConfig, error) {
- catStr := firstQuery(q, "category", "Category")
- if catStr == "" {
- catStr = "Host"
- }
- cat, err := ParseCategory(catStr)
- if err != nil {
- return ReportConfig{}, err
- }
- metStr := firstQuery(q, "metric", "Metric")
- if metStr == "" {
- metStr = "Uptime"
- }
- met, err := ParseMetric(metStr)
- if err != nil {
- return ReportConfig{}, err
- }
- limit := uint(20)
- if ls := q.Get("limit"); ls != "" {
- v, err := strconv.ParseUint(ls, 10, 32)
- if err != nil {
- return ReportConfig{}, fmt.Errorf("invalid limit %q", ls)
- }
- limit = uint(v)
- }
- outStr := firstQuery(q, "output-format", "OutputFormat")
- if outStr == "" {
- outStr = "Plaintext"
- }
- outFmt, err := ParseOutputFormat(outStr)
- if err != nil {
- return ReportConfig{}, err
- }
- all := false
- if v := q.Get("all"); v != "" {
- all, err = parseQueryBool(v)
- if err != nil {
- return ReportConfig{}, err
- }
- }
- includeKernel := false
- if v := q.Get("include-kernel"); v != "" {
- includeKernel, err = parseQueryBool(v)
- if err != nil {
- return ReportConfig{}, err
- }
- }
- return ReportConfig{
- Category: cat,
- Metric: met,
- Limit: limit,
- OutputFormat: outFmt,
- All: all,
- IncludeKernel: includeKernel,
- StatsOrder: q.Get("stats-order"),
- }, nil
-}
-
-func firstQuery(q url.Values, keys ...string) string {
- for _, k := range keys {
- if v := q.Get(k); v != "" {
- return v
- }
- }
- return ""
-}
-
-func parseQueryBool(s string) (bool, error) {
- switch strings.ToLower(strings.TrimSpace(s)) {
- case "true", "1", "yes":
- return true, nil
- case "false", "0", "no", "":
- return false, nil
- default:
- return false, fmt.Errorf("invalid boolean %q", s)
- }
-}
-
// WriteReports renders reports to w based on the given config.
func WriteReports(w io.Writer, aggregates *Aggregates, cfg ReportConfig) error {
if !cfg.All {
@@ -395,141 +248,3 @@ func (r reportBuilder) humanStrAgg(a *Aggregate) string {
return formatDuration(a.Uptime)
}
}
-
-func (r reportBuilder) formatReport(rows []tableRow, hasLastKernel bool, outputFormat OutputFormat) string {
- cW, nW, vW, lkW := r.reportWidths(rows, hasLastKernel)
- border := r.buildBorder(cW, nW, vW, lkW, hasLastKernel)
- header := r.buildReportHeader(cW, nW, vW, lkW, hasLastKernel, border, outputFormat)
- fmtStr := r.buildFormatStr(cW, nW, vW, lkW, hasLastKernel)
- body := r.buildReportBody(rows, fmtStr, hasLastKernel)
- out := header + body + border
- if outputFormat == FormatMarkdown || outputFormat == FormatGemtext {
- out += "```\n"
- }
- return out
-}
-
-func (r reportBuilder) formatReportHTML(rows []tableRow, hasLastKernel bool) string {
- cW, nW, vW, lkW := r.reportWidths(rows, hasLastKernel)
- border := r.buildBorder(cW, nW, vW, lkW, hasLastKernel)
- fmtStr := r.buildFormatStr(cW, nW, vW, lkW, hasLastKernel)
- var headRow string
- if hasLastKernel {
- headRow = fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String(), "Last Kernel")
- } else {
- headRow = fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String())
- }
- body := r.buildReportBody(rows, fmtStr, hasLastKernel)
- ascii := border + headRow + border + body + border
-
- hl := int(r.headerIndent)
- if hl < 1 {
- hl = 1
- }
- if hl > 6 {
- hl = 6
- }
- title := fmt.Sprintf("Top %d %s's by %s", r.limit, r.metric, r.category)
- desc := MetricDescription(r.metric)
-
- var b strings.Builder
- b.WriteString("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>")
- b.WriteString(template.HTMLEscapeString(title))
- b.WriteString("</title>\n</head>\n<body>\n<h")
- b.WriteString(strconv.Itoa(hl))
- b.WriteString(">")
- b.WriteString(template.HTMLEscapeString(title))
- b.WriteString("</h")
- b.WriteString(strconv.Itoa(hl))
- b.WriteString(">\n")
- if desc != "" {
- b.WriteString("<p>")
- b.WriteString(template.HTMLEscapeString(desc))
- b.WriteString("</p>\n")
- }
- b.WriteString("<pre>")
- b.WriteString(template.HTMLEscapeString(ascii))
- b.WriteString("</pre>\n</body>\n</html>\n")
- return b.String()
-}
-
-func (r reportBuilder) reportWidths(rows []tableRow, hasLastKernel bool) (countW, nameW, valueW, lastKernelW int) {
- countW = 3
- nameW = len(r.category.String())
- valueW = len(r.metric.String())
- if hasLastKernel {
- lastKernelW = len("Last Kernel")
- }
- for _, row := range rows {
- if len(row.Pos) > countW {
- countW = len(row.Pos)
- }
- if len(row.Name) > nameW {
- nameW = len(row.Name)
- }
- if len(row.Value) > valueW {
- valueW = len(row.Value)
- }
- if len(row.LastKernel) > lastKernelW {
- lastKernelW = len(row.LastKernel)
- }
- }
- return countW, nameW, valueW, lastKernelW
-}
-
-func (r reportBuilder) buildBorder(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string {
- parts := []string{
- "+" + strings.Repeat("-", 2+countW),
- "+" + strings.Repeat("-", 2+nameW),
- "+" + strings.Repeat("-", 2+valueW),
- }
- if hasLastKernel {
- parts = append(parts, "+"+strings.Repeat("-", 2+lastKernelW))
- }
- return strings.Join(parts, "") + "+\n"
-}
-
-func (r reportBuilder) buildReportHeader(countW, nameW, valueW, lastKernelW int, hasLastKernel bool, border string, outputFormat OutputFormat) string {
- var h string
- if outputFormat == FormatMarkdown || outputFormat == FormatGemtext {
- h = strings.Repeat("#", int(r.headerIndent)) + " "
- }
- h += fmt.Sprintf("Top %d %s's by %s\n\n", r.limit, r.metric, r.category)
- desc := MetricDescription(r.metric)
- lineLimit := len(border)
- if outputFormat == FormatPlaintext && lineLimit > 0 && len(desc) > lineLimit-1 {
- desc = " " + wordWrap(desc, lineLimit-1)
- }
- h += desc + "\n\n"
- if outputFormat == FormatMarkdown || outputFormat == FormatGemtext {
- h += "```\n"
- }
- h += border
- fmtStr := r.buildFormatStr(countW, nameW, valueW, lastKernelW, hasLastKernel)
- if hasLastKernel {
- h += fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String(), "Last Kernel")
- } else {
- h += fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String())
- }
- h += border
- return h
-}
-
-func (r reportBuilder) buildFormatStr(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string {
- if hasLastKernel {
- return fmt.Sprintf("| %%%ds | %%%ds | %%%ds | %%%ds |", countW, nameW, valueW, lastKernelW)
- }
- return fmt.Sprintf("| %%%ds | %%%ds | %%%ds |", countW, nameW, valueW)
-}
-
-func (r reportBuilder) buildReportBody(rows []tableRow, fmtStr string, hasLastKernel bool) string {
- var b strings.Builder
- for _, row := range rows {
- if hasLastKernel {
- b.WriteString(fmt.Sprintf(fmtStr+"\n", row.Pos, row.Name, row.Value, row.LastKernel))
- } else {
- b.WriteString(fmt.Sprintf(fmtStr+"\n", row.Pos, row.Name, row.Value))
- }
- }
- return b.String()
-}
diff --git a/internal/goprecords/report_config.go b/internal/goprecords/report_config.go
new file mode 100644
index 0000000..8cd8f8d
--- /dev/null
+++ b/internal/goprecords/report_config.go
@@ -0,0 +1,151 @@
+package goprecords
+
+import (
+ "flag"
+ "fmt"
+ "net/url"
+ "strconv"
+ "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, HTML"),
+ 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
+}
+
+// ParseReportQuery builds a ReportConfig from URL query parameters using the
+// same names and defaults as RegisterReportFlags (category, metric, limit,
+// output-format, all, include-kernel, stats-order). It also accepts Category,
+// Metric, and OutputFormat as alternate keys (same values as the CLI).
+func ParseReportQuery(q url.Values) (ReportConfig, error) {
+ catStr := firstQuery(q, "category", "Category")
+ if catStr == "" {
+ catStr = "Host"
+ }
+ cat, err := ParseCategory(catStr)
+ if err != nil {
+ return ReportConfig{}, err
+ }
+ metStr := firstQuery(q, "metric", "Metric")
+ if metStr == "" {
+ metStr = "Uptime"
+ }
+ met, err := ParseMetric(metStr)
+ if err != nil {
+ return ReportConfig{}, err
+ }
+ limit := uint(20)
+ if ls := q.Get("limit"); ls != "" {
+ v, err := strconv.ParseUint(ls, 10, 32)
+ if err != nil {
+ return ReportConfig{}, fmt.Errorf("invalid limit %q", ls)
+ }
+ limit = uint(v)
+ }
+ outStr := firstQuery(q, "output-format", "OutputFormat")
+ if outStr == "" {
+ outStr = "Plaintext"
+ }
+ outFmt, err := ParseOutputFormat(outStr)
+ if err != nil {
+ return ReportConfig{}, err
+ }
+ all := false
+ if v := q.Get("all"); v != "" {
+ all, err = parseQueryBool(v)
+ if err != nil {
+ return ReportConfig{}, err
+ }
+ }
+ includeKernel := false
+ if v := q.Get("include-kernel"); v != "" {
+ includeKernel, err = parseQueryBool(v)
+ if err != nil {
+ return ReportConfig{}, err
+ }
+ }
+ return ReportConfig{
+ Category: cat,
+ Metric: met,
+ Limit: limit,
+ OutputFormat: outFmt,
+ All: all,
+ IncludeKernel: includeKernel,
+ StatsOrder: q.Get("stats-order"),
+ }, nil
+}
+
+func firstQuery(q url.Values, keys ...string) string {
+ for _, k := range keys {
+ if v := q.Get(k); v != "" {
+ return v
+ }
+ }
+ return ""
+}
+
+func parseQueryBool(s string) (bool, error) {
+ switch strings.ToLower(strings.TrimSpace(s)) {
+ case "true", "1", "yes":
+ return true, nil
+ case "false", "0", "no", "":
+ return false, nil
+ default:
+ return false, fmt.Errorf("invalid boolean %q", s)
+ }
+}
diff --git a/internal/goprecords/report_format.go b/internal/goprecords/report_format.go
new file mode 100644
index 0000000..7ec66b3
--- /dev/null
+++ b/internal/goprecords/report_format.go
@@ -0,0 +1,146 @@
+package goprecords
+
+import (
+ "fmt"
+ "html/template"
+ "strconv"
+ "strings"
+)
+
+func (r reportBuilder) formatReport(rows []tableRow, hasLastKernel bool, outputFormat OutputFormat) string {
+ cW, nW, vW, lkW := r.reportWidths(rows, hasLastKernel)
+ border := r.buildBorder(cW, nW, vW, lkW, hasLastKernel)
+ header := r.buildReportHeader(cW, nW, vW, lkW, hasLastKernel, border, outputFormat)
+ fmtStr := r.buildFormatStr(cW, nW, vW, lkW, hasLastKernel)
+ body := r.buildReportBody(rows, fmtStr, hasLastKernel)
+ out := header + body + border
+ if outputFormat == FormatMarkdown || outputFormat == FormatGemtext {
+ out += "```\n"
+ }
+ return out
+}
+
+func (r reportBuilder) formatReportHTML(rows []tableRow, hasLastKernel bool) string {
+ cW, nW, vW, lkW := r.reportWidths(rows, hasLastKernel)
+ border := r.buildBorder(cW, nW, vW, lkW, hasLastKernel)
+ fmtStr := r.buildFormatStr(cW, nW, vW, lkW, hasLastKernel)
+ var headRow string
+ if hasLastKernel {
+ headRow = fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String(), "Last Kernel")
+ } else {
+ headRow = fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String())
+ }
+ body := r.buildReportBody(rows, fmtStr, hasLastKernel)
+ ascii := border + headRow + border + body + border
+
+ hl := int(r.headerIndent)
+ if hl < 1 {
+ hl = 1
+ }
+ if hl > 6 {
+ hl = 6
+ }
+ title := fmt.Sprintf("Top %d %s's by %s", r.limit, r.metric, r.category)
+ desc := MetricDescription(r.metric)
+
+ var b strings.Builder
+ b.WriteString("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>")
+ b.WriteString(template.HTMLEscapeString(title))
+ b.WriteString("</title>\n</head>\n<body>\n<h")
+ b.WriteString(strconv.Itoa(hl))
+ b.WriteString(">")
+ b.WriteString(template.HTMLEscapeString(title))
+ b.WriteString("</h")
+ b.WriteString(strconv.Itoa(hl))
+ b.WriteString(">\n")
+ if desc != "" {
+ b.WriteString("<p>")
+ b.WriteString(template.HTMLEscapeString(desc))
+ b.WriteString("</p>\n")
+ }
+ b.WriteString("<pre>")
+ b.WriteString(template.HTMLEscapeString(ascii))
+ b.WriteString("</pre>\n</body>\n</html>\n")
+ return b.String()
+}
+
+func (r reportBuilder) reportWidths(rows []tableRow, hasLastKernel bool) (countW, nameW, valueW, lastKernelW int) {
+ countW = 3
+ nameW = len(r.category.String())
+ valueW = len(r.metric.String())
+ if hasLastKernel {
+ lastKernelW = len("Last Kernel")
+ }
+ for _, row := range rows {
+ if len(row.Pos) > countW {
+ countW = len(row.Pos)
+ }
+ if len(row.Name) > nameW {
+ nameW = len(row.Name)
+ }
+ if len(row.Value) > valueW {
+ valueW = len(row.Value)
+ }
+ if len(row.LastKernel) > lastKernelW {
+ lastKernelW = len(row.LastKernel)
+ }
+ }
+ return countW, nameW, valueW, lastKernelW
+}
+
+func (r reportBuilder) buildBorder(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string {
+ parts := []string{
+ "+" + strings.Repeat("-", 2+countW),
+ "+" + strings.Repeat("-", 2+nameW),
+ "+" + strings.Repeat("-", 2+valueW),
+ }
+ if hasLastKernel {
+ parts = append(parts, "+"+strings.Repeat("-", 2+lastKernelW))
+ }
+ return strings.Join(parts, "") + "+\n"
+}
+
+func (r reportBuilder) buildReportHeader(countW, nameW, valueW, lastKernelW int, hasLastKernel bool, border string, outputFormat OutputFormat) string {
+ var h string
+ if outputFormat == FormatMarkdown || outputFormat == FormatGemtext {
+ h = strings.Repeat("#", int(r.headerIndent)) + " "
+ }
+ h += fmt.Sprintf("Top %d %s's by %s\n\n", r.limit, r.metric, r.category)
+ desc := MetricDescription(r.metric)
+ lineLimit := len(border)
+ if outputFormat == FormatPlaintext && lineLimit > 0 && len(desc) > lineLimit-1 {
+ desc = " " + wordWrap(desc, lineLimit-1)
+ }
+ h += desc + "\n\n"
+ if outputFormat == FormatMarkdown || outputFormat == FormatGemtext {
+ h += "```\n"
+ }
+ h += border
+ fmtStr := r.buildFormatStr(countW, nameW, valueW, lastKernelW, hasLastKernel)
+ if hasLastKernel {
+ h += fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String(), "Last Kernel")
+ } else {
+ h += fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String())
+ }
+ h += border
+ return h
+}
+
+func (r reportBuilder) buildFormatStr(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string {
+ if hasLastKernel {
+ return fmt.Sprintf("| %%%ds | %%%ds | %%%ds | %%%ds |", countW, nameW, valueW, lastKernelW)
+ }
+ return fmt.Sprintf("| %%%ds | %%%ds | %%%ds |", countW, nameW, valueW)
+}
+
+func (r reportBuilder) buildReportBody(rows []tableRow, fmtStr string, hasLastKernel bool) string {
+ var b strings.Builder
+ for _, row := range rows {
+ if hasLastKernel {
+ b.WriteString(fmt.Sprintf(fmtStr+"\n", row.Pos, row.Name, row.Value, row.LastKernel))
+ } else {
+ b.WriteString(fmt.Sprintf(fmtStr+"\n", row.Pos, row.Name, row.Value))
+ }
+ }
+ return b.String()
+}