github.com/go-graphite/carbonapi@v0.17.0/zipper/protocols/irondb/irondb_group.go (about) 1 package irondb 2 3 import ( 4 "context" 5 "fmt" 6 "math" 7 "net/url" 8 "strings" 9 "time" 10 11 "github.com/ansel1/merry" 12 "github.com/circonus-labs/gosnowth" 13 protov3 "github.com/go-graphite/protocol/carbonapi_v3_pb" 14 "go.uber.org/zap" 15 16 "github.com/go-graphite/carbonapi/limiter" 17 "github.com/go-graphite/carbonapi/zipper/metadata" 18 "github.com/go-graphite/carbonapi/zipper/types" 19 ) 20 21 func init() { 22 aliases := []string{"irondb", "snowthd"} 23 metadata.Metadata.Lock() 24 for _, name := range aliases { 25 metadata.Metadata.SupportedProtocols[name] = struct{}{} 26 metadata.Metadata.ProtocolInits[name] = New 27 metadata.Metadata.ProtocolInitsWithLimiter[name] = NewWithLimiter 28 } 29 defer metadata.Metadata.Unlock() 30 } 31 32 // IronDBGroup is a protocol group that can query IronDB servers 33 type IronDBGroup struct { 34 types.BackendServer 35 36 groupName string 37 servers []string 38 protocol string 39 40 client *gosnowth.SnowthClient 41 42 limiter limiter.ServerLimiter 43 logger *zap.Logger 44 timeout types.Timeouts 45 maxTries int 46 maxMetricsPerRequest int 47 48 accountID int64 49 graphiteRollup int64 50 graphitePrefix string 51 } 52 53 func NewWithLimiter(logger *zap.Logger, config types.BackendV2, tldCacheDisabled, requireSuccessAll bool, limiter limiter.ServerLimiter) (types.BackendServer, merry.Error) { 54 logger = logger.With(zap.String("type", "irondb"), zap.String("protocol", config.Protocol), zap.String("name", config.GroupName)) 55 56 logger.Warn("support for this backend protocol is experimental, use with caution") 57 58 // initializing config with list of servers from upstream 59 cfg := gosnowth.NewConfig(config.Servers...) 60 61 // enabling discovery. 62 // cfg.Discover = true 63 64 // parse backend options 65 var tmpInt int 66 accountID := int64(1) 67 if accountIDOpt, ok := config.BackendOptions["irondb_account_id"]; ok { 68 if tmpInt, ok = accountIDOpt.(int); !ok { 69 logger.Fatal("failed to parse irondb_account_id", 70 zap.String("type_parsed", fmt.Sprintf("%T", accountIDOpt)), 71 zap.String("type_expected", "int"), 72 ) 73 } 74 accountID = int64(tmpInt) 75 } 76 77 maxTries := int64(*config.MaxTries) 78 retries := int64(0) 79 if retriesOpt, ok := config.BackendOptions["irondb_retries"]; ok { 80 if tmpInt, ok = retriesOpt.(int); !ok { 81 logger.Fatal("failed to parse irondb_retries", 82 zap.String("type_parsed", fmt.Sprintf("%T", retriesOpt)), 83 zap.String("type_expected", "int"), 84 ) 85 } 86 retries = int64(tmpInt) 87 } 88 if maxTries > retries { 89 retries = maxTries 90 } 91 cfg.Retries = retries 92 93 connectRetries := int64(-1) 94 if connectRetriesOpt, ok := config.BackendOptions["irondb_connect_retries"]; ok { 95 if tmpInt, ok = connectRetriesOpt.(int); !ok { 96 logger.Fatal("failed to parse irondb_connect_retries", 97 zap.String("type_parsed", fmt.Sprintf("%T", connectRetriesOpt)), 98 zap.String("type_expected", "int"), 99 ) 100 } 101 connectRetries = int64(tmpInt) 102 } 103 cfg.ConnectRetries = connectRetries 104 105 var tmpStr string 106 dialTimeout := 500 * time.Millisecond 107 if dialTimeoutOpt, ok := config.BackendOptions["irondb_dial_timeout"]; ok { 108 if tmpStr, ok = dialTimeoutOpt.(string); ok { 109 interval, err := time.ParseDuration(tmpStr) 110 if err != nil { 111 logger.Fatal("failed to parse option", 112 zap.String("option_name", "irondb_dial_timeout"), 113 zap.String("option_value", tmpStr), 114 zap.Errors("errors", []error{err}), 115 ) 116 } 117 dialTimeout = interval 118 } else { 119 logger.Fatal("failed to parse option", 120 zap.String("option_name", "irondb_dial_timeout"), 121 zap.Any("option_value", tmpStr), 122 zap.Errors("errors", []error{fmt.Errorf("not a string")}), 123 ) 124 } 125 } 126 cfg.DialTimeout = dialTimeout 127 128 irondbTimeout := 10 * time.Second 129 if irondbTimeoutOpt, ok := config.BackendOptions["irondb_timeout"]; ok { 130 if tmpStr, ok = irondbTimeoutOpt.(string); ok { 131 interval, err := time.ParseDuration(tmpStr) 132 if err != nil { 133 logger.Fatal("failed to parse option", 134 zap.String("option_name", "irondb_timeout"), 135 zap.String("option_value", tmpStr), 136 zap.Errors("errors", []error{err}), 137 ) 138 } 139 irondbTimeout = interval 140 } else { 141 logger.Fatal("failed to parse option", 142 zap.String("option_name", "irondb_timeout"), 143 zap.Any("option_value", tmpStr), 144 zap.Errors("errors", []error{fmt.Errorf("not a string")}), 145 ) 146 } 147 } 148 cfg.Timeout = irondbTimeout 149 150 watchInterval := 30 * time.Second 151 if watchIntervalOpt, ok := config.BackendOptions["irondb_watch_interval"]; ok { 152 if tmpStr, ok = watchIntervalOpt.(string); ok { 153 interval, err := time.ParseDuration(tmpStr) 154 if err != nil { 155 logger.Fatal("failed to parse option", 156 zap.String("option_name", "irondb_watch_interval"), 157 zap.String("option_value", tmpStr), 158 zap.Errors("errors", []error{err}), 159 ) 160 } 161 watchInterval = interval 162 } else { 163 logger.Fatal("failed to parse option", 164 zap.String("option_name", "irondb_watch_interval"), 165 zap.Any("option_value", tmpStr), 166 zap.Errors("errors", []error{fmt.Errorf("not a string")}), 167 ) 168 } 169 } 170 cfg.WatchInterval = watchInterval 171 172 graphiteRollup := int64(60) 173 if graphiteRollupOpt, ok := config.BackendOptions["irondb_graphite_rollup"]; ok { 174 if tmpInt, ok = graphiteRollupOpt.(int); !ok { 175 logger.Fatal("failed to parse irondb_graphite_rollup", 176 zap.String("type_parsed", fmt.Sprintf("%T", graphiteRollupOpt)), 177 zap.String("type_expected", "int"), 178 ) 179 } 180 graphiteRollup = int64(tmpInt) 181 } 182 183 graphitePrefix := "" 184 if graphitePrefixOpt, ok := config.BackendOptions["irondb_graphite_prefix"]; ok { 185 if tmpStr, ok = graphitePrefixOpt.(string); !ok { 186 logger.Fatal("failed to parse irondb_graphite_prefix", 187 zap.String("type_parsed", fmt.Sprintf("%T", graphitePrefixOpt)), 188 zap.String("type_expected", "string"), 189 ) 190 } 191 graphitePrefix = tmpStr 192 } 193 194 snowthClient, err := gosnowth.NewClient(context.Background(), cfg) 195 if err != nil { 196 logger.Fatal("failed to create snowth client", 197 zap.Error(err)) 198 } 199 200 c := &IronDBGroup{ 201 groupName: config.GroupName, 202 servers: config.Servers, 203 protocol: config.Protocol, 204 timeout: *config.Timeouts, 205 maxTries: *config.MaxTries, 206 maxMetricsPerRequest: *config.MaxBatchSize, 207 client: snowthClient, 208 accountID: accountID, 209 graphiteRollup: graphiteRollup, 210 graphitePrefix: graphitePrefix, 211 limiter: limiter, 212 logger: logger, 213 } 214 215 return c, nil 216 } 217 218 func New(logger *zap.Logger, config types.BackendV2, tldCacheDisabled, requireSuccessAll bool) (types.BackendServer, merry.Error) { 219 if config.ConcurrencyLimit == nil { 220 return nil, types.ErrConcurrencyLimitNotSet 221 } 222 if len(config.Servers) == 0 { 223 return nil, types.ErrNoServersSpecified 224 } 225 l := limiter.NewServerLimiter([]string{config.GroupName}, *config.ConcurrencyLimit) 226 227 return NewWithLimiter(logger, config, tldCacheDisabled, requireSuccessAll, l) 228 } 229 230 func (c *IronDBGroup) Children() []types.BackendServer { 231 return []types.BackendServer{c} 232 } 233 234 func (c IronDBGroup) MaxMetricsPerRequest() int { 235 return c.maxMetricsPerRequest 236 } 237 238 func (c IronDBGroup) Name() string { 239 return c.groupName 240 } 241 242 func (c IronDBGroup) Backends() []string { 243 return c.servers 244 } 245 246 func processFindErrors(err error, e merry.Error, stats *types.Stats, query string) merry.Error { 247 stats.FindErrors++ 248 if merry.Is(err, types.ErrTimeoutExceeded) { 249 stats.Timeouts++ 250 stats.FindTimeouts++ 251 } 252 if e == nil { 253 e = merry.Wrap(err).WithValue("query", query) 254 } else { 255 e = e.WithCause(err) 256 } 257 return e 258 } 259 260 func processRenderErrors(err error, e merry.Error, stats *types.Stats, query string) merry.Error { 261 stats.RenderErrors++ 262 if merry.Is(err, types.ErrTimeoutExceeded) { 263 stats.Timeouts++ 264 stats.RenderTimeouts++ 265 } 266 if e == nil { 267 e = merry.Wrap(err).WithValue("query", query) 268 } else { 269 e = e.WithCause(err) 270 } 271 return e 272 } 273 274 func (c *IronDBGroup) Fetch(ctx context.Context, request *protov3.MultiFetchRequest) (*protov3.MultiFetchResponse, *types.Stats, merry.Error) { 275 logger := c.logger.With(zap.String("type", "fetch"), zap.String("request", request.String())) 276 stats := &types.Stats{} 277 278 pathExprToTargets := make(map[string][]string) 279 for _, m := range request.Metrics { 280 pathExprToTargets[m.PathExpression] = append(pathExprToTargets[m.PathExpression], m.Name) 281 } 282 283 var r protov3.MultiFetchResponse 284 var e merry.Error 285 286 start := request.Metrics[0].StartTime 287 stop := request.Metrics[0].StopTime 288 step := c.graphiteRollup 289 count := int64((stop-start)/step) + 1 290 maxCount := request.Metrics[0].MaxDataPoints 291 if count > maxCount && maxCount > 0 { 292 count = maxCount 293 step = adjustStep(start, stop, maxCount, step) 294 } 295 if count <= 0 { 296 logger.Fatal("stop time should be less then start", 297 zap.Int64("start", start), 298 zap.Int64("stop", stop), 299 ) 300 } 301 for pathExpr, targets := range pathExprToTargets { 302 for _, target := range targets { 303 logger.Debug("got some target to query", 304 zap.Any("pathExpr", pathExpr), 305 zap.String("target", target), 306 zap.Int64("start", start), 307 zap.Int64("stop", stop), 308 zap.Int64("count", count), 309 zap.Int64("period", step), 310 ) 311 312 var query string 313 if strings.HasPrefix(target, "seriesByTag") { 314 query = target[12 : len(target)-1] // 12 is len("seriesByTag(") 315 // d-oh, fetch_multi graphite compatible API not working with tags 316 // fallback to IRONdb Fetch API 317 query = graphiteExprListToIronDBTagQuery(strings.Split(query, ",")) 318 findTagOptions := &gosnowth.FindTagsOptions{ 319 // start and stop according to request 320 Start: time.Unix(start, 0), 321 End: time.Unix(stop, 0), 322 Activity: 0, 323 Latest: 0, 324 CountOnly: 0, 325 Limit: -1, 326 } 327 logger.Debug("send tag find result to irondb", 328 zap.String("query", query), 329 zap.Any("findTagOptions", findTagOptions), 330 ) 331 stats.FindRequests++ 332 tagMetrics, err := c.client.FindTags(c.accountID, query, findTagOptions) 333 if err != nil { 334 e = processFindErrors(err, e, stats, query) 335 continue 336 } 337 logger.Debug("got tag find result from irondb", 338 zap.String("query", query), 339 zap.Any("tagMetrics", tagMetrics), 340 ) 341 responses := []*gosnowth.DF4Response{} 342 for _, metric := range tagMetrics.Items { 343 stats.RenderRequests++ 344 res, err2 := c.client.FetchValues(&gosnowth.FetchQuery{ 345 Start: time.Unix(start, 0), 346 Period: time.Duration(step) * time.Second, 347 Count: count, 348 Streams: []gosnowth.FetchStream{{ 349 UUID: metric.UUID, 350 Name: metric.MetricName, 351 Kind: metric.Type, 352 Label: metric.MetricName, 353 Transform: "average", 354 }}, 355 Reduce: []gosnowth.FetchReduce{{ 356 Label: "pass", 357 Method: "pass", 358 }}, 359 }) 360 if err2 != nil { 361 e = processRenderErrors(err2, e, stats, query) 362 continue 363 } 364 responses = append(responses, res) 365 } 366 logger.Debug("got fetch result from irondb", 367 zap.String("query", query), 368 zap.Any("responses", responses), 369 ) 370 for _, response := range responses { 371 // We always should trust backend's response (to mimic behavior of graphite for grahpite native protoocols) 372 // See https://github.com/go-graphite/carbonapi/issues/504 and https://github.com/go-graphite/carbonapi/issues/514 373 realStart := start 374 realStop := stop 375 if len(response.Data) > 0 { 376 realStart = response.Head.Start 377 realStop = response.Head.Start + response.Head.Period*(response.Head.Count-1) 378 } 379 for i, meta := range response.Meta { 380 values := make([]float64, (realStop-realStart)/step+1) 381 for _, data := range response.Data[i] { 382 dv, ok := data.(float64) 383 if ok { 384 values = append(values, dv) 385 } else { 386 values = append(values, math.NaN()) 387 } 388 } 389 name := convertNameToGraphite(meta.Label) 390 r.Metrics = append(r.Metrics, protov3.FetchResponse{ 391 Name: name, 392 PathExpression: pathExpr, 393 ConsolidationFunc: "Average", 394 StartTime: realStart, 395 StopTime: realStop, 396 StepTime: step, 397 Values: values, 398 XFilesFactor: 0.0, 399 }) 400 } 401 } 402 continue 403 } 404 // target is not starting with seriesByTag 405 // we can use graphite compatible API to fetch non-tagged metrics 406 query = target 407 logger.Debug("send metric find result to irondb", 408 zap.String("query", query), 409 ) 410 stats.FindRequests++ 411 metrics, err := c.client.GraphiteFindMetrics(c.accountID, c.graphitePrefix, query, nil) 412 if err != nil { 413 e = processFindErrors(err, e, stats, query) 414 continue 415 } 416 logger.Debug("got find result from irondb", 417 zap.String("target", query), 418 zap.Any("metrics", metrics), 419 ) 420 // if we found no metrics - good luck with next target 421 if len(metrics) == 0 { 422 continue 423 } 424 names := make([]string, 0, len(metrics)+1) 425 for _, metric := range metrics { 426 names = append(names, metric.Name) 427 } 428 lookup := &gosnowth.GraphiteLookup{ 429 Start: start, 430 End: stop, 431 Names: names, 432 } 433 stats.RenderRequests++ 434 logger.Debug("send render request to irondb", 435 zap.String("query", query), 436 zap.Any("lookup", lookup), 437 ) 438 response, err2 := c.client.GraphiteGetDatapoints(c.accountID, c.graphitePrefix, lookup, nil) 439 if err2 != nil { 440 e = processRenderErrors(err2, e, stats, query) 441 continue 442 } 443 logger.Debug("got fetch result from irondb", 444 zap.String("query", query), 445 zap.Any("response", response), 446 ) 447 // We always should trust backend's response (to mimic behavior of graphite for grahpite native protoocols) 448 // See https://github.com/go-graphite/carbonapi/issues/504 and https://github.com/go-graphite/carbonapi/issues/514 449 realStart := start 450 realStop := stop 451 if len(response.Series) > 0 { 452 realStart = response.From 453 realStop = response.To 454 } 455 for name, values := range response.Series { 456 label := convertNameToGraphite(name) 457 vals := make([]float64, 0, (realStop-realStart)/step+1) 458 for _, data := range values { 459 if data != nil { 460 vals = append(vals, *data) 461 } else { 462 vals = append(vals, math.NaN()) 463 } 464 } 465 r.Metrics = append(r.Metrics, protov3.FetchResponse{ 466 Name: label, 467 PathExpression: pathExpr, 468 ConsolidationFunc: "Average", 469 StartTime: realStart, 470 StopTime: realStop, 471 StepTime: response.Step, 472 Values: vals, 473 XFilesFactor: 0.0, 474 }) 475 } 476 } 477 } 478 if e != nil { 479 stats.FailedServers = []string{c.groupName} 480 logger.Error("errors occurred while getting results", 481 zap.Any("errors", e), 482 ) 483 return &r, stats, e 484 } 485 return &r, stats, nil 486 } 487 488 func (c *IronDBGroup) Find(ctx context.Context, request *protov3.MultiGlobRequest) (*protov3.MultiGlobResponse, *types.Stats, merry.Error) { 489 logger := c.logger.With(zap.String("type", "find"), zap.Strings("request", request.Metrics)) 490 stats := &types.Stats{} 491 492 r := protov3.MultiGlobResponse{ 493 Metrics: make([]protov3.GlobResponse, 0), 494 } 495 var e merry.Error 496 497 for _, query := range request.Metrics { 498 resp := protov3.GlobResponse{ 499 Name: query, 500 Matches: make([]protov3.GlobMatch, 0), 501 } 502 503 logger.Debug("will do find query", 504 zap.Int64("accountID", c.accountID), 505 zap.String("query", query), 506 zap.String("prefix", c.graphitePrefix), 507 ) 508 stats.FindRequests++ 509 findResult, err := c.client.GraphiteFindMetrics(c.accountID, c.graphitePrefix, query, nil) 510 if err != nil { 511 e = processFindErrors(err, e, stats, query) 512 continue 513 } 514 logger.Debug("got find result from irondb", 515 zap.String("query", query), 516 zap.Any("result", findResult), 517 ) 518 519 for _, metric := range findResult { 520 name := convertNameToGraphite(metric.Name) 521 resp.Matches = append(resp.Matches, protov3.GlobMatch{ 522 IsLeaf: metric.Leaf, 523 Path: name, 524 }) 525 r.Metrics = append(r.Metrics, resp) 526 } 527 logger.Debug("parsed find result", 528 zap.Any("result", r.Metrics), 529 zap.Int64("start", request.StartTime), 530 zap.Int64("stop", request.StopTime), 531 ) 532 } 533 534 if e != nil { 535 logger.Error("errors occurred while getting results", 536 zap.Any("errors", e), 537 ) 538 return &r, stats, e 539 } 540 return &r, stats, nil 541 } 542 543 func (c *IronDBGroup) Info(ctx context.Context, request *protov3.MultiMetricsInfoRequest) (*protov3.ZipperInfoResponse, *types.Stats, merry.Error) { 544 return nil, nil, types.ErrNotSupportedByBackend 545 } 546 547 func (c *IronDBGroup) List(ctx context.Context) (*protov3.ListMetricsResponse, *types.Stats, merry.Error) { 548 return nil, nil, types.ErrNotImplementedYet 549 } 550 551 func (c *IronDBGroup) Stats(ctx context.Context) (*protov3.MetricDetailsResponse, *types.Stats, merry.Error) { 552 return nil, nil, types.ErrNotSupportedByBackend 553 } 554 555 func (c *IronDBGroup) doTagQuery(ctx context.Context, isTagName bool, query string, limit int64) ([]string, merry.Error) { 556 logger := c.logger 557 params := make(map[string][]string) 558 var result []string 559 var target string 560 var tagCategory string 561 562 // decoding query 563 queryDecoded, _ := url.QueryUnescape(query) 564 querySplit := strings.Split(queryDecoded, "&") 565 for _, qvRaw := range querySplit { 566 idx := strings.Index(qvRaw, "=") 567 // no parameters passed 568 if idx < 1 { 569 continue 570 } 571 k := qvRaw[:idx] 572 v := qvRaw[idx+1:] 573 if v2, ok := params[qvRaw[:idx]]; !ok { 574 params[k] = []string{v} 575 } else { 576 v2 = append(v2, v) 577 params[k] = v2 578 } 579 } 580 logger.Debug("doTagQuery", 581 zap.Any("query", queryDecoded), 582 zap.Bool("isTagName", isTagName), 583 zap.Any("params", params), 584 zap.Int64("limit", limit), 585 ) 586 587 // default target - all Graphite metrics 588 target = `__name=~.*` 589 // but use joined expr value if present 590 if len(params["expr"]) > 0 { 591 target = strings.Join(params["expr"], ",") 592 } 593 tagPrefix := "" 594 valuePrefix := "" 595 if v, ok := params["tagPrefix"]; ok { 596 tagPrefix = v[0] 597 } 598 if v, ok := params["valuePrefix"]; ok { 599 valuePrefix = v[0] 600 } 601 if isTagName { 602 logger = logger.With(zap.String("type", "tagName")) 603 } else { 604 logger = logger.With(zap.String("type", "tagValues")) 605 // get tag category from tag parameter 606 // if it's present and we're looking for values instead categories 607 if tagParam, ok := params["tag"]; ok { 608 tagCategory = tagParam[0] 609 } else { 610 return []string{}, types.ErrNoTagSpecified 611 } 612 } 613 614 logger.Debug("sending GraphiteFindTags request to irondb", 615 zap.Int64("accountID", c.accountID), 616 zap.String("prefix", c.graphitePrefix), 617 zap.String("target", target), 618 ) 619 tagResult, err := c.client.GraphiteFindTags(c.accountID, c.graphitePrefix, target, nil) 620 if err != nil { 621 return []string{}, merry.New("request returned an error").WithValue("error", err) 622 } 623 logger.Debug("got GraphiteFindTags result from irondb", 624 zap.String("target", target), 625 zap.Any("tagResult", tagResult), 626 ) 627 628 // struct for dedup 629 seen := make(map[string]struct{}) 630 for _, metric := range tagResult { 631 tagList := strings.Split(metric.Name, ";") 632 // skipping first element (metric name) 633 for i := 1; i < len(tagList); i++ { 634 // tags[0] is tag category and tags[1] is tag value 635 tags := strings.SplitN(tagList[i], "=", 2) 636 r := "" 637 if isTagName { 638 // this is true for empty prefix too 639 if strings.HasPrefix(tags[0], tagPrefix) { 640 r = tags[0] 641 } 642 } else { 643 if tags[0] == tagCategory && strings.HasPrefix(tags[1], valuePrefix) { 644 r = tags[1] 645 } 646 } 647 // if we got something - append to result if unique 648 if len(r) > 0 { 649 if _, ok := seen[r]; ok { 650 continue 651 } 652 seen[r] = struct{}{} 653 result = append(result, r) 654 } 655 } 656 } 657 658 // cut result if needed 659 if limit > 0 && int64(len(result)) > limit { 660 result = result[:limit] 661 } 662 663 return result, nil 664 665 } 666 667 func (c *IronDBGroup) TagNames(ctx context.Context, query string, limit int64) ([]string, merry.Error) { 668 return c.doTagQuery(ctx, true, query, limit) 669 } 670 671 func (c *IronDBGroup) TagValues(ctx context.Context, query string, limit int64) ([]string, merry.Error) { 672 return c.doTagQuery(ctx, false, query, limit) 673 } 674 675 func (c *IronDBGroup) ProbeTLDs(ctx context.Context) ([]string, merry.Error) { 676 // ProbeTLDs is not really needed for IronDB but returning nil causing error 677 // so, let's return empty list 678 return []string{}, nil 679 }