master
go 275 lines 7.65 KB
Raw
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 }