diff options
| -rw-r--r-- | internal/goprecords/report.go | 285 | ||||
| -rw-r--r-- | internal/goprecords/report_config.go | 151 | ||||
| -rw-r--r-- | internal/goprecords/report_format.go | 146 |
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() +} |
