github.com/go-graphite/carbonapi@v0.17.0/expr/functions/graphiteWeb/function.go (about)

     1  package graphiteWeb
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net"
     9  	"net/http"
    10  	"net/url"
    11  	"strconv"
    12  	"sync/atomic"
    13  	"time"
    14  
    15  	pb "github.com/go-graphite/protocol/carbonapi_v3_pb"
    16  	"github.com/lomik/zapwriter"
    17  	"github.com/spf13/viper"
    18  	"go.uber.org/zap"
    19  
    20  	"github.com/go-graphite/carbonapi/expr/interfaces"
    21  	"github.com/go-graphite/carbonapi/expr/metadata"
    22  	"github.com/go-graphite/carbonapi/expr/types"
    23  	"github.com/go-graphite/carbonapi/limiter"
    24  	"github.com/go-graphite/carbonapi/pkg/parser"
    25  )
    26  
    27  type graphiteWeb struct {
    28  	working      bool
    29  	strict       bool
    30  	maxTries     int
    31  	fallbackUrls []string
    32  	proxy        *http.Client
    33  
    34  	supportedFunctions map[string]types.FunctionDescription
    35  	limiter            limiter.ServerLimiter
    36  
    37  	logger         *zap.Logger
    38  	requestCounter uint64
    39  	timeout        time.Duration
    40  }
    41  
    42  func (f *graphiteWeb) pickServer() string {
    43  	sid := atomic.AddUint64(&f.requestCounter, 1)
    44  	return f.fallbackUrls[sid%uint64(len(f.fallbackUrls))]
    45  }
    46  
    47  func GetOrder() interfaces.Order {
    48  	return interfaces.Last
    49  }
    50  
    51  type graphiteWebConfig struct {
    52  	Enabled                  bool
    53  	FallbackUrls             []string
    54  	Strict                   bool
    55  	MaxConcurrentConnections int
    56  	MaxTries                 int
    57  	Timeout                  time.Duration
    58  	KeepAliveInterval        time.Duration
    59  	ForceSkip                []string
    60  	ForceAdd                 []string
    61  }
    62  
    63  func paramsIsEqual(first, second []types.FunctionParam) bool {
    64  	if len(first) != len(second) {
    65  		return false
    66  	}
    67  	for i, p1 := range first {
    68  		p2 := second[i]
    69  		equal := p1.Name == p2.Name && p1.Type == p2.Type
    70  		if !equal {
    71  			return false
    72  		}
    73  	}
    74  	return true
    75  }
    76  
    77  func New(configFile string) []interfaces.FunctionMetadata {
    78  	logger := zapwriter.Logger("functionInit").With(zap.String("function", "graphiteWeb"))
    79  	if configFile == "" {
    80  		logger.Debug("no config file specified",
    81  			zap.String("message", "this function requrires config file to work properly"),
    82  		)
    83  		return nil
    84  	}
    85  	v := viper.New()
    86  	v.SetConfigFile(configFile)
    87  	err := v.ReadInConfig()
    88  	if err != nil {
    89  		logger.Fatal("failed to read config file",
    90  			zap.Error(err),
    91  		)
    92  		return nil
    93  	}
    94  
    95  	cfg := graphiteWebConfig{
    96  		Enabled:                  false,
    97  		Strict:                   false,
    98  		MaxConcurrentConnections: 10,
    99  		Timeout:                  60 * time.Second,
   100  		KeepAliveInterval:        30 * time.Second,
   101  		MaxTries:                 3,
   102  	}
   103  	err = v.Unmarshal(&cfg)
   104  	if err != nil {
   105  		logger.Fatal("failed to parse config",
   106  			zap.Error(err),
   107  		)
   108  		return nil
   109  	}
   110  
   111  	if !cfg.Enabled {
   112  		logger.Warn("graphiteWeb config found but graphiteWeb proxy is disabled")
   113  		return nil
   114  	}
   115  
   116  	logger.Info("graphiteWeb configured",
   117  		zap.Any("config", cfg),
   118  		zap.String("config_file", configFile),
   119  	)
   120  
   121  	f := &graphiteWeb{
   122  		limiter: limiter.NewServerLimiter(cfg.FallbackUrls, cfg.MaxConcurrentConnections),
   123  		proxy: &http.Client{
   124  			Transport: &http.Transport{
   125  				MaxIdleConnsPerHost: cfg.MaxConcurrentConnections,
   126  				DialContext: (&net.Dialer{
   127  					Timeout:   cfg.Timeout,
   128  					KeepAlive: cfg.KeepAliveInterval,
   129  					DualStack: true,
   130  				}).DialContext,
   131  			},
   132  		},
   133  		fallbackUrls: cfg.FallbackUrls,
   134  		strict:       cfg.Strict,
   135  		maxTries:     cfg.MaxTries,
   136  		working:      false,
   137  		timeout:      cfg.Timeout,
   138  		logger:       zapwriter.Logger("graphiteWeb"),
   139  		supportedFunctions: map[string]types.FunctionDescription{
   140  			"graphiteWeb": {
   141  				Description: `This is special function which will pass everything inside to graphiteWeb (if configured)
   142  
   143  This function will pass everything inside of it to graphite-web and return result to any function above it
   144  
   145  If configured, it will also auto-register everything that's not supported by carbonapi as a passthrough to graphite-web 
   146  Example:
   147      target=sum(graphiteWeb(smartSummarize(foo.bar.*, '15min'))
   148  
   149  smartSummarise will be performed by graphite-web and then results will be passed to sum, that will be performed by carbonapi
   150  `,
   151  				Function: "graphiteWeb(seriesList)",
   152  				Group:    "Fallback",
   153  				Module:   "graphite.render.fallback.custom",
   154  				Name:     "graphiteWeb",
   155  				Params: []types.FunctionParam{
   156  					{
   157  						Name:     "seriesList",
   158  						Required: true,
   159  						Type:     types.SeriesList,
   160  					},
   161  				},
   162  			},
   163  		},
   164  	}
   165  
   166  	ok := false
   167  	var body []byte
   168  	for i := 0; i < len(f.fallbackUrls); i++ {
   169  		srv := f.fallbackUrls[i]
   170  		req, err := http.NewRequest("GET", srv+"/functions/?format=json", nil)
   171  		if err != nil {
   172  			logger.Warn("failed to create list of functions, will try next fallbackUrl",
   173  				zap.String("backend", srv),
   174  				zap.Error(err),
   175  			)
   176  			continue
   177  		}
   178  
   179  		resp, err := f.proxy.Do(req)
   180  		if err != nil {
   181  			logger.Warn("failed to obtain list of functions, will try next fallbackUrl",
   182  				zap.String("backend", srv),
   183  				zap.Error(err),
   184  			)
   185  			continue
   186  		}
   187  
   188  		body, err = io.ReadAll(resp.Body)
   189  		if err != nil {
   190  			logger.Warn("failed to obtain list of functions, will try next fallbackUrl",
   191  				zap.String("backend", srv),
   192  				zap.Error(fmt.Errorf("return code is not 200 OK")),
   193  				zap.Int("status_code", resp.StatusCode),
   194  			)
   195  			_ = resp.Body.Close()
   196  			continue
   197  		}
   198  
   199  		if resp.StatusCode != http.StatusOK {
   200  			logger.Warn("failed to obtain list of functions, will try next fallbackUrl",
   201  				zap.String("backend", srv),
   202  				zap.Error(fmt.Errorf("return code is not 200 OK")),
   203  				zap.Int("status_code", resp.StatusCode),
   204  				zap.String("body", string(body)),
   205  			)
   206  			_ = resp.Body.Close()
   207  			continue
   208  		}
   209  		_ = resp.Body.Close()
   210  		ok = true
   211  		break
   212  	}
   213  
   214  	if !ok {
   215  		logger.Error("failed to initialize graphiteWeb fallback function",
   216  			zap.Error(fmt.Errorf("no more backends to try, see warnings above for more details")),
   217  		)
   218  		return nil
   219  	}
   220  
   221  	forceAdd := make(map[string]struct{})
   222  	for _, n := range cfg.ForceAdd {
   223  		forceAdd[n] = struct{}{}
   224  	}
   225  
   226  	forceSkip := make(map[string]struct{})
   227  	for _, n := range cfg.ForceSkip {
   228  		forceSkip[n] = struct{}{}
   229  	}
   230  
   231  	graphiteWebSupportedFunctions := make(map[string]types.FunctionDescription)
   232  
   233  	err = json.Unmarshal(body, &graphiteWebSupportedFunctions)
   234  	if err != nil {
   235  		logger.Error("failed to parse list of functions",
   236  			zap.Error(err),
   237  		)
   238  		return nil
   239  	}
   240  
   241  	functions := []string{"graphiteWeb"}
   242  	metadata.FunctionMD.RLock()
   243  	for k, v := range graphiteWebSupportedFunctions {
   244  		var ok bool
   245  		if _, ok = forceSkip[k]; ok {
   246  			continue
   247  		}
   248  
   249  		if _, ok = forceAdd[k]; ok {
   250  			functions = append(functions, k)
   251  			v.Proxied = true
   252  			f.supportedFunctions[k] = v
   253  			continue
   254  		}
   255  
   256  		if v2, ok := metadata.FunctionMD.Descriptions[k]; ok {
   257  			if f.strict {
   258  				ok = paramsIsEqual(v.Params, v2.Params)
   259  			}
   260  			if ok {
   261  				continue
   262  			}
   263  		}
   264  
   265  		functions = append(functions, k)
   266  		v.Proxied = true
   267  		f.supportedFunctions[k] = v
   268  	}
   269  	metadata.FunctionMD.RUnlock()
   270  
   271  	f.working = true
   272  
   273  	logger.Info("will handle following functions",
   274  		zap.Strings("functions_metadata", functions),
   275  	)
   276  
   277  	res := make([]interfaces.FunctionMetadata, 0, len(functions))
   278  	for _, n := range functions {
   279  		res = append(res, interfaces.FunctionMetadata{Name: n, F: f, Order: interfaces.Any})
   280  	}
   281  	return res
   282  }
   283  
   284  type target string
   285  
   286  func (t *target) UnmarshalJSON(d []byte) error {
   287  	var res interface{}
   288  	err := json.Unmarshal(d, &res)
   289  	if err != nil {
   290  		return err
   291  	}
   292  	switch v := res.(type) {
   293  	case int:
   294  		*t = target(strconv.FormatInt(int64(v), 10))
   295  	case int32:
   296  		*t = target(strconv.FormatInt(int64(v), 10))
   297  	case int64:
   298  		*t = target(strconv.FormatInt(v, 10))
   299  	case float64:
   300  		*t = target(strconv.FormatFloat(v, 'f', -1, 64))
   301  	case string:
   302  		*t = target(v)
   303  	case bool:
   304  		*t = target(strconv.FormatBool(v))
   305  	default:
   306  		return fmt.Errorf("unsupported type for target")
   307  	}
   308  
   309  	return nil
   310  }
   311  
   312  type graphiteMetric struct {
   313  	Tags              map[string]json.RawMessage
   314  	Target            target
   315  	PathExpression    target
   316  	Datapoints        [][2]float64
   317  	XFilesFactor      float32
   318  	ConsolidationFunc string
   319  }
   320  
   321  type graphiteError struct {
   322  	server string
   323  	err    error
   324  }
   325  
   326  func (f *graphiteWeb) Do(ctx context.Context, eval interfaces.Evaluator, e parser.Expr, from, until int64, values map[parser.MetricRequest][]*types.MetricData) ([]*types.MetricData, error) {
   327  	f.logger.Info("received request",
   328  		zap.Bool("working", f.working),
   329  	)
   330  	if !f.working {
   331  		return nil, nil
   332  	}
   333  
   334  	var target string
   335  	if e.Target() == "graphiteWeb" {
   336  		target = e.RawArgs()
   337  	} else {
   338  		target = e.ToString()
   339  	}
   340  
   341  	var body []byte
   342  	var srv string
   343  	var request string
   344  	var errors []graphiteError
   345  	ok := false
   346  	for i := 0; i < f.maxTries; i++ {
   347  		srv = f.pickServer()
   348  		rewrite, _ := url.Parse(srv + "/render/")
   349  		v := url.Values{
   350  			"target": []string{target},
   351  			"from":   []string{strconv.FormatInt(from, 10)},
   352  			"until":  []string{strconv.FormatInt(until, 10)},
   353  			"format": []string{"json"},
   354  		}
   355  
   356  		rewrite.RawQuery = v.Encode()
   357  
   358  		ctx, cancel := context.WithTimeout(context.Background(), f.timeout)
   359  		defer cancel()
   360  		err := f.limiter.Enter(context.Background(), srv)
   361  		if err != nil {
   362  			// Timeout waiting for a new slot
   363  			return nil, err
   364  		}
   365  
   366  		req, err := http.NewRequest("GET", rewrite.String(), nil)
   367  		if err != nil {
   368  			f.limiter.Leave(ctx, srv)
   369  			return nil, err
   370  		}
   371  
   372  		resp, err := f.proxy.Do(req.WithContext(ctx))
   373  		f.limiter.Leave(ctx, srv)
   374  		if err != nil {
   375  			errors = append(errors, graphiteError{srv, err})
   376  			_ = resp.Body.Close()
   377  			continue
   378  		}
   379  
   380  		body, err = io.ReadAll(resp.Body)
   381  		if err != nil {
   382  			errors = append(errors, graphiteError{srv, err})
   383  			_ = resp.Body.Close()
   384  			continue
   385  		}
   386  
   387  		if resp.StatusCode != http.StatusOK {
   388  			_ = resp.Body.Close()
   389  			err := fmt.Errorf("return code is not 200 OK, code: %v, body: %v", resp.StatusCode, string(body))
   390  			errors = append(errors, graphiteError{srv, err})
   391  			continue
   392  		}
   393  		_ = resp.Body.Close()
   394  		ok = true
   395  		request = rewrite.String()
   396  		break
   397  	}
   398  
   399  	if !ok {
   400  		f.logger.Error("failed to get response from graphite-web, max tries exceeded",
   401  			zap.Any("errors", errors),
   402  		)
   403  		return nil, fmt.Errorf("max tries exceeded for request target=%v", target)
   404  	}
   405  
   406  	f.logger.Debug("got response",
   407  		zap.String("request", request),
   408  		zap.String("body", string(body)),
   409  	)
   410  
   411  	var tmp []graphiteMetric
   412  
   413  	err := json.Unmarshal(body, &tmp)
   414  	if err != nil {
   415  		return nil, err
   416  	}
   417  
   418  	res := make([]*types.MetricData, 0, len(tmp))
   419  
   420  	for _, m := range tmp {
   421  		stepTime := int64(60)
   422  		if len(m.Datapoints) > 1 {
   423  			stepTime = int64(m.Datapoints[1][1] - m.Datapoints[0][1])
   424  		}
   425  
   426  		if m.ConsolidationFunc == "" {
   427  			m.ConsolidationFunc = "avg"
   428  		}
   429  
   430  		pbResp := pb.FetchResponse{
   431  			Name:              string(m.Target),
   432  			StartTime:         int64(m.Datapoints[0][1]),
   433  			StopTime:          int64(m.Datapoints[len(m.Datapoints)-1][1]),
   434  			StepTime:          stepTime,
   435  			Values:            make([]float64, len(m.Datapoints)),
   436  			XFilesFactor:      m.XFilesFactor,
   437  			PathExpression:    string(m.PathExpression),
   438  			ConsolidationFunc: m.ConsolidationFunc,
   439  		}
   440  		tags := make(map[string]string, len(m.Tags))
   441  		for tag, rawValue := range m.Tags {
   442  			var value string
   443  			err = json.Unmarshal(rawValue, &value)
   444  			// TODO(civil): check if invalid message can ever occur
   445  			// We are currently ignoring all invalid tags
   446  			if err != nil {
   447  				continue
   448  			}
   449  			tags[tag] = value
   450  		}
   451  
   452  		for i, v := range m.Datapoints {
   453  			pbResp.Values[i] = v[0]
   454  		}
   455  		res = append(res, &types.MetricData{
   456  			FetchResponse: pbResp,
   457  			Tags:          tags,
   458  		})
   459  	}
   460  
   461  	return res, nil
   462  }
   463  
   464  func (f *graphiteWeb) Description() map[string]types.FunctionDescription {
   465  	return f.supportedFunctions
   466  }