Files
zavrsni-rad-otel-app/backend/analytics/internal/export/xlsx.go
2026-05-11 10:58:46 +02:00

107 lines
2.6 KiB
Go

package export
import (
"context"
"fmt"
"sort"
"github.com/xuri/excelize/v2"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
)
var (
exportTracer = otel.Tracer("otel-bi.export")
exportMeter = otel.Meter("otel-bi.export")
exportRowsTotal, _ = exportMeter.Int64Counter(
"export.rows_total",
metric.WithDescription("Total rows exported to XLSX"),
)
exportSizeBytes, _ = exportMeter.Int64Histogram(
"export.file_size_bytes",
metric.WithDescription("XLSX file size in bytes"),
metric.WithUnit("By"),
)
)
type Column struct {
Key string
Label string
}
// ToXLSXBytes writes rows to a single-sheet Excel workbook using the given
// column spec (controls header labels and order) and returns the raw bytes.
func ToXLSXBytes(ctx context.Context, sheetName string, cols []Column, rows []map[string]any) ([]byte, error) {
ctx, span := exportTracer.Start(ctx, "export.xlsx",
trace.WithAttributes(attribute.String("sheet_name", sheetName)),
)
defer span.End()
f := excelize.NewFile()
defer f.Close()
sheet := f.GetSheetName(0)
if err := f.SetSheetName(sheet, sheetName); err != nil {
return nil, err
}
// Header row
for col, c := range cols {
cell, _ := excelize.CoordinatesToCellName(col+1, 1)
if err := f.SetCellValue(sheetName, cell, c.Label); err != nil {
return nil, err
}
}
// Data rows
for rowIdx, row := range rows {
for colIdx, c := range cols {
cell, _ := excelize.CoordinatesToCellName(colIdx+1, rowIdx+2)
_ = f.SetCellValue(sheetName, cell, fmtCell(row[c.Key]))
}
}
buf, err := f.WriteToBuffer()
if err != nil {
return nil, err
}
b := buf.Bytes()
span.SetAttributes(
attribute.Int("row_count", len(rows)),
attribute.Int("file_size_bytes", len(b)),
)
exportRowsTotal.Add(ctx, int64(len(rows)), metric.WithAttributes(attribute.String("sheet", sheetName)))
exportSizeBytes.Record(ctx, int64(len(b)), metric.WithAttributes(attribute.String("sheet", sheetName)))
return b, nil
}
// GenericXLSX converts a slice of maps to XLSX with alphabetically-sorted headers.
// Use ToXLSXBytes when column order matters.
func GenericXLSX(ctx context.Context, sheetName string, rows []map[string]any) ([]byte, error) {
if len(rows) == 0 {
return ToXLSXBytes(ctx, sheetName, nil, nil)
}
keys := make([]string, 0, len(rows[0]))
for k := range rows[0] {
keys = append(keys, k)
}
sort.Strings(keys)
cols := make([]Column, len(keys))
for i, k := range keys {
cols[i] = Column{Key: k, Label: k}
}
return ToXLSXBytes(ctx, sheetName, cols, rows)
}
func fmtCell(v any) string {
if v == nil {
return ""
}
return fmt.Sprintf("%v", v)
}