1 package main
2
3 import (
4 "io/fs"
5 "math"
6 "os"
7 "path"
8 "regexp"
9 "slices"
10 "strconv"
11 "strings"
12
13 "flag"
14 "fmt"
15
16 "gopkg.in/yaml.v3"
17 )
18
19 type (
20 ExtensionInfo struct {
21 ExtName string `yaml:"ext_name"`
22 ExtMinVersion string `yaml:"ext_min_version"`
23 }
24
25 ExtensionOverrides struct {
26 TargetMetric string `yaml:"target_metric"`
27 ExpectedExtensionVersions []ExtensionInfo `yaml:"expected_extension_versions"`
28 }
29
30 MetricAttrs struct {
31 IsInstanceLevel bool `yaml:"is_instance_level,omitempty"`
32 MetricStorageName string `yaml:"metric_storage_name,omitempty"`
33 ExtensionVersionOverrides []ExtensionOverrides `yaml:"extension_version_based_overrides,omitempty"`
34 IsPrivate bool `yaml:"is_private,omitempty"`
35 DisabledDays string `yaml:"disabled_days,omitempty"`
36 DisableTimes []string `yaml:"disabled_times,omitempty"`
37 StatementTimeoutSeconds int64 `yaml:"statement_timeout_seconds,omitempty"`
38 }
39
40 SQLs map[int]string
41
42 Metric struct {
43 SQLs SQLs
44 InitSQL string `yaml:"init_sql,omitempty"`
45 NodeStatus string `yaml:"node_status,omitempty"`
46 Gauges []string `yaml:",omitempty"`
47 MetricAttrs `yaml:",inline,omitempty"`
48 }
49
50 Metrics map[string]Metric
51 )
52
53 const (
54 FileBasedMetricHelpersDir = "00_helpers"
55 PresetConfigYAMLFile = "preset-configs.yaml"
56 )
57
58
59
60
61
62 var regVer = regexp.MustCompile(`(\d+).?(\d*).?(\d*)`)
63
64 func VersionToInt(version string) (v int) {
65 if matches := regVer.FindStringSubmatch(version); len(matches) > 1 {
66 for i, match := range matches[1:] {
67 v += func() (m int) { m, _ = strconv.Atoi(match); return }() * int(math.Pow10(4-i*2))
68 }
69 }
70 return
71 }
72
73
74 func ReadMetricsFromFolder(folder string) (metricsMap Metrics, err error) {
75 metricFolders, err := os.ReadDir(folder)
76 if err != nil {
77 return
78 }
79
80 metricsMap = make(map[string]Metric)
81 metricNamePattern := `^[a-z0-9_\.]+$`
82 regexMetricNameFilter := regexp.MustCompile(metricNamePattern)
83 regexIsDigitOrPunctuation := regexp.MustCompile(`^[\d\.]+$`)
84
85 fmt.Printf("Searching for metrics from path %s ...\n", folder)
86
87 for _, metricFolder := range metricFolders {
88 if metricFolder.Name() == FileBasedMetricHelpersDir ||
89 !metricFolder.IsDir() ||
90 !regexMetricNameFilter.MatchString(metricFolder.Name()) {
91 continue
92 }
93
94 var versionFolders []fs.DirEntry
95 versionFolders, err = os.ReadDir(path.Join(folder, metricFolder.Name()))
96 if err != nil {
97 return
98 }
99
100 Metric := Metric{}
101 Metric.MetricAttrs, _ = ParseMetricAttrsFromYAML(path.Join(folder, metricFolder.Name(), "metric_attrs.yaml"))
102 _ = ParseMetricPrometheusAttrsFromYAML(path.Join(folder, metricFolder.Name(), "column_attrs.yaml"), &Metric)
103 Metric.SQLs = make(SQLs)
104
105 var version int
106 for _, versionFolder := range versionFolders {
107 if strings.HasSuffix(versionFolder.Name(), ".md") || versionFolder.Name() == "column_attrs.yaml" || versionFolder.Name() == "metric_attrs.yaml" {
108 continue
109 }
110 if !regexIsDigitOrPunctuation.MatchString(versionFolder.Name()) {
111 fmt.Printf("Invalid metric structure - version folder names should consist of only numerics/dots, found: %s", versionFolder.Name())
112 continue
113 }
114 if version, err = strconv.Atoi(versionFolder.Name()); err != nil {
115 version = 11
116 }
117
118 var metricDefs []fs.DirEntry
119 if metricDefs, err = os.ReadDir(path.Join(folder, metricFolder.Name(), versionFolder.Name())); err != nil {
120 return
121 }
122
123 for _, metricDef := range metricDefs {
124 if strings.HasPrefix(metricDef.Name(), "metric") && strings.HasSuffix(metricDef.Name(), ".sql") {
125 p := path.Join(folder, metricFolder.Name(), versionFolder.Name(), metricDef.Name())
126 metricSQL, err := os.ReadFile(p)
127 if err != nil {
128 continue
129 }
130 switch {
131 case strings.Contains(metricDef.Name(), "_master"):
132 Metric.NodeStatus = "primary"
133 case strings.Contains(metricDef.Name(), "_standby"):
134 Metric.NodeStatus = "standby"
135 }
136 Metric.SQLs[version] = strings.TrimRight(strings.TrimSpace(string(metricSQL)), ";")
137 }
138 }
139 }
140 metricsMap[metricFolder.Name()] = Metric
141 }
142 return
143 }
144
145 func ParseMetricPrometheusAttrsFromYAML(path string, m *Metric) (err error) {
146 type OldMetricPrometheusAttrs struct {
147 PrometheusGaugeColumns []string `yaml:"prometheus_gauge_columns,omitempty"`
148 PrometheusIgnoredColumns []string `yaml:"prometheus_ignored_columns,omitempty"`
149 PrometheusAllGaugeColumns bool `yaml:"prometheus_all_gauge_columns,omitempty"`
150 }
151
152 var val []byte
153 var oldPromAttrs OldMetricPrometheusAttrs
154 if val, err = os.ReadFile(path); err == nil {
155 if err = yaml.Unmarshal(val, &oldPromAttrs); err != nil {
156 return
157 }
158 if oldPromAttrs.PrometheusAllGaugeColumns {
159 m.Gauges = []string{"*"}
160 return
161 }
162 m.Gauges = slices.Clone(oldPromAttrs.PrometheusGaugeColumns)
163 }
164
165 return
166 }
167
168 func ParseMetricAttrsFromYAML(path string) (a MetricAttrs, err error) {
169 var val []byte
170 if val, err = os.ReadFile(path); err == nil {
171 err = yaml.Unmarshal(val, &a)
172 }
173 return
174 }
175
176 type Presets map[string]Preset
177
178 type Preset struct {
179 Name string `yaml:"name,omitempty"`
180 Description string
181 Metrics map[string]int
182 }
183
184
185 func ReadPresetsFromFolder(folder string) (presets Presets, err error) {
186 var presetMetrics []byte
187 fmt.Printf("Searching for presents from path %s ...\n", folder)
188 if presetMetrics, err = os.ReadFile(path.Join(folder, PresetConfigYAMLFile)); err != nil {
189 return
190 }
191 var oldPresets []Preset
192 if err = yaml.Unmarshal(presetMetrics, &oldPresets); err != nil {
193 return
194 }
195 presets = make(Presets, 0)
196 for _, p := range oldPresets {
197 pname := p.Name
198 p.Name = ""
199 presets[pname] = p
200 }
201 return
202 }
203
204 func WriteMetricsToFile(metricDefs any, filename string) error {
205 yamlData, err := yaml.Marshal(metricDefs)
206 if err != nil {
207 return err
208 }
209 return os.WriteFile(filename, yamlData, 0644)
210 }
211
212 func moveHelpersToMetrics(helpers, metrics Metrics) {
213 new_helper:
214 for helperName, h := range helpers {
215 for metricName, m := range metrics {
216 for _, sql := range m.SQLs {
217 if strings.Contains(sql, helperName) {
218 for _, v := range h.SQLs {
219 m.InitSQL = v
220 metrics[metricName] = m
221 continue new_helper
222 }
223 }
224 }
225 }
226 }
227 }
228
229 func main() {
230
231 src := flag.String("src", "", "pgwatch v2 metric folder, e.g. `./metrics/sql`")
232 dst := flag.String("dst", "", "pgwatch v3 output metric file, e.g. `metrics.yaml`")
233
234
235 flag.Parse()
236
237
238 if *src == "" {
239 fmt.Println("Error: src option is required")
240 return
241 }
242
243
244 if *dst == "" {
245 fmt.Println("Error: dst option is required")
246 return
247 }
248 helpers, err := ReadMetricsFromFolder(path.Join(*src, FileBasedMetricHelpersDir))
249 if err != nil {
250 panic(err)
251 }
252 metrics, err := ReadMetricsFromFolder(*src)
253 if err != nil {
254 panic(err)
255 }
256 moveHelpersToMetrics(helpers, metrics)
257 presets, err := ReadPresetsFromFolder(*src)
258 if err != nil {
259 panic(err)
260 }
261 err = WriteMetricsToFile(struct {
262 Metrics Metrics `yaml:"metrics,omitempty"`
263 Presets Presets `yaml:"presets,omitempty"`
264 }{
265 metrics,
266 presets,
267 }, *dst)
268 if err != nil {
269 panic(err)
270 }
271 }
272