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  }