github.com/go-graphite/carbonapi@v0.17.0/cmd/functiondiff/main.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "flag" 7 "fmt" 8 "io" 9 "log" 10 "net/http" 11 "sort" 12 "strings" 13 14 "github.com/go-graphite/carbonapi/expr/types" 15 ) 16 17 // It's ok to ignore values that's only in 'capi' as currently I don't care about superset of features. 18 func isUnsortedSuggestionSlicesEqual(capi, grWeb []types.Suggestion) []types.Suggestion { 19 capiMap := make(map[types.Suggestion]struct{}) 20 var diff []types.Suggestion 21 22 for _, v := range capi { 23 capiMap[v] = struct{}{} 24 } 25 26 for _, v := range grWeb { 27 if _, ok := capiMap[v]; !ok { 28 diff = append(diff, v) 29 } 30 } 31 return diff 32 } 33 34 func isFunctionParamEqual(fp1, fp2 types.FunctionParam) []string { 35 var incompatibilities []string 36 diffParams := isUnsortedSuggestionSlicesEqual(fp1.Options, fp2.Options) 37 if len(diffParams) > 0 { 38 // TODO(civil): Distingush and flag supersets (where we support more)\ 39 diffStr := make([]string, 0, len(diffParams)) 40 for _, v := range diffParams { 41 diffStr = append(diffStr, fmt.Sprintf("%v", v.Value)) 42 } 43 if len(fp1.Options) < len(fp2.Options) { 44 incompatibilities = append(incompatibilities, fmt.Sprintf("%v: different amount of parameters, `%+v` are missing", fp1.Name, diffStr)) 45 } 46 } 47 48 if fp1.Name != fp2.Name { 49 incompatibilities = append(incompatibilities, fmt.Sprintf("%v: name mismatch: got %v, should be %v", fp1.Name, fp1.Name, fp2.Name)) 50 } 51 52 if fp1.Multiple != fp2.Multiple { 53 incompatibilities = append(incompatibilities, fmt.Sprintf("%v: attribute `multiple` mismatch: got %v, should be %v", fp1.Name, fp1.Multiple, fp2.Multiple)) 54 } 55 56 if fp1.Type != fp2.Type { 57 v1 := types.FunctionTypeToStr[fp1.Type] 58 v2 := types.FunctionTypeToStr[fp2.Type] 59 incompatibilities = append(incompatibilities, fmt.Sprintf("%v: type mismatch: got %v, should be %v", fp1.Name, v1, v2)) 60 } 61 62 if fp1.Default != nil && fp2.Default != nil { 63 if fp1.Default.Type != fp2.Default.Type { 64 v1, _ := fp1.Default.MarshalJSON() 65 v2, _ := fp1.Default.MarshalJSON() 66 incompatibilities = append(incompatibilities, fmt.Sprintf("%v: default value's type mismatch: got %v, should be %v", fp1.Name, string(v1), string(v2))) 67 } 68 v1, _ := fp1.Default.MarshalJSON() 69 v2, _ := fp2.Default.MarshalJSON() 70 if !bytes.Equal(v1, v2) { 71 incompatibilities = append(incompatibilities, fmt.Sprintf("%v: default value mismatch: got %v, should be %v", fp1.Name, string(v1), string(v2))) 72 } 73 } 74 75 if fp1.Default == nil && fp2.Default != nil { 76 v2, _ := fp2.Default.MarshalJSON() 77 incompatibilities = append(incompatibilities, fmt.Sprintf("%v: default value mismatch: got %v, should be %v", fp1.Name, "(empty)", string(v2))) 78 } 79 80 if fp1.Default != nil && fp2.Default == nil { 81 v1, _ := fp1.Default.MarshalJSON() 82 incompatibilities = append(incompatibilities, fmt.Sprintf("%v: default value mismatch: got %v, should be %v", fp1.Name, string(v1), "(empty)")) 83 } 84 return incompatibilities 85 } 86 87 // type FunctionParam struct { 88 // Name string `json:"name"` 89 // Multiple bool `json:"multiple,omitempty"` 90 // Required bool `json:"required,omitempty"` 91 // Type FunctionType `json:"type,omitempty"` 92 // Options []string `json:"options,omitempty"` 93 // Suggestions []*Suggestion `json:"suggestions,omitempty"` 94 // Default *Suggestion `json:"default,omitempty"` 95 // } 96 func isFunctionParamsEqual(carbonapi, graphiteweb []types.FunctionParam) []string { 97 carbonapiToMap := make(map[string]types.FunctionParam) 98 var incompatibilities []string 99 100 for _, v := range carbonapi { 101 carbonapiToMap[v.Name] = v 102 } 103 104 for _, fp2 := range graphiteweb { 105 fp1, ok := carbonapiToMap[fp2.Name] 106 if !ok { 107 incompatibilities = append(incompatibilities, fmt.Sprintf("parameter not supported: %v", fp2.Name)) 108 continue 109 } 110 111 incompatibility := isFunctionParamEqual(fp1, fp2) 112 if len(incompatibility) != 0 { 113 incompatibilities = append(incompatibilities, incompatibility...) 114 } 115 } 116 117 return incompatibilities 118 } 119 120 func main() { 121 carbonapiURL := flag.String("carbonapi", "http://localhost:8079", "first server base url") 122 graphiteWebURL := flag.String("graphiteweb", "http://localhost:8082", "second server base url") 123 124 flag.Parse() 125 126 res, err := http.Get(*carbonapiURL + "/functions/") 127 if err != nil { 128 log.Fatal("failed to get response from carbonapi", err) 129 } 130 131 resp1, err := io.ReadAll(res.Body) 132 if err != nil { 133 log.Fatalf("failed to read response body for %v: %v", *carbonapiURL, err) 134 } 135 136 _ = res.Body.Close() 137 138 var firstDescription map[string]types.FunctionDescription 139 140 err = json.Unmarshal(resp1, &firstDescription) 141 if err != nil { 142 if e, ok := err.(*json.SyntaxError); ok { 143 log.Printf("syntax error at byte offset `%d`, error: %+v", e.Offset, e) 144 } 145 log.Fatalf("failed to Unmarshal carbonapi's description: `%v`", err) 146 } 147 148 res, err = http.Get(*graphiteWebURL + "/functions/?pretty=1") 149 if err != nil { 150 log.Fatalf("failed to read response body for %v: %v", *graphiteWebURL, err) 151 } 152 153 resp2, err := io.ReadAll(res.Body) 154 if err != nil { 155 log.Fatal("failed to read response body for graphiteWeb", err) 156 } 157 _ = res.Body.Close() 158 159 var secondDescription map[string]types.FunctionDescription 160 161 // Workaround for a case in json where parameter is set to `Infinity` which is not supported by GoLang's json parser 162 resp2 = bytes.ReplaceAll(resp2, []byte("\"default\": Infinity,"), []byte("\"default\": \"Infinity\",")) 163 164 err = json.Unmarshal(resp2, &secondDescription) 165 if err != nil { 166 if e, ok := err.(*json.SyntaxError); ok { 167 log.Printf("syntax error at byte offset `%d`, error: %+v", e.Offset, e) 168 } 169 log.Fatalf("failed to Unmarshal graphite-web's description: `%v`", err) 170 } 171 172 var carbonapiFunctions []string 173 var supportedFunctions []string 174 functionsWithIncompatibilities := make(map[string][]string) 175 var unsupportedFunctions []string 176 177 for k, grWeb := range secondDescription { 178 if capi, ok := firstDescription[k]; ok && !capi.Proxied { 179 incompatibilities := isFunctionParamsEqual(capi.Params, grWeb.Params) 180 supportedFunctions = append(supportedFunctions, capi.Function) 181 if len(incompatibilities) != 0 { 182 functionsWithIncompatibilities[k] = incompatibilities 183 } 184 } else { 185 unsupportedFunctions = append(unsupportedFunctions, k) 186 } 187 } 188 189 for k, v := range firstDescription { 190 if _, ok := secondDescription[k]; !ok { 191 carbonapiFunctions = append(carbonapiFunctions, v.Function) 192 } 193 } 194 195 sort.Strings(carbonapiFunctions) 196 sort.Strings(unsupportedFunctions) 197 sort.Strings(supportedFunctions) 198 199 res, err = http.Get(*graphiteWebURL + "/version/") 200 if err != nil { 201 log.Fatalf("failed to read response body for %v: %v", *graphiteWebURL, err) 202 } 203 204 resp2, err = io.ReadAll(res.Body) 205 if err != nil { 206 log.Fatal("failed to read response body for graphiteWeb", err) 207 } 208 _ = res.Body.Close() 209 version := strings.Trim(string(resp2), "\n") 210 211 fmt.Printf(`# CarbonAPI compatibility with Graphite 212 213 Topics: 214 * [Default settings](#default-settings) 215 * [URI Parameters](#uri-params) 216 * [Graphite-web 1.1 Compatibility](#graphite-web-11-compatibility) 217 * [Supported Functions](#supported-functions) 218 * [Features of configuration functions](#functions-features) 219 220 <a name="default-settings"></a> 221 ## Default Settings 222 223 ### Default Line Colors 224 Default colors for png or svg rendering intentionally specified like it is in graphite-web %s 225 226 You can redefine that in config to be more more precise. In default config example they are defined in the same way as in [original graphite PR to make them right](https://github.com/graphite-project/graphite-web/pull/2239) 227 228 Reason behind that change is that on dark background it's much nicer to read old colors than new one 229 230 <a name="uri-params"></a> 231 ## URI Parameters 232 233 ### /render/?... 234 235 * `+"`target` : graphite series, seriesList or function (likely containing series or seriesList)\n"+ 236 "* `from`, `until` : time specifiers. Eg. \"1d\", \"10min\", \"04:37_20150822\", \"now\", \"today\", ... (**NOTE** does not handle timezones the same as graphite)\n"+ 237 "* `format` : support graphite values of { json, raw, pickle, csv, png, svg } adds { protobuf } and does not support { pdf }\n"+ 238 "* `jsonp` : (...)\n"+ 239 "* `noCache` : prevent query-response caching (which is 60s if enabled)\n"+ 240 "* `cacheTimeout` : override default result cache (60s)\n"+ 241 "* `rawdata` -or- `rawData` : true for `format=raw`\n"+` 242 **Explicitly NOT supported** 243 * `+"`_salt`\n"+ 244 "* `_ts`\n"+ 245 "* `_t`\n"+` 246 _When `+"`format=png`_ (default if not specified)\n"+ 247 "* `width`, `height` : number of pixels (default: width=330 , height=250)\n"+ 248 "* `pixelRatio` : (1.0)\n"+ 249 "* `margin` : (10)\n"+ 250 "* `logBase` : Y-scale should use. Recognizes \"e\" or a floating point ( >= 1 )\n"+ 251 "* `fgcolor` : foreground color\n"+ 252 "* `bgcolor` : background color\n"+ 253 "* `majorLine` : major line color\n"+ 254 "* `minorLine` : minor line color\n"+ 255 "* `fontName` : (\"Sans\")\n"+ 256 "* `fontSize` : (10.0)\n"+ 257 "* `fontBold` : (false)\n"+ 258 "* `fontItalic` : (false)\n"+ 259 "* `graphOnly` : (false)\n"+ 260 "* `hideLegend` : (false) (**NOTE** if not defined and >10 result metrics this becomes true)\n"+ 261 "* `hideGrid` : (false)\n"+ 262 "* `hideAxes` : (false)\n"+ 263 "* `hideYAxis` : (false)\n"+ 264 "* `hideXAxis` : (false)\n"+ 265 "* `yAxisSide` : (\"left\")\n"+ 266 "* `connectedLimit` : number of missing points to bridge when `linemode` is not one of { \"slope\", \"staircase\" } likely \"connected\" (4294967296)\n"+ 267 "* `lineMode` : (\"slope\")\n"+ 268 "* `areaMode` : (\"none\") also recognizes { \"first\", \"all\", \"stacked\" }\n"+ 269 "* `areaAlpha` : ( <not defined> ) float value for area alpha\n"+ 270 "* `pieMode` : (\"average\") also recognizes { \"maximum\", \"minimum\" } (**NOTE** pie graph support is explicitly unplanned)\n"+ 271 "* `lineWidth` : (1.2) float value for line width\n"+ 272 "* `dashed` : (false) dashed lines\n"+ 273 "* `rightWidth` : (1.2) ...\n"+ 274 "* `rightDashed` : (false)\n"+ 275 "* `rightColor` : ...\n"+ 276 "* `leftWidth` : (1.2)\n"+ 277 "* `leftDashed` : (false)\n"+ 278 "* `leftColor` : ...\n"+ 279 "* `title` : (\"\") graph title\n"+ 280 "* `vtitle` : (\"\") ...\n"+ 281 "* `vtitleRight` : (\"\") ...\n"+ 282 "* `colorList` : (\"blue,green,red,purple,yellow,aqua,grey,magenta,pink,gold,rose\")\n"+ 283 "* `majorGridLineColor` : (\"rose\")\n"+ 284 "* `minorGridLineColor` : (\"grey\")\n"+ 285 "* `uniqueLegend` : (false)\n"+ 286 "* `drawNullAsZero` : (false) (**NOTE** affects display only - does not translate missing values to zero in functions. For that use ...)\n"+ 287 "* `drawAsInfinite` : (false) ...\n"+ 288 "* `yMin` : <undefined>\n"+ 289 "* `yMax` : <undefined>\n"+ 290 "* `yStep` : <undefined>\n"+ 291 "* `xMin` : <undefined>\n"+ 292 "* `xMax` : <undefined>\n"+ 293 "* `xStep` : <undefined>\n"+ 294 "* `xFormat` : (\"\") ...\n"+ 295 "* `minorY` : (1) ...\n"+ 296 "* `yMinLeft` : <undefined>\n"+ 297 "* `yMinRight` : <undefined>\n"+ 298 "* `yMaxLeft` : <undefined>\n"+ 299 "* `yMaxRight` : <undefined>\n"+ 300 "* `yStepL` : <undefined>\n"+ 301 "* `ySTepR` : <undefined>\n"+ 302 "* `yLimitLeft` : <undefined>\n"+ 303 "* `yLimitRight` : <undefined>\n"+ 304 "* `yUnitSystem` : (\"si\") also recognizes { \"binary\" }\n"+ 305 "* `yDivisors` : (4,5,6) ...\n"+` 306 ### /metrics/find/? 307 308 * `+"`format` : (\"treejson\") also recognizes { \"json\" (same as \"treejson\"), \"completer\", \"raw\" }\n"+ 309 "* `jsonp` : ...\n"+ 310 "* `query` : the metric or glob-pattern to find\n"+` 311 312 `, version) 313 fmt.Printf(` 314 315 ## Graphite-web %s compatibility 316 ### Unsupported functions 317 | Function | 318 | :------------------------------------------------------------------------ | 319 `, version) 320 for _, f := range unsupportedFunctions { 321 fmt.Printf("| %v |\n", f) 322 } 323 324 fmt.Println(` 325 326 ### Partly supported functions 327 | Function | Incompatibilities | 328 | :------------------------|:---------------------------------------------- |`) 329 330 keys := make([]string, 0, len(functionsWithIncompatibilities)) 331 for k := range functionsWithIncompatibilities { 332 keys = append(keys, k) 333 } 334 sort.Strings(keys) 335 for _, f := range keys { 336 fmt.Printf("| %v | %v |\n", f, strings.Join(functionsWithIncompatibilities[f], "\n")) 337 } 338 339 fmt.Println(` 340 ## Supported functions 341 | Function | Carbonapi-only | 342 | :-------------|:--------------------------------------------------------- |`) 343 344 for _, f := range supportedFunctions { 345 fmt.Printf("| %v | no |\n", f) 346 } 347 348 for _, f := range carbonapiFunctions { 349 fmt.Printf("| %v | yes |\n", f) 350 } 351 352 fmt.Println(`<a name="functions-features"></a> 353 ## Features of configuration functions 354 ### aliasByPostgres 355 1. Make config for function with pairs key-string - request 356 ` + "```" + `yaml 357 enabled: true 358 database: 359 "databaseAlias": 360 urlDB: "localhost:5432" 361 username: "portgres_user" 362 password: "postgres_password" 363 nameDB: "database_name" 364 keyString: 365 "resolve_switch_name_byId": 366 varName: "var" 367 queryString: "SELECT field_with_switch_name FROM some_table_with_switch_names_id_and_other WHERE field_with_switchID like 'var0';" 368 matchString: ".*" 369 "resolve_interface_description_from_table": 370 varName: "var" 371 queryString: "SELECT interface_desc FROM some_table_with_switch_data WHERE field_with_hostname like 'var0' AND field_with_interface_id like 'var1';" 372 matchString: ".*" 373 ` + "```" + ` 374 375 #### Examples 376 377 We have data series: 378 ` + "```" + ` 379 switches.switchId.CPU1Min 380 ` + "```" + ` 381 We need to get CPU load resolved by switchname, aliasByPostgres( switches.*.CPU1Min, databaseAlias, resolve_switch_name_byId, 1 ) will return series like this: 382 ` + "```" + ` 383 switchnameA 384 switchnameB 385 switchnameC 386 switchnameD 387 ` + "```" + ` 388 We have data series: 389 ` + "```" + ` 390 switches.hostname.interfaceID.scope_of_interface_metrics 391 ` + "```" + ` 392 We want to see interfaces stats sticked to their descriptions, aliasByPostgres(switches.hostname.*.ifOctets.rx, databaseAlias, resolve_interface_description_from_table, 1, 2 ) 393 will return series: 394 ` + "```" + ` 395 InterfaceADesc 396 InterfaceBDesc 397 InterfaceCDesc 398 InterfaceDDesc 399 ` + "```" + ` 400 401 2. Add to main config path to configuration file 402 ` + "```" + `yaml 403 functionsConfigs: 404 aliasByPostgres: /path/to/funcConfig.yaml 405 ` + "```" + ` 406 -----`) 407 408 }