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  }