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 }