1/* 2** Zabbix 3** Copyright (C) 2001-2021 Zabbix SIA 4** 5** This program is free software; you can redistribute it and/or modify 6** it under the terms of the GNU General Public License as published by 7** the Free Software Foundation; either version 2 of the License, or 8** (at your option) any later version. 9** 10** This program is distributed in the hope that it will be useful, 11** but WITHOUT ANY WARRANTY; without even the implied warranty of 12** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13** GNU General Public License for more details. 14** 15** You should have received a copy of the GNU General Public License 16** along with this program; if not, write to the Free Software 17** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18**/ 19 20package redis 21 22import ( 23 "bufio" 24 "encoding/json" 25 "regexp" 26 "strings" 27 28 "zabbix.com/pkg/zbxerr" 29 30 "github.com/mediocregopher/radix/v3" 31) 32 33type infoSection string 34type infoKey string 35type infoKeySpace map[infoKey]interface{} 36type infoExtKey string 37type infoExtKeySpace map[infoExtKey]string 38type redisInfo map[infoSection]infoKeySpace 39 40var redisSlaveMetricRE = regexp.MustCompile(`^slave\d+`) 41 42// parseRedisInfo parses an output of 'INFO' command. 43// https://redis.io/commands/info 44func parseRedisInfo(info string) (res redisInfo, err error) { 45 var ( 46 section infoSection 47 ) 48 49 scanner := bufio.NewScanner(strings.NewReader(info)) 50 res = make(redisInfo) 51 52 for scanner.Scan() { 53 line := scanner.Text() 54 55 if len(line) == 0 { 56 continue 57 } 58 59 // Names of sections are preceded by '#'. 60 if line[0] == '#' { 61 section = infoSection(line[2:]) 62 if _, ok := res[section]; !ok { 63 res[section] = make(infoKeySpace) 64 } 65 66 continue 67 } 68 69 // Each parameter represented in the 'key:value' format. 70 kv := strings.SplitN(line, ":", 2) 71 if len(kv) != 2 { 72 continue 73 } 74 75 key := infoKey(kv[0]) 76 value := strings.TrimSpace(kv[1]) 77 78 // Followed sections has a bit more complicated format. 79 // E.g: dbXXX: keys=XXX,expires=XXX 80 if section == "Keyspace" || section == "Commandstats" || 81 (section == "Replication" && redisSlaveMetricRE.MatchString(string(key))) { 82 extKeySpace := make(infoExtKeySpace) 83 84 for _, ksParams := range strings.Split(value, ",") { 85 ksParts := strings.Split(ksParams, "=") 86 extKeySpace[infoExtKey(ksParts[0])] = ksParts[1] 87 } 88 89 res[section][key] = extKeySpace 90 91 continue 92 } 93 94 if len(section) == 0 { 95 return nil, zbxerr.ErrorCannotParseResult 96 } 97 98 res[section][key] = value 99 } 100 101 if err = scanner.Err(); err != nil { 102 return nil, err 103 } 104 105 if len(res) == 0 { 106 return nil, zbxerr.ErrorEmptyResult 107 } 108 109 return res, nil 110} 111 112// infoHandler gets an output of 'INFO' command, parses it and returns it in JSON format. 113func infoHandler(conn redisClient, params map[string]string) (interface{}, error) { 114 var res string 115 116 section := infoSection(strings.ToLower(params["Section"])) 117 118 if err := conn.Query(radix.Cmd(&res, "INFO", string(section))); err != nil { 119 return nil, zbxerr.ErrorCannotFetchData.Wrap(err) 120 } 121 122 redisInfo, err := parseRedisInfo(res) 123 if err != nil { 124 return nil, err 125 } 126 127 jsonRes, err := json.Marshal(redisInfo) 128 if err != nil { 129 return nil, zbxerr.ErrorCannotMarshalJSON.Wrap(err) 130 } 131 132 return string(jsonRes), nil 133} 134