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

     1  package aliasByRedis
     2  
     3  import (
     4  	"context"
     5  	"strings"
     6  	"time"
     7  
     8  	"github.com/lomik/zapwriter"
     9  	"github.com/spf13/viper"
    10  	"go.uber.org/zap"
    11  
    12  	"github.com/go-graphite/carbonapi/expr/helper"
    13  	"github.com/go-graphite/carbonapi/expr/interfaces"
    14  	"github.com/go-graphite/carbonapi/expr/types"
    15  	"github.com/go-graphite/carbonapi/pkg/parser"
    16  
    17  	"github.com/gomodule/redigo/redis"
    18  )
    19  
    20  func prepareMetric(metric string) (string, []string) {
    21  	parts := strings.Split(metric, ".")
    22  	return parts[len(parts)-1], parts
    23  }
    24  
    25  func redisGetHash(name, key string, c redis.Conn, timeout time.Duration) (string, error) {
    26  	v, err := redis.DoWithTimeout(c, timeout, "HGET", key, name)
    27  	return redis.String(v, err)
    28  }
    29  
    30  type Database struct {
    31  	Enabled            bool
    32  	MaxIdleConnections *int
    33  	IdleTimeout        *time.Duration
    34  	PingInterval       *time.Duration
    35  	Address            *string
    36  	DatabaseNumber     *int
    37  	Username           *string
    38  	Password           *string
    39  	ConnectTimeout     *time.Duration
    40  	QueryTimeout       *time.Duration
    41  	KeepAliveInterval  *time.Duration
    42  	UseTLS             *bool
    43  	TLSSkipVerify      *bool
    44  }
    45  
    46  type aliasByRedis struct {
    47  	address      string
    48  	dialOptions  []redis.DialOption
    49  	queryTimeout time.Duration
    50  	pool         *redis.Pool
    51  	pingInterval *time.Duration
    52  }
    53  
    54  func GetOrder() interfaces.Order {
    55  	return interfaces.Any
    56  }
    57  
    58  func New(configFile string) []interfaces.FunctionMetadata {
    59  	logger := zapwriter.Logger("functionInit").With(zap.String("function", "aliasByRedis"))
    60  	if configFile == "" {
    61  		logger.Debug("no config file specified",
    62  			zap.String("message", "this function requrires config file to work properly"),
    63  		)
    64  		return nil
    65  	}
    66  	v := viper.New()
    67  	v.SetConfigFile(configFile)
    68  	err := v.ReadInConfig()
    69  	if err != nil {
    70  		logger.Error("failed to read config file",
    71  			zap.Error(err),
    72  		)
    73  		return nil
    74  	}
    75  
    76  	cfg := Database{}
    77  
    78  	err = v.Unmarshal(&cfg)
    79  	if err != nil {
    80  		logger.Error("failed to parse config",
    81  			zap.Error(err),
    82  		)
    83  		return nil
    84  	}
    85  
    86  	logger.Info("will use configuration",
    87  		zap.Any("config", cfg),
    88  	)
    89  
    90  	if !cfg.Enabled {
    91  		logger.Warn("aliasByRedis config found but aliasByRedis is disabled")
    92  		return nil
    93  	}
    94  
    95  	f := &aliasByRedis{
    96  		address:      "127.0.0.1:6379",
    97  		dialOptions:  make([]redis.DialOption, 0),
    98  		queryTimeout: 250 * time.Millisecond,
    99  	}
   100  
   101  	if cfg.Address != nil {
   102  		f.address = *cfg.Address
   103  	}
   104  
   105  	if cfg.DatabaseNumber != nil {
   106  		f.dialOptions = append(f.dialOptions, redis.DialDatabase(*cfg.DatabaseNumber))
   107  	}
   108  
   109  	if cfg.Username != nil {
   110  		f.dialOptions = append(f.dialOptions, redis.DialUsername(*cfg.Username))
   111  	}
   112  
   113  	if cfg.Password != nil {
   114  		f.dialOptions = append(f.dialOptions, redis.DialPassword(*cfg.Password))
   115  	}
   116  
   117  	if cfg.KeepAliveInterval != nil {
   118  		f.dialOptions = append(f.dialOptions, redis.DialKeepAlive(*cfg.KeepAliveInterval))
   119  	}
   120  
   121  	if cfg.ConnectTimeout != nil {
   122  		f.dialOptions = append(f.dialOptions, redis.DialConnectTimeout(*cfg.ConnectTimeout))
   123  	}
   124  
   125  	if cfg.UseTLS != nil {
   126  		f.dialOptions = append(f.dialOptions, redis.DialUseTLS(*cfg.UseTLS))
   127  	}
   128  
   129  	if cfg.TLSSkipVerify != nil {
   130  		f.dialOptions = append(f.dialOptions, redis.DialTLSSkipVerify(*cfg.TLSSkipVerify))
   131  	}
   132  
   133  	f.pingInterval = cfg.PingInterval
   134  
   135  	maxIdle := 1
   136  	if cfg.MaxIdleConnections != nil && *cfg.MaxIdleConnections > 1 {
   137  		maxIdle = *cfg.MaxIdleConnections
   138  	}
   139  
   140  	idleTimeout := 60 * time.Second
   141  	if cfg.IdleTimeout != nil {
   142  		idleTimeout = *cfg.IdleTimeout
   143  	}
   144  
   145  	f.pool = &redis.Pool{
   146  		MaxIdle:     maxIdle,
   147  		IdleTimeout: idleTimeout,
   148  		Dial: func() (redis.Conn, error) {
   149  			return redis.Dial("tcp", f.address, f.dialOptions...)
   150  		},
   151  		TestOnBorrow: func(c redis.Conn, t time.Time) error {
   152  			if f.pingInterval == nil || time.Since(t) < *f.pingInterval {
   153  				return nil
   154  			}
   155  			_, err := c.Do("PING")
   156  			return err
   157  		},
   158  	}
   159  
   160  	res := make([]interfaces.FunctionMetadata, 0)
   161  	for _, n := range []string{"aliasByRedis"} {
   162  		res = append(res, interfaces.FunctionMetadata{Name: n, F: f})
   163  	}
   164  	return res
   165  }
   166  
   167  func (f *aliasByRedis) Do(ctx context.Context, eval interfaces.Evaluator, e parser.Expr, from, until int64, values map[parser.MetricRequest][]*types.MetricData) ([]*types.MetricData, error) {
   168  	args, err := helper.GetSeriesArg(ctx, eval, e.Arg(0), from, until, values)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	redisHashName, err := e.GetStringArg(1)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	keepPath, err := e.GetBoolArgDefault(2, false)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  	_ = keepPath
   183  
   184  	redisConnection, err := f.pool.GetContext(ctx)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	defer redisConnection.Close()
   189  
   190  	results := make([]*types.MetricData, len(args))
   191  
   192  	for i, a := range args {
   193  		var r *types.MetricData
   194  		name, nodes := prepareMetric(a.Tags["name"])
   195  		redisName, err := redisGetHash(name, redisHashName, redisConnection, f.queryTimeout)
   196  		if err == nil {
   197  			if keepPath {
   198  				nodes[len(nodes)-1] = redisName
   199  				r = a.CopyName(strings.Join(nodes, "."))
   200  			} else {
   201  				r = a.CopyName(redisName)
   202  			}
   203  		} else {
   204  			r = a.CopyLink()
   205  		}
   206  		results[i] = r
   207  	}
   208  
   209  	return results, nil
   210  }
   211  
   212  func (f *aliasByRedis) Description() map[string]types.FunctionDescription {
   213  	return map[string]types.FunctionDescription{
   214  		"aliasByHash": {
   215  			Description: "Takes a seriesList, extracts first part of a metric name and use it as a field name for HGET redis query. Key name is specified by argument.\n\n.. code-block:: none\n\n  &target=aliasByRedis(some.metric, \"redis_key_name\")",
   216  			Function:    "aliasByRedis(seriesList, keyName[, keepPath])",
   217  			Group:       "Alias",
   218  			Module:      "graphite.render.functions",
   219  			Name:        "aliasByRedis",
   220  			Params: []types.FunctionParam{
   221  				{
   222  					Name:     "seriesList",
   223  					Required: true,
   224  					Type:     types.SeriesList,
   225  				},
   226  				{
   227  					Name:     "redisKey",
   228  					Required: true,
   229  					Type:     types.String,
   230  				},
   231  				{
   232  					Name: "keepPath",
   233  					Type: types.Boolean,
   234  					Default: &types.Suggestion{
   235  						Value: false,
   236  						Type:  types.SBool,
   237  					},
   238  				},
   239  			},
   240  			NameChange: true, // name changed
   241  			TagsChange: true, // name tag changed
   242  		},
   243  	}
   244  }