github.com/go-graphite/carbonapi@v0.17.0/zipper/protocols/prometheus/prometheus_group.go (about) 1 package prometheus 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "net/url" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/go-graphite/carbonapi/zipper/protocols/prometheus/helpers" 14 prometheusTypes "github.com/go-graphite/carbonapi/zipper/protocols/prometheus/types" 15 16 "github.com/ansel1/merry" 17 protov3 "github.com/go-graphite/protocol/carbonapi_v3_pb" 18 19 "github.com/go-graphite/carbonapi/limiter" 20 "github.com/go-graphite/carbonapi/zipper/helper" 21 "github.com/go-graphite/carbonapi/zipper/httpHeaders" 22 "github.com/go-graphite/carbonapi/zipper/metadata" 23 "github.com/go-graphite/carbonapi/zipper/types" 24 25 "go.uber.org/zap" 26 ) 27 28 func init() { 29 aliases := []string{"prometheus"} 30 metadata.Metadata.Lock() 31 for _, name := range aliases { 32 metadata.Metadata.SupportedProtocols[name] = struct{}{} 33 metadata.Metadata.ProtocolInits[name] = New 34 metadata.Metadata.ProtocolInitsWithLimiter[name] = NewWithLimiter 35 } 36 defer metadata.Metadata.Unlock() 37 } 38 39 type StartDelay struct { 40 IsSet bool 41 IsDuration bool 42 D time.Duration 43 T int64 44 S string 45 } 46 47 func (s *StartDelay) String() string { 48 if s.IsDuration { 49 return strconv.FormatInt(time.Now().Add(s.D).Unix(), 10) 50 } 51 if s.S == "" { 52 s.S = strconv.FormatInt(s.T, 10) 53 } 54 return s.S 55 } 56 57 // RoundRobin is used to connect to backends inside clientGroups, implements BackendServer interface 58 type PrometheusGroup struct { 59 groupName string 60 servers []string 61 protocol string 62 63 client *http.Client 64 65 limiter limiter.ServerLimiter 66 logger *zap.Logger 67 timeout types.Timeouts 68 maxTries int 69 maxMetricsPerRequest int 70 71 step int64 72 maxPointsPerQuery int64 73 forceMinStepInterval time.Duration 74 75 startDelay StartDelay 76 77 httpQuery *helper.HttpQuery 78 } 79 80 func NewWithLimiter(logger *zap.Logger, config types.BackendV2, tldCacheDisabled, requireSuccessAll bool, limiter limiter.ServerLimiter) (types.BackendServer, merry.Error) { 81 logger = logger.With(zap.String("type", "prometheus"), zap.String("protocol", config.Protocol), zap.String("name", config.GroupName)) 82 83 logger.Warn("support for this backend protocol is experimental, use with caution") 84 httpClient := helper.GetHTTPClient(logger, config) 85 86 step := int64(15) 87 stepI, ok := config.BackendOptions["step"] 88 if ok { 89 stepNew, ok := stepI.(string) 90 if ok { 91 if stepNew[len(stepNew)-1] >= '0' && stepNew[len(stepNew)-1] <= '9' { 92 stepNew += "s" 93 } 94 t, err := time.ParseDuration(stepNew) 95 if err != nil { 96 logger.Fatal("failed to parse option", 97 zap.String("option_name", "step"), 98 zap.String("option_value", stepNew), 99 zap.Error(err), 100 ) 101 } 102 step = int64(t.Seconds()) 103 } else { 104 logger.Fatal("failed to parse step", 105 zap.String("type_parsed", fmt.Sprintf("%T", stepI)), 106 zap.String("type_expected", "string"), 107 ) 108 } 109 } 110 111 maxPointsPerQuery := int64(11000) 112 mppqI, ok := config.BackendOptions["max_points_per_query"] 113 if ok { 114 mppq, ok := mppqI.(int) 115 if !ok { 116 logger.Fatal("failed to parse max_points_per_query", 117 zap.String("type_parsed", fmt.Sprintf("%T", mppqI)), 118 zap.String("type_expected", "int"), 119 ) 120 } 121 122 maxPointsPerQuery = int64(mppq) 123 } 124 125 var forceMinStepInterval time.Duration 126 fmsiI, ok := config.BackendOptions["force_min_step_interval"] 127 if ok { 128 fmsiS, ok := fmsiI.(string) 129 if !ok { 130 logger.Fatal("failed to parse force_min_step_interval", 131 zap.String("type_parsed", fmt.Sprintf("%T", fmsiI)), 132 zap.String("type_expected", "time.Duration"), 133 ) 134 } 135 var err error 136 forceMinStepInterval, err = time.ParseDuration(fmsiS) 137 if err != nil { 138 logger.Fatal("failed to parse force_min_step_interval", 139 zap.String("value_provided", fmsiS), 140 zap.String("type_expected", "time.Duration"), 141 ) 142 } 143 } 144 145 delay := StartDelay{ 146 IsSet: false, 147 IsDuration: false, 148 T: -1, 149 } 150 startI, ok := config.BackendOptions["start"] 151 if ok { 152 delay.IsSet = true 153 startNew, ok := startI.(string) 154 if ok { 155 startNewInt, err := strconv.Atoi(startNew) 156 if err != nil { 157 d, err2 := time.ParseDuration(startNew) 158 if err2 != nil { 159 logger.Fatal("failed to parse option", 160 zap.String("option_name", "start"), 161 zap.String("option_value", startNew), 162 zap.Errors("errors", []error{err, err2}), 163 ) 164 } 165 delay.IsDuration = true 166 delay.D = d 167 } else { 168 delay.T = int64(startNewInt) 169 } 170 } 171 } 172 173 httpQuery := helper.NewHttpQuery(config.GroupName, config.Servers, *config.MaxTries, limiter, httpClient, httpHeaders.ContentTypeCarbonAPIv2PB) 174 175 return NewWithEverythingInitialized(logger, config, tldCacheDisabled, requireSuccessAll, limiter, step, maxPointsPerQuery, forceMinStepInterval, delay, httpQuery, httpClient) 176 } 177 178 func NewWithEverythingInitialized(logger *zap.Logger, config types.BackendV2, tldCacheDisabled, requireSuccessAll bool, limiter limiter.ServerLimiter, step, maxPointsPerQuery int64, forceMinStepInterval time.Duration, delay StartDelay, httpQuery *helper.HttpQuery, httpClient *http.Client) (types.BackendServer, merry.Error) { 179 c := &PrometheusGroup{ 180 groupName: config.GroupName, 181 servers: config.Servers, 182 protocol: config.Protocol, 183 timeout: *config.Timeouts, 184 maxTries: *config.MaxTries, 185 maxMetricsPerRequest: *config.MaxBatchSize, 186 step: step, 187 forceMinStepInterval: forceMinStepInterval, 188 maxPointsPerQuery: maxPointsPerQuery, 189 startDelay: delay, 190 191 client: httpClient, 192 limiter: limiter, 193 logger: logger, 194 195 httpQuery: httpQuery, 196 } 197 return c, nil 198 } 199 200 func New(logger *zap.Logger, config types.BackendV2, tldCacheDisabled, requireSuccessAll bool) (types.BackendServer, merry.Error) { 201 if config.ConcurrencyLimit == nil { 202 return nil, types.ErrConcurrencyLimitNotSet 203 } 204 if len(config.Servers) == 0 { 205 return nil, types.ErrNoServersSpecified 206 } 207 l := limiter.NewServerLimiter([]string{config.GroupName}, *config.ConcurrencyLimit) 208 209 return NewWithLimiter(logger, config, tldCacheDisabled, requireSuccessAll, l) 210 } 211 212 func (c *PrometheusGroup) Children() []types.BackendServer { 213 return []types.BackendServer{c} 214 } 215 216 func (c PrometheusGroup) MaxMetricsPerRequest() int { 217 return c.maxMetricsPerRequest 218 } 219 220 func (c PrometheusGroup) Name() string { 221 return c.groupName 222 } 223 224 func (c PrometheusGroup) Backends() []string { 225 return c.servers 226 } 227 228 func (c *PrometheusGroup) Fetch(ctx context.Context, request *protov3.MultiFetchRequest) (*protov3.MultiFetchResponse, *types.Stats, merry.Error) { 229 logger := c.logger.With(zap.String("type", "fetch"), zap.String("request", request.String())) 230 stats := &types.Stats{} 231 rewrite, _ := url.Parse("http://127.0.0.1/api/v1/query_range") 232 233 pathExprToTargets := make(map[string][]string) 234 for _, m := range request.Metrics { 235 targets := pathExprToTargets[m.PathExpression] 236 pathExprToTargets[m.PathExpression] = append(targets, m.Name) 237 } 238 239 var r protov3.MultiFetchResponse 240 var e merry.Error 241 242 start := request.Metrics[0].StartTime 243 stop := request.Metrics[0].StopTime 244 245 maxPointsPerQuery := c.maxPointsPerQuery 246 if len(request.Metrics) > 0 && request.Metrics[0].MaxDataPoints != 0 { 247 maxPointsPerQuery = request.Metrics[0].MaxDataPoints 248 } 249 step := helpers.AdjustStep(start, stop, maxPointsPerQuery, c.step, c.forceMinStepInterval) 250 251 stepStr := strconv.FormatInt(step, 10) 252 for pathExpr, targets := range pathExprToTargets { 253 for _, target := range targets { 254 logger.Debug("got some target to query", 255 zap.Any("pathExpr", pathExpr), 256 zap.Any("target", target), 257 ) 258 // rewrite metric for Tag 259 // Make local copy 260 stepLocalStr := stepStr 261 if strings.HasPrefix(target, "seriesByTag") { 262 stepLocalStr, target = helpers.SeriesByTagToPromQL(stepLocalStr, target) 263 } else { 264 reQuery := helpers.ConvertGraphiteTargetToPromQL(target) 265 target = fmt.Sprintf("{__name__=~%q}", reQuery) 266 } 267 if stepLocalStr[len(stepLocalStr)-1] >= '0' && stepLocalStr[len(stepLocalStr)-1] <= '9' { 268 stepLocalStr += "s" 269 } 270 t, err := time.ParseDuration(stepLocalStr) 271 if err != nil { 272 stats.RenderErrors++ 273 logger.Debug("failed to parse step", 274 zap.String("step", stepLocalStr), 275 zap.Error(err), 276 ) 277 if e == nil { 278 e = merry.Wrap(err) 279 } 280 continue 281 } 282 stepLocal := int64(t.Seconds()) 283 /* 284 newStep, err3 := strToStep(stepStr) 285 if err3 == nil { 286 step = newStep 287 } 288 */ 289 logger.Debug("will do query", 290 zap.String("query", target), 291 zap.Int64("start", start), 292 zap.Int64("stop", stop), 293 zap.String("step", stepLocalStr), 294 ) 295 v := url.Values{ 296 "query": []string{target}, 297 "start": []string{strconv.Itoa(int(start))}, 298 "end": []string{strconv.Itoa(int(stop))}, 299 "step": []string{stepLocalStr}, 300 } 301 302 rewrite.RawQuery = v.Encode() 303 stats.RenderRequests++ 304 res, err2 := c.httpQuery.DoQuery(ctx, logger, rewrite.RequestURI(), nil) 305 if err2 != nil { 306 stats.RenderErrors++ 307 if merry.Is(err, types.ErrTimeoutExceeded) { 308 stats.Timeouts++ 309 stats.RenderTimeouts++ 310 } 311 if e == nil { 312 e = err2 313 } else { 314 e = e.WithCause(err2) 315 } 316 continue 317 } 318 319 var response prometheusTypes.HTTPResponse 320 err = json.Unmarshal(res.Response, &response) 321 if err != nil { 322 stats.RenderErrors++ 323 c.logger.Debug("failed to unmarshal response", 324 zap.Error(err), 325 ) 326 if e == nil { 327 e = err2 328 } else { 329 e = e.WithCause(err2) 330 } 331 continue 332 } 333 334 if response.Status != "success" { 335 stats.RenderErrors++ 336 if e == nil { 337 e = types.ErrFailedToFetch.WithMessage(response.Status).WithValue("query", target).WithValue("status", response.Status) 338 } else { 339 e = e.WithCause(err2).WithValue("query", target).WithValue("status", response.Status) 340 } 341 continue 342 } 343 344 for _, m := range response.Data.Result { 345 // We always should trust backend's response (to mimic behavior of graphite for grahpite native protoocols) 346 // See https://github.com/go-graphite/carbonapi/issues/504 and https://github.com/go-graphite/carbonapi/issues/514 347 realStart := start 348 realStop := stop 349 if len(m.Values) > 0 { 350 realStart = int64(m.Values[0].Timestamp) 351 realStop = int64(m.Values[len(m.Values)-1].Timestamp) 352 } 353 alignedValues := helpers.AlignValues(realStart, realStop, stepLocal, m.Values) 354 355 r.Metrics = append(r.Metrics, protov3.FetchResponse{ 356 Name: helpers.PromMetricToGraphite(m.Metric), 357 PathExpression: pathExpr, 358 ConsolidationFunc: "Average", 359 StartTime: realStart, 360 StopTime: realStop, 361 StepTime: stepLocal, 362 Values: alignedValues, 363 XFilesFactor: 0.0, 364 }) 365 } 366 } 367 } 368 369 if e != nil { 370 stats.FailedServers = []string{c.groupName} 371 logger.Error("errors occurred while getting results", 372 zap.Any("errors", e), 373 ) 374 return &r, stats, e 375 } 376 return &r, stats, nil 377 } 378 379 func (c *PrometheusGroup) Find(ctx context.Context, request *protov3.MultiGlobRequest) (*protov3.MultiGlobResponse, *types.Stats, merry.Error) { 380 logger := c.logger.With(zap.String("type", "find"), zap.Strings("request", request.Metrics)) 381 stats := &types.Stats{} 382 rewrite, _ := url.Parse("http://127.0.0.1/api/v1/series") 383 384 r := protov3.MultiGlobResponse{ 385 Metrics: make([]protov3.GlobResponse, 0), 386 } 387 var e merry.Error 388 uniqueMetrics := make(map[string]bool) 389 for _, query := range request.Metrics { 390 // Convert query to Prometheus-compatible regex 391 if !strings.HasSuffix(query, "*") { 392 query = query + "*" 393 } 394 395 reQuery := helpers.ConvertGraphiteTargetToPromQL(query) 396 matchQuery := fmt.Sprintf("{__name__=~%q}", reQuery) 397 v := url.Values{ 398 "match[]": []string{matchQuery}, 399 } 400 401 if c.startDelay.IsSet { 402 v.Add("start", c.startDelay.String()) 403 } 404 405 rewrite.RawQuery = v.Encode() 406 stats.FindRequests += 1 407 res, err := c.httpQuery.DoQuery(ctx, logger, rewrite.RequestURI(), nil) 408 if err != nil { 409 stats.FindErrors += 1 410 if merry.Is(err, types.ErrTimeoutExceeded) { 411 stats.Timeouts += 1 412 stats.FindTimeouts += 1 413 } 414 if e == nil { 415 e = err 416 } else { 417 e = e.WithCause(err) 418 } 419 continue 420 } 421 422 var pr prometheusTypes.PrometheusFindResponse 423 424 err2 := json.Unmarshal(res.Response, &pr) 425 if err2 != nil { 426 stats.FindErrors += 1 427 if e == nil { 428 e = err 429 } else { 430 e = e.WithCause(err) 431 } 432 continue 433 } 434 435 if pr.Status != "success" { 436 stats.FindErrors += 1 437 if e == nil { 438 e = types.ErrFailedToFetch.WithMessage(pr.Error).WithValue("query", matchQuery).WithValue("error_type", pr.ErrorType).WithValue("error", pr.Error) 439 } else { 440 e = e.WithCause(err2).WithValue("query", matchQuery).WithValue("error_type", pr.ErrorType).WithValue("error", pr.Error) 441 } 442 continue 443 } 444 445 querySplit := strings.Split(query, ".") 446 resp := protov3.GlobResponse{ 447 Name: query, 448 Matches: make([]protov3.GlobMatch, 0), 449 } 450 for _, m := range pr.Data { 451 name, ok := m["__name__"] 452 if !ok { 453 continue 454 } 455 nameSplit := strings.Split(name, ".") 456 457 if len(querySplit) > len(nameSplit) { 458 continue 459 } 460 461 isLeaf := false 462 if len(nameSplit) == len(querySplit) { 463 isLeaf = true 464 } 465 466 uniqueMetrics[strings.Join(nameSplit[:len(querySplit)], ".")] = isLeaf 467 } 468 469 for k, v := range uniqueMetrics { 470 resp.Matches = append(resp.Matches, protov3.GlobMatch{ 471 IsLeaf: v, 472 Path: k, 473 }) 474 r.Metrics = append(r.Metrics, resp) 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 *PrometheusGroup) Info(ctx context.Context, request *protov3.MultiMetricsInfoRequest) (*protov3.ZipperInfoResponse, *types.Stats, merry.Error) { 489 return nil, nil, types.ErrNotSupportedByBackend 490 } 491 492 func (c *PrometheusGroup) List(ctx context.Context) (*protov3.ListMetricsResponse, *types.Stats, merry.Error) { 493 return nil, nil, types.ErrNotImplementedYet 494 } 495 func (c *PrometheusGroup) Stats(ctx context.Context) (*protov3.MetricDetailsResponse, *types.Stats, merry.Error) { 496 return nil, nil, types.ErrNotSupportedByBackend 497 } 498 499 func (c *PrometheusGroup) doSimpleTagQuery(ctx context.Context, logger *zap.Logger, isTagName bool, params map[string][]string, limit int64) ([]string, merry.Error) { 500 var rewrite *url.URL 501 502 if isTagName { 503 logger = logger.With(zap.String("type", "tagName")) 504 rewrite, _ = url.Parse("http://127.0.0.1/api/v1/labels") 505 } else { 506 logger = logger.With(zap.String("type", "tagValues")) 507 if tag, ok := params["Tag"]; ok { 508 rewrite, _ = url.Parse(fmt.Sprintf("http://127.0.0.1/api/v1/label/%s/values", tag[0])) 509 } else { 510 return []string{}, types.ErrNoTagSpecified 511 } 512 } 513 514 var r prometheusTypes.PrometheusTagResponse 515 516 res, e := c.httpQuery.DoQuery(ctx, logger, rewrite.RequestURI(), nil) 517 if e != nil { 518 return []string{}, e 519 } 520 521 err := json.Unmarshal(res.Response, &r) 522 if err != nil { 523 return []string{}, merry.Wrap(err) 524 } 525 526 if r.Status != "success" { 527 return []string{}, merry.New("request returned an error").WithValue("status", r.Status).WithValue("error_type", r.ErrorType).WithValue("error", r.Error) 528 } 529 530 if isTagName { 531 if v, ok := params["tagPrefix"]; ok { 532 data := make([]string, 0) 533 for _, t := range r.Data { 534 if strings.HasPrefix(t, v[0]) { 535 data = append(data, t) 536 } 537 } 538 r.Data = data 539 } 540 } else { 541 if v, ok := params["valuePrefix"]; ok { 542 data := make([]string, 0) 543 for _, t := range r.Data { 544 if strings.HasPrefix(t, v[0]) { 545 data = append(data, t) 546 } 547 } 548 r.Data = data 549 } 550 } 551 552 if limit > 0 && len(r.Data) > int(limit) { 553 r.Data = r.Data[:int(limit)] 554 } 555 556 logger.Debug("got client response", 557 zap.Any("result", r), 558 ) 559 560 return r.Data, nil 561 } 562 563 func (c *PrometheusGroup) doComplexTagQuery(ctx context.Context, isTagName bool, params map[string][]string, limit int64) ([]string, merry.Error) { 564 logger := c.logger 565 var rewrite *url.URL 566 567 if isTagName { 568 logger = logger.With(zap.String("type", "tagName")) 569 } else { 570 logger = logger.With(zap.String("type", "tagValues")) 571 if _, ok := params["Tag"]; !ok { 572 return []string{}, types.ErrNoTagSpecified 573 } 574 } 575 576 matches := make([]string, 0, len(params["expr"])) 577 for _, e := range params["expr"] { 578 name, t := helpers.PromethizeTagValue(e) 579 matches = append(matches, "{"+name+t.OP+"\""+t.TagValue+"\"}") 580 } 581 582 rewrite, _ = url.Parse("http://127.0.0.1/api/v1/series") 583 v := url.Values{ 584 "match[]": matches, 585 } 586 rewrite.RawQuery = v.Encode() 587 588 result := make([]string, 0) 589 var r prometheusTypes.PrometheusFindResponse 590 591 res, e := c.httpQuery.DoQuery(ctx, logger, rewrite.RequestURI(), nil) 592 if e != nil { 593 return []string{}, e 594 } 595 596 err := json.Unmarshal(res.Response, &r) 597 if err != nil { 598 return []string{}, merry.Wrap(err) 599 } 600 601 if r.Status != "success" { 602 return []string{}, merry.New("request returned an error").WithValue("status", r.Status).WithValue("error_type", r.ErrorType).WithValue("error", r.Error) 603 } 604 605 var prefix string 606 if isTagName { 607 if prefixArr, ok := params["tagPrefix"]; ok { 608 prefix = prefixArr[0] 609 } 610 611 uniqueTagNames := make(map[string]struct{}) 612 for _, d := range r.Data { 613 for k := range d { 614 if strings.HasPrefix(k, prefix) { 615 uniqueTagNames[k] = struct{}{} 616 } 617 } 618 } 619 for k := range uniqueTagNames { 620 result = append(result, k) 621 } 622 } else { 623 if prefixArr, ok := params["valuePrefix"]; ok { 624 prefix = prefixArr[0] 625 } 626 627 uniqueTagValues := make(map[string]struct{}) 628 tag := params["Tag"][0] 629 for _, d := range r.Data { 630 if v, ok := d[tag]; ok { 631 if strings.HasPrefix(v, prefix) { 632 uniqueTagValues[v] = struct{}{} 633 } 634 } 635 } 636 for v := range uniqueTagValues { 637 result = append(result, v) 638 } 639 } 640 641 if limit > 0 && len(result) > int(limit) { 642 result = result[:int(limit)] 643 } 644 645 logger.Debug("got client response", 646 zap.Any("result", result), 647 ) 648 649 return result, nil 650 } 651 652 func (c *PrometheusGroup) doTagQuery(ctx context.Context, isTagName bool, query string, limit int64) ([]string, merry.Error) { 653 logger := c.logger 654 params := make(map[string][]string) 655 queryDecoded, _ := url.QueryUnescape(query) 656 querySplit := strings.Split(queryDecoded, "&") 657 for _, qvRaw := range querySplit { 658 idx := strings.Index(qvRaw, "=") 659 //no parameters passed 660 if idx < 1 { 661 continue 662 } 663 k := qvRaw[:idx] 664 v := qvRaw[idx+1:] 665 if v2, ok := params[qvRaw[:idx]]; !ok { 666 params[k] = []string{v} 667 } else { 668 v2 = append(v2, v) 669 params[k] = v2 670 } 671 } 672 logger.Debug("doTagQuery", 673 zap.Any("query", queryDecoded), 674 zap.Any("params", params), 675 ) 676 677 if _, ok := params["expr"]; !ok { 678 return c.doSimpleTagQuery(ctx, logger, isTagName, params, limit) 679 } 680 681 return c.doComplexTagQuery(ctx, isTagName, params, limit) 682 } 683 684 func (c *PrometheusGroup) TagNames(ctx context.Context, query string, limit int64) ([]string, merry.Error) { 685 return c.doTagQuery(ctx, true, query, limit) 686 } 687 688 func (c *PrometheusGroup) TagValues(ctx context.Context, query string, limit int64) ([]string, merry.Error) { 689 return c.doTagQuery(ctx, false, query, limit) 690 } 691 692 func (c *PrometheusGroup) ProbeTLDs(ctx context.Context) ([]string, merry.Error) { 693 logger := c.logger.With(zap.String("function", "prober")) 694 req := &protov3.MultiGlobRequest{ 695 Metrics: []string{"*"}, 696 } 697 698 logger.Debug("doing request", 699 zap.Strings("request", req.Metrics), 700 ) 701 702 res, _, err := c.Find(ctx, req) 703 if err != nil { 704 return nil, err 705 } 706 707 var tlds []string 708 for _, m := range res.Metrics { 709 for _, v := range m.Matches { 710 tlds = append(tlds, v.Path) 711 } 712 } 713 714 logger.Debug("will return data", 715 zap.Strings("tlds", tlds), 716 ) 717 718 return tlds, nil 719 }