mirror of
https://github.com/absmach/supermq.git
synced 2026-06-23 04:00:27 +00:00
5a6e0343dc
* fx to and from Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> * change to UTC Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> * fix template pagination and address comment Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> * revert env variable Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> * fix pagination Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> * fix failing linter Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> * fix failing linter Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> --------- Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
343 lines
9.1 KiB
Go
343 lines
9.1 KiB
Go
// Copyright (c) Abstract Machines
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package reports
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/csv"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"log/slog"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
_ "time/tzdata" // Embed timezone database
|
|
|
|
pkglog "github.com/absmach/magistrala/pkg/logger"
|
|
"github.com/absmach/supermq/pkg/errors"
|
|
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
|
"github.com/absmach/supermq/pkg/transformers/senml"
|
|
)
|
|
|
|
const nanosecondThreshold = float64(10 * time.Second / time.Nanosecond)
|
|
|
|
type ReportData struct {
|
|
Title string
|
|
GeneratedTime string
|
|
GeneratedDate string
|
|
Reports []Report
|
|
Timezone string
|
|
}
|
|
|
|
func (r *report) generatePDFReport(ctx context.Context, title string, reports []Report, template ReportTemplate, timezone string) ([]byte, error) {
|
|
for i := range reports {
|
|
sort.Slice(reports[i].Messages, func(j, k int) bool {
|
|
return reports[i].Messages[j].Time < reports[i].Messages[k].Time
|
|
})
|
|
}
|
|
|
|
loc, err := resolveTimezone(timezone)
|
|
if err != nil {
|
|
r.runInfo <- pkglog.RunInfo{
|
|
Level: slog.LevelWarn,
|
|
Message: fmt.Sprintf("failed to resolve timezone '%s', falling back to UTC: %s", timezone, err),
|
|
Details: []slog.Attr{
|
|
slog.String("report_title", title),
|
|
slog.Time("time", time.Now().UTC()),
|
|
},
|
|
}
|
|
}
|
|
|
|
now := time.Now().In(loc)
|
|
displayTZ := timezone
|
|
if strings.TrimSpace(displayTZ) == "" {
|
|
displayTZ = "UTC"
|
|
}
|
|
|
|
data := ReportData{
|
|
Title: title,
|
|
GeneratedTime: now.Format("15:04:05"),
|
|
GeneratedDate: now.Format("02 Jan 2006"),
|
|
Reports: reports,
|
|
Timezone: displayTZ,
|
|
}
|
|
|
|
templateContent := r.defaultTemplate.String()
|
|
if template.String() != "" {
|
|
templateContent = template.String()
|
|
}
|
|
return r.generate(ctx, templateContent, data)
|
|
}
|
|
|
|
func (r *report) generate(ctx context.Context, templateContent string, data ReportData) ([]byte, error) {
|
|
tmpl := template.New("report").Funcs(template.FuncMap{
|
|
"formatTime": func(t float64) string { return r.formatTimeWithTimezone(t, data.Timezone) },
|
|
"formatValue": formatValue,
|
|
"add": func(a, b int) int { return a + b },
|
|
"sub": func(a, b int) int { return a - b },
|
|
"iterate": func(count int) []int { return makeRange(count) },
|
|
"ge": func(a, b int) bool { return a >= b },
|
|
"lt": func(a, b int) bool { return a < b },
|
|
"eq": func(a, b int) bool { return a == b },
|
|
"div": func(a, b int) int {
|
|
if b == 0 {
|
|
return 0
|
|
}
|
|
return a / b
|
|
},
|
|
"mod": func(a, b int) int {
|
|
if b == 0 {
|
|
return 0
|
|
}
|
|
return a % b
|
|
},
|
|
"getStartRow": getStartRow,
|
|
"getEndRow": getEndRow,
|
|
})
|
|
|
|
tmpl, err := tmpl.Parse(templateContent)
|
|
if err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
|
|
var htmlBuf bytes.Buffer
|
|
if err := tmpl.Execute(&htmlBuf, data); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
|
|
htmlContent := htmlBuf.String()
|
|
pdfBytes, err := r.htmlToPDF(ctx, htmlContent)
|
|
if err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
|
|
return pdfBytes, nil
|
|
}
|
|
|
|
func (r *report) htmlToPDF(ctx context.Context, htmlContent string) ([]byte, error) {
|
|
var requestBody bytes.Buffer
|
|
writer := multipart.NewWriter(&requestBody)
|
|
|
|
htmlPart, err := writer.CreateFormFile("files", "index.html")
|
|
if err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
if _, err := htmlPart.Write([]byte(htmlContent)); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
|
|
if err := writer.WriteField("marginTop", "0"); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
if err := writer.WriteField("marginBottom", "0"); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
if err := writer.WriteField("marginLeft", "0"); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
if err := writer.WriteField("marginRight", "0"); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
|
|
if err := writer.WriteField("printBackground", "true"); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
|
|
if err := writer.WriteField("preferCSSPageSize", "true"); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
if err := writer.WriteField("emulatedMediaType", "print"); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
|
|
if err := writer.WriteField("waitForSelector", "body"); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
|
|
if err := writer.Close(); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.converterURL, &requestBody)
|
|
if err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
pdfBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil || resp.StatusCode != http.StatusOK {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
return pdfBytes, nil
|
|
}
|
|
|
|
func (r *report) formatTimeWithTimezone(t float64, timezone string) string {
|
|
loc, err := resolveTimezone(timezone)
|
|
if err != nil {
|
|
r.runInfo <- pkglog.RunInfo{
|
|
Level: slog.LevelWarn,
|
|
Message: fmt.Sprintf("failed to resolve timezone '%s', falling back to UTC: %s", timezone, err),
|
|
Details: []slog.Attr{slog.Time("time", time.Now().UTC())},
|
|
}
|
|
}
|
|
|
|
var timeVal time.Time
|
|
switch {
|
|
case t > nanosecondThreshold:
|
|
timeVal = time.Unix(0, int64(t)).In(loc)
|
|
default:
|
|
timeVal = time.Unix(int64(t), 0).In(loc)
|
|
}
|
|
|
|
return timeVal.Format("2006-01-02 15:04:05")
|
|
}
|
|
|
|
func formatValue(msg senml.Message) string {
|
|
switch {
|
|
case msg.Value != nil:
|
|
return fmt.Sprintf("%.2f", *msg.Value)
|
|
case msg.StringValue != nil:
|
|
return *msg.StringValue
|
|
case msg.BoolValue != nil:
|
|
return fmt.Sprintf("%t", *msg.BoolValue)
|
|
case msg.DataValue != nil:
|
|
return *msg.DataValue
|
|
default:
|
|
return "N/A"
|
|
}
|
|
}
|
|
|
|
func makeRange(n int) []int {
|
|
result := make([]int, n)
|
|
for i := range result {
|
|
result[i] = i
|
|
}
|
|
return result
|
|
}
|
|
|
|
func getStartRow(pageNum, firstPageRows, continuationPageRows int) int {
|
|
if pageNum == 0 {
|
|
return 0
|
|
}
|
|
return firstPageRows + (pageNum-1)*continuationPageRows
|
|
}
|
|
|
|
func getEndRow(pageNum, firstPageRows, continuationPageRows, totalMessages int) int {
|
|
var end int
|
|
if pageNum == 0 {
|
|
end = firstPageRows
|
|
} else {
|
|
start := firstPageRows + (pageNum-1)*continuationPageRows
|
|
end = start + continuationPageRows
|
|
}
|
|
|
|
if end > totalMessages {
|
|
end = totalMessages
|
|
}
|
|
return end
|
|
}
|
|
|
|
func (r *report) generateCSVReport(_ context.Context, title string, reports []Report, timezone string) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
writer := csv.NewWriter(&buf)
|
|
|
|
headers := []string{"Time", "Value", "Unit", "Protocol", "Subtopic"}
|
|
|
|
for i, report := range reports {
|
|
if i > 0 {
|
|
if err := writer.Write([]string{""}); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
if err := writer.Write([]string{"=== NEW REPORT ==="}); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
if err := writer.Write([]string{""}); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
} else {
|
|
if err := writer.Write([]string{title}); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
if err := writer.Write([]string{""}); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
}
|
|
|
|
if err := writer.Write([]string{"Report Information:"}); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
|
|
if err := writer.Write([]string{"Name", report.Metric.Name}); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
|
|
if report.Metric.ClientID != "" {
|
|
if err := writer.Write([]string{"Device ID", report.Metric.ClientID}); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
}
|
|
if err := writer.Write([]string{"Channel ID", report.Metric.ChannelID}); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
if err := writer.Write([]string{""}); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
|
|
if err := writer.Write(headers); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
|
|
sort.Slice(report.Messages, func(i, j int) bool {
|
|
return report.Messages[i].Time < report.Messages[j].Time
|
|
})
|
|
|
|
for _, msg := range report.Messages {
|
|
timeStr := r.formatTimeWithTimezone(msg.Time, timezone)
|
|
|
|
var valueStr string
|
|
if msg.Value != nil {
|
|
valueStr = fmt.Sprintf("%.2f", *msg.Value)
|
|
} else if msg.StringValue != nil {
|
|
valueStr = *msg.StringValue
|
|
} else if msg.BoolValue != nil {
|
|
valueStr = fmt.Sprintf("%v", *msg.BoolValue)
|
|
} else if msg.DataValue != nil {
|
|
valueStr = *msg.DataValue
|
|
} else {
|
|
valueStr = "N/A"
|
|
}
|
|
|
|
row := []string{
|
|
timeStr,
|
|
valueStr,
|
|
msg.Unit,
|
|
msg.Protocol,
|
|
msg.Subtopic,
|
|
}
|
|
|
|
if err := writer.Write(row); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
writer.Flush()
|
|
if err := writer.Error(); err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|