| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package collectorapi |
| 4 | |
| 5 | import ( |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | "strings" |
| 9 | "unicode" |
| 10 | ) |
| 11 | |
| 12 | type ( |
| 13 | ChartType string |
| 14 | DimAlgo string |
| 15 | ) |
| 16 | |
| 17 | const ( |
| 18 | // Line chart type. |
| 19 | Line ChartType = "line" |
| 20 | // Area chart type. |
| 21 | Area ChartType = "area" |
| 22 | // Stacked chart type. |
| 23 | Stacked ChartType = "stacked" |
| 24 | // Heatmap chart type for Prometheus histogram visualization. |
| 25 | Heatmap ChartType = "heatmap" |
| 26 | |
| 27 | // Absolute dimension algorithm. |
| 28 | // The value is to drawn as-is (interpolated to second boundary). |
| 29 | Absolute DimAlgo = "absolute" |
| 30 | // Incremental dimension algorithm. |
| 31 | // The value increases over time, the difference from the last value is presented in the chart, |
| 32 | // the server interpolates the value and calculates a per second figure. |
| 33 | Incremental DimAlgo = "incremental" |
| 34 | // PercentOfAbsolute dimension algorithm. |
| 35 | // The percent of this value compared to the total of all dimensions. |
| 36 | PercentOfAbsolute DimAlgo = "percentage-of-absolute-row" |
| 37 | // PercentOfIncremental dimension algorithm. |
| 38 | // The percent of this value compared to the incremental total of all dimensions |
| 39 | PercentOfIncremental DimAlgo = "percentage-of-incremental-row" |
| 40 | ) |
| 41 | |
| 42 | const ( |
| 43 | // Not documented. |
| 44 | // https://github.com/netdata/netdata/blob/cc2586de697702f86a3c34e60e23652dd4ddcb42/database/rrd.h#L204 |
| 45 | |
| 46 | LabelSourceAuto = 1 << 0 |
| 47 | LabelSourceConf = 1 << 1 |
| 48 | LabelSourceK8s = 1 << 2 |
| 49 | ) |
| 50 | |
| 51 | func (d DimAlgo) String() string { |
| 52 | switch d { |
| 53 | case Absolute, Incremental, PercentOfAbsolute, PercentOfIncremental: |
| 54 | return string(d) |
| 55 | } |
| 56 | return string(Absolute) |
| 57 | } |
| 58 | |
| 59 | func (c ChartType) String() string { |
| 60 | switch c { |
| 61 | case Line, Area, Stacked, Heatmap: |
| 62 | return string(c) |
| 63 | } |
| 64 | return string(Line) |
| 65 | } |
| 66 | |
| 67 | type ( |
| 68 | // Charts is a collection of Charts. |
| 69 | Charts []*Chart |
| 70 | |
| 71 | // Opts represents chart options. |
| 72 | Opts struct { |
| 73 | Obsolete bool |
| 74 | Detail bool |
| 75 | StoreFirst bool |
| 76 | Hidden bool |
| 77 | } |
| 78 | |
| 79 | // Chart represents a chart. |
| 80 | // For the full description please visit https://docs.netdata.cloud/plugins.d/#chart |
| 81 | Chart struct { |
| 82 | // typeID is the unique identification of the chart, if not specified, |
| 83 | // the orchestrator will use job full name + chart ID as typeID (default behaviour). |
| 84 | typ string |
| 85 | id string |
| 86 | |
| 87 | OverModule string |
| 88 | IDSep bool |
| 89 | ID string |
| 90 | OverID string |
| 91 | Title string |
| 92 | Units string |
| 93 | Fam string |
| 94 | Ctx string |
| 95 | Type ChartType |
| 96 | Priority int |
| 97 | UpdateEvery int // Override for this chart's update interval (0 means use job default) |
| 98 | SkipGaps bool // Skip chart entirely (no BEGIN/END) when no dimensions have data |
| 99 | Opts |
| 100 | |
| 101 | Labels []Label |
| 102 | Dims Dims |
| 103 | Vars Vars |
| 104 | |
| 105 | Retries int |
| 106 | |
| 107 | remove bool |
| 108 | // created flag is used to indicate whether the chart needs to be created by the orchestrator. |
| 109 | created bool |
| 110 | // updated flag is used to indicate whether the chart was updated on last data collection interval. |
| 111 | updated bool |
| 112 | |
| 113 | // ignore flag is used to indicate that the chart shouldn't be sent to the netdata plugins.d |
| 114 | ignore bool |
| 115 | } |
| 116 | |
| 117 | Label struct { |
| 118 | Key string |
| 119 | Value string |
| 120 | Source int |
| 121 | } |
| 122 | |
| 123 | // DimOpts represents dimension options. |
| 124 | DimOpts struct { |
| 125 | Obsolete bool |
| 126 | Hidden bool |
| 127 | NoReset bool |
| 128 | NoOverflow bool |
| 129 | Float bool |
| 130 | } |
| 131 | |
| 132 | // Dim represents a chart dimension. |
| 133 | // For detailed description please visit https://docs.netdata.cloud/plugins.d/#dimension. |
| 134 | Dim struct { |
| 135 | ID string |
| 136 | Name string |
| 137 | Algo DimAlgo |
| 138 | Mul int |
| 139 | Div int |
| 140 | DimOpts |
| 141 | |
| 142 | remove bool |
| 143 | } |
| 144 | |
| 145 | // Var represents a chart variable. |
| 146 | // For detailed description please visit https://docs.netdata.cloud/plugins.d/#variable |
| 147 | Var struct { |
| 148 | ID string |
| 149 | Name string |
| 150 | Value float64 |
| 151 | } |
| 152 | |
| 153 | // Dims is a collection of dims. |
| 154 | Dims []*Dim |
| 155 | // Vars is a collection of vars. |
| 156 | Vars []*Var |
| 157 | ) |
| 158 | |
| 159 | func (o Opts) String() string { |
| 160 | var b strings.Builder |
| 161 | if o.Detail { |
| 162 | b.WriteString(" detail") |
| 163 | } |
| 164 | if o.Hidden { |
| 165 | b.WriteString(" hidden") |
| 166 | } |
| 167 | if o.Obsolete { |
| 168 | b.WriteString(" obsolete") |
| 169 | } |
| 170 | if o.StoreFirst { |
| 171 | b.WriteString(" store_first") |
| 172 | } |
| 173 | |
| 174 | if len(b.String()) == 0 { |
| 175 | return "" |
| 176 | } |
| 177 | return b.String()[1:] |
| 178 | } |
| 179 | |
| 180 | func (o DimOpts) String() string { |
| 181 | var b strings.Builder |
| 182 | if o.Hidden { |
| 183 | b.WriteString(" hidden") |
| 184 | } |
| 185 | if o.NoOverflow { |
| 186 | b.WriteString(" nooverflow") |
| 187 | } |
| 188 | if o.NoReset { |
| 189 | b.WriteString(" noreset") |
| 190 | } |
| 191 | if o.Obsolete { |
| 192 | b.WriteString(" obsolete") |
| 193 | } |
| 194 | if o.Float { |
| 195 | b.WriteString(" type=float") |
| 196 | } |
| 197 | |
| 198 | if len(b.String()) == 0 { |
| 199 | return "" |
| 200 | } |
| 201 | return b.String()[1:] |
| 202 | } |
| 203 | |
| 204 | // Add adds (appends) a variable number of Charts. |
| 205 | func (c *Charts) Add(charts ...*Chart) error { |
| 206 | for _, chart := range charts { |
| 207 | err := checkChart(chart) |
| 208 | if err != nil { |
| 209 | return fmt.Errorf("error on adding chart '%s' : %s", chart.ID, err) |
| 210 | } |
| 211 | if chart := c.Get(chart.ID); chart != nil && !chart.remove { |
| 212 | return fmt.Errorf("error on adding chart : '%s' is already in charts", chart.ID) |
| 213 | } |
| 214 | *c = append(*c, chart) |
| 215 | } |
| 216 | |
| 217 | return nil |
| 218 | } |
| 219 | |
| 220 | // Get returns the chart by ID. |
| 221 | func (c Charts) Get(chartID string) *Chart { |
| 222 | idx := c.index(chartID) |
| 223 | if idx == -1 { |
| 224 | return nil |
| 225 | } |
| 226 | return c[idx] |
| 227 | } |
| 228 | |
| 229 | // Has returns true if ChartsFunc contain the chart with the given ID, false otherwise. |
| 230 | func (c Charts) Has(chartID string) bool { |
| 231 | return c.index(chartID) != -1 |
| 232 | } |
| 233 | |
| 234 | // Remove removes the chart from Charts by ID. |
| 235 | // Avoid to use it in runtime. |
| 236 | func (c *Charts) Remove(chartID string) error { |
| 237 | idx := c.index(chartID) |
| 238 | if idx == -1 { |
| 239 | return fmt.Errorf("error on removing chart : '%s' is not in charts", chartID) |
| 240 | } |
| 241 | copy((*c)[idx:], (*c)[idx+1:]) |
| 242 | (*c)[len(*c)-1] = nil |
| 243 | *c = (*c)[:len(*c)-1] |
| 244 | return nil |
| 245 | } |
| 246 | |
| 247 | // Copy returns a deep copy of ChartsFunc. |
| 248 | func (c Charts) Copy() *Charts { |
| 249 | charts := Charts{} |
| 250 | for idx := range c { |
| 251 | charts = append(charts, c[idx].Copy()) |
| 252 | } |
| 253 | return &charts |
| 254 | } |
| 255 | |
| 256 | func (c Charts) index(chartID string) int { |
| 257 | for idx := range c { |
| 258 | if c[idx].ID == chartID { |
| 259 | return idx |
| 260 | } |
| 261 | } |
| 262 | return -1 |
| 263 | } |
| 264 | |
| 265 | // MarkNotCreated changes 'created' chart flag to false. |
| 266 | // Use it to add dimension in runtime. |
| 267 | func (c *Chart) MarkNotCreated() { |
| 268 | c.created = false |
| 269 | } |
| 270 | |
| 271 | // MarkRemove sets 'remove' flag and Obsolete option to true. |
| 272 | // Use it to remove chart in runtime. |
| 273 | func (c *Chart) MarkRemove() { |
| 274 | c.Obsolete = true |
| 275 | c.remove = true |
| 276 | } |
| 277 | |
| 278 | // MarkDimRemove sets 'remove' flag, Obsolete and optionally Hidden options to true. |
| 279 | // Use it to remove dimension in runtime. |
| 280 | func (c *Chart) MarkDimRemove(dimID string, hide bool) error { |
| 281 | if !c.HasDim(dimID) { |
| 282 | return fmt.Errorf("chart '%s' has no '%s' dimension", c.ID, dimID) |
| 283 | } |
| 284 | dim := c.GetDim(dimID) |
| 285 | dim.Obsolete = true |
| 286 | if hide { |
| 287 | dim.Hidden = true |
| 288 | } |
| 289 | dim.remove = true |
| 290 | return nil |
| 291 | } |
| 292 | |
| 293 | // AddDim adds new dimension to the chart dimensions. |
| 294 | func (c *Chart) AddDim(newDim *Dim) error { |
| 295 | err := checkDim(newDim) |
| 296 | if err != nil { |
| 297 | return fmt.Errorf("error on adding dim to chart '%s' : %s", c.ID, err) |
| 298 | } |
| 299 | if c.HasDim(newDim.ID) { |
| 300 | return fmt.Errorf("error on adding dim : '%s' is already in chart '%s' dims", newDim.ID, c.ID) |
| 301 | } |
| 302 | c.Dims = append(c.Dims, newDim) |
| 303 | |
| 304 | return nil |
| 305 | } |
| 306 | |
| 307 | // AddVar adds new variable to the chart variables. |
| 308 | func (c *Chart) AddVar(newVar *Var) error { |
| 309 | err := checkVar(newVar) |
| 310 | if err != nil { |
| 311 | return fmt.Errorf("error on adding var to chart '%s' : %s", c.ID, err) |
| 312 | } |
| 313 | if c.indexVar(newVar.ID) != -1 { |
| 314 | return fmt.Errorf("error on adding var : '%s' is already in chart '%s' vars", newVar.ID, c.ID) |
| 315 | } |
| 316 | c.Vars = append(c.Vars, newVar) |
| 317 | |
| 318 | return nil |
| 319 | } |
| 320 | |
| 321 | // GetDim returns dimension by ID. |
| 322 | func (c *Chart) GetDim(dimID string) *Dim { |
| 323 | idx := c.indexDim(dimID) |
| 324 | if idx == -1 { |
| 325 | return nil |
| 326 | } |
| 327 | return c.Dims[idx] |
| 328 | } |
| 329 | |
| 330 | // RemoveDim removes dimension by ID. |
| 331 | // Avoid to use it in runtime. |
| 332 | func (c *Chart) RemoveDim(dimID string) error { |
| 333 | idx := c.indexDim(dimID) |
| 334 | if idx == -1 { |
| 335 | return fmt.Errorf("error on removing dim : '%s' isn't in chart '%s'", dimID, c.ID) |
| 336 | } |
| 337 | c.Dims = append(c.Dims[:idx], c.Dims[idx+1:]...) |
| 338 | |
| 339 | return nil |
| 340 | } |
| 341 | |
| 342 | // HasDim returns true if the chart contains dimension with the given ID, false otherwise. |
| 343 | func (c Chart) HasDim(dimID string) bool { |
| 344 | return c.indexDim(dimID) != -1 |
| 345 | } |
| 346 | |
| 347 | // Copy returns a deep copy of the chart. |
| 348 | func (c Chart) Copy() *Chart { |
| 349 | chart := c |
| 350 | chart.Dims = Dims{} |
| 351 | chart.Vars = Vars{} |
| 352 | |
| 353 | for idx := range c.Dims { |
| 354 | chart.Dims = append(chart.Dims, c.Dims[idx].copy()) |
| 355 | } |
| 356 | for idx := range c.Vars { |
| 357 | chart.Vars = append(chart.Vars, c.Vars[idx].copy()) |
| 358 | } |
| 359 | |
| 360 | return &chart |
| 361 | } |
| 362 | |
| 363 | func (c Chart) indexDim(dimID string) int { |
| 364 | for idx := range c.Dims { |
| 365 | if c.Dims[idx].ID == dimID { |
| 366 | return idx |
| 367 | } |
| 368 | } |
| 369 | return -1 |
| 370 | } |
| 371 | |
| 372 | func (c Chart) indexVar(varID string) int { |
| 373 | for idx := range c.Vars { |
| 374 | if c.Vars[idx].ID == varID { |
| 375 | return idx |
| 376 | } |
| 377 | } |
| 378 | return -1 |
| 379 | } |
| 380 | |
| 381 | func (d Dim) copy() *Dim { |
| 382 | return &d |
| 383 | } |
| 384 | |
| 385 | func (v Var) copy() *Var { |
| 386 | return &v |
| 387 | } |
| 388 | |
| 389 | func checkCharts(charts ...*Chart) error { |
| 390 | for _, chart := range charts { |
| 391 | err := checkChart(chart) |
| 392 | if err != nil { |
| 393 | return fmt.Errorf("chart '%s' : %v", chart.ID, err) |
| 394 | } |
| 395 | } |
| 396 | return nil |
| 397 | } |
| 398 | |
| 399 | // CheckCharts validates chart definitions. |
| 400 | func CheckCharts(charts ...*Chart) error { |
| 401 | return checkCharts(charts...) |
| 402 | } |
| 403 | |
| 404 | func checkChart(chart *Chart) error { |
| 405 | if chart.ID == "" { |
| 406 | return errors.New("empty ID") |
| 407 | } |
| 408 | |
| 409 | if chart.Title == "" { |
| 410 | return errors.New("empty Title") |
| 411 | } |
| 412 | |
| 413 | if chart.Units == "" { |
| 414 | return errors.New("empty Units") |
| 415 | } |
| 416 | |
| 417 | if id := checkID(chart.ID); id != -1 { |
| 418 | return fmt.Errorf("unacceptable symbol in ID : '%c'", id) |
| 419 | } |
| 420 | |
| 421 | set := make(map[string]bool) |
| 422 | |
| 423 | for _, d := range chart.Dims { |
| 424 | err := checkDim(d) |
| 425 | if err != nil { |
| 426 | return err |
| 427 | } |
| 428 | if set[d.ID] { |
| 429 | return fmt.Errorf("duplicate dim '%s'", d.ID) |
| 430 | } |
| 431 | set[d.ID] = true |
| 432 | } |
| 433 | |
| 434 | set = make(map[string]bool) |
| 435 | |
| 436 | for _, v := range chart.Vars { |
| 437 | if err := checkVar(v); err != nil { |
| 438 | return err |
| 439 | } |
| 440 | if set[v.ID] { |
| 441 | return fmt.Errorf("duplicate var '%s'", v.ID) |
| 442 | } |
| 443 | set[v.ID] = true |
| 444 | } |
| 445 | return nil |
| 446 | } |
| 447 | |
| 448 | func checkDim(d *Dim) error { |
| 449 | if d.ID == "" { |
| 450 | return errors.New("empty dim ID") |
| 451 | } |
| 452 | if id := checkID(d.ID); id != -1 && (d.Name == "" || checkID(d.Name) != -1) { |
| 453 | return fmt.Errorf("unacceptable symbol in dim ID '%s' : '%c'", d.ID, id) |
| 454 | } |
| 455 | return nil |
| 456 | } |
| 457 | |
| 458 | func checkVar(v *Var) error { |
| 459 | if v.ID == "" { |
| 460 | return errors.New("empty var ID") |
| 461 | } |
| 462 | if id := checkID(v.ID); id != -1 { |
| 463 | return fmt.Errorf("unacceptable symbol in var ID '%s' : '%c'", v.ID, id) |
| 464 | } |
| 465 | return nil |
| 466 | } |
| 467 | |
| 468 | func checkID(id string) int { |
| 469 | for _, r := range id { |
| 470 | if unicode.IsSpace(r) { |
| 471 | return int(r) |
| 472 | } |
| 473 | } |
| 474 | return -1 |
| 475 | } |