107 lines
2.6 KiB
Go
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)
|
|
}
|