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

     1  package aliasByPostgres
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"fmt"
     7  	"regexp"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/go-graphite/carbonapi/expr/helper"
    12  	"github.com/go-graphite/carbonapi/expr/interfaces"
    13  	"github.com/go-graphite/carbonapi/expr/types"
    14  	"github.com/go-graphite/carbonapi/pkg/parser"
    15  
    16  	_ "github.com/lib/pq" // Needed for proper work of postgresql requests
    17  	"github.com/lomik/zapwriter"
    18  	"github.com/spf13/viper"
    19  	"go.uber.org/zap"
    20  )
    21  
    22  type aliasByPostgres struct {
    23  	Enabled  bool
    24  	Database map[string]Database
    25  }
    26  
    27  // KeyString structure
    28  type KeyString struct {
    29  	VarName     string
    30  	QueryString string
    31  	MatchString string
    32  }
    33  
    34  // Database structure
    35  type Database struct {
    36  	URLDB     string
    37  	Username  string
    38  	Password  string
    39  	NameDB    string
    40  	KeyString map[string]KeyString
    41  }
    42  
    43  type aliasByPostgresConfig struct {
    44  	Enabled  bool
    45  	Database map[string]Database
    46  }
    47  
    48  func (f *aliasByPostgres) SQLConnectDB(databaseName string) (*sql.DB, error) {
    49  	logger := zapwriter.Logger("functionInit").With(zap.String("function", "aliasByPostgres"))
    50  	connectString := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", f.Database[databaseName].Username, f.Database[databaseName].Password, f.Database[databaseName].URLDB, f.Database[databaseName].NameDB)
    51  	logger.Debug(connectString)
    52  	db, err := sql.Open("postgres", connectString)
    53  	if err != nil {
    54  		logger.Error("Error connect to PostgreSQL Database")
    55  		return nil, err
    56  	}
    57  	return db, nil
    58  }
    59  
    60  // SQLQueryDB convenience function to query the database
    61  func (f *aliasByPostgres) SQLQueryDB(query, databaseName string) (res string, err error) {
    62  	var result string
    63  	logger := zapwriter.Logger("functionInit").With(zap.String("function", "aliasByPostgres"))
    64  	db, _ := f.SQLConnectDB(databaseName)
    65  	rows, err := db.Query(query)
    66  	if err != nil {
    67  		logger.Error("Error with query ti database")
    68  	}
    69  	defer func() {
    70  		_ = db.Close()
    71  	}()
    72  	for rows.Next() {
    73  		err := rows.Scan(&result)
    74  		if err != nil {
    75  			logger.Error("Error with scan response")
    76  		}
    77  		logger.Debug(result)
    78  	}
    79  	defer func() {
    80  		_ = rows.Close()
    81  	}()
    82  	return result, nil
    83  }
    84  
    85  // GetOrder - standard function
    86  func GetOrder() interfaces.Order {
    87  	return interfaces.Any
    88  }
    89  
    90  // New - function for parsing config
    91  func New(configFile string) []interfaces.FunctionMetadata {
    92  	logger := zapwriter.Logger("functionInit").With(zap.String("function", "aliasByPostgres"))
    93  	if configFile == "" {
    94  		logger.Debug("no config file specified",
    95  			zap.String("message", "this function requrires config file to work properly"),
    96  		)
    97  		return nil
    98  	}
    99  	v := viper.New()
   100  	v.SetConfigFile(configFile)
   101  	err := v.ReadInConfig()
   102  	if err != nil {
   103  		logger.Error("failed to read config file",
   104  			zap.Error(err),
   105  		)
   106  		return nil
   107  	}
   108  	key := map[string]KeyString{
   109  		"keyString": {
   110  			VarName:     "var",
   111  			QueryString: "select * from database.table where \"name\" =~ /^var0$/",
   112  			MatchString: ".*",
   113  		},
   114  	}
   115  	database := map[string]Database{
   116  		"postgres": {
   117  			URLDB:     "http://localhost:5432",
   118  			Username:  "User",
   119  			Password:  "Password",
   120  			NameDB:    "databaseName",
   121  			KeyString: key,
   122  		},
   123  	}
   124  
   125  	cfg := aliasByPostgresConfig{
   126  		Enabled:  false,
   127  		Database: database,
   128  	}
   129  	err = v.Unmarshal(&cfg)
   130  	if err != nil {
   131  		logger.Error("failed to parse config",
   132  			zap.Error(err),
   133  		)
   134  		return nil
   135  	}
   136  	if !cfg.Enabled {
   137  		logger.Warn("aliasByPostgres config found but aliasByPostgres is disabled")
   138  		return nil
   139  	}
   140  	f := &aliasByPostgres{
   141  		Enabled:  cfg.Enabled,
   142  		Database: cfg.Database,
   143  	}
   144  	res := make([]interfaces.FunctionMetadata, 0)
   145  	for _, n := range []string{"aliasByPostgres"} {
   146  		res = append(res, interfaces.FunctionMetadata{Name: n, F: f})
   147  	}
   148  	return res
   149  }
   150  
   151  func (f *aliasByPostgres) Do(ctx context.Context, eval interfaces.Evaluator, e parser.Expr, from, until int64, values map[parser.MetricRequest][]*types.MetricData) ([]*types.MetricData, error) {
   152  	if e.ArgsLen() < 4 {
   153  		return nil, parser.ErrMissingTimeseries
   154  	}
   155  
   156  	logger := zapwriter.Logger("functionInit").With(zap.String("function", "aliasByPostgres"))
   157  	args, err := helper.GetSeriesArg(ctx, eval, e.Arg(0), from, until, values)
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	fields, err := e.GetIntArgs(3)
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  	if len(fields) == 0 {
   167  		return nil, parser.ErrMissingArgument
   168  	}
   169  
   170  	databaseName, err := e.GetStringArg(1)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  
   175  	keyString, err := e.GetStringArg(2)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	results := make([]*types.MetricData, 0, len(args))
   181  	matchString := regexp.MustCompile(f.Database[databaseName].KeyString[keyString].MatchString)
   182  
   183  	for _, a := range args {
   184  		metric := a.Tags["name"]
   185  		logger.Debug(metric)
   186  		if metric == "" {
   187  			continue
   188  		}
   189  		nodes := strings.Split(metric, ".")
   190  		name := make([]string, 0, len(nodes))
   191  		for _, f := range fields {
   192  			if f < 0 {
   193  				f += len(nodes)
   194  			}
   195  			if f >= len(nodes) || f < 0 {
   196  				continue
   197  			}
   198  			name = append(name, nodes[f])
   199  		}
   200  		tempName := strings.Join(name, ".")
   201  		query := f.Database[databaseName].KeyString[keyString].QueryString
   202  		varName := regexp.MustCompile(f.Database[databaseName].KeyString[keyString].VarName)
   203  		queryFields := len(varName.FindAllString(query, -1))
   204  
   205  		for i := 0; i < queryFields; i++ {
   206  			reg := regexp.MustCompile("(" + f.Database[databaseName].KeyString[keyString].VarName + strings.TrimSpace(strconv.Itoa(i)) + ")")
   207  			query = reg.ReplaceAllString(query, name[i])
   208  		}
   209  
   210  		// TODO: may be batch fetch of aliases
   211  		res, err := f.SQLQueryDB(query, databaseName)
   212  		if err != nil {
   213  			logger.Error("failed query to Postgresql DB", zap.Error(err))
   214  			return nil, err
   215  		}
   216  		for i := range name {
   217  			if i < queryFields {
   218  				name = append(name[:0], name[0+1:]...)
   219  			}
   220  		}
   221  		if len(res) > 0 {
   222  			if matchString.MatchString(res) {
   223  				var newName string
   224  				if len(name) > 0 {
   225  					newName = res + "." + strings.Join(name, ".")
   226  				} else {
   227  					newName = res
   228  				}
   229  				r := a.CopyName(newName)
   230  				results = append(results, r)
   231  			}
   232  		} else {
   233  			r := a.CopyName(tempName)
   234  			results = append(results, r)
   235  		}
   236  	}
   237  	return results, nil
   238  }
   239  
   240  // Description is auto-generated description, based on output of https://github.com/graphite-project/graphite-web
   241  func (f *aliasByPostgres) Description() map[string]types.FunctionDescription {
   242  	return map[string]types.FunctionDescription{
   243  		"aliasByPostgres": {
   244  			Description: "Takes a seriesList and applies an alias derived from database for one or more \"node\"\n portion/s of the target name or tags. Node indices are 0 indexed.\n\n.. code-block:: none\n\n  &target=aliasByPostgres(ganglia.*.cpu.load5,'database','key-string',1)\n\nEach node may be an integer referencing a node in the series name or a string identifying a tag.\n\n.. code-block :: none\n\n aliasByPostgres(\"datacenter\", \"server\", 1)\n\n  # will produce output series like\n  # dc1.server1.load5, dc1.server2.load5, dc1.server1.load10, dc1.server2.load10",
   245  			Function:    "aliasByPostgres(seriesList, *nodes)",
   246  			Group:       "Alias",
   247  			Module:      "graphite.render.functions",
   248  			Name:        "aliasByPostgres",
   249  			Params: []types.FunctionParam{
   250  				{
   251  					Name:     "seriesList",
   252  					Required: true,
   253  					Type:     types.SeriesList,
   254  				},
   255  				{
   256  					Name:     "databaseName",
   257  					Required: true,
   258  					Type:     types.String,
   259  				},
   260  				{
   261  					Name:     "keyString",
   262  					Required: true,
   263  					Type:     types.String,
   264  				},
   265  				{
   266  					Multiple: true,
   267  					Name:     "nodes",
   268  					Required: true,
   269  					Type:     types.NodeOrTag,
   270  				},
   271  			},
   272  			NameChange: true, // name changed
   273  			TagsChange: true, // name tag changed
   274  		},
   275  	}
   276  }