| 1 | //go:build cgo |
| 2 | |
| 3 | package as400 |
| 4 | |
| 5 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 6 | |
| 7 | import ( |
| 8 | "encoding/json" |
| 9 | "fmt" |
| 10 | "maps" |
| 11 | "os" |
| 12 | "path/filepath" |
| 13 | "sync" |
| 14 | "time" |
| 15 | ) |
| 16 | |
| 17 | type dumpContext struct { |
| 18 | baseDir string |
| 19 | queriesDir string |
| 20 | rowsDir string |
| 21 | metricsDir string |
| 22 | metaDir string |
| 23 | |
| 24 | mu sync.Mutex |
| 25 | seq int |
| 26 | metricSeq int |
| 27 | metadataWritten bool |
| 28 | } |
| 29 | |
| 30 | func newDumpContext(baseDir string, cfg *Config) (*dumpContext, error) { |
| 31 | dirs := []string{ |
| 32 | filepath.Join(baseDir, "queries"), |
| 33 | filepath.Join(baseDir, "rows"), |
| 34 | filepath.Join(baseDir, "metrics"), |
| 35 | filepath.Join(baseDir, "meta"), |
| 36 | } |
| 37 | for _, dir := range dirs { |
| 38 | if err := os.MkdirAll(dir, 0o755); err != nil { |
| 39 | return nil, fmt.Errorf("creating dump directory %s: %w", dir, err) |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | dc := &dumpContext{ |
| 44 | baseDir: baseDir, |
| 45 | queriesDir: dirs[0], |
| 46 | rowsDir: dirs[1], |
| 47 | metricsDir: dirs[2], |
| 48 | metaDir: dirs[3], |
| 49 | } |
| 50 | |
| 51 | if err := dc.writeMetadata(cfg); err != nil { |
| 52 | return nil, err |
| 53 | } |
| 54 | |
| 55 | return dc, nil |
| 56 | } |
| 57 | |
| 58 | func (d *dumpContext) writeMetadata(cfg *Config) error { |
| 59 | d.mu.Lock() |
| 60 | defer d.mu.Unlock() |
| 61 | if d.metadataWritten { |
| 62 | return nil |
| 63 | } |
| 64 | safeCfg := *cfg |
| 65 | safeCfg.Password = "" |
| 66 | payload := struct { |
| 67 | GeneratedAt time.Time `json:"generated_at"` |
| 68 | Config Config `json:"config"` |
| 69 | }{ |
| 70 | GeneratedAt: time.Now(), |
| 71 | Config: safeCfg, |
| 72 | } |
| 73 | path := filepath.Join(d.metaDir, "config.json") |
| 74 | if err := writePrettyJSON(path, payload); err != nil { |
| 75 | return err |
| 76 | } |
| 77 | d.metadataWritten = true |
| 78 | return nil |
| 79 | } |
| 80 | |
| 81 | func (d *dumpContext) recordQuery(query string, columns []string, rows [][]string) { |
| 82 | d.mu.Lock() |
| 83 | defer d.mu.Unlock() |
| 84 | d.seq++ |
| 85 | name := fmt.Sprintf("query-%04d", d.seq) |
| 86 | _ = os.WriteFile(filepath.Join(d.queriesDir, name+".sql"), []byte(query), 0o644) |
| 87 | |
| 88 | rowObjs := make([]map[string]string, 0, len(rows)) |
| 89 | for _, row := range rows { |
| 90 | obj := make(map[string]string, len(columns)) |
| 91 | for idx, col := range columns { |
| 92 | if idx < len(row) { |
| 93 | obj[col] = row[idx] |
| 94 | } else { |
| 95 | obj[col] = "" |
| 96 | } |
| 97 | } |
| 98 | rowObjs = append(rowObjs, obj) |
| 99 | } |
| 100 | payload := struct { |
| 101 | Columns []string `json:"columns"` |
| 102 | Rows []map[string]string `json:"rows"` |
| 103 | }{ |
| 104 | Columns: columns, |
| 105 | Rows: rowObjs, |
| 106 | } |
| 107 | _ = writePrettyJSON(filepath.Join(d.rowsDir, name+".json"), payload) |
| 108 | } |
| 109 | |
| 110 | func (d *dumpContext) recordMetrics(metrics map[string]int64) { |
| 111 | d.mu.Lock() |
| 112 | defer d.mu.Unlock() |
| 113 | d.metricSeq++ |
| 114 | filename := fmt.Sprintf("metrics-%04d.json", d.metricSeq) |
| 115 | copyMetrics := make(map[string]int64, len(metrics)) |
| 116 | maps.Copy(copyMetrics, metrics) |
| 117 | payload := struct { |
| 118 | GeneratedAt time.Time `json:"generated_at"` |
| 119 | Metrics map[string]int64 `json:"metrics"` |
| 120 | }{ |
| 121 | GeneratedAt: time.Now(), |
| 122 | Metrics: copyMetrics, |
| 123 | } |
| 124 | _ = writePrettyJSON(filepath.Join(d.metricsDir, filename), payload) |
| 125 | } |
| 126 | |
| 127 | func writePrettyJSON(path string, payload any) error { |
| 128 | data, err := json.MarshalIndent(payload, "", " ") |
| 129 | if err != nil { |
| 130 | return err |
| 131 | } |
| 132 | return os.WriteFile(path, data, 0o644) |
| 133 | } |