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 }