| 1 | // metricgen - Universal metric context generator for ibm.d modules |
| 2 | package main |
| 3 | |
| 4 | import ( |
| 5 | "bytes" |
| 6 | "flag" |
| 7 | "fmt" |
| 8 | "go/format" |
| 9 | "log" |
| 10 | "os" |
| 11 | "path/filepath" |
| 12 | "strings" |
| 13 | "text/template" |
| 14 | |
| 15 | "gopkg.in/yaml.v3" |
| 16 | ) |
| 17 | |
| 18 | // Config represents the YAML structure |
| 19 | type Config struct { |
| 20 | Classes map[string]Class `yaml:",inline"` |
| 21 | } |
| 22 | |
| 23 | type Class struct { |
| 24 | Labels []string `yaml:"labels"` |
| 25 | Contexts []Context `yaml:"contexts"` |
| 26 | } |
| 27 | |
| 28 | type Context struct { |
| 29 | Name string `yaml:"name"` |
| 30 | Context string `yaml:"context"` // Full context name |
| 31 | Family string `yaml:"family"` |
| 32 | Title string `yaml:"title"` |
| 33 | Units string `yaml:"units"` |
| 34 | Type string `yaml:"type"` |
| 35 | Priority int `yaml:"priority"` |
| 36 | MinUpdateEvery int `yaml:"min_update_every"` // Minimum update interval |
| 37 | Dimensions []Dimension `yaml:"dimensions"` |
| 38 | } |
| 39 | |
| 40 | type Dimension struct { |
| 41 | Name string `yaml:"name"` |
| 42 | Algorithm string `yaml:"algo"` |
| 43 | Mul int `yaml:"mul"` |
| 44 | Div int `yaml:"div"` |
| 45 | Precision int `yaml:"precision"` |
| 46 | } |
| 47 | |
| 48 | const outputTemplate = `// Code generated by metricgen; DO NOT EDIT. |
| 49 | // source: {{.Source}} |
| 50 | |
| 51 | package {{.Package}} |
| 52 | |
| 53 | import ( |
| 54 | "strings" |
| 55 | "github.com/netdata/netdata/go/plugins/plugin/ibm.d/framework" |
| 56 | module "github.com/netdata/netdata/go/plugins/plugin/framework/collectorapi" |
| 57 | ) |
| 58 | |
| 59 | // cleanLabelValue cleans a label value for use in instance/dimension IDs |
| 60 | func cleanLabelValue(value string) string { |
| 61 | // Replace problematic characters |
| 62 | r := strings.NewReplacer( |
| 63 | " ", "_", |
| 64 | ".", "_", |
| 65 | "-", "_", |
| 66 | "/", "_", |
| 67 | ":", "_", |
| 68 | "=", "_", |
| 69 | ",", "_", |
| 70 | "(", "_", |
| 71 | ")", "_", |
| 72 | ) |
| 73 | return strings.ToLower(r.Replace(value)) |
| 74 | } |
| 75 | |
| 76 | // EmptyLabels is used for contexts without labels |
| 77 | type EmptyLabels struct{} |
| 78 | |
| 79 | // InstanceID for empty labels just returns the context name |
| 80 | func (EmptyLabels) InstanceID(contextName string) string { |
| 81 | return contextName |
| 82 | } |
| 83 | |
| 84 | {{range $className, $class := .Classes}} |
| 85 | // --- {{$className}} --- |
| 86 | |
| 87 | {{range $class.Contexts}} |
| 88 | // {{$className}}{{.Name}}Values defines the type-safe values for {{$className}}.{{.Name}} context |
| 89 | type {{$className}}{{.Name}}Values struct { |
| 90 | {{range .Dimensions}} {{title .Name}} int64 |
| 91 | {{end}}} |
| 92 | |
| 93 | // {{$className}}{{.Name}}Context provides type-safe operations for {{$className}}.{{.Name}} context |
| 94 | type {{$className}}{{.Name}}Context struct { |
| 95 | framework.Context[{{if $class.Labels}}{{$className}}Labels{{else}}EmptyLabels{{end}}] |
| 96 | } |
| 97 | |
| 98 | // Set provides type-safe dimension setting for {{$className}}.{{.Name}} context |
| 99 | func (c {{$className}}{{.Name}}Context) Set(state *framework.CollectorState, labels {{if $class.Labels}}{{$className}}Labels{{else}}EmptyLabels{{end}}, values {{$className}}{{.Name}}Values) { |
| 100 | state.SetMetricsForGeneratedCode(&c.Context, {{if $class.Labels}}labels{{else}}nil{{end}}, map[string]int64{ |
| 101 | {{range .Dimensions}} "{{.Name}}": values.{{title .Name}}, |
| 102 | {{end}} }) |
| 103 | } |
| 104 | |
| 105 | // SetUpdateEvery sets the update interval for this instance |
| 106 | func (c {{$className}}{{.Name}}Context) SetUpdateEvery(state *framework.CollectorState, labels {{if $class.Labels}}{{$className}}Labels{{else}}EmptyLabels{{end}}, updateEvery int) { |
| 107 | state.SetUpdateEveryOverrideForGeneratedCode(&c.Context, {{if $class.Labels}}labels{{else}}nil{{end}}, updateEvery) |
| 108 | } |
| 109 | {{end}} |
| 110 | |
| 111 | {{if $class.Labels}} |
| 112 | // {{$className}}Labels defines the required labels for {{$className}} contexts |
| 113 | type {{$className}}Labels struct { |
| 114 | {{range $class.Labels}} {{title .}} string |
| 115 | {{end}}} |
| 116 | |
| 117 | // InstanceID generates a unique instance ID using the hardcoded label order from YAML |
| 118 | func (l {{$className}}Labels) InstanceID(contextName string) string { |
| 119 | // Label order from YAML: {{range $i, $label := $class.Labels}}{{if $i}}, {{end}}{{$label}}{{end}} |
| 120 | return contextName + "." + {{range $i, $label := $class.Labels}}{{if $i}} + "_" + {{end}}cleanLabelValue(l.{{title $label}}){{end}} |
| 121 | } |
| 122 | {{end}} |
| 123 | |
| 124 | // {{$className}} contains all metric contexts for {{$className}} |
| 125 | var {{$className}} = struct { |
| 126 | {{range $class.Contexts}} {{.Name}} {{$className}}{{.Name}}Context |
| 127 | {{end}}}{ |
| 128 | {{range $class.Contexts}} {{.Name}}: {{$className}}{{.Name}}Context{ |
| 129 | Context: framework.Context[{{if $class.Labels}}{{$className}}Labels{{else}}EmptyLabels{{end}}]{ |
| 130 | Name: "{{.Context}}", |
| 131 | Family: "{{.Family}}", |
| 132 | Title: "{{.Title}}", |
| 133 | Units: "{{.Units}}", |
| 134 | Type: module.{{title .Type}}, |
| 135 | Priority: {{.Priority}}, |
| 136 | UpdateEvery: {{if .MinUpdateEvery}}{{.MinUpdateEvery}}{{else}}1{{end}}, |
| 137 | Dimensions: []framework.Dimension{ |
| 138 | {{range .Dimensions}} { |
| 139 | Name: "{{.Name}}", |
| 140 | Algorithm: module.{{title .Algorithm}}, |
| 141 | Mul: {{.Mul}}, |
| 142 | Div: {{.Div}}, |
| 143 | Precision: {{.Precision}}, |
| 144 | }, |
| 145 | {{end}} }, |
| 146 | LabelKeys: []string{ |
| 147 | {{range $class.Labels}} "{{.}}", |
| 148 | {{end}} }, |
| 149 | }, |
| 150 | }, |
| 151 | {{end}}} |
| 152 | |
| 153 | {{end}} |
| 154 | |
| 155 | // GetAllContexts returns all contexts for framework registration |
| 156 | func GetAllContexts() []interface{} { |
| 157 | return []interface{}{ |
| 158 | {{range $className, $class := .Classes}}{{range $class.Contexts}} &{{$className}}.{{.Name}}.Context, |
| 159 | {{end}}{{end}} } |
| 160 | } |
| 161 | ` |
| 162 | |
| 163 | func main() { |
| 164 | var ( |
| 165 | input = flag.String("input", "contexts.yaml", "Input YAML file") |
| 166 | output = flag.String("output", "zz_generated_contexts.go", "Output Go file") |
| 167 | pkg = flag.String("package", "contexts", "Package name") |
| 168 | module = flag.String("module", "", "Module prefix (e.g., as400, db2, mq)") |
| 169 | ) |
| 170 | flag.Parse() |
| 171 | |
| 172 | // Read input file |
| 173 | data, err := os.ReadFile(*input) |
| 174 | if err != nil { |
| 175 | log.Fatalf("failed to read input file: %v", err) |
| 176 | } |
| 177 | |
| 178 | // Parse YAML |
| 179 | var config Config |
| 180 | if err := yaml.Unmarshal(data, &config.Classes); err != nil { |
| 181 | log.Fatalf("failed to parse YAML: %v", err) |
| 182 | } |
| 183 | |
| 184 | // Process the config |
| 185 | processConfig(&config, *module) |
| 186 | |
| 187 | // Generate output |
| 188 | if err := generateOutput(config, *input, *output, *pkg); err != nil { |
| 189 | log.Fatalf("failed to generate output: %v", err) |
| 190 | } |
| 191 | |
| 192 | log.Printf("Generated %s from %s", *output, *input) |
| 193 | } |
| 194 | |
| 195 | func processConfig(config *Config, modulePrefix string) { |
| 196 | // Set defaults and add module prefix if specified |
| 197 | for className, class := range config.Classes { |
| 198 | for i := range class.Contexts { |
| 199 | ctx := &class.Contexts[i] |
| 200 | |
| 201 | // Context names should be fully qualified in the YAML file |
| 202 | // This allows flexibility to move/inject contexts anywhere |
| 203 | |
| 204 | // Set default algorithm |
| 205 | for j := range ctx.Dimensions { |
| 206 | dim := &ctx.Dimensions[j] |
| 207 | if dim.Algorithm == "" { |
| 208 | dim.Algorithm = "absolute" |
| 209 | } |
| 210 | if dim.Mul == 0 { |
| 211 | dim.Mul = 1 |
| 212 | } |
| 213 | if dim.Div == 0 { |
| 214 | dim.Div = 1 |
| 215 | } |
| 216 | if dim.Precision == 0 { |
| 217 | dim.Precision = 1 |
| 218 | } |
| 219 | } |
| 220 | |
| 221 | // Set default min_update_every |
| 222 | if ctx.MinUpdateEvery == 0 { |
| 223 | ctx.MinUpdateEvery = 1 |
| 224 | } |
| 225 | |
| 226 | // Set default priority |
| 227 | if ctx.Priority == 0 { |
| 228 | ctx.Priority = 70000 // Default priority |
| 229 | } |
| 230 | |
| 231 | // Set default chart type |
| 232 | if ctx.Type == "" { |
| 233 | ctx.Type = "line" |
| 234 | } |
| 235 | } |
| 236 | config.Classes[className] = class |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | func generateOutput(config Config, source, output, pkg string) error { |
| 241 | // Parse template with custom functions |
| 242 | tmpl, err := template.New("output").Funcs(template.FuncMap{ |
| 243 | "title": strings.Title, |
| 244 | }).Parse(outputTemplate) |
| 245 | if err != nil { |
| 246 | return fmt.Errorf("failed to parse template: %v", err) |
| 247 | } |
| 248 | |
| 249 | // Execute template into a buffer so we can run gofmt before writing |
| 250 | data := struct { |
| 251 | Source string |
| 252 | Package string |
| 253 | Classes map[string]Class |
| 254 | }{ |
| 255 | Source: filepath.Base(source), |
| 256 | Package: pkg, |
| 257 | Classes: config.Classes, |
| 258 | } |
| 259 | |
| 260 | var buf bytes.Buffer |
| 261 | if err := tmpl.Execute(&buf, data); err != nil { |
| 262 | return fmt.Errorf("failed to execute template: %v", err) |
| 263 | } |
| 264 | |
| 265 | formatted, err := format.Source(buf.Bytes()) |
| 266 | if err != nil { |
| 267 | return fmt.Errorf("failed to format generated code: %v", err) |
| 268 | } |
| 269 | |
| 270 | if err := os.WriteFile(output, formatted, 0o644); err != nil { |
| 271 | return fmt.Errorf("failed to write output file: %v", err) |
| 272 | } |
| 273 | |
| 274 | return nil |
| 275 | } |